Guia completo para implementação de sistemas de notificaçã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.
Pequenas mensagens temporárias que aparecem no canto da tela.
Ideal para: Confirmações, alertas rápidos
Janelas sobrepostas que exigem interação do usuário.
Ideal para: Confirmações críticas, formulários
Faixas fixas no topo ou base da página.
Ideal para: Avisos importantes, manutenção
Mensagens na parte inferior com ação opcional.
Ideal para: Feedback com ações, desfazer
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);
}
function showModal(title, message, type = 'info', buttons = []) {
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 fade-in';
container.innerHTML = `
${title}
${message}
${buttons.map(btn => ``).join('')}
`;
container.classList.remove('hidden');
}
function hideModal() {
document.getElementById('modal-container').classList.add('hidden');
}
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);
}
}
#!/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)
// 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 = `
`;
// 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();
});
$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');
?>
Flask==2.3.3
Flask-SocketIO==5.3.6
python-socketio==5.8.0
python-engineio==4.7.1
eventlet==0.33.3
#!/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
notification-serverapp.py e requirements.txtpython3 -m venv venv && source venv/bin/activatepip install -r requirements.txtpython app.pyhttp://localhost:5009notification-client.js em /templates/seu_template/js/index.php do seu templateSe 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"])
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.
🎯 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!