"use client"; import { useState, useEffect, useMemo, useRef, useTransition } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { calculateEventStatus } from "@/lib/eventStatus"; import FeedbackModal from "@/components/feedback/FeedbackModal"; import { registerForEvent, unregisterFromEvent, } from "@/actions/events/register"; import { Badge, Button, Modal, CloseButton, Card, BackgroundSection, SectionTitle, } from "@/components/ui"; interface Event { id: string; date: string | Date; name: string; description: string; type: "ATELIER" | "KATA" | "PRESENTATION" | "LEARNING_HOUR"; room?: string | null; time?: string | null; maxPlaces?: number | null; } interface EventsPageSectionProps { events: Event[]; backgroundImage: string; initialRegistrations?: Record; } const getEventTypeColor = (type: Event["type"]) => { switch (type) { case "ATELIER": return "from-blue-600 to-cyan-500"; case "KATA": return "from-yellow-600 to-amber-500"; case "PRESENTATION": return "from-purple-600 to-pink-500"; case "LEARNING_HOUR": return "from-green-600 to-emerald-500"; default: return "from-gray-600 to-gray-500"; } }; const getEventTypeLabel = (type: Event["type"]) => { switch (type) { case "ATELIER": return "Atelier"; case "KATA": return "Kata"; case "PRESENTATION": return "Présentation"; case "LEARNING_HOUR": return "Learning Hour"; default: return type; } }; const getStatusBadge = (status: "UPCOMING" | "LIVE" | "PAST") => { switch (status) { case "UPCOMING": return ( À venir ); case "LIVE": return ( En direct ); case "PAST": return ( Passé ); } }; export default function EventsPageSection({ events, backgroundImage, initialRegistrations = {}, }: EventsPageSectionProps) { const { data: session } = useSession(); const router = useRouter(); const [registrations, setRegistrations] = useState>(initialRegistrations); const [loading, setLoading] = useState>({}); const [error, setError] = useState(""); const [currentMonth, setCurrentMonth] = useState(new Date()); const [selectedEvent, setSelectedEvent] = useState(null); const [feedbackEventId, setFeedbackEventId] = useState(null); // Helper function pour obtenir le statut d'un événement const getEventStatus = (event: Event) => calculateEventStatus(event.date); // Déterminer si on a des données initiales valides const hasInitialData = useMemo( () => Object.keys(initialRegistrations).length > 0, [initialRegistrations] ); // Ref pour tracker si on a déjà utilisé les données initiales const hasUsedInitialData = useRef(hasInitialData); // Ref pour tracker si on a déjà fait les appels API const hasFetchedRegistrations = useRef(false); // Séparer et trier les événements (du plus récent au plus ancien) // Le statut est calculé automatiquement en fonction de la date const upcomingEvents = events .filter((e) => { const status = calculateEventStatus(e.date); return status === "UPCOMING" || status === "LIVE"; }) .sort((a, b) => { // Trier par date décroissante (du plus récent au plus ancien) const dateA = typeof a.date === "string" ? new Date(a.date) : a.date; const dateB = typeof b.date === "string" ? new Date(b.date) : b.date; return dateB.getTime() - dateA.getTime(); }); const pastEvents = events .filter((e) => calculateEventStatus(e.date) === "PAST") .sort((a, b) => { // Trier par date décroissante (du plus récent au plus ancien) const dateA = typeof a.date === "string" ? new Date(a.date) : a.date; const dateB = typeof b.date === "string" ? new Date(b.date) : b.date; return dateB.getTime() - dateA.getTime(); }); // Créer un map des événements par date pour le calendrier const eventsByDate: Record = {}; events.forEach((event) => { // Convertir la date en string YYYY-MM-DD pour le calendrier let eventDate: Date; if (typeof event.date === "string") { eventDate = new Date(event.date); } else if (event.date instanceof Date) { eventDate = event.date; } else { // Fallback si c'est déjà un objet Date eventDate = new Date(event.date); } // Utiliser UTC pour éviter les problèmes de fuseau horaire const year = eventDate.getUTCFullYear(); const month = String(eventDate.getUTCMonth() + 1).padStart(2, "0"); const day = String(eventDate.getUTCDate()).padStart(2, "0"); const dateKey = `${year}-${month}-${day}`; // YYYY-MM-DD if (!eventsByDate[dateKey]) { eventsByDate[dateKey] = []; } eventsByDate[dateKey].push(event); }); // Mettre à jour le ref quand on a des données initiales useEffect(() => { if (hasInitialData) { hasUsedInitialData.current = true; } }, [hasInitialData]); // Ne charger depuis l'API que si on n'a pas de données initiales // (cas où l'utilisateur se connecte après le chargement de la page) useEffect(() => { // Si on a déjà des données initiales, ne jamais charger depuis l'API if (hasUsedInitialData.current) { return; } // Si on a déjà fait les appels API, ne pas refaire if (hasFetchedRegistrations.current) { return; } // Si pas de session, ne rien faire (on garde les données vides) if (!session?.user?.id) { return; } // Si pas d'événements, ne rien faire if (events.length === 0) { return; } // Marquer qu'on va faire les appels hasFetchedRegistrations.current = true; // Charger les inscriptions depuis l'API seulement si on n'a pas de données initiales // On charge pour tous les événements (passés et à venir) pour permettre le feedback const checkRegistrations = async () => { const registrationChecks = events.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 { return { eventId: event.id, registered: false }; } }); const results = await Promise.all(registrationChecks); const registrationsMap: Record = {}; results.forEach(({ eventId, registered }) => { registrationsMap[eventId] = registered; }); setRegistrations(registrationsMap); }; checkRegistrations(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [session?.user?.id]); // Fonctions pour le calendrier const getDaysInMonth = (date: Date) => { // Utiliser UTC pour correspondre au format des événements const year = date.getUTCFullYear(); const month = date.getUTCMonth(); return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); }; const getFirstDayOfMonth = (date: Date) => { // Utiliser UTC pour correspondre au format des événements // getUTCDay() retourne 0 (dimanche) à 6 (samedi) // On convertit pour que lundi = 0, mardi = 1, ..., dimanche = 6 const year = date.getUTCFullYear(); const month = date.getUTCMonth(); const dayOfWeek = new Date(Date.UTC(year, month, 1)).getUTCDay(); // Convertir : dimanche (0) -> 6, lundi (1) -> 0, mardi (2) -> 1, etc. return (dayOfWeek + 6) % 7; }; 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); } // Utiliser UTC pour correspondre au format des événements const year = currentMonth.getUTCFullYear(); const month = currentMonth.getUTCMonth() + 1; return (
{/* Header du calendrier */}

{formatMonthYear(currentMonth)}

{/* Jours de la semaine */}
{["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"].map((day) => (
{day}
))}
{/* Grille du calendrier */}
{days.map((day, index) => { if (day === null) { return
; } const dateKey = `${year}-${String(month).padStart(2, "0")}-${String( day ).padStart(2, "0")}`; const dayEvents = eventsByDate[dateKey] || []; // Vérifier si c'est aujourd'hui en utilisant UTC const today = new Date(); const todayKey = `${today.getUTCFullYear()}-${String( today.getUTCMonth() + 1 ).padStart(2, "0")}-${String(today.getUTCDate()).padStart(2, "0")}`; const isToday = todayKey === dateKey; const hasEvents = dayEvents.length > 0; // Déterminer la couleur principale selon le type d'événement const hasUpcoming = dayEvents.some( (e) => getEventStatus(e) === "UPCOMING" ); const hasLive = dayEvents.some((e) => getEventStatus(e) === "LIVE"); const hasPast = dayEvents.some((e) => getEventStatus(e) === "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 (
{ if (hasEvents && dayEvents.length > 0) { setSelectedEvent(dayEvents[0]); } }} 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" : "" } ${ hasEvents ? "cursor-pointer hover:opacity-80 transition-opacity" : "" }`} >
{day}
{hasEvents && (
{dayEvents.slice(0, 3).map((event) => { const status = getEventStatus(event); return (
); })} {dayEvents.length > 3 && (
)}
)}
); })}
); }; const truncateDescription = (text: string, maxLength: number = 150) => { if (text.length <= maxLength) return text; return text.substring(0, maxLength).trim() + "..."; }; const renderEventCard = (event: Event) => ( setSelectedEvent(event)} className="overflow-hidden hover:border-pixel-gold/50 transition group cursor-pointer" > {/* Event Header */}
{/* Event Content */}
{/* Status Badge */}
{getStatusBadge(getEventStatus(event))} {getEventTypeLabel(event.type)}
{/* Date */}
{typeof event.date === "string" ? new Date(event.date).toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric", }) : event.date.toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric", })}
{/* Event Name */}

{event.name}

{/* Event Details */} {(event.room || event.time || event.maxPlaces) && (
{event.room && (
📍 Salle: {event.room}
)} {event.time && (
🕐 Heure: {event.time}
)} {event.maxPlaces && (
👥 Places: {event.maxPlaces}
)}
)} {/* Description (truncated) */}

{truncateDescription(event.description)}

{event.description.length > 150 && (

Cliquez pour voir plus...

)} {/* Action Button */} {getEventStatus(event) === "UPCOMING" && ( <> {registrations[event.id] ? ( ) : ( )} )} {getEventStatus(event) === "LIVE" && ( )} {getEventStatus(event) === "PAST" && ( )}
); const [, startTransition] = useTransition(); const handleRegister = async (eventId: string) => { if (!session?.user?.id) { router.push("/login"); return; } setLoading((prev) => ({ ...prev, [eventId]: true })); setError(""); startTransition(async () => { const result = await registerForEvent(eventId); if (result.success) { setRegistrations((prev) => ({ ...prev, [eventId]: true, })); // Rafraîchir le score dans le header window.dispatchEvent(new Event("refreshUserScore")); } else { setError(result.error || "Une erreur est survenue"); } setLoading((prev) => ({ ...prev, [eventId]: false })); }); }; const handleUnregister = async (eventId: string) => { setLoading((prev) => ({ ...prev, [eventId]: true })); setError(""); startTransition(async () => { const result = await unregisterFromEvent(eventId); if (result.success) { setRegistrations((prev) => ({ ...prev, [eventId]: false, })); // Rafraîchir le score dans le header window.dispatchEvent(new Event("refreshUserScore")); } else { setError(result.error || "Une erreur est survenue"); } setLoading((prev) => ({ ...prev, [eventId]: false })); }); }; return ( {/* Title Section */} EVENTS

Rejoignez-nous pour des événements tech passionnants, des compétitions et des célébrations tout au long de l'année

{/* Événements à venir */} {upcomingEvents.length > 0 && (

Événements à venir

{upcomingEvents.map(renderEventCard)}
)} {/* Calendrier */}

Calendrier

{renderCalendar()}
{/* Événements passés */} {pastEvents.length > 0 && (

Événements passés

{pastEvents.map(renderEventCard)}
)} {/* Error Message */} {error && (

{error}

)} {/* Footer Info */} {/*

Restez informé de nos derniers événements et annonces

*/} {/* Event Modal */} {selectedEvent && ( setSelectedEvent(null)} size="lg" >
{/* Header */}
{getStatusBadge(getEventStatus(selectedEvent))} {getEventTypeLabel(selectedEvent.type)}

{selectedEvent.name}

setSelectedEvent(null)} size="lg" className="ml-4" />
{/* Event Header Color Bar */}
{/* Date */}
{typeof selectedEvent.date === "string" ? new Date(selectedEvent.date).toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric", }) : selectedEvent.date instanceof Date ? selectedEvent.date.toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric", }) : new Date(selectedEvent.date).toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric", })}
{/* Event Details */} {(selectedEvent.room || selectedEvent.time || selectedEvent.maxPlaces) && (
{selectedEvent.room && (
📍
Salle
{selectedEvent.room}
)} {selectedEvent.time && (
🕐
Heure
{selectedEvent.time}
)} {selectedEvent.maxPlaces && (
👥
Places
{selectedEvent.maxPlaces}
)}
)} {/* Full Description */}

Description

{selectedEvent.description}

{/* Action Button */} {getEventStatus(selectedEvent) === "UPCOMING" && (
{registrations[selectedEvent.id] ? ( ) : ( )}
)} {getEventStatus(selectedEvent) === "LIVE" && (
)} {getEventStatus(selectedEvent) === "PAST" && (
)}
)} {/* Feedback Modal */} setFeedbackEventId(null)} />
); }