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

@@ -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 (
<main className="min-h-screen bg-black relative">
<Navigation />
<BackgroundSection backgroundImage="/got-2.jpg" className="pt-24 pb-16">
<div className="w-full max-w-6xl mx-auto px-8">
<SectionTitle variant="gradient" size="xl" className="mb-12">
STYLE GUIDE
</SectionTitle>
<p className="text-gray-400 text-center mb-12 max-w-3xl mx-auto">
Guide de style complet avec tous les composants UI disponibles et
leurs variantes
</p>
{/* Buttons */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">
Buttons
</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg text-gray-300 mb-3">Variantes</h3>
<div className="flex flex-wrap gap-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="success">Success</Button>
<Button variant="danger">Danger</Button>
<Button variant="ghost">Ghost</Button>
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Tailles</h3>
<div className="flex flex-wrap items-center gap-4">
<Button variant="primary" size="sm">
Small
</Button>
<Button variant="primary" size="md">
Medium
</Button>
<Button variant="primary" size="lg">
Large
</Button>
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">États</h3>
<div className="flex flex-wrap gap-4">
<Button variant="primary">Normal</Button>
<Button variant="primary" disabled>
Disabled
</Button>
</div>
</div>
</div>
</Card>
{/* Inputs */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">Inputs</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg text-gray-300 mb-3">Types</h3>
<div className="space-y-4 max-w-md">
<Input
label="Text Input"
type="text"
placeholder="Entrez du texte"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<Input
label="Email Input"
type="email"
placeholder="email@example.com"
/>
<Input
label="Password Input"
type="password"
placeholder="••••••••"
/>
<Input
label="Number Input"
type="number"
placeholder="123"
/>
<Input
label="Date Input"
type="date"
/>
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Avec erreur</h3>
<div className="max-w-md">
<Input
label="Input avec erreur"
type="text"
error="Ce champ est requis"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
</div>
</div>
</Card>
{/* Textarea */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">
Textarea
</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg text-gray-300 mb-3">Basique</h3>
<div className="max-w-md">
<Textarea
label="Commentaire"
placeholder="Écrivez votre commentaire..."
value={textareaValue}
onChange={(e) => setTextareaValue(e.target.value)}
rows={4}
/>
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">
Avec compteur de caractères
</h3>
<div className="max-w-md">
<Textarea
label="Bio"
placeholder="Parlez-nous de vous..."
value={textareaValue}
onChange={(e) => setTextareaValue(e.target.value)}
rows={4}
maxLength={500}
showCharCount
/>
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Avec erreur</h3>
<div className="max-w-md">
<Textarea
label="Textarea avec erreur"
placeholder="Écrivez quelque chose..."
error="Ce champ est requis"
value={textareaValue}
onChange={(e) => setTextareaValue(e.target.value)}
rows={4}
/>
</div>
</div>
</div>
</Card>
{/* Badges */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">Badges</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg text-gray-300 mb-3">Variantes</h3>
<div className="flex flex-wrap gap-4">
<Badge variant="default">Default</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="danger">Danger</Badge>
<Badge variant="info">Info</Badge>
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Tailles</h3>
<div className="flex flex-wrap items-center gap-4">
<Badge variant="default" size="sm">
Small
</Badge>
<Badge variant="default" size="md">
Medium
</Badge>
</div>
</div>
</div>
</Card>
{/* Alerts */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">Alerts</h2>
<div className="space-y-4 max-w-md">
<Alert variant="success">
Opération réussie ! Votre action a é effectuée avec succès.
</Alert>
<Alert variant="error">
Une erreur est survenue. Veuillez réessayer.
</Alert>
<Alert variant="warning">
Attention ! Cette action est irréversible.
</Alert>
<Alert variant="info">
Information : Voici quelques informations utiles.
</Alert>
</div>
</Card>
{/* Cards */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">Cards</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card variant="default" className="p-4">
<h3 className="text-lg font-bold text-pixel-gold mb-2">
Card Default
</h3>
<p className="text-gray-300 text-sm">
Contenu de la carte avec variant default
</p>
</Card>
<Card variant="dark" className="p-4">
<h3 className="text-lg font-bold text-pixel-gold mb-2">
Card Dark
</h3>
<p className="text-gray-300 text-sm">
Contenu de la carte avec variant dark
</p>
</Card>
</div>
</Card>
{/* Progress Bars */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">
Progress Bars
</h2>
<div className="space-y-6 max-w-md">
<div>
<h3 className="text-lg text-gray-300 mb-3">HP Bar (High)</h3>
<ProgressBar
value={75}
max={100}
variant="hp"
showLabel
label="HP"
/>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">HP Bar (Medium)</h3>
<ProgressBar
value={45}
max={100}
variant="hp"
showLabel
label="HP"
/>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">HP Bar (Low)</h3>
<ProgressBar
value={20}
max={100}
variant="hp"
showLabel
label="HP"
/>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">XP Bar</h3>
<ProgressBar
value={60}
max={100}
variant="xp"
showLabel
label="XP"
/>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Default</h3>
<ProgressBar
value={50}
max={100}
variant="default"
showLabel
label="Progress"
/>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Sans label</h3>
<ProgressBar value={60} max={100} variant="default" />
</div>
</div>
</Card>
{/* Star Rating */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">
Star Rating
</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg text-gray-300 mb-3">Interactif</h3>
<StarRating
value={rating}
onChange={setRating}
showValue
/>
<p className="text-gray-400 text-sm mt-2">
Note sélectionnée : {rating}/5
</p>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Tailles</h3>
<div className="space-y-4">
<div>
<p className="text-gray-400 text-sm mb-2">Small</p>
<StarRating value={4} size="sm" />
</div>
<div>
<p className="text-gray-400 text-sm mb-2">Medium</p>
<StarRating value={4} size="md" />
</div>
<div>
<p className="text-gray-400 text-sm mb-2">Large</p>
<StarRating value={4} size="lg" />
</div>
</div>
</div>
</div>
</Card>
{/* Avatar */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">Avatar</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg text-gray-300 mb-3">Tailles</h3>
<div className="flex items-center gap-6">
<Avatar
src="/avatar-1.jpg"
username="User"
size="sm"
/>
<Avatar
src="/avatar-2.jpg"
username="User"
size="md"
/>
<Avatar
src="/avatar-3.jpg"
username="User"
size="lg"
/>
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">
Sans image (fallback)
</h3>
<Avatar username="John Doe" size="lg" />
</div>
</div>
</Card>
{/* Section Title */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">
Section Title
</h2>
<div className="space-y-8">
<div>
<h3 className="text-lg text-gray-300 mb-3">Variantes</h3>
<div className="space-y-4">
<SectionTitle variant="default" size="md">
Default Title
</SectionTitle>
<SectionTitle variant="gradient" size="md">
Gradient Title
</SectionTitle>
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Tailles</h3>
<div className="space-y-4">
<SectionTitle variant="gradient" size="sm">
Small Title
</SectionTitle>
<SectionTitle variant="gradient" size="md">
Medium Title
</SectionTitle>
<SectionTitle variant="gradient" size="lg">
Large Title
</SectionTitle>
<SectionTitle variant="gradient" size="xl">
Extra Large Title
</SectionTitle>
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Avec sous-titre</h3>
<SectionTitle
variant="gradient"
size="lg"
subtitle="Un sous-titre descriptif"
>
Title with Subtitle
</SectionTitle>
</div>
</div>
</Card>
{/* Modal */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">Modal</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg text-gray-300 mb-3">Tailles</h3>
<div className="flex flex-wrap gap-4">
<Button onClick={() => setModalOpen(true)}>
Ouvrir Modal
</Button>
</div>
</div>
</div>
</Card>
{/* Close Button */}
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-2xl font-bold text-pixel-gold mb-6">
Close Button
</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg text-gray-300 mb-3">Tailles</h3>
<div className="flex items-center gap-6">
<CloseButton onClick={() => {}} size="sm" />
<CloseButton onClick={() => {}} size="md" />
<CloseButton onClick={() => {}} size="lg" />
</div>
</div>
<div>
<h3 className="text-lg text-gray-300 mb-3">Disabled</h3>
<CloseButton onClick={() => {}} disabled />
</div>
</div>
</Card>
{/* Modal Demo */}
<Modal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
size="md"
>
<div className="p-8">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-pixel-gold">
Exemple de Modal
</h2>
<CloseButton onClick={() => setModalOpen(false)} />
</div>
<p className="text-gray-300 mb-6">
Ceci est un exemple de modal avec différentes tailles
disponibles.
</p>
<div className="flex gap-4">
<Button onClick={() => setModalOpen(false)}>Fermer</Button>
</div>
</div>
</Modal>
</div>
</BackgroundSection>
</main>
);
}

View File

@@ -5,6 +5,15 @@ import { useSession } from "next-auth/react";
import { useRouter, useParams } from "next/navigation"; import { useRouter, useParams } from "next/navigation";
import Navigation from "@/components/Navigation"; import Navigation from "@/components/Navigation";
import { createFeedback } from "@/actions/events/feedback"; import { createFeedback } from "@/actions/events/feedback";
import {
StarRating,
Textarea,
Button,
Alert,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
interface Event { interface Event {
id: string; id: string;
@@ -156,25 +165,17 @@ export default function FeedbackPageClient({
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">
<Navigation /> <Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24"> <BackgroundSection backgroundImage={backgroundImage} className="pt-24">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
{/* Feedback Form */} {/* Feedback Form */}
<div className="relative z-10 w-full max-w-2xl mx-auto px-8"> <div className="w-full max-w-2xl mx-auto px-8">
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm"> <Card variant="dark" className="p-8">
<h1 className="text-4xl font-gaming font-black mb-2 text-center"> <SectionTitle
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"> variant="gradient"
FEEDBACK size="lg"
</span> className="mb-2 text-center"
</h1> >
FEEDBACK
</SectionTitle>
<p className="text-gray-400 text-sm text-center mb-2"> <p className="text-gray-400 text-sm text-center mb-2">
{existingFeedback {existingFeedback
? "Modifier votre feedback pour" ? "Modifier votre feedback pour"
@@ -185,15 +186,15 @@ export default function FeedbackPageClient({
</p> </p>
{success && ( {success && (
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm mb-6"> <Alert variant="success" className="mb-6">
Feedback enregistré avec succès ! Redirection... Feedback enregistré avec succès ! Redirection...
</div> </Alert>
)} )}
{error && ( {error && (
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm mb-6"> <Alert variant="error" className="mb-6">
{error} {error}
</div> </Alert>
)} )}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
@@ -202,66 +203,44 @@ export default function FeedbackPageClient({
<label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider"> <label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">
Note Note
</label> </label>
<div className="flex items-center justify-center gap-2"> <StarRating
{[1, 2, 3, 4, 5].map((star) => ( value={rating}
<button onChange={setRating}
key={star} size="lg"
type="button" showValue
onClick={() => setRating(star)} />
className={`text-4xl transition-transform hover:scale-110 ${
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> </div>
{/* Comment */} {/* Comment */}
<div> <Textarea
<label id="comment"
htmlFor="comment" label="Commentaire (optionnel)"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider" value={comment}
> onChange={(e) => setComment(e.target.value)}
Commentaire (optionnel) rows={6}
</label> maxLength={1000}
<textarea showCharCount
id="comment" placeholder="Partagez votre expérience, vos suggestions..."
value={comment} />
onChange={(e) => setComment(e.target.value)}
rows={6}
maxLength={1000}
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"
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 */} {/* Submit Button */}
<button <Button
type="submit" type="submit"
variant="primary"
size="lg"
disabled={submitting || rating === 0} 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" className="w-full"
> >
{submitting {submitting
? "Enregistrement..." ? "Enregistrement..."
: existingFeedback : existingFeedback
? "Modifier le feedback" ? "Modifier le feedback"
: "Envoyer le feedback"} : "Envoyer le feedback"}
</button> </Button>
</form> </form>
</div> </Card>
</div> </div>
</section> </BackgroundSection>
</main> </main>
); );
} }

View File

@@ -31,4 +31,17 @@
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.animate-shimmer {
animation: shimmer 2s infinite;
}
} }

View File

@@ -5,6 +5,14 @@ import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import Navigation from "@/components/Navigation"; import Navigation from "@/components/Navigation";
import {
Input,
Button,
Alert,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
@@ -46,79 +54,53 @@ export default function LoginPage() {
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">
<Navigation /> <Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24"> <BackgroundSection backgroundImage="/got-2.jpg" className="pt-24">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('/got-2.jpg')`,
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
{/* Login Form */} {/* Login Form */}
<div className="relative z-10 w-full max-w-md mx-auto px-8"> <div className="w-full max-w-md mx-auto px-8">
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm"> <Card variant="dark" className="p-8">
<h1 className="text-4xl font-gaming font-black mb-2 text-center"> <SectionTitle
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"> variant="gradient"
CONNEXION size="lg"
</span> className="mb-2 text-center"
</h1> >
CONNEXION
</SectionTitle>
<p className="text-gray-400 text-sm text-center mb-8"> <p className="text-gray-400 text-sm text-center mb-8">
Connectez-vous à votre compte Connectez-vous à votre compte
</p> </p>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{error && ( {error && <Alert variant="error">{error}</Alert>}
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
{error}
</div>
)}
<div> <Input
<label id="email"
htmlFor="email" type="email"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider" label="Email"
> value={email}
Email onChange={(e) => setEmail(e.target.value)}
</label> required
<input placeholder="votre@email.com"
id="email" />
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
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"
placeholder="votre@email.com"
/>
</div>
<div> <Input
<label id="password"
htmlFor="password" type="password"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider" label="Mot de passe"
> value={password}
Mot de passe onChange={(e) => setPassword(e.target.value)}
</label> required
<input placeholder="••••••••"
id="password" />
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
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"
placeholder="••••••••"
/>
</div>
<button <Button
type="submit" type="submit"
variant="primary"
size="lg"
disabled={loading} disabled={loading}
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" className="w-full"
> >
{loading ? "Connexion..." : "Se connecter"} {loading ? "Connexion..." : "Se connecter"}
</button> </Button>
</form> </form>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
@@ -132,9 +114,9 @@ export default function LoginPage() {
</Link> </Link>
</p> </p>
</div> </div>
</div> </Card>
</div> </div>
</section> </BackgroundSection>
</main> </main>
); );
} }

View File

@@ -4,7 +4,16 @@ import { useState, useRef, type ChangeEvent, type FormEvent } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import Navigation from "@/components/Navigation"; 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() { export default function RegisterPage() {
const router = useRouter(); const router = useRouter();
@@ -162,25 +171,17 @@ export default function RegisterPage() {
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">
<Navigation /> <Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24"> <BackgroundSection backgroundImage="/got-2.jpg" className="pt-24">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('/got-2.jpg')`,
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
{/* Register Form */} {/* Register Form */}
<div className="relative z-10 w-full max-w-md mx-auto px-8"> <div className="w-full max-w-md mx-auto px-8">
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm"> <Card variant="dark" className="p-8">
<h1 className="text-4xl font-gaming font-black mb-2 text-center"> <SectionTitle
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"> variant="gradient"
INSCRIPTION size="lg"
</span> className="mb-2 text-center"
</h1> >
INSCRIPTION
</SectionTitle>
<p className="text-gray-400 text-sm text-center mb-4"> <p className="text-gray-400 text-sm text-center mb-4">
{step === 1 {step === 1
? "Créez votre compte pour commencer" ? "Créez votre compte pour commencer"
@@ -216,103 +217,65 @@ export default function RegisterPage() {
{step === 1 ? ( {step === 1 ? (
<form onSubmit={handleStep1Submit} className="space-y-6"> <form onSubmit={handleStep1Submit} className="space-y-6">
{error && ( {error && <Alert variant="error">{error}</Alert>}
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
{error}
</div>
)}
<div> <Input
<label id="email"
htmlFor="email" name="email"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider" type="email"
> label="Email"
Email value={formData.email}
</label> onChange={handleChange}
<input required
id="email" placeholder="votre@email.com"
name="email" />
type="email"
value={formData.email}
onChange={handleChange}
required
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"
placeholder="votre@email.com"
/>
</div>
<div> <Input
<label id="username"
htmlFor="username" name="username"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider" type="text"
> label="Nom d'utilisateur"
Nom d&apos;utilisateur value={formData.username}
</label> onChange={handleChange}
<input required
id="username" placeholder="VotrePseudo"
name="username" />
type="text"
value={formData.username}
onChange={handleChange}
required
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"
placeholder="VotrePseudo"
/>
</div>
<div> <Input
<label id="password"
htmlFor="password" name="password"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider" type="password"
> label="Mot de passe"
Mot de passe value={formData.password}
</label> onChange={handleChange}
<input required
id="password" placeholder="••••••••"
name="password" />
type="password"
value={formData.password}
onChange={handleChange}
required
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"
placeholder="••••••••"
/>
</div>
<div> <Input
<label id="confirmPassword"
htmlFor="confirmPassword" name="confirmPassword"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider" type="password"
> label="Confirmer le mot de passe"
Confirmer le mot de passe value={formData.confirmPassword}
</label> onChange={handleChange}
<input required
id="confirmPassword" placeholder="••••••••"
name="confirmPassword" />
type="password"
value={formData.confirmPassword}
onChange={handleChange}
required
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"
placeholder="••••••••"
/>
</div>
<button <Button
type="submit" type="submit"
variant="primary"
size="lg"
disabled={loading} disabled={loading}
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" className="w-full"
> >
{loading ? "Création..." : "Suivant"} {loading ? "Création..." : "Suivant"}
</button> </Button>
</form> </form>
) : ( ) : (
<form onSubmit={handleStep2Submit} className="space-y-6"> <form onSubmit={handleStep2Submit} className="space-y-6">
{error && ( {error && <Alert variant="error">{error}</Alert>}
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
{error}
</div>
)}
{/* Avatar Selection */} {/* Avatar Selection */}
<div> <div>
@@ -387,61 +350,47 @@ export default function RegisterPage() {
className="hidden" className="hidden"
id="avatar-upload" id="avatar-upload"
/> />
<label <label htmlFor="avatar-upload">
htmlFor="avatar-upload" <Button
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" variant="primary"
> size="md"
{uploadingAvatar as="span"
? "Upload en cours..." className="cursor-pointer"
: "Upload un avatar custom"} >
{uploadingAvatar
? "Upload en cours..."
: "Upload un avatar custom"}
</Button>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div> <Input
<label id="username-step2"
htmlFor="username-step2" name="username"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider" type="text"
> label="Nom d'utilisateur"
Nom d&apos;utilisateur value={formData.username}
</label> onChange={handleChange}
<input required
id="username-step2" placeholder="VotrePseudo"
name="username" minLength={3}
type="text" maxLength={20}
value={formData.username} />
onChange={handleChange} <p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
required
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"
placeholder="VotrePseudo"
minLength={3}
maxLength={20}
/>
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
</div>
<div> <Textarea
<label id="bio"
htmlFor="bio" name="bio"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider" label="Bio (optionnel)"
> value={formData.bio}
Bio (optionnel) onChange={handleChange}
</label> rows={4}
<textarea maxLength={500}
id="bio" showCharCount
name="bio" placeholder="Parlez-nous de vous..."
value={formData.bio} />
onChange={handleChange}
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"
rows={4}
maxLength={500}
placeholder="Parlez-nous de vous..."
/>
<p className="text-gray-500 text-xs mt-1">
{formData.bio.length}/500 caractères
</p>
</div>
<div> <div>
<label className="block text-sm font-semibold text-gray-300 mb-3 uppercase tracking-wider"> <label className="block text-sm font-semibold text-gray-300 mb-3 uppercase tracking-wider">
@@ -500,20 +449,24 @@ export default function RegisterPage() {
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<button <Button
type="button" type="button"
variant="secondary"
size="lg"
onClick={() => setStep(1)} onClick={() => 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 Retour
</button> </Button>
<button <Button
type="submit" type="submit"
variant="primary"
size="lg"
disabled={loading} disabled={loading}
className="flex-1 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" className="flex-1"
> >
{loading ? "Finalisation..." : "Terminer"} {loading ? "Finalisation..." : "Terminer"}
</button> </Button>
</div> </div>
</form> </form>
)} )}
@@ -529,9 +482,9 @@ export default function RegisterPage() {
</Link> </Link>
</p> </p>
</div> </div>
</div> </Card>
</div> </div>
</section> </BackgroundSection>
</main> </main>
); );
} }

View File

@@ -1,10 +1,12 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link";
import UserManagement from "@/components/UserManagement"; import UserManagement from "@/components/UserManagement";
import EventManagement from "@/components/EventManagement"; import EventManagement from "@/components/EventManagement";
import FeedbackManagement from "@/components/FeedbackManagement"; import FeedbackManagement from "@/components/FeedbackManagement";
import BackgroundPreferences from "@/components/BackgroundPreferences"; import BackgroundPreferences from "@/components/BackgroundPreferences";
import { Button, Card, SectionTitle } from "@/components/ui";
interface SitePreferences { interface SitePreferences {
id: string; id: string;
@@ -26,92 +28,91 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
return ( return (
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16"> <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"> <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"> <SectionTitle variant="gradient" size="md" className="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
ADMIN </SectionTitle>
</span>
</h1>
{/* Navigation Tabs */} {/* Navigation Tabs */}
<div className="flex gap-4 mb-8 justify-center"> <div className="flex gap-4 mb-8 justify-center flex-wrap">
<button <Button
onClick={() => setActiveSection("preferences")} onClick={() => setActiveSection("preferences")}
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${ variant={activeSection === "preferences" ? "primary" : "secondary"}
activeSection === "preferences" size="md"
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold" className={
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50" activeSection === "preferences" ? "bg-pixel-gold/10" : ""
}`} }
> >
Préférences UI Préférences UI
</button> </Button>
<button <Button
onClick={() => setActiveSection("users")} onClick={() => setActiveSection("users")}
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${ variant={activeSection === "users" ? "primary" : "secondary"}
activeSection === "users" size="md"
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold" className={activeSection === "users" ? "bg-pixel-gold/10" : ""}
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
}`}
> >
Utilisateurs Utilisateurs
</button> </Button>
<button <Button
onClick={() => setActiveSection("events")} onClick={() => setActiveSection("events")}
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${ variant={activeSection === "events" ? "primary" : "secondary"}
activeSection === "events" size="md"
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold" className={activeSection === "events" ? "bg-pixel-gold/10" : ""}
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
}`}
> >
Événements Événements
</button> </Button>
<button <Button
onClick={() => setActiveSection("feedbacks")} onClick={() => setActiveSection("feedbacks")}
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${ variant={activeSection === "feedbacks" ? "primary" : "secondary"}
activeSection === "feedbacks" size="md"
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold" className={activeSection === "feedbacks" ? "bg-pixel-gold/10" : ""}
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
}`}
> >
Feedbacks Feedbacks
</button> </Button>
</div> </div>
{activeSection === "preferences" && ( {activeSection === "preferences" && (
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-4 sm:p-6 backdrop-blur-sm"> <Card variant="dark" className="p-4 sm:p-6">
<h2 className="text-xl sm:text-2xl font-gaming font-bold mb-6 text-pixel-gold break-words"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
Préférences UI Globales <h2 className="text-xl sm:text-2xl font-gaming font-bold text-pixel-gold break-words">
</h2> 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"> <div className="space-y-4">
<BackgroundPreferences initialPreferences={initialPreferences} /> <BackgroundPreferences initialPreferences={initialPreferences} />
</div> </div>
</div> </Card>
)} )}
{activeSection === "users" && ( {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"> <h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Utilisateurs Gestion des Utilisateurs
</h2> </h2>
<UserManagement /> <UserManagement />
</div> </Card>
)} )}
{activeSection === "events" && ( {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"> <h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Événements Gestion des Événements
</h2> </h2>
<EventManagement /> <EventManagement />
</div> </Card>
)} )}
{activeSection === "feedbacks" && ( {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"> <h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Feedbacks Gestion des Feedbacks
</h2> </h2>
<FeedbackManagement /> <FeedbackManagement />
</div> </Card>
)} )}
</div> </div>
</section> </section>

View File

@@ -3,6 +3,7 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import ImageSelector from "@/components/ImageSelector"; import ImageSelector from "@/components/ImageSelector";
import { updateSitePreferences } from "@/actions/admin/preferences"; import { updateSitePreferences } from "@/actions/admin/preferences";
import { Button, Card } from "@/components/ui";
interface SitePreferences { interface SitePreferences {
id: string; id: string;
@@ -142,7 +143,7 @@ export default function BackgroundPreferences({
}; };
return ( 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="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words"> <h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
@@ -153,12 +154,14 @@ export default function BackgroundPreferences({
</p> </p>
</div> </div>
{!isEditing && ( {!isEditing && (
<button <Button
onClick={handleEdit} 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 Modifier
</button> </Button>
)} )}
</div> </div>
@@ -195,18 +198,12 @@ export default function BackgroundPreferences({
label="Background Leaderboard" label="Background Leaderboard"
/> />
<div className="flex flex-col sm:flex-row gap-2 pt-4"> <div className="flex flex-col sm:flex-row gap-2 pt-4">
<button <Button onClick={handleSave} variant="success" size="md">
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"
>
Enregistrer Enregistrer
</button> </Button>
<button <Button onClick={handleCancel} variant="secondary" size="md">
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"
>
Annuler Annuler
</button> </Button>
</div> </div>
</div> </div>
) : ( ) : (
@@ -381,6 +378,6 @@ export default function BackgroundPreferences({
</div> </div>
</div> </div>
)} )}
</div> </Card>
); );
} }

View File

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

View File

@@ -9,6 +9,15 @@ import {
registerForEvent, registerForEvent,
unregisterFromEvent, unregisterFromEvent,
} from "@/actions/events/register"; } from "@/actions/events/register";
import {
Badge,
Button,
Modal,
CloseButton,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
interface Event { interface Event {
id: string; id: string;
@@ -61,21 +70,21 @@ const getStatusBadge = (status: "UPCOMING" | "LIVE" | "PAST") => {
switch (status) { switch (status) {
case "UPCOMING": case "UPCOMING":
return ( 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 À venir
</span> </Badge>
); );
case "LIVE": case "LIVE":
return ( 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 En direct
</span> </Badge>
); );
case "PAST": case "PAST":
return ( 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é Passé
</span> </Badge>
); );
} }
}; };
@@ -401,10 +410,10 @@ export default function EventsPageSection({
}; };
const renderEventCard = (event: Event) => ( const renderEventCard = (event: Event) => (
<div <Card
key={event.id} key={event.id}
onClick={() => setSelectedEvent(event)} 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 */} {/* Event Header */}
<div <div
@@ -482,37 +491,41 @@ export default function EventsPageSection({
{getEventStatus(event) === "UPCOMING" && ( {getEventStatus(event) === "UPCOMING" && (
<> <>
{registrations[event.id] ? ( {registrations[event.id] ? (
<button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleUnregister(event.id); handleUnregister(event.id);
}} }}
variant="success"
size="md"
disabled={loading[event.id]} 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 ✓"} {loading[event.id] ? "Annulation..." : "Inscrit ✓"}
</button> </Button>
) : ( ) : (
<button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleRegister(event.id); handleRegister(event.id);
}} }}
variant="primary"
size="md"
disabled={loading[event.id]} 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"} {loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
</button> </Button>
)} )}
</> </>
)} )}
{getEventStatus(event) === "LIVE" && ( {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 Rejoindre en direct
</button> </Button>
)} )}
{getEventStatus(event) === "PAST" && ( {getEventStatus(event) === "PAST" && (
<button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!session?.user?.id) { if (!session?.user?.id) {
@@ -521,13 +534,15 @@ export default function EventsPageSection({
} }
setFeedbackEventId(event.id); 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 Donner un feedback
</button> </Button>
)} )}
</div> </div>
</div> </Card>
); );
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
@@ -576,273 +591,254 @@ export default function EventsPageSection({
}; };
return ( return (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16"> <BackgroundSection backgroundImage={backgroundImage}>
{/* Background Image */} {/* Title Section */}
<div <SectionTitle
className="absolute inset-0 bg-cover bg-center bg-no-repeat" variant="gradient"
style={{ size="xl"
backgroundImage: `url('${backgroundImage}')`, subtitle="Événements à venir et passés"
}} className="mb-16"
> >
{/* Dark overlay for readability */} EVENTS
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div> </SectionTitle>
</div> <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 */} {/* Événements à venir */}
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16"> {upcomingEvents.length > 0 && (
{/* 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 */}
<div className="mb-16"> <div className="mb-16">
<h2 className="text-2xl font-bold text-white mb-6 text-center uppercase tracking-widest"> <h2 className="text-3xl font-bold text-white mb-8 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"> <span className="bg-gradient-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
Calendrier Événements à venir
</span> </span>
</h2> </h2>
{renderCalendar()} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{upcomingEvents.map(renderEventCard)}
</div>
</div> </div>
)}
{/* Événements passés */} {/* Calendrier */}
{pastEvents.length > 0 && ( <div className="mb-16">
<div className="mb-16"> <h2 className="text-2xl font-bold text-white mb-6 text-center uppercase tracking-widest">
<h2 className="text-3xl font-bold text-white mb-8 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">
<span className="bg-gradient-to-r from-gray-400 to-gray-600 bg-clip-text text-transparent"> Calendrier
Événements passés </span>
</span> </h2>
</h2> {renderCalendar()}
<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> */}
</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> */}
{/* Event Modal */} {/* Event Modal */}
{selectedEvent && ( {selectedEvent && (
<div <Modal
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" isOpen={!!selectedEvent}
onClick={() => setSelectedEvent(null)} onClose={() => setSelectedEvent(null)}
size="lg"
> >
<div <div className="p-8">
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto shadow-2xl" {/* Header */}
onClick={(e) => e.stopPropagation()} <div className="flex items-center justify-between mb-6">
> <div className="flex-1">
<div className="p-8"> <div className="flex items-center gap-3 mb-2">
{/* Header */} {getStatusBadge(getEventStatus(selectedEvent))}
<div className="flex items-center justify-between mb-6"> <Badge variant="default" size="md">
<div className="flex-1"> {getEventTypeLabel(selectedEvent.type)}
<div className="flex items-center gap-3 mb-2"> </Badge>
{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> </div>
<button <h2 className="text-3xl font-bold text-white uppercase tracking-wide">
onClick={() => setSelectedEvent(null)} {selectedEvent.name}
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition ml-4" </h2>
>
×
</button>
</div> </div>
<CloseButton
onClick={() => setSelectedEvent(null)}
size="lg"
className="ml-4"
/>
</div>
{/* Event Header Color Bar */} {/* Event Header Color Bar */}
<div <div
className={`h-1 bg-gradient-to-r ${getEventTypeColor( className={`h-1 bg-gradient-to-r ${getEventTypeColor(
selectedEvent.type selectedEvent.type
)} mb-6 rounded`} )} mb-6 rounded`}
></div> ></div>
{/* Date */} {/* Date */}
<div className="text-white text-lg font-bold uppercase tracking-widest mb-4"> <div className="text-white text-lg font-bold uppercase tracking-widest mb-4">
{typeof selectedEvent.date === "string" {typeof selectedEvent.date === "string"
? new Date(selectedEvent.date).toLocaleDateString("fr-FR", { ? new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
})
: selectedEvent.date instanceof Date
? selectedEvent.date.toLocaleDateString("fr-FR", {
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
}) })
: selectedEvent.date.toLocaleDateString("fr-FR", { : new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
})} })}
</div> </div>
{/* Event Details */} {/* Event Details */}
{(selectedEvent.room || {(selectedEvent.room ||
selectedEvent.time || selectedEvent.time ||
selectedEvent.maxPlaces) && ( selectedEvent.maxPlaces) && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{selectedEvent.room && ( {selectedEvent.room && (
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20"> <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> <span className="text-pixel-gold text-xl">📍</span>
<div> <div>
<div className="text-xs text-gray-400 uppercase tracking-wider"> <div className="text-xs text-gray-400 uppercase tracking-wider">
Salle Salle
</div>
<div className="font-semibold">
{selectedEvent.room}
</div>
</div> </div>
<div className="font-semibold">{selectedEvent.room}</div>
</div> </div>
)}
{selectedEvent.time && (
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
<span className="text-pixel-gold text-xl">🕐</span>
<div>
<div className="text-xs text-gray-400 uppercase tracking-wider">
Heure
</div>
<div className="font-semibold">
{selectedEvent.time}
</div>
</div>
</div>
)}
{selectedEvent.maxPlaces && (
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
<span className="text-pixel-gold text-xl">👥</span>
<div>
<div className="text-xs text-gray-400 uppercase tracking-wider">
Places
</div>
<div className="font-semibold">
{selectedEvent.maxPlaces}
</div>
</div>
</div>
)}
</div>
)}
{/* Full Description */}
<div className="mb-6">
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-3">
Description
</h3>
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-line">
{selectedEvent.description}
</p>
</div>
{/* Action Button */}
{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> </div>
)} )}
{selectedEvent && getEventStatus(selectedEvent) === "LIVE" && ( {selectedEvent.time && (
<div className="pt-4 border-t border-pixel-gold/20"> <div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border 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"> <span className="text-pixel-gold text-xl">🕐</span>
Rejoindre en direct <div>
</button> <div className="text-xs text-gray-400 uppercase tracking-wider">
</div> Heure
)} </div>
{selectedEvent && getEventStatus(selectedEvent) === "PAST" && ( <div className="font-semibold">{selectedEvent.time}</div>
<div className="pt-4 border-t border-pixel-gold/20"> </div>
<button </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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!session?.user?.id) { handleUnregister(selectedEvent.id);
router.push("/login");
setSelectedEvent(null);
return;
}
setFeedbackEventId(selectedEvent.id);
setSelectedEvent(null); 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]
</button> ? "Annulation..."
</div> : "Se désinscrire"}
)} </Button>
</div> ) : (
<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>
</div> </Modal>
)} )}
{/* Feedback Modal */} {/* Feedback Modal */}
@@ -850,6 +846,6 @@ export default function EventsPageSection({
eventId={feedbackEventId} eventId={feedbackEventId}
onClose={() => setFeedbackEventId(null)} onClose={() => setFeedbackEventId(null)}
/> />
</section> </BackgroundSection>
); );
} }

View File

@@ -3,6 +3,15 @@
import { useState, useEffect, useTransition, type FormEvent } from "react"; import { useState, useEffect, useTransition, type FormEvent } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { createFeedback } from "@/actions/events/feedback"; import { createFeedback } from "@/actions/events/feedback";
import {
Modal,
StarRating,
Textarea,
Button,
Alert,
SectionTitle,
CloseButton,
} from "@/components/ui";
interface Event { interface Event {
id: string; id: string;
@@ -163,129 +172,96 @@ export default function FeedbackModal({
if (!eventId) return null; if (!eventId) return null;
return ( return (
<div <Modal
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" isOpen={!!eventId}
onClick={handleClose} onClose={handleClose}
size="md"
closeOnOverlayClick={!submitting}
> >
<div <div className="p-8">
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl" {/* Header */}
onClick={(e) => e.stopPropagation()} <div className="flex items-center justify-between mb-6">
> <SectionTitle variant="gradient" size="lg">
<div className="p-8"> FEEDBACK
{/* Header */} </SectionTitle>
<div className="flex items-center justify-between mb-6"> <CloseButton onClick={handleClose} disabled={submitting} size="lg" />
<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> </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>
</div> </Modal>
); );
} }

View File

@@ -1,28 +1,16 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { Button, BackgroundSection } from "@/components/ui";
interface HeroSectionProps { interface HeroSectionProps {
backgroundImage: string; backgroundImage: string;
} }
export default function HeroSection({ backgroundImage }: HeroSectionProps) { export default function HeroSection({ backgroundImage }: HeroSectionProps) {
return ( return (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24"> <BackgroundSection backgroundImage={backgroundImage} className="pt-24">
{/* Background Image */} <div className="text-center flex flex-col items-center">
<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">
{/* Game Title */} {/* Game Title */}
<div className="w-full flex justify-center mb-4 overflow-hidden"> <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"> <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 */} {/* Call-to-Action Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16"> <div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
<Link href="/events"> <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"> <Button variant="primary" size="lg">
<span>See events</span> See events
</button> </Button>
</Link> </Link>
<Link href="/leaderboard"> <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></span>
<span>See leaderboard</span> <span>See leaderboard</span>
</button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
</section> </BackgroundSection>
); );
} }

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useEffect, useRef, type ChangeEvent } from "react"; import { useState, useEffect, useRef, type ChangeEvent } from "react";
import { Input, Button, Card } from "@/components/ui";
interface ImageSelectorProps { interface ImageSelectorProps {
value: string; value: string;
@@ -119,20 +120,22 @@ export default function ImageSelector({
<div className="flex-1 space-y-3 min-w-0"> <div className="flex-1 space-y-3 min-w-0">
{/* Input URL */} {/* Input URL */}
<div className="flex flex-col sm:flex-row gap-2"> <div className="flex flex-col sm:flex-row gap-2">
<input <Input
type="text" type="text"
value={urlInput} value={urlInput}
onChange={(e) => setUrlInput(e.target.value)} onChange={(e) => setUrlInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()} onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()}
placeholder="https://example.com/image.jpg ou /image.jpg" 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} 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 URL
</button> </Button>
</div> </div>
{/* Upload depuis le disque */} {/* Upload depuis le disque */}
@@ -145,20 +148,25 @@ export default function ImageSelector({
className="hidden" className="hidden"
id={`file-${label}`} id={`file-${label}`}
/> />
<label <label htmlFor={`file-${label}`}>
htmlFor={`file-${label}`} <Button
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 ${ variant="primary"
uploading ? "opacity-50 cursor-not-allowed" : "" size="sm"
}`} as="span"
> disabled={uploading}
{uploading ? "Upload..." : "Upload depuis le disque"} className="flex-1 text-center cursor-pointer"
>
{uploading ? "Upload..." : "Upload depuis le disque"}
</Button>
</label> </label>
<button <Button
onClick={() => setShowGallery(!showGallery)} 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"} {showGallery ? "Masquer" : "Galerie"}
</button> </Button>
</div> </div>
{/* Chemin de l'image */} {/* Chemin de l'image */}
@@ -170,7 +178,7 @@ export default function ImageSelector({
{/* Galerie d'images */} {/* Galerie d'images */}
{showGallery && ( {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"> <h4 className="text-xs sm:text-sm text-gray-300 mb-3">
Images disponibles Images disponibles
</h4> </h4>
@@ -210,7 +218,7 @@ export default function ImageSelector({
)) ))
)} )}
</div> </div>
</div> </Card>
)} )}
</div> </div>
); );

View File

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

View File

@@ -1,7 +1,14 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Avatar from "./Avatar"; import {
Avatar,
Modal,
CloseButton,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
interface LeaderboardEntry { interface LeaderboardEntry {
rank: number; rank: number;
@@ -33,258 +40,222 @@ export default function LeaderboardSection({
); );
return ( return (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center pt-24 pb-16"> <BackgroundSection backgroundImage={backgroundImage}>
{/* Background Image */} {/* Title Section */}
<div <SectionTitle
className="absolute inset-0 bg-cover bg-center bg-no-repeat" variant="gradient"
style={{ size="lg"
backgroundImage: `url('${backgroundImage}')`, subtitle="Top Players"
}} className="mb-12 overflow-hidden"
> >
{/* Dark overlay for readability */} LEADERBOARD
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div> </SectionTitle>
</div>
{/* Content */} {/* Leaderboard Table */}
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm:px-8 py-16"> <div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
{/* Title Section */} {/* Header */}
<div className="text-center mb-12 overflow-hidden"> <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">
<h1 className="text-3xl sm:text-4xl md:text-7xl font-gaming font-black mb-4 tracking-tight break-words"> <div className="col-span-2 sm:col-span-1 text-center">Rank</div>
<span <div className="col-span-5 sm:col-span-6">Player</div>
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent" <div className="col-span-3 text-right">Score</div>
style={{ <div className="col-span-2 text-right">Level</div>
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>
</div> </div>
{/* Leaderboard Table */} {/* Entries */}
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto"> <div className="divide-y divide-pixel-gold/10 overflow-visible">
{/* Header */} {leaderboard.map((entry) => (
<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
<div className="col-span-2 sm:col-span-1 text-center">Rank</div> key={entry.rank}
<div className="col-span-5 sm:col-span-6">Player</div> className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative ${
<div className="col-span-3 text-right">Score</div> entry.rank <= 3
<div className="col-span-2 text-right">Level</div> ? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
</div> : "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 */} {/* Player */}
<div className="divide-y divide-pixel-gold/10 overflow-visible"> <div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
{leaderboard.map((entry) => ( <Avatar
<div src={entry.avatar}
key={entry.rank} username={entry.username}
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative ${ size="sm"
entry.rank <= 3 className="flex-shrink-0"
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent" borderClassName="border-pixel-gold/30"
: "bg-black/40" />
}`} <div
> className="flex items-center gap-1 sm:gap-2 cursor-pointer hover:opacity-80 transition min-w-0"
{/* Rank */} onClick={() => setSelectedEntry(entry)}
<div className="col-span-2 sm:col-span-1 flex items-center justify-center"> >
<span <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 ${ className={`font-bold text-xs sm:text-sm truncate ${
entry.rank === 1 entry.rank <= 3 ? "text-pixel-gold" : "text-white"
? "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} {entry.username}
</span> </span>
</div> {entry.characterClass && (
<span className="text-xs text-gray-400 uppercase tracking-wider">
{/* Player */} [{entry.characterClass === "WARRIOR" && "⚔️"}
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0"> {entry.characterClass === "MAGE" && "🔮"}
<Avatar {entry.characterClass === "ROGUE" && "🗡️"}
src={entry.avatar} {entry.characterClass === "RANGER" && "🏹"}
username={entry.username} {entry.characterClass === "PALADIN" && "🛡️"}
size="sm" {entry.characterClass === "ENGINEER" && "⚙️"}
className="flex-shrink-0" {entry.characterClass === "MERCHANT" && "💰"}
borderClassName="border-pixel-gold/30" {entry.characterClass === "SCHOLAR" && "📚"}
/> {entry.characterClass === "BERSERKER" && "🔥"}
<div {entry.characterClass === "NECROMANCER" && "💀"}]
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}
</span> </span>
{entry.characterClass && ( )}
<span className="text-xs text-gray-400 uppercase tracking-wider"> {entry.rank <= 3 && (
[{entry.characterClass === "WARRIOR" && "⚔️"} <span className="text-pixel-gold text-xs"></span>
{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>
)} )}
</div> </div>
</div> </div>
{/* Stats */} {/* Score */}
<div className="grid grid-cols-2 gap-4 mb-6"> <div className="col-span-3 flex items-center justify-end">
<div className="bg-black/60 border border-pixel-gold/30 rounded p-4"> <span className="font-mono text-gray-300 text-xs sm:text-sm">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1"> {formatScore(entry.score)}
Score </span>
</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>
</div> </div>
{/* Bio */} {/* Level */}
{selectedEntry.bio && ( <div className="col-span-2 flex items-center justify-end">
<div className="border-t border-pixel-gold/30 pt-6"> <span className="font-bold text-gray-400 text-xs sm:text-sm">
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-3 font-bold"> Lv.{entry.level}
Bio </span>
</div> </div>
<p className="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
{selectedEntry.bio}
</p>
</div>
)}
</div> </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 { useState } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import PlayerStats from "./PlayerStats"; import PlayerStats from "./PlayerStats";
import { Button } from "@/components/ui";
interface UserData { interface UserData {
username: string; username: string;
@@ -100,12 +101,14 @@ export default function Navigation({
{/* Desktop Auth Buttons */} {/* Desktop Auth Buttons */}
<div className="hidden md:flex items-center gap-4"> <div className="hidden md:flex items-center gap-4">
{isAuthenticated ? ( {isAuthenticated ? (
<button <Button
onClick={() => signOut()} 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 Déconnexion
</button> </Button>
) : ( ) : (
<> <>
<Link <Link
@@ -114,11 +117,10 @@ export default function Navigation({
> >
Connexion Connexion
</Link> </Link>
<Link <Link href="/register">
href="/register" <Button variant="primary" size="sm" className="text-xs">
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
> </Button>
Inscription
</Link> </Link>
</> </>
)} )}
@@ -197,15 +199,17 @@ export default function Navigation({
{/* Mobile Auth Buttons */} {/* Mobile Auth Buttons */}
<div className="flex flex-col gap-3 pt-2 border-t border-gray-800/30"> <div className="flex flex-col gap-3 pt-2 border-t border-gray-800/30">
{isAuthenticated ? ( {isAuthenticated ? (
<button <Button
onClick={() => { onClick={() => {
signOut(); signOut();
setIsMenuOpen(false); 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 Déconnexion
</button> </Button>
) : ( ) : (
<> <>
<Link <Link
@@ -215,12 +219,14 @@ export default function Navigation({
> >
Connexion Connexion
</Link> </Link>
<Link <Link href="/register" onClick={() => setIsMenuOpen(false)}>
href="/register" <Button
onClick={() => setIsMenuOpen(false)} variant="primary"
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" size="sm"
> className="text-xs w-full text-center"
Inscription >
Inscription
</Button>
</Link> </Link>
</> </>
)} )}

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import Link from "next/link"; import Link from "next/link";
import Avatar from "./Avatar"; import { Avatar } from "@/components/ui";
interface UserData { interface UserData {
username: string; username: string;

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useRef, useTransition, type ChangeEvent } from "react"; 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 { updateProfile } from "@/actions/profile/update-profile";
import { updatePassword } from "@/actions/profile/update-password"; import { updatePassword } from "@/actions/profile/update-password";
@@ -170,53 +170,19 @@ export default function ProfileForm({
: "from-red-700 to-red-900"; : "from-red-700 to-red-900";
return ( return (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16"> <BackgroundSection backgroundImage={backgroundImage}>
{/* Background Image */} <div className="w-full max-w-4xl mx-auto px-8">
<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">
{/* Title Section */} {/* Title Section */}
<div className="text-center mb-12"> <SectionTitle variant="gradient" size="lg" subtitle="Gérez votre profil" className="mb-12">
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight"> PROFIL
<span </SectionTitle>
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>
{/* Profile Card */} {/* 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"> <form onSubmit={handleSubmit} className="p-8 space-y-8">
{/* Messages */} {/* Messages */}
{error && ( {error && <Alert variant="error">{error}</Alert>}
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm"> {success && <Alert variant="success">{success}</Alert>}
{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>
)}
{/* Avatar Section */} {/* Avatar Section */}
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
@@ -281,51 +247,38 @@ export default function ProfileForm({
className="hidden" className="hidden"
id="avatar-upload" id="avatar-upload"
/> />
<label <label htmlFor="avatar-upload">
htmlFor="avatar-upload" <Button variant="primary" size="md" as="span" className="cursor-pointer">
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"}
> </Button>
{uploadingAvatar
? "Upload en cours..."
: "Upload un avatar custom"}
</label> </label>
</div> </div>
</div> </div>
{/* Username Field */} {/* Username Field */}
<div> <Input
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2"> type="text"
Nom d&apos;utilisateur label="Nom d'utilisateur"
</label> value={username}
<input onChange={(e) => setUsername(e.target.value)}
type="text" required
value={username} minLength={3}
onChange={(e) => setUsername(e.target.value)} maxLength={20}
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" className="bg-black/40"
required />
minLength={3} <p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
maxLength={20}
/>
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
</div>
{/* Bio Field */} {/* Bio Field */}
<div> <Textarea
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2"> label="Bio"
Bio value={bio || ""}
</label> onChange={(e) => setBio(e.target.value)}
<textarea rows={4}
value={bio || ""} maxLength={500}
onChange={(e) => setBio(e.target.value)} showCharCount
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" placeholder="Parlez-nous de vous..."
rows={4} className="bg-black/40"
maxLength={500} />
placeholder="Parlez-nous de vous..."
/>
<p className="text-gray-500 text-xs mt-1">
{(bio || "").length}/500 caractères
</p>
</div>
{/* Character Class Selection */} {/* Character Class Selection */}
<div> <div>
@@ -458,36 +411,23 @@ export default function ProfileForm({
</div> </div>
{/* HP Bar */} {/* HP Bar */}
<div className="mb-4"> <ProgressBar
<div className="flex justify-between text-xs text-gray-400 mb-1"> value={profile.hp}
<span>HP</span> max={profile.maxHp}
<span> variant="hp"
{profile.hp} / {profile.maxHp} showLabel
</span> label="HP"
</div> className="mb-4"
<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>
{/* XP Bar */} {/* XP Bar */}
<div> <ProgressBar
<div className="flex justify-between text-xs text-gray-400 mb-1"> value={profile.xp}
<span>XP</span> max={profile.maxXp}
<span> variant="xp"
{formatNumber(profile.xp)} / {formatNumber(profile.maxXp)} showLabel
</span> label="XP"
</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>
</div> </div>
{/* Email (read-only) */} {/* Email (read-only) */}
@@ -505,13 +445,9 @@ export default function ProfileForm({
{/* Submit Button */} {/* Submit Button */}
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20"> <div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
<button <Button type="submit" variant="primary" size="md" disabled={isPending}>
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"
>
{isPending ? "Enregistrement..." : "Enregistrer les modifications"} {isPending ? "Enregistrement..." : "Enregistrer les modifications"}
</button> </Button>
</div> </div>
</form> </form>
@@ -522,65 +458,56 @@ export default function ProfileForm({
Mot de passe Mot de passe
</h3> </h3>
{!showPasswordForm && ( {!showPasswordForm && (
<button <Button
type="button" type="button"
variant="primary"
size="md"
onClick={() => setShowPasswordForm(true)} 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 Changer le mot de passe
</button> </Button>
)} )}
</div> </div>
{showPasswordForm && ( {showPasswordForm && (
<form onSubmit={handlePasswordChange} className="space-y-4"> <form onSubmit={handlePasswordChange} className="space-y-4">
<div> <Input
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2"> type="password"
Mot de passe actuel label="Mot de passe actuel"
</label> value={currentPassword}
<input onChange={(e) => setCurrentPassword(e.target.value)}
type="password" required
value={currentPassword} className="bg-black/40"
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>
<div> <Input
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2"> type="password"
Nouveau mot de passe label="Nouveau mot de passe"
</label> value={newPassword}
<input onChange={(e) => setNewPassword(e.target.value)}
type="password" required
value={newPassword} minLength={6}
onChange={(e) => setNewPassword(e.target.value)} className="bg-black/40"
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 <p className="text-gray-500 text-xs mt-1">
minLength={6} Minimum 6 caractères
/> </p>
<p className="text-gray-500 text-xs mt-1">
Minimum 6 caractères
</p>
</div>
<div> <Input
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2"> type="password"
Confirmer le nouveau mot de passe label="Confirmer le nouveau mot de passe"
</label> value={confirmPassword}
<input onChange={(e) => setConfirmPassword(e.target.value)}
type="password" required
value={confirmPassword} minLength={6}
onChange={(e) => setConfirmPassword(e.target.value)} className="bg-black/40"
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>
<div className="flex justify-end gap-4"> <div className="flex justify-end gap-4">
<button <Button
type="button" type="button"
variant="secondary"
size="md"
onClick={() => { onClick={() => {
setShowPasswordForm(false); setShowPasswordForm(false);
setCurrentPassword(""); setCurrentPassword("");
@@ -588,25 +515,25 @@ export default function ProfileForm({
setConfirmPassword(""); setConfirmPassword("");
setError(null); 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 Annuler
</button> </Button>
<button <Button
type="submit" type="submit"
variant="primary"
size="md"
disabled={isChangingPassword} 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 {isChangingPassword
? "Modification..." ? "Modification..."
: "Modifier le mot de passe"} : "Modifier le mot de passe"}
</button> </Button>
</div> </div>
</form> </form>
)} )}
</div> </div>
</div> </Card>
</div> </div>
</section> </BackgroundSection>
); );
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useEffect, useTransition } from "react"; 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"; import { updateUser, deleteUser } from "@/actions/admin/users";
interface User { interface User {
@@ -184,10 +184,7 @@ export default function UserManagement() {
: user.username; : user.username;
return ( return (
<div <Card key={user.id} variant="default" className="p-3 sm:p-4">
key={user.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-center gap-3 mb-2"> <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"> <div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0">
{/* Avatar */} {/* Avatar */}
@@ -248,23 +245,19 @@ export default function UserManagement() {
{isEditing ? ( {isEditing ? (
<div className="space-y-4"> <div className="space-y-4">
{/* Username Section */} {/* Username Section */}
<div> <Input
<label className="block text-xs sm:text-sm text-gray-300 mb-2"> type="text"
Nom d&apos;utilisateur label="Nom d'utilisateur"
</label> value={editingUser.username || ""}
<input onChange={(e) =>
type="text" setEditingUser({
value={editingUser.username || ""} ...editingUser,
onChange={(e) => username: e.target.value,
setEditingUser({ })
...editingUser, }
username: e.target.value, placeholder="Nom d'utilisateur"
}) className="text-xs sm:text-sm px-2 sm:px-3 py-1"
} />
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>
{/* Avatar Section */} {/* Avatar Section */}
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
@@ -372,13 +365,17 @@ export default function UserManagement() {
className="hidden" className="hidden"
id={`avatar-upload-${user.id}`} id={`avatar-upload-${user.id}`}
/> />
<label <label htmlFor={`avatar-upload-${user.id}`}>
htmlFor={`avatar-upload-${user.id}`} <Button
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" variant="primary"
> size="sm"
{uploadingAvatar === user.id as="span"
? "Upload en cours..." className="cursor-pointer"
: "Upload un avatar custom"} >
{uploadingAvatar === user.id
? "Upload en cours..."
: "Upload un avatar custom"}
</Button>
</label> </label>
</div> </div>
</div> </div>
@@ -690,19 +687,21 @@ export default function UserManagement() {
</div> </div>
<div className="flex flex-col sm:flex-row gap-2 pt-2"> <div className="flex flex-col sm:flex-row gap-2 pt-2">
<button <Button
onClick={handleSave} onClick={handleSave}
variant="success"
size="md"
disabled={saving} 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"} {saving ? "Enregistrement..." : "Enregistrer"}
</button> </Button>
<button <Button
onClick={handleCancel} 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 Annuler
</button> </Button>
</div> </div>
</div> </div>
) : ( ) : (
@@ -747,7 +746,7 @@ export default function UserManagement() {
</div> </div>
</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";