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

111 lines
2.9 KiB
TypeScript

'use client';
import { Icon } from '@/ui/Icon';
import { Toast } from '@/ui/Toast';
import { Text } from '@/ui/Text';
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 && (
<div
onClick={handleClick}
style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.25rem', cursor: 'pointer' }}
>
<Text size="xs" weight="medium" variant="primary">
View details
</Text>
<Icon icon={ExternalLink} size={3} intent="primary" />
</div>
)}
</Toast>
);
}