| import React, { useState, useEffect } from 'react'; | |
| import { motion } from 'framer-motion'; | |
| import { logStore } from '~/lib/stores/logs'; | |
| import { useStore } from '@nanostores/react'; | |
| import { formatDistanceToNow } from 'date-fns'; | |
| import { classNames } from '~/utils/classNames'; | |
| import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; | |
| interface NotificationDetails { | |
| type?: string; | |
| message?: string; | |
| currentVersion?: string; | |
| latestVersion?: string; | |
| branch?: string; | |
| updateUrl?: string; | |
| } | |
| type FilterType = 'all' | 'system' | 'error' | 'warning' | 'update' | 'info' | 'provider' | 'network'; | |
| const NotificationsTab = () => { | |
| const [filter, setFilter] = useState<FilterType>('all'); | |
| const logs = useStore(logStore.logs); | |
| useEffect(() => { | |
| const startTime = performance.now(); | |
| return () => { | |
| const duration = performance.now() - startTime; | |
| logStore.logPerformanceMetric('NotificationsTab', 'mount-duration', duration); | |
| }; | |
| }, []); | |
| const handleClearNotifications = () => { | |
| const count = Object.keys(logs).length; | |
| logStore.logInfo('Cleared notifications', { | |
| type: 'notification_clear', | |
| message: `Cleared ${count} notifications`, | |
| clearedCount: count, | |
| component: 'notifications', | |
| }); | |
| logStore.clearLogs(); | |
| }; | |
| const handleUpdateAction = (updateUrl: string) => { | |
| logStore.logInfo('Update link clicked', { | |
| type: 'update_click', | |
| message: 'User clicked update link', | |
| updateUrl, | |
| component: 'notifications', | |
| }); | |
| window.open(updateUrl, '_blank'); | |
| }; | |
| const handleFilterChange = (newFilter: FilterType) => { | |
| logStore.logInfo('Notification filter changed', { | |
| type: 'filter_change', | |
| message: `Filter changed to ${newFilter}`, | |
| previousFilter: filter, | |
| newFilter, | |
| component: 'notifications', | |
| }); | |
| setFilter(newFilter); | |
| }; | |
| const filteredLogs = Object.values(logs) | |
| .filter((log) => { | |
| if (filter === 'all') { | |
| return true; | |
| } | |
| if (filter === 'update') { | |
| return log.details?.type === 'update'; | |
| } | |
| if (filter === 'system') { | |
| return log.category === 'system'; | |
| } | |
| if (filter === 'provider') { | |
| return log.category === 'provider'; | |
| } | |
| if (filter === 'network') { | |
| return log.category === 'network'; | |
| } | |
| return log.level === filter; | |
| }) | |
| .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); | |
| const getNotificationStyle = (level: string, type?: string) => { | |
| if (type === 'update') { | |
| return { | |
| icon: 'i-ph:arrow-circle-up', | |
| color: 'text-purple-500 dark:text-purple-400', | |
| bg: 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20', | |
| }; | |
| } | |
| switch (level) { | |
| case 'error': | |
| return { | |
| icon: 'i-ph:warning-circle', | |
| color: 'text-red-500 dark:text-red-400', | |
| bg: 'hover:bg-red-500/10 dark:hover:bg-red-500/20', | |
| }; | |
| case 'warning': | |
| return { | |
| icon: 'i-ph:warning', | |
| color: 'text-yellow-500 dark:text-yellow-400', | |
| bg: 'hover:bg-yellow-500/10 dark:hover:bg-yellow-500/20', | |
| }; | |
| case 'info': | |
| return { | |
| icon: 'i-ph:info', | |
| color: 'text-blue-500 dark:text-blue-400', | |
| bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20', | |
| }; | |
| default: | |
| return { | |
| icon: 'i-ph:bell', | |
| color: 'text-gray-500 dark:text-gray-400', | |
| bg: 'hover:bg-gray-500/10 dark:hover:bg-gray-500/20', | |
| }; | |
| } | |
| }; | |
| const renderNotificationDetails = (details: NotificationDetails) => { | |
| if (details.type === 'update') { | |
| return ( | |
| <div className="flex flex-col gap-2"> | |
| <p className="text-sm text-gray-600 dark:text-gray-400">{details.message}</p> | |
| <div className="flex flex-col gap-1 text-xs text-gray-500 dark:text-gray-500"> | |
| <p>Current Version: {details.currentVersion}</p> | |
| <p>Latest Version: {details.latestVersion}</p> | |
| <p>Branch: {details.branch}</p> | |
| </div> | |
| <button | |
| onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)} | |
| className={classNames( | |
| 'mt-2 inline-flex items-center gap-2', | |
| 'rounded-lg px-3 py-1.5', | |
| 'text-sm font-medium', | |
| 'bg-[#FAFAFA] dark:bg-[#0A0A0A]', | |
| 'border border-[#E5E5E5] dark:border-[#1A1A1A]', | |
| 'text-gray-900 dark:text-white', | |
| 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20', | |
| 'transition-all duration-200', | |
| )} | |
| > | |
| <span className="i-ph:git-branch text-lg" /> | |
| View Changes | |
| </button> | |
| </div> | |
| ); | |
| } | |
| return details.message ? <p className="text-sm text-gray-600 dark:text-gray-400">{details.message}</p> : null; | |
| }; | |
| const filterOptions: { id: FilterType; label: string; icon: string; color: string }[] = [ | |
| { id: 'all', label: 'All Notifications', icon: 'i-ph:bell', color: '#9333ea' }, | |
| { id: 'system', label: 'System', icon: 'i-ph:gear', color: '#6b7280' }, | |
| { id: 'update', label: 'Updates', icon: 'i-ph:arrow-circle-up', color: '#9333ea' }, | |
| { id: 'error', label: 'Errors', icon: 'i-ph:warning-circle', color: '#ef4444' }, | |
| { id: 'warning', label: 'Warnings', icon: 'i-ph:warning', color: '#f59e0b' }, | |
| { id: 'info', label: 'Information', icon: 'i-ph:info', color: '#3b82f6' }, | |
| { id: 'provider', label: 'Providers', icon: 'i-ph:robot', color: '#10b981' }, | |
| { id: 'network', label: 'Network', icon: 'i-ph:wifi-high', color: '#6366f1' }, | |
| ]; | |
| return ( | |
| <div className="flex h-full flex-col gap-6"> | |
| <div className="flex items-center justify-between"> | |
| <DropdownMenu.Root> | |
| <DropdownMenu.Trigger asChild> | |
| <button | |
| className={classNames( | |
| 'flex items-center gap-2', | |
| 'rounded-lg px-3 py-1.5', | |
| 'text-sm text-gray-900 dark:text-white', | |
| 'bg-[#FAFAFA] dark:bg-[#0A0A0A]', | |
| 'border border-[#E5E5E5] dark:border-[#1A1A1A]', | |
| 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20', | |
| 'transition-all duration-200', | |
| )} | |
| > | |
| <span | |
| className={classNames('text-lg', filterOptions.find((opt) => opt.id === filter)?.icon || 'i-ph:funnel')} | |
| style={{ color: filterOptions.find((opt) => opt.id === filter)?.color }} | |
| /> | |
| {filterOptions.find((opt) => opt.id === filter)?.label || 'Filter Notifications'} | |
| <span className="i-ph:caret-down text-lg text-gray-500 dark:text-gray-400" /> | |
| </button> | |
| </DropdownMenu.Trigger> | |
| <DropdownMenu.Portal> | |
| <DropdownMenu.Content | |
| className="min-w-[200px] bg-white dark:bg-[#0A0A0A] rounded-lg shadow-lg py-1 z-[250] animate-in fade-in-0 zoom-in-95 border border-[#E5E5E5] dark:border-[#1A1A1A]" | |
| sideOffset={5} | |
| align="start" | |
| side="bottom" | |
| > | |
| {filterOptions.map((option) => ( | |
| <DropdownMenu.Item | |
| key={option.id} | |
| className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors" | |
| onClick={() => handleFilterChange(option.id)} | |
| > | |
| <div className="mr-3 flex h-5 w-5 items-center justify-center"> | |
| <div | |
| className={classNames(option.icon, 'text-lg group-hover:text-purple-500 transition-colors')} | |
| style={{ color: option.color }} | |
| /> | |
| </div> | |
| <span className="group-hover:text-purple-500 transition-colors">{option.label}</span> | |
| </DropdownMenu.Item> | |
| ))} | |
| </DropdownMenu.Content> | |
| </DropdownMenu.Portal> | |
| </DropdownMenu.Root> | |
| <button | |
| onClick={handleClearNotifications} | |
| className={classNames( | |
| 'group flex items-center gap-2', | |
| 'rounded-lg px-3 py-1.5', | |
| 'text-sm text-gray-900 dark:text-white', | |
| 'bg-[#FAFAFA] dark:bg-[#0A0A0A]', | |
| 'border border-[#E5E5E5] dark:border-[#1A1A1A]', | |
| 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20', | |
| 'transition-all duration-200', | |
| )} | |
| > | |
| <span className="i-ph:trash text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" /> | |
| Clear All | |
| </button> | |
| </div> | |
| <div className="flex flex-col gap-4"> | |
| {filteredLogs.length === 0 ? ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className={classNames( | |
| 'flex flex-col items-center justify-center gap-4', | |
| 'rounded-lg p-8 text-center', | |
| 'bg-[#FAFAFA] dark:bg-[#0A0A0A]', | |
| 'border border-[#E5E5E5] dark:border-[#1A1A1A]', | |
| )} | |
| > | |
| <span className="i-ph:bell-slash text-4xl text-gray-400 dark:text-gray-600" /> | |
| <div className="flex flex-col gap-1"> | |
| <h3 className="text-sm font-medium text-gray-900 dark:text-white">No Notifications</h3> | |
| <p className="text-sm text-gray-500 dark:text-gray-400">You're all caught up!</p> | |
| </div> | |
| </motion.div> | |
| ) : ( | |
| filteredLogs.map((log) => { | |
| const style = getNotificationStyle(log.level, log.details?.type); | |
| return ( | |
| <motion.div | |
| key={log.id} | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className={classNames( | |
| 'flex flex-col gap-2', | |
| 'rounded-lg p-4', | |
| 'bg-[#FAFAFA] dark:bg-[#0A0A0A]', | |
| 'border border-[#E5E5E5] dark:border-[#1A1A1A]', | |
| style.bg, | |
| 'transition-all duration-200', | |
| )} | |
| > | |
| <div className="flex items-start justify-between gap-4"> | |
| <div className="flex items-start gap-3"> | |
| <span className={classNames('text-lg', style.icon, style.color)} /> | |
| <div className="flex flex-col gap-1"> | |
| <h3 className="text-sm font-medium text-gray-900 dark:text-white">{log.message}</h3> | |
| {log.details && renderNotificationDetails(log.details as NotificationDetails)} | |
| <p className="text-xs text-gray-500 dark:text-gray-400"> | |
| Category: {log.category} | |
| {log.subCategory ? ` > ${log.subCategory}` : ''} | |
| </p> | |
| </div> | |
| </div> | |
| <time className="shrink-0 text-xs text-gray-500 dark:text-gray-400"> | |
| {formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })} | |
| </time> | |
| </div> | |
| </motion.div> | |
| ); | |
| }) | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default NotificationsTab; | |