Convert backends to Firebase and combine projects

This commit is contained in:
2026-06-18 14:07:24 +05:30
commit 0a76feafc5
147 changed files with 35104 additions and 0 deletions

View File

@@ -0,0 +1,559 @@
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { toPng } from 'html-to-image';
import { Event } from '../types';
import { supabase } from '../supabase';
import { CLUBS } from '../constants';
interface EventCardProps {
event: Event;
isBooked: boolean;
onToggle: () => void;
onTrackStatus?: (event: Event) => void;
currentUserName?: string;
userRole?: string;
registration?: any;
}
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return createPortal(children, document.body);
};
const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrackStatus, currentUserName = 'Student', userRole, registration }) => {
const [showConfirm, setShowConfirm] = useState(false);
const clubInfo = useMemo(() => CLUBS.find(c => c.name === event.club), [event.club]);
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
const [showTicket, setShowTicket] = useState(false);
const [showSummary, setShowSummary] = useState(false);
const [userDept, setUserDept] = useState<string>('');
const [userYear, setUserYear] = useState<string>('');
const [userSection, setUserSection] = useState<string>('');
const [now, setNow] = useState(new Date());
const [isDownloading, setIsDownloading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const ticketRef = useRef<HTMLDivElement>(null);
const [persistedTicket, setPersistedTicket] = useState<{id: string, qr: string} | null>(null);
useEffect(() => {
if (showTicket && registration?.id) {
const initializeTicket = async () => {
// 1. Check if already persisted
if (registration.ticket_id && registration.ticket_qrcode) {
setPersistedTicket({ id: registration.ticket_id, qr: registration.ticket_qrcode });
return;
}
// 2. Otherwise generate and save
try {
const newId = `RIT-EVT-${Math.random().toString(36).substring(2, 6).toUpperCase()}-${registration.id.substring(0, 4).toUpperCase()}`;
const newQr = `${window.location.origin}/?verify=${registration.id}`;
const { error } = await supabase
.from('registrations')
.update({ ticket_id: newId, ticket_qrcode: newQr })
.eq('id', registration.id);
if (!error) {
setPersistedTicket({ id: newId, qr: newQr });
}
} catch (err) {
console.error("Ticket initialization failed:", err);
}
};
initializeTicket();
}
}, [showTicket, registration?.id]);
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000);
supabase.auth.getUser().then(({ data: { user } }) => {
if (user) {
supabase.from('Studentusers').select('department, year, section').eq('id', user.id).single()
.then(({ data }) => {
if (data?.department) {
setUserDept(data.department);
setUserYear(data.year || '');
setUserSection(data.section || '');
} else {
supabase.from('externalusers').select('department, year, section').eq('id', user.id).single()
.then(({ data: extData }) => {
if (extData?.department) {
setUserDept(extData.department);
setUserYear(extData.year || '');
setUserSection(extData.section || '');
}
});
}
});
}
});
return () => clearInterval(timer);
}, []);
const admissionStatus = useMemo(() => {
if (event.status === 'Completed') return 'EVENT_ENDED';
if (event.registrationDeadline) {
const deadline = new Date(event.registrationDeadline);
if (now > deadline) return 'DEADLINE_PASSED';
}
if (userDept && event.deptLimits?.[userDept]) {
const sectionLimits = event.deptSectionLimits?.[userDept];
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
if (hasSectionLimits) {
const limit = sectionLimits[userSection];
if (!limit || limit <= 0) {
return 'SECTION_NOT_ALLOWED';
}
const currentSectionCount = event.currentDeptSectionCounts?.[userDept]?.[userSection] || 0;
if (currentSectionCount >= limit) {
return 'SECTION_FULL';
}
} else {
const currentDeptCount = event.currentDeptCounts?.[userDept] || 0;
if (currentDeptCount >= event.deptLimits[userDept]) return 'DEPT_FULL';
}
}
if (event.maxParticipants && (event.currentParticipants || 0) >= event.maxParticipants) {
return 'TOTAL_FULL';
}
return 'OPEN';
}, [event, now, userDept, userSection]);
const timeLeft = useMemo(() => {
const targetDate = new Date(event.date).getTime();
const distance = targetDate - now.getTime();
if (isNaN(targetDate) || distance <= 0) return { d: '00', h: '00', m: '00', s: '00' };
return {
d: Math.floor(distance / (1000 * 60 * 60 * 24)).toString().padStart(2, '0'),
h: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)).toString().padStart(2, '0'),
m: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)).toString().padStart(2, '0'),
s: Math.floor((distance % (1000 * 60)) / 1000).toString().padStart(2, '0')
};
}, [event.date, now]);
const handleDownload = async () => {
if (!ticketRef.current) return;
setIsDownloading(true);
try {
const dataUrl = await toPng(ticketRef.current, { cacheBust: true, quality: 1, backgroundColor: '#1e293b' });
const link = document.createElement('a');
link.download = `Pass_${event.title.replace(/\s+/g, '_')}.png`;
link.href = dataUrl;
link.click();
} catch (err) {
console.error("Capture failed:", err);
} finally {
setIsDownloading(false);
}
};
const verificationLink = `${window.location.origin}/?verify=local_${event.id}`;
return (
<div className="bg-white rounded-[40px] overflow-hidden shadow-xl shadow-gray-900/5 transition-all duration-500 hover:-translate-y-2 flex flex-col h-full group border border-gray-200">
<div className="h-64 relative overflow-hidden">
<img src={event.image} alt={event.title} className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110" />
<div className="absolute top-6 left-6 flex flex-col gap-3">
<div className="bg-white px-5 py-2 rounded-2xl text-[10px] font-black uppercase tracking-widest text-[#f97316] shadow-xl border border-gray-200">{event.category}</div>
{event.registrationDeadline && (
<div className={`px-5 py-2 rounded-2xl text-[9px] font-black uppercase tracking-widest text-white shadow-xl flex items-center gap-2 ${admissionStatus === 'DEADLINE_PASSED' ? 'bg-[#1A202C]' : 'bg-rose-600 animate-pulse'}`}>
<i className="fas fa-clock"></i>
{admissionStatus === 'DEADLINE_PASSED' ? 'Closed' : `Ends ${new Date(event.registrationDeadline).toLocaleDateString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`}
</div>
)}
</div>
</div>
<div className="p-10 flex flex-col flex-1">
<div className="flex justify-between items-start mb-6">
<div className="flex-1">
{clubInfo && (
<div className="flex items-center gap-2 mb-2">
<img
src={clubInfo.image}
alt={clubInfo.name}
className="w-12 h-12 rounded-full object-cover border border-gray-100 shadow-sm"
referrerPolicy="no-referrer"
/>
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{clubInfo.name}</span>
</div>
)}
<h3 className="text-3xl font-black text-[#1A202C] line-clamp-2 uppercase tracking-tight group-hover:text-[#f97316] transition-colors">{event.title}</h3>
</div>
<div className={`px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest border ${event.pricingType === 'PAID' ? 'bg-rose-50 border-rose-100 text-rose-600' : 'bg-emerald-50 border-emerald-100 text-emerald-600'}`}>{event.pricingType}</div>
</div>
<div className="space-y-3 mb-8 text-gray-400 font-bold text-xs uppercase tracking-widest">
<div className="flex items-center gap-4">
<i className="far fa-calendar-alt w-4 text-[#f97316]"></i>
<span>{event.date}{event.schedule?.[0]?.start_time ? `${event.schedule[0].start_time}` : ''}</span>
</div>
{event.durationDays && event.durationDays > 1 && (
<div className="flex items-center gap-4">
<i className="fas fa-hourglass-half w-4 text-[#f97316]"></i>
<span>Duration: {event.durationDays} Days</span>
</div>
)}
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4">
<i className="fas fa-location-dot w-4 text-[#f97316]"></i>
<span>{event.location}</span>
</div>
{event.event_summary && (
<button
onClick={() => setShowSummary(true)}
className="w-7 h-7 flex items-center justify-center rounded-xl bg-slate-50 border border-slate-200 text-slate-400 hover:bg-[#f97316] hover:text-white hover:border-[#f97316] transition-all shadow-sm group"
title="View summary"
>
<i className="fas fa-info text-[10px] group-hover:scale-110 transition-transform"></i>
</button>
)}
</div>
</div>
{event.schedule && event.schedule.length > 0 && (() => {
// Group schedule entries by day_idx
const dayMap = new Map<number, typeof event.schedule>();
event.schedule!.forEach(s => {
if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []);
dayMap.get(s.day_idx)!.push(s);
});
const sortedDays = Array.from(dayMap.entries()).sort((a, b) => a[0] - b[0]);
return (
<div className="mb-8 space-y-4">
<span className="text-[9px] font-black text-gray-400 uppercase tracking-widest block mb-2">Event Schedule</span>
<div className="space-y-4">
{sortedDays.map(([dayIdx, slots]) => (
<div key={dayIdx} className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-[10px] font-black text-[#1A202C] uppercase tracking-widest">Day {dayIdx}</span>
<span className="text-[9px] font-bold text-gray-400 uppercase">{slots![0]?.date}</span>
</div>
<div className="grid grid-cols-2 gap-2">
{slots!.map((slot) => (
<div key={slot.id} className="bg-gray-50 border border-gray-100 rounded-xl px-3 py-2 flex flex-col">
<span className="text-[8px] font-black text-[#f97316] uppercase tracking-tighter">Batch {slot.batch_idx}</span>
<span className="text-[10px] font-bold text-gray-600">{slot.start_time} - {slot.end_time}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
})()}
<div className="mb-8 p-6 bg-gray-50 rounded-3xl border border-gray-200">
<div className="flex justify-between items-center mb-3">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Global Seats</span>
<span className="text-xs font-black text-[#1A202C]">{event.currentParticipants || 0} / {event.maxParticipants || (event.deptLimits && Object.keys(event.deptLimits).length > 0 ? Object.values(event.deptLimits).map(Number).reduce((a, b) => a + b, 0) : '∞')}</span>
</div>
{userDept && event.deptLimits?.[userDept] && (() => {
const sectionLimits = event.deptSectionLimits?.[userDept];
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
if (hasSectionLimits) {
const limit = sectionLimits[userSection] || 0;
const current = event.currentDeptSectionCounts?.[userDept]?.[userSection] || 0;
return (
<div className="flex justify-between items-center pt-3 border-t border-gray-200">
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{userDept} Sec {userSection || 'N/A'} Allocation</span>
<span className={`text-xs font-black ${(admissionStatus === 'SECTION_FULL' || admissionStatus === 'SECTION_NOT_ALLOWED') ? 'text-rose-500' : 'text-[#1A202C]'}`}>
{limit > 0 ? `${current} / ${limit}` : 'RESTRICTED'}
</span>
</div>
);
}
return (
<div className="flex justify-between items-center pt-3 border-t border-gray-200">
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{userDept} Allocation</span>
<span className={`text-xs font-black ${admissionStatus === 'DEPT_FULL' ? 'text-rose-500' : 'text-[#1A202C]'}`}>
{event.currentDeptCounts?.[userDept] || 0} / {event.deptLimits[userDept]}
</span>
</div>
);
})()}
</div>
<div className="flex justify-between items-center mb-10 text-center">
{[ {l: 'D', v: timeLeft.d}, {l: 'H', v: timeLeft.h}, {l: 'M', v: timeLeft.m}, {l: 'S', v: timeLeft.s} ].map((t, i) => (
<div key={i} className="flex-1">
<span className={`block text-2xl font-black leading-none ${i === 3 ? 'text-[#f97316]' : 'text-[#1A202C]'}`}>{t.v}</span>
<span className="text-[9px] font-black text-gray-400 uppercase tracking-widest">{t.l}</span>
</div>
))}
</div>
<div className="mt-auto">
{userRole === 'ADMIN' || userRole === 'COORDINATOR' ? (
<button disabled className="w-full py-5 rounded-[2rem] bg-slate-100 border border-slate-200 text-slate-400 font-black uppercase tracking-[0.3em] text-xs cursor-not-allowed">
View Only ({userRole === 'ADMIN' ? 'Admin' : 'Coordinator'})
</button>
) : isBooked ? (
<div className="flex flex-col gap-4">
<button onClick={() => onTrackStatus?.(event)} className="w-full py-5 rounded-[2rem] bg-[#f97316] text-white font-black uppercase tracking-[0.3em] text-xs shadow-xl shadow-orange-100 flex items-center justify-center gap-3 active:scale-[0.98] transition-all">
Track Progress <i className="fas fa-arrow-right"></i>
</button>
{event.status !== 'Completed' && (
<button onClick={() => setShowTicket(true)} className="w-full py-5 rounded-[2rem] bg-[#1A202C] text-white font-black uppercase tracking-[0.3em] text-xs hover:bg-black transition-all active:scale-[0.98] shadow-xl shadow-gray-200 flex items-center justify-center gap-3">
View Ticket <i className="fas fa-ticket"></i>
</button>
)}
{event.status !== 'Event Ongoing' && event.status !== 'Completed' && (
<button onClick={() => setShowCancelConfirm(true)} className="w-full py-4 text-rose-500 font-black uppercase tracking-widest text-[9px] hover:bg-rose-50 rounded-2xl transition-all flex items-center justify-center gap-2 mt-2">
<i className="fas fa-times-circle"></i> Cancel Registration
</button>
)}
</div>
) : admissionStatus === 'OPEN' ? (
<button onClick={() => setShowConfirm(true)} className="w-full py-5 rounded-[2rem] bg-[#1A202C] text-white font-black uppercase tracking-[0.3em] text-xs hover:bg-black transition-all active:scale-95 shadow-xl shadow-gray-200">
Get Tickets
</button>
) : (
<button disabled className="w-full py-5 rounded-[2rem] bg-rose-50 border border-rose-100 text-rose-400 font-black uppercase tracking-[0.3em] text-xs cursor-not-allowed">
{admissionStatus === 'EVENT_ENDED'
? 'EVENT ENDED'
: admissionStatus === 'DEPT_FULL'
? 'THE SEATS ARE FULL'
: admissionStatus === 'DEADLINE_PASSED'
? 'Deadline Over'
: admissionStatus === 'SECTION_FULL'
? 'SEC SEATS FULL'
: admissionStatus === 'SECTION_NOT_ALLOWED'
? 'SEC RESTRICTED'
: 'Event Full'}
</button>
)}
</div>
</div>
{showConfirm && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
<div className="bg-white rounded-[3rem] w-full max-w-sm p-12 text-center shadow-2xl animate-in zoom-in-95">
<div className={`w-20 h-20 ${event.isTeamEvent ? 'bg-orange-50 text-orange-500' : 'bg-blue-50 text-blue-500'} rounded-full flex items-center justify-center text-3xl mx-auto mb-8`}>
<i className={`fas ${event.isTeamEvent ? 'fa-users' : 'fa-ticket'}`}></i>
</div>
<h3 className="text-2xl font-black text-[#1A202C] uppercase mb-4">
{event.isTeamEvent ? 'Team Registration' : 'Confirm Pass?'}
</h3>
{event.isTeamEvent ? (
<div className="space-y-6 mb-10">
<div className="bg-orange-50 border border-orange-100 rounded-2xl p-6 text-left">
<p className="text-[10px] font-black text-orange-600 uppercase tracking-widest mb-3 flex items-center gap-2">
<i className="fas fa-exclamation-triangle"></i> Important Notice
</p>
<p className="text-xs text-orange-800 font-bold leading-relaxed lowercase first-letter:uppercase">
This is a <span className="underline">team-based event</span>. You are about to register as an individual, after which you must <span className="underline">create or join a team</span> in the registrations section to participate.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-50 border border-slate-100 rounded-xl p-3">
<span className="block text-[8px] font-black text-gray-400 uppercase tracking-widest mb-1">Max Size</span>
<span className="text-xs font-black text-slate-900">{event.teamSizeLimit || '∞'} Members</span>
</div>
<div className="bg-slate-50 border border-slate-100 rounded-xl p-3">
<span className="block text-[8px] font-black text-gray-400 uppercase tracking-widest mb-1">Type</span>
<span className="text-xs font-black text-slate-900">{event.teamComposition === 'INTER_DEPT' ? 'Inter-Dept' : 'Mixed'}</span>
</div>
</div>
<p className="text-gray-500 text-[10px] font-bold uppercase tracking-widest">
Proceed with registration for <span className="text-slate-900 underline">"{event.title}"</span>?
</p>
</div>
) : (
<p className="text-gray-500 text-sm mb-10 lowercase first-letter:uppercase">Register for <span className="font-bold">"{event.title}"</span>?</p>
)}
<div className="flex gap-4">
<button onClick={() => setShowConfirm(false)} className="flex-1 py-4 bg-gray-100 text-gray-500 rounded-2xl font-black uppercase tracking-widest text-[10px] hover:bg-gray-200 transition-all">Cancel</button>
<button onClick={() => { onToggle(); setShowConfirm(false); }} className={`flex-1 py-4 ${event.isTeamEvent ? 'bg-orange-500 shadow-orange-200' : 'bg-[#1A202C] shadow-gray-200'} text-white rounded-2xl font-black uppercase tracking-widest text-[10px] shadow-lg active:scale-95 transition-all`}>
{event.isTeamEvent ? 'I Understand' : 'Confirm'}
</button>
</div>
</div>
</div>
</Portal>
)}
{showCancelConfirm && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
<div className="bg-white rounded-[3rem] w-full max-w-sm p-12 text-center shadow-2xl animate-in zoom-in-95">
<div className="w-20 h-20 bg-rose-50 text-rose-500 rounded-full flex items-center justify-center text-3xl mx-auto mb-8"><i className="fas fa-calendar-xmark"></i></div>
<h3 className="text-2xl font-black text-[#1A202C] uppercase mb-4">Cancel Booking?</h3>
<p className="text-gray-500 text-sm mb-10">Are you sure you want to cancel your registration for <span className="font-bold">"{event.title}"</span>?</p>
<div className="flex flex-col gap-3">
<button onClick={() => { onToggle(); setShowCancelConfirm(false); }} className="w-full py-4 bg-rose-600 text-white rounded-2xl font-black uppercase tracking-widest text-[10px] shadow-lg shadow-rose-200">Yes, Cancel Registration</button>
<button onClick={() => setShowCancelConfirm(false)} className="w-full py-4 bg-gray-100 text-gray-500 rounded-2xl font-black uppercase tracking-widest text-[10px]">Keep My Booking</button>
</div>
</div>
</div>
</Portal>
)}
{showTicket && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 md:p-6 bg-slate-900/70 backdrop-blur-md animate-in fade-in duration-300">
<div ref={ticketRef} className="relative bg-white rounded-[3rem] w-full max-w-md overflow-hidden shadow-[0_50px_100px_rgba(0,0,0,0.3)] animate-in zoom-in-95 duration-500 border border-white/20">
{/* Header - Now White Theme as Requested */}
<div className="p-8 border-b border-gray-100 flex justify-between items-center bg-white">
<div className="flex items-center gap-4">
<div className="w-[3px] h-10 bg-[#f97316] rounded-full"></div>
<img
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
alt="RIT Logo"
className="h-10 w-auto object-contain"
/>
<div className="flex flex-col leading-none">
<span className="text-[8px] font-black text-gray-400 uppercase tracking-[0.2em] mt-1">Secure Entry Pass</span>
</div>
</div>
<button onClick={() => setShowTicket(false)} className="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-all"><i className="fas fa-times"></i></button>
</div>
<div className="p-10">
<div className="flex flex-col items-center mb-10">
<div className="w-48 h-48 bg-gray-50 rounded-[2.5rem] flex items-center justify-center mb-8 p-6 border border-gray-100 shadow-inner relative group">
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(persistedTicket?.qr || verificationLink)}&color=1e293b`}
alt="QR"
className="w-full h-full object-contain"
/>
{persistedTicket?.id && (
<div className="absolute -bottom-3 bg-white px-4 py-1 rounded-full border border-gray-200 shadow-sm">
<span className="text-[8px] font-black text-gray-400 uppercase tracking-widest">{persistedTicket.id}</span>
</div>
)}
</div>
<h4 className="text-3xl font-black text-[#1A202C] text-center mb-2 tracking-tighter uppercase leading-none">{event.title}</h4>
<div className="w-12 h-1 bg-[#f97316] rounded-full"></div>
</div>
<div className="grid grid-cols-2 gap-y-8 gap-x-10 border-t border-dashed border-gray-200 pt-8 mb-10">
<div>
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Attendee</span>
<p className="text-sm font-black text-[#1A202C] truncate uppercase">{currentUserName}</p>
</div>
<div>
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Year / Section</span>
<p className="text-sm font-black text-[#1A202C] truncate uppercase">
{registration?.year || userYear || 'N/A'} - {registration?.section || userSection || 'N/A'}
</p>
</div>
{event.isTeamEvent && registration?.team_name && (
<div className="col-span-2">
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Team Identity</span>
<p className="text-sm font-black text-[#f97316] uppercase tracking-tight">{registration.team_name}</p>
</div>
)}
<div>
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Event Date</span>
<p className="text-sm font-black text-[#1A202C] truncate uppercase">{event.date.split(',')[0]}</p>
</div>
<div>
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Dept</span>
<p className="text-sm font-black text-[#1A202C] truncate uppercase">{registration?.dept || userDept || 'Student'}</p>
</div>
</div>
<div className="space-y-4">
<button
onClick={async () => {
setIsSaving(true);
await handleDownload();
// Handle bucket storage as requested: "once the student got the ticket then it must store in that bucket"
if (ticketRef.current && registration?.id) {
try {
const dataUrl = await toPng(ticketRef.current, { quality: 1, backgroundColor: '#ffffff' });
const fileName = `Ticket_${registration.id}.png`;
// Using 'ticket_views' bucket created in DB migration
const { uploadToSupabase: upload } = await import('../supabase');
await upload(dataUrl, fileName, 'ticket_views');
} catch (err) {
console.error("Auto-archiving to bucket failed:", err);
}
}
setIsSaving(false);
}}
disabled={isDownloading || isSaving}
className="w-full py-5 bg-[#1A202C] text-white rounded-[2rem] font-black uppercase text-xs tracking-[0.3em] flex items-center justify-center gap-4 hover:bg-black transition-all shadow-xl shadow-gray-200 disabled:opacity-50"
>
{isDownloading || isSaving ? <><i className="fas fa-spinner fa-spin"></i> Processing...</> : <><i className="fas fa-download"></i> Save Pass</>}
</button>
<p className="text-[8px] text-center font-bold text-gray-400 uppercase tracking-widest">Digital Ticket ID: {persistedTicket?.id || 'GENERATING...'}</p>
</div>
</div>
<div className="bg-[#f97316] h-3 w-full"></div>
</div>
</div>
</Portal>
)}
{showSummary && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 sm:p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[2rem] w-full max-w-lg shadow-2xl animate-in zoom-in-95 duration-500 max-h-[85vh] flex flex-col relative">
<button
onClick={() => setShowSummary(false)}
className="absolute top-6 right-6 w-8 h-8 rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-all flex items-center justify-center z-10"
>
<i className="fas fa-times text-sm"></i>
</button>
<div className="p-8 overflow-y-auto flex-1 custom-scrollbar">
<div className="flex items-center gap-4 mb-8 pb-6 border-b border-gray-100 pr-8">
{clubInfo && (
<img src={clubInfo.image} className="w-12 h-12 rounded-full object-cover border border-slate-200 shadow-sm" alt="" />
)}
<div>
<span className="block text-[10px] font-black text-[#f97316] uppercase tracking-[0.2em] mb-1">{event.club || 'Organized by'}</span>
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2">
<i className="fas fa-user text-[9px]"></i> {event.coordinator}
</span>
</div>
</div>
<div className="space-y-6 pb-6 mt-6 border-t border-gray-100 pt-6">
<div className="flex items-center gap-2 mb-2">
<div className="w-1 h-3 bg-[#f97316] rounded-full"></div>
<h4 className="text-[10px] font-black text-slate-800 uppercase tracking-[0.2em]">Summary</h4>
</div>
<div className="relative pt-2">
<i className="fas fa-quote-right absolute top-0 right-0 text-5xl text-slate-50 pointer-events-none -z-10"></i>
<p className="text-[14px] text-slate-600 leading-relaxed whitespace-pre-wrap font-medium">
{event.event_summary || "Details for this session will be provided soon."}
</p>
</div>
</div>
<div className="mt-4 shrink-0 border-t border-gray-100 pt-6">
<button
onClick={() => setShowSummary(false)}
className="w-full py-4 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-black transition-all shadow-xl shadow-slate-200 focus:outline-none focus:ring-4 focus:ring-slate-100"
>
Close Summary
</button>
</div>
</div>
</div>
</div>
</Portal>
)}
</div>
);
};
export default EventCard;