382 lines
22 KiB
TypeScript
382 lines
22 KiB
TypeScript
import React from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { Event } from '../types';
|
|
import EventCard from './EventCard';
|
|
import { supabase } from '../supabase';
|
|
|
|
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 [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) => {
|
|
setIsProcessing(true);
|
|
try {
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
if (!user) return;
|
|
|
|
const registrationId = `${user.id}_${eventId}`;
|
|
const { error } = await supabase.from('registrations').update({
|
|
team_code: null,
|
|
team_name: null,
|
|
is_team_leader: false
|
|
}).eq('id', registrationId);
|
|
|
|
if (error) throw error;
|
|
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 {
|
|
const { error } = await supabase.from('registrations').update({
|
|
team_code: null,
|
|
team_name: null,
|
|
is_team_leader: false
|
|
}).eq('event_id', eventId).eq('team_code', code);
|
|
|
|
if (error) throw error;
|
|
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 { data: members, error: fetchErr } = await supabase
|
|
.from('registrations')
|
|
.select('user_id, user_name, reg_no, year, dept, section, is_team_leader')
|
|
.eq('event_id', eventId)
|
|
.eq('team_code', code);
|
|
|
|
if (fetchErr) throw fetchErr;
|
|
|
|
const formattedMembers = members.map((m: any) => ({
|
|
user_id: m.user_id,
|
|
name: m.user_name,
|
|
reg_no: m.reg_no,
|
|
year: m.year,
|
|
department: m.dept,
|
|
section: m.section,
|
|
is_team_leader: 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-12 md:px-24 animate-in fade-in duration-500">
|
|
<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.event_id) === String(event.id));
|
|
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>
|
|
{reg?.team_code ? (
|
|
<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">{reg.team_name}</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">{reg.team_code}</span>
|
|
<button
|
|
onClick={() => handleCopyCode(reg.team_code)}
|
|
className="text-slate-400 hover:text-[#f97316] transition-colors"
|
|
title="Copy Code"
|
|
>
|
|
<i className={`fas ${copyingCode === reg.team_code ? '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 ${reg.is_team_leader ? 'bg-amber-100 text-amber-600' : 'bg-emerald-50 text-emerald-500'} rounded-2xl flex items-center justify-center text-lg shadow-sm border ${reg.is_team_leader ? 'border-amber-200' : 'border-emerald-100'}`}>
|
|
<i className={`fas ${reg.is_team_leader ? 'fa-crown animate-pulse' : 'fa-user-group'}`}></i>
|
|
</div>
|
|
<button
|
|
onClick={() => handleInspectTeam(event.id, reg.team_code, 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 { error } = await supabase.from('registrations').update({ team_code: null, team_name: null, is_team_leader: false }).eq('event_id', showRemoveConfirm.eventId).eq('user_id', showRemoveConfirm.member.user_id);
|
|
if (error) throw error;
|
|
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; |