Files
got-gaming/components/EventsPageSection.tsx

563 lines
19 KiB
TypeScript

"use client";
import { useState, useEffect, useMemo, useRef } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
interface Event {
id: string;
date: string;
name: string;
description: string;
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION";
status: "UPCOMING" | "LIVE" | "PAST";
}
interface EventsPageSectionProps {
events: Event[];
backgroundImage: string;
initialRegistrations?: Record<string, boolean>;
}
const getEventTypeColor = (type: Event["type"]) => {
switch (type) {
case "SUMMIT":
return "from-blue-600 to-cyan-500";
case "LAUNCH":
return "from-purple-600 to-pink-500";
case "FESTIVAL":
return "from-pixel-gold to-orange-500";
case "COMPETITION":
return "from-red-600 to-orange-500";
default:
return "from-gray-600 to-gray-500";
}
};
const getEventTypeLabel = (type: Event["type"]) => {
switch (type) {
case "SUMMIT":
return "Sommet";
case "LAUNCH":
return "Lancement";
case "FESTIVAL":
return "Festival";
case "COMPETITION":
return "Compétition";
default:
return type;
}
};
const getStatusBadge = (status: Event["status"]) => {
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());
// 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)
const upcomingEvents = events
.filter((e) => e.status === "UPCOMING" || e.status === "LIVE")
.sort((a, b) => {
// Trier par date décroissante (du plus récent au plus ancien)
return b.date.localeCompare(a.date);
});
const pastEvents = events
.filter((e) => e.status === "PAST")
.sort((a, b) => {
// Trier par date décroissante (du plus récent au plus ancien)
return b.date.localeCompare(a.date);
});
// 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);
});
// 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
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 */}
<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>
</section>
);
}