Enhance UI components and animations: Introduce a shimmer animation effect in globals.css, refactor FeedbackPageClient, LoginPage, RegisterPage, and AdminPanel components to utilize new UI components for improved consistency and maintainability. Update event and feedback handling in EventsPageSection and FeedbackModal, ensuring a cohesive user experience across the application.
This commit is contained in:
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";
|
||||||
|
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 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,6 +5,15 @@ import { useSession } from "next-auth/react";
|
|||||||
import { useRouter, useParams } from "next/navigation";
|
import { useRouter, useParams } from "next/navigation";
|
||||||
import Navigation from "@/components/Navigation";
|
import Navigation from "@/components/Navigation";
|
||||||
import { createFeedback } from "@/actions/events/feedback";
|
import { createFeedback } from "@/actions/events/feedback";
|
||||||
|
import {
|
||||||
|
StarRating,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Card,
|
||||||
|
BackgroundSection,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -156,25 +165,17 @@ export default function FeedbackPageClient({
|
|||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black relative">
|
<main className="min-h-screen bg-black relative">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
<BackgroundSection backgroundImage={backgroundImage} className="pt-24">
|
||||||
{/* Background Image */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('${backgroundImage}')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feedback Form */}
|
{/* Feedback Form */}
|
||||||
<div className="relative z-10 w-full max-w-2xl mx-auto px-8">
|
<div className="w-full max-w-2xl mx-auto px-8">
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm">
|
<Card variant="dark" className="p-8">
|
||||||
<h1 className="text-4xl font-gaming font-black mb-2 text-center">
|
<SectionTitle
|
||||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
variant="gradient"
|
||||||
|
size="lg"
|
||||||
|
className="mb-2 text-center"
|
||||||
|
>
|
||||||
FEEDBACK
|
FEEDBACK
|
||||||
</span>
|
</SectionTitle>
|
||||||
</h1>
|
|
||||||
<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
|
|
||||||
htmlFor="comment"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Commentaire (optionnel)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="comment"
|
id="comment"
|
||||||
|
label="Commentaire (optionnel)"
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={(e) => setComment(e.target.value)}
|
onChange={(e) => setComment(e.target.value)}
|
||||||
rows={6}
|
rows={6}
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none"
|
showCharCount
|
||||||
placeholder="Partagez votre expérience, vos suggestions..."
|
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>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BackgroundSection>
|
||||||
</section>
|
|
||||||
</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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ import { signIn } from "next-auth/react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Navigation from "@/components/Navigation";
|
import Navigation from "@/components/Navigation";
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Card,
|
||||||
|
BackgroundSection,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -46,79 +54,53 @@ export default function LoginPage() {
|
|||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black relative">
|
<main className="min-h-screen bg-black relative">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
<BackgroundSection backgroundImage="/got-2.jpg" className="pt-24">
|
||||||
{/* Background Image */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('/got-2.jpg')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Login Form */}
|
{/* Login Form */}
|
||||||
<div className="relative z-10 w-full max-w-md mx-auto px-8">
|
<div className="w-full max-w-md mx-auto px-8">
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm">
|
<Card variant="dark" className="p-8">
|
||||||
<h1 className="text-4xl font-gaming font-black mb-2 text-center">
|
<SectionTitle
|
||||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
variant="gradient"
|
||||||
|
size="lg"
|
||||||
|
className="mb-2 text-center"
|
||||||
|
>
|
||||||
CONNEXION
|
CONNEXION
|
||||||
</span>
|
</SectionTitle>
|
||||||
</h1>
|
|
||||||
<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
|
|
||||||
htmlFor="email"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
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"
|
placeholder="votre@email.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Mot de passe"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
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="••••••••"
|
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>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BackgroundSection>
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ import { useState, useRef, type ChangeEvent, type FormEvent } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Navigation from "@/components/Navigation";
|
import Navigation from "@/components/Navigation";
|
||||||
import Avatar from "@/components/Avatar";
|
import {
|
||||||
|
Avatar,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Card,
|
||||||
|
BackgroundSection,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -162,25 +171,17 @@ export default function RegisterPage() {
|
|||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black relative">
|
<main className="min-h-screen bg-black relative">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
<BackgroundSection backgroundImage="/got-2.jpg" className="pt-24">
|
||||||
{/* Background Image */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('/got-2.jpg')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Register Form */}
|
{/* Register Form */}
|
||||||
<div className="relative z-10 w-full max-w-md mx-auto px-8">
|
<div className="w-full max-w-md mx-auto px-8">
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm">
|
<Card variant="dark" className="p-8">
|
||||||
<h1 className="text-4xl font-gaming font-black mb-2 text-center">
|
<SectionTitle
|
||||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
variant="gradient"
|
||||||
|
size="lg"
|
||||||
|
className="mb-2 text-center"
|
||||||
|
>
|
||||||
INSCRIPTION
|
INSCRIPTION
|
||||||
</span>
|
</SectionTitle>
|
||||||
</h1>
|
|
||||||
<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
|
|
||||||
htmlFor="email"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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"
|
placeholder="votre@email.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
|
||||||
htmlFor="username"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Nom d'utilisateur
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Nom d'utilisateur"
|
||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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"
|
placeholder="VotrePseudo"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Mot de passe"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
|
||||||
htmlFor="confirmPassword"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Confirmer le mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Confirmer le mot de passe"
|
||||||
value={formData.confirmPassword}
|
value={formData.confirmPassword}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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="••••••••"
|
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"
|
||||||
|
as="span"
|
||||||
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
{uploadingAvatar
|
{uploadingAvatar
|
||||||
? "Upload en cours..."
|
? "Upload en cours..."
|
||||||
: "Upload un avatar custom"}
|
: "Upload un avatar custom"}
|
||||||
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label
|
|
||||||
htmlFor="username-step2"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Nom d'utilisateur
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="username-step2"
|
id="username-step2"
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Nom d'utilisateur"
|
||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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"
|
placeholder="VotrePseudo"
|
||||||
minLength={3}
|
minLength={3}
|
||||||
maxLength={20}
|
maxLength={20}
|
||||||
/>
|
/>
|
||||||
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Textarea
|
||||||
<label
|
|
||||||
htmlFor="bio"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Bio (optionnel)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="bio"
|
id="bio"
|
||||||
name="bio"
|
name="bio"
|
||||||
|
label="Bio (optionnel)"
|
||||||
value={formData.bio}
|
value={formData.bio}
|
||||||
onChange={handleChange}
|
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}
|
rows={4}
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
|
showCharCount
|
||||||
placeholder="Parlez-nous de vous..."
|
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>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BackgroundSection>
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import UserManagement from "@/components/UserManagement";
|
import UserManagement from "@/components/UserManagement";
|
||||||
import EventManagement from "@/components/EventManagement";
|
import EventManagement from "@/components/EventManagement";
|
||||||
import FeedbackManagement from "@/components/FeedbackManagement";
|
import FeedbackManagement from "@/components/FeedbackManagement";
|
||||||
import BackgroundPreferences from "@/components/BackgroundPreferences";
|
import BackgroundPreferences from "@/components/BackgroundPreferences";
|
||||||
|
import { Button, Card, SectionTitle } from "@/components/ui";
|
||||||
|
|
||||||
interface SitePreferences {
|
interface SitePreferences {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,92 +28,91 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
|
|||||||
return (
|
return (
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
|
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
|
||||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
||||||
<h1 className="text-4xl font-gaming font-black mb-8 text-center">
|
<SectionTitle variant="gradient" size="md" className="mb-8 text-center">
|
||||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
|
||||||
ADMIN
|
ADMIN
|
||||||
</span>
|
</SectionTitle>
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Navigation Tabs */}
|
{/* Navigation Tabs */}
|
||||||
<div className="flex gap-4 mb-8 justify-center">
|
<div className="flex gap-4 mb-8 justify-center flex-wrap">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setActiveSection("preferences")}
|
onClick={() => setActiveSection("preferences")}
|
||||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
variant={activeSection === "preferences" ? "primary" : "secondary"}
|
||||||
activeSection === "preferences"
|
size="md"
|
||||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
className={
|
||||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
activeSection === "preferences" ? "bg-pixel-gold/10" : ""
|
||||||
}`}
|
}
|
||||||
>
|
>
|
||||||
Préférences UI
|
Préférences UI
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setActiveSection("users")}
|
onClick={() => setActiveSection("users")}
|
||||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
variant={activeSection === "users" ? "primary" : "secondary"}
|
||||||
activeSection === "users"
|
size="md"
|
||||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
className={activeSection === "users" ? "bg-pixel-gold/10" : ""}
|
||||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Utilisateurs
|
Utilisateurs
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setActiveSection("events")}
|
onClick={() => setActiveSection("events")}
|
||||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
variant={activeSection === "events" ? "primary" : "secondary"}
|
||||||
activeSection === "events"
|
size="md"
|
||||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
className={activeSection === "events" ? "bg-pixel-gold/10" : ""}
|
||||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Événements
|
Événements
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setActiveSection("feedbacks")}
|
onClick={() => setActiveSection("feedbacks")}
|
||||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
variant={activeSection === "feedbacks" ? "primary" : "secondary"}
|
||||||
activeSection === "feedbacks"
|
size="md"
|
||||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
className={activeSection === "feedbacks" ? "bg-pixel-gold/10" : ""}
|
||||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Feedbacks
|
Feedbacks
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeSection === "preferences" && (
|
{activeSection === "preferences" && (
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-4 sm:p-6 backdrop-blur-sm">
|
<Card variant="dark" className="p-4 sm:p-6">
|
||||||
<h2 className="text-xl sm:text-2xl font-gaming font-bold mb-6 text-pixel-gold break-words">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-gaming font-bold text-pixel-gold break-words">
|
||||||
Préférences UI Globales
|
Préférences UI Globales
|
||||||
</h2>
|
</h2>
|
||||||
|
<Link href="/admin/style-guide" target="_blank">
|
||||||
|
<Button variant="primary" size="sm">
|
||||||
|
📖 Voir le Style Guide
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<BackgroundPreferences initialPreferences={initialPreferences} />
|
<BackgroundPreferences initialPreferences={initialPreferences} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeSection === "users" && (
|
{activeSection === "users" && (
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
|
<Card variant="dark" className="p-6">
|
||||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||||
Gestion des Utilisateurs
|
Gestion des Utilisateurs
|
||||||
</h2>
|
</h2>
|
||||||
<UserManagement />
|
<UserManagement />
|
||||||
</div>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeSection === "events" && (
|
{activeSection === "events" && (
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
|
<Card variant="dark" className="p-6">
|
||||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||||
Gestion des Événements
|
Gestion des Événements
|
||||||
</h2>
|
</h2>
|
||||||
<EventManagement />
|
<EventManagement />
|
||||||
</div>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeSection === "feedbacks" && (
|
{activeSection === "feedbacks" && (
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
|
<Card variant="dark" className="p-6">
|
||||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||||
Gestion des Feedbacks
|
Gestion des Feedbacks
|
||||||
</h2>
|
</h2>
|
||||||
<FeedbackManagement />
|
<FeedbackManagement />
|
||||||
</div>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import ImageSelector from "@/components/ImageSelector";
|
import ImageSelector from "@/components/ImageSelector";
|
||||||
import { updateSitePreferences } from "@/actions/admin/preferences";
|
import { updateSitePreferences } from "@/actions/admin/preferences";
|
||||||
|
import { Button, Card } from "@/components/ui";
|
||||||
|
|
||||||
interface SitePreferences {
|
interface SitePreferences {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -142,7 +143,7 @@ export default function BackgroundPreferences({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4">
|
<Card variant="default" className="p-3 sm:p-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||||
@@ -153,12 +154,14 @@ export default function BackgroundPreferences({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<button
|
<Button
|
||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap flex-shrink-0"
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap flex-shrink-0"
|
||||||
>
|
>
|
||||||
Modifier
|
Modifier
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -195,18 +198,12 @@ export default function BackgroundPreferences({
|
|||||||
label="Background Leaderboard"
|
label="Background Leaderboard"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
||||||
<button
|
<Button onClick={handleSave} variant="success" size="md">
|
||||||
onClick={handleSave}
|
|
||||||
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition"
|
|
||||||
>
|
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button onClick={handleCancel} variant="secondary" size="md">
|
||||||
onClick={handleCancel}
|
|
||||||
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
|
|
||||||
>
|
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -381,6 +378,6 @@ export default function BackgroundPreferences({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect, useTransition } from "react";
|
import { useState, useEffect, useTransition } from "react";
|
||||||
import { calculateEventStatus } from "@/lib/eventStatus";
|
import { calculateEventStatus } from "@/lib/eventStatus";
|
||||||
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
|
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
|
||||||
|
import { Input, Textarea, Button, Card, Badge } from "@/components/ui";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -209,62 +210,52 @@ export default function EventManagement() {
|
|||||||
Événements ({events.length})
|
Événements ({events.length})
|
||||||
</h3>
|
</h3>
|
||||||
{!isCreating && !editingEvent && (
|
{!isCreating && !editingEvent && (
|
||||||
<button
|
<Button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
className="px-3 sm:px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-green-900/30 transition whitespace-nowrap flex-shrink-0"
|
variant="success"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap flex-shrink-0"
|
||||||
>
|
>
|
||||||
+ Nouvel événement
|
+ Nouvel événement
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(isCreating || editingEvent) && (
|
{(isCreating || editingEvent) && (
|
||||||
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4 mb-4">
|
<Card variant="default" className="p-3 sm:p-4 mb-4">
|
||||||
<h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words">
|
<h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words">
|
||||||
{isCreating ? "Créer un événement" : "Modifier l'événement"}
|
{isCreating ? "Créer un événement" : "Modifier l'événement"}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
|
||||||
Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
type="date"
|
||||||
|
label="Date"
|
||||||
value={formData.date}
|
value={formData.date}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, date: e.target.value })
|
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"
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
<Input
|
||||||
<div>
|
|
||||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
|
||||||
Nom
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Nom"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, name: e.target.value })
|
setFormData({ ...formData, name: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="Nom de l'événement"
|
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"
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
<Textarea
|
||||||
<div>
|
label="Description"
|
||||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, description: e.target.value })
|
setFormData({ ...formData, description: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="Description de l'événement"
|
placeholder="Description de l'événement"
|
||||||
rows={4}
|
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"
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
||||||
@@ -289,40 +280,29 @@ export default function EventManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
|
||||||
Salle
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Salle"
|
||||||
value={formData.room || ""}
|
value={formData.room || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, room: e.target.value })
|
setFormData({ ...formData, room: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="Ex: Nautilus"
|
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"
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
<Input
|
||||||
<div>
|
|
||||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
|
||||||
Heure
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Heure"
|
||||||
value={formData.time || ""}
|
value={formData.time || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, time: e.target.value })
|
setFormData({ ...formData, time: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="Ex: 11h-12h"
|
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"
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
<Input
|
||||||
<div>
|
|
||||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
|
||||||
Places max
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
type="number"
|
||||||
|
label="Places max"
|
||||||
value={formData.maxPlaces || ""}
|
value={formData.maxPlaces || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -333,27 +313,24 @@ export default function EventManagement() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="Ex: 25"
|
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"
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
<button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
variant="success"
|
||||||
|
size="md"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
{saving ? "Enregistrement..." : "Enregistrer"}
|
{saving ? "Enregistrement..." : "Enregistrer"}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button onClick={handleCancel} variant="secondary" size="md">
|
||||||
onClick={handleCancel}
|
|
||||||
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
|
|
||||||
>
|
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{events.length === 0 ? (
|
{events.length === 0 ? (
|
||||||
@@ -362,32 +339,29 @@ export default function EventManagement() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{events.map((event) => (
|
{events.map((event) => {
|
||||||
<div
|
const status = calculateEventStatus(event.date);
|
||||||
key={event.id}
|
const statusVariant =
|
||||||
className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4"
|
status === "UPCOMING"
|
||||||
>
|
? "success"
|
||||||
|
: 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 flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
|
<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">
|
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||||
{event.name}
|
{event.name}
|
||||||
</h4>
|
</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">
|
<Badge variant="default" size="sm">
|
||||||
{getEventTypeLabel(event.type)}
|
{getEventTypeLabel(event.type)}
|
||||||
</span>
|
</Badge>
|
||||||
<span
|
<Badge variant={statusVariant} size="sm">
|
||||||
className={`px-2 py-1 text-[10px] sm:text-xs uppercase rounded whitespace-nowrap flex-shrink-0 ${(() => {
|
{getStatusLabel(status)}
|
||||||
const status = calculateEventStatus(event.date);
|
</Badge>
|
||||||
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>
|
</div>
|
||||||
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
|
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
|
||||||
{event.description}
|
{event.description}
|
||||||
@@ -411,31 +385,36 @@ export default function EventManagement() {
|
|||||||
👥 Places: {event.maxPlaces}
|
👥 Places: {event.maxPlaces}
|
||||||
</p>
|
</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">
|
<Badge variant="info" size="sm">
|
||||||
{event.registrationsCount || 0} inscrit
|
{event.registrationsCount || 0} inscrit
|
||||||
{event.registrationsCount !== 1 ? "s" : ""}
|
{event.registrationsCount !== 1 ? "s" : ""}
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isCreating && !editingEvent && (
|
{!isCreating && !editingEvent && (
|
||||||
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
|
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
|
||||||
<button
|
<Button
|
||||||
onClick={() => handleEdit(event)}
|
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"
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Modifier
|
Modifier
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => handleDelete(event.id)}
|
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"
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import {
|
|||||||
registerForEvent,
|
registerForEvent,
|
||||||
unregisterFromEvent,
|
unregisterFromEvent,
|
||||||
} from "@/actions/events/register";
|
} from "@/actions/events/register";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
CloseButton,
|
||||||
|
Card,
|
||||||
|
BackgroundSection,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -61,21 +70,21 @@ const getStatusBadge = (status: "UPCOMING" | "LIVE" | "PAST") => {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case "UPCOMING":
|
case "UPCOMING":
|
||||||
return (
|
return (
|
||||||
<span className="px-3 py-1 bg-green-900/50 border border-green-500/50 text-green-400 text-xs uppercase tracking-widest rounded">
|
<Badge variant="success" size="md">
|
||||||
À venir
|
À venir
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "LIVE":
|
case "LIVE":
|
||||||
return (
|
return (
|
||||||
<span className="px-3 py-1 bg-red-900/50 border border-red-500/50 text-red-400 text-xs uppercase tracking-widest rounded animate-pulse">
|
<Badge variant="danger" size="md" className="animate-pulse">
|
||||||
En direct
|
En direct
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "PAST":
|
case "PAST":
|
||||||
return (
|
return (
|
||||||
<span className="px-3 py-1 bg-gray-800/50 border border-gray-600/50 text-gray-400 text-xs uppercase tracking-widest rounded">
|
<Badge variant="default" size="md">
|
||||||
Passé
|
Passé
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -401,10 +410,10 @@ export default function EventsPageSection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderEventCard = (event: Event) => (
|
const renderEventCard = (event: Event) => (
|
||||||
<div
|
<Card
|
||||||
key={event.id}
|
key={event.id}
|
||||||
onClick={() => setSelectedEvent(event)}
|
onClick={() => setSelectedEvent(event)}
|
||||||
className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm hover:border-pixel-gold/50 transition group cursor-pointer"
|
className="overflow-hidden hover:border-pixel-gold/50 transition group cursor-pointer"
|
||||||
>
|
>
|
||||||
{/* Event Header */}
|
{/* Event Header */}
|
||||||
<div
|
<div
|
||||||
@@ -482,37 +491,41 @@ export default function EventsPageSection({
|
|||||||
{getEventStatus(event) === "UPCOMING" && (
|
{getEventStatus(event) === "UPCOMING" && (
|
||||||
<>
|
<>
|
||||||
{registrations[event.id] ? (
|
{registrations[event.id] ? (
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleUnregister(event.id);
|
handleUnregister(event.id);
|
||||||
}}
|
}}
|
||||||
|
variant="success"
|
||||||
|
size="md"
|
||||||
disabled={loading[event.id]}
|
disabled={loading[event.id]}
|
||||||
className="w-full px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{loading[event.id] ? "Annulation..." : "Inscrit ✓"}
|
{loading[event.id] ? "Annulation..." : "Inscrit ✓"}
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleRegister(event.id);
|
handleRegister(event.id);
|
||||||
}}
|
}}
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
disabled={loading[event.id]}
|
disabled={loading[event.id]}
|
||||||
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
|
{loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{getEventStatus(event) === "LIVE" && (
|
{getEventStatus(event) === "LIVE" && (
|
||||||
<button className="w-full px-4 py-2 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-xs tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
|
<Button variant="danger" size="md" className="w-full animate-pulse">
|
||||||
Rejoindre en direct
|
Rejoindre en direct
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{getEventStatus(event) === "PAST" && (
|
{getEventStatus(event) === "PAST" && (
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@@ -521,13 +534,15 @@ export default function EventsPageSection({
|
|||||||
}
|
}
|
||||||
setFeedbackEventId(event.id);
|
setFeedbackEventId(event.id);
|
||||||
}}
|
}}
|
||||||
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
Donner un feedback
|
Donner un feedback
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
@@ -576,42 +591,20 @@ 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 */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('${backgroundImage}')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Dark overlay for readability */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
|
||||||
{/* Title Section */}
|
{/* Title Section */}
|
||||||
<div className="text-center mb-16">
|
<SectionTitle
|
||||||
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
|
variant="gradient"
|
||||||
<span
|
size="xl"
|
||||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
subtitle="Événements à venir et passés"
|
||||||
style={{
|
className="mb-16"
|
||||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
EVENTS
|
EVENTS
|
||||||
</span>
|
</SectionTitle>
|
||||||
</h1>
|
<p className="text-gray-400 text-sm max-w-2xl mx-auto text-center mb-16">
|
||||||
<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">
|
Rejoignez-nous pour des événements tech passionnants, des compétitions
|
||||||
<span>✦</span>
|
et des célébrations tout au long de l'année
|
||||||
<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>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Événements à venir */}
|
{/* Événements à venir */}
|
||||||
{upcomingEvents.length > 0 && (
|
{upcomingEvents.length > 0 && (
|
||||||
@@ -664,40 +657,33 @@ export default function EventsPageSection({
|
|||||||
Restez informé de nos derniers événements et annonces
|
Restez informé de nos derniers événements et annonces
|
||||||
</p>
|
</p>
|
||||||
</div> */}
|
</div> */}
|
||||||
</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
|
|
||||||
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
{getStatusBadge(
|
{getStatusBadge(getEventStatus(selectedEvent))}
|
||||||
selectedEvent ? getEventStatus(selectedEvent) : "UPCOMING"
|
<Badge variant="default" size="md">
|
||||||
)}
|
|
||||||
<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)}
|
{getEventTypeLabel(selectedEvent.type)}
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
||||||
{selectedEvent.name}
|
{selectedEvent.name}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<CloseButton
|
||||||
onClick={() => setSelectedEvent(null)}
|
onClick={() => setSelectedEvent(null)}
|
||||||
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition ml-4"
|
size="lg"
|
||||||
>
|
className="ml-4"
|
||||||
×
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Event Header Color Bar */}
|
{/* Event Header Color Bar */}
|
||||||
@@ -715,7 +701,13 @@ export default function EventsPageSection({
|
|||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})
|
})
|
||||||
: selectedEvent.date.toLocaleDateString("fr-FR", {
|
: selectedEvent.date instanceof Date
|
||||||
|
? selectedEvent.date.toLocaleDateString("fr-FR", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
: new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@@ -734,9 +726,7 @@ export default function EventsPageSection({
|
|||||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||||
Salle
|
Salle
|
||||||
</div>
|
</div>
|
||||||
<div className="font-semibold">
|
<div className="font-semibold">{selectedEvent.room}</div>
|
||||||
{selectedEvent.room}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -747,9 +737,7 @@ export default function EventsPageSection({
|
|||||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||||
Heure
|
Heure
|
||||||
</div>
|
</div>
|
||||||
<div className="font-semibold">
|
<div className="font-semibold">{selectedEvent.time}</div>
|
||||||
{selectedEvent.time}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -780,50 +768,57 @@ export default function EventsPageSection({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
{selectedEvent &&
|
{getEventStatus(selectedEvent) === "UPCOMING" && (
|
||||||
getEventStatus(selectedEvent) === "UPCOMING" && (
|
|
||||||
<div className="pt-4 border-t border-pixel-gold/20">
|
<div className="pt-4 border-t border-pixel-gold/20">
|
||||||
{registrations[selectedEvent.id] ? (
|
{registrations[selectedEvent.id] ? (
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleUnregister(selectedEvent.id);
|
handleUnregister(selectedEvent.id);
|
||||||
setSelectedEvent(null);
|
setSelectedEvent(null);
|
||||||
}}
|
}}
|
||||||
|
variant="success"
|
||||||
|
size="lg"
|
||||||
disabled={loading[selectedEvent.id]}
|
disabled={loading[selectedEvent.id]}
|
||||||
className="w-full px-4 py-3 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-sm tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{loading[selectedEvent.id]
|
{loading[selectedEvent.id]
|
||||||
? "Annulation..."
|
? "Annulation..."
|
||||||
: "Se désinscrire"}
|
: "Se désinscrire"}
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleRegister(selectedEvent.id);
|
handleRegister(selectedEvent.id);
|
||||||
setSelectedEvent(null);
|
setSelectedEvent(null);
|
||||||
}}
|
}}
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
disabled={loading[selectedEvent.id]}
|
disabled={loading[selectedEvent.id]}
|
||||||
className="w-full px-4 py-3 border border-pixel-gold/50 bg-pixel-gold/10 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/20 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{loading[selectedEvent.id]
|
{loading[selectedEvent.id]
|
||||||
? "Inscription..."
|
? "Inscription..."
|
||||||
: "S'inscrire maintenant"}
|
: "S'inscrire maintenant"}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedEvent && getEventStatus(selectedEvent) === "LIVE" && (
|
{getEventStatus(selectedEvent) === "LIVE" && (
|
||||||
<div className="pt-4 border-t border-pixel-gold/20">
|
<div className="pt-4 border-t border-pixel-gold/20">
|
||||||
<button className="w-full px-4 py-3 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-sm tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="lg"
|
||||||
|
className="w-full animate-pulse"
|
||||||
|
>
|
||||||
Rejoindre en direct
|
Rejoindre en direct
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedEvent && getEventStatus(selectedEvent) === "PAST" && (
|
{getEventStatus(selectedEvent) === "PAST" && (
|
||||||
<div className="pt-4 border-t border-pixel-gold/20">
|
<div className="pt-4 border-t border-pixel-gold/20">
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@@ -834,15 +829,16 @@ export default function EventsPageSection({
|
|||||||
setFeedbackEventId(selectedEvent.id);
|
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="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
Donner un feedback
|
Donner un feedback
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Feedback Modal */}
|
{/* Feedback Modal */}
|
||||||
@@ -850,6 +846,6 @@ export default function EventsPageSection({
|
|||||||
eventId={feedbackEventId}
|
eventId={feedbackEventId}
|
||||||
onClose={() => setFeedbackEventId(null)}
|
onClose={() => setFeedbackEventId(null)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</BackgroundSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
import { useState, useEffect, useTransition, type FormEvent } from "react";
|
import { useState, useEffect, useTransition, type FormEvent } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { createFeedback } from "@/actions/events/feedback";
|
import { createFeedback } from "@/actions/events/feedback";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
StarRating,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
SectionTitle,
|
||||||
|
CloseButton,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -163,37 +172,27 @@ export default function FeedbackModal({
|
|||||||
if (!eventId) return null;
|
if (!eventId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal
|
||||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
isOpen={!!eventId}
|
||||||
onClick={handleClose}
|
onClose={handleClose}
|
||||||
>
|
size="md"
|
||||||
<div
|
closeOnOverlayClick={!submitting}
|
||||||
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">
|
<div className="p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h1 className="text-4xl font-gaming font-black">
|
<SectionTitle variant="gradient" size="lg">
|
||||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
|
||||||
FEEDBACK
|
FEEDBACK
|
||||||
</span>
|
</SectionTitle>
|
||||||
</h1>
|
<CloseButton onClick={handleClose} disabled={submitting} size="lg" />
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-white text-center py-8">Chargement...</div>
|
<div className="text-white text-center py-8">Chargement...</div>
|
||||||
) : !event ? (
|
) : !event ? (
|
||||||
<div className="text-red-400 text-center py-8">
|
<Alert variant="error" className="text-center">
|
||||||
Événement introuvable
|
Événement introuvable
|
||||||
</div>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-gray-400 text-sm text-center mb-2">
|
<p className="text-gray-400 text-sm text-center mb-2">
|
||||||
@@ -206,15 +205,15 @@ export default function FeedbackModal({
|
|||||||
</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 !
|
Feedback enregistré avec succès !
|
||||||
</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">
|
||||||
@@ -223,69 +222,46 @@ export default function FeedbackModal({
|
|||||||
<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}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setRating(star)}
|
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className={`text-4xl transition-transform hover:scale-110 disabled:hover:scale-100 ${
|
size="lg"
|
||||||
star <= rating
|
showValue
|
||||||
? "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
|
|
||||||
htmlFor="comment"
|
|
||||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Commentaire (optionnel)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="comment"
|
id="comment"
|
||||||
|
label="Commentaire (optionnel)"
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={(e) => setComment(e.target.value)}
|
onChange={(e) => setComment(e.target.value)}
|
||||||
rows={6}
|
rows={6}
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
disabled={submitting}
|
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"
|
showCharCount
|
||||||
placeholder="Partagez votre expérience, vos suggestions..."
|
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>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
className="flex-1 text-center cursor-pointer"
|
||||||
>
|
>
|
||||||
{uploading ? "Upload..." : "Upload depuis le disque"}
|
{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;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Avatar from "./Avatar";
|
import {
|
||||||
|
Avatar,
|
||||||
|
Modal,
|
||||||
|
CloseButton,
|
||||||
|
Card,
|
||||||
|
BackgroundSection,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
interface LeaderboardEntry {
|
interface LeaderboardEntry {
|
||||||
rank: number;
|
rank: number;
|
||||||
@@ -33,38 +40,16 @@ export default function LeaderboardSection({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center pt-24 pb-16">
|
<BackgroundSection backgroundImage={backgroundImage}>
|
||||||
{/* Background Image */}
|
|
||||||
<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 */}
|
{/* Title Section */}
|
||||||
<div className="text-center mb-12 overflow-hidden">
|
<SectionTitle
|
||||||
<h1 className="text-3xl sm:text-4xl md:text-7xl font-gaming font-black mb-4 tracking-tight break-words">
|
variant="gradient"
|
||||||
<span
|
size="lg"
|
||||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
subtitle="Top Players"
|
||||||
style={{
|
className="mb-12 overflow-hidden"
|
||||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
LEADERBOARD
|
LEADERBOARD
|
||||||
</span>
|
</SectionTitle>
|
||||||
</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 */}
|
{/* Leaderboard Table */}
|
||||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
|
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
|
||||||
@@ -167,21 +152,15 @@ export default function LeaderboardSection({
|
|||||||
<p className="text-gray-500 text-sm">
|
<p className="text-gray-500 text-sm">
|
||||||
Compete with players worldwide and climb the ranks!
|
Compete with players worldwide and climb the ranks!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-600 text-xs mt-2">
|
<p className="text-gray-600 text-xs mt-2">Rankings update every hour</p>
|
||||||
Rankings update every hour
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Character Modal */}
|
{/* Character Modal */}
|
||||||
{selectedEntry && (
|
{selectedEntry && (
|
||||||
<div
|
<Modal
|
||||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
isOpen={!!selectedEntry}
|
||||||
onClick={() => setSelectedEntry(null)}
|
onClose={() => setSelectedEntry(null)}
|
||||||
>
|
size="md"
|
||||||
<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">
|
<div className="p-4 sm:p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -189,12 +168,7 @@ export default function LeaderboardSection({
|
|||||||
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
|
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
|
||||||
{selectedEntry.username}
|
{selectedEntry.username}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<CloseButton onClick={() => setSelectedEntry(null)} size="md" />
|
||||||
onClick={() => setSelectedEntry(null)}
|
|
||||||
className="text-gray-400 hover:text-pixel-gold text-2xl font-bold transition"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Avatar and Class */}
|
{/* Avatar and Class */}
|
||||||
@@ -228,13 +202,11 @@ export default function LeaderboardSection({
|
|||||||
{selectedEntry.characterClass === "NECROMANCER" && "💀"}
|
{selectedEntry.characterClass === "NECROMANCER" && "💀"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg font-bold text-pixel-gold uppercase tracking-wider">
|
<span className="text-lg font-bold text-pixel-gold uppercase tracking-wider">
|
||||||
{selectedEntry.characterClass === "WARRIOR" &&
|
{selectedEntry.characterClass === "WARRIOR" && "Guerrier"}
|
||||||
"Guerrier"}
|
|
||||||
{selectedEntry.characterClass === "MAGE" && "Mage"}
|
{selectedEntry.characterClass === "MAGE" && "Mage"}
|
||||||
{selectedEntry.characterClass === "ROGUE" && "Voleur"}
|
{selectedEntry.characterClass === "ROGUE" && "Voleur"}
|
||||||
{selectedEntry.characterClass === "RANGER" && "Rôdeur"}
|
{selectedEntry.characterClass === "RANGER" && "Rôdeur"}
|
||||||
{selectedEntry.characterClass === "PALADIN" &&
|
{selectedEntry.characterClass === "PALADIN" && "Paladin"}
|
||||||
"Paladin"}
|
|
||||||
{selectedEntry.characterClass === "ENGINEER" &&
|
{selectedEntry.characterClass === "ENGINEER" &&
|
||||||
"Ingénieur"}
|
"Ingénieur"}
|
||||||
{selectedEntry.characterClass === "MERCHANT" &&
|
{selectedEntry.characterClass === "MERCHANT" &&
|
||||||
@@ -252,22 +224,22 @@ export default function LeaderboardSection({
|
|||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||||
<div className="bg-black/60 border border-pixel-gold/30 rounded p-4">
|
<Card variant="default" className="p-4">
|
||||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
||||||
Score
|
Score
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-pixel-gold">
|
<div className="text-2xl font-bold text-pixel-gold">
|
||||||
{formatScore(selectedEntry.score)}
|
{formatScore(selectedEntry.score)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
<div className="bg-black/60 border border-pixel-gold/30 rounded p-4">
|
<Card variant="default" className="p-4">
|
||||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
||||||
Niveau
|
Niveau
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-pixel-gold">
|
<div className="text-2xl font-bold text-pixel-gold">
|
||||||
Lv.{selectedEntry.level}
|
Lv.{selectedEntry.level}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bio */}
|
{/* Bio */}
|
||||||
@@ -282,9 +254,8 @@ export default function LeaderboardSection({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</BackgroundSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useSession, signOut } from "next-auth/react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import PlayerStats from "./PlayerStats";
|
import PlayerStats from "./PlayerStats";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -100,12 +101,14 @@ export default function Navigation({
|
|||||||
{/* Desktop Auth Buttons */}
|
{/* Desktop Auth Buttons */}
|
||||||
<div className="hidden md:flex items-center gap-4">
|
<div className="hidden md:flex items-center gap-4">
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<button
|
<Button
|
||||||
onClick={() => signOut()}
|
onClick={() => signOut()}
|
||||||
className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs font-normal"
|
||||||
>
|
>
|
||||||
Déconnexion
|
Déconnexion
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
@@ -114,11 +117,10 @@ export default function Navigation({
|
|||||||
>
|
>
|
||||||
Connexion
|
Connexion
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link href="/register">
|
||||||
href="/register"
|
<Button variant="primary" size="sm" className="text-xs">
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
|
||||||
>
|
|
||||||
Inscription
|
Inscription
|
||||||
|
</Button>
|
||||||
</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";
|
||||||
|
|
||||||
@@ -170,53 +170,19 @@ export default function ProfileForm({
|
|||||||
: "from-red-700 to-red-900";
|
: "from-red-700 to-red-900";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
<BackgroundSection backgroundImage={backgroundImage}>
|
||||||
{/* Background Image */}
|
<div className="w-full max-w-4xl mx-auto px-8">
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('${backgroundImage}')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Dark overlay for readability */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="relative z-10 w-full max-w-4xl mx-auto px-8 py-16">
|
|
||||||
{/* Title Section */}
|
{/* Title Section */}
|
||||||
<div className="text-center mb-12">
|
<SectionTitle variant="gradient" size="lg" subtitle="Gérez votre profil" className="mb-12">
|
||||||
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
|
|
||||||
<span
|
|
||||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
|
||||||
style={{
|
|
||||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
PROFIL
|
PROFIL
|
||||||
</span>
|
</SectionTitle>
|
||||||
</h1>
|
|
||||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
|
|
||||||
<span>✦</span>
|
|
||||||
<span>Gérez votre profil</span>
|
|
||||||
<span>✦</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm">
|
<Card variant="default" className="overflow-hidden">
|
||||||
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
{error && (
|
{error && <Alert variant="error">{error}</Alert>}
|
||||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
|
{success && <Alert variant="success">{success}</Alert>}
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm">
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Avatar Section */}
|
{/* Avatar Section */}
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
@@ -281,51 +247,38 @@ export default function ProfileForm({
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
id="avatar-upload"
|
id="avatar-upload"
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor="avatar-upload">
|
||||||
htmlFor="avatar-upload"
|
<Button variant="primary" size="md" as="span" className="cursor-pointer">
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
|
{uploadingAvatar ? "Upload en cours..." : "Upload un avatar custom"}
|
||||||
>
|
</Button>
|
||||||
{uploadingAvatar
|
|
||||||
? "Upload en cours..."
|
|
||||||
: "Upload un avatar custom"}
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Username Field */}
|
{/* Username Field */}
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
|
||||||
Nom d'utilisateur
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Nom d'utilisateur"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
required
|
required
|
||||||
minLength={3}
|
minLength={3}
|
||||||
maxLength={20}
|
maxLength={20}
|
||||||
|
className="bg-black/40"
|
||||||
/>
|
/>
|
||||||
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
<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
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={bio || ""}
|
value={bio || ""}
|
||||||
onChange={(e) => setBio(e.target.value)}
|
onChange={(e) => setBio(e.target.value)}
|
||||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition resize-none"
|
|
||||||
rows={4}
|
rows={4}
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
|
showCharCount
|
||||||
placeholder="Parlez-nous de vous..."
|
placeholder="Parlez-nous de vous..."
|
||||||
|
className="bg-black/40"
|
||||||
/>
|
/>
|
||||||
<p className="text-gray-500 text-xs mt-1">
|
|
||||||
{(bio || "").length}/500 caractères
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Character Class Selection */}
|
{/* Character Class Selection */}
|
||||||
<div>
|
<div>
|
||||||
@@ -458,37 +411,24 @@ 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) */}
|
||||||
<div>
|
<div>
|
||||||
@@ -505,13 +445,9 @@ export default function ProfileForm({
|
|||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
|
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
|
||||||
<button
|
<Button type="submit" variant="primary" size="md" disabled={isPending}>
|
||||||
type="submit"
|
|
||||||
disabled={isPending}
|
|
||||||
className="px-6 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isPending ? "Enregistrement..." : "Enregistrer les modifications"}
|
{isPending ? "Enregistrement..." : "Enregistrer les modifications"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -522,65 +458,56 @@ export default function ProfileForm({
|
|||||||
Mot de passe
|
Mot de passe
|
||||||
</h3>
|
</h3>
|
||||||
{!showPasswordForm && (
|
{!showPasswordForm && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
onClick={() => setShowPasswordForm(true)}
|
onClick={() => setShowPasswordForm(true)}
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
|
||||||
>
|
>
|
||||||
Changer le mot de passe
|
Changer le mot de passe
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPasswordForm && (
|
{showPasswordForm && (
|
||||||
<form onSubmit={handlePasswordChange} className="space-y-4">
|
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
|
||||||
Mot de passe actuel
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Mot de passe actuel"
|
||||||
value={currentPassword}
|
value={currentPassword}
|
||||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
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
|
required
|
||||||
|
className="bg-black/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
|
||||||
Nouveau mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Nouveau mot de passe"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={6}
|
||||||
|
className="bg-black/40"
|
||||||
/>
|
/>
|
||||||
<p className="text-gray-500 text-xs mt-1">
|
<p className="text-gray-500 text-xs mt-1">
|
||||||
Minimum 6 caractères
|
Minimum 6 caractères
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
|
||||||
Confirmer le nouveau mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Confirmer le nouveau mot de passe"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={6}
|
||||||
|
className="bg-black/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowPasswordForm(false);
|
setShowPasswordForm(false);
|
||||||
setCurrentPassword("");
|
setCurrentPassword("");
|
||||||
@@ -588,25 +515,25 @@ export default function ProfileForm({
|
|||||||
setConfirmPassword("");
|
setConfirmPassword("");
|
||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 border border-gray-600/50 bg-black/40 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/40 hover:border-gray-500 transition"
|
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
disabled={isChangingPassword}
|
disabled={isChangingPassword}
|
||||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
>
|
||||||
{isChangingPassword
|
{isChangingPassword
|
||||||
? "Modification..."
|
? "Modification..."
|
||||||
: "Modifier le mot de passe"}
|
: "Modifier le mot de passe"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BackgroundSection>
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +245,9 @@ 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">
|
|
||||||
Nom d'utilisateur
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Nom d'utilisateur"
|
||||||
value={editingUser.username || ""}
|
value={editingUser.username || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditingUser({
|
setEditingUser({
|
||||||
@@ -261,10 +255,9 @@ export default function UserManagement() {
|
|||||||
username: e.target.value,
|
username: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
|
||||||
placeholder="Nom d'utilisateur"
|
placeholder="Nom d'utilisateur"
|
||||||
|
className="text-xs sm:text-sm px-2 sm:px-3 py-1"
|
||||||
/>
|
/>
|
||||||
</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"
|
||||||
|
as="span"
|
||||||
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
{uploadingAvatar === user.id
|
{uploadingAvatar === user.id
|
||||||
? "Upload en cours..."
|
? "Upload en cours..."
|
||||||
: "Upload un avatar custom"}
|
: "Upload un avatar custom"}
|
||||||
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -690,19 +687,21 @@ export default function UserManagement() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-2 pt-2">
|
<div className="flex flex-col sm:flex-row gap-2 pt-2">
|
||||||
<button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
variant="success"
|
||||||
|
size="md"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
{saving ? "Enregistrement..." : "Enregistrer"}
|
{saving ? "Enregistrement..." : "Enregistrer"}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
|
variant="secondary"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -747,7 +746,7 @@ export default function UserManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
|||||||
32
components/ui/Alert.tsx
Normal file
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