155 lines
6.6 KiB
TypeScript
155 lines
6.6 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { supabase } from '../supabase';
|
|
import { Announcement } from '../types';
|
|
|
|
const FacultyNotifications: React.FC = () => {
|
|
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [now, setNow] = useState(new Date());
|
|
|
|
useEffect(() => {
|
|
const timer = setInterval(() => setNow(new Date()), 30000);
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
|
|
// Fetch announcements directly from Supabase — no prop dependency
|
|
useEffect(() => {
|
|
const fetchAnnouncements = async () => {
|
|
setIsLoading(true);
|
|
const { data, error } = await supabase
|
|
.from('announcements')
|
|
.select('*')
|
|
.order('timestamp', { ascending: false });
|
|
|
|
if (data && !error) {
|
|
setAnnouncements(data.map(ann => ({
|
|
id: String(ann.id),
|
|
title: ann.title,
|
|
message: ann.message,
|
|
type: ann.type,
|
|
eventId: ann.event_id ? String(ann.event_id) : undefined,
|
|
timestamp: ann.timestamp || ann.created_at || new Date().toISOString(),
|
|
expiresAt: ann.expires_at
|
|
})));
|
|
}
|
|
setIsLoading(false);
|
|
};
|
|
|
|
fetchAnnouncements();
|
|
|
|
// Subscribe to realtime changes on announcements
|
|
const channel = supabase
|
|
.channel('announcements-faculty')
|
|
.on('postgres_changes', { event: '*', schema: 'public', table: 'announcements' }, () => {
|
|
fetchAnnouncements();
|
|
})
|
|
.subscribe();
|
|
|
|
return () => { supabase.removeChannel(channel); };
|
|
}, []);
|
|
|
|
const getTypeIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'URGENT': return 'fa-triangle-exclamation text-rose-500';
|
|
case 'DELAY': return 'fa-clock text-amber-500';
|
|
case 'INFO': return 'fa-info-circle text-blue-500';
|
|
case 'ENDED': return 'fa-flag-checkered text-purple-500';
|
|
case 'ONGOING': return 'fa-play-circle text-emerald-500';
|
|
default: return 'fa-bullhorn text-[#f97316]';
|
|
}
|
|
};
|
|
|
|
const formatTime = (timestamp: any): string => {
|
|
if (!timestamp) return 'Now';
|
|
try {
|
|
// Supabase returns ISO strings — parse directly
|
|
const date = new Date(timestamp);
|
|
if (isNaN(date.getTime())) return 'Now';
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
} catch {
|
|
return 'Now';
|
|
}
|
|
};
|
|
|
|
const formatDate = (timestamp: any): string => {
|
|
if (!timestamp) return '';
|
|
try {
|
|
const date = new Date(timestamp);
|
|
if (isNaN(date.getTime())) return '';
|
|
const today = new Date();
|
|
const isToday = date.toDateString() === today.toDateString();
|
|
if (isToday) return 'Today';
|
|
return date.toLocaleDateString([], { day: 'numeric', month: 'short' });
|
|
} catch {
|
|
return '';
|
|
}
|
|
};
|
|
|
|
const isExpired = (expiresAt: any): boolean => {
|
|
if (!expiresAt) return false;
|
|
try {
|
|
return new Date(expiresAt) < now;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const activeAnnouncements = announcements.filter(ann => !isExpired(ann.expiresAt));
|
|
|
|
return (
|
|
<div className="animate-in fade-in slide-in-from-right-10 duration-700 max-w-4xl">
|
|
<div className="mb-12">
|
|
<h3 className="text-4xl font-black tracking-tight uppercase mb-2">Notice <span className="text-blue-500">Board</span></h3>
|
|
<p className="text-[10px] font-black text-gray-500 uppercase tracking-[0.4em]">Administrative Updates & Alerts</p>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="py-20 flex flex-col items-center justify-center">
|
|
<div className="w-12 h-12 border-4 border-blue-500/20 border-t-blue-500 rounded-full animate-spin mb-4"></div>
|
|
<p className="text-[10px] font-black text-gray-600 uppercase tracking-widest">Syncing Notices...</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{activeAnnouncements.length > 0 ? activeAnnouncements.map((n) => (
|
|
<div key={n.id} className="bg-white/5 border border-white/5 rounded-[2.5rem] p-8 flex items-start justify-between group hover:bg-white/[0.08] transition-all cursor-pointer">
|
|
<div className="flex items-start gap-8">
|
|
<div className="w-16 h-16 rounded-[1.5rem] bg-white/5 flex items-center justify-center border border-white/10 group-hover:border-blue-500/50 transition-colors">
|
|
<i className={`fas ${getTypeIcon(n.type)} text-xl`}></i>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-lg font-black uppercase tracking-tight group-hover:text-blue-500 transition-colors">{n.title}</h4>
|
|
<p className="text-gray-400 text-sm font-medium mt-1 mb-3">{n.message}</p>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-1.5 h-1.5 rounded-full ${n.type === 'URGENT' ? 'bg-rose-500 animate-ping' : n.type === 'ENDED' ? 'bg-purple-500' : n.type === 'ONGOING' ? 'bg-emerald-500 animate-pulse' : 'bg-blue-500 animate-pulse'}`}></div>
|
|
<span className={`text-[9px] font-black uppercase tracking-widest ${n.type === 'URGENT' ? 'text-rose-500' : n.type === 'ENDED' ? 'text-purple-500' : n.type === 'ONGOING' ? 'text-emerald-500' : 'text-blue-500/70'}`}>
|
|
{n.type === 'ENDED' ? 'Status: Concluded' : n.type === 'ONGOING' ? 'Status: Live' : `Priority ${n.type === 'URGENT' ? 'Critical' : 'Regular'}`}
|
|
</span>
|
|
</div>
|
|
{n.expiresAt && (
|
|
<div className="flex items-center gap-2 text-rose-500/40">
|
|
<i className="fas fa-hourglass-end text-[8px]"></i>
|
|
<span className="text-[8px] font-black uppercase tracking-widest">Temporal Notice</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col items-end gap-1 shrink-0 ml-4">
|
|
<span className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">{formatTime(n.timestamp)}</span>
|
|
<span className="text-[9px] font-bold text-gray-700 uppercase tracking-widest">{formatDate(n.timestamp)}</span>
|
|
</div>
|
|
</div>
|
|
)) : (
|
|
<div className="py-20 text-center bg-white/5 rounded-[2.5rem] border border-dashed border-white/10">
|
|
<i className="fas fa-inbox text-4xl text-white/10 mb-4"></i>
|
|
<p className="text-[10px] font-black text-gray-600 uppercase tracking-widest">No active notices</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FacultyNotifications; |