196 lines
6.1 KiB
TypeScript
196 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 { Box } from '@/ui/Box';
|
|
import { Grid } from '@/ui/Grid';
|
|
import { Surface } from '@/ui/Surface';
|
|
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 && (
|
|
<Box marginTop={6}>
|
|
<Grid cols={2} gap={4}>
|
|
<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'}
|
|
/>
|
|
</Grid>
|
|
</Box>
|
|
)}
|
|
|
|
{hasDeadline && !isRaceNotification && (
|
|
<NotificationDeadline
|
|
label="Response Required"
|
|
deadline={`By ${deadline ? deadline.toLocaleDateString() : ''} ${deadline ? deadline.toLocaleTimeString() : ''}`}
|
|
icon={Clock}
|
|
/>
|
|
)}
|
|
|
|
{protestId && (
|
|
<Surface
|
|
marginTop={6}
|
|
padding={3}
|
|
variant="dark"
|
|
border
|
|
rounded="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>
|
|
</Surface>
|
|
)}
|
|
|
|
{notification.requiresResponse && !isRaceNotification && (
|
|
<Box marginTop={4} textAlign="center">
|
|
<Text size="xs" variant="critical" weight="medium">
|
|
This action is required to continue
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Modal>
|
|
);
|
|
}
|