856 lines
32 KiB
TypeScript
856 lines
32 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useMemo, useRef } from "react";
|
||
import { useSession } from "next-auth/react";
|
||
import { useRouter } from "next/navigation";
|
||
import { calculateEventStatus } from "@/lib/eventStatus";
|
||
import FeedbackModal from "@/components/FeedbackModal";
|
||
|
||
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<string, boolean>;
|
||
}
|
||
|
||
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 (
|
||
<span className="px-3 py-1 bg-green-900/50 border border-green-500/50 text-green-400 text-xs uppercase tracking-widest rounded">
|
||
À venir
|
||
</span>
|
||
);
|
||
case "LIVE":
|
||
return (
|
||
<span className="px-3 py-1 bg-red-900/50 border border-red-500/50 text-red-400 text-xs uppercase tracking-widest rounded animate-pulse">
|
||
En direct
|
||
</span>
|
||
);
|
||
case "PAST":
|
||
return (
|
||
<span className="px-3 py-1 bg-gray-800/50 border border-gray-600/50 text-gray-400 text-xs uppercase tracking-widest rounded">
|
||
Passé
|
||
</span>
|
||
);
|
||
}
|
||
};
|
||
|
||
export default function EventsPageSection({
|
||
events,
|
||
backgroundImage,
|
||
initialRegistrations = {},
|
||
}: EventsPageSectionProps) {
|
||
const { data: session } = useSession();
|
||
const router = useRouter();
|
||
const [registrations, setRegistrations] =
|
||
useState<Record<string, boolean>>(initialRegistrations);
|
||
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
||
const [error, setError] = useState<string>("");
|
||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||
const [feedbackEventId, setFeedbackEventId] = useState<string | null>(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);
|
||
|
||
// 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<string, Event[]> = {};
|
||
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 pas de session, ne rien faire (on garde les données vides)
|
||
if (!session?.user?.id) {
|
||
return;
|
||
}
|
||
|
||
// 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<string, boolean> = {};
|
||
results.forEach(({ eventId, registered }) => {
|
||
registrationsMap[eventId] = registered;
|
||
});
|
||
setRegistrations(registrationsMap);
|
||
};
|
||
|
||
checkRegistrations();
|
||
}, [session?.user?.id, events]);
|
||
|
||
// 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 (
|
||
<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={() => {
|
||
const year = currentMonth.getUTCFullYear();
|
||
const month = currentMonth.getUTCMonth();
|
||
setCurrentMonth(new Date(Date.UTC(year, month - 1, 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={() => {
|
||
const year = currentMonth.getUTCFullYear();
|
||
const month = currentMonth.getUTCMonth();
|
||
setCurrentMonth(new Date(Date.UTC(year, month + 1, 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">
|
||
{["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"].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] || [];
|
||
|
||
// 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 (
|
||
<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) => {
|
||
const status = getEventStatus(event);
|
||
return (
|
||
<div
|
||
key={event.id}
|
||
className={`w-1 h-1 rounded-full ${
|
||
status === "UPCOMING"
|
||
? "bg-green-400"
|
||
: 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 truncateDescription = (text: string, maxLength: number = 150) => {
|
||
if (text.length <= maxLength) return text;
|
||
return text.substring(0, maxLength).trim() + "...";
|
||
};
|
||
|
||
const renderEventCard = (event: Event) => (
|
||
<div
|
||
key={event.id}
|
||
onClick={() => setSelectedEvent(event)}
|
||
className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm hover:border-pixel-gold/50 transition group cursor-pointer"
|
||
>
|
||
{/* 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(getEventStatus(event))}
|
||
<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">
|
||
{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",
|
||
})}
|
||
</div>
|
||
|
||
{/* Event Name */}
|
||
<h3 className="text-xl font-bold text-white mb-3 group-hover:text-pixel-gold transition">
|
||
{event.name}
|
||
</h3>
|
||
|
||
{/* Event Details */}
|
||
{(event.room || event.time || event.maxPlaces) && (
|
||
<div className="flex flex-wrap gap-3 mb-4 text-sm">
|
||
{event.room && (
|
||
<div className="flex items-center gap-1.5 text-gray-300">
|
||
<span className="text-pixel-gold">📍</span>
|
||
<span className="font-semibold">Salle:</span>
|
||
<span>{event.room}</span>
|
||
</div>
|
||
)}
|
||
{event.time && (
|
||
<div className="flex items-center gap-1.5 text-gray-300">
|
||
<span className="text-pixel-gold">🕐</span>
|
||
<span className="font-semibold">Heure:</span>
|
||
<span>{event.time}</span>
|
||
</div>
|
||
)}
|
||
{event.maxPlaces && (
|
||
<div className="flex items-center gap-1.5 text-gray-300">
|
||
<span className="text-pixel-gold">👥</span>
|
||
<span className="font-semibold">Places:</span>
|
||
<span>{event.maxPlaces}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Description (truncated) */}
|
||
<p className="text-gray-400 text-sm leading-relaxed mb-4">
|
||
{truncateDescription(event.description)}
|
||
</p>
|
||
{event.description.length > 150 && (
|
||
<p className="text-pixel-gold text-xs mb-4 italic">
|
||
Cliquez pour voir plus...
|
||
</p>
|
||
)}
|
||
|
||
{/* Action Button */}
|
||
{getEventStatus(event) === "UPCOMING" && (
|
||
<>
|
||
{registrations[event.id] ? (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
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={(e) => {
|
||
e.stopPropagation();
|
||
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>
|
||
)}
|
||
</>
|
||
)}
|
||
{getEventStatus(event) === "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>
|
||
)}
|
||
{getEventStatus(event) === "PAST" && (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
if (!session?.user?.id) {
|
||
router.push("/login");
|
||
return;
|
||
}
|
||
setFeedbackEventId(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"
|
||
>
|
||
Donner un feedback
|
||
</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 {
|
||
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 {
|
||
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 */}
|
||
<div
|
||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||
style={{
|
||
backgroundImage: `url('${backgroundImage}')`,
|
||
}}
|
||
>
|
||
{/* Dark overlay for readability */}
|
||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
||
{/* Title Section */}
|
||
<div className="text-center mb-16">
|
||
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
|
||
<span
|
||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
||
style={{
|
||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
||
}}
|
||
>
|
||
EVENTS
|
||
</span>
|
||
</h1>
|
||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 mb-6 tracking-wide">
|
||
<span>✦</span>
|
||
<span>Événements à venir et passés</span>
|
||
<span>✦</span>
|
||
</div>
|
||
<p className="text-gray-400 text-sm max-w-2xl mx-auto">
|
||
Rejoignez-nous pour des événements tech passionnants, des
|
||
compétitions et des célébrations tout au long de l'année
|
||
</p>
|
||
</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">
|
||
Restez informé de nos derniers événements et annonces
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Event Modal */}
|
||
{selectedEvent && (
|
||
<div
|
||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||
onClick={() => setSelectedEvent(null)}
|
||
>
|
||
<div
|
||
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="p-8">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-3 mb-2">
|
||
{getStatusBadge(
|
||
selectedEvent ? getEventStatus(selectedEvent) : "UPCOMING"
|
||
)}
|
||
<span className="px-3 py-1 bg-pixel-gold/20 border border-pixel-gold/50 text-pixel-gold text-xs uppercase rounded">
|
||
{getEventTypeLabel(selectedEvent.type)}
|
||
</span>
|
||
</div>
|
||
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
||
{selectedEvent.name}
|
||
</h2>
|
||
</div>
|
||
<button
|
||
onClick={() => setSelectedEvent(null)}
|
||
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition ml-4"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
{/* Event Header Color Bar */}
|
||
<div
|
||
className={`h-1 bg-gradient-to-r ${getEventTypeColor(
|
||
selectedEvent.type
|
||
)} mb-6 rounded`}
|
||
></div>
|
||
|
||
{/* Date */}
|
||
<div className="text-white text-lg font-bold uppercase tracking-widest mb-4">
|
||
{typeof selectedEvent.date === "string"
|
||
? new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
|
||
day: "numeric",
|
||
month: "long",
|
||
year: "numeric",
|
||
})
|
||
: selectedEvent.date.toLocaleDateString("fr-FR", {
|
||
day: "numeric",
|
||
month: "long",
|
||
year: "numeric",
|
||
})}
|
||
</div>
|
||
|
||
{/* Event Details */}
|
||
{(selectedEvent.room ||
|
||
selectedEvent.time ||
|
||
selectedEvent.maxPlaces) && (
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||
{selectedEvent.room && (
|
||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||
<span className="text-pixel-gold text-xl">📍</span>
|
||
<div>
|
||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||
Salle
|
||
</div>
|
||
<div className="font-semibold">
|
||
{selectedEvent.room}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{selectedEvent.time && (
|
||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||
<span className="text-pixel-gold text-xl">🕐</span>
|
||
<div>
|
||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||
Heure
|
||
</div>
|
||
<div className="font-semibold">
|
||
{selectedEvent.time}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{selectedEvent.maxPlaces && (
|
||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||
<span className="text-pixel-gold text-xl">👥</span>
|
||
<div>
|
||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||
Places
|
||
</div>
|
||
<div className="font-semibold">
|
||
{selectedEvent.maxPlaces}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Full Description */}
|
||
<div className="mb-6">
|
||
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-3">
|
||
Description
|
||
</h3>
|
||
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-line">
|
||
{selectedEvent.description}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Action Button */}
|
||
{selectedEvent &&
|
||
getEventStatus(selectedEvent) === "UPCOMING" && (
|
||
<div className="pt-4 border-t border-pixel-gold/20">
|
||
{registrations[selectedEvent.id] ? (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleUnregister(selectedEvent.id);
|
||
setSelectedEvent(null);
|
||
}}
|
||
disabled={loading[selectedEvent.id]}
|
||
className="w-full px-4 py-3 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-sm tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{loading[selectedEvent.id]
|
||
? "Annulation..."
|
||
: "Se désinscrire"}
|
||
</button>
|
||
) : (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleRegister(selectedEvent.id);
|
||
setSelectedEvent(null);
|
||
}}
|
||
disabled={loading[selectedEvent.id]}
|
||
className="w-full px-4 py-3 border border-pixel-gold/50 bg-pixel-gold/10 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/20 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{loading[selectedEvent.id]
|
||
? "Inscription..."
|
||
: "S'inscrire maintenant"}
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
{selectedEvent && getEventStatus(selectedEvent) === "LIVE" && (
|
||
<div className="pt-4 border-t border-pixel-gold/20">
|
||
<button className="w-full px-4 py-3 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-sm tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
|
||
Rejoindre en direct
|
||
</button>
|
||
</div>
|
||
)}
|
||
{selectedEvent && getEventStatus(selectedEvent) === "PAST" && (
|
||
<div className="pt-4 border-t border-pixel-gold/20">
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
if (!session?.user?.id) {
|
||
router.push("/login");
|
||
setSelectedEvent(null);
|
||
return;
|
||
}
|
||
setFeedbackEventId(selectedEvent.id);
|
||
setSelectedEvent(null);
|
||
}}
|
||
className="w-full px-4 py-3 border border-pixel-gold/50 bg-black/40 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
||
>
|
||
Donner un feedback
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Feedback Modal */}
|
||
<FeedbackModal
|
||
eventId={feedbackEventId}
|
||
onClose={() => setFeedbackEventId(null)}
|
||
/>
|
||
</section>
|
||
);
|
||
}
|