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

@@ -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 (
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
<h1 className="text-4xl font-gaming font-black mb-8 text-center">
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
ADMIN
</span>
</h1>
<SectionTitle variant="gradient" size="md" className="mb-8 text-center">
ADMIN
</SectionTitle>
{/* Navigation Tabs */}
<div className="flex gap-4 mb-8 justify-center">
<button
<div className="flex gap-4 mb-8 justify-center flex-wrap">
<Button
onClick={() => 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
</button>
<button
</Button>
<Button
onClick={() => 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
</button>
<button
</Button>
<Button
onClick={() => 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
</button>
<button
</Button>
<Button
onClick={() => 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
</button>
</Button>
</div>
{activeSection === "preferences" && (
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-4 sm:p-6 backdrop-blur-sm">
<h2 className="text-xl sm:text-2xl font-gaming font-bold mb-6 text-pixel-gold break-words">
Préférences UI Globales
</h2>
<Card variant="dark" className="p-4 sm:p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<h2 className="text-xl sm:text-2xl font-gaming font-bold text-pixel-gold break-words">
Préférences UI Globales
</h2>
<Link href="/admin/style-guide" target="_blank">
<Button variant="primary" size="sm">
📖 Voir le Style Guide
</Button>
</Link>
</div>
<div className="space-y-4">
<BackgroundPreferences initialPreferences={initialPreferences} />
</div>
</div>
</Card>
)}
{activeSection === "users" && (
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
<Card variant="dark" className="p-6">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Utilisateurs
</h2>
<UserManagement />
</div>
</Card>
)}
{activeSection === "events" && (
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
<Card variant="dark" className="p-6">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Événements
</h2>
<EventManagement />
</div>
</Card>
)}
{activeSection === "feedbacks" && (
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
<Card variant="dark" className="p-6">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Feedbacks
</h2>
<FeedbackManagement />
</div>
</Card>
)}
</div>
</section>

View File

@@ -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 (
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4">
<Card variant="default" className="p-3 sm:p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
<div className="min-w-0 flex-1">
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
@@ -153,12 +154,14 @@ export default function BackgroundPreferences({
</p>
</div>
{!isEditing && (
<button
<Button
onClick={handleEdit}
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 flex-shrink-0"
variant="primary"
size="sm"
className="whitespace-nowrap flex-shrink-0"
>
Modifier
</button>
</Button>
)}
</div>
@@ -195,18 +198,12 @@ export default function BackgroundPreferences({
label="Background Leaderboard"
/>
<div className="flex flex-col sm:flex-row gap-2 pt-4">
<button
onClick={handleSave}
className="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"
>
<Button onClick={handleSave} variant="success" size="md">
Enregistrer
</button>
<button
onClick={handleCancel}
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
>
</Button>
<Button onClick={handleCancel} variant="secondary" size="md">
Annuler
</button>
</Button>
</div>
</div>
) : (
@@ -381,6 +378,6 @@ export default function BackgroundPreferences({
</div>
</div>
)}
</div>
</Card>
);
}

View File

@@ -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})
</h3>
{!isCreating && !editingEvent && (
<button
<Button
onClick={handleCreate}
className="px-3 sm:px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-green-900/30 transition whitespace-nowrap flex-shrink-0"
variant="success"
size="sm"
className="whitespace-nowrap flex-shrink-0"
>
+ Nouvel événement
</button>
</Button>
)}
</div>
{(isCreating || editingEvent) && (
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4 mb-4">
<Card variant="default" className="p-3 sm:p-4 mb-4">
<h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words">
{isCreating ? "Créer un événement" : "Modifier l'événement"}
</h4>
<div className="space-y-4">
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Date
</label>
<input
type="date"
value={formData.date}
onChange={(e) =>
setFormData({ ...formData, date: e.target.value })
}
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Nom
</label>
<input
type="text"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
placeholder="Nom de l'événement"
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Description
</label>
<textarea
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
placeholder="Description de l'événement"
rows={4}
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<Input
type="date"
label="Date"
value={formData.date}
onChange={(e) =>
setFormData({ ...formData, date: e.target.value })
}
className="text-xs sm:text-sm px-3 py-2"
/>
<Input
type="text"
label="Nom"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
placeholder="Nom de l'événement"
className="text-xs sm:text-sm px-3 py-2"
/>
<Textarea
label="Description"
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
placeholder="Description de l'événement"
rows={4}
className="text-xs sm:text-sm px-3 py-2"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
@@ -289,71 +280,57 @@ export default function EventManagement() {
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Salle
</label>
<input
type="text"
value={formData.room || ""}
onChange={(e) =>
setFormData({ ...formData, room: e.target.value })
}
placeholder="Ex: Nautilus"
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Heure
</label>
<input
type="text"
value={formData.time || ""}
onChange={(e) =>
setFormData({ ...formData, time: e.target.value })
}
placeholder="Ex: 11h-12h"
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Places max
</label>
<input
type="number"
value={formData.maxPlaces || ""}
onChange={(e) =>
setFormData({
...formData,
maxPlaces: e.target.value
? parseInt(e.target.value)
: undefined,
})
}
placeholder="Ex: 25"
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<Input
type="text"
label="Salle"
value={formData.room || ""}
onChange={(e) =>
setFormData({ ...formData, room: e.target.value })
}
placeholder="Ex: Nautilus"
className="text-xs sm:text-sm px-3 py-2"
/>
<Input
type="text"
label="Heure"
value={formData.time || ""}
onChange={(e) =>
setFormData({ ...formData, time: e.target.value })
}
placeholder="Ex: 11h-12h"
className="text-xs sm:text-sm px-3 py-2"
/>
<Input
type="number"
label="Places max"
value={formData.maxPlaces || ""}
onChange={(e) =>
setFormData({
...formData,
maxPlaces: e.target.value
? parseInt(e.target.value)
: undefined,
})
}
placeholder="Ex: 25"
className="text-xs sm:text-sm px-3 py-2"
/>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<button
<Button
onClick={handleSave}
variant="success"
size="md"
disabled={saving}
className="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"
>
{saving ? "Enregistrement..." : "Enregistrer"}
</button>
<button
onClick={handleCancel}
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
>
</Button>
<Button onClick={handleCancel} variant="secondary" size="md">
Annuler
</button>
</Button>
</div>
</div>
</div>
</Card>
)}
{events.length === 0 ? (
@@ -362,80 +339,82 @@ export default function EventManagement() {
</div>
) : (
<div className="space-y-3">
{events.map((event) => (
<div
key={event.id}
className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4"
>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
{event.name}
</h4>
<span className="px-2 py-1 bg-pixel-gold/20 border border-pixel-gold/50 text-pixel-gold text-[10px] sm:text-xs uppercase rounded whitespace-nowrap flex-shrink-0">
{getEventTypeLabel(event.type)}
</span>
<span
className={`px-2 py-1 text-[10px] sm:text-xs uppercase rounded whitespace-nowrap flex-shrink-0 ${(() => {
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))}
</span>
</div>
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
{event.description}
</p>
<div className="flex flex-wrap items-center gap-2 sm:gap-4 mt-2">
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
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 (
<Card key={event.id} variant="default" className="p-3 sm:p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
{event.name}
</h4>
<Badge variant="default" size="sm">
{getEventTypeLabel(event.type)}
</Badge>
<Badge variant={statusVariant} size="sm">
{getStatusLabel(status)}
</Badge>
</div>
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
{event.description}
</p>
{event.room && (
<div className="flex flex-wrap items-center gap-2 sm:gap-4 mt-2">
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
📍 Salle: {event.room}
Date: {new Date(event.date).toLocaleDateString("fr-FR")}
</p>
)}
{event.time && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
🕐 Heure: {event.time}
</p>
)}
{event.maxPlaces && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
👥 Places: {event.maxPlaces}
</p>
)}
<span className="px-2 py-1 bg-blue-900/30 border border-blue-500/50 text-blue-400 text-[10px] sm:text-xs rounded whitespace-nowrap flex-shrink-0">
{event.registrationsCount || 0} inscrit
{event.registrationsCount !== 1 ? "s" : ""}
</span>
{event.room && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
📍 Salle: {event.room}
</p>
)}
{event.time && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
🕐 Heure: {event.time}
</p>
)}
{event.maxPlaces && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
👥 Places: {event.maxPlaces}
</p>
)}
<Badge variant="info" size="sm">
{event.registrationsCount || 0} inscrit
{event.registrationsCount !== 1 ? "s" : ""}
</Badge>
</div>
</div>
{!isCreating && !editingEvent && (
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
<Button
onClick={() => handleEdit(event)}
variant="primary"
size="sm"
className="whitespace-nowrap"
>
Modifier
</Button>
<Button
onClick={() => handleDelete(event.id)}
variant="danger"
size="sm"
className="whitespace-nowrap"
>
Supprimer
</Button>
</div>
)}
</div>
{!isCreating && !editingEvent && (
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
<button
onClick={() => 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
</button>
<button
onClick={() => 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
</button>
</div>
)}
</div>
</div>
))}
</Card>
);
})}
</div>
)}
</div>

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>
);
}

View File

@@ -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 (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={handleClose}
<Modal
isOpen={!!eventId}
onClose={handleClose}
size="md"
closeOnOverlayClick={!submitting}
>
<div
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-2xl 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">
<h1 className="text-4xl font-gaming font-black">
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
FEEDBACK
</span>
</h1>
<button
onClick={handleClose}
disabled={submitting}
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition disabled:opacity-50 disabled:cursor-not-allowed"
>
×
</button>
</div>
{loading ? (
<div className="text-white text-center py-8">Chargement...</div>
) : !event ? (
<div className="text-red-400 text-center py-8">
Événement introuvable
</div>
) : (
<>
<p className="text-gray-400 text-sm text-center mb-2">
{existingFeedback
? "Modifier votre feedback pour"
: "Donnez votre avis sur"}
</p>
<p className="text-pixel-gold text-lg font-semibold text-center mb-8">
{event.name}
</p>
{success && (
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm mb-6">
Feedback enregistré avec succès !
</div>
)}
{error && (
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm mb-6">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Rating */}
<div>
<label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">
Note
</label>
<div className="flex items-center justify-center gap-2">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => setRating(star)}
disabled={submitting}
className={`text-4xl transition-transform hover:scale-110 disabled:hover:scale-100 ${
star <= rating
? "text-pixel-gold"
: "text-gray-600 hover:text-gray-500"
}`}
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
>
</button>
))}
</div>
<p className="text-gray-500 text-xs text-center mt-2">
{rating > 0 && `${rating}/5`}
</p>
</div>
{/* Comment */}
<div>
<label
htmlFor="comment"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
>
Commentaire (optionnel)
</label>
<textarea
id="comment"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={6}
maxLength={1000}
disabled={submitting}
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none disabled:opacity-50"
placeholder="Partagez votre expérience, vos suggestions..."
/>
<p className="text-gray-500 text-xs mt-1 text-right">
{comment.length}/1000 caractères
</p>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={submitting || rating === 0}
className="w-full px-6 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{submitting
? "Enregistrement..."
: existingFeedback
? "Modifier le feedback"
: "Envoyer le feedback"}
</button>
</form>
</>
)}
<div className="p-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<SectionTitle variant="gradient" size="lg">
FEEDBACK
</SectionTitle>
<CloseButton onClick={handleClose} disabled={submitting} size="lg" />
</div>
{loading ? (
<div className="text-white text-center py-8">Chargement...</div>
) : !event ? (
<Alert variant="error" className="text-center">
Événement introuvable
</Alert>
) : (
<>
<p className="text-gray-400 text-sm text-center mb-2">
{existingFeedback
? "Modifier votre feedback pour"
: "Donnez votre avis sur"}
</p>
<p className="text-pixel-gold text-lg font-semibold text-center mb-8">
{event.name}
</p>
{success && (
<Alert variant="success" className="mb-6">
Feedback enregistré avec succès !
</Alert>
)}
{error && (
<Alert variant="error" className="mb-6">
{error}
</Alert>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Rating */}
<div>
<label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">
Note
</label>
<StarRating
value={rating}
onChange={setRating}
disabled={submitting}
size="lg"
showValue
/>
</div>
{/* Comment */}
<Textarea
id="comment"
label="Commentaire (optionnel)"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={6}
maxLength={1000}
disabled={submitting}
showCharCount
placeholder="Partagez votre expérience, vos suggestions..."
/>
{/* Submit Button */}
<Button
type="submit"
variant="primary"
size="lg"
disabled={submitting || rating === 0}
className="w-full"
>
{submitting
? "Enregistrement..."
: existingFeedback
? "Modifier le feedback"
: "Envoyer le feedback"}
</Button>
</form>
</>
)}
</div>
</div>
</Modal>
);
}

View File

@@ -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 (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* Dark overlay for readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80 z-[1]"></div>
</div>
{/* Hero Content */}
<div className="relative z-10 w-full max-w-5xl xl:max-w-6xl mx-auto px-4 sm:px-8 py-16 text-center flex flex-col items-center">
<BackgroundSection backgroundImage={backgroundImage} className="pt-24">
<div className="text-center flex flex-col items-center">
{/* Game Title */}
<div className="w-full flex justify-center mb-4 overflow-hidden">
<h1 className="text-4xl sm:text-5xl md:text-8xl lg:text-9xl xl:text-9xl font-gaming font-black tracking-tight relative break-words">
@@ -62,18 +50,22 @@ export default function HeroSection({ backgroundImage }: HeroSectionProps) {
{/* Call-to-Action Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
<Link href="/events">
<button className="px-8 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition">
<span>See events</span>
</button>
<Button variant="primary" size="lg">
See events
</Button>
</Link>
<Link href="/leaderboard">
<button className="px-8 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition flex items-center gap-2">
<Button
variant="primary"
size="lg"
className="flex items-center gap-2"
>
<span></span>
<span>See leaderboard</span>
</button>
</Button>
</Link>
</div>
</div>
</section>
</BackgroundSection>
);
}

View File

@@ -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({
<div className="flex-1 space-y-3 min-w-0">
{/* Input URL */}
<div className="flex flex-col sm:flex-row gap-2">
<input
<Input
type="text"
value={urlInput}
onChange={(e) => 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"
/>
<button
<Button
onClick={handleUrlSubmit}
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 flex-shrink-0"
variant="primary"
size="sm"
className="whitespace-nowrap flex-shrink-0"
>
URL
</button>
</Button>
</div>
{/* Upload depuis le disque */}
@@ -145,20 +148,25 @@ export default function ImageSelector({
className="hidden"
id={`file-${label}`}
/>
<label
htmlFor={`file-${label}`}
className={`flex-1 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 text-center cursor-pointer ${
uploading ? "opacity-50 cursor-not-allowed" : ""
}`}
>
{uploading ? "Upload..." : "Upload depuis le disque"}
<label htmlFor={`file-${label}`}>
<Button
variant="primary"
size="sm"
as="span"
disabled={uploading}
className="flex-1 text-center cursor-pointer"
>
{uploading ? "Upload..." : "Upload depuis le disque"}
</Button>
</label>
<button
<Button
onClick={() => 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"}
</button>
</Button>
</div>
{/* Chemin de l'image */}
@@ -170,7 +178,7 @@ export default function ImageSelector({
{/* Galerie d'images */}
{showGallery && (
<div className="mt-4 p-3 sm:p-4 bg-black/40 border border-pixel-gold/20 rounded">
<Card variant="dark" className="mt-4 p-3 sm:p-4">
<h4 className="text-xs sm:text-sm text-gray-300 mb-3">
Images disponibles
</h4>
@@ -210,7 +218,7 @@ export default function ImageSelector({
))
)}
</div>
</div>
</Card>
)}
</div>
);

View File

@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import Avatar from "./Avatar";
import { Avatar } from "@/components/ui";
interface LeaderboardEntry {
rank: number;

View File

@@ -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 (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center 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="lg"
subtitle="Top Players"
className="mb-12 overflow-hidden"
>
{/* Dark overlay for readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
LEADERBOARD
</SectionTitle>
{/* Content */}
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm:px-8 py-16">
{/* Title Section */}
<div className="text-center mb-12 overflow-hidden">
<h1 className="text-3xl sm:text-4xl md:text-7xl font-gaming font-black mb-4 tracking-tight break-words">
<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)",
}}
>
LEADERBOARD
</span>
</h1>
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
<span></span>
<span>Top Players</span>
<span></span>
</div>
{/* Leaderboard Table */}
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
{/* Header */}
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
<div className="col-span-5 sm:col-span-6">Player</div>
<div className="col-span-3 text-right">Score</div>
<div className="col-span-2 text-right">Level</div>
</div>
{/* Leaderboard Table */}
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
{/* Header */}
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
<div className="col-span-5 sm:col-span-6">Player</div>
<div className="col-span-3 text-right">Score</div>
<div className="col-span-2 text-right">Level</div>
</div>
{/* Entries */}
<div className="divide-y divide-pixel-gold/10 overflow-visible">
{leaderboard.map((entry) => (
<div
key={entry.rank}
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative ${
entry.rank <= 3
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
: "bg-black/40"
}`}
>
{/* Rank */}
<div className="col-span-2 sm:col-span-1 flex items-center justify-center">
<span
className={`inline-flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-bold text-xs sm:text-sm ${
entry.rank === 1
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
: entry.rank === 2
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
: entry.rank === 3
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
: "bg-gray-900 text-gray-400 border border-gray-800"
}`}
>
{entry.rank}
</span>
</div>
{/* Entries */}
<div className="divide-y divide-pixel-gold/10 overflow-visible">
{leaderboard.map((entry) => (
<div
key={entry.rank}
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative ${
entry.rank <= 3
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
: "bg-black/40"
}`}
>
{/* Rank */}
<div className="col-span-2 sm:col-span-1 flex items-center justify-center">
{/* Player */}
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
<Avatar
src={entry.avatar}
username={entry.username}
size="sm"
className="flex-shrink-0"
borderClassName="border-pixel-gold/30"
/>
<div
className="flex items-center gap-1 sm:gap-2 cursor-pointer hover:opacity-80 transition min-w-0"
onClick={() => setSelectedEntry(entry)}
>
<span
className={`inline-flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-bold text-xs sm:text-sm ${
entry.rank === 1
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
: entry.rank === 2
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
: entry.rank === 3
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
: "bg-gray-900 text-gray-400 border border-gray-800"
className={`font-bold text-xs sm:text-sm truncate ${
entry.rank <= 3 ? "text-pixel-gold" : "text-white"
}`}
>
{entry.rank}
{entry.username}
</span>
</div>
{/* Player */}
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
<Avatar
src={entry.avatar}
username={entry.username}
size="sm"
className="flex-shrink-0"
borderClassName="border-pixel-gold/30"
/>
<div
className="flex items-center gap-1 sm:gap-2 cursor-pointer hover:opacity-80 transition min-w-0"
onClick={() => setSelectedEntry(entry)}
>
<span
className={`font-bold text-xs sm:text-sm truncate ${
entry.rank <= 3 ? "text-pixel-gold" : "text-white"
}`}
>
{entry.username}
{entry.characterClass && (
<span className="text-xs text-gray-400 uppercase tracking-wider">
[{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" && "💀"}]
</span>
{entry.characterClass && (
<span className="text-xs text-gray-400 uppercase tracking-wider">
[{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" && "💀"}]
</span>
)}
{entry.rank <= 3 && (
<span className="text-pixel-gold text-xs"></span>
)}
</div>
</div>
{/* Score */}
<div className="col-span-3 flex items-center justify-end">
<span className="font-mono text-gray-300 text-xs sm:text-sm">
{formatScore(entry.score)}
</span>
</div>
{/* Level */}
<div className="col-span-2 flex items-center justify-end">
<span className="font-bold text-gray-400 text-xs sm:text-sm">
Lv.{entry.level}
</span>
</div>
</div>
))}
</div>
</div>
{/* Footer Info */}
<div className="mt-8 text-center">
<p className="text-gray-500 text-sm">
Compete with players worldwide and climb the ranks!
</p>
<p className="text-gray-600 text-xs mt-2">
Rankings update every hour
</p>
</div>
</div>
{/* Character Modal */}
{selectedEntry && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={() => setSelectedEntry(null)}
>
<div
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="p-4 sm:p-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
{selectedEntry.username}
</h2>
<button
onClick={() => setSelectedEntry(null)}
className="text-gray-400 hover:text-pixel-gold text-2xl font-bold transition"
>
×
</button>
</div>
{/* Avatar and Class */}
<div className="flex items-center gap-4 sm:gap-6 mb-6">
<Avatar
src={selectedEntry.avatar}
username={selectedEntry.username}
size="lg"
className="flex-shrink-0"
borderClassName="border-2 sm:border-4 border-pixel-gold/50"
/>
<div>
<div className="text-xs text-gray-400 uppercase tracking-widest mb-2">
Rank #{selectedEntry.rank}
</div>
<div className="text-sm text-gray-300 mb-2">
{selectedEntry.email}
</div>
{selectedEntry.characterClass && (
<div className="flex items-center gap-2">
<span className="text-2xl">
{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" && "💀"}
</span>
<span className="text-lg font-bold text-pixel-gold uppercase tracking-wider">
{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"}
</span>
</div>
)}
{entry.rank <= 3 && (
<span className="text-pixel-gold text-xs"></span>
)}
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-black/60 border border-pixel-gold/30 rounded p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Score
</div>
<div className="text-2xl font-bold text-pixel-gold">
{formatScore(selectedEntry.score)}
</div>
</div>
<div className="bg-black/60 border border-pixel-gold/30 rounded p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Niveau
</div>
<div className="text-2xl font-bold text-pixel-gold">
Lv.{selectedEntry.level}
</div>
</div>
{/* Score */}
<div className="col-span-3 flex items-center justify-end">
<span className="font-mono text-gray-300 text-xs sm:text-sm">
{formatScore(entry.score)}
</span>
</div>
{/* Bio */}
{selectedEntry.bio && (
<div className="border-t border-pixel-gold/30 pt-6">
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-3 font-bold">
Bio
</div>
<p className="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
{selectedEntry.bio}
</p>
</div>
)}
{/* Level */}
<div className="col-span-2 flex items-center justify-end">
<span className="font-bold text-gray-400 text-xs sm:text-sm">
Lv.{entry.level}
</span>
</div>
</div>
</div>
))}
</div>
</div>
{/* Footer Info */}
<div className="mt-8 text-center">
<p className="text-gray-500 text-sm">
Compete with players worldwide and climb the ranks!
</p>
<p className="text-gray-600 text-xs mt-2">Rankings update every hour</p>
</div>
{/* Character Modal */}
{selectedEntry && (
<Modal
isOpen={!!selectedEntry}
onClose={() => setSelectedEntry(null)}
size="md"
>
<div className="p-4 sm:p-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
{selectedEntry.username}
</h2>
<CloseButton onClick={() => setSelectedEntry(null)} size="md" />
</div>
{/* Avatar and Class */}
<div className="flex items-center gap-4 sm:gap-6 mb-6">
<Avatar
src={selectedEntry.avatar}
username={selectedEntry.username}
size="lg"
className="flex-shrink-0"
borderClassName="border-2 sm:border-4 border-pixel-gold/50"
/>
<div>
<div className="text-xs text-gray-400 uppercase tracking-widest mb-2">
Rank #{selectedEntry.rank}
</div>
<div className="text-sm text-gray-300 mb-2">
{selectedEntry.email}
</div>
{selectedEntry.characterClass && (
<div className="flex items-center gap-2">
<span className="text-2xl">
{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" && "💀"}
</span>
<span className="text-lg font-bold text-pixel-gold uppercase tracking-wider">
{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"}
</span>
</div>
)}
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-4 mb-6">
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Score
</div>
<div className="text-2xl font-bold text-pixel-gold">
{formatScore(selectedEntry.score)}
</div>
</Card>
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Niveau
</div>
<div className="text-2xl font-bold text-pixel-gold">
Lv.{selectedEntry.level}
</div>
</Card>
</div>
{/* Bio */}
{selectedEntry.bio && (
<div className="border-t border-pixel-gold/30 pt-6">
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-3 font-bold">
Bio
</div>
<p className="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
{selectedEntry.bio}
</p>
</div>
)}
</div>
</Modal>
)}
</section>
</BackgroundSection>
);
}

View File

@@ -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 */}
<div className="hidden md:flex items-center gap-4">
{isAuthenticated ? (
<button
<Button
onClick={() => 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
</button>
</Button>
) : (
<>
<Link
@@ -114,11 +117,10 @@ export default function Navigation({
>
Connexion
</Link>
<Link
href="/register"
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"
>
Inscription
<Link href="/register">
<Button variant="primary" size="sm" className="text-xs">
Inscription
</Button>
</Link>
</>
)}
@@ -197,15 +199,17 @@ export default function Navigation({
{/* Mobile Auth Buttons */}
<div className="flex flex-col gap-3 pt-2 border-t border-gray-800/30">
{isAuthenticated ? (
<button
<Button
onClick={() => {
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
</button>
</Button>
) : (
<>
<Link
@@ -215,12 +219,14 @@ export default function Navigation({
>
Connexion
</Link>
<Link
href="/register"
onClick={() => 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
<Link href="/register" onClick={() => setIsMenuOpen(false)}>
<Button
variant="primary"
size="sm"
className="text-xs w-full text-center"
>
Inscription
</Button>
</Link>
</>
)}

View File

@@ -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;

View File

@@ -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 (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* Dark overlay for readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
{/* Content */}
<div className="relative z-10 w-full max-w-4xl mx-auto px-8 py-16">
<BackgroundSection backgroundImage={backgroundImage}>
<div className="w-full max-w-4xl mx-auto px-8">
{/* Title Section */}
<div className="text-center mb-12">
<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)",
}}
>
PROFIL
</span>
</h1>
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
<span></span>
<span>Gérez votre profil</span>
<span></span>
</div>
</div>
<SectionTitle variant="gradient" size="lg" subtitle="Gérez votre profil" className="mb-12">
PROFIL
</SectionTitle>
{/* Profile Card */}
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm">
<Card variant="default" className="overflow-hidden">
<form onSubmit={handleSubmit} className="p-8 space-y-8">
{/* Messages */}
{error && (
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
{error}
</div>
)}
{success && (
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm">
{success}
</div>
)}
{error && <Alert variant="error">{error}</Alert>}
{success && <Alert variant="success">{success}</Alert>}
{/* Avatar Section */}
<div className="flex flex-col items-center gap-4">
@@ -281,51 +247,38 @@ export default function ProfileForm({
className="hidden"
id="avatar-upload"
/>
<label
htmlFor="avatar-upload"
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 cursor-pointer inline-block"
>
{uploadingAvatar
? "Upload en cours..."
: "Upload un avatar custom"}
<label htmlFor="avatar-upload">
<Button variant="primary" size="md" as="span" className="cursor-pointer">
{uploadingAvatar ? "Upload en cours..." : "Upload un avatar custom"}
</Button>
</label>
</div>
</div>
{/* Username Field */}
<div>
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
Nom d&apos;utilisateur
</label>
<input
type="text"
value={username}
onChange={(e) => 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}
/>
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
</div>
<Input
type="text"
label="Nom d'utilisateur"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
minLength={3}
maxLength={20}
className="bg-black/40"
/>
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
{/* Bio Field */}
<div>
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
Bio
</label>
<textarea
value={bio || ""}
onChange={(e) => setBio(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 resize-none"
rows={4}
maxLength={500}
placeholder="Parlez-nous de vous..."
/>
<p className="text-gray-500 text-xs mt-1">
{(bio || "").length}/500 caractères
</p>
</div>
<Textarea
label="Bio"
value={bio || ""}
onChange={(e) => setBio(e.target.value)}
rows={4}
maxLength={500}
showCharCount
placeholder="Parlez-nous de vous..."
className="bg-black/40"
/>
{/* Character Class Selection */}
<div>
@@ -458,36 +411,23 @@ export default function ProfileForm({
</div>
{/* HP Bar */}
<div className="mb-4">
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span>HP</span>
<span>
{profile.hp} / {profile.maxHp}
</span>
</div>
<div className="relative h-3 bg-gray-900 border border-gray-700 rounded overflow-hidden">
<div
className={`absolute inset-0 bg-gradient-to-r ${hpColor} transition-all duration-1000 ease-out`}
style={{ width: `${hpPercentage}%` }}
/>
</div>
</div>
<ProgressBar
value={profile.hp}
max={profile.maxHp}
variant="hp"
showLabel
label="HP"
className="mb-4"
/>
{/* XP Bar */}
<div>
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span>XP</span>
<span>
{formatNumber(profile.xp)} / {formatNumber(profile.maxXp)}
</span>
</div>
<div className="relative h-3 bg-gray-900 border border-pixel-gold/30 rounded overflow-hidden">
<div
className="absolute inset-0 bg-gradient-to-r from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80 transition-all duration-1000 ease-out"
style={{ width: `${xpPercentage}%` }}
/>
</div>
</div>
<ProgressBar
value={profile.xp}
max={profile.maxXp}
variant="xp"
showLabel
label="XP"
/>
</div>
{/* Email (read-only) */}
@@ -505,13 +445,9 @@ export default function ProfileForm({
{/* Submit Button */}
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
<button
type="submit"
disabled={isPending}
className="px-6 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"
>
<Button type="submit" variant="primary" size="md" disabled={isPending}>
{isPending ? "Enregistrement..." : "Enregistrer les modifications"}
</button>
</Button>
</div>
</form>
@@ -522,65 +458,56 @@ export default function ProfileForm({
Mot de passe
</h3>
{!showPasswordForm && (
<button
<Button
type="button"
variant="primary"
size="md"
onClick={() => 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
</button>
</Button>
)}
</div>
{showPasswordForm && (
<form onSubmit={handlePasswordChange} className="space-y-4">
<div>
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
Mot de passe actuel
</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(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
/>
</div>
<Input
type="password"
label="Mot de passe actuel"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
className="bg-black/40"
/>
<div>
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
Nouveau mot de passe
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(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={6}
/>
<p className="text-gray-500 text-xs mt-1">
Minimum 6 caractères
</p>
</div>
<Input
type="password"
label="Nouveau mot de passe"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={6}
className="bg-black/40"
/>
<p className="text-gray-500 text-xs mt-1">
Minimum 6 caractères
</p>
<div>
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
Confirmer le nouveau mot de passe
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(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={6}
/>
</div>
<Input
type="password"
label="Confirmer le nouveau mot de passe"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={6}
className="bg-black/40"
/>
<div className="flex justify-end gap-4">
<button
<Button
type="button"
variant="secondary"
size="md"
onClick={() => {
setShowPasswordForm(false);
setCurrentPassword("");
@@ -588,25 +515,25 @@ export default function ProfileForm({
setConfirmPassword("");
setError(null);
}}
className="px-4 py-2 border border-gray-600/50 bg-black/40 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/40 hover:border-gray-500 transition"
>
Annuler
</button>
<button
</Button>
<Button
type="submit"
variant="primary"
size="md"
disabled={isChangingPassword}
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 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isChangingPassword
? "Modification..."
: "Modifier le mot de passe"}
</button>
</Button>
</div>
</form>
)}
</div>
</div>
</Card>
</div>
</section>
</BackgroundSection>
);
}

View File

@@ -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 (
<div
key={user.id}
className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4"
>
<Card key={user.id} variant="default" className="p-3 sm:p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-2">
<div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0">
{/* Avatar */}
@@ -248,23 +245,19 @@ export default function UserManagement() {
{isEditing ? (
<div className="space-y-4">
{/* Username Section */}
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-2">
Nom d&apos;utilisateur
</label>
<input
type="text"
value={editingUser.username || ""}
onChange={(e) =>
setEditingUser({
...editingUser,
username: e.target.value,
})
}
className="w-full px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
placeholder="Nom d'utilisateur"
/>
</div>
<Input
type="text"
label="Nom d'utilisateur"
value={editingUser.username || ""}
onChange={(e) =>
setEditingUser({
...editingUser,
username: e.target.value,
})
}
placeholder="Nom d'utilisateur"
className="text-xs sm:text-sm px-2 sm:px-3 py-1"
/>
{/* Avatar Section */}
<div className="flex flex-col items-center gap-3">
@@ -372,13 +365,17 @@ export default function UserManagement() {
className="hidden"
id={`avatar-upload-${user.id}`}
/>
<label
htmlFor={`avatar-upload-${user.id}`}
className="px-3 sm:px-4 py-1.5 border border-pixel-gold/50 bg-black/40 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
>
{uploadingAvatar === user.id
? "Upload en cours..."
: "Upload un avatar custom"}
<label htmlFor={`avatar-upload-${user.id}`}>
<Button
variant="primary"
size="sm"
as="span"
className="cursor-pointer"
>
{uploadingAvatar === user.id
? "Upload en cours..."
: "Upload un avatar custom"}
</Button>
</label>
</div>
</div>
@@ -690,19 +687,21 @@ export default function UserManagement() {
</div>
<div className="flex flex-col sm:flex-row gap-2 pt-2">
<button
<Button
onClick={handleSave}
variant="success"
size="md"
disabled={saving}
className="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"
>
{saving ? "Enregistrement..." : "Enregistrer"}
</button>
<button
</Button>
<Button
onClick={handleCancel}
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
variant="secondary"
size="md"
>
Annuler
</button>
</Button>
</div>
</div>
) : (
@@ -747,7 +746,7 @@ export default function UserManagement() {
</div>
</div>
)}
</div>
</Card>
);
})
)}

32
components/ui/Alert.tsx Normal file
View File

@@ -0,0 +1,32 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface AlertProps extends HTMLAttributes<HTMLDivElement> {
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 (
<div
className={`border rounded px-4 py-3 text-sm ${variantClasses[variant]} ${className}`}
{...props}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,43 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface BackgroundSectionProps extends HTMLAttributes<HTMLElement> {
children: ReactNode;
backgroundImage: string;
overlay?: boolean;
}
export default function BackgroundSection({
children,
backgroundImage,
overlay = true,
className = "",
...props
}: BackgroundSectionProps) {
return (
<section
className={`relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16 ${className}`}
{...props}
>
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* Dark overlay for readability */}
{overlay && (
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
)}
</div>
{/* Content */}
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm:px-8 py-16">
{children}
</div>
</section>
);
}

40
components/ui/Badge.tsx Normal file
View File

@@ -0,0 +1,40 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
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 (
<span
className={`inline-block border uppercase rounded whitespace-nowrap flex-shrink-0 ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
{...props}
>
{children}
</span>
);
}

47
components/ui/Button.tsx Normal file
View File

@@ -0,0 +1,47 @@
"use client";
import { ButtonHTMLAttributes, ReactNode, ElementType } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
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 (
<Component
className={`border uppercase tracking-widest rounded transition disabled:opacity-50 disabled:cursor-not-allowed ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
disabled={disabled}
{...props}
>
{children}
</Component>
);
}

30
components/ui/Card.tsx Normal file
View File

@@ -0,0 +1,30 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface CardProps extends HTMLAttributes<HTMLDivElement> {
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 (
<div
className={`rounded-lg backdrop-blur-sm ${variantClasses[variant]} ${className}`}
{...props}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,29 @@
"use client";
import { ButtonHTMLAttributes } from "react";
interface CloseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
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 (
<button
className={`text-gray-400 hover:text-pixel-gold font-bold transition disabled:opacity-50 disabled:cursor-not-allowed ${sizeClasses[size]} ${className}`}
{...props}
>
×
</button>
);
}

38
components/ui/Input.tsx Normal file
View File

@@ -0,0 +1,38 @@
"use client";
import { InputHTMLAttributes, forwardRef } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, className = "", ...props }, ref) => {
return (
<div>
{label && (
<label
htmlFor={props.id}
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
>
{label}
</label>
)}
<input
ref={ref}
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition ${className}`}
{...props}
/>
{error && (
<p className="text-red-400 text-xs mt-1">{error}</p>
)}
</div>
);
}
);
Input.displayName = "Input";
export default Input;

54
components/ui/Modal.tsx Normal file
View File

@@ -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 (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={closeOnOverlayClick ? onClose : undefined}
>
<div
className={`bg-black border-2 border-pixel-gold/70 rounded-lg w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto shadow-2xl`}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,73 @@
"use client";
import { HTMLAttributes } from "react";
interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {
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 (
<div className={className} {...props}>
{showLabel && (
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span>{label || variant.toUpperCase()}</span>
<span>
{value} / {max}
</span>
</div>
)}
<div className="relative h-2 sm:h-3 bg-gray-900 border border-gray-700 rounded overflow-hidden">
<div
className={`absolute inset-0 bg-gradient-to-r ${gradientClass} transition-all duration-1000 ease-out`}
style={{ width: `${percentage}%` }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"></div>
</div>
{variant === "hp" && percentage < 30 && (
<div className="absolute inset-0 border border-red-500 rounded animate-pulse"></div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface SectionTitleProps extends HTMLAttributes<HTMLHeadingElement> {
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 (
<div className="text-center">
<h1 className={titleClasses} {...props}>
{variant === "gradient" ? (
<span
style={{
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
}}
>
{children}
</span>
) : (
children
)}
</h1>
{subtitle && (
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
<span></span>
<span>{subtitle}</span>
<span></span>
</div>
)}
</div>
);
}

View File

@@ -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 (
<div className="flex flex-col items-center gap-2">
<div className="flex items-center justify-center gap-2">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => 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" : ""}`}
>
</button>
))}
</div>
{showValue && value > 0 && (
<p className="text-gray-500 text-xs text-center">
{value}/5
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,49 @@
"use client";
import { TextareaHTMLAttributes, forwardRef } from "react";
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
error?: string;
showCharCount?: boolean;
maxLength?: number;
}
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
({ label, error, showCharCount, maxLength, className = "", value, ...props }, ref) => {
const charCount = typeof value === "string" ? value.length : 0;
return (
<div>
{label && (
<label
htmlFor={props.id}
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
>
{label}
</label>
)}
<textarea
ref={ref}
maxLength={maxLength}
value={value}
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none ${className}`}
{...props}
/>
{showCharCount && maxLength && (
<p className="text-gray-500 text-xs mt-1 text-right">
{charCount}/{maxLength} caractères
</p>
)}
{error && (
<p className="text-red-400 text-xs mt-1">{error}</p>
)}
</div>
);
}
);
Textarea.displayName = "Textarea";
export default Textarea;

14
components/ui/index.ts Normal file
View File

@@ -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";