'use client'; import { Button } from '@/ui/Button'; import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; import { IconButton } from '@/ui/IconButton'; import { Box } from '@/ui/primitives/Box'; import { Text } from '@/ui/Text'; import { AlertCircle, AlertTriangle, Bell, Clock, Flag, Medal, Shield, Star, Trophy, Users, Vote, X, } from 'lucide-react'; import { useEffect, useState } from 'react'; import type { Notification, NotificationAction } from './notificationTypes'; interface ModalNotificationProps { notification: Notification; onAction: (notification: Notification, actionId?: string) => void; onDismiss?: (notification: Notification) => void; onNavigate?: (href: string) => void; } const notificationIcons: Record = { protest_filed: AlertTriangle, protest_defense_requested: Shield, protest_vote_required: Vote, penalty_issued: AlertTriangle, race_results_posted: Trophy, race_performance_summary: Medal, race_final_results: Star, league_invite: Users, race_reminder: Flag, }; const notificationColors: Record = { protest_filed: { bg: 'bg-critical-red/10', border: 'border-critical-red/50', text: 'text-critical-red', glow: 'shadow-[0_0_60px_rgba(227,92,92,0.3)]', }, protest_defense_requested: { bg: 'bg-warning-amber/10', border: 'border-warning-amber/50', text: 'text-warning-amber', glow: 'shadow-[0_0_60px_rgba(255,197,86,0.3)]', }, protest_vote_required: { bg: 'bg-primary-accent/10', border: 'border-primary-accent/50', text: 'text-primary-accent', glow: 'shadow-[0_0_60px_rgba(25,140,255,0.3)]', }, penalty_issued: { bg: 'bg-critical-red/10', border: 'border-critical-red/50', text: 'text-critical-red', glow: 'shadow-[0_0_60px_rgba(227,92,92,0.3)]', }, race_performance_summary: { bg: 'bg-panel-gray', border: 'border-warning-amber/60', text: 'text-warning-amber', glow: 'shadow-[0_0_80px_rgba(255,197,86,0.2)]', }, race_final_results: { bg: 'bg-panel-gray', border: 'border-primary-accent/60', text: 'text-primary-accent', glow: 'shadow-[0_0_80px_rgba(25,140,255,0.2)]', }, }; export function ModalNotification({ notification, onAction, onDismiss, onNavigate, }: ModalNotificationProps) { const [isVisible, setIsVisible] = useState(false); useEffect(() => { // Animate in const timeout = setTimeout(() => setIsVisible(true), 10); return () => clearTimeout(timeout); }, []); // Handle ESC key to dismiss useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape' && onDismiss && !notification.requiresResponse) { onDismiss(notification); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [notification, onDismiss]); const handleAction = (action: NotificationAction) => { onAction(notification, action.id); if (action.href && onNavigate) { onNavigate(action.href); } }; const handlePrimaryAction = () => { onAction(notification, 'primary'); if (notification.actionUrl && onNavigate) { onNavigate(notification.actionUrl); } }; const NotificationIcon = notificationIcons[notification.type] || AlertCircle; const colors = notificationColors[notification.type] || { bg: 'bg-panel-gray', border: 'border-border-gray', text: 'text-gray-400', glow: 'shadow-card', }; const data: Record = notification.data ?? {}; const getNumber = (value: unknown): number | null => { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string') { const parsed = Number(value); if (Number.isFinite(parsed)) return parsed; } return null; }; const getString = (value: unknown): string | null => { if (typeof value === 'string') return value; if (typeof value === 'number' && Number.isFinite(value)) return String(value); return null; }; const isValidDate = (value: unknown): value is Date => value instanceof Date && !Number.isNaN(value.getTime()); // Check if there's a deadline const deadlineValue = data.deadline; const deadline: Date | null = isValidDate(deadlineValue) ? deadlineValue : typeof deadlineValue === 'string' || typeof deadlineValue === 'number' ? new Date(deadlineValue) : null; const hasDeadline = !!deadline && !Number.isNaN(deadline.getTime()); // Special celebratory styling for race notifications const isRaceNotification = notification.type.startsWith('race_'); const provisionalRatingChange = getNumber(data.provisionalRatingChange) ?? 0; const finalRatingChange = getNumber(data.finalRatingChange) ?? 0; const ratingChange = provisionalRatingChange || finalRatingChange; const protestId = getString(data.protestId); return ( {/* Header */} {isRaceNotification ? 'Race Update' : 'Action Required'} {notification.title} {/* X button for dismissible notifications */} {onDismiss && !notification.requiresResponse && ( onDismiss(notification)} variant="ghost" size="md" color="text-gray-500" title="Dismiss notification" /> )} {/* Body */} {notification.message} {/* Race performance stats */} {isRaceNotification && ( POSITION {notification.data?.position === 'DNF' ? 'DNF' : `P${notification.data?.position || '?'}`} RATING = 0 ? 'text-success-green' : 'text-critical-red'} block> {ratingChange >= 0 ? '+' : ''} {ratingChange} )} {/* Deadline warning */} {hasDeadline && !isRaceNotification && ( Response Required By {deadline ? deadline.toLocaleDateString() : ''} {deadline ? deadline.toLocaleTimeString() : ''} )} {/* Additional context from data */} {protestId && ( PROTEST ID {protestId} )} {/* Actions */} {notification.actions && notification.actions.length > 0 ? ( notification.actions.map((action, index) => ( )) ) : ( isRaceNotification ? ( <> ) : ( ) )} {/* Cannot dismiss warning */} {notification.requiresResponse && !isRaceNotification && ( This action is required to continue )} ); }