Files
gridpilot.gg/apps/website/components/notifications/ToastNotification.tsx
2026-01-19 01:24:07 +01:00

114 lines
2.9 KiB
TypeScript

'use client';
import { Icon } from '@/ui/Icon';
import { Toast } from '@/ui/Toast';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import {
AlertTriangle,
Bell,
ExternalLink,
Flag,
Shield,
Trophy,
Users,
Vote,
} from 'lucide-react';
import React, { 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,
};
export function ToastNotification({
notification,
onDismiss,
onRead,
onNavigate,
autoHideDuration = 5000,
}: ToastNotificationProps) {
const [isVisible, setIsVisible] = useState(false);
const [isExiting, setIsExiting] = useState(false);
const [progress, setProgress] = useState(100);
const handleDismiss = useCallback(() => {
setIsExiting(true);
setTimeout(() => {
onDismiss(notification);
}, 300);
}, [notification, onDismiss]);
useEffect(() => {
const showTimeout = setTimeout(() => setIsVisible(true), 10);
const startTime = Date.now();
const interval = setInterval(() => {
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, 100 - (elapsed / autoHideDuration) * 100);
setProgress(remaining);
if (remaining === 0) {
clearInterval(interval);
handleDismiss();
}
}, 10);
return () => {
clearTimeout(showTimeout);
clearInterval(interval);
};
}, [autoHideDuration, handleDismiss]);
const handleClick = () => {
onRead(notification);
if (notification.actionUrl && onNavigate) {
onNavigate(notification.actionUrl);
}
handleDismiss();
};
const NotificationIcon = notificationIcons[notification.type] || Bell;
return (
<Toast
title={notification.title ?? 'Notification'}
onClose={handleDismiss}
icon={<Icon icon={NotificationIcon} size={5} intent="primary" />}
isVisible={isVisible}
isExiting={isExiting}
progress={progress}
>
<Text size="xs" variant="low" lineClamp={2}>
{notification.message}
</Text>
{notification.actionUrl && (
<Box marginTop={2}>
<Box onClick={handleClick} style={{ cursor: 'pointer' }}>
<Group gap={1}>
<Text size="xs" weight="medium" variant="primary">
View details
</Text>
<Icon icon={ExternalLink} size={3} intent="primary" />
</Group>
</Box>
</Box>
)}
</Toast>
);
}