Files
Event-Management-System/RIT-EMS-main/frontend/src/components/student/RegistrationsView.tsx
2026-06-18 17:57:54 +05:30

414 lines
23 KiB
TypeScript

import React from 'react';
import { createPortal } from 'react-dom';
import type { Event } from '../../types';
import EventCard from './EventCard';
import { API_BASE_URL } from '../../lib/config';
import { useAuth } from '../../context/AuthContext';
interface RegistrationsViewProps {
events: Event[];
bookedEventIds: string[];
userRegistrations: any[];
onToggleBooking: (id: string) => void;
onTrackStatus: (event: Event) => void;
currentUserName: string;
userRole?: string;
}
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return createPortal(children, document.body);
};
const RegistrationsView: React.FC<RegistrationsViewProps> = ({ events, bookedEventIds, userRegistrations, onToggleBooking, onTrackStatus, currentUserName, userRole }) => {
const { user } = useAuth();
const [showInspectModal, setShowInspectModal] = React.useState<{eventId: string, title: string, teamCode: string} | null>(null);
const [showRemoveConfirm, setShowRemoveConfirm] = React.useState<{member: any, eventId: string, teamCode: string} | null>(null);
const [showRemoveSuccess, setShowRemoveSuccess] = React.useState<{name: string} | null>(null);
const [showDisbandConfirm, setShowDisbandConfirm] = React.useState<{eventId: string, teamCode: string} | null>(null);
const [showLeaveConfirm, setShowLeaveConfirm] = React.useState<{eventId: string} | null>(null);
const [teamMembers, setTeamMembers] = React.useState<any[]>([]);
const [isProcessing, setIsProcessing] = React.useState(false);
const [copyingCode, setCopyingCode] = React.useState<string | null>(null);
const bookedEvents = events.filter(e => bookedEventIds.includes(e.id));
const handleCopyCode = async (code: string) => {
try {
await navigator.clipboard.writeText(code);
setCopyingCode(code);
setTimeout(() => setCopyingCode(null), 2000);
} catch (err) {
console.error("Copy failed", err);
}
};
const executeLeaveTeam = async (eventId: string) => {
if (!user) return;
setIsProcessing(true);
try {
const registrationId = `${user.id}_${eventId}`;
const res = await fetch(`${API_BASE_URL}/api/registrations/${registrationId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
team_code: null,
team_name: null,
is_team_leader: false
})
});
if (!res.ok) throw new Error("Failed to leave team");
setShowLeaveConfirm(null);
setShowInspectModal(null);
window.location.reload();
} catch (err) {
console.error(err);
alert("Failed to leave team");
} finally {
setIsProcessing(false);
}
};
const executeDisbandTeam = async (eventId: string, code: string) => {
setIsProcessing(true);
try {
// Fetch all members of this team
const resMembers = await fetch(`${API_BASE_URL}/api/registrations?eventId=${eventId}&teamCode=${code}`);
const members = resMembers.ok ? await resMembers.json() : [];
// Update all members to disband
for (const m of members) {
const regId = m.id || `${m.userId || m.user_id}_${eventId}`;
await fetch(`${API_BASE_URL}/api/registrations/${regId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
team_code: null,
team_name: null,
is_team_leader: false
})
});
}
setShowDisbandConfirm(null);
setShowInspectModal(null);
window.location.reload();
} catch (err) {
console.error(err);
alert("Failed to disband team");
} finally {
setIsProcessing(false);
}
};
const handleInspectTeam = async (eventId: string, code: string, title: string) => {
setIsProcessing(true);
try {
const res = await fetch(`${API_BASE_URL}/api/registrations?eventId=${eventId}&teamCode=${code}`);
if (!res.ok) throw new Error("Failed to fetch team roster");
const members = await res.json();
const formattedMembers = members.map((m: any) => ({
user_id: m.userId || m.user_id,
name: m.userName || m.user_name,
reg_no: m.regNo || m.reg_no,
year: m.year,
department: m.dept || m.department,
section: m.section,
is_team_leader: m.isTeamLeader || m.is_team_leader
}));
setTeamMembers(formattedMembers);
setShowInspectModal({ eventId, title, teamCode: code });
} catch (err) {
console.error(err);
alert("Failed to fetch team members");
} finally {
setIsProcessing(false);
}
};
return (
<div className="pt-40 pb-20 px-6 md:px-12 lg:px-24 animate-in fade-in duration-500 font-inter">
<h1 className="text-5xl font-black text-[#1A202C] mb-16 tracking-tight uppercase">My <span className="text-[#f97316]">Registrations</span></h1>
{bookedEvents.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
{bookedEvents.map(event => {
const reg = userRegistrations.find(r => String(r.eventId || r.event_id) === String(event.id));
const teamCodeVal = reg?.teamCode || reg?.team_code;
const teamNameVal = reg?.teamName || reg?.team_name;
const isLeaderVal = reg?.isTeamLeader || reg?.is_team_leader;
return (
<div key={event.id} className="space-y-6">
<EventCard
event={event}
isBooked={true}
onToggle={() => onToggleBooking(event.id)}
onTrackStatus={onTrackStatus}
currentUserName={currentUserName}
userRole={userRole}
registration={reg}
/>
{event.isTeamEvent && (
<div className="bg-white rounded-3xl p-6 border border-slate-200 shadow-sm relative overflow-hidden group">
<div className="absolute top-0 right-0 w-24 h-24 bg-orange-50/50 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl group-hover:bg-orange-100/50 transition-colors"></div>
{teamCodeVal ? (
<div className="flex items-center justify-between relative z-10">
<div>
<span className="block text-[8px] font-black text-emerald-500 uppercase tracking-widest mb-1 flex items-center gap-1">
<i className="fas fa-check-circle"></i> Status: Teamed Up
</span>
<h4 className="text-sm font-black text-slate-900 uppercase">{teamNameVal}</h4>
<p className="text-[10px] text-slate-400 font-bold mt-1 uppercase flex items-center gap-2">
CODE: <span className="text-slate-900 underline font-black">{teamCodeVal}</span>
<button
onClick={() => handleCopyCode(teamCodeVal)}
className="text-slate-400 hover:text-[#f97316] transition-colors"
title="Copy Code"
>
<i className={`fas ${copyingCode === teamCodeVal ? 'fa-check text-emerald-500' : 'fa-copy'}`}></i>
</button>
</p>
</div>
<div className="flex flex-col items-end gap-2">
<div className={`w-12 h-12 ${isLeaderVal ? 'bg-amber-100 text-amber-600' : 'bg-emerald-50 text-emerald-500'} rounded-2xl flex items-center justify-center text-lg shadow-sm border ${isLeaderVal ? 'border-amber-200' : 'border-emerald-100'}`}>
<i className={`fas ${isLeaderVal ? 'fa-crown animate-pulse' : 'fa-user-group'}`}></i>
</div>
<button
onClick={() => handleInspectTeam(event.id, teamCodeVal, event.title)}
className="text-[9px] font-black text-slate-400 uppercase tracking-widest hover:text-[#f97316] transition-colors flex items-center gap-1"
>
<i className="fas fa-search"></i> Inspect Team
</button>
</div>
</div>
) : (
<div className="space-y-5 relative z-10">
<div className="flex items-center gap-3">
<div className="w-1.5 h-4 bg-slate-200 rounded-full"></div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Team Required</span>
</div>
<p className="text-[9px] text-slate-400 font-bold uppercase leading-relaxed">Please use the "Track Progress" button to manage your team for this event.</p>
</div>
)}
</div>
)}
</div>
);
})}
</div>
) : (
<div className="flex flex-col items-center justify-center min-h-[40vh] bg-white rounded-[40px] border border-dashed border-gray-300 p-10 text-center">
<i className="fas fa-ticket-alt text-6xl text-gray-200 mb-6"></i>
<p className="text-gray-400 text-xl font-medium max-w-sm">You haven't booked any events yet, or your previously registered events have been removed.</p>
</div>
)}
{showInspectModal && (
<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-2xl p-10 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-[#f97316] to-amber-500"></div>
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-2xl font-black text-slate-900 uppercase tracking-tight">Team Roster</h3>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest mt-1">Code: <span className="text-[#f97316]">{showInspectModal.teamCode}</span> | {showInspectModal.title}</p>
</div>
<button onClick={() => setShowInspectModal(null)} className="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-colors">
<i className="fas fa-times"></i>
</button>
</div>
<div className="max-h-[50vh] overflow-y-auto no-scrollbar space-y-4 pr-2 mb-8">
{teamMembers.map((member, idx) => {
const isLeader = teamMembers.find(m => m.is_team_leader && m.name === currentUserName);
return (
<div key={idx} className="bg-slate-50 rounded-3xl p-6 flex items-center justify-between border border-slate-100 hover:border-slate-200 transition-all group">
<div className="flex items-center gap-5">
<div className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center text-xl text-[#f97316] border border-slate-100 shadow-sm group-hover:scale-110 transition-transform">
<i className={`fas ${member.is_team_leader ? 'fa-crown' : 'fa-user'}`}></i>
</div>
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-black text-slate-900 uppercase">{member.name}</h4>
{member.is_team_leader && <span className="bg-amber-100 text-amber-600 text-[8px] font-black px-2 py-0.5 rounded-full uppercase tracking-tighter">Leader</span>}
</div>
<p className="text-[10px] text-slate-400 font-bold mt-0.5 uppercase tracking-widest">{member.reg_no}</p>
</div>
</div>
<div className="flex items-center gap-6 text-right">
<div>
<p className="text-[9px] font-black text-slate-900 uppercase">{member.department}</p>
<p className="text-[8px] text-slate-400 font-bold uppercase">{member.year} • SEC {member.section}</p>
</div>
{isLeader && !member.is_team_leader && (
<button
onClick={() => {
setShowRemoveConfirm({
member,
eventId: showInspectModal.eventId,
teamCode: showInspectModal.teamCode
});
}}
className="w-10 h-10 rounded-xl bg-white text-rose-500 border border-rose-100 flex items-center justify-center transition-all hover:bg-rose-500 hover:text-white shadow-sm"
title="Remove from team"
>
<i className="fas fa-user-minus"></i>
</button>
)}
</div>
</div>
);
})}
</div>
<div className="flex flex-col gap-3">
{teamMembers.find(m => m.is_team_leader && m.name === currentUserName) ? (
<button
onClick={() => setShowDisbandConfirm({ eventId: showInspectModal.eventId, teamCode: showInspectModal.teamCode })}
disabled={isProcessing}
className="w-full py-4 bg-rose-50 text-rose-600 border border-rose-100 rounded-[2rem] font-black uppercase text-[10px] tracking-widest hover:bg-rose-100 transition-all flex items-center justify-center gap-2"
>
<i className="fas fa-trash-can"></i> Disband Team
</button>
) : (
<button
onClick={() => setShowLeaveConfirm({ eventId: showInspectModal?.eventId || '' })}
disabled={isProcessing}
className="w-full py-4 bg-amber-50 text-amber-600 border border-amber-100 rounded-[2rem] font-black uppercase text-[10px] tracking-widest hover:bg-amber-100 transition-all flex items-center justify-center gap-2"
>
<i className="fas fa-right-from-bracket"></i> Leave Team
</button>
)}
<button
onClick={() => setShowInspectModal(null)}
className="w-full py-5 bg-slate-900 text-white rounded-[2rem] font-black uppercase text-[11px] tracking-[0.2em] shadow-xl hover:bg-black transition-all active:scale-95"
>
Close Inspection
</button>
</div>
{/* Confirmation Modals (Portaled to prevent clipping) */}
{showRemoveConfirm && (
<Portal>
<div className="fixed inset-0 z-[13000] 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-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
<div className="p-8 text-center">
<div className="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
<i className="fas fa-user-slash"></i>
</div>
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Remove Member?</h3>
<p className="text-sm text-slate-500 font-medium">Are you sure you want to remove <span className="font-black text-slate-900 uppercase">{showRemoveConfirm.member.name}</span> from the team?</p>
</div>
<div className="p-6 bg-slate-50 flex gap-4">
<button onClick={() => setShowRemoveConfirm(null)} className="flex-1 py-4 bg-white text-slate-400 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
<button
onClick={async () => {
setIsProcessing(true);
try {
const registrationId = `${showRemoveConfirm.member.user_id}_${showRemoveConfirm.eventId}`;
const res = await fetch(`${API_BASE_URL}/api/registrations/${registrationId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
team_code: null,
team_name: null,
is_team_leader: false
})
});
if (!res.ok) throw new Error("Failed to remove member");
setTeamMembers(prev => prev.filter(m => m.user_id !== showRemoveConfirm.member.user_id));
const removedName = showRemoveConfirm.member.name;
setShowRemoveConfirm(null);
setShowRemoveSuccess({ name: removedName });
} catch (err) {
console.error(err);
alert("Failed to remove member");
} finally {
setIsProcessing(false);
}
}}
disabled={isProcessing}
className="flex-1 py-4 bg-rose-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 transition-all shadow-lg shadow-rose-200"
>
{isProcessing ? 'Removing...' : 'Confirm'}
</button>
</div>
</div>
</div>
</Portal>
)}
{showRemoveSuccess && (
<Portal>
<div className="fixed inset-0 z-[13000] 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-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
<div className="p-10 text-center">
<div className="w-16 h-16 bg-emerald-50 text-emerald-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
<i className="fas fa-check-circle"></i>
</div>
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Member Removed</h3>
<p className="text-sm text-slate-500 font-medium"><span className="font-black text-slate-900 uppercase">{showRemoveSuccess.name}</span> has been removed.</p>
<button onClick={() => setShowRemoveSuccess(null)} className="mt-8 w-full py-4 bg-[#1A202C] text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-black transition-all shadow-xl shadow-slate-200">Got it</button>
</div>
</div>
</div>
</Portal>
)}
{showLeaveConfirm && (
<Portal>
<div className="fixed inset-0 z-[13000] 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-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
<div className="p-8 text-center">
<div className="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
<i className="fas fa-sign-out-alt"></i>
</div>
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Leave Team?</h3>
<p className="text-sm text-slate-500 font-medium">Are you sure you want to leave this team?</p>
</div>
<div className="p-6 bg-slate-50 flex gap-4">
<button onClick={() => setShowLeaveConfirm(null)} className="flex-1 py-4 bg-white text-slate-400 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
<button onClick={() => executeLeaveTeam(showLeaveConfirm.eventId)} disabled={isProcessing} className="flex-1 py-4 bg-rose-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 transition-all shadow-lg shadow-rose-200">{isProcessing ? 'Leaving...' : 'Confirm'}</button>
</div>
</div>
</div>
</Portal>
)}
{showDisbandConfirm && (
<Portal>
<div className="fixed inset-0 z-[13000] 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-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
<div className="p-8 text-center">
<div className="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
<i className="fas fa-ban"></i>
</div>
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Disband Team?</h3>
<p className="text-sm text-slate-500 font-medium">Are you sure? This will disband the team for <span className="font-black text-slate-900">ALL</span> members.</p>
</div>
<div className="p-6 bg-slate-50 flex gap-4">
<button onClick={() => setShowDisbandConfirm(null)} className="flex-1 py-4 bg-white text-slate-400 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
<button onClick={() => executeDisbandTeam(showDisbandConfirm.eventId, showDisbandConfirm.teamCode)} disabled={isProcessing} className="flex-1 py-4 bg-rose-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 transition-all shadow-lg shadow-rose-200">{isProcessing ? 'Disbanding...' : 'Confirm'}</button>
</div>
</div>
</div>
</Portal>
)}
</div>
</div>
</Portal>
)}
</div>
);
};
export default RegistrationsView;