Files
gridpilot.gg/apps/website/components/notifications/ToastNotification.tsx
2026-01-18 16:43:32 +01:00

175 lines
5.4 KiB
TypeScript

'use client';
import { Icon } from '@/ui/Icon';
import { IconButton } from '@/ui/IconButton';
import { Box } from '@/ui/primitives/Box';
import { Text } from '@/ui/Text';
import {
AlertTriangle,
Bell,
ExternalLink,
Flag,
Shield,
Trophy,
Users,
Vote,
X,
} from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import type { Notification } from './notificationTypes';
interface ToastNotificationProps {
notification: Notification;
onDismiss: (notification: Notification) => void;
onRead: (notification: Notification) => void;
onNavigate?: (href: string) => void;
autoHideDuration?: number;
}
const notificationIcons: Record<string, typeof Bell> = {
protest_filed: AlertTriangle,
protest_defense_requested: Shield,
protest_vote_required: Vote,
penalty_issued: AlertTriangle,
race_results_posted: Trophy,
league_invite: Users,
race_reminder: Flag,
};
const notificationColors: Record<string, { bg: string; border: string; text: string }> = {
protest_filed: { bg: 'bg-red-500/10', border: 'border-red-500/30', text: 'text-red-400' },
protest_defense_requested: { bg: 'bg-warning-amber/10', border: 'border-warning-amber/30', text: 'text-warning-amber' },
protest_vote_required: { bg: 'bg-primary-blue/10', border: 'border-primary-blue/30', text: 'text-primary-blue' },
penalty_issued: { bg: 'bg-red-500/10', border: 'border-red-500/30', text: 'text-red-400' },
race_results_posted: { bg: 'bg-performance-green/10', border: 'border-performance-green/30', text: 'text-performance-green' },
league_invite: { bg: 'bg-primary-blue/10', border: 'border-primary-blue/30', text: 'text-primary-blue' },
race_reminder: { bg: 'bg-warning-amber/10', border: 'border-warning-amber/30', text: 'text-warning-amber' },
};
export function ToastNotification({
notification,
onDismiss,
onRead,
onNavigate,
autoHideDuration = 5000,
}: ToastNotificationProps) {
const [isVisible, setIsVisible] = useState(false);
const [isExiting, setIsExiting] = useState(false);
const handleDismiss = useCallback(() => {
setIsExiting(true);
setTimeout(() => {
onDismiss(notification);
}, 300);
}, [notification, onDismiss]);
useEffect(() => {
// Animate in
const showTimeout = setTimeout(() => setIsVisible(true), 10);
// Auto-hide
const hideTimeout = setTimeout(() => {
handleDismiss();
}, autoHideDuration);
return () => {
clearTimeout(showTimeout);
clearTimeout(hideTimeout);
};
}, [autoHideDuration, handleDismiss]);
const handleClick = () => {
onRead(notification);
if (notification.actionUrl && onNavigate) {
onNavigate(notification.actionUrl);
}
handleDismiss();
};
const NotificationIcon = notificationIcons[notification.type] || Bell;
const colors = notificationColors[notification.type] || {
bg: 'bg-gray-500/10',
border: 'border-gray-500/30',
text: 'text-gray-400',
};
return (
<Box
transform
transition
translateX={isVisible && !isExiting ? '0' : 'full'}
opacity={isVisible && !isExiting ? 1 : 0}
>
<Box
w="96"
rounded="xl"
border
borderColor={colors.border}
bg={colors.bg}
shadow="2xl"
overflow="hidden"
>
{/* Progress bar */}
<Box h="1" bg="bg-iron-gray/50" overflow="hidden">
<Box
h="full"
bg={colors.text.replace('text-', 'bg-')}
// eslint-disable-next-line gridpilot-rules/component-classification
className="animate-toast-progress"
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ animationDuration: `${autoHideDuration}ms` }}
/>
</Box>
<Box p={4}>
<Box display="flex" gap={3}>
{/* Icon */}
<Box p={2} rounded="lg" bg={colors.bg} flexShrink={0}>
<Icon icon={NotificationIcon} size={5} color={colors.text} />
</Box>
{/* Content */}
<Box flexGrow={1} minWidth="0">
<Box display="flex" alignItems="start" justifyContent="between" gap={2}>
<Text size="sm" weight="semibold" color="text-white" truncate>
{notification.title ?? 'Notification'}
</Text>
<IconButton
icon={X}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
handleDismiss();
}}
variant="ghost"
size="sm"
color="text-gray-400"
/>
</Box>
<Text size="xs" color="text-gray-400" lineClamp={2} mt={1}>
{notification.message}
</Text>
{notification.actionUrl && (
<Box
as="button"
onClick={handleClick}
mt={2}
display="flex"
alignItems="center"
gap={1}
cursor="pointer"
hoverScale
>
<Text size="xs" weight="medium" color={colors.text}>
View details
</Text>
<Icon icon={ExternalLink} size={3} color={colors.text} />
</Box>
)}
</Box>
</Box>
</Box>
</Box>
</Box>
);
}