diff --git a/app/admin/style-guide/page.tsx b/app/admin/style-guide/page.tsx
new file mode 100644
index 0000000..bc2ba2e
--- /dev/null
+++ b/app/admin/style-guide/page.tsx
@@ -0,0 +1,495 @@
+"use client";
+
+import { useState } from "react";
+import Navigation from "@/components/Navigation";
+import {
+ Button,
+ Input,
+ Textarea,
+ Card,
+ Badge,
+ Alert,
+ Modal,
+ ProgressBar,
+ StarRating,
+ Avatar,
+ SectionTitle,
+ BackgroundSection,
+ CloseButton,
+} from "@/components/ui";
+
+export default function StyleGuidePage() {
+ const [modalOpen, setModalOpen] = useState(false);
+ const [inputValue, setInputValue] = useState("");
+ const [textareaValue, setTextareaValue] = useState("");
+ const [rating, setRating] = useState(0);
+
+ return (
+
+
+
+
+
+ STYLE GUIDE
+
+
+ Guide de style complet avec tous les composants UI disponibles et
+ leurs variantes
+
+
+ {/* Buttons */}
+
+
+ Buttons
+
+
+
+
Variantes
+
+ Primary
+ Secondary
+ Success
+ Danger
+ Ghost
+
+
+
+
Tailles
+
+
+ Small
+
+
+ Medium
+
+
+ Large
+
+
+
+
+
États
+
+ Normal
+
+ Disabled
+
+
+
+
+
+
+ {/* Inputs */}
+
+ Inputs
+
+
+
+
Avec erreur
+
+ setInputValue(e.target.value)}
+ />
+
+
+
+
+
+ {/* Textarea */}
+
+
+ Textarea
+
+
+
+
+
+ Avec compteur de caractères
+
+
+
+
+
+
+
+
+ {/* Badges */}
+
+ Badges
+
+
+
Variantes
+
+ Default
+ Success
+ Warning
+ Danger
+ Info
+
+
+
+
Tailles
+
+
+ Small
+
+
+ Medium
+
+
+
+
+
+
+ {/* Alerts */}
+
+ Alerts
+
+
+ Opération réussie ! Votre action a été effectuée avec succès.
+
+
+ Une erreur est survenue. Veuillez réessayer.
+
+
+ Attention ! Cette action est irréversible.
+
+
+ Information : Voici quelques informations utiles.
+
+
+
+
+ {/* Cards */}
+
+ Cards
+
+
+
+ Card Default
+
+
+ Contenu de la carte avec variant default
+
+
+
+
+ Card Dark
+
+
+ Contenu de la carte avec variant dark
+
+
+
+
+
+ {/* Progress Bars */}
+
+
+ Progress Bars
+
+
+
+
+ {/* Star Rating */}
+
+
+ Star Rating
+
+
+
+
Interactif
+
+
+ Note sélectionnée : {rating}/5
+
+
+
+
+
+
+ {/* Avatar */}
+
+ Avatar
+
+
+
+
+ Sans image (fallback)
+
+
+
+
+
+
+ {/* Section Title */}
+
+
+ Section Title
+
+
+
+
Variantes
+
+
+ Default Title
+
+
+ Gradient Title
+
+
+
+
+
Tailles
+
+
+ Small Title
+
+
+ Medium Title
+
+
+ Large Title
+
+
+ Extra Large Title
+
+
+
+
+
Avec sous-titre
+
+ Title with Subtitle
+
+
+
+
+
+ {/* Modal */}
+
+ Modal
+
+
+
Tailles
+
+ setModalOpen(true)}>
+ Ouvrir Modal
+
+
+
+
+
+
+ {/* Close Button */}
+
+
+ Close Button
+
+
+
+
Tailles
+
+ {}} size="sm" />
+ {}} size="md" />
+ {}} size="lg" />
+
+
+
+
Disabled
+ {}} disabled />
+
+
+
+
+ {/* Modal Demo */}
+
setModalOpen(false)}
+ size="md"
+ >
+
+
+
+ Exemple de Modal
+
+ setModalOpen(false)} />
+
+
+ Ceci est un exemple de modal avec différentes tailles
+ disponibles.
+
+
+ setModalOpen(false)}>Fermer
+
+
+
+
+
+
+ );
+}
+
diff --git a/app/feedback/[eventId]/FeedbackPageClient.tsx b/app/feedback/[eventId]/FeedbackPageClient.tsx
index 8cb7ed5..12d270a 100644
--- a/app/feedback/[eventId]/FeedbackPageClient.tsx
+++ b/app/feedback/[eventId]/FeedbackPageClient.tsx
@@ -5,6 +5,15 @@ import { useSession } from "next-auth/react";
import { useRouter, useParams } from "next/navigation";
import Navigation from "@/components/Navigation";
import { createFeedback } from "@/actions/events/feedback";
+import {
+ StarRating,
+ Textarea,
+ Button,
+ Alert,
+ Card,
+ BackgroundSection,
+ SectionTitle,
+} from "@/components/ui";
interface Event {
id: string;
@@ -156,25 +165,17 @@ export default function FeedbackPageClient({
return (
-
- {/* Background Image */}
-
-
+
{/* Feedback Form */}
-
-
-
-
- FEEDBACK
-
-
+
+
+
+ FEEDBACK
+
{existingFeedback
? "Modifier votre feedback pour"
@@ -185,15 +186,15 @@ export default function FeedbackPageClient({
{success && (
-
+
Feedback enregistré avec succès ! Redirection...
-
+
)}
{error && (
-
+
)}
{/* Comment */}
-
-
- Commentaire (optionnel)
-
-
+
+
-
+
);
}
diff --git a/app/globals.css b/app/globals.css
index 68379c7..a03c732 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -31,4 +31,17 @@
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
+
+ @keyframes shimmer {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ transform: translateX(100%);
+ }
+ }
+
+ .animate-shimmer {
+ animation: shimmer 2s infinite;
+ }
}
diff --git a/app/login/page.tsx b/app/login/page.tsx
index 07627b5..3d77513 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -5,6 +5,14 @@ import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import Navigation from "@/components/Navigation";
+import {
+ Input,
+ Button,
+ Alert,
+ Card,
+ BackgroundSection,
+ SectionTitle,
+} from "@/components/ui";
export default function LoginPage() {
const router = useRouter();
@@ -46,79 +54,53 @@ export default function LoginPage() {
return (
-
- {/* Background Image */}
-
-
+
{/* Login Form */}
-
-
-
-
- CONNEXION
-
-
+
+
+
+ CONNEXION
+
Connectez-vous à votre compte
@@ -132,9 +114,9 @@ export default function LoginPage() {
-
+
-
+
);
}
diff --git a/app/register/page.tsx b/app/register/page.tsx
index c347127..07bfe79 100644
--- a/app/register/page.tsx
+++ b/app/register/page.tsx
@@ -4,7 +4,16 @@ import { useState, useRef, type ChangeEvent, type FormEvent } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import Navigation from "@/components/Navigation";
-import Avatar from "@/components/Avatar";
+import {
+ Avatar,
+ Input,
+ Textarea,
+ Button,
+ Alert,
+ Card,
+ BackgroundSection,
+ SectionTitle,
+} from "@/components/ui";
export default function RegisterPage() {
const router = useRouter();
@@ -162,25 +171,17 @@ export default function RegisterPage() {
return (
-
- {/* Background Image */}
-
-
+
{/* Register Form */}
-
-
-
-
- INSCRIPTION
-
-
+
+
+
+ INSCRIPTION
+
{step === 1
? "Créez votre compte pour commencer"
@@ -216,103 +217,65 @@ export default function RegisterPage() {
{step === 1 ? (
) : (
-
-
- Nom d'utilisateur
-
-
-
3-20 caractères
-
+
+
3-20 caractères
-
-
- Bio (optionnel)
-
-
-
- {formData.bio.length}/500 caractères
-
-
+
@@ -500,20 +449,24 @@ export default function RegisterPage() {
- setStep(1)}
- className="flex-1 px-6 py-3 border border-gray-600/50 bg-black/40 text-gray-400 uppercase text-sm tracking-widest rounded hover:bg-gray-900/40 hover:border-gray-500 transition"
+ className="flex-1"
>
Retour
-
-
+
{loading ? "Finalisation..." : "Terminer"}
-
+
)}
@@ -529,9 +482,9 @@ export default function RegisterPage() {
-
+
-
+
);
}
diff --git a/components/AdminPanel.tsx b/components/AdminPanel.tsx
index eb58247..960b3fb 100644
--- a/components/AdminPanel.tsx
+++ b/components/AdminPanel.tsx
@@ -1,10 +1,12 @@
"use client";
import { useState } from "react";
+import Link from "next/link";
import UserManagement from "@/components/UserManagement";
import EventManagement from "@/components/EventManagement";
import FeedbackManagement from "@/components/FeedbackManagement";
import BackgroundPreferences from "@/components/BackgroundPreferences";
+import { Button, Card, SectionTitle } from "@/components/ui";
interface SitePreferences {
id: string;
@@ -26,92 +28,91 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
return (
-
-
- ADMIN
-
-
+
+ ADMIN
+
{/* Navigation Tabs */}
-
-
+ setActiveSection("preferences")}
- className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
- activeSection === "preferences"
- ? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
- : "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
- }`}
+ variant={activeSection === "preferences" ? "primary" : "secondary"}
+ size="md"
+ className={
+ activeSection === "preferences" ? "bg-pixel-gold/10" : ""
+ }
>
Préférences UI
-
-
+ setActiveSection("users")}
- className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
- activeSection === "users"
- ? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
- : "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
- }`}
+ variant={activeSection === "users" ? "primary" : "secondary"}
+ size="md"
+ className={activeSection === "users" ? "bg-pixel-gold/10" : ""}
>
Utilisateurs
-
-
+ setActiveSection("events")}
- className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
- activeSection === "events"
- ? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
- : "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
- }`}
+ variant={activeSection === "events" ? "primary" : "secondary"}
+ size="md"
+ className={activeSection === "events" ? "bg-pixel-gold/10" : ""}
>
Événements
-
-
+ setActiveSection("feedbacks")}
- className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
- activeSection === "feedbacks"
- ? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
- : "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
- }`}
+ variant={activeSection === "feedbacks" ? "primary" : "secondary"}
+ size="md"
+ className={activeSection === "feedbacks" ? "bg-pixel-gold/10" : ""}
>
Feedbacks
-
+
{activeSection === "preferences" && (
-
-
- Préférences UI Globales
-
+
+
+
+ Préférences UI Globales
+
+
+
+ 📖 Voir le Style Guide
+
+
+
-
+
)}
{activeSection === "users" && (
-
+
Gestion des Utilisateurs
-
+
)}
{activeSection === "events" && (
-
+
Gestion des Événements
-
+
)}
{activeSection === "feedbacks" && (
-
+
Gestion des Feedbacks
-
+
)}
diff --git a/components/BackgroundPreferences.tsx b/components/BackgroundPreferences.tsx
index 812b4e6..d7ed931 100644
--- a/components/BackgroundPreferences.tsx
+++ b/components/BackgroundPreferences.tsx
@@ -3,6 +3,7 @@
import { useState, useEffect, useMemo } from "react";
import ImageSelector from "@/components/ImageSelector";
import { updateSitePreferences } from "@/actions/admin/preferences";
+import { Button, Card } from "@/components/ui";
interface SitePreferences {
id: string;
@@ -142,7 +143,7 @@ export default function BackgroundPreferences({
};
return (
-
+
@@ -153,12 +154,14 @@ export default function BackgroundPreferences({
{!isEditing && (
-
Modifier
-
+
)}
@@ -195,18 +198,12 @@ export default function BackgroundPreferences({
label="Background Leaderboard"
/>
-
+
Enregistrer
-
-
+
+
Annuler
-
+
) : (
@@ -381,6 +378,6 @@ export default function BackgroundPreferences({
)}
-
+
);
}
diff --git a/components/EventManagement.tsx b/components/EventManagement.tsx
index b73e132..5276322 100644
--- a/components/EventManagement.tsx
+++ b/components/EventManagement.tsx
@@ -3,6 +3,7 @@
import { useState, useEffect, useTransition } from "react";
import { calculateEventStatus } from "@/lib/eventStatus";
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
+import { Input, Textarea, Button, Card, Badge } from "@/components/ui";
interface Event {
id: string;
@@ -209,62 +210,52 @@ export default function EventManagement() {
Événements ({events.length})
{!isCreating && !editingEvent && (
-
+ Nouvel événement
-
+
)}
{(isCreating || editingEvent) && (
-
+
{isCreating ? "Créer un événement" : "Modifier l'événement"}
-
+
)}
{events.length === 0 ? (
@@ -362,80 +339,82 @@ export default function EventManagement() {
) : (
- {events.map((event) => (
-
-
-
-
-
- {event.name}
-
-
- {getEventTypeLabel(event.type)}
-
- {
- const status = calculateEventStatus(event.date);
- return status === "UPCOMING"
- ? "bg-green-900/50 border border-green-500/50 text-green-400"
- : status === "LIVE"
- ? "bg-yellow-900/50 border border-yellow-500/50 text-yellow-400"
- : "bg-gray-900/50 border border-gray-500/50 text-gray-400";
- })()}`}
- >
- {getStatusLabel(calculateEventStatus(event.date))}
-
-
-
- {event.description}
-
-
-
- Date: {new Date(event.date).toLocaleDateString("fr-FR")}
+ {events.map((event) => {
+ const status = calculateEventStatus(event.date);
+ const statusVariant =
+ status === "UPCOMING"
+ ? "success"
+ : status === "LIVE"
+ ? "warning"
+ : "default";
+
+ return (
+
+
+
+
+
+ {event.name}
+
+
+ {getEventTypeLabel(event.type)}
+
+
+ {getStatusLabel(status)}
+
+
+
+ {event.description}
- {event.room && (
+
- 📍 Salle: {event.room}
+ Date: {new Date(event.date).toLocaleDateString("fr-FR")}
- )}
- {event.time && (
-
- 🕐 Heure: {event.time}
-
- )}
- {event.maxPlaces && (
-
- 👥 Places: {event.maxPlaces}
-
- )}
-
- {event.registrationsCount || 0} inscrit
- {event.registrationsCount !== 1 ? "s" : ""}
-
+ {event.room && (
+
+ 📍 Salle: {event.room}
+
+ )}
+ {event.time && (
+
+ 🕐 Heure: {event.time}
+
+ )}
+ {event.maxPlaces && (
+
+ 👥 Places: {event.maxPlaces}
+
+ )}
+
+ {event.registrationsCount || 0} inscrit
+ {event.registrationsCount !== 1 ? "s" : ""}
+
+
+ {!isCreating && !editingEvent && (
+
+ handleEdit(event)}
+ variant="primary"
+ size="sm"
+ className="whitespace-nowrap"
+ >
+ Modifier
+
+ handleDelete(event.id)}
+ variant="danger"
+ size="sm"
+ className="whitespace-nowrap"
+ >
+ Supprimer
+
+
+ )}
- {!isCreating && !editingEvent && (
-
- handleEdit(event)}
- className="px-2 sm:px-3 py-1 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap"
- >
- Modifier
-
- handleDelete(event.id)}
- className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-red-900/30 transition whitespace-nowrap"
- >
- Supprimer
-
-
- )}
-
-
- ))}
+
+ );
+ })}
)}
diff --git a/components/EventsPageSection.tsx b/components/EventsPageSection.tsx
index 288fdcf..1c61f8f 100644
--- a/components/EventsPageSection.tsx
+++ b/components/EventsPageSection.tsx
@@ -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 (
-
+
À venir
-
+
);
case "LIVE":
return (
-
+
En direct
-
+
);
case "PAST":
return (
-
+
Passé
-
+
);
}
};
@@ -401,10 +410,10 @@ export default function EventsPageSection({
};
const renderEventCard = (event: Event) => (
-
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 */}
{registrations[event.id] ? (
- {
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 ✓"}
-
+
) : (
- {
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"}
-
+
)}
>
)}
{getEventStatus(event) === "LIVE" && (
-
+
Rejoindre en direct
-
+
)}
{getEventStatus(event) === "PAST" && (
- {
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
-
+
)}
-
+
);
const [, startTransition] = useTransition();
@@ -576,273 +591,254 @@ export default function EventsPageSection({
};
return (
-
- {/* Background Image */}
-
+ {/* Title Section */}
+
- {/* Dark overlay for readability */}
-
-
+ EVENTS
+
+
+ Rejoignez-nous pour des événements tech passionnants, des compétitions
+ et des célébrations tout au long de l'année
+
- {/* Content */}
-
- {/* Title Section */}
-
-
-
- EVENTS
-
-
-
- ✦
- Événements à venir et passés
- ✦
-
-
- Rejoignez-nous pour des événements tech passionnants, des
- compétitions et des célébrations tout au long de l'année
-
-
-
- {/* Événements à venir */}
- {upcomingEvents.length > 0 && (
-
-
-
- Événements à venir
-
-
-
- {upcomingEvents.map(renderEventCard)}
-
-
- )}
-
- {/* Calendrier */}
+ {/* Événements à venir */}
+ {upcomingEvents.length > 0 && (
-
-
- Calendrier
+
+
+ Événements à venir
- {renderCalendar()}
+
+ {upcomingEvents.map(renderEventCard)}
+
+ )}
- {/* Événements passés */}
- {pastEvents.length > 0 && (
-
-
-
- Événements passés
-
-
-
- {pastEvents.map(renderEventCard)}
-
-
- )}
-
- {/* Error Message */}
- {error && (
-
- )}
-
- {/* Footer Info */}
- {/*
-
- Restez informé de nos derniers événements et annonces
-
-
*/}
+ {/* Calendrier */}
+
+
+
+ Calendrier
+
+
+ {renderCalendar()}
+ {/* Événements passés */}
+ {pastEvents.length > 0 && (
+
+
+
+ Événements passés
+
+
+
+ {pastEvents.map(renderEventCard)}
+
+
+ )}
+
+ {/* Error Message */}
+ {error && (
+
+ )}
+
+ {/* Footer Info */}
+ {/*
+
+ Restez informé de nos derniers événements et annonces
+
+
*/}
+
{/* Event Modal */}
{selectedEvent && (
-
setSelectedEvent(null)}
+
setSelectedEvent(null)}
+ size="lg"
>
- e.stopPropagation()}
- >
-
- {/* Header */}
-
-
-
- {getStatusBadge(
- selectedEvent ? getEventStatus(selectedEvent) : "UPCOMING"
- )}
-
- {getEventTypeLabel(selectedEvent.type)}
-
-
-
- {selectedEvent.name}
-
+
+ {/* Header */}
+
+
+
+ {getStatusBadge(getEventStatus(selectedEvent))}
+
+ {getEventTypeLabel(selectedEvent.type)}
+
-
setSelectedEvent(null)}
- className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition ml-4"
- >
- ×
-
+
+ {selectedEvent.name}
+
+
setSelectedEvent(null)}
+ size="lg"
+ className="ml-4"
+ />
+
- {/* Event Header Color Bar */}
-
+ {/* Event Header Color Bar */}
+
- {/* Date */}
-
- {typeof selectedEvent.date === "string"
- ? new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
+ {/* Date */}
+
+ {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",
})}
-
+
- {/* Event Details */}
- {(selectedEvent.room ||
- selectedEvent.time ||
- selectedEvent.maxPlaces) && (
-
- {selectedEvent.room && (
-
-
📍
-
-
- Salle
-
-
- {selectedEvent.room}
-
+ {/* Event Details */}
+ {(selectedEvent.room ||
+ selectedEvent.time ||
+ selectedEvent.maxPlaces) && (
+
+ {selectedEvent.room && (
+
+
📍
+
+
+ Salle
+
{selectedEvent.room}
- )}
- {selectedEvent.time && (
-
-
🕐
-
-
- Heure
-
-
- {selectedEvent.time}
-
-
-
- )}
- {selectedEvent.maxPlaces && (
-
-
👥
-
-
- Places
-
-
- {selectedEvent.maxPlaces}
-
-
-
- )}
-
- )}
-
- {/* Full Description */}
-
-
- Description
-
-
- {selectedEvent.description}
-
-
-
- {/* Action Button */}
- {selectedEvent &&
- getEventStatus(selectedEvent) === "UPCOMING" && (
-
- {registrations[selectedEvent.id] ? (
- {
- 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"}
-
- ) : (
- {
- 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"}
-
- )}
)}
- {selectedEvent && getEventStatus(selectedEvent) === "LIVE" && (
-
-
- Rejoindre en direct
-
-
- )}
- {selectedEvent && getEventStatus(selectedEvent) === "PAST" && (
-
-
+ 🕐
+
+
+ Heure
+
+
{selectedEvent.time}
+
+
+ )}
+ {selectedEvent.maxPlaces && (
+
+
👥
+
+
+ Places
+
+
+ {selectedEvent.maxPlaces}
+
+
+
+ )}
+
+ )}
+
+ {/* Full Description */}
+
+
+ Description
+
+
+ {selectedEvent.description}
+
+
+
+ {/* Action Button */}
+ {getEventStatus(selectedEvent) === "UPCOMING" && (
+
+ {registrations[selectedEvent.id] ? (
+ {
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
-
-
- )}
-
+ {loading[selectedEvent.id]
+ ? "Annulation..."
+ : "Se désinscrire"}
+
+ ) : (
+
{
+ 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"}
+
+ )}
+
+ )}
+ {getEventStatus(selectedEvent) === "LIVE" && (
+
+
+ Rejoindre en direct
+
+
+ )}
+ {getEventStatus(selectedEvent) === "PAST" && (
+
+ {
+ 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
+
+
+ )}
-
+
)}
{/* Feedback Modal */}
@@ -850,6 +846,6 @@ export default function EventsPageSection({
eventId={feedbackEventId}
onClose={() => setFeedbackEventId(null)}
/>
-
+
);
}
diff --git a/components/FeedbackModal.tsx b/components/FeedbackModal.tsx
index bcbe6a2..7396405 100644
--- a/components/FeedbackModal.tsx
+++ b/components/FeedbackModal.tsx
@@ -3,6 +3,15 @@
import { useState, useEffect, useTransition, type FormEvent } from "react";
import { useSession } from "next-auth/react";
import { createFeedback } from "@/actions/events/feedback";
+import {
+ Modal,
+ StarRating,
+ Textarea,
+ Button,
+ Alert,
+ SectionTitle,
+ CloseButton,
+} from "@/components/ui";
interface Event {
id: string;
@@ -163,129 +172,96 @@ export default function FeedbackModal({
if (!eventId) return null;
return (
-
-
e.stopPropagation()}
- >
-
- {/* Header */}
-
-
-
- FEEDBACK
-
-
-
- ×
-
-
-
- {loading ? (
-
Chargement...
- ) : !event ? (
-
- Événement introuvable
-
- ) : (
- <>
-
- {existingFeedback
- ? "Modifier votre feedback pour"
- : "Donnez votre avis sur"}
-
-
- {event.name}
-
-
- {success && (
-
- Feedback enregistré avec succès !
-
- )}
-
- {error && (
-
- {error}
-
- )}
-
-
- >
- )}
+
+ {/* Header */}
+
+
+ FEEDBACK
+
+
+
+ {loading ? (
+
Chargement...
+ ) : !event ? (
+
+ Événement introuvable
+
+ ) : (
+ <>
+
+ {existingFeedback
+ ? "Modifier votre feedback pour"
+ : "Donnez votre avis sur"}
+
+
+ {event.name}
+
+
+ {success && (
+
+ Feedback enregistré avec succès !
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ >
+ )}
-
+
);
}
diff --git a/components/HeroSection.tsx b/components/HeroSection.tsx
index f723751..b400796 100644
--- a/components/HeroSection.tsx
+++ b/components/HeroSection.tsx
@@ -1,28 +1,16 @@
"use client";
import Link from "next/link";
+import { Button, BackgroundSection } from "@/components/ui";
interface HeroSectionProps {
backgroundImage: string;
}
export default function HeroSection({ backgroundImage }: HeroSectionProps) {
-
return (
-
- {/* Background Image */}
-
- {/* Dark overlay for readability */}
-
-
-
- {/* Hero Content */}
-
+
+
{/* Game Title */}
@@ -62,18 +50,22 @@ export default function HeroSection({ backgroundImage }: HeroSectionProps) {
{/* Call-to-Action Buttons */}
-
- See events
-
+
+ See events
+
-
+
⏵
See leaderboard
-
+
-
+
);
}
diff --git a/components/ImageSelector.tsx b/components/ImageSelector.tsx
index 8f37ee9..4e25ec1 100644
--- a/components/ImageSelector.tsx
+++ b/components/ImageSelector.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useRef, type ChangeEvent } from "react";
+import { Input, Button, Card } from "@/components/ui";
interface ImageSelectorProps {
value: string;
@@ -119,20 +120,22 @@ export default function ImageSelector({
{/* Input URL */}
- setUrlInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()}
placeholder="https://example.com/image.jpg ou /image.jpg"
- className="flex-1 px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm min-w-0"
+ className="flex-1 text-xs sm:text-sm px-3 py-2 min-w-0"
/>
-
URL
-
+
{/* Upload depuis le disque */}
@@ -145,20 +148,25 @@ export default function ImageSelector({
className="hidden"
id={`file-${label}`}
/>
-
- {uploading ? "Upload..." : "Upload depuis le disque"}
+
+
+ {uploading ? "Upload..." : "Upload depuis le disque"}
+
- setShowGallery(!showGallery)}
- className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap"
+ variant="primary"
+ size="sm"
+ className="whitespace-nowrap"
>
{showGallery ? "Masquer" : "Galerie"}
-
+
{/* Chemin de l'image */}
@@ -170,7 +178,7 @@ export default function ImageSelector({
{/* Galerie d'images */}
{showGallery && (
-
+
Images disponibles
@@ -210,7 +218,7 @@ export default function ImageSelector({
))
)}
-
+
)}
);
diff --git a/components/Leaderboard.tsx b/components/Leaderboard.tsx
index 3871284..8b1640b 100644
--- a/components/Leaderboard.tsx
+++ b/components/Leaderboard.tsx
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
-import Avatar from "./Avatar";
+import { Avatar } from "@/components/ui";
interface LeaderboardEntry {
rank: number;
diff --git a/components/LeaderboardSection.tsx b/components/LeaderboardSection.tsx
index 991373e..69cd8fe 100644
--- a/components/LeaderboardSection.tsx
+++ b/components/LeaderboardSection.tsx
@@ -1,7 +1,14 @@
"use client";
import { useState } from "react";
-import Avatar from "./Avatar";
+import {
+ Avatar,
+ Modal,
+ CloseButton,
+ Card,
+ BackgroundSection,
+ SectionTitle,
+} from "@/components/ui";
interface LeaderboardEntry {
rank: number;
@@ -33,258 +40,222 @@ export default function LeaderboardSection({
);
return (
-
- {/* Background Image */}
-
+ {/* Title Section */}
+
- {/* Dark overlay for readability */}
-
-
+ LEADERBOARD
+
- {/* Content */}
-
- {/* Title Section */}
-
-
-
- LEADERBOARD
-
-
-
- ✦
- Top Players
- ✦
-
+ {/* Leaderboard Table */}
+
+ {/* Header */}
+
+
Rank
+
Player
+
Score
+
Level
- {/* Leaderboard Table */}
-
- {/* Header */}
-
-
Rank
-
Player
-
Score
-
Level
-
+ {/* Entries */}
+
+ {leaderboard.map((entry) => (
+
+ {/* Rank */}
+
+
+ {entry.rank}
+
+
- {/* Entries */}
-
- {leaderboard.map((entry) => (
-
- {/* Rank */}
-
+ {/* Player */}
+
+
+
setSelectedEntry(entry)}
+ >
- {entry.rank}
+ {entry.username}
-
-
- {/* Player */}
-
-
-
setSelectedEntry(entry)}
- >
-
- {entry.username}
+ {entry.characterClass && (
+
+ [{entry.characterClass === "WARRIOR" && "⚔️"}
+ {entry.characterClass === "MAGE" && "🔮"}
+ {entry.characterClass === "ROGUE" && "🗡️"}
+ {entry.characterClass === "RANGER" && "🏹"}
+ {entry.characterClass === "PALADIN" && "🛡️"}
+ {entry.characterClass === "ENGINEER" && "⚙️"}
+ {entry.characterClass === "MERCHANT" && "💰"}
+ {entry.characterClass === "SCHOLAR" && "📚"}
+ {entry.characterClass === "BERSERKER" && "🔥"}
+ {entry.characterClass === "NECROMANCER" && "💀"}]
- {entry.characterClass && (
-
- [{entry.characterClass === "WARRIOR" && "⚔️"}
- {entry.characterClass === "MAGE" && "🔮"}
- {entry.characterClass === "ROGUE" && "🗡️"}
- {entry.characterClass === "RANGER" && "🏹"}
- {entry.characterClass === "PALADIN" && "🛡️"}
- {entry.characterClass === "ENGINEER" && "⚙️"}
- {entry.characterClass === "MERCHANT" && "💰"}
- {entry.characterClass === "SCHOLAR" && "📚"}
- {entry.characterClass === "BERSERKER" && "🔥"}
- {entry.characterClass === "NECROMANCER" && "💀"}]
-
- )}
- {entry.rank <= 3 && (
- ✦
- )}
-
-
-
- {/* Score */}
-
-
- {formatScore(entry.score)}
-
-
-
- {/* Level */}
-
-
- Lv.{entry.level}
-
-
-
- ))}
-
-
-
- {/* Footer Info */}
-
-
- Compete with players worldwide and climb the ranks!
-
-
- Rankings update every hour
-
-
-
-
- {/* Character Modal */}
- {selectedEntry && (
-
setSelectedEntry(null)}
- >
-
e.stopPropagation()}
- >
-
- {/* Header */}
-
-
- {selectedEntry.username}
-
- setSelectedEntry(null)}
- className="text-gray-400 hover:text-pixel-gold text-2xl font-bold transition"
- >
- ×
-
-
-
- {/* Avatar and Class */}
-
-
-
-
- Rank #{selectedEntry.rank}
-
-
- {selectedEntry.email}
-
- {selectedEntry.characterClass && (
-
-
- {selectedEntry.characterClass === "WARRIOR" && "⚔️"}
- {selectedEntry.characterClass === "MAGE" && "🔮"}
- {selectedEntry.characterClass === "ROGUE" && "🗡️"}
- {selectedEntry.characterClass === "RANGER" && "🏹"}
- {selectedEntry.characterClass === "PALADIN" && "🛡️"}
- {selectedEntry.characterClass === "ENGINEER" && "⚙️"}
- {selectedEntry.characterClass === "MERCHANT" && "💰"}
- {selectedEntry.characterClass === "SCHOLAR" && "📚"}
- {selectedEntry.characterClass === "BERSERKER" && "🔥"}
- {selectedEntry.characterClass === "NECROMANCER" && "💀"}
-
-
- {selectedEntry.characterClass === "WARRIOR" &&
- "Guerrier"}
- {selectedEntry.characterClass === "MAGE" && "Mage"}
- {selectedEntry.characterClass === "ROGUE" && "Voleur"}
- {selectedEntry.characterClass === "RANGER" && "Rôdeur"}
- {selectedEntry.characterClass === "PALADIN" &&
- "Paladin"}
- {selectedEntry.characterClass === "ENGINEER" &&
- "Ingénieur"}
- {selectedEntry.characterClass === "MERCHANT" &&
- "Marchand"}
- {selectedEntry.characterClass === "SCHOLAR" && "Érudit"}
- {selectedEntry.characterClass === "BERSERKER" &&
- "Berserker"}
- {selectedEntry.characterClass === "NECROMANCER" &&
- "Nécromancien"}
-
-
+ )}
+ {entry.rank <= 3 && (
+
✦
)}
- {/* Stats */}
-
-
-
- Score
-
-
- {formatScore(selectedEntry.score)}
-
-
-
-
- Niveau
-
-
- Lv.{selectedEntry.level}
-
-
+ {/* Score */}
+
+
+ {formatScore(entry.score)}
+
- {/* Bio */}
- {selectedEntry.bio && (
-
-
- Bio
-
-
- {selectedEntry.bio}
-
-
- )}
+ {/* Level */}
+
+
+ Lv.{entry.level}
+
+
-
+ ))}
+
+
+ {/* Footer Info */}
+
+
+ Compete with players worldwide and climb the ranks!
+
+
Rankings update every hour
+
+
+ {/* Character Modal */}
+ {selectedEntry && (
+
setSelectedEntry(null)}
+ size="md"
+ >
+
+ {/* Header */}
+
+
+ {selectedEntry.username}
+
+ setSelectedEntry(null)} size="md" />
+
+
+ {/* Avatar and Class */}
+
+
+
+
+ Rank #{selectedEntry.rank}
+
+
+ {selectedEntry.email}
+
+ {selectedEntry.characterClass && (
+
+
+ {selectedEntry.characterClass === "WARRIOR" && "⚔️"}
+ {selectedEntry.characterClass === "MAGE" && "🔮"}
+ {selectedEntry.characterClass === "ROGUE" && "🗡️"}
+ {selectedEntry.characterClass === "RANGER" && "🏹"}
+ {selectedEntry.characterClass === "PALADIN" && "🛡️"}
+ {selectedEntry.characterClass === "ENGINEER" && "⚙️"}
+ {selectedEntry.characterClass === "MERCHANT" && "💰"}
+ {selectedEntry.characterClass === "SCHOLAR" && "📚"}
+ {selectedEntry.characterClass === "BERSERKER" && "🔥"}
+ {selectedEntry.characterClass === "NECROMANCER" && "💀"}
+
+
+ {selectedEntry.characterClass === "WARRIOR" && "Guerrier"}
+ {selectedEntry.characterClass === "MAGE" && "Mage"}
+ {selectedEntry.characterClass === "ROGUE" && "Voleur"}
+ {selectedEntry.characterClass === "RANGER" && "Rôdeur"}
+ {selectedEntry.characterClass === "PALADIN" && "Paladin"}
+ {selectedEntry.characterClass === "ENGINEER" &&
+ "Ingénieur"}
+ {selectedEntry.characterClass === "MERCHANT" &&
+ "Marchand"}
+ {selectedEntry.characterClass === "SCHOLAR" && "Érudit"}
+ {selectedEntry.characterClass === "BERSERKER" &&
+ "Berserker"}
+ {selectedEntry.characterClass === "NECROMANCER" &&
+ "Nécromancien"}
+
+
+ )}
+
+
+
+ {/* Stats */}
+
+
+
+ Score
+
+
+ {formatScore(selectedEntry.score)}
+
+
+
+
+ Niveau
+
+
+ Lv.{selectedEntry.level}
+
+
+
+
+ {/* Bio */}
+ {selectedEntry.bio && (
+
+
+ Bio
+
+
+ {selectedEntry.bio}
+
+
+ )}
+
+
)}
-
+
);
}
diff --git a/components/Navigation.tsx b/components/Navigation.tsx
index 2a9106b..d938577 100644
--- a/components/Navigation.tsx
+++ b/components/Navigation.tsx
@@ -5,6 +5,7 @@ import { useSession, signOut } from "next-auth/react";
import { useState } from "react";
import { usePathname } from "next/navigation";
import PlayerStats from "./PlayerStats";
+import { Button } from "@/components/ui";
interface UserData {
username: string;
@@ -100,12 +101,14 @@ export default function Navigation({
{/* Desktop Auth Buttons */}
{isAuthenticated ? (
-
signOut()}
- className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
+ variant="ghost"
+ size="sm"
+ className="text-xs font-normal"
>
Déconnexion
-
+
) : (
<>
Connexion
-
- Inscription
+
+
+ Inscription
+
>
)}
@@ -197,15 +199,17 @@ export default function Navigation({
{/* Mobile Auth Buttons */}
{isAuthenticated ? (
-
{
signOut();
setIsMenuOpen(false);
}}
- className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest text-left py-2"
+ variant="ghost"
+ size="sm"
+ className="text-xs font-normal text-left py-2"
>
Déconnexion
-
+
) : (
<>
Connexion
-
setIsMenuOpen(false)}
- className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition text-center"
- >
- Inscription
+
setIsMenuOpen(false)}>
+
+ Inscription
+
>
)}
diff --git a/components/PlayerStats.tsx b/components/PlayerStats.tsx
index c8b31f3..d4b1b3c 100644
--- a/components/PlayerStats.tsx
+++ b/components/PlayerStats.tsx
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import Link from "next/link";
-import Avatar from "./Avatar";
+import { Avatar } from "@/components/ui";
interface UserData {
username: string;
diff --git a/components/ProfileForm.tsx b/components/ProfileForm.tsx
index ffdef49..aef95a8 100644
--- a/components/ProfileForm.tsx
+++ b/components/ProfileForm.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState, useRef, useTransition, type ChangeEvent } from "react";
-import Avatar from "./Avatar";
+import { Avatar, Input, Textarea, Button, Alert, Card, BackgroundSection, SectionTitle, ProgressBar } from "@/components/ui";
import { updateProfile } from "@/actions/profile/update-profile";
import { updatePassword } from "@/actions/profile/update-password";
@@ -170,53 +170,19 @@ export default function ProfileForm({
: "from-red-700 to-red-900";
return (
-
- {/* Background Image */}
-
- {/* Dark overlay for readability */}
-
-
-
- {/* Content */}
-
+
+
{/* Title Section */}
-
-
-
- PROFIL
-
-
-
- ✦
- Gérez votre profil
- ✦
-
-
+
+ PROFIL
+
{/* Profile Card */}
-
{/* Username Field */}
-
-
- Nom d'utilisateur
-
-
setUsername(e.target.value)}
- className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
- required
- minLength={3}
- maxLength={20}
- />
-
3-20 caractères
-
+
setUsername(e.target.value)}
+ required
+ minLength={3}
+ maxLength={20}
+ className="bg-black/40"
+ />
+
3-20 caractères
{/* Bio Field */}
-
+
{/* Email (read-only) */}
@@ -505,13 +445,9 @@ export default function ProfileForm({
{/* Submit Button */}
-
+
{isPending ? "Enregistrement..." : "Enregistrer les modifications"}
-
+
@@ -522,65 +458,56 @@ export default function ProfileForm({
Mot de passe
{!showPasswordForm && (
- setShowPasswordForm(true)}
- className="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"
>
Changer le mot de passe
-
+
)}
{showPasswordForm && (
)}
-
+
-
+
);
}
diff --git a/components/UserManagement.tsx b/components/UserManagement.tsx
index fb707dc..227208f 100644
--- a/components/UserManagement.tsx
+++ b/components/UserManagement.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, useTransition } from "react";
-import Avatar from "./Avatar";
+import { Avatar, Input, Button, Card } from "@/components/ui";
import { updateUser, deleteUser } from "@/actions/admin/users";
interface User {
@@ -184,10 +184,7 @@ export default function UserManagement() {
: user.username;
return (
-
+
{/* Avatar */}
@@ -248,23 +245,19 @@ export default function UserManagement() {
{isEditing ? (
@@ -690,19 +687,21 @@ export default function UserManagement() {
-
{saving ? "Enregistrement..." : "Enregistrer"}
-
-
+
Annuler
-
+
) : (
@@ -747,7 +746,7 @@ export default function UserManagement() {
)}
-
+
);
})
)}
diff --git a/components/ui/Alert.tsx b/components/ui/Alert.tsx
new file mode 100644
index 0000000..41d6fb8
--- /dev/null
+++ b/components/ui/Alert.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { HTMLAttributes, ReactNode } from "react";
+
+interface AlertProps extends HTMLAttributes
{
+ children: ReactNode;
+ variant?: "success" | "error" | "warning" | "info";
+}
+
+const variantClasses = {
+ success: "bg-green-900/50 border-green-500/50 text-green-400",
+ error: "bg-red-900/50 border-red-500/50 text-red-400",
+ warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
+ info: "bg-blue-900/50 border-blue-500/50 text-blue-400",
+};
+
+export default function Alert({
+ children,
+ variant = "info",
+ className = "",
+ ...props
+}: AlertProps) {
+ return (
+
+ {children}
+
+ );
+}
+
diff --git a/components/Avatar.tsx b/components/ui/Avatar.tsx
similarity index 100%
rename from components/Avatar.tsx
rename to components/ui/Avatar.tsx
diff --git a/components/ui/BackgroundSection.tsx b/components/ui/BackgroundSection.tsx
new file mode 100644
index 0000000..0af4828
--- /dev/null
+++ b/components/ui/BackgroundSection.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { HTMLAttributes, ReactNode } from "react";
+
+interface BackgroundSectionProps extends HTMLAttributes {
+ children: ReactNode;
+ backgroundImage: string;
+ overlay?: boolean;
+}
+
+export default function BackgroundSection({
+ children,
+ backgroundImage,
+ overlay = true,
+ className = "",
+ ...props
+}: BackgroundSectionProps) {
+ return (
+
+ {/* Background Image */}
+
+ {/* Dark overlay for readability */}
+ {overlay && (
+
+ )}
+
+
+ {/* Content */}
+
+ {children}
+
+
+ );
+}
+
diff --git a/components/ui/Badge.tsx b/components/ui/Badge.tsx
new file mode 100644
index 0000000..733657a
--- /dev/null
+++ b/components/ui/Badge.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { HTMLAttributes, ReactNode } from "react";
+
+interface BadgeProps extends HTMLAttributes {
+ children: ReactNode;
+ variant?: "default" | "success" | "warning" | "danger" | "info";
+ size?: "sm" | "md";
+}
+
+const variantClasses = {
+ default: "bg-pixel-gold/20 border-pixel-gold/50 text-pixel-gold",
+ success: "bg-green-900/50 border-green-500/50 text-green-400",
+ warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
+ danger: "bg-red-900/50 border-red-500/50 text-red-400",
+ info: "bg-blue-900/30 border-blue-500/50 text-blue-400",
+};
+
+const sizeClasses = {
+ sm: "px-2 py-1 text-[10px] sm:text-xs",
+ md: "px-3 py-1 text-xs",
+};
+
+export default function Badge({
+ children,
+ variant = "default",
+ size = "sm",
+ className = "",
+ ...props
+}: BadgeProps) {
+ return (
+
+ {children}
+
+ );
+}
+
diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx
new file mode 100644
index 0000000..c79aaba
--- /dev/null
+++ b/components/ui/Button.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { ButtonHTMLAttributes, ReactNode, ElementType } from "react";
+
+interface ButtonProps extends ButtonHTMLAttributes {
+ variant?: "primary" | "secondary" | "success" | "danger" | "ghost";
+ size?: "sm" | "md" | "lg";
+ children: ReactNode;
+ as?: ElementType;
+}
+
+const variantClasses = {
+ primary:
+ "border-pixel-gold/50 bg-black/60 text-white hover:bg-pixel-gold/10 hover:border-pixel-gold",
+ secondary:
+ "border-gray-600/50 bg-gray-900/20 text-gray-400 hover:bg-gray-900/30 hover:border-gray-500",
+ success:
+ "border-green-500/50 bg-green-900/20 text-green-400 hover:bg-green-900/30",
+ danger: "border-red-500/50 bg-red-900/20 text-red-400 hover:bg-red-900/30",
+ ghost: "border-transparent bg-transparent text-white hover:text-pixel-gold",
+};
+
+const sizeClasses = {
+ sm: "px-2 sm:px-3 py-1 text-[10px] sm:text-xs",
+ md: "px-4 py-2 text-xs",
+ lg: "px-6 py-3 text-sm",
+};
+
+export default function Button({
+ variant = "primary",
+ size = "md",
+ className = "",
+ disabled,
+ children,
+ as: Component = "button",
+ ...props
+}: ButtonProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/ui/Card.tsx b/components/ui/Card.tsx
new file mode 100644
index 0000000..638c1e0
--- /dev/null
+++ b/components/ui/Card.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { HTMLAttributes, ReactNode } from "react";
+
+interface CardProps extends HTMLAttributes {
+ children: ReactNode;
+ variant?: "default" | "dark";
+}
+
+const variantClasses = {
+ default: "bg-black/60 border border-pixel-gold/30",
+ dark: "bg-black/80 border border-pixel-gold/30",
+};
+
+export default function Card({
+ children,
+ variant = "default",
+ className = "",
+ ...props
+}: CardProps) {
+ return (
+
+ {children}
+
+ );
+}
+
diff --git a/components/ui/CloseButton.tsx b/components/ui/CloseButton.tsx
new file mode 100644
index 0000000..3a7cd85
--- /dev/null
+++ b/components/ui/CloseButton.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { ButtonHTMLAttributes } from "react";
+
+interface CloseButtonProps extends ButtonHTMLAttributes {
+ size?: "sm" | "md" | "lg";
+}
+
+const sizeClasses = {
+ sm: "text-xl",
+ md: "text-2xl",
+ lg: "text-3xl",
+};
+
+export default function CloseButton({
+ size = "md",
+ className = "",
+ ...props
+}: CloseButtonProps) {
+ return (
+
+ ×
+
+ );
+}
+
diff --git a/components/ui/Input.tsx b/components/ui/Input.tsx
new file mode 100644
index 0000000..59ede04
--- /dev/null
+++ b/components/ui/Input.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { InputHTMLAttributes, forwardRef } from "react";
+
+interface InputProps extends InputHTMLAttributes {
+ label?: string;
+ error?: string;
+}
+
+const Input = forwardRef(
+ ({ label, error, className = "", ...props }, ref) => {
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {error && (
+
{error}
+ )}
+
+ );
+ }
+);
+
+Input.displayName = "Input";
+
+export default Input;
+
diff --git a/components/ui/Modal.tsx b/components/ui/Modal.tsx
new file mode 100644
index 0000000..e38a8e1
--- /dev/null
+++ b/components/ui/Modal.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { ReactNode, useEffect } from "react";
+
+interface ModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ children: ReactNode;
+ size?: "sm" | "md" | "lg" | "xl";
+ closeOnOverlayClick?: boolean;
+}
+
+const sizeClasses = {
+ sm: "max-w-md",
+ md: "max-w-2xl",
+ lg: "max-w-3xl",
+ xl: "max-w-4xl",
+};
+
+export default function Modal({
+ isOpen,
+ onClose,
+ children,
+ size = "md",
+ closeOnOverlayClick = true,
+}: ModalProps) {
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "";
+ }
+ return () => {
+ document.body.style.overflow = "";
+ };
+ }, [isOpen]);
+
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {children}
+
+
+ );
+}
+
diff --git a/components/ui/ProgressBar.tsx b/components/ui/ProgressBar.tsx
new file mode 100644
index 0000000..143425a
--- /dev/null
+++ b/components/ui/ProgressBar.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import { HTMLAttributes } from "react";
+
+interface ProgressBarProps extends HTMLAttributes {
+ value: number;
+ max: number;
+ variant?: "hp" | "xp" | "default";
+ showLabel?: boolean;
+ label?: string;
+}
+
+const variantClasses = {
+ hp: {
+ high: "from-green-600 to-green-700",
+ medium: "from-yellow-600 to-orange-700",
+ low: "from-red-700 to-red-900",
+ },
+ xp: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
+ default: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
+};
+
+export default function ProgressBar({
+ value,
+ max,
+ variant = "default",
+ showLabel = false,
+ label,
+ className = "",
+ ...props
+}: ProgressBarProps) {
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
+
+ let gradientClass = "";
+ if (variant === "hp") {
+ if (percentage > 60) {
+ gradientClass = variantClasses.hp.high;
+ } else if (percentage > 30) {
+ gradientClass = variantClasses.hp.medium;
+ } else {
+ gradientClass = variantClasses.hp.low;
+ }
+ } else if (variant === "xp") {
+ gradientClass = variantClasses.xp;
+ } else {
+ gradientClass = variantClasses.default;
+ }
+
+ return (
+
+ {showLabel && (
+
+ {label || variant.toUpperCase()}
+
+ {value} / {max}
+
+
+ )}
+
+
+ {variant === "hp" && percentage < 30 && (
+
+ )}
+
+
+ );
+}
+
diff --git a/components/ui/SectionTitle.tsx b/components/ui/SectionTitle.tsx
new file mode 100644
index 0000000..4bc53b7
--- /dev/null
+++ b/components/ui/SectionTitle.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { HTMLAttributes, ReactNode } from "react";
+
+interface SectionTitleProps extends HTMLAttributes {
+ children: ReactNode;
+ variant?: "default" | "gradient" | "gold";
+ size?: "sm" | "md" | "lg" | "xl";
+ subtitle?: ReactNode;
+}
+
+const sizeClasses = {
+ sm: "text-2xl sm:text-3xl",
+ md: "text-3xl sm:text-4xl md:text-5xl",
+ lg: "text-4xl sm:text-5xl md:text-6xl lg:text-7xl",
+ xl: "text-5xl md:text-7xl",
+};
+
+export default function SectionTitle({
+ children,
+ variant = "default",
+ size = "md",
+ subtitle,
+ className = "",
+ ...props
+}: SectionTitleProps) {
+ const baseClasses = "font-gaming font-black tracking-tight mb-4";
+
+ let titleClasses = `${baseClasses} ${sizeClasses[size]} ${className}`;
+
+ if (variant === "gradient") {
+ titleClasses += " bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent";
+ } else if (variant === "gold") {
+ titleClasses += " text-pixel-gold";
+ } else {
+ titleClasses += " text-white";
+ }
+
+ return (
+
+
+ {variant === "gradient" ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
+ {subtitle && (
+
+ ✦
+ {subtitle}
+ ✦
+
+ )}
+
+ );
+}
+
diff --git a/components/ui/StarRating.tsx b/components/ui/StarRating.tsx
new file mode 100644
index 0000000..08b55b4
--- /dev/null
+++ b/components/ui/StarRating.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { useState } from "react";
+
+interface StarRatingProps {
+ value: number;
+ onChange?: (rating: number) => void;
+ disabled?: boolean;
+ size?: "sm" | "md" | "lg";
+ showValue?: boolean;
+}
+
+const sizeClasses = {
+ sm: "text-lg sm:text-xl",
+ md: "text-2xl sm:text-3xl",
+ lg: "text-4xl",
+};
+
+export default function StarRating({
+ value,
+ onChange,
+ disabled = false,
+ size = "md",
+ showValue = false,
+}: StarRatingProps) {
+ const [hoverValue, setHoverValue] = useState(0);
+
+ const handleClick = (rating: number) => {
+ if (!disabled && onChange) {
+ onChange(rating);
+ }
+ };
+
+ const displayValue = hoverValue || value;
+
+ return (
+
+
+ {[1, 2, 3, 4, 5].map((star) => (
+ handleClick(star)}
+ disabled={disabled}
+ onMouseEnter={() => !disabled && setHoverValue(star)}
+ onMouseLeave={() => !disabled && setHoverValue(0)}
+ className={`transition-transform hover:scale-110 disabled:hover:scale-100 disabled:cursor-not-allowed ${
+ star <= displayValue
+ ? "text-pixel-gold"
+ : "text-gray-600 hover:text-gray-500"
+ } ${sizeClasses[size]}`}
+ aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
+ >
+ ★
+
+ ))}
+
+ {showValue && value > 0 && (
+
+ {value}/5
+
+ )}
+
+ );
+}
+
diff --git a/components/ui/Textarea.tsx b/components/ui/Textarea.tsx
new file mode 100644
index 0000000..e59694f
--- /dev/null
+++ b/components/ui/Textarea.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import { TextareaHTMLAttributes, forwardRef } from "react";
+
+interface TextareaProps extends TextareaHTMLAttributes {
+ label?: string;
+ error?: string;
+ showCharCount?: boolean;
+ maxLength?: number;
+}
+
+const Textarea = forwardRef(
+ ({ label, error, showCharCount, maxLength, className = "", value, ...props }, ref) => {
+ const charCount = typeof value === "string" ? value.length : 0;
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {showCharCount && maxLength && (
+
+ {charCount}/{maxLength} caractères
+
+ )}
+ {error && (
+
{error}
+ )}
+
+ );
+ }
+);
+
+Textarea.displayName = "Textarea";
+
+export default Textarea;
+
diff --git a/components/ui/index.ts b/components/ui/index.ts
new file mode 100644
index 0000000..525aaac
--- /dev/null
+++ b/components/ui/index.ts
@@ -0,0 +1,14 @@
+export { default as Avatar } from "./Avatar";
+export { default as Button } from "./Button";
+export { default as Input } from "./Input";
+export { default as Textarea } from "./Textarea";
+export { default as Card } from "./Card";
+export { default as Modal } from "./Modal";
+export { default as Badge } from "./Badge";
+export { default as ProgressBar } from "./ProgressBar";
+export { default as StarRating } from "./StarRating";
+export { default as SectionTitle } from "./SectionTitle";
+export { default as BackgroundSection } from "./BackgroundSection";
+export { default as Alert } from "./Alert";
+export { default as CloseButton } from "./CloseButton";
+