194 lines
7.1 KiB
TypeScript
194 lines
7.1 KiB
TypeScript
// app/events/[id]/page.tsx
|
|
// Event detail page with registration form
|
|
|
|
import { query } from '@/lib/db';
|
|
import { Event, EventRegistration } from '@/types/racing';
|
|
import { notFound } from 'next/navigation';
|
|
import { TrophyIcon, MapPinIcon, UsersIcon } from '@/components/ui/icons';
|
|
import EventRegistrationForm from '@/components/events/EventRegistrationForm';
|
|
|
|
async function getEvent(eventId: number): Promise<Event | null> {
|
|
const sql = `
|
|
SELECT e.*, COUNT(er.registration_id) as registrations_count
|
|
FROM events e
|
|
LEFT JOIN event_registrations er ON e.event_id = er.event_id AND er.status = 'REGISTERED'
|
|
WHERE e.event_id = $1
|
|
GROUP BY e.event_id
|
|
`;
|
|
|
|
const rows = await query(sql, [eventId]);
|
|
return rows.length > 0 ? rows[0] as Event : null;
|
|
}
|
|
|
|
async function getEventRegistrations(eventId: number): Promise<EventRegistration[]> {
|
|
const sql = `
|
|
SELECT
|
|
er.*,
|
|
u.driver_name
|
|
FROM event_registrations er
|
|
JOIN users u ON er.driver_guid = u.driver_guid
|
|
WHERE er.event_id = $1 AND er.status = 'REGISTERED'
|
|
ORDER BY er.registration_date ASC
|
|
`;
|
|
|
|
const rows = await query(sql, [eventId]);
|
|
return rows as EventRegistration[];
|
|
}
|
|
|
|
function formatDate(date: Date): string {
|
|
return new Date(date).toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
export default async function EventDetailPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>;
|
|
}) {
|
|
const { id } = await params;
|
|
const eventId = parseInt(id);
|
|
|
|
const event: any = await getEvent(eventId);
|
|
if (!event) {
|
|
notFound();
|
|
}
|
|
|
|
const registrations = await getEventRegistrations(eventId);
|
|
|
|
const isOpen = event.event_status === 'OPEN';
|
|
const isFull = event.registrations_count >= event.max_participants;
|
|
const deadlinePassed = event.registration_deadline && new Date(event.registration_deadline) < new Date();
|
|
const canRegister = isOpen && !isFull && !deadlinePassed;
|
|
|
|
return (
|
|
<>
|
|
{/* Hero */}
|
|
<div className="relative border-b border-white/10 grid-overlay">
|
|
<div className="max-w-7xl mx-auto px-6 py-16">
|
|
<div className="space-y-4">
|
|
<div className="inline-flex items-center space-x-2 px-3 py-1 border border-white/20 bg-black">
|
|
<TrophyIcon className="w-4 h-4" />
|
|
<span className="text-xs font-medium tracking-wider">EVENT DETAILS</span>
|
|
</div>
|
|
<h1 className="text-5xl font-bold tracking-tight">
|
|
{event.event_name}
|
|
</h1>
|
|
<p className="text-white/70 text-lg max-w-3xl">
|
|
{event.event_description}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Event Info Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-12">
|
|
<InfoCard label="TRACK" value={event.event_track} icon={<MapPinIcon className="w-6 h-6" />} />
|
|
<InfoCard label="DATE" value={formatDate(event.event_date)} icon="📅" />
|
|
<InfoCard label="DURATION" value={`${event.event_duration || 'TBD'} min`} icon="⏱️" />
|
|
<InfoCard
|
|
label="PARTICIPANTS"
|
|
value={`${event.registrations_count}/${event.max_participants}`}
|
|
icon={<UsersIcon className="w-6 h-6" />}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="max-w-7xl mx-auto px-6 py-12">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Left Column - Registration */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{/* Registration Form */}
|
|
{canRegister ? (
|
|
<div className="border border-white/10 bg-black p-6">
|
|
<h2 className="text-2xl font-bold tracking-tight mb-6">REGISTER FOR EVENT</h2>
|
|
<EventRegistrationForm eventId={event.event_id} />
|
|
</div>
|
|
) : (
|
|
<div className="border border-white/10 bg-black p-6">
|
|
<h2 className="text-2xl font-bold tracking-tight mb-4">REGISTRATION CLOSED</h2>
|
|
<p className="text-white/60">
|
|
{isFull && 'This event is full.'}
|
|
{deadlinePassed && !isFull && 'Registration deadline has passed.'}
|
|
{event.event_status === 'CLOSED' && !isFull && !deadlinePassed && 'Registration is closed for this event.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Event Rules */}
|
|
{event.event_rules && (
|
|
<div className="border border-white/10 bg-black p-6 topo-lines-dense">
|
|
<h3 className="text-xl font-bold tracking-tight mb-4">EVENT RULES</h3>
|
|
<div className="text-white/70 text-sm space-y-2 whitespace-pre-line">
|
|
{event.event_rules}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right Column - Registered Participants */}
|
|
<div className="lg:col-span-1">
|
|
<div className="border border-white/10 bg-black p-6 sticky top-20">
|
|
<h3 className="text-xl font-bold tracking-tight mb-4">
|
|
REGISTERED DRIVERS ({event.registrations_count})
|
|
</h3>
|
|
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
|
{registrations.length === 0 ? (
|
|
<p className="text-white/40 text-sm">No registrations yet</p>
|
|
) : (
|
|
registrations.map((reg: any, index: number) => (
|
|
<div key={reg.registration_id} className="border border-white/10 p-3">
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-xs font-bold text-white/40">
|
|
#{String(index + 1).padStart(2, '0')}
|
|
</span>
|
|
<span className="font-semibold text-sm">{reg.driver_name}</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1 text-xs text-white/60">
|
|
<div>Car: <span className="text-white/80 font-mono">{reg.car_model}</span></div>
|
|
{reg.car_skin && <div>Skin: <span className="text-white/80">{reg.car_skin}</span></div>}
|
|
{reg.team_name && <div>Team: <span className="text-white/80">{reg.team_name}</span></div>}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function InfoCard({
|
|
label,
|
|
value,
|
|
icon
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
icon: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="border border-white/10 p-4 sharp-border">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-xs font-bold tracking-wider text-white/60">{label}</span>
|
|
<div className="text-white/40 text-sm">
|
|
{icon}
|
|
</div>
|
|
</div>
|
|
<div className="text-lg font-bold tracking-tight">
|
|
{value}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|