feat: add WebSocket real-time updates with toast notifications (#5)
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -10,7 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@gar/promisify": {
|
"node_modules/@gar/promisify": {
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1444,3 +1444,92 @@ function makeTaskCardDraggable(cardEl, task) {
|
|||||||
cardEl.classList.remove('dragging');
|
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);
|
background: var(--primary);
|
||||||
color: white;
|
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