Enhance UI components and animations: Introduce a shimmer animation effect in globals.css, refactor FeedbackPageClient, LoginPage, RegisterPage, and AdminPanel components to utilize new UI components for improved consistency and maintainability. Update event and feedback handling in EventsPageSection and FeedbackModal, ensuring a cohesive user experience across the application.

This commit is contained in:
Julien Froidefond
2025-12-12 16:44:57 +01:00
parent db01c25de7
commit 99a475736b
32 changed files with 2242 additions and 1389 deletions

View File

@@ -9,6 +9,15 @@ import {
registerForEvent,
unregisterFromEvent,
} from "@/actions/events/register";
import {
Badge,
Button,
Modal,
CloseButton,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
interface Event {
id: string;
@@ -61,21 +70,21 @@ 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">
<Badge variant="success" size="md">
À venir
</span>
</Badge>
);
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">
<Badge variant="danger" size="md" className="animate-pulse">
En direct
</span>
</Badge>
);
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">
<Badge variant="default" size="md">
Passé
</span>
</Badge>
);
}
};
@@ -401,10 +410,10 @@ export default function EventsPageSection({
};
const renderEventCard = (event: Event) => (
<div
<Card
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"
className="overflow-hidden hover:border-pixel-gold/50 transition group cursor-pointer"
>
{/* Event Header */}
<div
@@ -482,37 +491,41 @@ export default function EventsPageSection({
{getEventStatus(event) === "UPCOMING" && (
<>
{registrations[event.id] ? (
<button
<Button
onClick={(e) => {
e.stopPropagation();
handleUnregister(event.id);
}}
variant="success"
size="md"
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"
className="w-full"
>
{loading[event.id] ? "Annulation..." : "Inscrit ✓"}
</button>
</Button>
) : (
<button
<Button
onClick={(e) => {
e.stopPropagation();
handleRegister(event.id);
}}
variant="primary"
size="md"
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"
className="w-full"
>
{loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
</button>
</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">
<Button variant="danger" size="md" className="w-full animate-pulse">
Rejoindre en direct
</button>
</Button>
)}
{getEventStatus(event) === "PAST" && (
<button
<Button
onClick={(e) => {
e.stopPropagation();
if (!session?.user?.id) {
@@ -521,13 +534,15 @@ export default function EventsPageSection({
}
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"
variant="primary"
size="md"
className="w-full"
>
Donner un feedback
</button>
</Button>
)}
</div>
</div>
</Card>
);
const [, startTransition] = useTransition();
@@ -576,273 +591,254 @@ export default function EventsPageSection({
};
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}')`,
}}
<BackgroundSection backgroundImage={backgroundImage}>
{/* Title Section */}
<SectionTitle
variant="gradient"
size="xl"
subtitle="Événements à venir et passés"
className="mb-16"
>
{/* Dark overlay for readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
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&apos;année
</p>
{/* 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&apos;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 */}
{/* Événements à venir */}
{upcomingEvents.length > 0 && (
<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
<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>
{renderCalendar()}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{upcomingEvents.map(renderEventCard)}
</div>
</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> */}
{/* 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 && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={() => setSelectedEvent(null)}
<Modal
isOpen={!!selectedEvent}
onClose={() => setSelectedEvent(null)}
size="lg"
>
<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 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>
<button
onClick={() => setSelectedEvent(null)}
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition ml-4"
>
×
</button>
<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>
{/* 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", {
{/* 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",
})
: selectedEvent.date.toLocaleDateString("fr-FR", {
: new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
})}
</div>
</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>
{/* 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>
)}
{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
{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();
if (!session?.user?.id) {
router.push("/login");
setSelectedEvent(null);
return;
}
setFeedbackEventId(selectedEvent.id);
handleUnregister(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"
variant="success"
size="lg"
disabled={loading[selectedEvent.id]}
className="w-full"
>
Donner un feedback
</button>
</div>
)}
</div>
{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>
</div>
</Modal>
)}
{/* Feedback Modal */}
@@ -850,6 +846,6 @@ export default function EventsPageSection({
eventId={feedbackEventId}
onClose={() => setFeedbackEventId(null)}
/>
</section>
</BackgroundSection>
);
}