Files
gridpilot.gg/apps/website/components/notifications/ModalNotification.tsx
2026-01-18 22:55:55 +01:00

185 lines
6.1 KiB
TypeScript

'use client';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Modal } from '@/ui/Modal';
import { Text } from '@/ui/Text';
import { NotificationStat, NotificationDeadline } from '@/ui/NotificationContent';
import {
AlertCircle,
AlertTriangle,
Bell,
Clock,
Flag,
Medal,
Shield,
Star,
Trophy,
Users,
Vote,
} from 'lucide-react';
import React 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<string, typeof Bell> = {
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,
};
export function ModalNotification({
notification,
onAction,
onDismiss,
onNavigate,
}: ModalNotificationProps) {
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 data: Record<string, unknown> = 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());
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());
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 (
<Modal
isOpen={true}
onClose={onDismiss ? () => onDismiss(notification) : undefined}
title={notification.title}
description={isRaceNotification ? 'Race Update' : 'Action Required'}
icon={<Icon icon={NotificationIcon} size={5} intent="primary" />}
footer={
<React.Fragment>
{notification.actions && notification.actions.length > 0 ? (
notification.actions.map((action, index) => (
<Button
key={index}
variant={action.type === 'primary' ? 'primary' : 'secondary'}
onClick={() => handleAction(action)}
>
{action.label}
</Button>
))
) : (
isRaceNotification ? (
<>
<Button
variant="secondary"
onClick={() => (onDismiss ? onDismiss(notification) : onAction(notification, 'dismiss'))}
>
Dismiss
</Button>
<Button
variant="primary"
onClick={handlePrimaryAction}
>
{notification.type === 'race_performance_summary' ? 'View Results' : 'View Standings'}
</Button>
</>
) : (
<Button variant="primary" onClick={handlePrimaryAction}>
{notification.actionUrl ? 'View Details' : 'Acknowledge'}
</Button>
)
)}
</React.Fragment>
}
>
<Text leading="relaxed" size="base" variant="med" block>
{notification.message}
</Text>
{isRaceNotification && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '1.5rem' }}>
<NotificationStat
label="POSITION"
value={notification.data?.position === 'DNF' ? 'DNF' : `P${notification.data?.position || '?'}`}
/>
<NotificationStat
label="RATING"
value={`${ratingChange >= 0 ? '+' : ''}${ratingChange}`}
intent={ratingChange >= 0 ? 'success' : 'critical'}
/>
</div>
)}
{hasDeadline && !isRaceNotification && (
<NotificationDeadline
label="Response Required"
deadline={`By ${deadline ? deadline.toLocaleDateString() : ''} ${deadline ? deadline.toLocaleTimeString() : ''}`}
icon={Clock}
/>
)}
{protestId && (
<div style={{ marginTop: '1.5rem', padding: '0.75rem', backgroundColor: 'var(--ui-color-bg-base)', border: '1px solid var(--ui-color-border-default)', borderRadius: 'var(--ui-radius-sm)' }}>
<Text size="xs" variant="low" weight="bold" uppercase block marginBottom={1}>PROTEST ID</Text>
<Text size="xs" variant="med" font="mono" block>{protestId}</Text>
</div>
)}
{notification.requiresResponse && !isRaceNotification && (
<div style={{ marginTop: '1rem', textAlign: 'center' }}>
<Text size="xs" variant="critical" weight="medium">
This action is required to continue
</Text>
</div>
)}
</Modal>
);
}