initial version of the app
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
'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-red-500' />;
|
||||
case 'medium':
|
||||
return <AlertTriangle className='h-4 w-4 text-yellow-500' />;
|
||||
default:
|
||||
return <Info className='h-4 w-4 text-blue-500' />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return 'border-red-200 bg-red-50';
|
||||
case 'medium':
|
||||
return 'border-yellow-200 bg-yellow-50';
|
||||
default:
|
||||
return 'border-blue-200 bg-blue-50';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||
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-gray-900'></div>
|
||||
<p className='ml-2'>Loading announcements...</p>
|
||||
</div>
|
||||
) : announcements.length === 0 ? (
|
||||
<div className='text-center py-8 text-gray-500'>
|
||||
<Bell className='h-12 w-12 mx-auto mb-4 text-gray-300' />
|
||||
<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-gray-700 mb-2'>{announcement.content}</p>
|
||||
<p className='text-xs text-gray-500'>{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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user