126 lines
3.7 KiB
TypeScript
126 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Bell, Info, AlertTriangle, AlertCircle } from 'lucide-react';
|
|
|
|
interface Announcement {
|
|
id: string;
|
|
title: string;
|
|
content: string;
|
|
priority: 'low' | 'medium' | 'high';
|
|
isActive: boolean;
|
|
expiresAt?: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export function AnnouncementsList() {
|
|
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetchAnnouncements();
|
|
}, []);
|
|
|
|
const fetchAnnouncements = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch('/api/admin/announcements');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
// Filter to show only active, non-expired announcements
|
|
const activeAnnouncements = data.announcements.filter((announcement: Announcement) => {
|
|
if (!announcement.isActive) return false;
|
|
if (announcement.expiresAt && new Date(announcement.expiresAt) < new Date()) return false;
|
|
return true;
|
|
});
|
|
setAnnouncements(activeAnnouncements);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching announcements:', error);
|
|
// Fallback to default announcements if API fails
|
|
setAnnouncements([
|
|
{
|
|
id: '1',
|
|
title: 'Welcome to Table Tennis Booking!',
|
|
content:
|
|
'Book your favorite court slots up to 7 days in advance. Remember to arrive 5 minutes early for your booking.',
|
|
priority: 'medium',
|
|
isActive: true,
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
]);
|
|
} 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 'bg-destructive/10 text-destructive border-destructive/20 dark:bg-destructive/20';
|
|
case 'medium':
|
|
return 'bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950/50 dark:text-amber-400 dark:border-amber-800/30';
|
|
default:
|
|
return 'bg-primary/10 text-primary border-primary/20 dark:bg-primary/20';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className='flex items-center gap-2'>
|
|
<Bell className='h-5 w-5' />
|
|
Announcements
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className='space-y-4'>
|
|
{announcements
|
|
.filter((a) => a.isActive)
|
|
.map((announcement) => (
|
|
<div key={announcement.id} className='p-4 border rounded-lg bg-card'>
|
|
<div className='flex items-start justify-between gap-3'>
|
|
<div className='flex items-start gap-2 flex-1'>
|
|
{getPriorityIcon(announcement.priority)}
|
|
<div className='space-y-1'>
|
|
<h4 className='font-medium text-sm text-foreground'>
|
|
{announcement.title}
|
|
</h4>
|
|
<p className='text-sm text-muted-foreground'>{announcement.content}</p>
|
|
</div>
|
|
</div>
|
|
<Badge
|
|
variant='outline'
|
|
className={`text-xs ${getPriorityColor(announcement.priority)}`}
|
|
>
|
|
{announcement.priority}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{announcements.filter((a) => a.isActive).length === 0 && (
|
|
<div className='text-center py-8 text-muted-foreground'>
|
|
<Bell className='h-8 w-8 mx-auto mb-2 text-muted-foreground/30' />
|
|
<p>No announcements at this time</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|