Refactor component imports and structure: Update import paths for various components to improve organization, moving them into appropriate subdirectories. Remove unused components related to user and event management, enhancing code clarity and maintainability across the application.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
This commit is contained in:
851
components/events/EventsPageSection.tsx
Normal file
851
components/events/EventsPageSection.tsx
Normal file
@@ -0,0 +1,851 @@
|
||||
"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<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>
|
||||
);
|
||||
}
|
||||
43
components/events/EventsSection.tsx
Normal file
43
components/events/EventsSection.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
interface Event {
|
||||
id: string;
|
||||
date: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface EventsSectionProps {
|
||||
events: Event[];
|
||||
}
|
||||
|
||||
export default function EventsSection({ events }: EventsSectionProps) {
|
||||
if (events.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<section className="w-full bg-gray-950 border-t border-pixel-gold/30 py-16">
|
||||
<div className="max-w-7xl mx-auto px-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-around gap-8">
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="flex flex-col items-center">
|
||||
<div className="flex flex-col items-center mb-4">
|
||||
<span className="text-pixel-gold text-xs uppercase tracking-widest mb-2">
|
||||
Événement
|
||||
</span>
|
||||
<div className="w-16 h-px bg-pixel-gold"></div>
|
||||
</div>
|
||||
<div className="text-white text-lg font-bold mb-2 uppercase tracking-wide">
|
||||
{new Date(event.date).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</div>
|
||||
<div className="text-white text-base text-center">
|
||||
{event.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user