Spaces:
Running
Running
| class NotificationLog extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.attachShadow({ mode: 'open' }); | |
| this.notifications = []; | |
| this.filteredNotifications = []; | |
| this.isLoading = false; | |
| this.channelFilter = 'all'; | |
| this.statusFilter = 'all'; | |
| } | |
| static get observedAttributes() { | |
| return ['loading', 'channel-filter', 'status-filter']; | |
| } | |
| connectedCallback() { | |
| this.loadNotifications(); | |
| } | |
| async loadNotifications() { | |
| this.isLoading = true; | |
| this.render(); | |
| try { | |
| // Simulated API call | |
| await new Promise(r => setTimeout(r, 600)); | |
| this.notifications = [ | |
| { | |
| id: 1, | |
| channel: 'telegram', | |
| recipient: '-1001234567890', | |
| message: 'Alert: Suspicious activity detected on @elonmusk', | |
| status: 'sent', | |
| error: null, | |
| createdAt: '2024-03-20T10:30:00Z' | |
| }, | |
| { | |
| id: 2, | |
| channel: 'whatsapp', | |
| recipient: '+1234567890', | |
| message: 'Alert: New post from monitored account @sama', | |
| status: 'sent', | |
| error: null, | |
| createdAt: '2024-03-20T09:15:00Z' | |
| }, | |
| { | |
| id: 3, | |
| channel: 'telegram', | |
| recipient: '-1001234567890', | |
| message: 'Alert: Status change on account +1-555-0123', | |
| status: 'failed', | |
| error: 'Chat not found', | |
| createdAt: '2024-03-20T08:45:00Z' | |
| }, | |
| { | |
| id: 4, | |
| channel: 'whatsapp', | |
| recipient: '+1234567890', | |
| message: 'Test message: WhatsApp configuration verified', | |
| status: 'pending', | |
| error: null, | |
| createdAt: '2024-03-20T08:30:00Z' | |
| }, | |
| { | |
| id: 5, | |
| channel: 'telegram', | |
| recipient: '-1001234567890', | |
| message: 'Alert: Multiple failed login attempts detected', | |
| status: 'sent', | |
| error: null, | |
| createdAt: '2024-03-19T22:00:00Z' | |
| }, | |
| { | |
| id: 6, | |
| channel: 'whatsapp', | |
| recipient: '+1234567890', | |
| message: 'Alert: Account @naval posted flagged content', | |
| status: 'sent', | |
| error: null, | |
| createdAt: '2024-03-19T18:30:00Z' | |
| } | |
| ]; | |
| this.isLoading = false; | |
| this.applyFilters(); | |
| } catch (err) { | |
| this.isLoading = false; | |
| this.dispatchEvent(new CustomEvent('toast', { | |
| detail: { type: 'error', title: 'Error', message: 'Failed to load notifications' }, | |
| bubbles: true, | |
| composed: true | |
| })); | |
| this.render(); | |
| } | |
| } | |
| applyFilters() { | |
| this.filteredNotifications = this.notifications.filter(n => { | |
| const matchChannel = this.channelFilter === 'all' || n.channel === this.channelFilter; | |
| const matchStatus = this.statusFilter === 'all' || n.status === this.statusFilter; | |
| return matchChannel && matchStatus; | |
| }); | |
| this.render(); | |
| } | |
| onChannelChange(value) { | |
| this.channelFilter = value; | |
| this.applyFilters(); | |
| } | |
| onStatusChange(value) { | |
| this.statusFilter = value; | |
| this.applyFilters(); | |
| } | |
| formatDate(dateString) { | |
| const date = new Date(dateString); | |
| return date.toLocaleString('en-US', { | |
| month: 'short', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| } | |
| timeAgo(dateString) { | |
| const date = new Date(dateString); | |
| const now = new Date(); | |
| const seconds = Math.floor((now - date) / 1000); | |
| let interval = Math.floor(seconds / 86400); | |
| if (interval > 1) return `${interval} days ago`; | |
| if (interval === 1) return 'Yesterday'; | |
| interval = Math.floor(seconds / 3600); | |
| if (interval > 1) return `${interval} hours ago`; | |
| if (interval === 1) return '1 hour ago'; | |
| interval = Math.floor(seconds / 60); | |
| if (interval > 1) return `${interval} minutes ago`; | |
| return 'Just now'; | |
| } | |
| escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| getStatusConfig(status) { | |
| const configs = { | |
| sent: { bg: 'bg-green-100', text: 'text-green-800', dot: 'bg-green-500' }, | |
| pending: { bg: 'bg-yellow-100', text: 'text-yellow-800', dot: 'bg-yellow-500' }, | |
| failed: { bg: 'bg-red-100', text: 'text-red-800', dot: 'bg-red-500' } | |
| }; | |
| return configs[status] || configs.pending; | |
| } | |
| render() { | |
| const loadingHtml = ` | |
| <div class="py-16"> | |
| <div class="flex flex-col items-center justify-center"> | |
| <div class="w-12 h-12 border-4 border-amber-200 border-t-amber-500 rounded-full animate-spin"></div> | |
| <p class="mt-4 text-gray-600 text-sm">Loading notifications...</p> | |
| </div> | |
| </div> | |
| `; | |
| const emptyHtml = ` | |
| <div class="py-16"> | |
| <div class="flex flex-col items-center justify-center text-center px-4"> | |
| <div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"></path><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path></svg> | |
| </div> | |
| <h3 class="text-lg font-semibold text-gray-900 mb-2">No notifications found</h3> | |
| <p class="text-gray-600 text-sm">No notifications match your filter criteria</p> | |
| </div> | |
| </div> | |
| `; | |
| const tableHtml = this.filteredNotifications.length > 0 ? ` | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Created</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Channel</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Recipient</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Status</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Message</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Error</th> | |
| </tr> | |
| </thead> | |
| <tbody class="bg-white divide-y divide-gray-200"> | |
| ${this.filteredNotifications.map((n, i) => { | |
| const sc = this.getStatusConfig(n.status); | |
| return ` | |
| <tr class="hover:bg-gray-50/80 transition-colors duration-200" style="animation: fadeIn 0.3s ease-out ${i * 0.03}s both;"> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="text-sm text-gray-900">${this.formatDate(n.createdAt)}</div> | |
| <div class="text-xs text-gray-500">${this.timeAgo(n.createdAt)}</div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="inline-flex items-center gap-1.5 text-sm font-medium capitalize"> | |
| ${n.channel === 'telegram' | |
| ? '<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-blue-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>' | |
| : '<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-green-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>'} | |
| ${n.channel} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <code class="text-sm text-gray-700 bg-gray-100 px-2 py-1 rounded">${n.recipient}</code> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${sc.bg} ${sc.text}"> | |
| <span class="w-1.5 h-1.5 ${sc.dot} rounded-full mr-1.5 ${n.status === 'pending' ? 'animate-pulse' : ''}"></span> | |
| ${n.status.charAt(0).toUpperCase() + n.status.slice(1)} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4"> | |
| <p class="text-sm text-gray-700 max-w-xs truncate" title="${this.escapeHtml(n.message)}">${this.escapeHtml(n.message)}</p> | |
| </td> | |
| <td class="px-6 py-4"> | |
| ${n.error ? `<span class="text-sm text-red-600" title="${this.escapeHtml(n.error)}">${this.escapeHtml(n.error.slice(0, 30))}${n.error.length > 30 ? '...' : ''}</span>` : '<span class="text-sm text-gray-400">-</span>'} | |
| </td> | |
| </tr> | |
| `; | |
| }).join('')} | |
| </tbody> | |
| </table> | |
| </div> | |
| ` : emptyHtml; | |
| this.shadowRoot.innerHTML = ` | |
| <style> | |
| @import url('https://cdn.tailwindcss.com'); | |
| :host { | |
| display: block; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .table-row-hover:hover::before { | |
| opacity: 1; | |
| } | |
| </style> | |
| <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden animate-slide-up"> | |
| <div class="px-6 py-4 border-b border-gray-200 bg-gray-50/50"> | |
| <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> | |
| <h2 class="text-lg font-semibold text-gray-900">Notification Log</h2> | |
| <div class="flex items-center gap-3"> | |
| <select id="channel-filter" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 bg-white"> | |
| <option value="all" ${this.channelFilter === 'all' ? 'selected' : ''}>All Channels</option> | |
| <option value="telegram" ${this.channelFilter === 'telegram' ? 'selected' : ''}>Telegram</option> | |
| <option value="whatsapp" ${this.channelFilter === 'whatsapp' ? 'selected' : ''}>WhatsApp</option> | |
| </select> | |
| <select id="status-filter" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 bg-white"> | |
| <option value="all" ${this.statusFilter === 'all' ? 'selected' : ''}>All Statuses</option> | |
| <option value="sent" ${this.statusFilter === 'sent' ? 'selected' : ''}>Sent</option> | |
| <option value="pending" ${this.statusFilter === 'pending' ? 'selected' : ''}>Pending</option> | |
| <option value="failed" ${this.statusFilter === 'failed' ? 'selected' : ''}>Failed</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| ${this.isLoading ? loadingHtml : tableHtml} | |
| </div> | |
| `; | |
| // Attach event listeners | |
| if (!this.isLoading) { | |
| this.shadowRoot.getElementById('channel-filter')?.addEventListener('change', (e) => this.onChannelChange(e.target.value)); | |
| this.shadowRoot.getElementById('status-filter')?.addEventListener('change', (e) => this.onStatusChange(e.target.value)); | |
| } | |
| } | |
| } | |
| customElements.define('notification-log', NotificationLog); |