Enhance UI components and animations: Introduce a shimmer animation effect in globals.css, refactor FeedbackPageClient, LoginPage, RegisterPage, and AdminPanel components to utilize new UI components for improved consistency and maintainability. Update event and feedback handling in EventsPageSection and FeedbackModal, ensuring a cohesive user experience across the application.

This commit is contained in:
Julien Froidefond
2025-12-12 16:44:57 +01:00
parent db01c25de7
commit 99a475736b
32 changed files with 2242 additions and 1389 deletions

View File

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

View File

@@ -5,6 +5,15 @@ import { useSession } from "next-auth/react";
import { useRouter, useParams } from "next/navigation"; import { useRouter, useParams } from "next/navigation";
import Navigation from "@/components/Navigation"; import Navigation from "@/components/Navigation";
import { createFeedback } from "@/actions/events/feedback"; import { createFeedback } from "@/actions/events/feedback";
import {
StarRating,
Textarea,
Button,
Alert,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
interface Event { interface Event {
id: string; id: string;
@@ -156,25 +165,17 @@ export default function FeedbackPageClient({
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">
<Navigation /> <Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24"> <BackgroundSection backgroundImage={backgroundImage} className="pt-24">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
{/* Feedback Form */} {/* Feedback Form */}
<div className="relative z-10 w-full max-w-2xl mx-auto px-8"> <div className="w-full max-w-2xl mx-auto px-8">
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm"> <Card variant="dark" className="p-8">
<h1 className="text-4xl font-gaming font-black mb-2 text-center"> <SectionTitle
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"> variant="gradient"
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>
); );
} }

View File

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

View File

@@ -5,6 +5,14 @@ import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import Navigation from "@/components/Navigation"; import Navigation from "@/components/Navigation";
import {
Input,
Button,
Alert,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
@@ -46,79 +54,53 @@ export default function LoginPage() {
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">
<Navigation /> <Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24"> <BackgroundSection backgroundImage="/got-2.jpg" className="pt-24">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('/got-2.jpg')`,
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
{/* Login Form */} {/* Login Form */}
<div className="relative z-10 w-full max-w-md mx-auto px-8"> <div className="w-full max-w-md mx-auto px-8">
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm"> <Card variant="dark" className="p-8">
<h1 className="text-4xl font-gaming font-black mb-2 text-center"> <SectionTitle
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"> variant="gradient"
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>
); );
} }

View File

@@ -4,7 +4,16 @@ import { useState, useRef, type ChangeEvent, type FormEvent } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import Navigation from "@/components/Navigation"; import Navigation from "@/components/Navigation";
import Avatar from "@/components/Avatar"; import {
Avatar,
Input,
Textarea,
Button,
Alert,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
export default function RegisterPage() { export default function RegisterPage() {
const router = useRouter(); const router = useRouter();
@@ -162,25 +171,17 @@ export default function RegisterPage() {
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">
<Navigation /> <Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24"> <BackgroundSection backgroundImage="/got-2.jpg" className="pt-24">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('/got-2.jpg')`,
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
{/* Register Form */} {/* Register Form */}
<div className="relative z-10 w-full max-w-md mx-auto px-8"> <div className="w-full max-w-md mx-auto px-8">
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm"> <Card variant="dark" className="p-8">
<h1 className="text-4xl font-gaming font-black mb-2 text-center"> <SectionTitle
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"> variant="gradient"
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&apos;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&apos;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>
); );
} }

View File

@@ -1,10 +1,12 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link";
import UserManagement from "@/components/UserManagement"; import UserManagement from "@/components/UserManagement";
import EventManagement from "@/components/EventManagement"; import EventManagement from "@/components/EventManagement";
import FeedbackManagement from "@/components/FeedbackManagement"; import FeedbackManagement from "@/components/FeedbackManagement";
import BackgroundPreferences from "@/components/BackgroundPreferences"; import BackgroundPreferences from "@/components/BackgroundPreferences";
import { Button, Card, SectionTitle } from "@/components/ui";
interface SitePreferences { interface SitePreferences {
id: string; id: string;
@@ -26,92 +28,91 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
return ( return (
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16"> <section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16"> <div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
<h1 className="text-4xl font-gaming font-black mb-8 text-center"> <SectionTitle variant="gradient" size="md" className="mb-8 text-center">
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
ADMIN ADMIN
</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>

View File

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

View File

@@ -3,6 +3,7 @@
import { useState, useEffect, useTransition } from "react"; import { useState, useEffect, useTransition } from "react";
import { calculateEventStatus } from "@/lib/eventStatus"; import { calculateEventStatus } from "@/lib/eventStatus";
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events"; import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
import { Input, Textarea, Button, Card, Badge } from "@/components/ui";
interface Event { interface Event {
id: string; id: string;
@@ -209,62 +210,52 @@ export default function EventManagement() {
Événements ({events.length}) Événements ({events.length})
</h3> </h3>
{!isCreating && !editingEvent && ( {!isCreating && !editingEvent && (
<button <Button
onClick={handleCreate} onClick={handleCreate}
className="px-3 sm:px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-green-900/30 transition whitespace-nowrap flex-shrink-0" variant="success"
size="sm"
className="whitespace-nowrap flex-shrink-0"
> >
+ Nouvel événement + Nouvel événement
</button> </Button>
)} )}
</div> </div>
{(isCreating || editingEvent) && ( {(isCreating || editingEvent) && (
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4 mb-4"> <Card variant="default" className="p-3 sm:p-4 mb-4">
<h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words"> <h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words">
{isCreating ? "Créer un événement" : "Modifier l'événement"} {isCreating ? "Créer un événement" : "Modifier l'événement"}
</h4> </h4>
<div className="space-y-4"> <div className="space-y-4">
<div> <Input
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
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>

View File

@@ -9,6 +9,15 @@ import {
registerForEvent, registerForEvent,
unregisterFromEvent, unregisterFromEvent,
} from "@/actions/events/register"; } from "@/actions/events/register";
import {
Badge,
Button,
Modal,
CloseButton,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
interface Event { interface Event {
id: string; id: string;
@@ -61,21 +70,21 @@ const getStatusBadge = (status: "UPCOMING" | "LIVE" | "PAST") => {
switch (status) { switch (status) {
case "UPCOMING": case "UPCOMING":
return ( return (
<span className="px-3 py-1 bg-green-900/50 border border-green-500/50 text-green-400 text-xs uppercase tracking-widest rounded"> <Badge variant="success" size="md">
À venir À venir
</span> </Badge>
); );
case "LIVE": case "LIVE":
return ( return (
<span className="px-3 py-1 bg-red-900/50 border border-red-500/50 text-red-400 text-xs uppercase tracking-widest rounded animate-pulse"> <Badge variant="danger" size="md" className="animate-pulse">
En direct En direct
</span> </Badge>
); );
case "PAST": case "PAST":
return ( return (
<span className="px-3 py-1 bg-gray-800/50 border border-gray-600/50 text-gray-400 text-xs uppercase tracking-widest rounded"> <Badge variant="default" size="md">
Passé Passé
</span> </Badge>
); );
} }
}; };
@@ -401,10 +410,10 @@ export default function EventsPageSection({
}; };
const renderEventCard = (event: Event) => ( const renderEventCard = (event: Event) => (
<div <Card
key={event.id} key={event.id}
onClick={() => setSelectedEvent(event)} onClick={() => setSelectedEvent(event)}
className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm hover:border-pixel-gold/50 transition group cursor-pointer" className="overflow-hidden hover:border-pixel-gold/50 transition group cursor-pointer"
> >
{/* Event Header */} {/* Event Header */}
<div <div
@@ -482,37 +491,41 @@ export default function EventsPageSection({
{getEventStatus(event) === "UPCOMING" && ( {getEventStatus(event) === "UPCOMING" && (
<> <>
{registrations[event.id] ? ( {registrations[event.id] ? (
<button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleUnregister(event.id); handleUnregister(event.id);
}} }}
variant="success"
size="md"
disabled={loading[event.id]} disabled={loading[event.id]}
className="w-full px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed" className="w-full"
> >
{loading[event.id] ? "Annulation..." : "Inscrit ✓"} {loading[event.id] ? "Annulation..." : "Inscrit ✓"}
</button> </Button>
) : ( ) : (
<button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleRegister(event.id); handleRegister(event.id);
}} }}
variant="primary"
size="md"
disabled={loading[event.id]} disabled={loading[event.id]}
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed" className="w-full"
> >
{loading[event.id] ? "Inscription..." : "S'inscrire maintenant"} {loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
</button> </Button>
)} )}
</> </>
)} )}
{getEventStatus(event) === "LIVE" && ( {getEventStatus(event) === "LIVE" && (
<button className="w-full px-4 py-2 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-xs tracking-widest rounded hover:bg-red-900/30 transition animate-pulse"> <Button variant="danger" size="md" className="w-full animate-pulse">
Rejoindre en direct Rejoindre en direct
</button> </Button>
)} )}
{getEventStatus(event) === "PAST" && ( {getEventStatus(event) === "PAST" && (
<button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!session?.user?.id) { if (!session?.user?.id) {
@@ -521,13 +534,15 @@ export default function EventsPageSection({
} }
setFeedbackEventId(event.id); setFeedbackEventId(event.id);
}} }}
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition" variant="primary"
size="md"
className="w-full"
> >
Donner un feedback Donner un feedback
</button> </Button>
)} )}
</div> </div>
</div> </Card>
); );
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
@@ -576,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&apos;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&apos;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>
); );
} }

View File

@@ -3,6 +3,15 @@
import { useState, useEffect, useTransition, type FormEvent } from "react"; import { useState, useEffect, useTransition, type FormEvent } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { createFeedback } from "@/actions/events/feedback"; import { createFeedback } from "@/actions/events/feedback";
import {
Modal,
StarRating,
Textarea,
Button,
Alert,
SectionTitle,
CloseButton,
} from "@/components/ui";
interface Event { interface Event {
id: string; id: string;
@@ -163,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>
); );
} }

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,14 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Avatar from "./Avatar"; import {
Avatar,
Modal,
CloseButton,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
interface LeaderboardEntry { interface LeaderboardEntry {
rank: number; rank: number;
@@ -33,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>
); );
} }

View File

@@ -5,6 +5,7 @@ import { useSession, signOut } from "next-auth/react";
import { useState } from "react"; import { useState } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import PlayerStats from "./PlayerStats"; import PlayerStats from "./PlayerStats";
import { Button } from "@/components/ui";
interface UserData { interface UserData {
username: string; username: string;
@@ -100,12 +101,14 @@ export default function Navigation({
{/* Desktop Auth Buttons */} {/* Desktop Auth Buttons */}
<div className="hidden md:flex items-center gap-4"> <div className="hidden md:flex items-center gap-4">
{isAuthenticated ? ( {isAuthenticated ? (
<button <Button
onClick={() => signOut()} onClick={() => signOut()}
className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest" variant="ghost"
size="sm"
className="text-xs font-normal"
> >
Déconnexion Déconnexion
</button> </Button>
) : ( ) : (
<> <>
<Link <Link
@@ -114,11 +117,10 @@ export default function Navigation({
> >
Connexion Connexion
</Link> </Link>
<Link <Link href="/register">
href="/register" <Button variant="primary" size="sm" className="text-xs">
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
>
Inscription 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>
</> </>
)} )}

View File

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

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useRef, useTransition, type ChangeEvent } from "react"; import { useState, useRef, useTransition, type ChangeEvent } from "react";
import Avatar from "./Avatar"; import { Avatar, Input, Textarea, Button, Alert, Card, BackgroundSection, SectionTitle, ProgressBar } from "@/components/ui";
import { updateProfile } from "@/actions/profile/update-profile"; import { updateProfile } from "@/actions/profile/update-profile";
import { updatePassword } from "@/actions/profile/update-password"; import { updatePassword } from "@/actions/profile/update-password";
@@ -170,53 +170,19 @@ export default function ProfileForm({
: "from-red-700 to-red-900"; : "from-red-700 to-red-900";
return ( return (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16"> <BackgroundSection backgroundImage={backgroundImage}>
{/* Background Image */} <div className="w-full max-w-4xl mx-auto px-8">
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* Dark overlay for readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
{/* Content */}
<div className="relative z-10 w-full max-w-4xl mx-auto px-8 py-16">
{/* Title Section */} {/* Title Section */}
<div className="text-center mb-12"> <SectionTitle variant="gradient" size="lg" subtitle="Gérez votre profil" className="mb-12">
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
<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&apos;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>
); );
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useEffect, useTransition } from "react"; import { useState, useEffect, useTransition } from "react";
import Avatar from "./Avatar"; import { Avatar, Input, Button, Card } from "@/components/ui";
import { updateUser, deleteUser } from "@/actions/admin/users"; import { updateUser, deleteUser } from "@/actions/admin/users";
interface User { interface User {
@@ -184,10 +184,7 @@ export default function UserManagement() {
: user.username; : user.username;
return ( return (
<div <Card key={user.id} variant="default" className="p-3 sm:p-4">
key={user.id}
className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4"
>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-2"> <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-2">
<div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0"> <div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0">
{/* Avatar */} {/* Avatar */}
@@ -248,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&apos;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
View File

@@ -0,0 +1,32 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface AlertProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
variant?: "success" | "error" | "warning" | "info";
}
const variantClasses = {
success: "bg-green-900/50 border-green-500/50 text-green-400",
error: "bg-red-900/50 border-red-500/50 text-red-400",
warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
info: "bg-blue-900/50 border-blue-500/50 text-blue-400",
};
export default function Alert({
children,
variant = "info",
className = "",
...props
}: AlertProps) {
return (
<div
className={`border rounded px-4 py-3 text-sm ${variantClasses[variant]} ${className}`}
{...props}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,43 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface BackgroundSectionProps extends HTMLAttributes<HTMLElement> {
children: ReactNode;
backgroundImage: string;
overlay?: boolean;
}
export default function BackgroundSection({
children,
backgroundImage,
overlay = true,
className = "",
...props
}: BackgroundSectionProps) {
return (
<section
className={`relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16 ${className}`}
{...props}
>
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* Dark overlay for readability */}
{overlay && (
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
)}
</div>
{/* Content */}
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm:px-8 py-16">
{children}
</div>
</section>
);
}

40
components/ui/Badge.tsx Normal file
View File

@@ -0,0 +1,40 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
children: ReactNode;
variant?: "default" | "success" | "warning" | "danger" | "info";
size?: "sm" | "md";
}
const variantClasses = {
default: "bg-pixel-gold/20 border-pixel-gold/50 text-pixel-gold",
success: "bg-green-900/50 border-green-500/50 text-green-400",
warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
danger: "bg-red-900/50 border-red-500/50 text-red-400",
info: "bg-blue-900/30 border-blue-500/50 text-blue-400",
};
const sizeClasses = {
sm: "px-2 py-1 text-[10px] sm:text-xs",
md: "px-3 py-1 text-xs",
};
export default function Badge({
children,
variant = "default",
size = "sm",
className = "",
...props
}: BadgeProps) {
return (
<span
className={`inline-block border uppercase rounded whitespace-nowrap flex-shrink-0 ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
{...props}
>
{children}
</span>
);
}

47
components/ui/Button.tsx Normal file
View File

@@ -0,0 +1,47 @@
"use client";
import { ButtonHTMLAttributes, ReactNode, ElementType } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "success" | "danger" | "ghost";
size?: "sm" | "md" | "lg";
children: ReactNode;
as?: ElementType;
}
const variantClasses = {
primary:
"border-pixel-gold/50 bg-black/60 text-white hover:bg-pixel-gold/10 hover:border-pixel-gold",
secondary:
"border-gray-600/50 bg-gray-900/20 text-gray-400 hover:bg-gray-900/30 hover:border-gray-500",
success:
"border-green-500/50 bg-green-900/20 text-green-400 hover:bg-green-900/30",
danger: "border-red-500/50 bg-red-900/20 text-red-400 hover:bg-red-900/30",
ghost: "border-transparent bg-transparent text-white hover:text-pixel-gold",
};
const sizeClasses = {
sm: "px-2 sm:px-3 py-1 text-[10px] sm:text-xs",
md: "px-4 py-2 text-xs",
lg: "px-6 py-3 text-sm",
};
export default function Button({
variant = "primary",
size = "md",
className = "",
disabled,
children,
as: Component = "button",
...props
}: ButtonProps) {
return (
<Component
className={`border uppercase tracking-widest rounded transition disabled:opacity-50 disabled:cursor-not-allowed ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
disabled={disabled}
{...props}
>
{children}
</Component>
);
}

30
components/ui/Card.tsx Normal file
View File

@@ -0,0 +1,30 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface CardProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
variant?: "default" | "dark";
}
const variantClasses = {
default: "bg-black/60 border border-pixel-gold/30",
dark: "bg-black/80 border border-pixel-gold/30",
};
export default function Card({
children,
variant = "default",
className = "",
...props
}: CardProps) {
return (
<div
className={`rounded-lg backdrop-blur-sm ${variantClasses[variant]} ${className}`}
{...props}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,29 @@
"use client";
import { ButtonHTMLAttributes } from "react";
interface CloseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
size?: "sm" | "md" | "lg";
}
const sizeClasses = {
sm: "text-xl",
md: "text-2xl",
lg: "text-3xl",
};
export default function CloseButton({
size = "md",
className = "",
...props
}: CloseButtonProps) {
return (
<button
className={`text-gray-400 hover:text-pixel-gold font-bold transition disabled:opacity-50 disabled:cursor-not-allowed ${sizeClasses[size]} ${className}`}
{...props}
>
×
</button>
);
}

38
components/ui/Input.tsx Normal file
View File

@@ -0,0 +1,38 @@
"use client";
import { InputHTMLAttributes, forwardRef } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, className = "", ...props }, ref) => {
return (
<div>
{label && (
<label
htmlFor={props.id}
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
>
{label}
</label>
)}
<input
ref={ref}
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition ${className}`}
{...props}
/>
{error && (
<p className="text-red-400 text-xs mt-1">{error}</p>
)}
</div>
);
}
);
Input.displayName = "Input";
export default Input;

54
components/ui/Modal.tsx Normal file
View File

@@ -0,0 +1,54 @@
"use client";
import { ReactNode, useEffect } from "react";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
size?: "sm" | "md" | "lg" | "xl";
closeOnOverlayClick?: boolean;
}
const sizeClasses = {
sm: "max-w-md",
md: "max-w-2xl",
lg: "max-w-3xl",
xl: "max-w-4xl",
};
export default function Modal({
isOpen,
onClose,
children,
size = "md",
closeOnOverlayClick = true,
}: ModalProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={closeOnOverlayClick ? onClose : undefined}
>
<div
className={`bg-black border-2 border-pixel-gold/70 rounded-lg w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto shadow-2xl`}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,73 @@
"use client";
import { HTMLAttributes } from "react";
interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {
value: number;
max: number;
variant?: "hp" | "xp" | "default";
showLabel?: boolean;
label?: string;
}
const variantClasses = {
hp: {
high: "from-green-600 to-green-700",
medium: "from-yellow-600 to-orange-700",
low: "from-red-700 to-red-900",
},
xp: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
default: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
};
export default function ProgressBar({
value,
max,
variant = "default",
showLabel = false,
label,
className = "",
...props
}: ProgressBarProps) {
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
let gradientClass = "";
if (variant === "hp") {
if (percentage > 60) {
gradientClass = variantClasses.hp.high;
} else if (percentage > 30) {
gradientClass = variantClasses.hp.medium;
} else {
gradientClass = variantClasses.hp.low;
}
} else if (variant === "xp") {
gradientClass = variantClasses.xp;
} else {
gradientClass = variantClasses.default;
}
return (
<div className={className} {...props}>
{showLabel && (
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span>{label || variant.toUpperCase()}</span>
<span>
{value} / {max}
</span>
</div>
)}
<div className="relative h-2 sm:h-3 bg-gray-900 border border-gray-700 rounded overflow-hidden">
<div
className={`absolute inset-0 bg-gradient-to-r ${gradientClass} transition-all duration-1000 ease-out`}
style={{ width: `${percentage}%` }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"></div>
</div>
{variant === "hp" && percentage < 30 && (
<div className="absolute inset-0 border border-red-500 rounded animate-pulse"></div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
"use client";
import { HTMLAttributes, ReactNode } from "react";
interface SectionTitleProps extends HTMLAttributes<HTMLHeadingElement> {
children: ReactNode;
variant?: "default" | "gradient" | "gold";
size?: "sm" | "md" | "lg" | "xl";
subtitle?: ReactNode;
}
const sizeClasses = {
sm: "text-2xl sm:text-3xl",
md: "text-3xl sm:text-4xl md:text-5xl",
lg: "text-4xl sm:text-5xl md:text-6xl lg:text-7xl",
xl: "text-5xl md:text-7xl",
};
export default function SectionTitle({
children,
variant = "default",
size = "md",
subtitle,
className = "",
...props
}: SectionTitleProps) {
const baseClasses = "font-gaming font-black tracking-tight mb-4";
let titleClasses = `${baseClasses} ${sizeClasses[size]} ${className}`;
if (variant === "gradient") {
titleClasses += " bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent";
} else if (variant === "gold") {
titleClasses += " text-pixel-gold";
} else {
titleClasses += " text-white";
}
return (
<div className="text-center">
<h1 className={titleClasses} {...props}>
{variant === "gradient" ? (
<span
style={{
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
}}
>
{children}
</span>
) : (
children
)}
</h1>
{subtitle && (
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
<span></span>
<span>{subtitle}</span>
<span></span>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,66 @@
"use client";
import { useState } from "react";
interface StarRatingProps {
value: number;
onChange?: (rating: number) => void;
disabled?: boolean;
size?: "sm" | "md" | "lg";
showValue?: boolean;
}
const sizeClasses = {
sm: "text-lg sm:text-xl",
md: "text-2xl sm:text-3xl",
lg: "text-4xl",
};
export default function StarRating({
value,
onChange,
disabled = false,
size = "md",
showValue = false,
}: StarRatingProps) {
const [hoverValue, setHoverValue] = useState(0);
const handleClick = (rating: number) => {
if (!disabled && onChange) {
onChange(rating);
}
};
const displayValue = hoverValue || value;
return (
<div className="flex flex-col items-center gap-2">
<div className="flex items-center justify-center gap-2">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => handleClick(star)}
disabled={disabled}
onMouseEnter={() => !disabled && setHoverValue(star)}
onMouseLeave={() => !disabled && setHoverValue(0)}
className={`transition-transform hover:scale-110 disabled:hover:scale-100 disabled:cursor-not-allowed ${
star <= displayValue
? "text-pixel-gold"
: "text-gray-600 hover:text-gray-500"
} ${sizeClasses[size]}`}
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
>
</button>
))}
</div>
{showValue && value > 0 && (
<p className="text-gray-500 text-xs text-center">
{value}/5
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,49 @@
"use client";
import { TextareaHTMLAttributes, forwardRef } from "react";
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
error?: string;
showCharCount?: boolean;
maxLength?: number;
}
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
({ label, error, showCharCount, maxLength, className = "", value, ...props }, ref) => {
const charCount = typeof value === "string" ? value.length : 0;
return (
<div>
{label && (
<label
htmlFor={props.id}
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
>
{label}
</label>
)}
<textarea
ref={ref}
maxLength={maxLength}
value={value}
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none ${className}`}
{...props}
/>
{showCharCount && maxLength && (
<p className="text-gray-500 text-xs mt-1 text-right">
{charCount}/{maxLength} caractères
</p>
)}
{error && (
<p className="text-red-400 text-xs mt-1">{error}</p>
)}
</div>
);
}
);
Textarea.displayName = "Textarea";
export default Textarea;

14
components/ui/index.ts Normal file
View File

@@ -0,0 +1,14 @@
export { default as Avatar } from "./Avatar";
export { default as Button } from "./Button";
export { default as Input } from "./Input";
export { default as Textarea } from "./Textarea";
export { default as Card } from "./Card";
export { default as Modal } from "./Modal";
export { default as Badge } from "./Badge";
export { default as ProgressBar } from "./ProgressBar";
export { default as StarRating } from "./StarRating";
export { default as SectionTitle } from "./SectionTitle";
export { default as BackgroundSection } from "./BackgroundSection";
export { default as Alert } from "./Alert";
export { default as CloseButton } from "./CloseButton";