Apostila: Modelos de Notificação para Joomla

Guia completo para implementação de sistemas de notificação

1. Introdução

As notificações são elementos fundamentais da experiência do usuário, fornecendo feedback imediato sobre ações realizadas, alertas importantes e informações relevantes.

Objetivos desta apostila:

  • Apresentar os principais tipos de notificação
  • Fornecer exemplos práticos de implementação
  • Demonstrar integração com Joomla
  • Oferecer código reutilizável

2. Tipos de Notificação

Toast Notifications

Pequenas mensagens temporárias que aparecem no canto da tela.

Ideal para: Confirmações, alertas rápidos

Modal Notifications

Janelas sobrepostas que exigem interação do usuário.

Ideal para: Confirmações críticas, formulários

Banner Notifications

Faixas fixas no topo ou base da página.

Ideal para: Avisos importantes, manutenção

Snackbar

Mensagens na parte inferior com ação opcional.

Ideal para: Feedback com ações, desfazer

3. Toast Notifications

Características:

  • Aparecem temporariamente (3-5 segundos)
  • Não bloqueiam a interface
  • Podem ser empilhadas
  • Suportam diferentes tipos (sucesso, erro, aviso, info)

Código JavaScript:

function showToast(message, type = 'info', duration = 3000) {
    const container = document.getElementById('notification-container');
    const toast = document.createElement('div');
    
    const typeClasses = {
        success: 'bg-green-500 border-green-600',
        error: 'bg-red-500 border-red-600', 
        warning: 'bg-yellow-500 border-yellow-600',
        info: 'bg-blue-500 border-blue-600'
    };
    
    toast.className = `notification-demo slide-in-right ${typeClasses[type]} text-white p-4 rounded-lg shadow-lg border-l-4 max-w-sm`;
    toast.innerHTML = `
        
${message}
`; container.appendChild(toast); setTimeout(() => { if (toast.parentElement) { toast.remove(); } }, duration); }

6. Snackbar

Código JavaScript:

function showSnackbar(message, actionText = null, actionCallback = null, duration = 4000) {
    // Remove snackbar existente
    const existing = document.getElementById('snackbar');
    if (existing) existing.remove();
    
    const snackbar = document.createElement('div');
    snackbar.id = 'snackbar';
    snackbar.className = 'fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gray-800 dark:bg-gray-700 text-white px-6 py-3 rounded-lg shadow-lg slide-in-top z-50';
    
    snackbar.innerHTML = `
        
${message} ${actionText ? `` : ''}
`; document.body.appendChild(snackbar); if (duration > 0) { setTimeout(() => { if (snackbar.parentElement) { snackbar.remove(); } }, duration); } }

7. Implementação Flask + SocketIO (Tempo Real)

7.1 Servidor Flask (app.py)

#!/usr/bin/env python3
# app.py - Servidor de Notificações em Tempo Real

from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit, join_room, leave_room
from datetime import datetime, timedelta
import sqlite3
import uuid
import threading
import time

app = Flask(__name__)
app.config['SECRET_KEY'] = 'joomla_notifications_secret_key'
socketio = SocketIO(app, cors_allowed_origins="*")

# Banco de dados em memória para simplicidade
# Em produção, use MySQL/PostgreSQL
DATABASE = 'notifications.db'

def init_db():
    conn = sqlite3.connect(DATABASE)
    cursor = conn.cursor()
    
    # Tabela de notificações
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS notifications (
            id TEXT PRIMARY KEY,
            message TEXT NOT NULL,
            type TEXT NOT NULL,
            level TEXT NOT NULL,
            duration INTEGER DEFAULT 5000,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            created_by TEXT,
            active BOOLEAN DEFAULT 1
        )
    ''')
    
    # Tabela de visualizações
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS notification_views (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            notification_id TEXT,
            user_id TEXT,
            viewed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (notification_id) REFERENCES notifications (id)
        )
    ''')
    
    # Tabela de usuários conectados
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS connected_users (
            session_id TEXT PRIMARY KEY,
            user_id TEXT,
            username TEXT,
            connected_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    
    conn.commit()
    conn.close()

# Armazenar sessões ativas
active_sessions = {}

@app.route('/')
def admin_panel():
    return '''
    
    
    
        Painel Admin - Notificações Joomla
        
        
    
    
        

Painel de Notificações - Joomla

Enviar Notificação

Usuários Conectados

Últimas Visualizações

''' @socketio.on('connect') def handle_connect(): print(f'Cliente conectado: {request.sid}') @socketio.on('disconnect') def handle_disconnect(): print(f'Cliente desconectado: {request.sid}') # Remover da lista de usuários conectados if request.sid in active_sessions: conn = sqlite3.connect(DATABASE) cursor = conn.cursor() cursor.execute('DELETE FROM connected_users WHERE session_id = ?', (request.sid,)) conn.commit() conn.close() del active_sessions[request.sid] emit_users_update() @socketio.on('user_connect') def handle_user_connect(data): """Usuário normal se conectando""" active_sessions[request.sid] = { 'user_id': data.get('user_id', 'anonymous'), 'username': data.get('username', 'Usuário'), 'is_admin': False } # Salvar no banco conn = sqlite3.connect(DATABASE) cursor = conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO connected_users (session_id, user_id, username) VALUES (?, ?, ?) ''', (request.sid, data.get('user_id'), data.get('username'))) conn.commit() conn.close() emit_users_update() @socketio.on('admin_connect') def handle_admin_connect(data): """Admin se conectando""" active_sessions[request.sid] = { 'user_id': 'admin', 'username': data.get('username', 'Admin'), 'is_admin': True } emit_users_update() emit_view_history() @socketio.on('send_notification') def handle_send_notification(data): """Enviar notificação para todos os usuários""" # Verificar se é admin if request.sid not in active_sessions or not active_sessions[request.sid]['is_admin']: return # Criar notificação notification_id = str(uuid.uuid4()) conn = sqlite3.connect(DATABASE) cursor = conn.cursor() cursor.execute(''' INSERT INTO notifications (id, message, type, level, duration, created_by) VALUES (?, ?, ?, ?, ?, ?) ''', (notification_id, data['message'], data['type'], data['level'], data['duration'], active_sessions[request.sid]['username'])) conn.commit() conn.close() # Enviar para todos os clientes notification_data = { 'id': notification_id, 'type': data['type'], 'level': data['level'], 'message': data['message'], 'duration': data['duration'] } emit('notification', notification_data, broadcast=True) @socketio.on('notification_viewed') def handle_notification_viewed(data): """Registrar visualização da notificação""" if request.sid not in active_sessions: return user_info = active_sessions[request.sid] conn = sqlite3.connect(DATABASE) cursor = conn.cursor() cursor.execute(''' INSERT INTO notification_views (notification_id, user_id) VALUES (?, ?) ''', (data['notification_id'], user_info['user_id'])) conn.commit() conn.close() emit_view_history() def emit_users_update(): """Enviar lista de usuários conectados para admins""" users = [] for session_id, user_info in active_sessions.items(): if not user_info['is_admin']: users.append({ 'session_id': session_id, 'username': user_info['username'], 'user_id': user_info['user_id'] }) # Enviar apenas para admins for session_id, user_info in active_sessions.items(): if user_info['is_admin']: socketio.emit('users_update', users, room=session_id) def emit_view_history(): """Enviar histórico de visualizações para admins""" conn = sqlite3.connect(DATABASE) cursor = conn.cursor() cursor.execute(''' SELECT nv.viewed_at, n.message, cu.username FROM notification_views nv JOIN notifications n ON nv.notification_id = n.id LEFT JOIN connected_users cu ON nv.user_id = cu.user_id ORDER BY nv.viewed_at DESC LIMIT 10 ''') views = [] for row in cursor.fetchall(): views.append({ 'viewed_at': row[0], 'message': row[1], 'username': row[2] or 'Usuário Desconhecido' }) conn.close() # Enviar apenas para admins for session_id, user_info in active_sessions.items(): if user_info['is_admin']: socketio.emit('view_update', views, room=session_id) if __name__ == '__main__': init_db() print("Servidor de Notificações iniciado na porta 5009") print("Acesse: http://localhost:5009 para o painel admin") socketio.run(app, host='0.0.0.0', port=5009, debug=True)

7.2 Cliente JavaScript para Joomla

// notification-client.js - Para incluir nas páginas do Joomla

class JoomlaNotificationClient {
    constructor(serverUrl = 'http://localhost:5009') {
        this.serverUrl = serverUrl;
        this.socket = null;
        this.userId = null;
        this.username = null;
        this.init();
    }
    
    init() {
        // Carregar Socket.IO
        const script = document.createElement('script');
        script.src = 'https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js';
        script.onload = () => this.connect();
        document.head.appendChild(script);
        
        // Obter informações do usuário Joomla
        this.getUserInfo();
        this.createNotificationContainers();
    }
    
    getUserInfo() {
        // Integração com Joomla - obter dados do usuário logado
        if (window.Joomla && window.Joomla.getOptions) {
            const userOptions = window.Joomla.getOptions('user');
            if (userOptions) {
                this.userId = userOptions.id || 'anonymous';
                this.username = userOptions.name || 'Visitante';
            }
        } else {
            // Fallback - usar sessão do browser
            this.userId = this.getOrCreateSessionId();
            this.username = 'Visitante';
        }
    }
    
    getOrCreateSessionId() {
        let sessionId = localStorage.getItem('joomla_notification_session');
        if (!sessionId) {
            sessionId = 'user_' + Math.random().toString(36).substr(2, 9);
            localStorage.setItem('joomla_notification_session', sessionId);
        }
        return sessionId;
    }
    
    createNotificationContainers() {
        // Containers para diferentes tipos de notificação
        const containers = {
            'notification-container': 'fixed top-4 right-4 z-50 space-y-2',
            'modal-container': 'fixed inset-0 z-40 hidden',
            'banner-container': 'fixed top-0 left-0 right-0 z-30',
            'snackbar-container': 'fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50'
        };
        
        Object.entries(containers).forEach(([id, classes]) => {
            if (!document.getElementById(id)) {
                const container = document.createElement('div');
                container.id = id;
                container.className = classes;
                document.body.appendChild(container);
            }
        });
    }
    
    connect() {
        this.socket = io(this.serverUrl);
        
        this.socket.on('connect', () => {
            console.log('Conectado ao servidor de notificações');
            
            // Registrar usuário
            this.socket.emit('user_connect', {
                user_id: this.userId,
                username: this.username
            });
        });
        
        this.socket.on('notification', (data) => {
            this.showNotification(data);
        });
        
        this.socket.on('disconnect', () => {
            console.log('Desconectado do servidor de notificações');
        });
    }
    
    showNotification(data) {
        switch(data.type) {
            case 'toast':
                this.showToast(data);
                break;
            case 'modal':
                this.showModal(data);
                break;
            case 'banner':
                this.showBanner(data);
                break;
            case 'snackbar':
                this.showSnackbar(data);
                break;
        }
        
        // Registrar visualização
        this.markAsViewed(data.id);
    }
    
    showToast(data) {
        const container = document.getElementById('notification-container');
        const toast = document.createElement('div');
        
        const typeClasses = {
            success: 'bg-green-500 border-green-600',
            error: 'bg-red-500 border-red-600',
            warning: 'bg-yellow-500 border-yellow-600',
            info: 'bg-blue-500 border-blue-600'
        };
        
        toast.className = `notification-item slide-in-right ${typeClasses[data.level]} text-white p-4 rounded-lg shadow-lg border-l-4 max-w-sm transform transition-all duration-300`;
        toast.innerHTML = `
            
${data.message}
`; container.appendChild(toast); // Auto-remove após duração especificada setTimeout(() => { if (toast.parentElement) { toast.style.transform = 'translateX(100%)'; toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); } }, data.duration); } showModal(data) { const container = document.getElementById('modal-container'); const typeClasses = { success: 'border-green-500', error: 'border-red-500', warning: 'border-yellow-500', info: 'border-blue-500' }; container.className = 'fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-50 animate-fade-in'; container.innerHTML = `

${data.level}

${data.message}

`; container.classList.remove('hidden'); // Auto-close para modais não críticos if (data.level !== 'error' && data.duration > 0) { setTimeout(() => { container.classList.add('hidden'); }, data.duration); } } showBanner(data) { const container = document.getElementById('banner-container'); const typeClasses = { success: 'bg-green-600', error: 'bg-red-600', warning: 'bg-yellow-600', info: 'bg-blue-600' }; container.innerHTML = `
${data.message}
`; // Ajustar padding do body document.body.style.paddingTop = '60px'; // Auto-remove if (data.duration > 0) { setTimeout(() => { container.innerHTML = ''; document.body.style.paddingTop = '0'; }, data.duration); } } showSnackbar(data) { // Remover snackbar existente const existing = document.getElementById('snackbar-active'); if (existing) existing.remove(); const snackbar = document.createElement('div'); snackbar.id = 'snackbar-active'; snackbar.className = 'bg-gray-800 dark:bg-gray-700 text-white px-6 py-3 rounded-lg shadow-lg animate-slide-up'; snackbar.innerHTML = `
${data.message}
`; document.getElementById('snackbar-container').appendChild(snackbar); // Auto-remove if (data.duration > 0) { setTimeout(() => { if (snackbar.parentElement) { snackbar.style.transform = 'translateY(100%)'; snackbar.style.opacity = '0'; setTimeout(() => snackbar.remove(), 300); } }, data.duration); } } markAsViewed(notificationId) { if (this.socket) { this.socket.emit('notification_viewed', { notification_id: notificationId }); } } } // CSS para animações const style = document.createElement('style'); style.textContent = ` @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideDown { from { transform: translateY(-100%); } to { transform: translateY(0); } } @keyframes slideUp { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes bounceIn { 0% { transform: scale(0.3); opacity: 0; } 50% { transform: scale(1.05); } 70% { transform: scale(0.9); } 100% { transform: scale(1); opacity: 1; } } .slide-in-right { animation: slideInRight 0.3s ease-out; } .animate-slide-down { animation: slideDown 0.3s ease-out; } .animate-slide-up { animation: slideUp 0.3s ease-out; } .animate-fade-in { animation: fadeIn 0.3s ease-out; } .animate-bounce-in { animation: bounceIn 0.5s ease-out; } `; document.head.appendChild(style); // Inicializar cliente automaticamente document.addEventListener('DOMContentLoaded', () => { window.joomlaNotifications = new JoomlaNotificationClient(); });

7.3 Integração no Template Joomla

 $user->id,
    'name' => $user->name,
    'username' => $user->username,
    'groups' => $user->groups
];

$doc->addScriptOptions('user', $userOptions);

// Incluir cliente de notificações
$doc->addScript(Uri::root() . 'templates/' . $this->template . '/js/notification-client.js');
?>




    
    
    


    
    
    
    
    

7.4 Requirements.txt para Flask

Flask==2.3.3
Flask-SocketIO==5.3.6
python-socketio==5.8.0
python-engineio==4.7.1
eventlet==0.33.3

7.5 Script de Inicialização

#!/bin/bash
# start_notification_server.sh

echo "Iniciando Servidor de Notificações Joomla..."

# Instalar dependências se necessário
if [ ! -d "venv" ]; then
    echo "Criando ambiente virtual..."
    python3 -m venv venv
    source venv/bin/activate
    pip install -r requirements.txt
else
    source venv/bin/activate
fi

# Iniciar servidor
echo "Servidor rodando em http://localhost:5009"
python app.py

8. Demonstração Interativa

Teste os diferentes tipos de notificação:

9. Instruções de Instalação

Passo 1: Configurar Servidor Flask

  1. Crie uma pasta notification-server
  2. Salve o arquivo app.py e requirements.txt
  3. Execute: python3 -m venv venv && source venv/bin/activate
  4. Instale dependências: pip install -r requirements.txt
  5. Inicie o servidor: python app.py
  6. Acesse: http://localhost:5009

Passo 2: Integrar no Joomla

  1. Salve o arquivo notification-client.js em /templates/seu_template/js/
  2. Edite o arquivo index.php do seu template
  3. Adicione o código de integração mostrado na seção 7.3
  4. Teste acessando qualquer página do Joomla

Passo 3: Configurar CORS (se necessário)

Se o Joomla estiver em domínio diferente do servidor Flask:

# No app.py, modifique a linha:
socketio = SocketIO(app, cors_allowed_origins="*")

# Para permitir apenas seu domínio:
socketio = SocketIO(app, cors_allowed_origins=["https://seusite.com"])

Passo 4: Produção

  1. Use um servidor web como Nginx como proxy reverso
  2. Configure SSL/HTTPS
  3. Use banco PostgreSQL ou MySQL em vez de SQLite
  4. Configure monitoramento com PM2 ou supervisor
  5. Ajuste as configurações de CORS para seu domínio

10. Recursos Avançados

✨ Recursos Implementados

  • Notificações em tempo real via WebSocket
  • Controle de duração personalizável
  • Rastreamento de usuários conectados
  • Histórico de visualizações
  • 4 tipos de notificação diferentes
  • 4 níveis de prioridade
  • Interface administrativa
  • Integração automática com Joomla
  • Banco de dados SQLite incluído
  • Suporte a modo escuro

🚀 Melhorias Futuras

  • Notificações programadas
  • Segmentação por grupos de usuários
  • Templates personalizáveis
  • Integração com push notifications
  • API REST completa
  • Dashboard de analytics
  • Notificações por email/SMS
  • Sistema de aprovação
  • Multiidioma
  • Cache Redis

Conclusão

Esta apostila apresentou um sistema completo de notificações em tempo real para Joomla usando Flask, WebSocket e SocketIO. O administrador tem controle total sobre o tipo, duração e conteúdo das mensagens, com rastreamento de visualizações em tempo real.

Características principais:

🎯 Para começar a usar:

1. Instale o servidor Flask (Seção 9)

2. Integre no seu template Joomla

3. Acesse http://localhost:5009 para enviar notificações

4. Monitore as visualizações em tempo real!