852 lines
29 KiB
TypeScript
852 lines
29 KiB
TypeScript
"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/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<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 (
|
|
<Badge variant="success" size="md">
|
|
À venir
|
|
</Badge>
|
|
);
|
|
case "LIVE":
|
|
return (
|
|
<Badge variant="danger" size="md" className="animate-pulse">
|
|
En direct
|
|
</Badge>
|
|
);
|
|
case "PAST":
|
|
return (
|
|
<Badge variant="default" size="md">
|
|
Passé
|
|
</Badge>
|
|
);
|
|
}
|
|
};
|
|
|
|
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}
|
|
onClick={() => {
|
|
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"
|
|
: ""
|
|
}`}
|
|
>
|
|
<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) => (
|
|
<Card
|
|
key={event.id}
|
|
onClick={() => setSelectedEvent(event)}
|
|
className="overflow-hidden 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);
|
|
}}
|
|
variant="success"
|
|
size="md"
|
|
disabled={loading[event.id]}
|
|
className="w-full"
|
|
>
|
|
{loading[event.id] ? "Annulation..." : "Inscrit ✓"}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleRegister(event.id);
|
|
}}
|
|
variant="primary"
|
|
size="md"
|
|
disabled={loading[event.id]}
|
|
className="w-full"
|
|
>
|
|
{loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
{getEventStatus(event) === "LIVE" && (
|
|
<Button variant="danger" size="md" className="w-full 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);
|
|
}}
|
|
variant="primary"
|
|
size="md"
|
|
className="w-full"
|
|
>
|
|
Donner un feedback
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
|
|
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,
|
|
}));
|
|
} 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,
|
|
}));
|
|
} else {
|
|
setError(result.error || "Une erreur est survenue");
|
|
}
|
|
setLoading((prev) => ({ ...prev, [eventId]: false }));
|
|
});
|
|
};
|
|
|
|
return (
|
|
<BackgroundSection backgroundImage={backgroundImage}>
|
|
{/* Title Section */}
|
|
<SectionTitle
|
|
variant="gradient"
|
|
size="xl"
|
|
subtitle="Événements à venir et passés"
|
|
className="mb-16"
|
|
>
|
|
EVENTS
|
|
</SectionTitle>
|
|
<p className="text-gray-400 text-sm max-w-2xl mx-auto text-center mb-16">
|
|
Rejoignez-nous pour des événements tech passionnants, des compétitions
|
|
et des célébrations tout au long de l'année
|
|
</p>
|
|
|
|
{/* É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> */}
|
|
|
|
{/* Event Modal */}
|
|
{selectedEvent && (
|
|
<Modal
|
|
isOpen={!!selectedEvent}
|
|
onClose={() => setSelectedEvent(null)}
|
|
size="lg"
|
|
>
|
|
<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(getEventStatus(selectedEvent))}
|
|
<Badge variant="default" size="md">
|
|
{getEventTypeLabel(selectedEvent.type)}
|
|
</Badge>
|
|
</div>
|
|
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
|
{selectedEvent.name}
|
|
</h2>
|
|
</div>
|
|
<CloseButton
|
|
onClick={() => setSelectedEvent(null)}
|
|
size="lg"
|
|
className="ml-4"
|
|
/>
|
|
</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 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",
|
|
})}
|
|
</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 */}
|
|
{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);
|
|
}}
|
|
variant="success"
|
|
size="lg"
|
|
disabled={loading[selectedEvent.id]}
|
|
className="w-full"
|
|
>
|
|
{loading[selectedEvent.id]
|
|
? "Annulation..."
|
|
: "Se désinscrire"}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleRegister(selectedEvent.id);
|
|
setSelectedEvent(null);
|
|
}}
|
|
variant="primary"
|
|
size="lg"
|
|
disabled={loading[selectedEvent.id]}
|
|
className="w-full"
|
|
>
|
|
{loading[selectedEvent.id]
|
|
? "Inscription..."
|
|
: "S'inscrire maintenant"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
{getEventStatus(selectedEvent) === "LIVE" && (
|
|
<div className="pt-4 border-t border-pixel-gold/20">
|
|
<Button
|
|
variant="danger"
|
|
size="lg"
|
|
className="w-full animate-pulse"
|
|
>
|
|
Rejoindre en direct
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{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);
|
|
}}
|
|
variant="primary"
|
|
size="lg"
|
|
className="w-full"
|
|
>
|
|
Donner un feedback
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
)}
|
|
|
|
{/* Feedback Modal */}
|
|
<FeedbackModal
|
|
eventId={feedbackEventId}
|
|
onClose={() => setFeedbackEventId(null)}
|
|
/>
|
|
</BackgroundSection>
|
|
);
|
|
}
|