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 { auth } from "@/lib/auth";
|
||||||
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
||||||
import { Role } from "@/prisma/generated/prisma/client";
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
import NavigationWrapper from "@/components/NavigationWrapper";
|
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
|
||||||
import AdminPanel from "@/components/AdminPanel";
|
import AdminPanel from "@/components/admin/AdminPanel";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
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 NavigationWrapper from "@/components/navigation/NavigationWrapper";
|
||||||
import EventsPageSection from "@/components/EventsPageSection";
|
import EventsPageSection from "@/components/events/EventsPageSection";
|
||||||
import { eventService } from "@/services/events/event.service";
|
import { eventService } from "@/services/events/event.service";
|
||||||
import { eventRegistrationService } from "@/services/events/event-registration.service";
|
import { eventRegistrationService } from "@/services/events/event-registration.service";
|
||||||
import { getBackgroundImage } from "@/lib/preferences";
|
import { getBackgroundImage } from "@/lib/preferences";
|
||||||
|
|||||||
@@ -3,8 +3,17 @@
|
|||||||
import { useState, useEffect, useTransition, type FormEvent } from "react";
|
import { useState, useEffect, useTransition, type FormEvent } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter, useParams } from "next/navigation";
|
import { useRouter, useParams } from "next/navigation";
|
||||||
import Navigation from "@/components/Navigation";
|
import Navigation from "@/components/navigation/Navigation";
|
||||||
import { createFeedback } from "@/actions/events/feedback";
|
import { createFeedback } from "@/actions/events/feedback";
|
||||||
|
import {
|
||||||
|
StarRating,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Card,
|
||||||
|
BackgroundSection,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -156,25 +165,17 @@ export default function FeedbackPageClient({
|
|||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black relative">
|
<main className="min-h-screen bg-black relative">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
<BackgroundSection backgroundImage={backgroundImage} className="pt-24">
|
||||||
{/* Background Image */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('${backgroundImage}')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feedback Form */}
|
{/* Feedback Form */}
|
||||||
<div className="relative z-10 w-full max-w-2xl mx-auto px-8">
|
<div className="w-full max-w-2xl mx-auto px-8">
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm">
|
<Card variant="dark" className="p-8">
|
||||||
<h1 className="text-4xl font-gaming font-black mb-2 text-center">
|
<SectionTitle
|
||||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
variant="gradient"
|
||||||
FEEDBACK
|
size="lg"
|
||||||
</span>
|
className="mb-2 text-center"
|
||||||
</h1>
|
>
|
||||||
|
FEEDBACK
|
||||||
|
</SectionTitle>
|
||||||
<p className="text-gray-400 text-sm text-center mb-2">
|
<p className="text-gray-400 text-sm text-center mb-2">
|
||||||
{existingFeedback
|
{existingFeedback
|
||||||
? "Modifier votre feedback pour"
|
? "Modifier votre feedback pour"
|
||||||
@@ -185,15 +186,15 @@ export default function FeedbackPageClient({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{success && (
|
{success && (
|
||||||
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm mb-6">
|
<Alert variant="success" className="mb-6">
|
||||||
Feedback enregistré avec succès ! Redirection...
|
Feedback enregistré avec succès ! Redirection...
|
||||||
</div>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm mb-6">
|
<Alert variant="error" className="mb-6">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
@@ -202,66 +203,44 @@ export default function FeedbackPageClient({
|
|||||||
<label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">
|
<label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">
|
||||||
Note
|
Note
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<StarRating
|
||||||
{[1, 2, 3, 4, 5].map((star) => (
|
value={rating}
|
||||||
<button
|
onChange={setRating}
|
||||||
key={star}
|
size="lg"
|
||||||
type="button"
|
showValue
|
||||||
onClick={() => setRating(star)}
|
/>
|
||||||
className={`text-4xl transition-transform hover:scale-110 ${
|
|
||||||
star <= rating
|
|
||||||
? "text-pixel-gold"
|
|
||||||
: "text-gray-600 hover:text-gray-500"
|
|
||||||
}`}
|
|
||||||
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
|
|
||||||
>
|
|
||||||
★
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-500 text-xs text-center mt-2">
|
|
||||||
{rating > 0 && `${rating}/5`}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Comment */}
|
{/* Comment */}
|
||||||
<div>
|
<Textarea
|
||||||
<label
|
id="comment"
|
||||||
htmlFor="comment"
|
label="Commentaire (optionnel)"
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
value={comment}
|
||||||
>
|
onChange={(e) => setComment(e.target.value)}
|
||||||
Commentaire (optionnel)
|
rows={6}
|
||||||
</label>
|
maxLength={1000}
|
||||||
<textarea
|
showCharCount
|
||||||
id="comment"
|
placeholder="Partagez votre expérience, vos suggestions..."
|
||||||
value={comment}
|
/>
|
||||||
onChange={(e) => setComment(e.target.value)}
|
|
||||||
rows={6}
|
|
||||||
maxLength={1000}
|
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none"
|
|
||||||
placeholder="Partagez votre expérience, vos suggestions..."
|
|
||||||
/>
|
|
||||||
<p className="text-gray-500 text-xs mt-1 text-right">
|
|
||||||
{comment.length}/1000 caractères
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
disabled={submitting || rating === 0}
|
disabled={submitting || rating === 0}
|
||||||
className="w-full px-6 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{submitting
|
{submitting
|
||||||
? "Enregistrement..."
|
? "Enregistrement..."
|
||||||
: existingFeedback
|
: existingFeedback
|
||||||
? "Modifier le feedback"
|
? "Modifier le feedback"
|
||||||
: "Envoyer le feedback"}
|
: "Envoyer le feedback"}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</BackgroundSection>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,17 @@
|
|||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer {
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Orbitron, Rajdhani } from "next/font/google";
|
import { Orbitron, Rajdhani } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import SessionProvider from "@/components/SessionProvider";
|
import SessionProvider from "@/components/layout/SessionProvider";
|
||||||
|
|
||||||
const orbitron = Orbitron({
|
const orbitron = Orbitron({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import NavigationWrapper from "@/components/NavigationWrapper";
|
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
|
||||||
import LeaderboardSection from "@/components/LeaderboardSection";
|
import LeaderboardSection from "@/components/leaderboard/LeaderboardSection";
|
||||||
import { userStatsService } from "@/services/users/user-stats.service";
|
import { userStatsService } from "@/services/users/user-stats.service";
|
||||||
import { getBackgroundImage } from "@/lib/preferences";
|
import { getBackgroundImage } from "@/lib/preferences";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ import { useState, type FormEvent } from "react";
|
|||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Navigation from "@/components/Navigation";
|
import Navigation from "@/components/navigation/Navigation";
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Card,
|
||||||
|
BackgroundSection,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -46,79 +54,53 @@ export default function LoginPage() {
|
|||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black relative">
|
<main className="min-h-screen bg-black relative">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
<BackgroundSection backgroundImage="/got-2.jpg" className="pt-24">
|
||||||
{/* Background Image */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('/got-2.jpg')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Login Form */}
|
{/* Login Form */}
|
||||||
<div className="relative z-10 w-full max-w-md mx-auto px-8">
|
<div className="w-full max-w-md mx-auto px-8">
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm">
|
<Card variant="dark" className="p-8">
|
||||||
<h1 className="text-4xl font-gaming font-black mb-2 text-center">
|
<SectionTitle
|
||||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
variant="gradient"
|
||||||
CONNEXION
|
size="lg"
|
||||||
</span>
|
className="mb-2 text-center"
|
||||||
</h1>
|
>
|
||||||
|
CONNEXION
|
||||||
|
</SectionTitle>
|
||||||
<p className="text-gray-400 text-sm text-center mb-8">
|
<p className="text-gray-400 text-sm text-center mb-8">
|
||||||
Connectez-vous à votre compte
|
Connectez-vous à votre compte
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{error && (
|
{error && <Alert variant="error">{error}</Alert>}
|
||||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
id="email"
|
||||||
htmlFor="email"
|
type="email"
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
label="Email"
|
||||||
>
|
value={email}
|
||||||
Email
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
</label>
|
required
|
||||||
<input
|
placeholder="votre@email.com"
|
||||||
id="email"
|
/>
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
placeholder="votre@email.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
id="password"
|
||||||
htmlFor="password"
|
type="password"
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
label="Mot de passe"
|
||||||
>
|
value={password}
|
||||||
Mot de passe
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
</label>
|
required
|
||||||
<input
|
placeholder="••••••••"
|
||||||
id="password"
|
/>
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full px-6 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{loading ? "Connexion..." : "Se connecter"}
|
{loading ? "Connexion..." : "Se connecter"}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
@@ -132,9 +114,9 @@ export default function LoginPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</BackgroundSection>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import NavigationWrapper from "@/components/NavigationWrapper";
|
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
|
||||||
import HeroSection from "@/components/HeroSection";
|
import HeroSection from "@/components/layout/HeroSection";
|
||||||
import EventsSection from "@/components/EventsSection";
|
import EventsSection from "@/components/events/EventsSection";
|
||||||
import { eventService } from "@/services/events/event.service";
|
import { eventService } from "@/services/events/event.service";
|
||||||
import { getBackgroundImage } from "@/lib/preferences";
|
import { getBackgroundImage } from "@/lib/preferences";
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { redirect } from "next/navigation";
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { userService } from "@/services/users/user.service";
|
import { userService } from "@/services/users/user.service";
|
||||||
import { getBackgroundImage } from "@/lib/preferences";
|
import { getBackgroundImage } from "@/lib/preferences";
|
||||||
import NavigationWrapper from "@/components/NavigationWrapper";
|
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
|
||||||
import ProfileForm from "@/components/ProfileForm";
|
import ProfileForm from "@/components/profile/ProfileForm";
|
||||||
|
|
||||||
export default async function ProfilePage() {
|
export default async function ProfilePage() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|||||||
@@ -3,8 +3,17 @@
|
|||||||
import { useState, useRef, type ChangeEvent, type FormEvent } from "react";
|
import { useState, useRef, type ChangeEvent, type FormEvent } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Navigation from "@/components/Navigation";
|
import Navigation from "@/components/navigation/Navigation";
|
||||||
import Avatar from "@/components/Avatar";
|
import {
|
||||||
|
Avatar,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Card,
|
||||||
|
BackgroundSection,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -162,25 +171,17 @@ export default function RegisterPage() {
|
|||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black relative">
|
<main className="min-h-screen bg-black relative">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
<BackgroundSection backgroundImage="/got-2.jpg" className="pt-24">
|
||||||
{/* Background Image */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('/got-2.jpg')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Register Form */}
|
{/* Register Form */}
|
||||||
<div className="relative z-10 w-full max-w-md mx-auto px-8">
|
<div className="w-full max-w-md mx-auto px-8">
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm">
|
<Card variant="dark" className="p-8">
|
||||||
<h1 className="text-4xl font-gaming font-black mb-2 text-center">
|
<SectionTitle
|
||||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
variant="gradient"
|
||||||
INSCRIPTION
|
size="lg"
|
||||||
</span>
|
className="mb-2 text-center"
|
||||||
</h1>
|
>
|
||||||
|
INSCRIPTION
|
||||||
|
</SectionTitle>
|
||||||
<p className="text-gray-400 text-sm text-center mb-4">
|
<p className="text-gray-400 text-sm text-center mb-4">
|
||||||
{step === 1
|
{step === 1
|
||||||
? "Créez votre compte pour commencer"
|
? "Créez votre compte pour commencer"
|
||||||
@@ -216,103 +217,65 @@ export default function RegisterPage() {
|
|||||||
|
|
||||||
{step === 1 ? (
|
{step === 1 ? (
|
||||||
<form onSubmit={handleStep1Submit} className="space-y-6">
|
<form onSubmit={handleStep1Submit} className="space-y-6">
|
||||||
{error && (
|
{error && <Alert variant="error">{error}</Alert>}
|
||||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
id="email"
|
||||||
htmlFor="email"
|
name="email"
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
type="email"
|
||||||
>
|
label="Email"
|
||||||
Email
|
value={formData.email}
|
||||||
</label>
|
onChange={handleChange}
|
||||||
<input
|
required
|
||||||
id="email"
|
placeholder="votre@email.com"
|
||||||
name="email"
|
/>
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
placeholder="votre@email.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
id="username"
|
||||||
htmlFor="username"
|
name="username"
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
type="text"
|
||||||
>
|
label="Nom d'utilisateur"
|
||||||
Nom d'utilisateur
|
value={formData.username}
|
||||||
</label>
|
onChange={handleChange}
|
||||||
<input
|
required
|
||||||
id="username"
|
placeholder="VotrePseudo"
|
||||||
name="username"
|
/>
|
||||||
type="text"
|
|
||||||
value={formData.username}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
placeholder="VotrePseudo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
id="password"
|
||||||
htmlFor="password"
|
name="password"
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
type="password"
|
||||||
>
|
label="Mot de passe"
|
||||||
Mot de passe
|
value={formData.password}
|
||||||
</label>
|
onChange={handleChange}
|
||||||
<input
|
required
|
||||||
id="password"
|
placeholder="••••••••"
|
||||||
name="password"
|
/>
|
||||||
type="password"
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
id="confirmPassword"
|
||||||
htmlFor="confirmPassword"
|
name="confirmPassword"
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
type="password"
|
||||||
>
|
label="Confirmer le mot de passe"
|
||||||
Confirmer le mot de passe
|
value={formData.confirmPassword}
|
||||||
</label>
|
onChange={handleChange}
|
||||||
<input
|
required
|
||||||
id="confirmPassword"
|
placeholder="••••••••"
|
||||||
name="confirmPassword"
|
/>
|
||||||
type="password"
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full px-6 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{loading ? "Création..." : "Suivant"}
|
{loading ? "Création..." : "Suivant"}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleStep2Submit} className="space-y-6">
|
<form onSubmit={handleStep2Submit} className="space-y-6">
|
||||||
{error && (
|
{error && <Alert variant="error">{error}</Alert>}
|
||||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Avatar Selection */}
|
{/* Avatar Selection */}
|
||||||
<div>
|
<div>
|
||||||
@@ -387,61 +350,47 @@ export default function RegisterPage() {
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
id="avatar-upload"
|
id="avatar-upload"
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor="avatar-upload">
|
||||||
htmlFor="avatar-upload"
|
<Button
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
|
variant="primary"
|
||||||
>
|
size="md"
|
||||||
{uploadingAvatar
|
as="span"
|
||||||
? "Upload en cours..."
|
className="cursor-pointer"
|
||||||
: "Upload un avatar custom"}
|
>
|
||||||
|
{uploadingAvatar
|
||||||
|
? "Upload en cours..."
|
||||||
|
: "Upload un avatar custom"}
|
||||||
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
id="username-step2"
|
||||||
htmlFor="username-step2"
|
name="username"
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
type="text"
|
||||||
>
|
label="Nom d'utilisateur"
|
||||||
Nom d'utilisateur
|
value={formData.username}
|
||||||
</label>
|
onChange={handleChange}
|
||||||
<input
|
required
|
||||||
id="username-step2"
|
placeholder="VotrePseudo"
|
||||||
name="username"
|
minLength={3}
|
||||||
type="text"
|
maxLength={20}
|
||||||
value={formData.username}
|
/>
|
||||||
onChange={handleChange}
|
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
||||||
required
|
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
placeholder="VotrePseudo"
|
|
||||||
minLength={3}
|
|
||||||
maxLength={20}
|
|
||||||
/>
|
|
||||||
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Textarea
|
||||||
<label
|
id="bio"
|
||||||
htmlFor="bio"
|
name="bio"
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
label="Bio (optionnel)"
|
||||||
>
|
value={formData.bio}
|
||||||
Bio (optionnel)
|
onChange={handleChange}
|
||||||
</label>
|
rows={4}
|
||||||
<textarea
|
maxLength={500}
|
||||||
id="bio"
|
showCharCount
|
||||||
name="bio"
|
placeholder="Parlez-nous de vous..."
|
||||||
value={formData.bio}
|
/>
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none"
|
|
||||||
rows={4}
|
|
||||||
maxLength={500}
|
|
||||||
placeholder="Parlez-nous de vous..."
|
|
||||||
/>
|
|
||||||
<p className="text-gray-500 text-xs mt-1">
|
|
||||||
{formData.bio.length}/500 caractères
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-gray-300 mb-3 uppercase tracking-wider">
|
<label className="block text-sm font-semibold text-gray-300 mb-3 uppercase tracking-wider">
|
||||||
@@ -500,20 +449,24 @@ export default function RegisterPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
onClick={() => setStep(1)}
|
onClick={() => setStep(1)}
|
||||||
className="flex-1 px-6 py-3 border border-gray-600/50 bg-black/40 text-gray-400 uppercase text-sm tracking-widest rounded hover:bg-gray-900/40 hover:border-gray-500 transition"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
Retour
|
Retour
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex-1 px-6 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{loading ? "Finalisation..." : "Terminer"}
|
{loading ? "Finalisation..." : "Terminer"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
@@ -529,9 +482,9 @@ export default function RegisterPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</BackgroundSection>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import ImageSelector from "@/components/ImageSelector";
|
import ImageSelector from "@/components/layout/ImageSelector";
|
||||||
import { updateSitePreferences } from "@/actions/admin/preferences";
|
import { updateSitePreferences } from "@/actions/admin/preferences";
|
||||||
|
import { Button, Card } from "@/components/ui";
|
||||||
|
|
||||||
interface SitePreferences {
|
interface SitePreferences {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -142,7 +143,7 @@ export default function BackgroundPreferences({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4">
|
<Card variant="default" className="p-3 sm:p-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||||
@@ -153,12 +154,14 @@ export default function BackgroundPreferences({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<button
|
<Button
|
||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap flex-shrink-0"
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap flex-shrink-0"
|
||||||
>
|
>
|
||||||
Modifier
|
Modifier
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -195,18 +198,12 @@ export default function BackgroundPreferences({
|
|||||||
label="Background Leaderboard"
|
label="Background Leaderboard"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
||||||
<button
|
<Button onClick={handleSave} variant="success" size="md">
|
||||||
onClick={handleSave}
|
|
||||||
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition"
|
|
||||||
>
|
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button onClick={handleCancel} variant="secondary" size="md">
|
||||||
onClick={handleCancel}
|
|
||||||
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
|
|
||||||
>
|
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -381,6 +378,6 @@ export default function BackgroundPreferences({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useTransition } from "react";
|
import { useState, useEffect, useTransition } from "react";
|
||||||
import Avatar from "./Avatar";
|
import { Avatar, Input, Button, Card } from "@/components/ui";
|
||||||
import { updateUser, deleteUser } from "@/actions/admin/users";
|
import { updateUser, deleteUser } from "@/actions/admin/users";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -184,10 +184,7 @@ export default function UserManagement() {
|
|||||||
: user.username;
|
: user.username;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Card key={user.id} variant="default" className="p-3 sm:p-4">
|
||||||
key={user.id}
|
|
||||||
className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-2">
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-2">
|
||||||
<div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0">
|
<div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
@@ -248,23 +245,19 @@ export default function UserManagement() {
|
|||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Username Section */}
|
{/* Username Section */}
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-xs sm:text-sm text-gray-300 mb-2">
|
type="text"
|
||||||
Nom d'utilisateur
|
label="Nom d'utilisateur"
|
||||||
</label>
|
value={editingUser.username || ""}
|
||||||
<input
|
onChange={(e) =>
|
||||||
type="text"
|
setEditingUser({
|
||||||
value={editingUser.username || ""}
|
...editingUser,
|
||||||
onChange={(e) =>
|
username: e.target.value,
|
||||||
setEditingUser({
|
})
|
||||||
...editingUser,
|
}
|
||||||
username: e.target.value,
|
placeholder="Nom d'utilisateur"
|
||||||
})
|
className="text-xs sm:text-sm px-2 sm:px-3 py-1"
|
||||||
}
|
/>
|
||||||
className="w-full px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
|
||||||
placeholder="Nom d'utilisateur"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Avatar Section */}
|
{/* Avatar Section */}
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
@@ -372,13 +365,17 @@ export default function UserManagement() {
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
id={`avatar-upload-${user.id}`}
|
id={`avatar-upload-${user.id}`}
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor={`avatar-upload-${user.id}`}>
|
||||||
htmlFor={`avatar-upload-${user.id}`}
|
<Button
|
||||||
className="px-3 sm:px-4 py-1.5 border border-pixel-gold/50 bg-black/40 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
|
variant="primary"
|
||||||
>
|
size="sm"
|
||||||
{uploadingAvatar === user.id
|
as="span"
|
||||||
? "Upload en cours..."
|
className="cursor-pointer"
|
||||||
: "Upload un avatar custom"}
|
>
|
||||||
|
{uploadingAvatar === user.id
|
||||||
|
? "Upload en cours..."
|
||||||
|
: "Upload un avatar custom"}
|
||||||
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -690,19 +687,21 @@ export default function UserManagement() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-2 pt-2">
|
<div className="flex flex-col sm:flex-row gap-2 pt-2">
|
||||||
<button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
variant="success"
|
||||||
|
size="md"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
{saving ? "Enregistrement..." : "Enregistrer"}
|
{saving ? "Enregistrement..." : "Enregistrer"}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
|
variant="secondary"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -747,7 +746,7 @@ export default function UserManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
@@ -4,11 +4,20 @@ import { useState, useEffect, useMemo, useRef, useTransition } from "react";
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { calculateEventStatus } from "@/lib/eventStatus";
|
import { calculateEventStatus } from "@/lib/eventStatus";
|
||||||
import FeedbackModal from "@/components/FeedbackModal";
|
import FeedbackModal from "@/components/feedback/FeedbackModal";
|
||||||
import {
|
import {
|
||||||
registerForEvent,
|
registerForEvent,
|
||||||
unregisterFromEvent,
|
unregisterFromEvent,
|
||||||
} from "@/actions/events/register";
|
} from "@/actions/events/register";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
CloseButton,
|
||||||
|
Card,
|
||||||
|
BackgroundSection,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -61,21 +70,21 @@ const getStatusBadge = (status: "UPCOMING" | "LIVE" | "PAST") => {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case "UPCOMING":
|
case "UPCOMING":
|
||||||
return (
|
return (
|
||||||
<span className="px-3 py-1 bg-green-900/50 border border-green-500/50 text-green-400 text-xs uppercase tracking-widest rounded">
|
<Badge variant="success" size="md">
|
||||||
À venir
|
À venir
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "LIVE":
|
case "LIVE":
|
||||||
return (
|
return (
|
||||||
<span className="px-3 py-1 bg-red-900/50 border border-red-500/50 text-red-400 text-xs uppercase tracking-widest rounded animate-pulse">
|
<Badge variant="danger" size="md" className="animate-pulse">
|
||||||
En direct
|
En direct
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "PAST":
|
case "PAST":
|
||||||
return (
|
return (
|
||||||
<span className="px-3 py-1 bg-gray-800/50 border border-gray-600/50 text-gray-400 text-xs uppercase tracking-widest rounded">
|
<Badge variant="default" size="md">
|
||||||
Passé
|
Passé
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -401,10 +410,10 @@ export default function EventsPageSection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderEventCard = (event: Event) => (
|
const renderEventCard = (event: Event) => (
|
||||||
<div
|
<Card
|
||||||
key={event.id}
|
key={event.id}
|
||||||
onClick={() => setSelectedEvent(event)}
|
onClick={() => setSelectedEvent(event)}
|
||||||
className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm hover:border-pixel-gold/50 transition group cursor-pointer"
|
className="overflow-hidden hover:border-pixel-gold/50 transition group cursor-pointer"
|
||||||
>
|
>
|
||||||
{/* Event Header */}
|
{/* Event Header */}
|
||||||
<div
|
<div
|
||||||
@@ -482,37 +491,41 @@ export default function EventsPageSection({
|
|||||||
{getEventStatus(event) === "UPCOMING" && (
|
{getEventStatus(event) === "UPCOMING" && (
|
||||||
<>
|
<>
|
||||||
{registrations[event.id] ? (
|
{registrations[event.id] ? (
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleUnregister(event.id);
|
handleUnregister(event.id);
|
||||||
}}
|
}}
|
||||||
|
variant="success"
|
||||||
|
size="md"
|
||||||
disabled={loading[event.id]}
|
disabled={loading[event.id]}
|
||||||
className="w-full px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{loading[event.id] ? "Annulation..." : "Inscrit ✓"}
|
{loading[event.id] ? "Annulation..." : "Inscrit ✓"}
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleRegister(event.id);
|
handleRegister(event.id);
|
||||||
}}
|
}}
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
disabled={loading[event.id]}
|
disabled={loading[event.id]}
|
||||||
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
|
{loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{getEventStatus(event) === "LIVE" && (
|
{getEventStatus(event) === "LIVE" && (
|
||||||
<button className="w-full px-4 py-2 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-xs tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
|
<Button variant="danger" size="md" className="w-full animate-pulse">
|
||||||
Rejoindre en direct
|
Rejoindre en direct
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{getEventStatus(event) === "PAST" && (
|
{getEventStatus(event) === "PAST" && (
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@@ -521,13 +534,15 @@ export default function EventsPageSection({
|
|||||||
}
|
}
|
||||||
setFeedbackEventId(event.id);
|
setFeedbackEventId(event.id);
|
||||||
}}
|
}}
|
||||||
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
Donner un feedback
|
Donner un feedback
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
@@ -576,273 +591,254 @@ export default function EventsPageSection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
<BackgroundSection backgroundImage={backgroundImage}>
|
||||||
{/* Background Image */}
|
{/* Title Section */}
|
||||||
<div
|
<SectionTitle
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
variant="gradient"
|
||||||
style={{
|
size="xl"
|
||||||
backgroundImage: `url('${backgroundImage}')`,
|
subtitle="Événements à venir et passés"
|
||||||
}}
|
className="mb-16"
|
||||||
>
|
>
|
||||||
{/* Dark overlay for readability */}
|
EVENTS
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
</SectionTitle>
|
||||||
</div>
|
<p className="text-gray-400 text-sm max-w-2xl mx-auto text-center mb-16">
|
||||||
|
Rejoignez-nous pour des événements tech passionnants, des compétitions
|
||||||
|
et des célébrations tout au long de l'année
|
||||||
|
</p>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Événements à venir */}
|
||||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
{upcomingEvents.length > 0 && (
|
||||||
{/* Title Section */}
|
|
||||||
<div className="text-center mb-16">
|
|
||||||
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
|
|
||||||
<span
|
|
||||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
|
||||||
style={{
|
|
||||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
EVENTS
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 mb-6 tracking-wide">
|
|
||||||
<span>✦</span>
|
|
||||||
<span>Événements à venir et passés</span>
|
|
||||||
<span>✦</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-400 text-sm max-w-2xl mx-auto">
|
|
||||||
Rejoignez-nous pour des événements tech passionnants, des
|
|
||||||
compétitions et des célébrations tout au long de l'année
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Événements à venir */}
|
|
||||||
{upcomingEvents.length > 0 && (
|
|
||||||
<div className="mb-16">
|
|
||||||
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
|
|
||||||
<span className="bg-gradient-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
|
|
||||||
Événements à venir
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{upcomingEvents.map(renderEventCard)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Calendrier */}
|
|
||||||
<div className="mb-16">
|
<div className="mb-16">
|
||||||
<h2 className="text-2xl font-bold text-white mb-6 text-center uppercase tracking-widest">
|
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
|
||||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
<span className="bg-gradient-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
|
||||||
Calendrier
|
Événements à venir
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
{renderCalendar()}
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{upcomingEvents.map(renderEventCard)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Événements passés */}
|
{/* Calendrier */}
|
||||||
{pastEvents.length > 0 && (
|
<div className="mb-16">
|
||||||
<div className="mb-16">
|
<h2 className="text-2xl font-bold text-white mb-6 text-center uppercase tracking-widest">
|
||||||
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
|
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
||||||
<span className="bg-gradient-to-r from-gray-400 to-gray-600 bg-clip-text text-transparent">
|
Calendrier
|
||||||
Événements passés
|
</span>
|
||||||
</span>
|
</h2>
|
||||||
</h2>
|
{renderCalendar()}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{pastEvents.map(renderEventCard)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-red-400 text-sm">{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Footer Info */}
|
|
||||||
{/* <div className="mt-12 text-center">
|
|
||||||
<p className="text-gray-500 text-sm">
|
|
||||||
Restez informé de nos derniers événements et annonces
|
|
||||||
</p>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Événements passés */}
|
||||||
|
{pastEvents.length > 0 && (
|
||||||
|
<div className="mb-16">
|
||||||
|
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
|
||||||
|
<span className="bg-gradient-to-r from-gray-400 to-gray-600 bg-clip-text text-transparent">
|
||||||
|
Événements passés
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{pastEvents.map(renderEventCard)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-red-400 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer Info */}
|
||||||
|
{/* <div className="mt-12 text-center">
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
Restez informé de nos derniers événements et annonces
|
||||||
|
</p>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
{/* Event Modal */}
|
{/* Event Modal */}
|
||||||
{selectedEvent && (
|
{selectedEvent && (
|
||||||
<div
|
<Modal
|
||||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
isOpen={!!selectedEvent}
|
||||||
onClick={() => setSelectedEvent(null)}
|
onClose={() => setSelectedEvent(null)}
|
||||||
|
size="lg"
|
||||||
>
|
>
|
||||||
<div
|
<div className="p-8">
|
||||||
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
|
{/* Header */}
|
||||||
onClick={(e) => e.stopPropagation()}
|
<div className="flex items-center justify-between mb-6">
|
||||||
>
|
<div className="flex-1">
|
||||||
<div className="p-8">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
{/* Header */}
|
{getStatusBadge(getEventStatus(selectedEvent))}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<Badge variant="default" size="md">
|
||||||
<div className="flex-1">
|
{getEventTypeLabel(selectedEvent.type)}
|
||||||
<div className="flex items-center gap-3 mb-2">
|
</Badge>
|
||||||
{getStatusBadge(
|
|
||||||
selectedEvent ? getEventStatus(selectedEvent) : "UPCOMING"
|
|
||||||
)}
|
|
||||||
<span className="px-3 py-1 bg-pixel-gold/20 border border-pixel-gold/50 text-pixel-gold text-xs uppercase rounded">
|
|
||||||
{getEventTypeLabel(selectedEvent.type)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
|
||||||
{selectedEvent.name}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
||||||
onClick={() => setSelectedEvent(null)}
|
{selectedEvent.name}
|
||||||
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition ml-4"
|
</h2>
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<CloseButton
|
||||||
|
onClick={() => setSelectedEvent(null)}
|
||||||
|
size="lg"
|
||||||
|
className="ml-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Event Header Color Bar */}
|
{/* Event Header Color Bar */}
|
||||||
<div
|
<div
|
||||||
className={`h-1 bg-gradient-to-r ${getEventTypeColor(
|
className={`h-1 bg-gradient-to-r ${getEventTypeColor(
|
||||||
selectedEvent.type
|
selectedEvent.type
|
||||||
)} mb-6 rounded`}
|
)} mb-6 rounded`}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
{/* Date */}
|
{/* Date */}
|
||||||
<div className="text-white text-lg font-bold uppercase tracking-widest mb-4">
|
<div className="text-white text-lg font-bold uppercase tracking-widest mb-4">
|
||||||
{typeof selectedEvent.date === "string"
|
{typeof selectedEvent.date === "string"
|
||||||
? new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
|
? new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
: selectedEvent.date instanceof Date
|
||||||
|
? selectedEvent.date.toLocaleDateString("fr-FR", {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})
|
})
|
||||||
: selectedEvent.date.toLocaleDateString("fr-FR", {
|
: new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Event Details */}
|
{/* Event Details */}
|
||||||
{(selectedEvent.room ||
|
{(selectedEvent.room ||
|
||||||
selectedEvent.time ||
|
selectedEvent.time ||
|
||||||
selectedEvent.maxPlaces) && (
|
selectedEvent.maxPlaces) && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
{selectedEvent.room && (
|
{selectedEvent.room && (
|
||||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||||
<span className="text-pixel-gold text-xl">📍</span>
|
<span className="text-pixel-gold text-xl">📍</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||||
Salle
|
Salle
|
||||||
</div>
|
|
||||||
<div className="font-semibold">
|
|
||||||
{selectedEvent.room}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="font-semibold">{selectedEvent.room}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{selectedEvent.time && (
|
|
||||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
|
||||||
<span className="text-pixel-gold text-xl">🕐</span>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
|
||||||
Heure
|
|
||||||
</div>
|
|
||||||
<div className="font-semibold">
|
|
||||||
{selectedEvent.time}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedEvent.maxPlaces && (
|
|
||||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
|
||||||
<span className="text-pixel-gold text-xl">👥</span>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
|
||||||
Places
|
|
||||||
</div>
|
|
||||||
<div className="font-semibold">
|
|
||||||
{selectedEvent.maxPlaces}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Full Description */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-3">
|
|
||||||
Description
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-line">
|
|
||||||
{selectedEvent.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Button */}
|
|
||||||
{selectedEvent &&
|
|
||||||
getEventStatus(selectedEvent) === "UPCOMING" && (
|
|
||||||
<div className="pt-4 border-t border-pixel-gold/20">
|
|
||||||
{registrations[selectedEvent.id] ? (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleUnregister(selectedEvent.id);
|
|
||||||
setSelectedEvent(null);
|
|
||||||
}}
|
|
||||||
disabled={loading[selectedEvent.id]}
|
|
||||||
className="w-full px-4 py-3 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-sm tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{loading[selectedEvent.id]
|
|
||||||
? "Annulation..."
|
|
||||||
: "Se désinscrire"}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleRegister(selectedEvent.id);
|
|
||||||
setSelectedEvent(null);
|
|
||||||
}}
|
|
||||||
disabled={loading[selectedEvent.id]}
|
|
||||||
className="w-full px-4 py-3 border border-pixel-gold/50 bg-pixel-gold/10 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/20 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{loading[selectedEvent.id]
|
|
||||||
? "Inscription..."
|
|
||||||
: "S'inscrire maintenant"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedEvent && getEventStatus(selectedEvent) === "LIVE" && (
|
{selectedEvent.time && (
|
||||||
<div className="pt-4 border-t border-pixel-gold/20">
|
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||||
<button className="w-full px-4 py-3 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-sm tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
|
<span className="text-pixel-gold text-xl">🕐</span>
|
||||||
Rejoindre en direct
|
<div>
|
||||||
</button>
|
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||||
</div>
|
Heure
|
||||||
)}
|
</div>
|
||||||
{selectedEvent && getEventStatus(selectedEvent) === "PAST" && (
|
<div className="font-semibold">{selectedEvent.time}</div>
|
||||||
<div className="pt-4 border-t border-pixel-gold/20">
|
</div>
|
||||||
<button
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedEvent.maxPlaces && (
|
||||||
|
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||||
|
<span className="text-pixel-gold text-xl">👥</span>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||||
|
Places
|
||||||
|
</div>
|
||||||
|
<div className="font-semibold">
|
||||||
|
{selectedEvent.maxPlaces}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Full Description */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-3">
|
||||||
|
Description
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-line">
|
||||||
|
{selectedEvent.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
{getEventStatus(selectedEvent) === "UPCOMING" && (
|
||||||
|
<div className="pt-4 border-t border-pixel-gold/20">
|
||||||
|
{registrations[selectedEvent.id] ? (
|
||||||
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!session?.user?.id) {
|
handleUnregister(selectedEvent.id);
|
||||||
router.push("/login");
|
|
||||||
setSelectedEvent(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setFeedbackEventId(selectedEvent.id);
|
|
||||||
setSelectedEvent(null);
|
setSelectedEvent(null);
|
||||||
}}
|
}}
|
||||||
className="w-full px-4 py-3 border border-pixel-gold/50 bg-black/40 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
variant="success"
|
||||||
|
size="lg"
|
||||||
|
disabled={loading[selectedEvent.id]}
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
Donner un feedback
|
{loading[selectedEvent.id]
|
||||||
</button>
|
? "Annulation..."
|
||||||
</div>
|
: "Se désinscrire"}
|
||||||
)}
|
</Button>
|
||||||
</div>
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRegister(selectedEvent.id);
|
||||||
|
setSelectedEvent(null);
|
||||||
|
}}
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
disabled={loading[selectedEvent.id]}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading[selectedEvent.id]
|
||||||
|
? "Inscription..."
|
||||||
|
: "S'inscrire maintenant"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{getEventStatus(selectedEvent) === "LIVE" && (
|
||||||
|
<div className="pt-4 border-t border-pixel-gold/20">
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="lg"
|
||||||
|
className="w-full animate-pulse"
|
||||||
|
>
|
||||||
|
Rejoindre en direct
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{getEventStatus(selectedEvent) === "PAST" && (
|
||||||
|
<div className="pt-4 border-t border-pixel-gold/20">
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
router.push("/login");
|
||||||
|
setSelectedEvent(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFeedbackEventId(selectedEvent.id);
|
||||||
|
setSelectedEvent(null);
|
||||||
|
}}
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Donner un feedback
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Feedback Modal */}
|
{/* Feedback Modal */}
|
||||||
@@ -850,6 +846,6 @@ export default function EventsPageSection({
|
|||||||
eventId={feedbackEventId}
|
eventId={feedbackEventId}
|
||||||
onClose={() => setFeedbackEventId(null)}
|
onClose={() => setFeedbackEventId(null)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</BackgroundSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Button, BackgroundSection } from "@/components/ui";
|
||||||
|
|
||||||
interface HeroSectionProps {
|
interface HeroSectionProps {
|
||||||
backgroundImage: string;
|
backgroundImage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HeroSection({ backgroundImage }: HeroSectionProps) {
|
export default function HeroSection({ backgroundImage }: HeroSectionProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
<BackgroundSection backgroundImage={backgroundImage} className="pt-24">
|
||||||
{/* Background Image */}
|
<div className="text-center flex flex-col items-center">
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('${backgroundImage}')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Dark overlay for readability */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80 z-[1]"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hero Content */}
|
|
||||||
<div className="relative z-10 w-full max-w-5xl xl:max-w-6xl mx-auto px-4 sm:px-8 py-16 text-center flex flex-col items-center">
|
|
||||||
{/* Game Title */}
|
{/* Game Title */}
|
||||||
<div className="w-full flex justify-center mb-4 overflow-hidden">
|
<div className="w-full flex justify-center mb-4 overflow-hidden">
|
||||||
<h1 className="text-4xl sm:text-5xl md:text-8xl lg:text-9xl xl:text-9xl font-gaming font-black tracking-tight relative break-words">
|
<h1 className="text-4xl sm:text-5xl md:text-8xl lg:text-9xl xl:text-9xl font-gaming font-black tracking-tight relative break-words">
|
||||||
@@ -62,18 +50,22 @@ export default function HeroSection({ backgroundImage }: HeroSectionProps) {
|
|||||||
{/* Call-to-Action Buttons */}
|
{/* Call-to-Action Buttons */}
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
|
||||||
<Link href="/events">
|
<Link href="/events">
|
||||||
<button className="px-8 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition">
|
<Button variant="primary" size="lg">
|
||||||
<span>See events</span>
|
See events
|
||||||
</button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/leaderboard">
|
<Link href="/leaderboard">
|
||||||
<button className="px-8 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition flex items-center gap-2">
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
<span>⏵</span>
|
<span>⏵</span>
|
||||||
<span>See leaderboard</span>
|
<span>See leaderboard</span>
|
||||||
</button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</BackgroundSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef, type ChangeEvent } from "react";
|
import { useState, useEffect, useRef, type ChangeEvent } from "react";
|
||||||
|
import { Input, Button, Card } from "@/components/ui";
|
||||||
|
|
||||||
interface ImageSelectorProps {
|
interface ImageSelectorProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -119,20 +120,22 @@ export default function ImageSelector({
|
|||||||
<div className="flex-1 space-y-3 min-w-0">
|
<div className="flex-1 space-y-3 min-w-0">
|
||||||
{/* Input URL */}
|
{/* Input URL */}
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={urlInput}
|
value={urlInput}
|
||||||
onChange={(e) => setUrlInput(e.target.value)}
|
onChange={(e) => setUrlInput(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()}
|
onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()}
|
||||||
placeholder="https://example.com/image.jpg ou /image.jpg"
|
placeholder="https://example.com/image.jpg ou /image.jpg"
|
||||||
className="flex-1 px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm min-w-0"
|
className="flex-1 text-xs sm:text-sm px-3 py-2 min-w-0"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={handleUrlSubmit}
|
onClick={handleUrlSubmit}
|
||||||
className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap flex-shrink-0"
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap flex-shrink-0"
|
||||||
>
|
>
|
||||||
URL
|
URL
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload depuis le disque */}
|
{/* Upload depuis le disque */}
|
||||||
@@ -145,20 +148,25 @@ export default function ImageSelector({
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
id={`file-${label}`}
|
id={`file-${label}`}
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor={`file-${label}`}>
|
||||||
htmlFor={`file-${label}`}
|
<Button
|
||||||
className={`flex-1 px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition text-center cursor-pointer ${
|
variant="primary"
|
||||||
uploading ? "opacity-50 cursor-not-allowed" : ""
|
size="sm"
|
||||||
}`}
|
as="span"
|
||||||
>
|
disabled={uploading}
|
||||||
{uploading ? "Upload..." : "Upload depuis le disque"}
|
className="flex-1 text-center cursor-pointer"
|
||||||
|
>
|
||||||
|
{uploading ? "Upload..." : "Upload depuis le disque"}
|
||||||
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setShowGallery(!showGallery)}
|
onClick={() => setShowGallery(!showGallery)}
|
||||||
className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap"
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{showGallery ? "Masquer" : "Galerie"}
|
{showGallery ? "Masquer" : "Galerie"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chemin de l'image */}
|
{/* Chemin de l'image */}
|
||||||
@@ -170,7 +178,7 @@ export default function ImageSelector({
|
|||||||
|
|
||||||
{/* Galerie d'images */}
|
{/* Galerie d'images */}
|
||||||
{showGallery && (
|
{showGallery && (
|
||||||
<div className="mt-4 p-3 sm:p-4 bg-black/40 border border-pixel-gold/20 rounded">
|
<Card variant="dark" className="mt-4 p-3 sm:p-4">
|
||||||
<h4 className="text-xs sm:text-sm text-gray-300 mb-3">
|
<h4 className="text-xs sm:text-sm text-gray-300 mb-3">
|
||||||
Images disponibles
|
Images disponibles
|
||||||
</h4>
|
</h4>
|
||||||
@@ -210,7 +218,7 @@ export default function ImageSelector({
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Avatar from "./Avatar";
|
import { Avatar } from "@/components/ui";
|
||||||
|
|
||||||
interface LeaderboardEntry {
|
interface LeaderboardEntry {
|
||||||
rank: number;
|
rank: number;
|
||||||
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 { useSession, signOut } from "next-auth/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import PlayerStats from "./PlayerStats";
|
import PlayerStats from "@/components/profile/PlayerStats";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -100,12 +101,14 @@ export default function Navigation({
|
|||||||
{/* Desktop Auth Buttons */}
|
{/* Desktop Auth Buttons */}
|
||||||
<div className="hidden md:flex items-center gap-4">
|
<div className="hidden md:flex items-center gap-4">
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<button
|
<Button
|
||||||
onClick={() => signOut()}
|
onClick={() => signOut()}
|
||||||
className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs font-normal"
|
||||||
>
|
>
|
||||||
Déconnexion
|
Déconnexion
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
@@ -114,11 +117,10 @@ export default function Navigation({
|
|||||||
>
|
>
|
||||||
Connexion
|
Connexion
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link href="/register">
|
||||||
href="/register"
|
<Button variant="primary" size="sm" className="text-xs">
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
Inscription
|
||||||
>
|
</Button>
|
||||||
Inscription
|
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -197,15 +199,17 @@ export default function Navigation({
|
|||||||
{/* Mobile Auth Buttons */}
|
{/* Mobile Auth Buttons */}
|
||||||
<div className="flex flex-col gap-3 pt-2 border-t border-gray-800/30">
|
<div className="flex flex-col gap-3 pt-2 border-t border-gray-800/30">
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
signOut();
|
signOut();
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest text-left py-2"
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs font-normal text-left py-2"
|
||||||
>
|
>
|
||||||
Déconnexion
|
Déconnexion
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
@@ -215,12 +219,14 @@ export default function Navigation({
|
|||||||
>
|
>
|
||||||
Connexion
|
Connexion
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link href="/register" onClick={() => setIsMenuOpen(false)}>
|
||||||
href="/register"
|
<Button
|
||||||
onClick={() => setIsMenuOpen(false)}
|
variant="primary"
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition text-center"
|
size="sm"
|
||||||
>
|
className="text-xs w-full text-center"
|
||||||
Inscription
|
>
|
||||||
|
Inscription
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Avatar from "./Avatar";
|
import { Avatar } from "@/components/ui";
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useTransition, type ChangeEvent } from "react";
|
import { useState, useRef, useTransition, type ChangeEvent } from "react";
|
||||||
import Avatar from "./Avatar";
|
import { Avatar, Input, Textarea, Button, Alert, Card, BackgroundSection, SectionTitle, ProgressBar } from "@/components/ui";
|
||||||
import { updateProfile } from "@/actions/profile/update-profile";
|
import { updateProfile } from "@/actions/profile/update-profile";
|
||||||
import { updatePassword } from "@/actions/profile/update-password";
|
import { updatePassword } from "@/actions/profile/update-password";
|
||||||
|
|
||||||
@@ -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 (
|
return (
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
<BackgroundSection backgroundImage={backgroundImage}>
|
||||||
{/* Background Image */}
|
<div className="w-full max-w-4xl mx-auto px-8">
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('${backgroundImage}')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Dark overlay for readability */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="relative z-10 w-full max-w-4xl mx-auto px-8 py-16">
|
|
||||||
{/* Title Section */}
|
{/* Title Section */}
|
||||||
<div className="text-center mb-12">
|
<SectionTitle variant="gradient" size="lg" subtitle="Gérez votre profil" className="mb-12">
|
||||||
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
|
PROFIL
|
||||||
<span
|
</SectionTitle>
|
||||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
|
||||||
style={{
|
|
||||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
PROFIL
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
|
|
||||||
<span>✦</span>
|
|
||||||
<span>Gérez votre profil</span>
|
|
||||||
<span>✦</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm">
|
<Card variant="default" className="overflow-hidden">
|
||||||
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
{error && (
|
{error && <Alert variant="error">{error}</Alert>}
|
||||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
|
{success && <Alert variant="success">{success}</Alert>}
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm">
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Avatar Section */}
|
{/* Avatar Section */}
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
@@ -281,51 +237,38 @@ export default function ProfileForm({
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
id="avatar-upload"
|
id="avatar-upload"
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor="avatar-upload">
|
||||||
htmlFor="avatar-upload"
|
<Button variant="primary" size="md" as="span" className="cursor-pointer">
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
|
{uploadingAvatar ? "Upload en cours..." : "Upload un avatar custom"}
|
||||||
>
|
</Button>
|
||||||
{uploadingAvatar
|
|
||||||
? "Upload en cours..."
|
|
||||||
: "Upload un avatar custom"}
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Username Field */}
|
{/* Username Field */}
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
type="text"
|
||||||
Nom d'utilisateur
|
label="Nom d'utilisateur"
|
||||||
</label>
|
value={username}
|
||||||
<input
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
type="text"
|
required
|
||||||
value={username}
|
minLength={3}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
maxLength={20}
|
||||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
className="bg-black/40"
|
||||||
required
|
/>
|
||||||
minLength={3}
|
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
||||||
maxLength={20}
|
|
||||||
/>
|
|
||||||
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bio Field */}
|
{/* Bio Field */}
|
||||||
<div>
|
<Textarea
|
||||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
label="Bio"
|
||||||
Bio
|
value={bio || ""}
|
||||||
</label>
|
onChange={(e) => setBio(e.target.value)}
|
||||||
<textarea
|
rows={4}
|
||||||
value={bio || ""}
|
maxLength={500}
|
||||||
onChange={(e) => setBio(e.target.value)}
|
showCharCount
|
||||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition resize-none"
|
placeholder="Parlez-nous de vous..."
|
||||||
rows={4}
|
className="bg-black/40"
|
||||||
maxLength={500}
|
/>
|
||||||
placeholder="Parlez-nous de vous..."
|
|
||||||
/>
|
|
||||||
<p className="text-gray-500 text-xs mt-1">
|
|
||||||
{(bio || "").length}/500 caractères
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Character Class Selection */}
|
{/* Character Class Selection */}
|
||||||
<div>
|
<div>
|
||||||
@@ -458,36 +401,23 @@ export default function ProfileForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* HP Bar */}
|
{/* HP Bar */}
|
||||||
<div className="mb-4">
|
<ProgressBar
|
||||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
value={profile.hp}
|
||||||
<span>HP</span>
|
max={profile.maxHp}
|
||||||
<span>
|
variant="hp"
|
||||||
{profile.hp} / {profile.maxHp}
|
showLabel
|
||||||
</span>
|
label="HP"
|
||||||
</div>
|
className="mb-4"
|
||||||
<div className="relative h-3 bg-gray-900 border border-gray-700 rounded overflow-hidden">
|
/>
|
||||||
<div
|
|
||||||
className={`absolute inset-0 bg-gradient-to-r ${hpColor} transition-all duration-1000 ease-out`}
|
|
||||||
style={{ width: `${hpPercentage}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* XP Bar */}
|
{/* XP Bar */}
|
||||||
<div>
|
<ProgressBar
|
||||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
value={profile.xp}
|
||||||
<span>XP</span>
|
max={profile.maxXp}
|
||||||
<span>
|
variant="xp"
|
||||||
{formatNumber(profile.xp)} / {formatNumber(profile.maxXp)}
|
showLabel
|
||||||
</span>
|
label="XP"
|
||||||
</div>
|
/>
|
||||||
<div className="relative h-3 bg-gray-900 border border-pixel-gold/30 rounded overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-gradient-to-r from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80 transition-all duration-1000 ease-out"
|
|
||||||
style={{ width: `${xpPercentage}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email (read-only) */}
|
{/* Email (read-only) */}
|
||||||
@@ -505,13 +435,9 @@ export default function ProfileForm({
|
|||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
|
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
|
||||||
<button
|
<Button type="submit" variant="primary" size="md" disabled={isPending}>
|
||||||
type="submit"
|
|
||||||
disabled={isPending}
|
|
||||||
className="px-6 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isPending ? "Enregistrement..." : "Enregistrer les modifications"}
|
{isPending ? "Enregistrement..." : "Enregistrer les modifications"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -522,65 +448,56 @@ export default function ProfileForm({
|
|||||||
Mot de passe
|
Mot de passe
|
||||||
</h3>
|
</h3>
|
||||||
{!showPasswordForm && (
|
{!showPasswordForm && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
onClick={() => setShowPasswordForm(true)}
|
onClick={() => setShowPasswordForm(true)}
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
|
||||||
>
|
>
|
||||||
Changer le mot de passe
|
Changer le mot de passe
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPasswordForm && (
|
{showPasswordForm && (
|
||||||
<form onSubmit={handlePasswordChange} className="space-y-4">
|
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
type="password"
|
||||||
Mot de passe actuel
|
label="Mot de passe actuel"
|
||||||
</label>
|
value={currentPassword}
|
||||||
<input
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
type="password"
|
required
|
||||||
value={currentPassword}
|
className="bg-black/40"
|
||||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
/>
|
||||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
type="password"
|
||||||
Nouveau mot de passe
|
label="Nouveau mot de passe"
|
||||||
</label>
|
value={newPassword}
|
||||||
<input
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
type="password"
|
required
|
||||||
value={newPassword}
|
minLength={6}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
className="bg-black/40"
|
||||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
/>
|
||||||
required
|
<p className="text-gray-500 text-xs mt-1">
|
||||||
minLength={6}
|
Minimum 6 caractères
|
||||||
/>
|
</p>
|
||||||
<p className="text-gray-500 text-xs mt-1">
|
|
||||||
Minimum 6 caractères
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
type="password"
|
||||||
Confirmer le nouveau mot de passe
|
label="Confirmer le nouveau mot de passe"
|
||||||
</label>
|
value={confirmPassword}
|
||||||
<input
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
type="password"
|
required
|
||||||
value={confirmPassword}
|
minLength={6}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
className="bg-black/40"
|
||||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
/>
|
||||||
required
|
|
||||||
minLength={6}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowPasswordForm(false);
|
setShowPasswordForm(false);
|
||||||
setCurrentPassword("");
|
setCurrentPassword("");
|
||||||
@@ -588,25 +505,25 @@ export default function ProfileForm({
|
|||||||
setConfirmPassword("");
|
setConfirmPassword("");
|
||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 border border-gray-600/50 bg-black/40 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/40 hover:border-gray-500 transition"
|
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
disabled={isChangingPassword}
|
disabled={isChangingPassword}
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
>
|
||||||
{isChangingPassword
|
{isChangingPassword
|
||||||
? "Modification..."
|
? "Modification..."
|
||||||
: "Modifier le mot de passe"}
|
: "Modifier le mot de passe"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</BackgroundSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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