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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user