572 lines
32 KiB
TypeScript
572 lines
32 KiB
TypeScript
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.registrationDeadline && (
|
|
<div className={`flex items-center gap-4 font-black ${admissionStatus === 'DEADLINE_PASSED' ? 'text-rose-600 animate-pulse' : 'text-emerald-600'}`}>
|
|
<i className="fas fa-clock w-4 text-[#f97316]"></i>
|
|
<span>
|
|
{admissionStatus === 'DEADLINE_PASSED' ? 'Deadline Passed' : 'Register Before'}: {new Date(event.registrationDeadline).toLocaleDateString([], {
|
|
month: 'short',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})}
|
|
</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; |