Spaces:
Running
Running
| import { useTranslation } from 'react-i18next'; | |
| import { CheckCircle, AlertTriangle, Store, Monitor, X } from 'lucide-react'; | |
| import { cn } from '@/lib/utils'; | |
| interface Notification { | |
| id: string; | |
| type: 'success' | 'warning' | 'info' | 'system'; | |
| titleKey: string; | |
| descriptionKey: string; | |
| timeKey: string; | |
| isRead: boolean; | |
| actions?: { labelKey: string; variant: 'primary' | 'secondary' | 'warning' }[]; | |
| } | |
| const todayNotifications: Notification[] = [ | |
| { | |
| id: '1', | |
| type: 'success', | |
| titleKey: 'notifications.scanComplete', | |
| descriptionKey: 'notifications.scanCompleteDesc', | |
| timeKey: 'notifications.time.2mAgo', | |
| isRead: false, | |
| actions: [{ labelKey: 'notifications.viewResults', variant: 'primary' }], | |
| }, | |
| { | |
| id: '2', | |
| type: 'warning', | |
| titleKey: 'notifications.parsingError', | |
| descriptionKey: 'notifications.parsingErrorDesc', | |
| timeKey: 'notifications.time.15mAgo', | |
| isRead: false, | |
| actions: [ | |
| { labelKey: 'notifications.ignore', variant: 'secondary' }, | |
| { labelKey: 'notifications.fix', variant: 'warning' }, | |
| ], | |
| }, | |
| { | |
| id: '3', | |
| type: 'info', | |
| titleKey: 'notifications.newBranch', | |
| descriptionKey: 'notifications.newBranchDesc', | |
| timeKey: 'notifications.time.1hAgo', | |
| isRead: true, | |
| }, | |
| ]; | |
| const yesterdayNotifications: Notification[] = [ | |
| { | |
| id: '4', | |
| type: 'system', | |
| titleKey: 'notifications.systemUpdate', | |
| descriptionKey: 'notifications.systemUpdateDesc', | |
| timeKey: '10:00 AM', | |
| isRead: true, | |
| }, | |
| ]; | |
| const iconMap = { | |
| success: { icon: CheckCircle, bg: 'bg-emerald-100', text: 'text-emerald-600' }, | |
| warning: { icon: AlertTriangle, bg: 'bg-amber-100', text: 'text-amber-600' }, | |
| info: { icon: Store, bg: 'bg-blue-100', text: 'text-blue-600' }, | |
| system: { icon: Monitor, bg: 'bg-slate-100', text: 'text-slate-600' }, | |
| }; | |
| const actionStyles = { | |
| primary: 'text-emerald-700 bg-emerald-50 hover:bg-emerald-100 border-emerald-100', | |
| secondary: 'text-slate-700 bg-white hover:bg-slate-50 border-slate-200 shadow-sm', | |
| warning: 'text-amber-700 bg-amber-50 hover:bg-amber-100 border-amber-100', | |
| }; | |
| interface NotificationPanelProps { | |
| onClose: () => void; | |
| } | |
| export default function NotificationPanel({ onClose }: NotificationPanelProps) { | |
| const { t } = useTranslation(); | |
| const renderNotification = (notif: Notification) => { | |
| const { icon: Icon, bg, text } = iconMap[notif.type]; | |
| return ( | |
| <div | |
| key={notif.id} | |
| className={cn( | |
| 'group relative flex gap-4 px-5 py-4 hover:bg-white transition-colors cursor-pointer', | |
| notif.isRead ? 'bg-white/60' : 'bg-white' | |
| )} | |
| > | |
| {/* Left accent bar */} | |
| {!notif.isRead ? ( | |
| <div className="absolute left-0 top-0 bottom-0 w-1 bg-primary" /> | |
| ) : ( | |
| <div className="absolute left-0 top-0 bottom-0 w-1 bg-transparent group-hover:bg-slate-200 transition-colors" /> | |
| )} | |
| {/* Icon */} | |
| <div className={cn('mt-1 flex h-9 w-9 flex-none items-center justify-center rounded-full ring-4 ring-white', bg)}> | |
| <Icon className={cn('h-[18px] w-[18px]', text)} /> | |
| </div> | |
| {/* Content */} | |
| <div className="flex-auto"> | |
| <div className="flex items-baseline justify-between gap-x-4"> | |
| <p className="text-sm font-semibold leading-6 text-slate-900">{t(notif.titleKey)}</p> | |
| <p className="flex-none text-xs text-slate-500"> | |
| {notif.timeKey.startsWith('notifications.') ? t(notif.timeKey) : notif.timeKey} | |
| </p> | |
| </div> | |
| <p | |
| className="text-sm leading-5 text-slate-600" | |
| dangerouslySetInnerHTML={{ __html: t(notif.descriptionKey) }} | |
| /> | |
| {notif.actions && ( | |
| <div className="mt-3 flex gap-2"> | |
| {notif.actions.map((action) => ( | |
| <button | |
| key={action.labelKey} | |
| className={cn( | |
| 'text-xs font-medium px-3 py-1.5 rounded-md border transition-colors', | |
| actionStyles[action.variant] | |
| )} | |
| > | |
| {t(action.labelKey)} | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| {/* Unread dot */} | |
| {!notif.isRead && ( | |
| <div className="flex-none self-start"> | |
| <div className="h-2 w-2 rounded-full bg-primary mt-2" /> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <div className="absolute top-4 right-8 z-50 w-full max-w-md animate-in slide-in-from-top-4 fade-in duration-200"> | |
| <div className="flex flex-col rounded-2xl border border-border bg-white shadow-2xl overflow-hidden ring-1 ring-black/5"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between border-b border-border px-5 py-4 bg-white/50 backdrop-blur-sm"> | |
| <div className="flex items-center gap-2"> | |
| <div className="bg-indigo-50 text-indigo-600 rounded-md p-1"> | |
| <svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor"> | |
| <path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z" /> | |
| </svg> | |
| </div> | |
| <div> | |
| <h3 className="text-sm font-bold text-slate-900">{t('notifications.title')}</h3> | |
| <p className="text-[11px] text-slate-500 font-medium">{t('notifications.newCount', { count: 3 })}</p> | |
| </div> | |
| </div> | |
| <div className="flex gap-2"> | |
| <button className="text-[11px] font-semibold text-primary hover:text-primary/80 hover:bg-primary/5 px-2.5 py-1.5 rounded-md transition-colors"> | |
| {t('notifications.markAllRead')} | |
| </button> | |
| <button | |
| onClick={onClose} | |
| className="text-slate-400 hover:text-slate-600 p-1 hover:bg-slate-100 rounded-md transition-colors" | |
| > | |
| <X className="h-[18px] w-[18px]" /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Scrollable body */} | |
| <div className="max-h-[600px] overflow-y-auto bg-slate-50/50"> | |
| {/* Today */} | |
| <div className="px-5 py-2.5 sticky top-0 bg-slate-50/95 backdrop-blur-sm border-b border-border z-10"> | |
| <span className="text-xs font-semibold text-slate-500 uppercase tracking-wider"> | |
| {t('notifications.today')} | |
| </span> | |
| </div> | |
| <div className="divide-y divide-border"> | |
| {todayNotifications.map(renderNotification)} | |
| </div> | |
| {/* Yesterday */} | |
| <div className="px-5 py-2.5 sticky top-0 bg-slate-50/95 backdrop-blur-sm border-b border-border border-t z-10"> | |
| <span className="text-xs font-semibold text-slate-500 uppercase tracking-wider"> | |
| {t('notifications.yesterday')} | |
| </span> | |
| </div> | |
| <div className="divide-y divide-border"> | |
| {yesterdayNotifications.map(renderNotification)} | |
| </div> | |
| {/* View all */} | |
| <div className="p-3 text-center border-t border-border bg-slate-50"> | |
| <button className="text-xs font-semibold text-primary hover:text-primary/80 transition-colors"> | |
| {t('notifications.viewAll')} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |