Implement event registration functionality: Add EventRegistration model to Prisma schema, enabling user event registrations. Enhance EventsPageSection component with registration checks, calendar view, and improved event display. Refactor event rendering logic to separate upcoming and past events, improving user experience.

This commit is contained in:
Julien Froidefond
2025-12-09 21:53:10 +01:00
parent 5ae6cde14e
commit 50a2eaf109
13 changed files with 2483 additions and 61 deletions

View File

@@ -1,5 +1,9 @@
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
interface Event {
id: string;
date: string;
@@ -71,6 +75,365 @@ export default function EventsPageSection({
events,
backgroundImage,
}: EventsPageSectionProps) {
const { data: session } = useSession();
const router = useRouter();
const [registrations, setRegistrations] = useState<Record<string, boolean>>(
{}
);
const [loading, setLoading] = useState<Record<string, boolean>>({});
const [error, setError] = useState<string>("");
const [currentMonth, setCurrentMonth] = useState(new Date());
// Séparer les événements
const upcomingEvents = events.filter(
(e) => e.status === "UPCOMING" || e.status === "LIVE"
);
const pastEvents = events.filter((e) => e.status === "PAST");
// Créer un map des événements par date pour le calendrier
const eventsByDate: Record<string, Event[]> = {};
events.forEach((event) => {
const dateKey = event.date; // YYYY-MM-DD
if (!eventsByDate[dateKey]) {
eventsByDate[dateKey] = [];
}
eventsByDate[dateKey].push(event);
});
// Vérifier les inscriptions au chargement
useEffect(() => {
if (!session?.user?.id) {
return;
}
const checkRegistrations = async () => {
const upcomingOnlyEvents = events.filter((e) => e.status === "UPCOMING");
const registrationChecks = upcomingOnlyEvents.map(async (event) => {
try {
const response = await fetch(`/api/events/${event.id}/register`);
const data = await response.json();
return { eventId: event.id, registered: data.registered || false };
} catch (err) {
console.error("Error checking registration:", err);
return { eventId: event.id, registered: false };
}
});
const results = await Promise.all(registrationChecks);
const registrationsMap: Record<string, boolean> = {};
results.forEach(({ eventId, registered }) => {
registrationsMap[eventId] = registered;
});
setRegistrations(registrationsMap);
};
checkRegistrations();
}, [session?.user?.id, events]);
// Fonctions pour le calendrier
const getDaysInMonth = (date: Date) => {
const year = date.getFullYear();
const month = date.getMonth();
return new Date(year, month + 1, 0).getDate();
};
const getFirstDayOfMonth = (date: Date) => {
const year = date.getFullYear();
const month = date.getMonth();
return new Date(year, month, 1).getDay();
};
const formatMonthYear = (date: Date) => {
return date.toLocaleDateString("fr-FR", {
month: "long",
year: "numeric",
});
};
const renderCalendar = () => {
const daysInMonth = getDaysInMonth(currentMonth);
const firstDay = getFirstDayOfMonth(currentMonth);
const days: (number | null)[] = [];
// Ajouter des jours vides pour le début du mois
for (let i = 0; i < firstDay; i++) {
days.push(null);
}
// Ajouter les jours du mois
for (let day = 1; day <= daysInMonth; day++) {
days.push(day);
}
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth() + 1;
return (
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg p-4 backdrop-blur-sm max-w-2xl mx-auto">
{/* Header du calendrier */}
<div className="flex items-center justify-between mb-3">
<button
onClick={() => {
setCurrentMonth(
new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() - 1
)
);
}}
className="px-2 py-1 border border-pixel-gold/50 text-pixel-gold hover:bg-pixel-gold/10 rounded transition text-sm"
>
</button>
<h3 className="text-base font-bold text-white uppercase tracking-widest">
{formatMonthYear(currentMonth)}
</h3>
<button
onClick={() => {
setCurrentMonth(
new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() + 1
)
);
}}
className="px-2 py-1 border border-pixel-gold/50 text-pixel-gold hover:bg-pixel-gold/10 rounded transition text-sm"
>
</button>
</div>
{/* Jours de la semaine */}
<div className="grid grid-cols-7 gap-0.5 mb-1">
{["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"].map((day) => (
<div
key={day}
className="text-center text-[10px] text-gray-400 font-semibold py-1"
>
{day}
</div>
))}
</div>
{/* Grille du calendrier */}
<div className="grid grid-cols-7 gap-0.5">
{days.map((day, index) => {
if (day === null) {
return <div key={index} className="aspect-square"></div>;
}
const dateKey = `${year}-${String(month).padStart(2, "0")}-${String(
day
).padStart(2, "0")}`;
const dayEvents = eventsByDate[dateKey] || [];
const isToday = new Date().toISOString().split("T")[0] === dateKey;
const hasEvents = dayEvents.length > 0;
// Déterminer la couleur principale selon le type d'événement
const hasUpcoming = dayEvents.some((e) => e.status === "UPCOMING");
const hasLive = dayEvents.some((e) => e.status === "LIVE");
const hasPast = dayEvents.some((e) => e.status === "PAST");
let eventBorderColor = "";
let eventBgColor = "";
if (hasLive) {
eventBorderColor = "border-red-500/80";
eventBgColor = "bg-red-500/20";
} else if (hasUpcoming) {
eventBorderColor = "border-green-500/80";
eventBgColor = "bg-green-500/20";
} else if (hasPast) {
eventBorderColor = "border-gray-500/60";
eventBgColor = "bg-gray-500/15";
}
return (
<div
key={index}
className={`aspect-square border rounded flex flex-col items-center justify-center relative ${
isToday
? "bg-pixel-gold/30 border-pixel-gold/70 border-2"
: hasEvents
? `${eventBgColor} ${eventBorderColor} border-2`
: "border-pixel-gold/10"
} ${
hasEvents ? "ring-2 ring-offset-1 ring-offset-black" : ""
} ${
hasLive
? "ring-red-500/50"
: hasUpcoming
? "ring-green-500/50"
: ""
}`}
>
<div
className={`text-[11px] font-bold ${
isToday
? "text-pixel-gold"
: hasEvents
? "text-white"
: "text-gray-400"
}`}
>
{day}
</div>
{hasEvents && (
<div className="absolute bottom-0.5 left-0 right-0 flex justify-center gap-0.5">
{dayEvents.slice(0, 3).map((event) => (
<div
key={event.id}
className={`w-1 h-1 rounded-full ${
event.status === "UPCOMING"
? "bg-green-400"
: event.status === "LIVE"
? "bg-red-400 animate-pulse"
: "bg-gray-400"
}`}
title={event.name}
/>
))}
{dayEvents.length > 3 && (
<div className="w-1 h-1 rounded-full bg-gray-400" />
)}
</div>
)}
</div>
);
})}
</div>
</div>
);
};
const renderEventCard = (event: Event) => (
<div
key={event.id}
className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm hover:border-pixel-gold/50 transition group"
>
{/* Event Header */}
<div
className={`h-2 bg-gradient-to-r ${getEventTypeColor(event.type)}`}
></div>
{/* Event Content */}
<div className="p-6">
{/* Status Badge */}
<div className="flex justify-between items-start mb-4">
{getStatusBadge(event.status)}
<span className="text-pixel-gold text-xs uppercase tracking-widest">
{getEventTypeLabel(event.type)}
</span>
</div>
{/* Date */}
<div className="text-white text-sm font-bold uppercase tracking-widest mb-3">
{event.date}
</div>
{/* Event Name */}
<h3 className="text-xl font-bold text-white mb-3 group-hover:text-pixel-gold transition">
{event.name}
</h3>
{/* Description */}
<p className="text-gray-400 text-sm leading-relaxed mb-4">
{event.description}
</p>
{/* Action Button */}
{event.status === "UPCOMING" && (
<>
{registrations[event.id] ? (
<button
onClick={() => handleUnregister(event.id)}
disabled={loading[event.id]}
className="w-full px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading[event.id] ? "Annulation..." : "Inscrit ✓"}
</button>
) : (
<button
onClick={() => handleRegister(event.id)}
disabled={loading[event.id]}
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
</button>
)}
</>
)}
{event.status === "LIVE" && (
<button className="w-full px-4 py-2 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-xs tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
Rejoindre en direct
</button>
)}
{event.status === "PAST" && (
<button className="w-full px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-500 uppercase text-xs tracking-widest rounded cursor-not-allowed">
Événement terminé
</button>
)}
</div>
</div>
);
const handleRegister = async (eventId: string) => {
if (!session?.user?.id) {
router.push("/login");
return;
}
setLoading((prev) => ({ ...prev, [eventId]: true }));
setError("");
try {
const response = await fetch(`/api/events/${eventId}/register`, {
method: "POST",
});
const data = await response.json();
if (!response.ok) {
setError(data.error || "Une erreur est survenue");
return;
}
setRegistrations((prev) => ({
...prev,
[eventId]: true,
}));
} catch (err) {
setError("Une erreur est survenue");
} finally {
setLoading((prev) => ({ ...prev, [eventId]: false }));
}
};
const handleUnregister = async (eventId: string) => {
setLoading((prev) => ({ ...prev, [eventId]: true }));
setError("");
try {
const response = await fetch(`/api/events/${eventId}/register`, {
method: "DELETE",
});
if (!response.ok) {
const data = await response.json();
setError(data.error || "Une erreur est survenue");
return;
}
setRegistrations((prev) => ({
...prev,
[eventId]: false,
}));
} catch (err) {
setError("Une erreur est survenue");
} finally {
setLoading((prev) => ({ ...prev, [eventId]: false }));
}
};
return (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
{/* Background Image */}
@@ -109,66 +472,51 @@ export default function EventsPageSection({
</p>
</div>
{/* Events Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{events.map((event) => (
<div
key={event.id}
className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm hover:border-pixel-gold/50 transition group"
>
{/* Event Header */}
<div
className={`h-2 bg-gradient-to-r ${getEventTypeColor(
event.type
)}`}
></div>
{/* Event Content */}
<div className="p-6">
{/* Status Badge */}
<div className="flex justify-between items-start mb-4">
{getStatusBadge(event.status)}
<span className="text-pixel-gold text-xs uppercase tracking-widest">
{getEventTypeLabel(event.type)}
</span>
</div>
{/* Date */}
<div className="text-white text-sm font-bold uppercase tracking-widest mb-3">
{event.date}
</div>
{/* Event Name */}
<h3 className="text-xl font-bold text-white mb-3 group-hover:text-pixel-gold transition">
{event.name}
</h3>
{/* Description */}
<p className="text-gray-400 text-sm leading-relaxed mb-4">
{event.description}
</p>
{/* Action Button */}
{event.status === "UPCOMING" && (
<button className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition">
S'inscrire maintenant
</button>
)}
{event.status === "LIVE" && (
<button className="w-full px-4 py-2 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-xs tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
Rejoindre en direct
</button>
)}
{event.status === "PAST" && (
<button className="w-full px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-500 uppercase text-xs tracking-widest rounded cursor-not-allowed">
Événement terminé
</button>
)}
</div>
{/* Événements à venir */}
{upcomingEvents.length > 0 && (
<div className="mb-16">
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
<span className="bg-gradient-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
Événements à venir
</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{upcomingEvents.map(renderEventCard)}
</div>
))}
</div>
)}
{/* Calendrier */}
<div className="mb-16">
<h2 className="text-2xl font-bold text-white mb-6 text-center uppercase tracking-widest">
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
Calendrier
</span>
</h2>
{renderCalendar()}
</div>
{/* Événements passés */}
{pastEvents.length > 0 && (
<div className="mb-16">
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
<span className="bg-gradient-to-r from-gray-400 to-gray-600 bg-clip-text text-transparent">
Événements passés
</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{pastEvents.map(renderEventCard)}
</div>
</div>
)}
{/* Error Message */}
{error && (
<div className="mt-6 text-center">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
{/* Footer Info */}
<div className="mt-12 text-center">
<p className="text-gray-500 text-sm">