367 lines
12 KiB
TypeScript
367 lines
12 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
|
import { Badge } from './ui/badge';
|
|
import { Button } from './ui/button';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
|
import {
|
|
Calendar,
|
|
Clock,
|
|
Users,
|
|
Video,
|
|
FileText,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Filter
|
|
} from 'lucide-react';
|
|
|
|
interface ScheduleEvent {
|
|
id: string;
|
|
title: string;
|
|
type: 'webinar' | 'workshop' | 'assessment' | 'deadline' | 'class';
|
|
date: Date;
|
|
time: string;
|
|
duration?: string;
|
|
programme: string;
|
|
attendees?: number;
|
|
maxAttendees?: number;
|
|
status: 'upcoming' | 'live' | 'completed';
|
|
}
|
|
|
|
interface ProgrammeScheduleProps {
|
|
events?: ScheduleEvent[];
|
|
onEventClick?: (event: ScheduleEvent) => void;
|
|
}
|
|
|
|
const mockEvents: ScheduleEvent[] = [
|
|
{
|
|
id: 'evt-001',
|
|
title: 'Leadership Fundamentals Webinar',
|
|
type: 'webinar',
|
|
date: new Date('2024-12-28'),
|
|
time: '10:00 AM',
|
|
duration: '90 min',
|
|
programme: 'Leadership Development',
|
|
attendees: 32,
|
|
maxAttendees: 50,
|
|
status: 'upcoming'
|
|
},
|
|
{
|
|
id: 'evt-002',
|
|
title: 'Technical Skills Assessment',
|
|
type: 'assessment',
|
|
date: new Date('2024-12-28'),
|
|
time: '2:00 PM',
|
|
duration: '60 min',
|
|
programme: 'Technical Skills',
|
|
status: 'upcoming'
|
|
},
|
|
{
|
|
id: 'evt-003',
|
|
title: 'Communication Workshop',
|
|
type: 'workshop',
|
|
date: new Date('2024-12-29'),
|
|
time: '11:00 AM',
|
|
duration: '2 hrs',
|
|
programme: 'Communication Excellence',
|
|
attendees: 18,
|
|
maxAttendees: 25,
|
|
status: 'upcoming'
|
|
},
|
|
{
|
|
id: 'evt-004',
|
|
title: 'Project Management Live Class',
|
|
type: 'class',
|
|
date: new Date('2024-12-29'),
|
|
time: '3:30 PM',
|
|
duration: '45 min',
|
|
programme: 'Project Management',
|
|
attendees: 45,
|
|
maxAttendees: 60,
|
|
status: 'live'
|
|
},
|
|
{
|
|
id: 'evt-005',
|
|
title: 'Assignment Submission Due',
|
|
type: 'deadline',
|
|
date: new Date('2024-12-30'),
|
|
time: '11:59 PM',
|
|
programme: 'Leadership Development',
|
|
status: 'upcoming'
|
|
},
|
|
{
|
|
id: 'evt-006',
|
|
title: 'Data Analytics Bootcamp',
|
|
type: 'workshop',
|
|
date: new Date('2024-12-30'),
|
|
time: '9:00 AM',
|
|
duration: '4 hrs',
|
|
programme: 'Technical Skills',
|
|
attendees: 22,
|
|
maxAttendees: 30,
|
|
status: 'upcoming'
|
|
}
|
|
];
|
|
|
|
export const ProgrammeSchedule: React.FC<ProgrammeScheduleProps> = ({
|
|
events = mockEvents,
|
|
onEventClick
|
|
}) => {
|
|
const [selectedProgramme, setSelectedProgramme] = useState('all');
|
|
const [selectedType, setSelectedType] = useState('all');
|
|
const [currentWeekStart, setCurrentWeekStart] = useState(() => {
|
|
const today = new Date();
|
|
const dayOfWeek = today.getDay();
|
|
const mondayDate = new Date(today);
|
|
mondayDate.setDate(today.getDate() - dayOfWeek + 1);
|
|
return mondayDate;
|
|
});
|
|
|
|
// Get unique programmes and types for filters
|
|
const programmes = Array.from(new Set(events.map(e => e.programme)));
|
|
const eventTypes = Array.from(new Set(events.map(e => e.type)));
|
|
|
|
// Generate week days
|
|
const weekDays = Array.from({ length: 7 }, (_, i) => {
|
|
const date = new Date(currentWeekStart);
|
|
date.setDate(currentWeekStart.getDate() + i);
|
|
return date;
|
|
});
|
|
|
|
// Filter and group events by date
|
|
const filteredEvents = events.filter(event => {
|
|
const matchesProgramme = selectedProgramme === 'all' || event.programme === selectedProgramme;
|
|
const matchesType = selectedType === 'all' || event.type === selectedType;
|
|
return matchesProgramme && matchesType;
|
|
});
|
|
|
|
const eventsByDate = weekDays.reduce((acc, date) => {
|
|
const dateKey = date.toDateString();
|
|
acc[dateKey] = filteredEvents.filter(event =>
|
|
event.date.toDateString() === dateKey
|
|
).sort((a, b) => a.time.localeCompare(b.time));
|
|
return acc;
|
|
}, {} as Record<string, ScheduleEvent[]>);
|
|
|
|
const navigateWeek = (direction: 'prev' | 'next') => {
|
|
const newDate = new Date(currentWeekStart);
|
|
newDate.setDate(currentWeekStart.getDate() + (direction === 'next' ? 7 : -7));
|
|
setCurrentWeekStart(newDate);
|
|
};
|
|
|
|
const getEventIcon = (type: ScheduleEvent['type']) => {
|
|
switch (type) {
|
|
case 'webinar':
|
|
return <Video className="h-3 w-3" />;
|
|
case 'workshop':
|
|
return <Users className="h-3 w-3" />;
|
|
case 'assessment':
|
|
return <FileText className="h-3 w-3" />;
|
|
case 'deadline':
|
|
return <Clock className="h-3 w-3" />;
|
|
case 'class':
|
|
return <Calendar className="h-3 w-3" />;
|
|
default:
|
|
return <Calendar className="h-3 w-3" />;
|
|
}
|
|
};
|
|
|
|
const getEventColor = (type: ScheduleEvent['type'], status: ScheduleEvent['status']) => {
|
|
if (status === 'live') return 'bg-status-error text-status-error-foreground';
|
|
if (status === 'completed') return 'bg-muted text-muted-foreground';
|
|
|
|
switch (type) {
|
|
case 'webinar':
|
|
return 'bg-brand-primary text-brand-navy-foreground';
|
|
case 'workshop':
|
|
return 'bg-status-success text-status-success-foreground';
|
|
case 'assessment':
|
|
return 'bg-status-warn text-status-warn-foreground';
|
|
case 'deadline':
|
|
return 'bg-status-error text-status-error-foreground';
|
|
case 'class':
|
|
return 'bg-brand-charcoal text-brand-charcoal-foreground';
|
|
default:
|
|
return 'bg-secondary text-secondary-foreground';
|
|
}
|
|
};
|
|
|
|
const formatDate = (date: Date) => {
|
|
return date.toLocaleDateString('en-AU', {
|
|
weekday: 'short',
|
|
day: '2-digit',
|
|
month: 'short'
|
|
});
|
|
};
|
|
|
|
const isToday = (date: Date) => {
|
|
const today = new Date();
|
|
return date.toDateString() === today.toDateString();
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<CardTitle>Programme Schedule</CardTitle>
|
|
<CardDescription>Weekly view of upcoming classes, webinars, and deadlines</CardDescription>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Select value={selectedProgramme} onValueChange={setSelectedProgramme}>
|
|
<SelectTrigger className="w-[180px]">
|
|
<Filter className="h-4 w-4 mr-2" />
|
|
<SelectValue placeholder="All Programmes" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Programmes</SelectItem>
|
|
{programmes.map(programme => (
|
|
<SelectItem key={programme} value={programme}>
|
|
{programme}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Select value={selectedType} onValueChange={setSelectedType}>
|
|
<SelectTrigger className="w-[140px]">
|
|
<SelectValue placeholder="All Types" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Types</SelectItem>
|
|
{eventTypes.map(type => (
|
|
<SelectItem key={type} value={type}>
|
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* Week Navigation */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigateWeek('prev')}
|
|
className="min-tap-44"
|
|
>
|
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
|
Previous Week
|
|
</Button>
|
|
<div className="text-center">
|
|
<h3 className="font-semibold">
|
|
{weekDays[0].toLocaleDateString('en-AU', { day: '2-digit', month: 'short' })} - {weekDays[6].toLocaleDateString('en-AU', { day: '2-digit', month: 'short', year: 'numeric' })}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">Week View</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigateWeek('next')}
|
|
className="min-tap-44"
|
|
>
|
|
Next Week
|
|
<ChevronRight className="h-4 w-4 ml-1" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Horizontal Weekly Calendar */}
|
|
<div className="grid grid-cols-7 gap-2">
|
|
{weekDays.map((date, dayIndex) => {
|
|
const dateKey = date.toDateString();
|
|
const dayEvents = eventsByDate[dateKey] || [];
|
|
|
|
return (
|
|
<div
|
|
key={dayIndex}
|
|
className={`
|
|
min-h-[120px] p-3 rounded-lg border
|
|
${isToday(date)
|
|
? 'bg-brand-primary/5 border-brand-primary/20'
|
|
: 'bg-card border-chrome-divider'
|
|
}
|
|
`}
|
|
>
|
|
<div className="text-center mb-3">
|
|
<p className={`font-medium ${isToday(date) ? 'text-brand-primary' : ''}`}>
|
|
{formatDate(date)}
|
|
</p>
|
|
{isToday(date) && (
|
|
<Badge variant="secondary" className="text-xs mt-1">Today</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
{dayEvents.slice(0, 3).map((event) => (
|
|
<button
|
|
key={event.id}
|
|
onClick={() => onEventClick?.(event)}
|
|
className={`
|
|
w-full p-2 rounded text-left text-xs transition-all duration-200
|
|
hover:shadow-sm hover:scale-105 min-tap-44
|
|
${getEventColor(event.type, event.status)}
|
|
`}
|
|
>
|
|
<div className="flex items-center gap-1 mb-1">
|
|
{getEventIcon(event.type)}
|
|
<span className="font-medium truncate">{event.title}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span>{event.time}</span>
|
|
{event.status === 'live' && (
|
|
<Badge variant="destructive" className="text-xs px-1 py-0">
|
|
LIVE
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
{event.attendees && (
|
|
<div className="flex items-center gap-1 mt-1 text-xs opacity-80">
|
|
<Users className="h-2 w-2" />
|
|
<span>{event.attendees}/{event.maxAttendees}</span>
|
|
</div>
|
|
)}
|
|
</button>
|
|
))}
|
|
|
|
{dayEvents.length > 3 && (
|
|
<div className="text-center">
|
|
<span className="text-xs text-muted-foreground">
|
|
+{dayEvents.length - 3} more
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{dayEvents.length === 0 && (
|
|
<div className="text-center py-4">
|
|
<span className="text-xs text-muted-foreground">No events</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Legend */}
|
|
<div className="mt-6 pt-4 border-t border-chrome-divider">
|
|
<p className="text-sm font-medium mb-2">Event Types:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{[
|
|
{ type: 'webinar', label: 'Webinar' },
|
|
{ type: 'workshop', label: 'Workshop' },
|
|
{ type: 'class', label: 'Live Class' },
|
|
{ type: 'assessment', label: 'Assessment' },
|
|
{ type: 'deadline', label: 'Deadline' }
|
|
].map(({ type, label }) => (
|
|
<div key={type} className="flex items-center gap-1">
|
|
<div className={`w-3 h-3 rounded ${getEventColor(type as ScheduleEvent['type'], 'upcoming').replace('text-', 'bg-').split(' ')[0]}`} />
|
|
<span className="text-xs text-muted-foreground">{label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}; |