feat: implement reference agenda weekly timetable grid with category pills, live indicator, and star favorites
This commit is contained in:
@@ -6,7 +6,8 @@ import {
|
|||||||
Sparkles, KeyRound, ArrowRight, LogOut,
|
Sparkles, KeyRound, ArrowRight, LogOut,
|
||||||
AlertCircle, User, HelpCircle, Compass, ChevronRight, ExternalLink,
|
AlertCircle, User, HelpCircle, Compass, ChevronRight, ExternalLink,
|
||||||
Award, CheckCircle2, AlertTriangle, FileText, Calculator, TrendingUp,
|
Award, CheckCircle2, AlertTriangle, FileText, Calculator, TrendingUp,
|
||||||
Layers, BookOpen, Check, Mail, Phone, RefreshCw, Edit2, Save, FlaskConical
|
Layers, BookOpen, Check, Mail, Phone, RefreshCw, Edit2, Save, FlaskConical,
|
||||||
|
Star, Search, Filter, Grid, List, X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
@@ -43,6 +44,85 @@ export default function StudentDashboard() {
|
|||||||
const [selectedSemFilter, setSelectedSemFilter] = useState<number>(0);
|
const [selectedSemFilter, setSelectedSemFilter] = useState<number>(0);
|
||||||
const [marksSubTab, setMarksSubTab] = useState<'cat' | 'assignment' | 'lab'>('cat');
|
const [marksSubTab, setMarksSubTab] = useState<'cat' | 'assignment' | 'lab'>('cat');
|
||||||
|
|
||||||
|
// Timetable Customization States (Matching Reference Agenda Layout)
|
||||||
|
const [ttViewMode, setTtViewMode] = useState<'grid' | 'daily'>('grid');
|
||||||
|
const [ttFilterType, setTtFilterType] = useState<'ALL' | 'LAB' | 'THEORY' | 'ELECTIVE' | 'FREE'>('ALL');
|
||||||
|
const [ttSearchQuery, setTtSearchQuery] = useState('');
|
||||||
|
const [starredPeriods, setStarredPeriods] = useState<Record<string, boolean>>(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('rit_starred_periods');
|
||||||
|
return saved ? JSON.parse(saved) : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [currentTimeMinutes, setCurrentTimeMinutes] = useState<number>(() => {
|
||||||
|
const now = new Date();
|
||||||
|
return now.getHours() * 60 + now.getMinutes();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const now = new Date();
|
||||||
|
setCurrentTimeMinutes(now.getHours() * 60 + now.getMinutes());
|
||||||
|
}, 30000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleStarPeriod = (key: string) => {
|
||||||
|
setStarredPeriods(prev => {
|
||||||
|
const next = { ...prev, [key]: !prev[key] };
|
||||||
|
try {
|
||||||
|
localStorage.setItem('rit_starred_periods', JSON.stringify(next));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to save starred period:', err);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Agenda Timetable Helpers & Indicators
|
||||||
|
const dayNamesMap: Record<number, 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday'> = {
|
||||||
|
1: 'monday', 2: 'tuesday', 3: 'wednesday', 4: 'thursday', 5: 'friday'
|
||||||
|
};
|
||||||
|
const todayKey = dayNamesMap[new Date().getDay()] || null;
|
||||||
|
|
||||||
|
const periodTimeRanges: Record<number, { start: number; end: number }> = {
|
||||||
|
1: { start: 525, end: 580 },
|
||||||
|
2: { start: 580, end: 635 },
|
||||||
|
3: { start: 650, end: 705 },
|
||||||
|
4: { start: 705, end: 760 },
|
||||||
|
5: { start: 810, end: 865 },
|
||||||
|
6: { start: 865, end: 920 },
|
||||||
|
7: { start: 920, end: 975 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPeriodLive = (dayKey: string, pNum: number) => {
|
||||||
|
if (!todayKey || todayKey !== dayKey) return false;
|
||||||
|
const r = periodTimeRanges[pNum];
|
||||||
|
if (!r) return false;
|
||||||
|
return currentTimeMinutes >= r.start && currentTimeMinutes < r.end;
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchesFilter = (period: any) => {
|
||||||
|
if (!period) return false;
|
||||||
|
const query = ttSearchQuery.toLowerCase().trim();
|
||||||
|
if (query) {
|
||||||
|
const matchSub = (period.subjectName || '').toLowerCase().includes(query);
|
||||||
|
const matchCode = (period.subjectCode || '').toLowerCase().includes(query);
|
||||||
|
const matchStaff = (period.staffName || '').toLowerCase().includes(query);
|
||||||
|
if (!matchSub && !matchCode && !matchStaff) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ttFilterType === 'LAB') return period.type === 'LAB';
|
||||||
|
if (ttFilterType === 'THEORY') return period.type === 'THEORY' && period.subjectCode !== 'FREE';
|
||||||
|
if (ttFilterType === 'ELECTIVE') return (period.subjectCode || '').includes('V1') || (period.subjectName || '').toLowerCase().includes('elective');
|
||||||
|
if (ttFilterType === 'FREE') return period.subjectCode === 'FREE';
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// Loaded Data States
|
// Loaded Data States
|
||||||
const [profile, setProfile] = useState<StudentProfile | null>(session?.profile || null);
|
const [profile, setProfile] = useState<StudentProfile | null>(session?.profile || null);
|
||||||
const [timetable, setTimetable] = useState<WeeklySchedule | null>(null);
|
const [timetable, setTimetable] = useState<WeeklySchedule | null>(null);
|
||||||
@@ -741,38 +821,89 @@ export default function StudentDashboard() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ─── 1. TIMETABLE TAB ─── */}
|
{/* ─── 1. TIMETABLE TAB (NEXT-GEN AGENDA UI) ─── */}
|
||||||
{activeTab === 'timetable' && (
|
{activeTab === 'timetable' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Day Switcher */}
|
{/* Top Toolbar: Filter Pills & Search & View Toggle */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 bg-white p-4 rounded-2xl border border-[#E5E7EB] shadow-sm">
|
<div className="bg-white p-4 rounded-3xl border border-[#E5E7EB] shadow-sm space-y-4">
|
||||||
<div className="flex items-center gap-2 overflow-x-auto py-1">
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||||
{(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'] as const).map((dayKey) => {
|
|
||||||
const count = timetable?.[dayKey]?.length || 0;
|
{/* Filter Pills (Matching Screenshot Top Bar) */}
|
||||||
const isSelected = selectedDay === dayKey;
|
<div className="flex items-center gap-2 overflow-x-auto py-1 no-scrollbar">
|
||||||
return (
|
{[
|
||||||
|
{ id: 'ALL', label: 'ALL', color: 'bg-slate-900 text-white' },
|
||||||
|
{ id: 'LAB', label: 'LABORATORY', color: 'bg-amber-500 text-white shadow-amber-500/20' },
|
||||||
|
{ id: 'THEORY', label: 'THEORY', color: 'bg-blue-600 text-white shadow-blue-500/20' },
|
||||||
|
{ id: 'ELECTIVE', label: 'ELECTIVES', color: 'bg-emerald-600 text-white shadow-emerald-500/20' },
|
||||||
|
{ id: 'FREE', label: 'FREE PERIODS', color: 'bg-slate-500 text-white' },
|
||||||
|
].map((filter) => {
|
||||||
|
const isSelected = ttFilterType === filter.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={filter.id}
|
||||||
|
onClick={() => setTtFilterType(filter.id as any)}
|
||||||
|
className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-[11px] font-extrabold tracking-wide transition-all cursor-pointer ${
|
||||||
|
isSelected
|
||||||
|
? `${filter.color} shadow-md scale-105`
|
||||||
|
: 'bg-slate-100 text-[#64748B] hover:bg-slate-200 hover:text-[#1E293B]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{filter.label}</span>
|
||||||
|
{isSelected && filter.id !== 'ALL' && (
|
||||||
|
<X className="w-3 h-3 hover:opacity-75" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side: Search & View Switcher */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Search Input */}
|
||||||
|
<div className="relative flex-1 sm:w-64">
|
||||||
|
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search course or faculty..."
|
||||||
|
value={ttSearchQuery}
|
||||||
|
onChange={(e) => setTtSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-9 pr-4 py-2 rounded-2xl bg-slate-50 border border-[#E5E7EB] text-xs text-[#1E293B] focus:outline-none focus:border-[#F97316] transition-all"
|
||||||
|
/>
|
||||||
|
{ttSearchQuery && (
|
||||||
|
<button onClick={() => setTtSearchQuery('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
|
||||||
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Mode Switcher */}
|
||||||
|
<div className="flex items-center bg-slate-100 p-1 rounded-2xl border border-slate-200">
|
||||||
<button
|
<button
|
||||||
key={dayKey}
|
onClick={() => setTtViewMode('grid')}
|
||||||
onClick={() => setSelectedDay(dayKey)}
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer ${
|
||||||
className={`flex items-center gap-2 px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer capitalize ${
|
ttViewMode === 'grid'
|
||||||
isSelected
|
? 'bg-white text-[#F97316] shadow-sm'
|
||||||
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-md shadow-orange-500/20'
|
: 'text-[#64748B] hover:text-[#1E293B]'
|
||||||
: 'bg-slate-100 hover:bg-slate-200 text-[#64748B]'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>{dayKey}</span>
|
<Grid className="w-3.5 h-3.5" />
|
||||||
<span className={`px-1.5 py-0.2 rounded-md text-[10px] font-mono ${isSelected ? 'bg-white/20 text-white' : 'bg-white text-[#64748B] border border-[#E5E7EB]'}`}>
|
<span className="hidden sm:inline">Weekly Grid</span>
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
<button
|
||||||
})}
|
onClick={() => setTtViewMode('daily')}
|
||||||
</div>
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer ${
|
||||||
|
ttViewMode === 'daily'
|
||||||
|
? 'bg-white text-[#F97316] shadow-sm'
|
||||||
|
: 'text-[#64748B] hover:text-[#1E293B]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<List className="w-3.5 h-3.5" />
|
||||||
|
<span className="hidden sm:inline">Daily View</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span className="text-xs text-emerald-600 font-bold flex items-center gap-1.5">
|
</div>
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
|
||||||
Live IMS Timetable
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isInitialLoading ? (
|
{isInitialLoading ? (
|
||||||
@@ -780,72 +911,278 @@ export default function StudentDashboard() {
|
|||||||
<div className="w-8 h-8 border-3 border-[#F97316] border-t-transparent rounded-full animate-spin" />
|
<div className="w-8 h-8 border-3 border-[#F97316] border-t-transparent rounded-full animate-spin" />
|
||||||
<p className="text-xs text-[#64748B]">Fetching timetable from RIT IMS...</p>
|
<p className="text-xs text-[#64748B]">Fetching timetable from RIT IMS...</p>
|
||||||
</div>
|
</div>
|
||||||
) : currentPeriods.length === 0 ? (
|
) : ttViewMode === 'grid' ? (
|
||||||
<div className="bg-white border border-[#E5E7EB] rounded-3xl p-12 text-center space-y-3">
|
/* ─── WEEKLY MATRIX AGENDA VIEW (MATCHING USER SCREENSHOT) ─── */
|
||||||
<div className="w-12 h-12 rounded-2xl bg-orange-50 text-[#F97316] flex items-center justify-center mx-auto">
|
<div className="bg-white rounded-3xl border border-[#E5E7EB] shadow-sm overflow-hidden">
|
||||||
<Calendar className="w-6 h-6" />
|
<div className="overflow-x-auto">
|
||||||
|
<div className="min-w-[900px] p-6 space-y-4">
|
||||||
|
|
||||||
|
{/* Matrix Header Row: Days of Week */}
|
||||||
|
<div className="grid grid-cols-6 gap-3 pb-3 border-b border-slate-200 text-xs font-extrabold tracking-wider">
|
||||||
|
<div className="text-slate-400 uppercase flex items-center justify-center font-mono">TIME</div>
|
||||||
|
{(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'] as const).map((dayKey) => {
|
||||||
|
const isToday = todayKey === dayKey;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={dayKey}
|
||||||
|
className={`py-2 px-3 rounded-2xl text-center uppercase flex items-center justify-between ${
|
||||||
|
isToday
|
||||||
|
? 'bg-orange-500 text-white shadow-md shadow-orange-500/20'
|
||||||
|
: 'bg-slate-50 text-slate-700 border border-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{dayKey}</span>
|
||||||
|
{isToday && <span className="w-2 h-2 rounded-full bg-white animate-pulse" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Matrix Grid Periods (P1 to P7 with Gap Rows) */}
|
||||||
|
<div className="relative space-y-3">
|
||||||
|
|
||||||
|
{[
|
||||||
|
{ type: 'period', pNum: 1, timeLabel: '08:45 AM' },
|
||||||
|
{ type: 'period', pNum: 2, timeLabel: '09:40 AM' },
|
||||||
|
{ type: 'gap', label: '15M TEA BREAK', timeLabel: '10:35 AM' },
|
||||||
|
{ type: 'period', pNum: 3, timeLabel: '10:50 AM' },
|
||||||
|
{ type: 'period', pNum: 4, timeLabel: '11:45 AM' },
|
||||||
|
{ type: 'gap', label: '50M LUNCH BREAK', timeLabel: '12:40 PM' },
|
||||||
|
{ type: 'period', pNum: 5, timeLabel: '01:30 PM' },
|
||||||
|
{ type: 'period', pNum: 6, timeLabel: '02:25 PM' },
|
||||||
|
{ type: 'period', pNum: 7, timeLabel: '03:20 PM' },
|
||||||
|
].map((rowItem, rIdx) => {
|
||||||
|
if (rowItem.type === 'gap') {
|
||||||
|
// Diagonal-Striped Gap Row (Matching Screenshot "1.5H GAP" block)
|
||||||
|
return (
|
||||||
|
<div key={`gap-${rIdx}`} className="grid grid-cols-6 gap-3 items-center">
|
||||||
|
<div className="text-[11px] font-mono font-bold text-slate-400 text-center">
|
||||||
|
{rowItem.timeLabel}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-5 py-2.5 px-4 rounded-2xl bg-slate-50/80 border border-dashed border-slate-300 text-center flex items-center justify-center gap-2">
|
||||||
|
<Clock className="w-3.5 h-3.5 text-slate-400" />
|
||||||
|
<span className="text-[11px] font-mono font-extrabold tracking-widest text-slate-500 uppercase">
|
||||||
|
{rowItem.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pNum = rowItem.pNum!;
|
||||||
|
return (
|
||||||
|
<div key={`prow-${pNum}`} className="grid grid-cols-6 gap-3 items-stretch">
|
||||||
|
{/* Time Column Marker */}
|
||||||
|
<div className="flex flex-col justify-center items-center py-2 bg-slate-50 rounded-2xl border border-slate-200">
|
||||||
|
<span className="text-xs font-mono font-extrabold text-slate-700">{rowItem.timeLabel}</span>
|
||||||
|
<span className="text-[10px] font-bold text-orange-600">P{pNum}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 5 Day Period Cells */}
|
||||||
|
{(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'] as const).map((dayKey) => {
|
||||||
|
const dayList = timetable?.[dayKey] || [];
|
||||||
|
const period = dayList.find((p) => p.periodNumber === pNum);
|
||||||
|
|
||||||
|
if (!period || !matchesFilter(period)) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${dayKey}-${pNum}`}
|
||||||
|
className="p-3 rounded-2xl bg-slate-50/50 border border-slate-100 flex items-center justify-center opacity-40"
|
||||||
|
>
|
||||||
|
<span className="text-[10px] text-slate-400 font-mono">-</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const starKey = `${dayKey}-${pNum}`;
|
||||||
|
const isStarred = starredPeriods[starKey];
|
||||||
|
const isLiveNow = isPeriodLive(dayKey, pNum);
|
||||||
|
const isFree = period.subjectCode === 'FREE';
|
||||||
|
const isLab = period.type === 'LAB';
|
||||||
|
const isElective = (period.subjectCode || '').includes('V1') || (period.subjectName || '').toLowerCase().includes('elective');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${dayKey}-${pNum}`}
|
||||||
|
className={`relative p-3.5 rounded-2xl border transition-all flex flex-col justify-between group ${
|
||||||
|
isLiveNow
|
||||||
|
? 'bg-orange-50 border-orange-400 ring-2 ring-orange-400/30 shadow-md'
|
||||||
|
: isFree
|
||||||
|
? 'bg-slate-50/80 border-slate-200'
|
||||||
|
: isLab
|
||||||
|
? 'bg-purple-50/50 border-purple-200 hover:border-purple-300'
|
||||||
|
: isElective
|
||||||
|
? 'bg-emerald-50/50 border-emerald-200 hover:border-emerald-300'
|
||||||
|
: 'bg-white border-slate-200 hover:border-slate-300 shadow-sm hover:shadow-md'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Live Now Indicator Badge */}
|
||||||
|
{isLiveNow && (
|
||||||
|
<span className="absolute -top-2 right-3 px-2 py-0.5 rounded-full bg-red-600 text-white text-[9px] font-black uppercase tracking-wider flex items-center gap-1 shadow-sm animate-pulse">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-white" />
|
||||||
|
LIVE
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{/* Subject Title */}
|
||||||
|
<h4 className={`text-xs font-black line-clamp-2 ${
|
||||||
|
isFree ? 'text-slate-400' : isLab ? 'text-purple-900' : isElective ? 'text-emerald-900' : 'text-slate-800'
|
||||||
|
}`} style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
|
{period.subjectName}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Faculty Instructor */}
|
||||||
|
{!isFree && (
|
||||||
|
<p className="text-[10px] font-semibold text-slate-500 mt-1 line-clamp-1">
|
||||||
|
{period.staffName || 'Faculty Instructor'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Badges & Star Footer */}
|
||||||
|
<div className="flex items-center justify-between gap-1 mt-2 pt-2 border-t border-slate-100">
|
||||||
|
<div className="flex items-center gap-1 overflow-hidden">
|
||||||
|
{period.subjectCode && !isFree && (
|
||||||
|
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono font-bold bg-slate-100 text-slate-600 border border-slate-200">
|
||||||
|
{period.subjectCode}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={`px-1.5 py-0.5 rounded text-[9px] font-extrabold uppercase ${
|
||||||
|
isLab ? 'bg-purple-100 text-purple-700' : isElective ? 'bg-emerald-100 text-emerald-700' : 'bg-blue-100 text-blue-700'
|
||||||
|
}`}>
|
||||||
|
{period.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Star Favorite Button (Matching Screenshot) */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleStarPeriod(starKey)}
|
||||||
|
className={`p-1 rounded-lg transition-all ${
|
||||||
|
isStarred ? 'text-amber-500 fill-amber-500 scale-110' : 'text-slate-300 hover:text-amber-400'
|
||||||
|
}`}
|
||||||
|
title={isStarred ? 'Unfavorite' : 'Favorite period'}
|
||||||
|
>
|
||||||
|
<Star className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-bold text-[#1E293B]">
|
|
||||||
No classes scheduled for {selectedDay.toUpperCase()}
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-[#64748B] max-w-sm mx-auto">
|
|
||||||
Select another day of the week above to view your timetable periods and staff.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
/* ─── DAILY CARD VIEW ─── */
|
||||||
{currentPeriods.map((period) => (
|
<div className="space-y-6">
|
||||||
<motion.div
|
{/* Day Switcher */}
|
||||||
key={period.periodNumber}
|
<div className="flex items-center justify-between bg-white p-4 rounded-3xl border border-[#E5E7EB] shadow-sm">
|
||||||
initial={{ opacity: 0, y: 10 }}
|
<div className="flex items-center gap-2 overflow-x-auto py-1">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
{(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'] as const).map((dayKey) => {
|
||||||
className="bg-white rounded-2xl p-5 border border-[#E5E7EB] hover:border-slate-300 shadow-sm transition-all"
|
const count = timetable?.[dayKey]?.length || 0;
|
||||||
>
|
const isSelected = selectedDay === dayKey;
|
||||||
{/* Header */}
|
return (
|
||||||
<div className="flex items-center justify-between gap-2 mb-3">
|
<button
|
||||||
<div className="flex items-center gap-2">
|
key={dayKey}
|
||||||
<span className="w-6 h-6 rounded-lg bg-slate-100 text-[#1E293B] font-bold text-xs flex items-center justify-center">
|
onClick={() => setSelectedDay(dayKey)}
|
||||||
P{period.periodNumber}
|
className={`flex items-center gap-2 px-4 py-2 rounded-2xl text-xs font-bold transition-all cursor-pointer capitalize ${
|
||||||
</span>
|
isSelected
|
||||||
<span className="text-xs font-semibold text-[#64748B] flex items-center gap-1">
|
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-md shadow-orange-500/20'
|
||||||
<Clock className="w-3 h-3 text-[#94A3B8]" />
|
: 'bg-slate-100 hover:bg-slate-200 text-[#64748B]'
|
||||||
{period.timeSlot}
|
}`}
|
||||||
</span>
|
>
|
||||||
</div>
|
<span>{dayKey}</span>
|
||||||
|
<span className={`px-2 py-0.5 rounded-md text-[10px] font-mono ${isSelected ? 'bg-white/20 text-white' : 'bg-white text-[#64748B] border border-[#E5E7EB]'}`}>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
className={`px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wider flex items-center gap-1 ${
|
{(timetable?.[selectedDay] || []).filter(matchesFilter).map((period) => {
|
||||||
period.type === 'LAB'
|
const starKey = `${selectedDay}-${period.periodNumber}`;
|
||||||
? 'bg-purple-100 text-purple-700 border border-purple-300'
|
const isStarred = starredPeriods[starKey];
|
||||||
: 'bg-blue-100 text-blue-700 border border-blue-300'
|
const isLiveNow = isPeriodLive(selectedDay, period.periodNumber);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={period.periodNumber}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className={`bg-white rounded-3xl p-5 border transition-all shadow-sm flex flex-col justify-between ${
|
||||||
|
isLiveNow ? 'border-orange-400 ring-2 ring-orange-400/20 bg-orange-50/30' : 'border-[#E5E7EB] hover:border-slate-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{period.type === 'LAB' ? <FlaskConical className="w-3 h-3 text-purple-600" /> : <BookOpen className="w-3 h-3 text-blue-600" />}
|
{/* Header */}
|
||||||
{period.type}
|
<div className="flex items-center justify-between gap-2 mb-3">
|
||||||
</span>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<span className="w-7 h-7 rounded-xl bg-slate-100 text-[#1E293B] font-extrabold text-xs flex items-center justify-center">
|
||||||
|
P{period.periodNumber}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-bold text-[#64748B] flex items-center gap-1">
|
||||||
|
<Clock className="w-3.5 h-3.5 text-[#94A3B8]" />
|
||||||
|
{period.timeSlot}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Subject Details */}
|
<div className="flex items-center gap-2">
|
||||||
<div className="space-y-1.5 mb-4">
|
{isLiveNow && (
|
||||||
<div className="flex items-center gap-2">
|
<span className="px-2 py-0.5 rounded-full bg-red-600 text-white text-[9px] font-black uppercase tracking-wider animate-pulse">
|
||||||
<span className="text-[11px] font-mono font-bold text-[#F97316]">
|
LIVE
|
||||||
{period.subjectCode}
|
</span>
|
||||||
</span>
|
)}
|
||||||
</div>
|
<span
|
||||||
<h3 className="text-base font-extrabold text-[#1E293B] line-clamp-1" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
className={`px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wider flex items-center gap-1 ${
|
||||||
{period.subjectName}
|
period.type === 'LAB'
|
||||||
</h3>
|
? 'bg-purple-100 text-purple-700 border border-purple-300'
|
||||||
</div>
|
: 'bg-blue-100 text-blue-700 border border-blue-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{period.type === 'LAB' ? <FlaskConical className="w-3 h-3 text-purple-600" /> : <BookOpen className="w-3 h-3 text-blue-600" />}
|
||||||
|
{period.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Faculty Footer */}
|
{/* Subject Details */}
|
||||||
<div className="pt-3 border-t border-[#E5E7EB] flex items-center justify-between text-xs text-[#475569]">
|
<div className="space-y-1.5 my-3">
|
||||||
<span className="truncate pr-2 font-medium">{period.staffName || 'Faculty Instructor'}</span>
|
<div className="flex items-center justify-between">
|
||||||
{period.staffCode && (
|
<span className="text-[11px] font-mono font-extrabold text-[#F97316] bg-orange-50 px-2 py-0.5 rounded-md border border-orange-200">
|
||||||
<span className="font-mono text-[10px] text-[#94A3B8]">{period.staffCode}</span>
|
{period.subjectCode}
|
||||||
)}
|
</span>
|
||||||
</div>
|
<button
|
||||||
</motion.div>
|
onClick={() => toggleStarPeriod(starKey)}
|
||||||
))}
|
className={`p-1 rounded-lg transition-all ${
|
||||||
|
isStarred ? 'text-amber-500 fill-amber-500' : 'text-slate-300 hover:text-amber-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Star className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-base font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
|
{period.subjectName}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Faculty Footer */}
|
||||||
|
<div className="pt-3 border-t border-[#E5E7EB] flex items-center justify-between text-xs text-[#475569]">
|
||||||
|
<span className="truncate pr-2 font-medium">{period.staffName || 'Faculty Instructor'}</span>
|
||||||
|
{period.staffCode && (
|
||||||
|
<span className="font-mono text-[10px] text-[#94A3B8]">{period.staffCode}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user