Files
tt-booking/components/notifications/announcements.tsx
T

182 lines
5.1 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Bell, X, AlertCircle, Info, AlertTriangle } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
interface Announcement {
id: string;
title: string;
content: string;
priority: 'low' | 'medium' | 'high';
isActive: boolean;
createdAt: string;
}
interface AnnouncementsProps {
isOpen: boolean;
onClose: () => void;
unreadCount: number;
onCountUpdate: (count: number) => void;
}
export function AnnouncementsModal({ isOpen, onClose, unreadCount, onCountUpdate }: AnnouncementsProps) {
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [loading, setLoading] = useState(false);
const { toast } = useToast();
useEffect(() => {
if (isOpen) {
fetchAnnouncements();
}
}, [isOpen]);
const fetchAnnouncements = async () => {
setLoading(true);
try {
const response = await fetch('/api/announcements');
if (response.ok) {
const data = await response.json();
setAnnouncements(data.announcements || []);
onCountUpdate(data.unreadCount || 0);
} else {
toast({
title: 'Error',
description: 'Failed to fetch announcements',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error fetching announcements:', error);
toast({
title: 'Error',
description: 'Failed to fetch announcements',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const getPriorityIcon = (priority: string) => {
switch (priority) {
case 'high':
return <AlertCircle className='h-4 w-4 text-destructive' />;
case 'medium':
return <AlertTriangle className='h-4 w-4 text-amber-500 dark:text-amber-400' />;
default:
return <Info className='h-4 w-4 text-primary' />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high':
return 'border-destructive/20 bg-destructive/5';
case 'medium':
return 'border-amber-500/20 bg-amber-500/5 dark:border-amber-400/20 dark:bg-amber-400/5';
default:
return 'border-primary/20 bg-primary/5';
}
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-IE', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className='sm:max-w-2xl max-h-[80vh] overflow-hidden flex flex-col'>
<DialogHeader>
<DialogTitle className='flex items-center gap-2'>
<Bell className='h-5 w-5' />
Announcements
{unreadCount > 0 && (
<Badge variant='destructive' className='text-xs'>
{unreadCount}
</Badge>
)}
</DialogTitle>
</DialogHeader>
<div className='flex-1 overflow-y-auto'>
{loading ? (
<div className='flex items-center justify-center py-8'>
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-foreground'></div>
<p className='ml-2 text-foreground'>Loading announcements...</p>
</div>
) : announcements.length === 0 ? (
<div className='text-center py-8 text-muted-foreground'>
<Bell className='h-12 w-12 mx-auto mb-4 text-muted-foreground/50' />
<p>No announcements at this time</p>
</div>
) : (
<div className='space-y-4'>
{announcements.map((announcement) => (
<Card
key={announcement.id}
className={`${getPriorityColor(announcement.priority)} border-l-4`}
>
<CardHeader className='pb-2'>
<CardTitle className='flex items-center gap-2 text-base'>
{getPriorityIcon(announcement.priority)}
{announcement.title}
<Badge variant='outline' className='ml-auto text-xs'>
{announcement.priority}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className='pt-0'>
<p className='text-sm text-foreground mb-2'>{announcement.content}</p>
<p className='text-xs text-muted-foreground'>
{formatDate(announcement.createdAt)}
</p>
</CardContent>
</Card>
))}
</div>
)}
</div>
<div className='flex justify-end pt-4 border-t'>
<Button onClick={onClose} variant='outline'>
Close
</Button>
</div>
</DialogContent>
</Dialog>
);
}
// Bell button component for header
interface NotificationBellProps {
unreadCount: number;
onClick: () => void;
}
export function NotificationBell({ unreadCount, onClick }: NotificationBellProps) {
return (
<Button variant='ghost' size='sm' onClick={onClick} className='relative'>
<Bell className='h-4 w-4' />
{unreadCount > 0 && (
<Badge
variant='destructive'
className='absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center text-xs p-0 min-w-[20px]'
>
{unreadCount > 99 ? '99+' : unreadCount}
</Badge>
)}
</Button>
);
}