538 lines
18 KiB
TypeScript
538 lines
18 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } 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;
|
|
}
|
|
|
|
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,
|
|
}: 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 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);
|
|
});
|
|
|
|
// 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 */}
|
|
<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>
|
|
);
|
|
}
|