Compare commits

...

3 Commits

46 changed files with 2832 additions and 1989 deletions

View File

@@ -2,8 +2,8 @@ import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
import { Role } from "@/prisma/generated/prisma/client";
import NavigationWrapper from "@/components/NavigationWrapper";
import AdminPanel from "@/components/AdminPanel";
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
import AdminPanel from "@/components/admin/AdminPanel";
export const dynamic = "force-dynamic";

View File

@@ -0,0 +1,495 @@
"use client";
import { useState } from "react";
import Navigation from "@/components/navigation/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 src={null} 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

@@ -1,5 +1,5 @@
import NavigationWrapper from "@/components/NavigationWrapper";
import EventsPageSection from "@/components/EventsPageSection";
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
import EventsPageSection from "@/components/events/EventsPageSection";
import { eventService } from "@/services/events/event.service";
import { eventRegistrationService } from "@/services/events/event-registration.service";
import { getBackgroundImage } from "@/lib/preferences";

View File

@@ -3,8 +3,17 @@
import { useState, useEffect, useTransition, type FormEvent } from "react";
import { useSession } from "next-auth/react";
import { useRouter, useParams } from "next/navigation";
import Navigation from "@/components/Navigation";
import Navigation from "@/components/navigation/Navigation";
import { createFeedback } from "@/actions/events/feedback";
import {
StarRating,
Textarea,
Button,
Alert,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
interface Event {
id: string;
@@ -156,25 +165,17 @@ export default function FeedbackPageClient({
return (
<main className="min-h-screen bg-black relative">
<Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
<BackgroundSection backgroundImage={backgroundImage} className="pt-24">
{/* Feedback Form */}
<div className="relative z-10 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">
<h1 className="text-4xl font-gaming font-black mb-2 text-center">
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
FEEDBACK
</span>
</h1>
<div className="w-full max-w-2xl mx-auto px-8">
<Card variant="dark" className="p-8">
<SectionTitle
variant="gradient"
size="lg"
className="mb-2 text-center"
>
FEEDBACK
</SectionTitle>
<p className="text-gray-400 text-sm text-center mb-2">
{existingFeedback
? "Modifier votre feedback pour"
@@ -185,15 +186,15 @@ export default function FeedbackPageClient({
</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">
<Alert variant="success" className="mb-6">
Feedback enregistré avec succès ! Redirection...
</div>
</Alert>
)}
{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}
</div>
</Alert>
)}
<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">
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)}
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>
<StarRating
value={rating}
onChange={setRating}
size="lg"
showValue
/>
</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}
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>
<Textarea
id="comment"
label="Commentaire (optionnel)"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={6}
maxLength={1000}
showCharCount
placeholder="Partagez votre expérience, vos suggestions..."
/>
{/* Submit Button */}
<button
<Button
type="submit"
variant="primary"
size="lg"
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
? "Enregistrement..."
: existingFeedback
? "Modifier le feedback"
: "Envoyer le feedback"}
</button>
</Button>
</form>
</div>
</Card>
</div>
</section>
</BackgroundSection>
</main>
);
}

View File

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

View File

@@ -2,7 +2,7 @@ import type { Metadata } from "next";
import type { ReactNode } from "react";
import { Orbitron, Rajdhani } from "next/font/google";
import "./globals.css";
import SessionProvider from "@/components/SessionProvider";
import SessionProvider from "@/components/layout/SessionProvider";
const orbitron = Orbitron({
subsets: ["latin"],

View File

@@ -1,5 +1,5 @@
import NavigationWrapper from "@/components/NavigationWrapper";
import LeaderboardSection from "@/components/LeaderboardSection";
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
import LeaderboardSection from "@/components/leaderboard/LeaderboardSection";
import { userStatsService } from "@/services/users/user-stats.service";
import { getBackgroundImage } from "@/lib/preferences";

View File

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

View File

@@ -1,6 +1,6 @@
import NavigationWrapper from "@/components/NavigationWrapper";
import HeroSection from "@/components/HeroSection";
import EventsSection from "@/components/EventsSection";
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
import HeroSection from "@/components/layout/HeroSection";
import EventsSection from "@/components/events/EventsSection";
import { eventService } from "@/services/events/event.service";
import { getBackgroundImage } from "@/lib/preferences";

View File

@@ -2,8 +2,8 @@ import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { userService } from "@/services/users/user.service";
import { getBackgroundImage } from "@/lib/preferences";
import NavigationWrapper from "@/components/NavigationWrapper";
import ProfileForm from "@/components/ProfileForm";
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
import ProfileForm from "@/components/profile/ProfileForm";
export default async function ProfilePage() {
const session = await auth();

View File

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

View File

@@ -1,119 +0,0 @@
"use client";
import { useState } from "react";
import UserManagement from "@/components/UserManagement";
import EventManagement from "@/components/EventManagement";
import FeedbackManagement from "@/components/FeedbackManagement";
import BackgroundPreferences from "@/components/BackgroundPreferences";
interface SitePreferences {
id: string;
homeBackground: string | null;
eventsBackground: string | null;
leaderboardBackground: string | null;
}
interface AdminPanelProps {
initialPreferences: SitePreferences;
}
type AdminSection = "preferences" | "users" | "events" | "feedbacks";
export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
const [activeSection, setActiveSection] =
useState<AdminSection>("preferences");
return (
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
<h1 className="text-4xl font-gaming font-black mb-8 text-center">
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
ADMIN
</span>
</h1>
{/* Navigation Tabs */}
<div className="flex gap-4 mb-8 justify-center">
<button
onClick={() => setActiveSection("preferences")}
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
activeSection === "preferences"
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
}`}
>
Préférences UI
</button>
<button
onClick={() => setActiveSection("users")}
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
activeSection === "users"
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
}`}
>
Utilisateurs
</button>
<button
onClick={() => setActiveSection("events")}
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
activeSection === "events"
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
}`}
>
Événements
</button>
<button
onClick={() => setActiveSection("feedbacks")}
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
activeSection === "feedbacks"
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
}`}
>
Feedbacks
</button>
</div>
{activeSection === "preferences" && (
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-4 sm:p-6 backdrop-blur-sm">
<h2 className="text-xl sm:text-2xl font-gaming font-bold mb-6 text-pixel-gold break-words">
Préférences UI Globales
</h2>
<div className="space-y-4">
<BackgroundPreferences initialPreferences={initialPreferences} />
</div>
</div>
)}
{activeSection === "users" && (
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Utilisateurs
</h2>
<UserManagement />
</div>
)}
{activeSection === "events" && (
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Événements
</h2>
<EventManagement />
</div>
)}
{activeSection === "feedbacks" && (
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Feedbacks
</h2>
<FeedbackManagement />
</div>
)}
</div>
</section>
);
}

View File

@@ -1,443 +0,0 @@
"use client";
import { useState, useEffect, useTransition } from "react";
import { calculateEventStatus } from "@/lib/eventStatus";
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
interface Event {
id: string;
date: string;
name: string;
description: string;
type: "ATELIER" | "KATA" | "PRESENTATION" | "LEARNING_HOUR";
status: "UPCOMING" | "LIVE" | "PAST";
room?: string | null;
time?: string | null;
maxPlaces?: number | null;
createdAt: string;
updatedAt: string;
registrationsCount?: number;
}
interface EventFormData {
date: string;
name: string;
description: string;
type: "ATELIER" | "KATA" | "PRESENTATION" | "LEARNING_HOUR";
room?: string;
time?: string;
maxPlaces?: number;
}
const eventTypes: Event["type"][] = [
"ATELIER",
"KATA",
"PRESENTATION",
"LEARNING_HOUR",
];
const getEventTypeLabel = (type: Event["type"]) => {
switch (type) {
case "ATELIER":
return "Atelier";
case "KATA":
return "Kata";
case "PRESENTATION":
return "Présentation";
case "LEARNING_HOUR":
return "Learning Hour";
default:
return type;
}
};
const getStatusLabel = (status: Event["status"]) => {
switch (status) {
case "UPCOMING":
return "À venir";
case "LIVE":
return "En cours";
case "PAST":
return "Passé";
default:
return status;
}
};
export default function EventManagement() {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState<EventFormData>({
date: "",
name: "",
description: "",
type: "ATELIER",
room: "",
time: "",
maxPlaces: undefined,
});
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => {
try {
const response = await fetch("/api/admin/events");
if (response.ok) {
const data = await response.json();
setEvents(data);
}
} catch (error) {
console.error("Error fetching events:", error);
} finally {
setLoading(false);
}
};
const handleCreate = () => {
setIsCreating(true);
setEditingEvent(null);
setFormData({
date: "",
name: "",
description: "",
type: "ATELIER",
room: "",
time: "",
maxPlaces: undefined,
});
};
const handleEdit = (event: Event) => {
setEditingEvent(event);
setIsCreating(false);
setFormData({
date: event.date,
name: event.name,
description: event.description,
type: event.type,
room: event.room || "",
time: event.time || "",
maxPlaces: event.maxPlaces || undefined,
});
};
const [, startTransition] = useTransition();
const handleSave = async () => {
setSaving(true);
startTransition(async () => {
try {
let result;
if (isCreating) {
result = await createEvent(formData);
} else if (editingEvent) {
result = await updateEvent(editingEvent.id, formData);
}
if (result?.success) {
await fetchEvents();
setEditingEvent(null);
setIsCreating(false);
setFormData({
date: "",
name: "",
description: "",
type: "ATELIER",
room: "",
time: "",
maxPlaces: undefined,
});
} else {
alert(result?.error || "Erreur lors de la sauvegarde");
}
} catch (error) {
console.error("Error saving event:", error);
alert("Erreur lors de la sauvegarde");
} finally {
setSaving(false);
}
});
};
const handleDelete = async (eventId: string) => {
if (!confirm("Êtes-vous sûr de vouloir supprimer cet événement ?")) {
return;
}
startTransition(async () => {
try {
const result = await deleteEvent(eventId);
if (result.success) {
await fetchEvents();
} else {
alert(result.error || "Erreur lors de la suppression");
}
} catch (error) {
console.error("Error deleting event:", error);
alert("Erreur lors de la suppression");
}
});
};
const handleCancel = () => {
setEditingEvent(null);
setIsCreating(false);
setFormData({
date: "",
name: "",
description: "",
type: "ATELIER",
room: "",
time: "",
maxPlaces: undefined,
});
};
if (loading) {
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
}
return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-4">
<h3 className="text-lg sm:text-xl font-gaming font-bold text-pixel-gold break-words">
Événements ({events.length})
</h3>
{!isCreating && !editingEvent && (
<button
onClick={handleCreate}
className="px-3 sm:px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-green-900/30 transition whitespace-nowrap flex-shrink-0"
>
+ Nouvel événement
</button>
)}
</div>
{(isCreating || editingEvent) && (
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4 mb-4">
<h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words">
{isCreating ? "Créer un événement" : "Modifier l'événement"}
</h4>
<div className="space-y-4">
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Date
</label>
<input
type="date"
value={formData.date}
onChange={(e) =>
setFormData({ ...formData, date: e.target.value })
}
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Nom
</label>
<input
type="text"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
placeholder="Nom de l'événement"
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Description
</label>
<textarea
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
placeholder="Description de l'événement"
rows={4}
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Type
</label>
<select
value={formData.type}
onChange={(e) =>
setFormData({
...formData,
type: e.target.value as Event["type"],
})
}
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
>
{eventTypes.map((type) => (
<option key={type} value={type}>
{getEventTypeLabel(type)}
</option>
))}
</select>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Salle
</label>
<input
type="text"
value={formData.room || ""}
onChange={(e) =>
setFormData({ ...formData, room: e.target.value })
}
placeholder="Ex: Nautilus"
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Heure
</label>
<input
type="text"
value={formData.time || ""}
onChange={(e) =>
setFormData({ ...formData, time: e.target.value })
}
placeholder="Ex: 11h-12h"
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Places max
</label>
<input
type="number"
value={formData.maxPlaces || ""}
onChange={(e) =>
setFormData({
...formData,
maxPlaces: e.target.value
? parseInt(e.target.value)
: undefined,
})
}
placeholder="Ex: 25"
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
/>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<button
onClick={handleSave}
disabled={saving}
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50"
>
{saving ? "Enregistrement..." : "Enregistrer"}
</button>
<button
onClick={handleCancel}
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
>
Annuler
</button>
</div>
</div>
</div>
)}
{events.length === 0 ? (
<div className="text-center text-gray-400 py-8">
Aucun événement trouvé
</div>
) : (
<div className="space-y-3">
{events.map((event) => (
<div
key={event.id}
className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4"
>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
{event.name}
</h4>
<span className="px-2 py-1 bg-pixel-gold/20 border border-pixel-gold/50 text-pixel-gold text-[10px] sm:text-xs uppercase rounded whitespace-nowrap flex-shrink-0">
{getEventTypeLabel(event.type)}
</span>
<span
className={`px-2 py-1 text-[10px] sm:text-xs uppercase rounded whitespace-nowrap flex-shrink-0 ${(() => {
const status = calculateEventStatus(event.date);
return status === "UPCOMING"
? "bg-green-900/50 border border-green-500/50 text-green-400"
: status === "LIVE"
? "bg-yellow-900/50 border border-yellow-500/50 text-yellow-400"
: "bg-gray-900/50 border border-gray-500/50 text-gray-400";
})()}`}
>
{getStatusLabel(calculateEventStatus(event.date))}
</span>
</div>
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
{event.description}
</p>
<div className="flex flex-wrap items-center gap-2 sm:gap-4 mt-2">
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
Date: {new Date(event.date).toLocaleDateString("fr-FR")}
</p>
{event.room && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
📍 Salle: {event.room}
</p>
)}
{event.time && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
🕐 Heure: {event.time}
</p>
)}
{event.maxPlaces && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
👥 Places: {event.maxPlaces}
</p>
)}
<span className="px-2 py-1 bg-blue-900/30 border border-blue-500/50 text-blue-400 text-[10px] sm:text-xs rounded whitespace-nowrap flex-shrink-0">
{event.registrationsCount || 0} inscrit
{event.registrationsCount !== 1 ? "s" : ""}
</span>
</div>
</div>
{!isCreating && !editingEvent && (
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
<button
onClick={() => handleEdit(event)}
className="px-2 sm:px-3 py-1 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap"
>
Modifier
</button>
<button
onClick={() => handleDelete(event.id)}
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-red-900/30 transition whitespace-nowrap"
>
Supprimer
</button>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -1,291 +0,0 @@
"use client";
import { useState, useEffect, useTransition, type FormEvent } from "react";
import { useSession } from "next-auth/react";
import { createFeedback } from "@/actions/events/feedback";
interface Event {
id: string;
name: string;
date: string;
description: string;
}
interface Feedback {
id: string;
rating: number;
comment: string | null;
}
interface FeedbackModalProps {
eventId: string | null;
eventName?: string;
onClose: () => void;
}
export default function FeedbackModal({
eventId,
eventName: _eventName,
onClose,
}: FeedbackModalProps) {
const { status } = useSession();
const [event, setEvent] = useState<Event | null>(null);
const [existingFeedback, setExistingFeedback] = useState<Feedback | null>(
null
);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [, startTransition] = useTransition();
const [rating, setRating] = useState(0);
const [comment, setComment] = useState("");
// Réinitialiser les états quand eventId change
useEffect(() => {
if (eventId) {
setEvent(null);
setExistingFeedback(null);
setRating(0);
setComment("");
setError("");
setSuccess(false);
setLoading(true);
}
}, [eventId]);
const fetchEventAndFeedback = async () => {
if (!eventId) return;
try {
// Récupérer l'événement
const eventResponse = await fetch(`/api/events/${eventId}`);
if (!eventResponse.ok) {
setError("Événement introuvable");
setLoading(false);
return;
}
const eventData = await eventResponse.json();
setEvent(eventData);
// Récupérer le feedback existant si disponible
const feedbackResponse = await fetch(`/api/feedback/${eventId}`);
if (feedbackResponse.ok) {
const feedbackData = await feedbackResponse.json();
if (feedbackData.feedback) {
setExistingFeedback(feedbackData.feedback);
setRating(feedbackData.feedback.rating);
setComment(feedbackData.feedback.comment || "");
} else {
// Pas de feedback existant, réinitialiser
setRating(0);
setComment("");
}
} else {
// Pas de feedback existant, réinitialiser
setRating(0);
setComment("");
}
} catch {
setError("Erreur lors du chargement des données");
} finally {
setLoading(false);
}
};
useEffect(() => {
if (status === "unauthenticated") {
onClose();
return;
}
if (status === "authenticated" && eventId) {
fetchEventAndFeedback();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status, eventId]);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!eventId) return;
setError("");
setSuccess(false);
if (rating === 0) {
setError("Veuillez sélectionner une note");
return;
}
setSubmitting(true);
startTransition(async () => {
try {
const result = await createFeedback(eventId, {
rating,
comment: comment.trim() || null,
});
if (!result.success) {
setError(result.error || "Erreur lors de l'enregistrement");
setSubmitting(false);
return;
}
setSuccess(true);
if (result.data) {
setExistingFeedback({
id: result.data.id,
rating: result.data.rating,
comment: result.data.comment,
});
}
// Fermer la modale après 1.5 secondes
setTimeout(() => {
onClose();
}, 1500);
} catch {
setError("Erreur lors de l'enregistrement");
} finally {
setSubmitting(false);
}
});
};
const handleClose = () => {
if (!submitting) {
onClose();
}
};
if (!eventId) return null;
return (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={handleClose}
>
<div
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="p-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h1 className="text-4xl font-gaming font-black">
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
FEEDBACK
</span>
</h1>
<button
onClick={handleClose}
disabled={submitting}
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition disabled:opacity-50 disabled:cursor-not-allowed"
>
×
</button>
</div>
{loading ? (
<div className="text-white text-center py-8">Chargement...</div>
) : !event ? (
<div className="text-red-400 text-center py-8">
Événement introuvable
</div>
) : (
<>
<p className="text-gray-400 text-sm text-center mb-2">
{existingFeedback
? "Modifier votre feedback pour"
: "Donnez votre avis sur"}
</p>
<p className="text-pixel-gold text-lg font-semibold text-center mb-8">
{event.name}
</p>
{success && (
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm mb-6">
Feedback enregistré avec succès !
</div>
)}
{error && (
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm mb-6">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Rating */}
<div>
<label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">
Note
</label>
<div className="flex items-center justify-center gap-2">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => setRating(star)}
disabled={submitting}
className={`text-4xl transition-transform hover:scale-110 disabled:hover:scale-100 ${
star <= rating
? "text-pixel-gold"
: "text-gray-600 hover:text-gray-500"
}`}
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
>
</button>
))}
</div>
<p className="text-gray-500 text-xs text-center mt-2">
{rating > 0 && `${rating}/5`}
</p>
</div>
{/* Comment */}
<div>
<label
htmlFor="comment"
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
>
Commentaire (optionnel)
</label>
<textarea
id="comment"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={6}
maxLength={1000}
disabled={submitting}
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none disabled:opacity-50"
placeholder="Partagez votre expérience, vos suggestions..."
/>
<p className="text-gray-500 text-xs mt-1 text-right">
{comment.length}/1000 caractères
</p>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={submitting || rating === 0}
className="w-full px-6 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{submitting
? "Enregistrement..."
: existingFeedback
? "Modifier le feedback"
: "Envoyer le feedback"}
</button>
</form>
</>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,290 +0,0 @@
"use client";
import { useState } from "react";
import Avatar from "./Avatar";
interface LeaderboardEntry {
rank: number;
username: string;
email: string;
score: number;
level: number;
avatar?: string | null;
bio?: string | null;
characterClass?: string | null;
}
interface LeaderboardSectionProps {
leaderboard: LeaderboardEntry[];
backgroundImage: string;
}
// Format number with consistent locale to avoid hydration mismatch
const formatScore = (score: number): string => {
return score.toLocaleString("en-US");
};
export default function LeaderboardSection({
leaderboard,
backgroundImage,
}: LeaderboardSectionProps) {
const [selectedEntry, setSelectedEntry] = useState<LeaderboardEntry | null>(
null
);
return (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center pt-24 pb-16">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* 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-6xl mx-auto px-4 sm:px-8 py-16">
{/* Title Section */}
<div className="text-center mb-12 overflow-hidden">
<h1 className="text-3xl sm:text-4xl md:text-7xl font-gaming font-black mb-4 tracking-tight break-words">
<span
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
style={{
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
}}
>
LEADERBOARD
</span>
</h1>
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
<span></span>
<span>Top Players</span>
<span></span>
</div>
</div>
{/* Leaderboard Table */}
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
{/* Header */}
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
<div className="col-span-5 sm:col-span-6">Player</div>
<div className="col-span-3 text-right">Score</div>
<div className="col-span-2 text-right">Level</div>
</div>
{/* Entries */}
<div className="divide-y divide-pixel-gold/10 overflow-visible">
{leaderboard.map((entry) => (
<div
key={entry.rank}
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative ${
entry.rank <= 3
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
: "bg-black/40"
}`}
>
{/* Rank */}
<div className="col-span-2 sm:col-span-1 flex items-center justify-center">
<span
className={`inline-flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-bold text-xs sm:text-sm ${
entry.rank === 1
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
: entry.rank === 2
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
: entry.rank === 3
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
: "bg-gray-900 text-gray-400 border border-gray-800"
}`}
>
{entry.rank}
</span>
</div>
{/* Player */}
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
<Avatar
src={entry.avatar}
username={entry.username}
size="sm"
className="flex-shrink-0"
borderClassName="border-pixel-gold/30"
/>
<div
className="flex items-center gap-1 sm:gap-2 cursor-pointer hover:opacity-80 transition min-w-0"
onClick={() => setSelectedEntry(entry)}
>
<span
className={`font-bold text-xs sm:text-sm truncate ${
entry.rank <= 3 ? "text-pixel-gold" : "text-white"
}`}
>
{entry.username}
</span>
{entry.characterClass && (
<span className="text-xs text-gray-400 uppercase tracking-wider">
[{entry.characterClass === "WARRIOR" && "⚔️"}
{entry.characterClass === "MAGE" && "🔮"}
{entry.characterClass === "ROGUE" && "🗡️"}
{entry.characterClass === "RANGER" && "🏹"}
{entry.characterClass === "PALADIN" && "🛡️"}
{entry.characterClass === "ENGINEER" && "⚙️"}
{entry.characterClass === "MERCHANT" && "💰"}
{entry.characterClass === "SCHOLAR" && "📚"}
{entry.characterClass === "BERSERKER" && "🔥"}
{entry.characterClass === "NECROMANCER" && "💀"}]
</span>
)}
{entry.rank <= 3 && (
<span className="text-pixel-gold text-xs"></span>
)}
</div>
</div>
{/* Score */}
<div className="col-span-3 flex items-center justify-end">
<span className="font-mono text-gray-300 text-xs sm:text-sm">
{formatScore(entry.score)}
</span>
</div>
{/* Level */}
<div className="col-span-2 flex items-center justify-end">
<span className="font-bold text-gray-400 text-xs sm:text-sm">
Lv.{entry.level}
</span>
</div>
</div>
))}
</div>
</div>
{/* Footer Info */}
<div className="mt-8 text-center">
<p className="text-gray-500 text-sm">
Compete with players worldwide and climb the ranks!
</p>
<p className="text-gray-600 text-xs mt-2">
Rankings update every hour
</p>
</div>
</div>
{/* Character Modal */}
{selectedEntry && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={() => setSelectedEntry(null)}
>
<div
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="p-4 sm:p-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
{selectedEntry.username}
</h2>
<button
onClick={() => setSelectedEntry(null)}
className="text-gray-400 hover:text-pixel-gold text-2xl font-bold transition"
>
×
</button>
</div>
{/* Avatar and Class */}
<div className="flex items-center gap-4 sm:gap-6 mb-6">
<Avatar
src={selectedEntry.avatar}
username={selectedEntry.username}
size="lg"
className="flex-shrink-0"
borderClassName="border-2 sm:border-4 border-pixel-gold/50"
/>
<div>
<div className="text-xs text-gray-400 uppercase tracking-widest mb-2">
Rank #{selectedEntry.rank}
</div>
<div className="text-sm text-gray-300 mb-2">
{selectedEntry.email}
</div>
{selectedEntry.characterClass && (
<div className="flex items-center gap-2">
<span className="text-2xl">
{selectedEntry.characterClass === "WARRIOR" && "⚔️"}
{selectedEntry.characterClass === "MAGE" && "🔮"}
{selectedEntry.characterClass === "ROGUE" && "🗡️"}
{selectedEntry.characterClass === "RANGER" && "🏹"}
{selectedEntry.characterClass === "PALADIN" && "🛡️"}
{selectedEntry.characterClass === "ENGINEER" && "⚙️"}
{selectedEntry.characterClass === "MERCHANT" && "💰"}
{selectedEntry.characterClass === "SCHOLAR" && "📚"}
{selectedEntry.characterClass === "BERSERKER" && "🔥"}
{selectedEntry.characterClass === "NECROMANCER" && "💀"}
</span>
<span className="text-lg font-bold text-pixel-gold uppercase tracking-wider">
{selectedEntry.characterClass === "WARRIOR" &&
"Guerrier"}
{selectedEntry.characterClass === "MAGE" && "Mage"}
{selectedEntry.characterClass === "ROGUE" && "Voleur"}
{selectedEntry.characterClass === "RANGER" && "Rôdeur"}
{selectedEntry.characterClass === "PALADIN" &&
"Paladin"}
{selectedEntry.characterClass === "ENGINEER" &&
"Ingénieur"}
{selectedEntry.characterClass === "MERCHANT" &&
"Marchand"}
{selectedEntry.characterClass === "SCHOLAR" && "Érudit"}
{selectedEntry.characterClass === "BERSERKER" &&
"Berserker"}
{selectedEntry.characterClass === "NECROMANCER" &&
"Nécromancien"}
</span>
</div>
)}
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-black/60 border border-pixel-gold/30 rounded p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Score
</div>
<div className="text-2xl font-bold text-pixel-gold">
{formatScore(selectedEntry.score)}
</div>
</div>
<div className="bg-black/60 border border-pixel-gold/30 rounded p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Niveau
</div>
<div className="text-2xl font-bold text-pixel-gold">
Lv.{selectedEntry.level}
</div>
</div>
</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>
</div>
</div>
)}
</section>
);
}

View File

@@ -0,0 +1,120 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import UserManagement from "@/components/admin/UserManagement";
import EventManagement from "@/components/admin/EventManagement";
import FeedbackManagement from "@/components/admin/FeedbackManagement";
import BackgroundPreferences from "@/components/admin/BackgroundPreferences";
import { Button, Card, SectionTitle } from "@/components/ui";
interface SitePreferences {
id: string;
homeBackground: string | null;
eventsBackground: string | null;
leaderboardBackground: string | null;
}
interface AdminPanelProps {
initialPreferences: SitePreferences;
}
type AdminSection = "preferences" | "users" | "events" | "feedbacks";
export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
const [activeSection, setActiveSection] =
useState<AdminSection>("preferences");
return (
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
<SectionTitle variant="gradient" size="md" className="mb-8 text-center">
ADMIN
</SectionTitle>
{/* Navigation Tabs */}
<div className="flex gap-4 mb-8 justify-center flex-wrap">
<Button
onClick={() => setActiveSection("preferences")}
variant={activeSection === "preferences" ? "primary" : "secondary"}
size="md"
className={
activeSection === "preferences" ? "bg-pixel-gold/10" : ""
}
>
Préférences UI
</Button>
<Button
onClick={() => setActiveSection("users")}
variant={activeSection === "users" ? "primary" : "secondary"}
size="md"
className={activeSection === "users" ? "bg-pixel-gold/10" : ""}
>
Utilisateurs
</Button>
<Button
onClick={() => setActiveSection("events")}
variant={activeSection === "events" ? "primary" : "secondary"}
size="md"
className={activeSection === "events" ? "bg-pixel-gold/10" : ""}
>
Événements
</Button>
<Button
onClick={() => setActiveSection("feedbacks")}
variant={activeSection === "feedbacks" ? "primary" : "secondary"}
size="md"
className={activeSection === "feedbacks" ? "bg-pixel-gold/10" : ""}
>
Feedbacks
</Button>
</div>
{activeSection === "preferences" && (
<Card variant="dark" className="p-4 sm:p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<h2 className="text-xl sm:text-2xl font-gaming font-bold text-pixel-gold break-words">
Préférences UI Globales
</h2>
<Link href="/admin/style-guide" target="_blank">
<Button variant="primary" size="sm">
📖 Voir le Style Guide
</Button>
</Link>
</div>
<div className="space-y-4">
<BackgroundPreferences initialPreferences={initialPreferences} />
</div>
</Card>
)}
{activeSection === "users" && (
<Card variant="dark" className="p-6">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Utilisateurs
</h2>
<UserManagement />
</Card>
)}
{activeSection === "events" && (
<Card variant="dark" className="p-6">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Événements
</h2>
<EventManagement />
</Card>
)}
{activeSection === "feedbacks" && (
<Card variant="dark" className="p-6">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Feedbacks
</h2>
<FeedbackManagement />
</Card>
)}
</div>
</section>
);
}

View File

@@ -1,8 +1,9 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import ImageSelector from "@/components/ImageSelector";
import ImageSelector from "@/components/layout/ImageSelector";
import { updateSitePreferences } from "@/actions/admin/preferences";
import { Button, Card } from "@/components/ui";
interface SitePreferences {
id: string;
@@ -142,7 +143,7 @@ export default function BackgroundPreferences({
};
return (
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4">
<Card variant="default" className="p-3 sm:p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
<div className="min-w-0 flex-1">
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
@@ -153,12 +154,14 @@ export default function BackgroundPreferences({
</p>
</div>
{!isEditing && (
<button
<Button
onClick={handleEdit}
className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap flex-shrink-0"
variant="primary"
size="sm"
className="whitespace-nowrap flex-shrink-0"
>
Modifier
</button>
</Button>
)}
</div>
@@ -195,18 +198,12 @@ export default function BackgroundPreferences({
label="Background Leaderboard"
/>
<div className="flex flex-col sm:flex-row gap-2 pt-4">
<button
onClick={handleSave}
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition"
>
<Button onClick={handleSave} variant="success" size="md">
Enregistrer
</button>
<button
onClick={handleCancel}
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
>
</Button>
<Button onClick={handleCancel} variant="secondary" size="md">
Annuler
</button>
</Button>
</div>
</div>
) : (
@@ -381,6 +378,6 @@ export default function BackgroundPreferences({
</div>
</div>
)}
</div>
</Card>
);
}

View File

@@ -0,0 +1,422 @@
"use client";
import { useState, useEffect, useTransition } from "react";
import { calculateEventStatus } from "@/lib/eventStatus";
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
import { Input, Textarea, Button, Card, Badge } from "@/components/ui";
interface Event {
id: string;
date: string;
name: string;
description: string;
type: "ATELIER" | "KATA" | "PRESENTATION" | "LEARNING_HOUR";
status: "UPCOMING" | "LIVE" | "PAST";
room?: string | null;
time?: string | null;
maxPlaces?: number | null;
createdAt: string;
updatedAt: string;
registrationsCount?: number;
}
interface EventFormData {
date: string;
name: string;
description: string;
type: "ATELIER" | "KATA" | "PRESENTATION" | "LEARNING_HOUR";
room?: string;
time?: string;
maxPlaces?: number;
}
const eventTypes: Event["type"][] = [
"ATELIER",
"KATA",
"PRESENTATION",
"LEARNING_HOUR",
];
const getEventTypeLabel = (type: Event["type"]) => {
switch (type) {
case "ATELIER":
return "Atelier";
case "KATA":
return "Kata";
case "PRESENTATION":
return "Présentation";
case "LEARNING_HOUR":
return "Learning Hour";
default:
return type;
}
};
const getStatusLabel = (status: Event["status"]) => {
switch (status) {
case "UPCOMING":
return "À venir";
case "LIVE":
return "En cours";
case "PAST":
return "Passé";
default:
return status;
}
};
export default function EventManagement() {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState<EventFormData>({
date: "",
name: "",
description: "",
type: "ATELIER",
room: "",
time: "",
maxPlaces: undefined,
});
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => {
try {
const response = await fetch("/api/admin/events");
if (response.ok) {
const data = await response.json();
setEvents(data);
}
} catch (error) {
console.error("Error fetching events:", error);
} finally {
setLoading(false);
}
};
const handleCreate = () => {
setIsCreating(true);
setEditingEvent(null);
setFormData({
date: "",
name: "",
description: "",
type: "ATELIER",
room: "",
time: "",
maxPlaces: undefined,
});
};
const handleEdit = (event: Event) => {
setEditingEvent(event);
setIsCreating(false);
setFormData({
date: event.date,
name: event.name,
description: event.description,
type: event.type,
room: event.room || "",
time: event.time || "",
maxPlaces: event.maxPlaces || undefined,
});
};
const [, startTransition] = useTransition();
const handleSave = async () => {
setSaving(true);
startTransition(async () => {
try {
let result;
if (isCreating) {
result = await createEvent(formData);
} else if (editingEvent) {
result = await updateEvent(editingEvent.id, formData);
}
if (result?.success) {
await fetchEvents();
setEditingEvent(null);
setIsCreating(false);
setFormData({
date: "",
name: "",
description: "",
type: "ATELIER",
room: "",
time: "",
maxPlaces: undefined,
});
} else {
alert(result?.error || "Erreur lors de la sauvegarde");
}
} catch (error) {
console.error("Error saving event:", error);
alert("Erreur lors de la sauvegarde");
} finally {
setSaving(false);
}
});
};
const handleDelete = async (eventId: string) => {
if (!confirm("Êtes-vous sûr de vouloir supprimer cet événement ?")) {
return;
}
startTransition(async () => {
try {
const result = await deleteEvent(eventId);
if (result.success) {
await fetchEvents();
} else {
alert(result.error || "Erreur lors de la suppression");
}
} catch (error) {
console.error("Error deleting event:", error);
alert("Erreur lors de la suppression");
}
});
};
const handleCancel = () => {
setEditingEvent(null);
setIsCreating(false);
setFormData({
date: "",
name: "",
description: "",
type: "ATELIER",
room: "",
time: "",
maxPlaces: undefined,
});
};
if (loading) {
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
}
return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-4">
<h3 className="text-lg sm:text-xl font-gaming font-bold text-pixel-gold break-words">
Événements ({events.length})
</h3>
{!isCreating && !editingEvent && (
<Button
onClick={handleCreate}
variant="success"
size="sm"
className="whitespace-nowrap flex-shrink-0"
>
+ Nouvel événement
</Button>
)}
</div>
{(isCreating || editingEvent) && (
<Card variant="default" className="p-3 sm:p-4 mb-4">
<h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words">
{isCreating ? "Créer un événement" : "Modifier l'événement"}
</h4>
<div className="space-y-4">
<Input
type="date"
label="Date"
value={formData.date}
onChange={(e) =>
setFormData({ ...formData, date: e.target.value })
}
className="text-xs sm:text-sm px-3 py-2"
/>
<Input
type="text"
label="Nom"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
placeholder="Nom de l'événement"
className="text-xs sm:text-sm px-3 py-2"
/>
<Textarea
label="Description"
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
placeholder="Description de l'événement"
rows={4}
className="text-xs sm:text-sm px-3 py-2"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
Type
</label>
<select
value={formData.type}
onChange={(e) =>
setFormData({
...formData,
type: e.target.value as Event["type"],
})
}
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
>
{eventTypes.map((type) => (
<option key={type} value={type}>
{getEventTypeLabel(type)}
</option>
))}
</select>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<Input
type="text"
label="Salle"
value={formData.room || ""}
onChange={(e) =>
setFormData({ ...formData, room: e.target.value })
}
placeholder="Ex: Nautilus"
className="text-xs sm:text-sm px-3 py-2"
/>
<Input
type="text"
label="Heure"
value={formData.time || ""}
onChange={(e) =>
setFormData({ ...formData, time: e.target.value })
}
placeholder="Ex: 11h-12h"
className="text-xs sm:text-sm px-3 py-2"
/>
<Input
type="number"
label="Places max"
value={formData.maxPlaces || ""}
onChange={(e) =>
setFormData({
...formData,
maxPlaces: e.target.value
? parseInt(e.target.value)
: undefined,
})
}
placeholder="Ex: 25"
className="text-xs sm:text-sm px-3 py-2"
/>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<Button
onClick={handleSave}
variant="success"
size="md"
disabled={saving}
>
{saving ? "Enregistrement..." : "Enregistrer"}
</Button>
<Button onClick={handleCancel} variant="secondary" size="md">
Annuler
</Button>
</div>
</div>
</Card>
)}
{events.length === 0 ? (
<div className="text-center text-gray-400 py-8">
Aucun événement trouvé
</div>
) : (
<div className="space-y-3">
{events.map((event) => {
const status = calculateEventStatus(event.date);
const statusVariant =
status === "UPCOMING"
? "success"
: status === "LIVE"
? "warning"
: "default";
return (
<Card key={event.id} variant="default" className="p-3 sm:p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
{event.name}
</h4>
<Badge variant="default" size="sm">
{getEventTypeLabel(event.type)}
</Badge>
<Badge variant={statusVariant} size="sm">
{getStatusLabel(status)}
</Badge>
</div>
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
{event.description}
</p>
<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>
{event.room && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
📍 Salle: {event.room}
</p>
)}
{event.time && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
🕐 Heure: {event.time}
</p>
)}
{event.maxPlaces && (
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
👥 Places: {event.maxPlaces}
</p>
)}
<Badge variant="info" size="sm">
{event.registrationsCount || 0} inscrit
{event.registrationsCount !== 1 ? "s" : ""}
</Badge>
</div>
</div>
{!isCreating && !editingEvent && (
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
<Button
onClick={() => handleEdit(event)}
variant="primary"
size="sm"
className="whitespace-nowrap"
>
Modifier
</Button>
<Button
onClick={() => handleDelete(event.id)}
variant="danger"
size="sm"
className="whitespace-nowrap"
>
Supprimer
</Button>
</div>
)}
</div>
</Card>
);
})}
</div>
)}
</div>
);
}

View File

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

View File

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

View File

@@ -0,0 +1,267 @@
"use client";
import { useState, useEffect, useTransition, type FormEvent } from "react";
import { useSession } from "next-auth/react";
import { createFeedback } from "@/actions/events/feedback";
import {
Modal,
StarRating,
Textarea,
Button,
Alert,
SectionTitle,
CloseButton,
} from "@/components/ui";
interface Event {
id: string;
name: string;
date: string;
description: string;
}
interface Feedback {
id: string;
rating: number;
comment: string | null;
}
interface FeedbackModalProps {
eventId: string | null;
eventName?: string;
onClose: () => void;
}
export default function FeedbackModal({
eventId,
eventName: _eventName,
onClose,
}: FeedbackModalProps) {
const { status } = useSession();
const [event, setEvent] = useState<Event | null>(null);
const [existingFeedback, setExistingFeedback] = useState<Feedback | null>(
null
);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [, startTransition] = useTransition();
const [rating, setRating] = useState(0);
const [comment, setComment] = useState("");
// Réinitialiser les états quand eventId change
useEffect(() => {
if (eventId) {
setEvent(null);
setExistingFeedback(null);
setRating(0);
setComment("");
setError("");
setSuccess(false);
setLoading(true);
}
}, [eventId]);
const fetchEventAndFeedback = async () => {
if (!eventId) return;
try {
// Récupérer l'événement
const eventResponse = await fetch(`/api/events/${eventId}`);
if (!eventResponse.ok) {
setError("Événement introuvable");
setLoading(false);
return;
}
const eventData = await eventResponse.json();
setEvent(eventData);
// Récupérer le feedback existant si disponible
const feedbackResponse = await fetch(`/api/feedback/${eventId}`);
if (feedbackResponse.ok) {
const feedbackData = await feedbackResponse.json();
if (feedbackData.feedback) {
setExistingFeedback(feedbackData.feedback);
setRating(feedbackData.feedback.rating);
setComment(feedbackData.feedback.comment || "");
} else {
// Pas de feedback existant, réinitialiser
setRating(0);
setComment("");
}
} else {
// Pas de feedback existant, réinitialiser
setRating(0);
setComment("");
}
} catch {
setError("Erreur lors du chargement des données");
} finally {
setLoading(false);
}
};
useEffect(() => {
if (status === "unauthenticated") {
onClose();
return;
}
if (status === "authenticated" && eventId) {
fetchEventAndFeedback();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status, eventId]);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!eventId) return;
setError("");
setSuccess(false);
if (rating === 0) {
setError("Veuillez sélectionner une note");
return;
}
setSubmitting(true);
startTransition(async () => {
try {
const result = await createFeedback(eventId, {
rating,
comment: comment.trim() || null,
});
if (!result.success) {
setError(result.error || "Erreur lors de l'enregistrement");
setSubmitting(false);
return;
}
setSuccess(true);
if (result.data) {
setExistingFeedback({
id: result.data.id,
rating: result.data.rating,
comment: result.data.comment,
});
}
// Fermer la modale après 1.5 secondes
setTimeout(() => {
onClose();
}, 1500);
} catch {
setError("Erreur lors de l'enregistrement");
} finally {
setSubmitting(false);
}
});
};
const handleClose = () => {
if (!submitting) {
onClose();
}
};
if (!eventId) return null;
return (
<Modal
isOpen={!!eventId}
onClose={handleClose}
size="md"
closeOnOverlayClick={!submitting}
>
<div className="p-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<SectionTitle variant="gradient" size="lg">
FEEDBACK
</SectionTitle>
<CloseButton onClick={handleClose} disabled={submitting} size="lg" />
</div>
{loading ? (
<div className="text-white text-center py-8">Chargement...</div>
) : !event ? (
<Alert variant="error" className="text-center">
Événement introuvable
</Alert>
) : (
<>
<p className="text-gray-400 text-sm text-center mb-2">
{existingFeedback
? "Modifier votre feedback pour"
: "Donnez votre avis sur"}
</p>
<p className="text-pixel-gold text-lg font-semibold text-center mb-8">
{event.name}
</p>
{success && (
<Alert variant="success" className="mb-6">
Feedback enregistré avec succès !
</Alert>
)}
{error && (
<Alert variant="error" className="mb-6">
{error}
</Alert>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Rating */}
<div>
<label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">
Note
</label>
<StarRating
value={rating}
onChange={setRating}
disabled={submitting}
size="lg"
showValue
/>
</div>
{/* Comment */}
<Textarea
id="comment"
label="Commentaire (optionnel)"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={6}
maxLength={1000}
disabled={submitting}
showCharCount
placeholder="Partagez votre expérience, vos suggestions..."
/>
{/* Submit Button */}
<Button
type="submit"
variant="primary"
size="lg"
disabled={submitting || rating === 0}
className="w-full"
>
{submitting
? "Enregistrement..."
: existingFeedback
? "Modifier le feedback"
: "Envoyer le feedback"}
</Button>
</form>
</>
)}
</div>
</Modal>
);
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,261 @@
"use client";
import { useState } from "react";
import {
Avatar,
Modal,
CloseButton,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
interface LeaderboardEntry {
rank: number;
username: string;
email: string;
score: number;
level: number;
avatar?: string | null;
bio?: string | null;
characterClass?: string | null;
}
interface LeaderboardSectionProps {
leaderboard: LeaderboardEntry[];
backgroundImage: string;
}
// Format number with consistent locale to avoid hydration mismatch
const formatScore = (score: number): string => {
return score.toLocaleString("en-US");
};
export default function LeaderboardSection({
leaderboard,
backgroundImage,
}: LeaderboardSectionProps) {
const [selectedEntry, setSelectedEntry] = useState<LeaderboardEntry | null>(
null
);
return (
<BackgroundSection backgroundImage={backgroundImage}>
{/* Title Section */}
<SectionTitle
variant="gradient"
size="lg"
subtitle="Top Players"
className="mb-12 overflow-hidden"
>
LEADERBOARD
</SectionTitle>
{/* Leaderboard Table */}
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
{/* Header */}
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
<div className="col-span-5 sm:col-span-6">Player</div>
<div className="col-span-3 text-right">Score</div>
<div className="col-span-2 text-right">Level</div>
</div>
{/* Entries */}
<div className="divide-y divide-pixel-gold/10 overflow-visible">
{leaderboard.map((entry) => (
<div
key={entry.rank}
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative ${
entry.rank <= 3
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
: "bg-black/40"
}`}
>
{/* Rank */}
<div className="col-span-2 sm:col-span-1 flex items-center justify-center">
<span
className={`inline-flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-bold text-xs sm:text-sm ${
entry.rank === 1
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
: entry.rank === 2
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
: entry.rank === 3
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
: "bg-gray-900 text-gray-400 border border-gray-800"
}`}
>
{entry.rank}
</span>
</div>
{/* Player */}
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
<Avatar
src={entry.avatar}
username={entry.username}
size="sm"
className="flex-shrink-0"
borderClassName="border-pixel-gold/30"
/>
<div
className="flex items-center gap-1 sm:gap-2 cursor-pointer hover:opacity-80 transition min-w-0"
onClick={() => setSelectedEntry(entry)}
>
<span
className={`font-bold text-xs sm:text-sm truncate ${
entry.rank <= 3 ? "text-pixel-gold" : "text-white"
}`}
>
{entry.username}
</span>
{entry.characterClass && (
<span className="text-xs text-gray-400 uppercase tracking-wider">
[{entry.characterClass === "WARRIOR" && "⚔️"}
{entry.characterClass === "MAGE" && "🔮"}
{entry.characterClass === "ROGUE" && "🗡️"}
{entry.characterClass === "RANGER" && "🏹"}
{entry.characterClass === "PALADIN" && "🛡️"}
{entry.characterClass === "ENGINEER" && "⚙️"}
{entry.characterClass === "MERCHANT" && "💰"}
{entry.characterClass === "SCHOLAR" && "📚"}
{entry.characterClass === "BERSERKER" && "🔥"}
{entry.characterClass === "NECROMANCER" && "💀"}]
</span>
)}
{entry.rank <= 3 && (
<span className="text-pixel-gold text-xs"></span>
)}
</div>
</div>
{/* Score */}
<div className="col-span-3 flex items-center justify-end">
<span className="font-mono text-gray-300 text-xs sm:text-sm">
{formatScore(entry.score)}
</span>
</div>
{/* Level */}
<div className="col-span-2 flex items-center justify-end">
<span className="font-bold text-gray-400 text-xs sm:text-sm">
Lv.{entry.level}
</span>
</div>
</div>
))}
</div>
</div>
{/* Footer Info */}
<div className="mt-8 text-center">
<p className="text-gray-500 text-sm">
Compete with players worldwide and climb the ranks!
</p>
<p className="text-gray-600 text-xs mt-2">Rankings update every hour</p>
</div>
{/* 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>
)}
</BackgroundSection>
);
}

View File

@@ -4,7 +4,8 @@ import Link from "next/link";
import { useSession, signOut } from "next-auth/react";
import { useState } from "react";
import { usePathname } from "next/navigation";
import PlayerStats from "./PlayerStats";
import PlayerStats from "@/components/profile/PlayerStats";
import { Button } from "@/components/ui";
interface UserData {
username: string;
@@ -100,12 +101,14 @@ export default function Navigation({
{/* Desktop Auth Buttons */}
<div className="hidden md:flex items-center gap-4">
{isAuthenticated ? (
<button
<Button
onClick={() => signOut()}
className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
variant="ghost"
size="sm"
className="text-xs font-normal"
>
Déconnexion
</button>
</Button>
) : (
<>
<Link
@@ -114,11 +117,10 @@ export default function Navigation({
>
Connexion
</Link>
<Link
href="/register"
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
>
Inscription
<Link href="/register">
<Button variant="primary" size="sm" className="text-xs">
Inscription
</Button>
</Link>
</>
)}
@@ -197,15 +199,17 @@ export default function Navigation({
{/* Mobile Auth Buttons */}
<div className="flex flex-col gap-3 pt-2 border-t border-gray-800/30">
{isAuthenticated ? (
<button
<Button
onClick={() => {
signOut();
setIsMenuOpen(false);
}}
className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest text-left py-2"
variant="ghost"
size="sm"
className="text-xs font-normal text-left py-2"
>
Déconnexion
</button>
</Button>
) : (
<>
<Link
@@ -215,12 +219,14 @@ export default function Navigation({
>
Connexion
</Link>
<Link
href="/register"
onClick={() => setIsMenuOpen(false)}
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition text-center"
>
Inscription
<Link href="/register" onClick={() => setIsMenuOpen(false)}>
<Button
variant="primary"
size="sm"
className="text-xs w-full text-center"
>
Inscription
</Button>
</Link>
</>
)}

View File

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

View File

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

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