Compare commits
3 Commits
db01c25de7
...
97db800c73
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97db800c73 | ||
|
|
880e96d6e4 | ||
|
|
99a475736b |
@@ -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";
|
||||
|
||||
|
||||
495
app/admin/style-guide/page.tsx
Normal file
495
app/admin/style-guide/page.tsx
Normal 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 été 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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">
|
||||
<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
|
||||
</span>
|
||||
</h1>
|
||||
</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
|
||||
<Textarea
|
||||
id="comment"
|
||||
label="Commentaire (optionnel)"
|
||||
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"
|
||||
showCharCount
|
||||
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
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BackgroundSection>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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">
|
||||
<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
|
||||
</span>
|
||||
</h1>
|
||||
</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
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="votre@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
label="Mot de passe"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BackgroundSection>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
<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
|
||||
</span>
|
||||
</h1>
|
||||
</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
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="votre@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Nom d'utilisateur
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
label="Nom d'utilisateur"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="VotrePseudo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
label="Mot de passe"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="confirmPassword"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Confirmer le mot de passe
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
label="Confirmer le mot de passe"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
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"
|
||||
<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'utilisateur
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
id="username-step2"
|
||||
name="username"
|
||||
type="text"
|
||||
label="Nom d'utilisateur"
|
||||
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>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="bio"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Bio (optionnel)
|
||||
</label>
|
||||
<textarea
|
||||
<Textarea
|
||||
id="bio"
|
||||
name="bio"
|
||||
label="Bio (optionnel)"
|
||||
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}
|
||||
showCharCount
|
||||
placeholder="Parlez-nous de vous..."
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
{formData.bio.length}/500 caractères
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BackgroundSection>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
120
components/admin/AdminPanel.tsx
Normal file
120
components/admin/AdminPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
422
components/admin/EventManagement.tsx
Normal file
422
components/admin/EventManagement.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,12 +245,9 @@ 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'utilisateur
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
label="Nom d'utilisateur"
|
||||
value={editingUser.username || ""}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
@@ -261,10 +255,9 @@ export default function UserManagement() {
|
||||
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"
|
||||
className="text-xs sm:text-sm px-2 sm:px-3 py-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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"
|
||||
<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>
|
||||
);
|
||||
})
|
||||
)}
|
||||
@@ -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,42 +591,20 @@ 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}')`,
|
||||
}}
|
||||
>
|
||||
{/* 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-8 py-16">
|
||||
<BackgroundSection backgroundImage={backgroundImage}>
|
||||
{/* 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)",
|
||||
}}
|
||||
<SectionTitle
|
||||
variant="gradient"
|
||||
size="xl"
|
||||
subtitle="Événements à venir et passés"
|
||||
className="mb-16"
|
||||
>
|
||||
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'année
|
||||
</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'année
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Événements à venir */}
|
||||
{upcomingEvents.length > 0 && (
|
||||
@@ -664,40 +657,33 @@ export default function EventsPageSection({
|
||||
Restez informé de nos derniers événements et annonces
|
||||
</p>
|
||||
</div> */}
|
||||
</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)}
|
||||
>
|
||||
<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()}
|
||||
<Modal
|
||||
isOpen={!!selectedEvent}
|
||||
onClose={() => setSelectedEvent(null)}
|
||||
size="lg"
|
||||
>
|
||||
<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">
|
||||
{getStatusBadge(getEventStatus(selectedEvent))}
|
||||
<Badge variant="default" size="md">
|
||||
{getEventTypeLabel(selectedEvent.type)}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
||||
{selectedEvent.name}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
<CloseButton
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition ml-4"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
size="lg"
|
||||
className="ml-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Event Header Color Bar */}
|
||||
@@ -715,7 +701,13 @@ export default function EventsPageSection({
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
: selectedEvent.date.toLocaleDateString("fr-FR", {
|
||||
: selectedEvent.date instanceof Date
|
||||
? selectedEvent.date.toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
: new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
@@ -734,9 +726,7 @@ export default function EventsPageSection({
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Salle
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{selectedEvent.room}
|
||||
</div>
|
||||
<div className="font-semibold">{selectedEvent.room}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -747,9 +737,7 @@ export default function EventsPageSection({
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Heure
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{selectedEvent.time}
|
||||
</div>
|
||||
<div className="font-semibold">{selectedEvent.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -780,50 +768,57 @@ export default function EventsPageSection({
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
{selectedEvent &&
|
||||
getEventStatus(selectedEvent) === "UPCOMING" && (
|
||||
{getEventStatus(selectedEvent) === "UPCOMING" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
{registrations[selectedEvent.id] ? (
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUnregister(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
variant="success"
|
||||
size="lg"
|
||||
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"
|
||||
className="w-full"
|
||||
>
|
||||
{loading[selectedEvent.id]
|
||||
? "Annulation..."
|
||||
: "Se désinscrire"}
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRegister(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
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"
|
||||
className="w-full"
|
||||
>
|
||||
{loading[selectedEvent.id]
|
||||
? "Inscription..."
|
||||
: "S'inscrire maintenant"}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent && getEventStatus(selectedEvent) === "LIVE" && (
|
||||
{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">
|
||||
<Button
|
||||
variant="danger"
|
||||
size="lg"
|
||||
className="w-full animate-pulse"
|
||||
>
|
||||
Rejoindre en direct
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent && getEventStatus(selectedEvent) === "PAST" && (
|
||||
{getEventStatus(selectedEvent) === "PAST" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!session?.user?.id) {
|
||||
@@ -834,15 +829,16 @@ export default function EventsPageSection({
|
||||
setFeedbackEventId(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="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
Donner un feedback
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Feedback Modal */}
|
||||
@@ -850,6 +846,6 @@ export default function EventsPageSection({
|
||||
eventId={feedbackEventId}
|
||||
onClose={() => setFeedbackEventId(null)}
|
||||
/>
|
||||
</section>
|
||||
</BackgroundSection>
|
||||
);
|
||||
}
|
||||
267
components/feedback/FeedbackModal.tsx
Normal file
267
components/feedback/FeedbackModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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" : ""
|
||||
}`}
|
||||
<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>
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Avatar from "./Avatar";
|
||||
import { Avatar } from "@/components/ui";
|
||||
|
||||
interface LeaderboardEntry {
|
||||
rank: number;
|
||||
261
components/leaderboard/LeaderboardSection.tsx
Normal file
261
components/leaderboard/LeaderboardSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
<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"
|
||||
<Link href="/register" onClick={() => setIsMenuOpen(false)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="text-xs w-full text-center"
|
||||
>
|
||||
Inscription
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
@@ -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;
|
||||
@@ -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)",
|
||||
}}
|
||||
>
|
||||
<SectionTitle variant="gradient" size="lg" subtitle="Gérez votre profil" className="mb-12">
|
||||
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>
|
||||
|
||||
{/* 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'utilisateur
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
label="Nom d'utilisateur"
|
||||
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}
|
||||
className="bg-black/40"
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
||||
</div>
|
||||
|
||||
{/* Bio Field */}
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
<Textarea
|
||||
label="Bio"
|
||||
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}
|
||||
showCharCount
|
||||
placeholder="Parlez-nous de vous..."
|
||||
className="bg-black/40"
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
{(bio || "").length}/500 caractères
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Character Class Selection */}
|
||||
<div>
|
||||
@@ -458,37 +401,24 @@ 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}%` }}
|
||||
<ProgressBar
|
||||
value={profile.hp}
|
||||
max={profile.maxHp}
|
||||
variant="hp"
|
||||
showLabel
|
||||
label="HP"
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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}%` }}
|
||||
<ProgressBar
|
||||
value={profile.xp}
|
||||
max={profile.maxXp}
|
||||
variant="xp"
|
||||
showLabel
|
||||
label="XP"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email (read-only) */}
|
||||
<div>
|
||||
@@ -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
|
||||
<Input
|
||||
type="password"
|
||||
label="Mot de passe actuel"
|
||||
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
|
||||
className="bg-black/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
label="Nouveau mot de passe"
|
||||
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}
|
||||
className="bg-black/40"
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
Minimum 6 caractères
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Confirmer le nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
label="Confirmer le nouveau mot de passe"
|
||||
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}
|
||||
className="bg-black/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BackgroundSection>
|
||||
);
|
||||
}
|
||||
32
components/ui/Alert.tsx
Normal file
32
components/ui/Alert.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
43
components/ui/BackgroundSection.tsx
Normal file
43
components/ui/BackgroundSection.tsx
Normal 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
40
components/ui/Badge.tsx
Normal 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
47
components/ui/Button.tsx
Normal 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
30
components/ui/Card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
29
components/ui/CloseButton.tsx
Normal file
29
components/ui/CloseButton.tsx
Normal 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
38
components/ui/Input.tsx
Normal 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
54
components/ui/Modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
73
components/ui/ProgressBar.tsx
Normal file
73
components/ui/ProgressBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
64
components/ui/SectionTitle.tsx
Normal file
64
components/ui/SectionTitle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
66
components/ui/StarRating.tsx
Normal file
66
components/ui/StarRating.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
49
components/ui/Textarea.tsx
Normal file
49
components/ui/Textarea.tsx
Normal 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
14
components/ui/index.ts
Normal 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";
|
||||
|
||||
Reference in New Issue
Block a user