335 lines
13 KiB
TypeScript
335 lines
13 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { UserRole, DashboardView, Event, Announcement, Ticket } from '../types';
|
|
import { supabase } from '../supabase';
|
|
import Navbar from './Navbar';
|
|
import Hero from './Hero';
|
|
import HomeDashboard from './HomeDashboard';
|
|
import EventList from './EventList';
|
|
import EventCard from './EventCard';
|
|
import ProfileView from './ProfileView';
|
|
import StatusTrackerView from './StatusTrackerView';
|
|
import RegistrationsView from './RegistrationsView';
|
|
import UpcomingEventsSlider from './UpcomingEventsSlider';
|
|
import AboutHubSection from './AboutHubSection';
|
|
import StatsSection from './StatsSection';
|
|
import AboutSection from './AboutSection';
|
|
import ContactSection from './ContactSection';
|
|
import Footer from './Footer';
|
|
|
|
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
return createPortal(children, document.body);
|
|
};
|
|
|
|
interface DashboardProps {
|
|
userRole: UserRole;
|
|
events: Event[];
|
|
announcements: Announcement[];
|
|
bookedEventIds: string[];
|
|
userRegistrations: any[];
|
|
onToggleBooking: (id: string) => void;
|
|
onLogout: () => void;
|
|
onUploadCertificate: (eventId: string, data: string) => Promise<string>;
|
|
studentDocuments: Record<string, string>;
|
|
onUploadDoc: (studentId: string, data: string) => void;
|
|
currentUserName: string;
|
|
onSupabaseError?: () => void;
|
|
onBackToCoordinatorHub?: () => void;
|
|
specialEvents: any[];
|
|
}
|
|
|
|
const Dashboard: React.FC<DashboardProps> = ({
|
|
userRole,
|
|
events,
|
|
announcements,
|
|
bookedEventIds,
|
|
userRegistrations,
|
|
onToggleBooking,
|
|
onLogout,
|
|
onUploadCertificate,
|
|
studentDocuments,
|
|
onUploadDoc,
|
|
currentUserName,
|
|
onSupabaseError,
|
|
onBackToCoordinatorHub,
|
|
specialEvents
|
|
}) => {
|
|
const [activeView, setActiveView] = useState<DashboardView>('HOME');
|
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
const [trackedEvent, setTrackedEvent] = useState<Event | null>(null);
|
|
|
|
// Team Management States
|
|
const [showCreateModal, setShowCreateModal] = useState<{eventId: string, title: string} | null>(null);
|
|
const [showJoinModal, setShowJoinModal] = useState<{eventId: string, title: string} | null>(null);
|
|
const [teamName, setTeamName] = useState('');
|
|
const [teamCode, setTeamCode] = useState('');
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
|
|
const generateCode = () => Math.random().toString(36).substring(2, 8).toUpperCase();
|
|
|
|
const handleCreateTeam = async (eventId: string) => {
|
|
if (!teamName.trim()) return alert("Enter team name");
|
|
setIsProcessing(true);
|
|
try {
|
|
const code = generateCode();
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
if (!user) return;
|
|
|
|
const registrationId = `${user.id}_${eventId}`;
|
|
const { error } = await supabase.from('registrations').update({
|
|
team_code: code,
|
|
team_name: teamName.trim(),
|
|
is_team_leader: true
|
|
}).eq('id', registrationId);
|
|
|
|
if (error) throw error;
|
|
setShowCreateModal(null);
|
|
setTeamName('');
|
|
window.location.reload();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Failed to create team");
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
const handleJoinTeam = async (eventId: string) => {
|
|
if (!teamCode.trim()) return alert("Enter team code");
|
|
setIsProcessing(true);
|
|
try {
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
if (!user) return;
|
|
|
|
const { data: teamRegs, error: fetchErr } = await supabase
|
|
.from('registrations')
|
|
.select('*')
|
|
.eq('event_id', eventId)
|
|
.eq('team_code', teamCode.trim().toUpperCase());
|
|
|
|
if (fetchErr) throw fetchErr;
|
|
if (!teamRegs || teamRegs.length === 0) return alert("Invalid team code for this event");
|
|
|
|
const leader = teamRegs.find(r => r.is_team_leader);
|
|
const event = events.find(e => e.id === eventId);
|
|
|
|
if (event?.teamSizeLimit && teamRegs.length >= event.teamSizeLimit) {
|
|
return alert("Team is already full");
|
|
}
|
|
|
|
const { data: myProfile } = await supabase.from('Studentusers').select('department').eq('id', user.id).single();
|
|
if (event?.teamComposition === 'INTER_DEPT' && leader && myProfile?.department !== leader.dept) {
|
|
return alert(`This event requires INTER-DEPARTMENT teams. You must join a team from ${leader.dept}`);
|
|
}
|
|
|
|
const registrationId = `${user.id}_${eventId}`;
|
|
const { error: joinErr } = await supabase.from('registrations').update({
|
|
team_code: teamCode.trim().toUpperCase(),
|
|
team_name: leader?.team_name || 'Team',
|
|
is_team_leader: false
|
|
}).eq('id', registrationId);
|
|
|
|
if (joinErr) throw joinErr;
|
|
setShowJoinModal(null);
|
|
setTeamCode('');
|
|
window.location.reload();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Failed to join team");
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (activeView !== 'EVENTS' && activeView !== 'STATUS_TRACKER') {
|
|
setSelectedCategory(null);
|
|
}
|
|
}, [activeView]);
|
|
|
|
const handleTrackStatus = (event: Event) => {
|
|
setTrackedEvent(event);
|
|
setActiveView('STATUS_TRACKER');
|
|
};
|
|
|
|
const renderContent = () => {
|
|
switch (activeView) {
|
|
case 'HOME':
|
|
return (
|
|
<>
|
|
<Hero events={events} />
|
|
<UpcomingEventsSlider events={events} />
|
|
<AboutHubSection />
|
|
<StatsSection events={events} />
|
|
<HomeDashboard
|
|
events={events}
|
|
announcements={announcements}
|
|
onNavigateToEvents={() => setActiveView('EVENTS')}
|
|
specialEvents={specialEvents}
|
|
/>
|
|
</>
|
|
);
|
|
case 'ABOUT':
|
|
return (
|
|
<div className="pt-16 min-h-screen bg-white">
|
|
<AboutSection />
|
|
</div>
|
|
);
|
|
case 'CONTACT':
|
|
return (
|
|
<div className="pt-16 min-h-screen bg-white">
|
|
<ContactSection />
|
|
</div>
|
|
);
|
|
case 'EVENTS':
|
|
return (
|
|
<EventList
|
|
events={events}
|
|
selectedCategory={selectedCategory}
|
|
bookedEventIds={bookedEventIds}
|
|
onToggleBooking={onToggleBooking}
|
|
onSelectCategory={setSelectedCategory}
|
|
onTrackStatus={handleTrackStatus}
|
|
currentUserName={currentUserName}
|
|
userRole={userRole}
|
|
userRegistrations={userRegistrations}
|
|
/>
|
|
);
|
|
case 'REGISTRATIONS':
|
|
return (
|
|
<RegistrationsView
|
|
events={events}
|
|
bookedEventIds={bookedEventIds}
|
|
userRegistrations={userRegistrations}
|
|
onToggleBooking={onToggleBooking}
|
|
onTrackStatus={handleTrackStatus}
|
|
currentUserName={currentUserName}
|
|
userRole={userRole}
|
|
/>
|
|
);
|
|
case 'PROFILE':
|
|
return <ProfileView onLogout={onLogout} onSupabaseError={onSupabaseError} />;
|
|
case 'STATUS_TRACKER':
|
|
const registration = userRegistrations.find(r => String(r.event_id) === String(trackedEvent?.id));
|
|
return trackedEvent ? (
|
|
<StatusTrackerView
|
|
event={trackedEvent}
|
|
registration={registration}
|
|
onBack={() => setActiveView('REGISTRATIONS')}
|
|
onUploadCertificate={(data) => onUploadCertificate(trackedEvent.id, data)}
|
|
onShowCreateTeam={() => setShowCreateModal({ eventId: trackedEvent.id, title: trackedEvent.title })}
|
|
onShowJoinTeam={() => setShowJoinModal({ eventId: trackedEvent.id, title: trackedEvent.title })}
|
|
/>
|
|
) : null;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#F3F4F6]">
|
|
<Navbar
|
|
activeView={activeView}
|
|
onViewChange={setActiveView}
|
|
onLogout={onLogout}
|
|
onBackToCoordinatorHub={onBackToCoordinatorHub}
|
|
/>
|
|
<main className="pb-20">
|
|
{renderContent()}
|
|
</main>
|
|
<Footer />
|
|
|
|
{/* Team Management Modals */}
|
|
{showCreateModal && (
|
|
<Portal>
|
|
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
|
<div className="bg-white rounded-[3rem] w-full max-w-md p-10 md:p-12 shadow-2xl animate-in zoom-in-95 duration-500 relative overflow-hidden">
|
|
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-orange-400 to-amber-400"></div>
|
|
<div className="w-16 h-16 flex items-center justify-center mx-auto mb-8">
|
|
<img
|
|
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
|
alt="RIT Logo"
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
</div>
|
|
<h3 className="text-2xl font-black text-slate-900 text-center uppercase mb-2 tracking-tight">Form a Team</h3>
|
|
<p className="text-[10px] text-gray-400 text-center font-bold uppercase tracking-widest mb-10 line-clamp-1 px-4">{showCreateModal.title}</p>
|
|
|
|
<div className="space-y-8">
|
|
<div>
|
|
<label className="block text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] mb-3 px-2">Team Identity</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Enter team name..."
|
|
className="w-full bg-slate-50 border border-slate-200 rounded-[2rem] px-8 py-5 text-sm font-bold outline-none focus:ring-4 focus:ring-orange-100 focus:border-[#f97316] transition-all placeholder:text-slate-300"
|
|
value={teamName}
|
|
onChange={e => setTeamName(e.target.value)}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-3">
|
|
<button
|
|
onClick={() => handleCreateTeam(showCreateModal.eventId)}
|
|
disabled={isProcessing}
|
|
className="w-full py-5 bg-[#f97316] text-white rounded-[2rem] font-black uppercase text-[11px] tracking-[0.2em] shadow-xl shadow-orange-200 hover:bg-[#ea580c] transition-all active:scale-95 disabled:opacity-50"
|
|
>
|
|
{isProcessing ? 'INITIALIZING...' : 'Establish Team & Code'}
|
|
</button>
|
|
<button onClick={() => setShowCreateModal(null)} className="w-full py-4 text-gray-400 font-black uppercase text-[9px] tracking-widest hover:text-slate-600 transition-colors">Dismiss</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Portal>
|
|
)}
|
|
|
|
{showJoinModal && (
|
|
<Portal>
|
|
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
|
<div className="bg-white rounded-[3rem] w-full max-w-md p-10 md:p-12 shadow-2xl animate-in zoom-in-95 duration-500 relative overflow-hidden">
|
|
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-blue-400 to-indigo-400"></div>
|
|
<div className="w-16 h-16 flex items-center justify-center mx-auto mb-8">
|
|
<img
|
|
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
|
alt="RIT Logo"
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
</div>
|
|
<h3 className="text-2xl font-black text-slate-900 text-center uppercase mb-2 tracking-tight">Join Alliance</h3>
|
|
<p className="text-[10px] text-gray-400 text-center font-bold uppercase tracking-widest mb-10 line-clamp-1 px-4">{showJoinModal.title}</p>
|
|
|
|
<div className="space-y-8">
|
|
<div>
|
|
<label className="block text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] mb-3 px-2 text-center">Team Access Token</label>
|
|
<input
|
|
type="text"
|
|
placeholder="TOKEN"
|
|
className="w-full bg-slate-50 border border-slate-200 rounded-[2rem] px-5 py-6 text-center text-3xl font-black tracking-[0.4em] outline-none focus:ring-4 focus:ring-blue-100 focus:border-blue-500 uppercase transition-all placeholder:text-slate-200"
|
|
value={teamCode}
|
|
onChange={e => setTeamCode(e.target.value)}
|
|
maxLength={6}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-3">
|
|
<button
|
|
onClick={() => handleJoinTeam(showJoinModal.eventId)}
|
|
disabled={isProcessing}
|
|
className="w-full py-5 bg-slate-900 text-white rounded-[2rem] font-black uppercase text-[11px] tracking-[0.2em] shadow-xl shadow-gray-200 hover:bg-black transition-all active:scale-95 disabled:opacity-50"
|
|
>
|
|
{isProcessing ? 'AUTHENTICATING...' : 'Validate & Join'}
|
|
</button>
|
|
<button onClick={() => setShowJoinModal(null)} className="w-full py-4 text-gray-400 font-black uppercase text-[9px] tracking-widest hover:text-slate-600 transition-colors">Dismiss</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Portal>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Dashboard; |