feat: add WebSocket real-time updates with toast notifications (#5)
This commit is contained in:
@@ -1444,3 +1444,92 @@ function makeTaskCardDraggable(cardEl, task) {
|
||||
cardEl.classList.remove('dragging');
|
||||
});
|
||||
}
|
||||
|
||||
// ============ WEBSOCKET REAL-TIME UPDATES ============
|
||||
let ws = null;
|
||||
let reconnectAttempts = 0;
|
||||
const MAX_RECONNECT = 5;
|
||||
|
||||
function initWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}`;
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
reconnectAttempts = 0;
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const { type, payload } = JSON.parse(event.data);
|
||||
handleWebSocketMessage(type, payload);
|
||||
} catch (err) {
|
||||
console.error('WebSocket message error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
if (reconnectAttempts < MAX_RECONNECT) {
|
||||
reconnectAttempts++;
|
||||
setTimeout(initWebSocket, 2000 * reconnectAttempts);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error('WebSocket error:', err);
|
||||
};
|
||||
}
|
||||
|
||||
function handleWebSocketMessage(type, payload) {
|
||||
switch (type) {
|
||||
case 'task_created':
|
||||
if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') {
|
||||
loadTasks();
|
||||
showNotification(`New task: ${payload.title}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_updated':
|
||||
if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') {
|
||||
loadTasks();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_deleted':
|
||||
if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') {
|
||||
loadTasks();
|
||||
showNotification('Task deleted');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'usage_updated':
|
||||
if (typeof loadUsage === 'function' && CURRENT_PAGE === 'usage') {
|
||||
loadUsage();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown WebSocket message:', type);
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message) {
|
||||
// Create a simple toast notification
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast-notification';
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('fade-out');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Initialize WebSocket on page load
|
||||
if (typeof CURRENT_PAGE !== 'undefined') {
|
||||
initWebSocket();
|
||||
}
|
||||
|
||||
@@ -1025,3 +1025,41 @@ label {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Toast Notifications */
|
||||
.toast-notification {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.toast-notification.fade-out {
|
||||
animation: fadeOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user