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:
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import UserManagement from "@/components/UserManagement";
|
||||
import EventManagement from "@/components/EventManagement";
|
||||
import FeedbackManagement from "@/components/FeedbackManagement";
|
||||
import BackgroundPreferences from "@/components/BackgroundPreferences";
|
||||
import { Button, Card, SectionTitle } from "@/components/ui";
|
||||
|
||||
interface SitePreferences {
|
||||
id: string;
|
||||
@@ -26,92 +28,91 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
||||
<h1 className="text-4xl font-gaming font-black mb-8 text-center">
|
||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
||||
ADMIN
|
||||
</span>
|
||||
</h1>
|
||||
<SectionTitle variant="gradient" size="md" className="mb-8 text-center">
|
||||
ADMIN
|
||||
</SectionTitle>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex gap-4 mb-8 justify-center">
|
||||
<button
|
||||
<div className="flex gap-4 mb-8 justify-center flex-wrap">
|
||||
<Button
|
||||
onClick={() => setActiveSection("preferences")}
|
||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
||||
activeSection === "preferences"
|
||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
||||
}`}
|
||||
variant={activeSection === "preferences" ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={
|
||||
activeSection === "preferences" ? "bg-pixel-gold/10" : ""
|
||||
}
|
||||
>
|
||||
Préférences UI
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveSection("users")}
|
||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
||||
activeSection === "users"
|
||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
||||
}`}
|
||||
variant={activeSection === "users" ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={activeSection === "users" ? "bg-pixel-gold/10" : ""}
|
||||
>
|
||||
Utilisateurs
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveSection("events")}
|
||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
||||
activeSection === "events"
|
||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
||||
}`}
|
||||
variant={activeSection === "events" ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={activeSection === "events" ? "bg-pixel-gold/10" : ""}
|
||||
>
|
||||
Événements
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveSection("feedbacks")}
|
||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
||||
activeSection === "feedbacks"
|
||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
||||
}`}
|
||||
variant={activeSection === "feedbacks" ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={activeSection === "feedbacks" ? "bg-pixel-gold/10" : ""}
|
||||
>
|
||||
Feedbacks
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{activeSection === "preferences" && (
|
||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-4 sm:p-6 backdrop-blur-sm">
|
||||
<h2 className="text-xl sm:text-2xl font-gaming font-bold mb-6 text-pixel-gold break-words">
|
||||
Préférences UI Globales
|
||||
</h2>
|
||||
<Card variant="dark" className="p-4 sm:p-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||
<h2 className="text-xl sm:text-2xl font-gaming font-bold text-pixel-gold break-words">
|
||||
Préférences UI Globales
|
||||
</h2>
|
||||
<Link href="/admin/style-guide" target="_blank">
|
||||
<Button variant="primary" size="sm">
|
||||
📖 Voir le Style Guide
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<BackgroundPreferences initialPreferences={initialPreferences} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{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">
|
||||
Gestion des Utilisateurs
|
||||
</h2>
|
||||
<UserManagement />
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{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">
|
||||
Gestion des Événements
|
||||
</h2>
|
||||
<EventManagement />
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{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">
|
||||
Gestion des Feedbacks
|
||||
</h2>
|
||||
<FeedbackManagement />
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import ImageSelector from "@/components/ImageSelector";
|
||||
import { updateSitePreferences } from "@/actions/admin/preferences";
|
||||
import { Button, Card } from "@/components/ui";
|
||||
|
||||
interface SitePreferences {
|
||||
id: string;
|
||||
@@ -142,7 +143,7 @@ export default function BackgroundPreferences({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4">
|
||||
<Card variant="default" className="p-3 sm:p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||
@@ -153,12 +154,14 @@ export default function BackgroundPreferences({
|
||||
</p>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<button
|
||||
<Button
|
||||
onClick={handleEdit}
|
||||
className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap flex-shrink-0"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -195,18 +198,12 @@ export default function BackgroundPreferences({
|
||||
label="Background Leaderboard"
|
||||
/>
|
||||
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition"
|
||||
>
|
||||
<Button onClick={handleSave} variant="success" size="md">
|
||||
Enregistrer
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
|
||||
>
|
||||
</Button>
|
||||
<Button onClick={handleCancel} variant="secondary" size="md">
|
||||
Annuler
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -381,6 +378,6 @@ export default function BackgroundPreferences({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useTransition } from "react";
|
||||
import { calculateEventStatus } from "@/lib/eventStatus";
|
||||
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
|
||||
import { Input, Textarea, Button, Card, Badge } from "@/components/ui";
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
@@ -209,62 +210,52 @@ export default function EventManagement() {
|
||||
Événements ({events.length})
|
||||
</h3>
|
||||
{!isCreating && !editingEvent && (
|
||||
<button
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
className="px-3 sm:px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-green-900/30 transition whitespace-nowrap flex-shrink-0"
|
||||
variant="success"
|
||||
size="sm"
|
||||
className="whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
+ Nouvel événement
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(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">
|
||||
{isCreating ? "Créer un événement" : "Modifier l'événement"}
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
||||
Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.date}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, date: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
||||
Nom
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, name: e.target.value })
|
||||
}
|
||||
placeholder="Nom de l'événement"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, description: e.target.value })
|
||||
}
|
||||
placeholder="Description de l'événement"
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="date"
|
||||
label="Date"
|
||||
value={formData.date}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, date: e.target.value })
|
||||
}
|
||||
className="text-xs sm:text-sm px-3 py-2"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
label="Nom"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, name: e.target.value })
|
||||
}
|
||||
placeholder="Nom de l'événement"
|
||||
className="text-xs sm:text-sm px-3 py-2"
|
||||
/>
|
||||
<Textarea
|
||||
label="Description"
|
||||
value={formData.description}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, description: e.target.value })
|
||||
}
|
||||
placeholder="Description de l'événement"
|
||||
rows={4}
|
||||
className="text-xs sm:text-sm px-3 py-2"
|
||||
/>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
||||
@@ -289,71 +280,57 @@ export default function EventManagement() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
||||
Salle
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.room || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, room: e.target.value })
|
||||
}
|
||||
placeholder="Ex: Nautilus"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
||||
Heure
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.time || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, time: e.target.value })
|
||||
}
|
||||
placeholder="Ex: 11h-12h"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
||||
Places max
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.maxPlaces || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
maxPlaces: e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
placeholder="Ex: 25"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
label="Salle"
|
||||
value={formData.room || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, room: e.target.value })
|
||||
}
|
||||
placeholder="Ex: Nautilus"
|
||||
className="text-xs sm:text-sm px-3 py-2"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
label="Heure"
|
||||
value={formData.time || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, time: e.target.value })
|
||||
}
|
||||
placeholder="Ex: 11h-12h"
|
||||
className="text-xs sm:text-sm px-3 py-2"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Places max"
|
||||
value={formData.maxPlaces || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
maxPlaces: e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
placeholder="Ex: 25"
|
||||
className="text-xs sm:text-sm px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="success"
|
||||
size="md"
|
||||
disabled={saving}
|
||||
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Enregistrement..." : "Enregistrer"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
|
||||
>
|
||||
</Button>
|
||||
<Button onClick={handleCancel} variant="secondary" size="md">
|
||||
Annuler
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{events.length === 0 ? (
|
||||
@@ -362,80 +339,82 @@ export default function EventManagement() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
|
||||
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||
{event.name}
|
||||
</h4>
|
||||
<span className="px-2 py-1 bg-pixel-gold/20 border border-pixel-gold/50 text-pixel-gold text-[10px] sm:text-xs uppercase rounded whitespace-nowrap flex-shrink-0">
|
||||
{getEventTypeLabel(event.type)}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-1 text-[10px] sm:text-xs uppercase rounded whitespace-nowrap flex-shrink-0 ${(() => {
|
||||
const status = calculateEventStatus(event.date);
|
||||
return status === "UPCOMING"
|
||||
? "bg-green-900/50 border border-green-500/50 text-green-400"
|
||||
: status === "LIVE"
|
||||
? "bg-yellow-900/50 border border-yellow-500/50 text-yellow-400"
|
||||
: "bg-gray-900/50 border border-gray-500/50 text-gray-400";
|
||||
})()}`}
|
||||
>
|
||||
{getStatusLabel(calculateEventStatus(event.date))}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
|
||||
{event.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-4 mt-2">
|
||||
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
||||
Date: {new Date(event.date).toLocaleDateString("fr-FR")}
|
||||
{events.map((event) => {
|
||||
const status = calculateEventStatus(event.date);
|
||||
const statusVariant =
|
||||
status === "UPCOMING"
|
||||
? "success"
|
||||
: status === "LIVE"
|
||||
? "warning"
|
||||
: "default";
|
||||
|
||||
return (
|
||||
<Card key={event.id} variant="default" className="p-3 sm:p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
|
||||
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||
{event.name}
|
||||
</h4>
|
||||
<Badge variant="default" size="sm">
|
||||
{getEventTypeLabel(event.type)}
|
||||
</Badge>
|
||||
<Badge variant={statusVariant} size="sm">
|
||||
{getStatusLabel(status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
|
||||
{event.description}
|
||||
</p>
|
||||
{event.room && (
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-4 mt-2">
|
||||
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
||||
📍 Salle: {event.room}
|
||||
Date: {new Date(event.date).toLocaleDateString("fr-FR")}
|
||||
</p>
|
||||
)}
|
||||
{event.time && (
|
||||
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
||||
🕐 Heure: {event.time}
|
||||
</p>
|
||||
)}
|
||||
{event.maxPlaces && (
|
||||
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
||||
👥 Places: {event.maxPlaces}
|
||||
</p>
|
||||
)}
|
||||
<span className="px-2 py-1 bg-blue-900/30 border border-blue-500/50 text-blue-400 text-[10px] sm:text-xs rounded whitespace-nowrap flex-shrink-0">
|
||||
{event.registrationsCount || 0} inscrit
|
||||
{event.registrationsCount !== 1 ? "s" : ""}
|
||||
</span>
|
||||
{event.room && (
|
||||
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
||||
📍 Salle: {event.room}
|
||||
</p>
|
||||
)}
|
||||
{event.time && (
|
||||
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
||||
🕐 Heure: {event.time}
|
||||
</p>
|
||||
)}
|
||||
{event.maxPlaces && (
|
||||
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
||||
👥 Places: {event.maxPlaces}
|
||||
</p>
|
||||
)}
|
||||
<Badge variant="info" size="sm">
|
||||
{event.registrationsCount || 0} inscrit
|
||||
{event.registrationsCount !== 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{!isCreating && !editingEvent && (
|
||||
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
|
||||
<Button
|
||||
onClick={() => handleEdit(event)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Modifier
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleDelete(event.id)}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isCreating && !editingEvent && (
|
||||
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => handleEdit(event)}
|
||||
className="px-2 sm:px-3 py-1 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(event.id)}
|
||||
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-red-900/30 transition whitespace-nowrap"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,15 @@ import {
|
||||
registerForEvent,
|
||||
unregisterFromEvent,
|
||||
} from "@/actions/events/register";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Modal,
|
||||
CloseButton,
|
||||
Card,
|
||||
BackgroundSection,
|
||||
SectionTitle,
|
||||
} from "@/components/ui";
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
@@ -61,21 +70,21 @@ const getStatusBadge = (status: "UPCOMING" | "LIVE" | "PAST") => {
|
||||
switch (status) {
|
||||
case "UPCOMING":
|
||||
return (
|
||||
<span className="px-3 py-1 bg-green-900/50 border border-green-500/50 text-green-400 text-xs uppercase tracking-widest rounded">
|
||||
<Badge variant="success" size="md">
|
||||
À venir
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
case "LIVE":
|
||||
return (
|
||||
<span className="px-3 py-1 bg-red-900/50 border border-red-500/50 text-red-400 text-xs uppercase tracking-widest rounded animate-pulse">
|
||||
<Badge variant="danger" size="md" className="animate-pulse">
|
||||
En direct
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
case "PAST":
|
||||
return (
|
||||
<span className="px-3 py-1 bg-gray-800/50 border border-gray-600/50 text-gray-400 text-xs uppercase tracking-widest rounded">
|
||||
<Badge variant="default" size="md">
|
||||
Passé
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -401,10 +410,10 @@ export default function EventsPageSection({
|
||||
};
|
||||
|
||||
const renderEventCard = (event: Event) => (
|
||||
<div
|
||||
<Card
|
||||
key={event.id}
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm hover:border-pixel-gold/50 transition group cursor-pointer"
|
||||
className="overflow-hidden hover:border-pixel-gold/50 transition group cursor-pointer"
|
||||
>
|
||||
{/* Event Header */}
|
||||
<div
|
||||
@@ -482,37 +491,41 @@ export default function EventsPageSection({
|
||||
{getEventStatus(event) === "UPCOMING" && (
|
||||
<>
|
||||
{registrations[event.id] ? (
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUnregister(event.id);
|
||||
}}
|
||||
variant="success"
|
||||
size="md"
|
||||
disabled={loading[event.id]}
|
||||
className="w-full px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full"
|
||||
>
|
||||
{loading[event.id] ? "Annulation..." : "Inscrit ✓"}
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRegister(event.id);
|
||||
}}
|
||||
variant="primary"
|
||||
size="md"
|
||||
disabled={loading[event.id]}
|
||||
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full"
|
||||
>
|
||||
{loading[event.id] ? "Inscription..." : "S'inscrire maintenant"}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{getEventStatus(event) === "LIVE" && (
|
||||
<button className="w-full px-4 py-2 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-xs tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
|
||||
<Button variant="danger" size="md" className="w-full animate-pulse">
|
||||
Rejoindre en direct
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{getEventStatus(event) === "PAST" && (
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!session?.user?.id) {
|
||||
@@ -521,13 +534,15 @@ export default function EventsPageSection({
|
||||
}
|
||||
setFeedbackEventId(event.id);
|
||||
}}
|
||||
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="w-full"
|
||||
>
|
||||
Donner un feedback
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const [, startTransition] = useTransition();
|
||||
@@ -576,273 +591,254 @@ export default function EventsPageSection({
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
<BackgroundSection backgroundImage={backgroundImage}>
|
||||
{/* Title Section */}
|
||||
<SectionTitle
|
||||
variant="gradient"
|
||||
size="xl"
|
||||
subtitle="Événements à venir et passés"
|
||||
className="mb-16"
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
</div>
|
||||
EVENTS
|
||||
</SectionTitle>
|
||||
<p className="text-gray-400 text-sm max-w-2xl mx-auto text-center mb-16">
|
||||
Rejoignez-nous pour des événements tech passionnants, des compétitions
|
||||
et des célébrations tout au long de l'année
|
||||
</p>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
||||
{/* Title Section */}
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
|
||||
<span
|
||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
||||
style={{
|
||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
||||
}}
|
||||
>
|
||||
EVENTS
|
||||
</span>
|
||||
</h1>
|
||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 mb-6 tracking-wide">
|
||||
<span>✦</span>
|
||||
<span>Événements à venir et passés</span>
|
||||
<span>✦</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm max-w-2xl mx-auto">
|
||||
Rejoignez-nous pour des événements tech passionnants, des
|
||||
compétitions et des célébrations tout au long de l'année
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Événements à venir */}
|
||||
{upcomingEvents.length > 0 && (
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
|
||||
<span className="bg-gradient-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
|
||||
Événements à venir
|
||||
</span>
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{upcomingEvents.map(renderEventCard)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Calendrier */}
|
||||
{/* Événements à venir */}
|
||||
{upcomingEvents.length > 0 && (
|
||||
<div className="mb-16">
|
||||
<h2 className="text-2xl font-bold text-white mb-6 text-center uppercase tracking-widest">
|
||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
||||
Calendrier
|
||||
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
|
||||
<span className="bg-gradient-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
|
||||
Événements à venir
|
||||
</span>
|
||||
</h2>
|
||||
{renderCalendar()}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{upcomingEvents.map(renderEventCard)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Événements passés */}
|
||||
{pastEvents.length > 0 && (
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
|
||||
<span className="bg-gradient-to-r from-gray-400 to-gray-600 bg-clip-text text-transparent">
|
||||
Événements passés
|
||||
</span>
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{pastEvents.map(renderEventCard)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer Info */}
|
||||
{/* <div className="mt-12 text-center">
|
||||
<p className="text-gray-500 text-sm">
|
||||
Restez informé de nos derniers événements et annonces
|
||||
</p>
|
||||
</div> */}
|
||||
{/* Calendrier */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-2xl font-bold text-white mb-6 text-center uppercase tracking-widest">
|
||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
||||
Calendrier
|
||||
</span>
|
||||
</h2>
|
||||
{renderCalendar()}
|
||||
</div>
|
||||
|
||||
{/* Événements passés */}
|
||||
{pastEvents.length > 0 && (
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-white mb-8 text-center uppercase tracking-widest">
|
||||
<span className="bg-gradient-to-r from-gray-400 to-gray-600 bg-clip-text text-transparent">
|
||||
Événements passés
|
||||
</span>
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{pastEvents.map(renderEventCard)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer Info */}
|
||||
{/* <div className="mt-12 text-center">
|
||||
<p className="text-gray-500 text-sm">
|
||||
Restez informé de nos derniers événements et annonces
|
||||
</p>
|
||||
</div> */}
|
||||
|
||||
{/* Event Modal */}
|
||||
{selectedEvent && (
|
||||
<div
|
||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
<Modal
|
||||
isOpen={!!selectedEvent}
|
||||
onClose={() => setSelectedEvent(null)}
|
||||
size="lg"
|
||||
>
|
||||
<div
|
||||
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
{getStatusBadge(
|
||||
selectedEvent ? getEventStatus(selectedEvent) : "UPCOMING"
|
||||
)}
|
||||
<span className="px-3 py-1 bg-pixel-gold/20 border border-pixel-gold/50 text-pixel-gold text-xs uppercase rounded">
|
||||
{getEventTypeLabel(selectedEvent.type)}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
||||
{selectedEvent.name}
|
||||
</h2>
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
{getStatusBadge(getEventStatus(selectedEvent))}
|
||||
<Badge variant="default" size="md">
|
||||
{getEventTypeLabel(selectedEvent.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition ml-4"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
||||
{selectedEvent.name}
|
||||
</h2>
|
||||
</div>
|
||||
<CloseButton
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
size="lg"
|
||||
className="ml-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Event Header Color Bar */}
|
||||
<div
|
||||
className={`h-1 bg-gradient-to-r ${getEventTypeColor(
|
||||
selectedEvent.type
|
||||
)} mb-6 rounded`}
|
||||
></div>
|
||||
{/* Event Header Color Bar */}
|
||||
<div
|
||||
className={`h-1 bg-gradient-to-r ${getEventTypeColor(
|
||||
selectedEvent.type
|
||||
)} mb-6 rounded`}
|
||||
></div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="text-white text-lg font-bold uppercase tracking-widest mb-4">
|
||||
{typeof selectedEvent.date === "string"
|
||||
? new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
|
||||
{/* Date */}
|
||||
<div className="text-white text-lg font-bold uppercase tracking-widest mb-4">
|
||||
{typeof selectedEvent.date === "string"
|
||||
? new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
: selectedEvent.date instanceof Date
|
||||
? selectedEvent.date.toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
: selectedEvent.date.toLocaleDateString("fr-FR", {
|
||||
: new Date(selectedEvent.date).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Details */}
|
||||
{(selectedEvent.room ||
|
||||
selectedEvent.time ||
|
||||
selectedEvent.maxPlaces) && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
{selectedEvent.room && (
|
||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||
<span className="text-pixel-gold text-xl">📍</span>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Salle
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{selectedEvent.room}
|
||||
</div>
|
||||
{/* Event Details */}
|
||||
{(selectedEvent.room ||
|
||||
selectedEvent.time ||
|
||||
selectedEvent.maxPlaces) && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
{selectedEvent.room && (
|
||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||
<span className="text-pixel-gold text-xl">📍</span>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Salle
|
||||
</div>
|
||||
<div className="font-semibold">{selectedEvent.room}</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.time && (
|
||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||
<span className="text-pixel-gold text-xl">🕐</span>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Heure
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{selectedEvent.time}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.maxPlaces && (
|
||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||
<span className="text-pixel-gold text-xl">👥</span>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Places
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{selectedEvent.maxPlaces}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Full Description */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-3">
|
||||
Description
|
||||
</h3>
|
||||
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-line">
|
||||
{selectedEvent.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
{selectedEvent &&
|
||||
getEventStatus(selectedEvent) === "UPCOMING" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
{registrations[selectedEvent.id] ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUnregister(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
disabled={loading[selectedEvent.id]}
|
||||
className="w-full px-4 py-3 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-sm tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading[selectedEvent.id]
|
||||
? "Annulation..."
|
||||
: "Se désinscrire"}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRegister(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
disabled={loading[selectedEvent.id]}
|
||||
className="w-full px-4 py-3 border border-pixel-gold/50 bg-pixel-gold/10 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/20 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading[selectedEvent.id]
|
||||
? "Inscription..."
|
||||
: "S'inscrire maintenant"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent && getEventStatus(selectedEvent) === "LIVE" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
<button className="w-full px-4 py-3 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-sm tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
|
||||
Rejoindre en direct
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent && getEventStatus(selectedEvent) === "PAST" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
<button
|
||||
{selectedEvent.time && (
|
||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||
<span className="text-pixel-gold text-xl">🕐</span>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Heure
|
||||
</div>
|
||||
<div className="font-semibold">{selectedEvent.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.maxPlaces && (
|
||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||
<span className="text-pixel-gold text-xl">👥</span>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Places
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{selectedEvent.maxPlaces}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Full Description */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-3">
|
||||
Description
|
||||
</h3>
|
||||
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-line">
|
||||
{selectedEvent.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
{getEventStatus(selectedEvent) === "UPCOMING" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
{registrations[selectedEvent.id] ? (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!session?.user?.id) {
|
||||
router.push("/login");
|
||||
setSelectedEvent(null);
|
||||
return;
|
||||
}
|
||||
setFeedbackEventId(selectedEvent.id);
|
||||
handleUnregister(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
className="w-full px-4 py-3 border border-pixel-gold/50 bg-black/40 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
||||
variant="success"
|
||||
size="lg"
|
||||
disabled={loading[selectedEvent.id]}
|
||||
className="w-full"
|
||||
>
|
||||
Donner un feedback
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{loading[selectedEvent.id]
|
||||
? "Annulation..."
|
||||
: "Se désinscrire"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRegister(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
disabled={loading[selectedEvent.id]}
|
||||
className="w-full"
|
||||
>
|
||||
{loading[selectedEvent.id]
|
||||
? "Inscription..."
|
||||
: "S'inscrire maintenant"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{getEventStatus(selectedEvent) === "LIVE" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
<Button
|
||||
variant="danger"
|
||||
size="lg"
|
||||
className="w-full animate-pulse"
|
||||
>
|
||||
Rejoindre en direct
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{getEventStatus(selectedEvent) === "PAST" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!session?.user?.id) {
|
||||
router.push("/login");
|
||||
setSelectedEvent(null);
|
||||
return;
|
||||
}
|
||||
setFeedbackEventId(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
Donner un feedback
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Feedback Modal */}
|
||||
@@ -850,6 +846,6 @@ export default function EventsPageSection({
|
||||
eventId={feedbackEventId}
|
||||
onClose={() => setFeedbackEventId(null)}
|
||||
/>
|
||||
</section>
|
||||
</BackgroundSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
import { useState, useEffect, useTransition, type FormEvent } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { createFeedback } from "@/actions/events/feedback";
|
||||
import {
|
||||
Modal,
|
||||
StarRating,
|
||||
Textarea,
|
||||
Button,
|
||||
Alert,
|
||||
SectionTitle,
|
||||
CloseButton,
|
||||
} from "@/components/ui";
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
@@ -163,129 +172,96 @@ export default function FeedbackModal({
|
||||
if (!eventId) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||||
onClick={handleClose}
|
||||
<Modal
|
||||
isOpen={!!eventId}
|
||||
onClose={handleClose}
|
||||
size="md"
|
||||
closeOnOverlayClick={!submitting}
|
||||
>
|
||||
<div
|
||||
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-4xl font-gaming font-black">
|
||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
||||
FEEDBACK
|
||||
</span>
|
||||
</h1>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
disabled={submitting}
|
||||
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-white text-center py-8">Chargement...</div>
|
||||
) : !event ? (
|
||||
<div className="text-red-400 text-center py-8">
|
||||
Événement introuvable
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-gray-400 text-sm text-center mb-2">
|
||||
{existingFeedback
|
||||
? "Modifier votre feedback pour"
|
||||
: "Donnez votre avis sur"}
|
||||
</p>
|
||||
<p className="text-pixel-gold text-lg font-semibold text-center mb-8">
|
||||
{event.name}
|
||||
</p>
|
||||
|
||||
{success && (
|
||||
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm mb-6">
|
||||
Feedback enregistré avec succès !
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm mb-6">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Rating */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">
|
||||
Note
|
||||
</label>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
key={star}
|
||||
type="button"
|
||||
onClick={() => setRating(star)}
|
||||
disabled={submitting}
|
||||
className={`text-4xl transition-transform hover:scale-110 disabled:hover:scale-100 ${
|
||||
star <= rating
|
||||
? "text-pixel-gold"
|
||||
: "text-gray-600 hover:text-gray-500"
|
||||
}`}
|
||||
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
|
||||
>
|
||||
★
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs text-center mt-2">
|
||||
{rating > 0 && `${rating}/5`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Comment */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="comment"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Commentaire (optionnel)
|
||||
</label>
|
||||
<textarea
|
||||
id="comment"
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
rows={6}
|
||||
maxLength={1000}
|
||||
disabled={submitting}
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none disabled:opacity-50"
|
||||
placeholder="Partagez votre expérience, vos suggestions..."
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1 text-right">
|
||||
{comment.length}/1000 caractères
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || rating === 0}
|
||||
className="w-full px-6 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{submitting
|
||||
? "Enregistrement..."
|
||||
: existingFeedback
|
||||
? "Modifier le feedback"
|
||||
: "Envoyer le feedback"}
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<SectionTitle variant="gradient" size="lg">
|
||||
FEEDBACK
|
||||
</SectionTitle>
|
||||
<CloseButton onClick={handleClose} disabled={submitting} size="lg" />
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-white text-center py-8">Chargement...</div>
|
||||
) : !event ? (
|
||||
<Alert variant="error" className="text-center">
|
||||
Événement introuvable
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-gray-400 text-sm text-center mb-2">
|
||||
{existingFeedback
|
||||
? "Modifier votre feedback pour"
|
||||
: "Donnez votre avis sur"}
|
||||
</p>
|
||||
<p className="text-pixel-gold text-lg font-semibold text-center mb-8">
|
||||
{event.name}
|
||||
</p>
|
||||
|
||||
{success && (
|
||||
<Alert variant="success" className="mb-6">
|
||||
Feedback enregistré avec succès !
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="error" className="mb-6">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Rating */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">
|
||||
Note
|
||||
</label>
|
||||
<StarRating
|
||||
value={rating}
|
||||
onChange={setRating}
|
||||
disabled={submitting}
|
||||
size="lg"
|
||||
showValue
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Comment */}
|
||||
<Textarea
|
||||
id="comment"
|
||||
label="Commentaire (optionnel)"
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
rows={6}
|
||||
maxLength={1000}
|
||||
disabled={submitting}
|
||||
showCharCount
|
||||
placeholder="Partagez votre expérience, vos suggestions..."
|
||||
/>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
disabled={submitting || rating === 0}
|
||||
className="w-full"
|
||||
>
|
||||
{submitting
|
||||
? "Enregistrement..."
|
||||
: existingFeedback
|
||||
? "Modifier le feedback"
|
||||
: "Envoyer le feedback"}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Button, BackgroundSection } from "@/components/ui";
|
||||
|
||||
interface HeroSectionProps {
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
export default function HeroSection({ backgroundImage }: HeroSectionProps) {
|
||||
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80 z-[1]"></div>
|
||||
</div>
|
||||
|
||||
{/* Hero Content */}
|
||||
<div className="relative z-10 w-full max-w-5xl xl:max-w-6xl mx-auto px-4 sm:px-8 py-16 text-center flex flex-col items-center">
|
||||
<BackgroundSection backgroundImage={backgroundImage} className="pt-24">
|
||||
<div className="text-center flex flex-col items-center">
|
||||
{/* Game Title */}
|
||||
<div className="w-full flex justify-center mb-4 overflow-hidden">
|
||||
<h1 className="text-4xl sm:text-5xl md:text-8xl lg:text-9xl xl:text-9xl font-gaming font-black tracking-tight relative break-words">
|
||||
@@ -62,18 +50,22 @@ export default function HeroSection({ backgroundImage }: HeroSectionProps) {
|
||||
{/* Call-to-Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
|
||||
<Link href="/events">
|
||||
<button className="px-8 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition">
|
||||
<span>See events</span>
|
||||
</button>
|
||||
<Button variant="primary" size="lg">
|
||||
See events
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/leaderboard">
|
||||
<button className="px-8 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition flex items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<span>⏵</span>
|
||||
<span>See leaderboard</span>
|
||||
</button>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BackgroundSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef, type ChangeEvent } from "react";
|
||||
import { Input, Button, Card } from "@/components/ui";
|
||||
|
||||
interface ImageSelectorProps {
|
||||
value: string;
|
||||
@@ -119,20 +120,22 @@ export default function ImageSelector({
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
{/* Input URL */}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={urlInput}
|
||||
onChange={(e) => setUrlInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()}
|
||||
placeholder="https://example.com/image.jpg ou /image.jpg"
|
||||
className="flex-1 px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm min-w-0"
|
||||
className="flex-1 text-xs sm:text-sm px-3 py-2 min-w-0"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
onClick={handleUrlSubmit}
|
||||
className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap flex-shrink-0"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
URL
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Upload depuis le disque */}
|
||||
@@ -145,20 +148,25 @@ export default function ImageSelector({
|
||||
className="hidden"
|
||||
id={`file-${label}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`file-${label}`}
|
||||
className={`flex-1 px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition text-center cursor-pointer ${
|
||||
uploading ? "opacity-50 cursor-not-allowed" : ""
|
||||
}`}
|
||||
>
|
||||
{uploading ? "Upload..." : "Upload depuis le disque"}
|
||||
<label htmlFor={`file-${label}`}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
as="span"
|
||||
disabled={uploading}
|
||||
className="flex-1 text-center cursor-pointer"
|
||||
>
|
||||
{uploading ? "Upload..." : "Upload depuis le disque"}
|
||||
</Button>
|
||||
</label>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setShowGallery(!showGallery)}
|
||||
className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{showGallery ? "Masquer" : "Galerie"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Chemin de l'image */}
|
||||
@@ -170,7 +178,7 @@ export default function ImageSelector({
|
||||
|
||||
{/* Galerie d'images */}
|
||||
{showGallery && (
|
||||
<div className="mt-4 p-3 sm:p-4 bg-black/40 border border-pixel-gold/20 rounded">
|
||||
<Card variant="dark" className="mt-4 p-3 sm:p-4">
|
||||
<h4 className="text-xs sm:text-sm text-gray-300 mb-3">
|
||||
Images disponibles
|
||||
</h4>
|
||||
@@ -210,7 +218,7 @@ export default function ImageSelector({
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Avatar from "./Avatar";
|
||||
import { Avatar } from "@/components/ui";
|
||||
|
||||
interface LeaderboardEntry {
|
||||
rank: number;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Avatar from "./Avatar";
|
||||
import {
|
||||
Avatar,
|
||||
Modal,
|
||||
CloseButton,
|
||||
Card,
|
||||
BackgroundSection,
|
||||
SectionTitle,
|
||||
} from "@/components/ui";
|
||||
|
||||
interface LeaderboardEntry {
|
||||
rank: number;
|
||||
@@ -33,258 +40,222 @@ export default function LeaderboardSection({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center pt-24 pb-16">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
<BackgroundSection backgroundImage={backgroundImage}>
|
||||
{/* Title Section */}
|
||||
<SectionTitle
|
||||
variant="gradient"
|
||||
size="lg"
|
||||
subtitle="Top Players"
|
||||
className="mb-12 overflow-hidden"
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
</div>
|
||||
LEADERBOARD
|
||||
</SectionTitle>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm:px-8 py-16">
|
||||
{/* Title Section */}
|
||||
<div className="text-center mb-12 overflow-hidden">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-7xl font-gaming font-black mb-4 tracking-tight break-words">
|
||||
<span
|
||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
||||
style={{
|
||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
||||
}}
|
||||
>
|
||||
LEADERBOARD
|
||||
</span>
|
||||
</h1>
|
||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
|
||||
<span>✦</span>
|
||||
<span>Top Players</span>
|
||||
<span>✦</span>
|
||||
</div>
|
||||
{/* Leaderboard Table */}
|
||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
|
||||
{/* Header */}
|
||||
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
|
||||
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
|
||||
<div className="col-span-5 sm:col-span-6">Player</div>
|
||||
<div className="col-span-3 text-right">Score</div>
|
||||
<div className="col-span-2 text-right">Level</div>
|
||||
</div>
|
||||
|
||||
{/* Leaderboard Table */}
|
||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
|
||||
{/* Header */}
|
||||
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
|
||||
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
|
||||
<div className="col-span-5 sm:col-span-6">Player</div>
|
||||
<div className="col-span-3 text-right">Score</div>
|
||||
<div className="col-span-2 text-right">Level</div>
|
||||
</div>
|
||||
{/* Entries */}
|
||||
<div className="divide-y divide-pixel-gold/10 overflow-visible">
|
||||
{leaderboard.map((entry) => (
|
||||
<div
|
||||
key={entry.rank}
|
||||
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative ${
|
||||
entry.rank <= 3
|
||||
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
|
||||
: "bg-black/40"
|
||||
}`}
|
||||
>
|
||||
{/* Rank */}
|
||||
<div className="col-span-2 sm:col-span-1 flex items-center justify-center">
|
||||
<span
|
||||
className={`inline-flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-bold text-xs sm:text-sm ${
|
||||
entry.rank === 1
|
||||
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
|
||||
: entry.rank === 2
|
||||
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
|
||||
: entry.rank === 3
|
||||
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
|
||||
: "bg-gray-900 text-gray-400 border border-gray-800"
|
||||
}`}
|
||||
>
|
||||
{entry.rank}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Entries */}
|
||||
<div className="divide-y divide-pixel-gold/10 overflow-visible">
|
||||
{leaderboard.map((entry) => (
|
||||
<div
|
||||
key={entry.rank}
|
||||
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative ${
|
||||
entry.rank <= 3
|
||||
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
|
||||
: "bg-black/40"
|
||||
}`}
|
||||
>
|
||||
{/* Rank */}
|
||||
<div className="col-span-2 sm:col-span-1 flex items-center justify-center">
|
||||
{/* Player */}
|
||||
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
|
||||
<Avatar
|
||||
src={entry.avatar}
|
||||
username={entry.username}
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
borderClassName="border-pixel-gold/30"
|
||||
/>
|
||||
<div
|
||||
className="flex items-center gap-1 sm:gap-2 cursor-pointer hover:opacity-80 transition min-w-0"
|
||||
onClick={() => setSelectedEntry(entry)}
|
||||
>
|
||||
<span
|
||||
className={`inline-flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-bold text-xs sm:text-sm ${
|
||||
entry.rank === 1
|
||||
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
|
||||
: entry.rank === 2
|
||||
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
|
||||
: entry.rank === 3
|
||||
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
|
||||
: "bg-gray-900 text-gray-400 border border-gray-800"
|
||||
className={`font-bold text-xs sm:text-sm truncate ${
|
||||
entry.rank <= 3 ? "text-pixel-gold" : "text-white"
|
||||
}`}
|
||||
>
|
||||
{entry.rank}
|
||||
{entry.username}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Player */}
|
||||
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
|
||||
<Avatar
|
||||
src={entry.avatar}
|
||||
username={entry.username}
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
borderClassName="border-pixel-gold/30"
|
||||
/>
|
||||
<div
|
||||
className="flex items-center gap-1 sm:gap-2 cursor-pointer hover:opacity-80 transition min-w-0"
|
||||
onClick={() => setSelectedEntry(entry)}
|
||||
>
|
||||
<span
|
||||
className={`font-bold text-xs sm:text-sm truncate ${
|
||||
entry.rank <= 3 ? "text-pixel-gold" : "text-white"
|
||||
}`}
|
||||
>
|
||||
{entry.username}
|
||||
{entry.characterClass && (
|
||||
<span className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
[{entry.characterClass === "WARRIOR" && "⚔️"}
|
||||
{entry.characterClass === "MAGE" && "🔮"}
|
||||
{entry.characterClass === "ROGUE" && "🗡️"}
|
||||
{entry.characterClass === "RANGER" && "🏹"}
|
||||
{entry.characterClass === "PALADIN" && "🛡️"}
|
||||
{entry.characterClass === "ENGINEER" && "⚙️"}
|
||||
{entry.characterClass === "MERCHANT" && "💰"}
|
||||
{entry.characterClass === "SCHOLAR" && "📚"}
|
||||
{entry.characterClass === "BERSERKER" && "🔥"}
|
||||
{entry.characterClass === "NECROMANCER" && "💀"}]
|
||||
</span>
|
||||
{entry.characterClass && (
|
||||
<span className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
[{entry.characterClass === "WARRIOR" && "⚔️"}
|
||||
{entry.characterClass === "MAGE" && "🔮"}
|
||||
{entry.characterClass === "ROGUE" && "🗡️"}
|
||||
{entry.characterClass === "RANGER" && "🏹"}
|
||||
{entry.characterClass === "PALADIN" && "🛡️"}
|
||||
{entry.characterClass === "ENGINEER" && "⚙️"}
|
||||
{entry.characterClass === "MERCHANT" && "💰"}
|
||||
{entry.characterClass === "SCHOLAR" && "📚"}
|
||||
{entry.characterClass === "BERSERKER" && "🔥"}
|
||||
{entry.characterClass === "NECROMANCER" && "💀"}]
|
||||
</span>
|
||||
)}
|
||||
{entry.rank <= 3 && (
|
||||
<span className="text-pixel-gold text-xs">✦</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Score */}
|
||||
<div className="col-span-3 flex items-center justify-end">
|
||||
<span className="font-mono text-gray-300 text-xs sm:text-sm">
|
||||
{formatScore(entry.score)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Level */}
|
||||
<div className="col-span-2 flex items-center justify-end">
|
||||
<span className="font-bold text-gray-400 text-xs sm:text-sm">
|
||||
Lv.{entry.level}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-gray-500 text-sm">
|
||||
Compete with players worldwide and climb the ranks!
|
||||
</p>
|
||||
<p className="text-gray-600 text-xs mt-2">
|
||||
Rankings update every hour
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Character Modal */}
|
||||
{selectedEntry && (
|
||||
<div
|
||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||||
onClick={() => setSelectedEntry(null)}
|
||||
>
|
||||
<div
|
||||
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-4 sm:p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
|
||||
{selectedEntry.username}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setSelectedEntry(null)}
|
||||
className="text-gray-400 hover:text-pixel-gold text-2xl font-bold transition"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Avatar and Class */}
|
||||
<div className="flex items-center gap-4 sm:gap-6 mb-6">
|
||||
<Avatar
|
||||
src={selectedEntry.avatar}
|
||||
username={selectedEntry.username}
|
||||
size="lg"
|
||||
className="flex-shrink-0"
|
||||
borderClassName="border-2 sm:border-4 border-pixel-gold/50"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-2">
|
||||
Rank #{selectedEntry.rank}
|
||||
</div>
|
||||
<div className="text-sm text-gray-300 mb-2">
|
||||
{selectedEntry.email}
|
||||
</div>
|
||||
{selectedEntry.characterClass && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">
|
||||
{selectedEntry.characterClass === "WARRIOR" && "⚔️"}
|
||||
{selectedEntry.characterClass === "MAGE" && "🔮"}
|
||||
{selectedEntry.characterClass === "ROGUE" && "🗡️"}
|
||||
{selectedEntry.characterClass === "RANGER" && "🏹"}
|
||||
{selectedEntry.characterClass === "PALADIN" && "🛡️"}
|
||||
{selectedEntry.characterClass === "ENGINEER" && "⚙️"}
|
||||
{selectedEntry.characterClass === "MERCHANT" && "💰"}
|
||||
{selectedEntry.characterClass === "SCHOLAR" && "📚"}
|
||||
{selectedEntry.characterClass === "BERSERKER" && "🔥"}
|
||||
{selectedEntry.characterClass === "NECROMANCER" && "💀"}
|
||||
</span>
|
||||
<span className="text-lg font-bold text-pixel-gold uppercase tracking-wider">
|
||||
{selectedEntry.characterClass === "WARRIOR" &&
|
||||
"Guerrier"}
|
||||
{selectedEntry.characterClass === "MAGE" && "Mage"}
|
||||
{selectedEntry.characterClass === "ROGUE" && "Voleur"}
|
||||
{selectedEntry.characterClass === "RANGER" && "Rôdeur"}
|
||||
{selectedEntry.characterClass === "PALADIN" &&
|
||||
"Paladin"}
|
||||
{selectedEntry.characterClass === "ENGINEER" &&
|
||||
"Ingénieur"}
|
||||
{selectedEntry.characterClass === "MERCHANT" &&
|
||||
"Marchand"}
|
||||
{selectedEntry.characterClass === "SCHOLAR" && "Érudit"}
|
||||
{selectedEntry.characterClass === "BERSERKER" &&
|
||||
"Berserker"}
|
||||
{selectedEntry.characterClass === "NECROMANCER" &&
|
||||
"Nécromancien"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{entry.rank <= 3 && (
|
||||
<span className="text-pixel-gold text-xs">✦</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="bg-black/60 border border-pixel-gold/30 rounded p-4">
|
||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
||||
Score
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-pixel-gold">
|
||||
{formatScore(selectedEntry.score)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-black/60 border border-pixel-gold/30 rounded p-4">
|
||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
||||
Niveau
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-pixel-gold">
|
||||
Lv.{selectedEntry.level}
|
||||
</div>
|
||||
</div>
|
||||
{/* Score */}
|
||||
<div className="col-span-3 flex items-center justify-end">
|
||||
<span className="font-mono text-gray-300 text-xs sm:text-sm">
|
||||
{formatScore(entry.score)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bio */}
|
||||
{selectedEntry.bio && (
|
||||
<div className="border-t border-pixel-gold/30 pt-6">
|
||||
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-3 font-bold">
|
||||
Bio
|
||||
</div>
|
||||
<p className="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
|
||||
{selectedEntry.bio}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* Level */}
|
||||
<div className="col-span-2 flex items-center justify-end">
|
||||
<span className="font-bold text-gray-400 text-xs sm:text-sm">
|
||||
Lv.{entry.level}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-gray-500 text-sm">
|
||||
Compete with players worldwide and climb the ranks!
|
||||
</p>
|
||||
<p className="text-gray-600 text-xs mt-2">Rankings update every hour</p>
|
||||
</div>
|
||||
|
||||
{/* Character Modal */}
|
||||
{selectedEntry && (
|
||||
<Modal
|
||||
isOpen={!!selectedEntry}
|
||||
onClose={() => setSelectedEntry(null)}
|
||||
size="md"
|
||||
>
|
||||
<div className="p-4 sm:p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
|
||||
{selectedEntry.username}
|
||||
</h2>
|
||||
<CloseButton onClick={() => setSelectedEntry(null)} size="md" />
|
||||
</div>
|
||||
|
||||
{/* Avatar and Class */}
|
||||
<div className="flex items-center gap-4 sm:gap-6 mb-6">
|
||||
<Avatar
|
||||
src={selectedEntry.avatar}
|
||||
username={selectedEntry.username}
|
||||
size="lg"
|
||||
className="flex-shrink-0"
|
||||
borderClassName="border-2 sm:border-4 border-pixel-gold/50"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-2">
|
||||
Rank #{selectedEntry.rank}
|
||||
</div>
|
||||
<div className="text-sm text-gray-300 mb-2">
|
||||
{selectedEntry.email}
|
||||
</div>
|
||||
{selectedEntry.characterClass && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">
|
||||
{selectedEntry.characterClass === "WARRIOR" && "⚔️"}
|
||||
{selectedEntry.characterClass === "MAGE" && "🔮"}
|
||||
{selectedEntry.characterClass === "ROGUE" && "🗡️"}
|
||||
{selectedEntry.characterClass === "RANGER" && "🏹"}
|
||||
{selectedEntry.characterClass === "PALADIN" && "🛡️"}
|
||||
{selectedEntry.characterClass === "ENGINEER" && "⚙️"}
|
||||
{selectedEntry.characterClass === "MERCHANT" && "💰"}
|
||||
{selectedEntry.characterClass === "SCHOLAR" && "📚"}
|
||||
{selectedEntry.characterClass === "BERSERKER" && "🔥"}
|
||||
{selectedEntry.characterClass === "NECROMANCER" && "💀"}
|
||||
</span>
|
||||
<span className="text-lg font-bold text-pixel-gold uppercase tracking-wider">
|
||||
{selectedEntry.characterClass === "WARRIOR" && "Guerrier"}
|
||||
{selectedEntry.characterClass === "MAGE" && "Mage"}
|
||||
{selectedEntry.characterClass === "ROGUE" && "Voleur"}
|
||||
{selectedEntry.characterClass === "RANGER" && "Rôdeur"}
|
||||
{selectedEntry.characterClass === "PALADIN" && "Paladin"}
|
||||
{selectedEntry.characterClass === "ENGINEER" &&
|
||||
"Ingénieur"}
|
||||
{selectedEntry.characterClass === "MERCHANT" &&
|
||||
"Marchand"}
|
||||
{selectedEntry.characterClass === "SCHOLAR" && "Érudit"}
|
||||
{selectedEntry.characterClass === "BERSERKER" &&
|
||||
"Berserker"}
|
||||
{selectedEntry.characterClass === "NECROMANCER" &&
|
||||
"Nécromancien"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<Card variant="default" className="p-4">
|
||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
||||
Score
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-pixel-gold">
|
||||
{formatScore(selectedEntry.score)}
|
||||
</div>
|
||||
</Card>
|
||||
<Card variant="default" className="p-4">
|
||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
||||
Niveau
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-pixel-gold">
|
||||
Lv.{selectedEntry.level}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Bio */}
|
||||
{selectedEntry.bio && (
|
||||
<div className="border-t border-pixel-gold/30 pt-6">
|
||||
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-3 font-bold">
|
||||
Bio
|
||||
</div>
|
||||
<p className="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
|
||||
{selectedEntry.bio}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</section>
|
||||
</BackgroundSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useSession, signOut } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import PlayerStats from "./PlayerStats";
|
||||
import { Button } from "@/components/ui";
|
||||
|
||||
interface UserData {
|
||||
username: string;
|
||||
@@ -100,12 +101,14 @@ export default function Navigation({
|
||||
{/* Desktop Auth Buttons */}
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
{isAuthenticated ? (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => signOut()}
|
||||
className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs font-normal"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
@@ -114,11 +117,10 @@ export default function Navigation({
|
||||
>
|
||||
Connexion
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
||||
>
|
||||
Inscription
|
||||
<Link href="/register">
|
||||
<Button variant="primary" size="sm" className="text-xs">
|
||||
Inscription
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
@@ -197,15 +199,17 @@ export default function Navigation({
|
||||
{/* Mobile Auth Buttons */}
|
||||
<div className="flex flex-col gap-3 pt-2 border-t border-gray-800/30">
|
||||
{isAuthenticated ? (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
signOut();
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest text-left py-2"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs font-normal text-left py-2"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
@@ -215,12 +219,14 @@ export default function Navigation({
|
||||
>
|
||||
Connexion
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition text-center"
|
||||
>
|
||||
Inscription
|
||||
<Link href="/register" onClick={() => setIsMenuOpen(false)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="text-xs w-full text-center"
|
||||
>
|
||||
Inscription
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import Avatar from "./Avatar";
|
||||
import { Avatar } from "@/components/ui";
|
||||
|
||||
interface UserData {
|
||||
username: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useTransition, type ChangeEvent } from "react";
|
||||
import Avatar from "./Avatar";
|
||||
import { Avatar, Input, Textarea, Button, Alert, Card, BackgroundSection, SectionTitle, ProgressBar } from "@/components/ui";
|
||||
import { updateProfile } from "@/actions/profile/update-profile";
|
||||
import { updatePassword } from "@/actions/profile/update-password";
|
||||
|
||||
@@ -170,53 +170,19 @@ export default function ProfileForm({
|
||||
: "from-red-700 to-red-900";
|
||||
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full max-w-4xl mx-auto px-8 py-16">
|
||||
<BackgroundSection backgroundImage={backgroundImage}>
|
||||
<div className="w-full max-w-4xl mx-auto px-8">
|
||||
{/* Title Section */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
|
||||
<span
|
||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
||||
style={{
|
||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
||||
}}
|
||||
>
|
||||
PROFIL
|
||||
</span>
|
||||
</h1>
|
||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
|
||||
<span>✦</span>
|
||||
<span>Gérez votre profil</span>
|
||||
<span>✦</span>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTitle variant="gradient" size="lg" subtitle="Gérez votre profil" className="mb-12">
|
||||
PROFIL
|
||||
</SectionTitle>
|
||||
|
||||
{/* Profile Card */}
|
||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm">
|
||||
<Card variant="default" className="overflow-hidden">
|
||||
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
||||
{/* Messages */}
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
{success && <Alert variant="success">{success}</Alert>}
|
||||
|
||||
{/* Avatar Section */}
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
@@ -281,51 +247,38 @@ export default function ProfileForm({
|
||||
className="hidden"
|
||||
id="avatar-upload"
|
||||
/>
|
||||
<label
|
||||
htmlFor="avatar-upload"
|
||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
|
||||
>
|
||||
{uploadingAvatar
|
||||
? "Upload en cours..."
|
||||
: "Upload un avatar custom"}
|
||||
<label htmlFor="avatar-upload">
|
||||
<Button variant="primary" size="md" as="span" className="cursor-pointer">
|
||||
{uploadingAvatar ? "Upload en cours..." : "Upload un avatar custom"}
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Nom d'utilisateur
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={20}
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
label="Nom d'utilisateur"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={20}
|
||||
className="bg-black/40"
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
||||
|
||||
{/* Bio Field */}
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
value={bio || ""}
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition resize-none"
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
placeholder="Parlez-nous de vous..."
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
{(bio || "").length}/500 caractères
|
||||
</p>
|
||||
</div>
|
||||
<Textarea
|
||||
label="Bio"
|
||||
value={bio || ""}
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
showCharCount
|
||||
placeholder="Parlez-nous de vous..."
|
||||
className="bg-black/40"
|
||||
/>
|
||||
|
||||
{/* Character Class Selection */}
|
||||
<div>
|
||||
@@ -458,36 +411,23 @@ export default function ProfileForm({
|
||||
</div>
|
||||
|
||||
{/* HP Bar */}
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
||||
<span>HP</span>
|
||||
<span>
|
||||
{profile.hp} / {profile.maxHp}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-3 bg-gray-900 border border-gray-700 rounded overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-r ${hpColor} transition-all duration-1000 ease-out`}
|
||||
style={{ width: `${hpPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ProgressBar
|
||||
value={profile.hp}
|
||||
max={profile.maxHp}
|
||||
variant="hp"
|
||||
showLabel
|
||||
label="HP"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
{/* XP Bar */}
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
||||
<span>XP</span>
|
||||
<span>
|
||||
{formatNumber(profile.xp)} / {formatNumber(profile.maxXp)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-3 bg-gray-900 border border-pixel-gold/30 rounded overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80 transition-all duration-1000 ease-out"
|
||||
style={{ width: `${xpPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ProgressBar
|
||||
value={profile.xp}
|
||||
max={profile.maxXp}
|
||||
variant="xp"
|
||||
showLabel
|
||||
label="XP"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email (read-only) */}
|
||||
@@ -505,13 +445,9 @@ export default function ProfileForm({
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="px-6 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Button type="submit" variant="primary" size="md" disabled={isPending}>
|
||||
{isPending ? "Enregistrement..." : "Enregistrer les modifications"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -522,65 +458,56 @@ export default function ProfileForm({
|
||||
Mot de passe
|
||||
</h3>
|
||||
{!showPasswordForm && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="md"
|
||||
onClick={() => setShowPasswordForm(true)}
|
||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
||||
>
|
||||
Changer le mot de passe
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPasswordForm && (
|
||||
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Mot de passe actuel
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
label="Mot de passe actuel"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
required
|
||||
className="bg-black/40"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
||||
required
|
||||
minLength={6}
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
Minimum 6 caractères
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
label="Nouveau mot de passe"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
className="bg-black/40"
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
Minimum 6 caractères
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Confirmer le nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
||||
required
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
label="Confirmer le nouveau mot de passe"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
className="bg-black/40"
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
setShowPasswordForm(false);
|
||||
setCurrentPassword("");
|
||||
@@ -588,25 +515,25 @@ export default function ProfileForm({
|
||||
setConfirmPassword("");
|
||||
setError(null);
|
||||
}}
|
||||
className="px-4 py-2 border border-gray-600/50 bg-black/40 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/40 hover:border-gray-500 transition"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="md"
|
||||
disabled={isChangingPassword}
|
||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isChangingPassword
|
||||
? "Modification..."
|
||||
: "Modifier le mot de passe"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</BackgroundSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useTransition } from "react";
|
||||
import Avatar from "./Avatar";
|
||||
import { Avatar, Input, Button, Card } from "@/components/ui";
|
||||
import { updateUser, deleteUser } from "@/actions/admin/users";
|
||||
|
||||
interface User {
|
||||
@@ -184,10 +184,7 @@ export default function UserManagement() {
|
||||
: user.username;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={user.id}
|
||||
className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4"
|
||||
>
|
||||
<Card key={user.id} variant="default" className="p-3 sm:p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-2">
|
||||
<div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0">
|
||||
{/* Avatar */}
|
||||
@@ -248,23 +245,19 @@ export default function UserManagement() {
|
||||
{isEditing ? (
|
||||
<div className="space-y-4">
|
||||
{/* Username Section */}
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-2">
|
||||
Nom d'utilisateur
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingUser.username || ""}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
username: e.target.value,
|
||||
})
|
||||
}
|
||||
className="w-full px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
||||
placeholder="Nom d'utilisateur"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
label="Nom d'utilisateur"
|
||||
value={editingUser.username || ""}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
username: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Nom d'utilisateur"
|
||||
className="text-xs sm:text-sm px-2 sm:px-3 py-1"
|
||||
/>
|
||||
|
||||
{/* Avatar Section */}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
@@ -372,13 +365,17 @@ export default function UserManagement() {
|
||||
className="hidden"
|
||||
id={`avatar-upload-${user.id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`avatar-upload-${user.id}`}
|
||||
className="px-3 sm:px-4 py-1.5 border border-pixel-gold/50 bg-black/40 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
|
||||
>
|
||||
{uploadingAvatar === user.id
|
||||
? "Upload en cours..."
|
||||
: "Upload un avatar custom"}
|
||||
<label htmlFor={`avatar-upload-${user.id}`}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
as="span"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{uploadingAvatar === user.id
|
||||
? "Upload en cours..."
|
||||
: "Upload un avatar custom"}
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -690,19 +687,21 @@ export default function UserManagement() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2 pt-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="success"
|
||||
size="md"
|
||||
disabled={saving}
|
||||
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Enregistrement..." : "Enregistrer"}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
|
||||
variant="secondary"
|
||||
size="md"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -747,7 +746,7 @@ export default function UserManagement() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
32
components/ui/Alert.tsx
Normal file
32
components/ui/Alert.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface AlertProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
variant?: "success" | "error" | "warning" | "info";
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
success: "bg-green-900/50 border-green-500/50 text-green-400",
|
||||
error: "bg-red-900/50 border-red-500/50 text-red-400",
|
||||
warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
|
||||
info: "bg-blue-900/50 border-blue-500/50 text-blue-400",
|
||||
};
|
||||
|
||||
export default function Alert({
|
||||
children,
|
||||
variant = "info",
|
||||
className = "",
|
||||
...props
|
||||
}: AlertProps) {
|
||||
return (
|
||||
<div
|
||||
className={`border rounded px-4 py-3 text-sm ${variantClasses[variant]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
43
components/ui/BackgroundSection.tsx
Normal file
43
components/ui/BackgroundSection.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface BackgroundSectionProps extends HTMLAttributes<HTMLElement> {
|
||||
children: ReactNode;
|
||||
backgroundImage: string;
|
||||
overlay?: boolean;
|
||||
}
|
||||
|
||||
export default function BackgroundSection({
|
||||
children,
|
||||
backgroundImage,
|
||||
overlay = true,
|
||||
className = "",
|
||||
...props
|
||||
}: BackgroundSectionProps) {
|
||||
return (
|
||||
<section
|
||||
className={`relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
{overlay && (
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm:px-8 py-16">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
40
components/ui/Badge.tsx
Normal file
40
components/ui/Badge.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
children: ReactNode;
|
||||
variant?: "default" | "success" | "warning" | "danger" | "info";
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
default: "bg-pixel-gold/20 border-pixel-gold/50 text-pixel-gold",
|
||||
success: "bg-green-900/50 border-green-500/50 text-green-400",
|
||||
warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
|
||||
danger: "bg-red-900/50 border-red-500/50 text-red-400",
|
||||
info: "bg-blue-900/30 border-blue-500/50 text-blue-400",
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "px-2 py-1 text-[10px] sm:text-xs",
|
||||
md: "px-3 py-1 text-xs",
|
||||
};
|
||||
|
||||
export default function Badge({
|
||||
children,
|
||||
variant = "default",
|
||||
size = "sm",
|
||||
className = "",
|
||||
...props
|
||||
}: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-block border uppercase rounded whitespace-nowrap flex-shrink-0 ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
47
components/ui/Button.tsx
Normal file
47
components/ui/Button.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { ButtonHTMLAttributes, ReactNode, ElementType } from "react";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "success" | "danger" | "ghost";
|
||||
size?: "sm" | "md" | "lg";
|
||||
children: ReactNode;
|
||||
as?: ElementType;
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
"border-pixel-gold/50 bg-black/60 text-white hover:bg-pixel-gold/10 hover:border-pixel-gold",
|
||||
secondary:
|
||||
"border-gray-600/50 bg-gray-900/20 text-gray-400 hover:bg-gray-900/30 hover:border-gray-500",
|
||||
success:
|
||||
"border-green-500/50 bg-green-900/20 text-green-400 hover:bg-green-900/30",
|
||||
danger: "border-red-500/50 bg-red-900/20 text-red-400 hover:bg-red-900/30",
|
||||
ghost: "border-transparent bg-transparent text-white hover:text-pixel-gold",
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "px-2 sm:px-3 py-1 text-[10px] sm:text-xs",
|
||||
md: "px-4 py-2 text-xs",
|
||||
lg: "px-6 py-3 text-sm",
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
className = "",
|
||||
disabled,
|
||||
children,
|
||||
as: Component = "button",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<Component
|
||||
className={`border uppercase tracking-widest rounded transition disabled:opacity-50 disabled:cursor-not-allowed ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
30
components/ui/Card.tsx
Normal file
30
components/ui/Card.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
variant?: "default" | "dark";
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
default: "bg-black/60 border border-pixel-gold/30",
|
||||
dark: "bg-black/80 border border-pixel-gold/30",
|
||||
};
|
||||
|
||||
export default function Card({
|
||||
children,
|
||||
variant = "default",
|
||||
className = "",
|
||||
...props
|
||||
}: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg backdrop-blur-sm ${variantClasses[variant]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
29
components/ui/CloseButton.tsx
Normal file
29
components/ui/CloseButton.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
|
||||
interface CloseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "text-xl",
|
||||
md: "text-2xl",
|
||||
lg: "text-3xl",
|
||||
};
|
||||
|
||||
export default function CloseButton({
|
||||
size = "md",
|
||||
className = "",
|
||||
...props
|
||||
}: CloseButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={`text-gray-400 hover:text-pixel-gold font-bold transition disabled:opacity-50 disabled:cursor-not-allowed ${sizeClasses[size]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
38
components/ui/Input.tsx
Normal file
38
components/ui/Input.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { InputHTMLAttributes, forwardRef } from "react";
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, className = "", ...props }, ref) => {
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-red-400 text-xs mt-1">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
||||
export default Input;
|
||||
|
||||
54
components/ui/Modal.tsx
Normal file
54
components/ui/Modal.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useEffect } from "react";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
closeOnOverlayClick?: boolean;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "max-w-md",
|
||||
md: "max-w-2xl",
|
||||
lg: "max-w-3xl",
|
||||
xl: "max-w-4xl",
|
||||
};
|
||||
|
||||
export default function Modal({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
size = "md",
|
||||
closeOnOverlayClick = true,
|
||||
}: ModalProps) {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||||
onClick={closeOnOverlayClick ? onClose : undefined}
|
||||
>
|
||||
<div
|
||||
className={`bg-black border-2 border-pixel-gold/70 rounded-lg w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto shadow-2xl`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
73
components/ui/ProgressBar.tsx
Normal file
73
components/ui/ProgressBar.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {
|
||||
value: number;
|
||||
max: number;
|
||||
variant?: "hp" | "xp" | "default";
|
||||
showLabel?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
hp: {
|
||||
high: "from-green-600 to-green-700",
|
||||
medium: "from-yellow-600 to-orange-700",
|
||||
low: "from-red-700 to-red-900",
|
||||
},
|
||||
xp: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
|
||||
default: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
|
||||
};
|
||||
|
||||
export default function ProgressBar({
|
||||
value,
|
||||
max,
|
||||
variant = "default",
|
||||
showLabel = false,
|
||||
label,
|
||||
className = "",
|
||||
...props
|
||||
}: ProgressBarProps) {
|
||||
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
|
||||
let gradientClass = "";
|
||||
if (variant === "hp") {
|
||||
if (percentage > 60) {
|
||||
gradientClass = variantClasses.hp.high;
|
||||
} else if (percentage > 30) {
|
||||
gradientClass = variantClasses.hp.medium;
|
||||
} else {
|
||||
gradientClass = variantClasses.hp.low;
|
||||
}
|
||||
} else if (variant === "xp") {
|
||||
gradientClass = variantClasses.xp;
|
||||
} else {
|
||||
gradientClass = variantClasses.default;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{showLabel && (
|
||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
||||
<span>{label || variant.toUpperCase()}</span>
|
||||
<span>
|
||||
{value} / {max}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative h-2 sm:h-3 bg-gray-900 border border-gray-700 rounded overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-r ${gradientClass} transition-all duration-1000 ease-out`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"></div>
|
||||
</div>
|
||||
{variant === "hp" && percentage < 30 && (
|
||||
<div className="absolute inset-0 border border-red-500 rounded animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
64
components/ui/SectionTitle.tsx
Normal file
64
components/ui/SectionTitle.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface SectionTitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
||||
children: ReactNode;
|
||||
variant?: "default" | "gradient" | "gold";
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
subtitle?: ReactNode;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "text-2xl sm:text-3xl",
|
||||
md: "text-3xl sm:text-4xl md:text-5xl",
|
||||
lg: "text-4xl sm:text-5xl md:text-6xl lg:text-7xl",
|
||||
xl: "text-5xl md:text-7xl",
|
||||
};
|
||||
|
||||
export default function SectionTitle({
|
||||
children,
|
||||
variant = "default",
|
||||
size = "md",
|
||||
subtitle,
|
||||
className = "",
|
||||
...props
|
||||
}: SectionTitleProps) {
|
||||
const baseClasses = "font-gaming font-black tracking-tight mb-4";
|
||||
|
||||
let titleClasses = `${baseClasses} ${sizeClasses[size]} ${className}`;
|
||||
|
||||
if (variant === "gradient") {
|
||||
titleClasses += " bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent";
|
||||
} else if (variant === "gold") {
|
||||
titleClasses += " text-pixel-gold";
|
||||
} else {
|
||||
titleClasses += " text-white";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h1 className={titleClasses} {...props}>
|
||||
{variant === "gradient" ? (
|
||||
<span
|
||||
style={{
|
||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</h1>
|
||||
{subtitle && (
|
||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
|
||||
<span>✦</span>
|
||||
<span>{subtitle}</span>
|
||||
<span>✦</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
66
components/ui/StarRating.tsx
Normal file
66
components/ui/StarRating.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface StarRatingProps {
|
||||
value: number;
|
||||
onChange?: (rating: number) => void;
|
||||
disabled?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
showValue?: boolean;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "text-lg sm:text-xl",
|
||||
md: "text-2xl sm:text-3xl",
|
||||
lg: "text-4xl",
|
||||
};
|
||||
|
||||
export default function StarRating({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
size = "md",
|
||||
showValue = false,
|
||||
}: StarRatingProps) {
|
||||
const [hoverValue, setHoverValue] = useState(0);
|
||||
|
||||
const handleClick = (rating: number) => {
|
||||
if (!disabled && onChange) {
|
||||
onChange(rating);
|
||||
}
|
||||
};
|
||||
|
||||
const displayValue = hoverValue || value;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
key={star}
|
||||
type="button"
|
||||
onClick={() => handleClick(star)}
|
||||
disabled={disabled}
|
||||
onMouseEnter={() => !disabled && setHoverValue(star)}
|
||||
onMouseLeave={() => !disabled && setHoverValue(0)}
|
||||
className={`transition-transform hover:scale-110 disabled:hover:scale-100 disabled:cursor-not-allowed ${
|
||||
star <= displayValue
|
||||
? "text-pixel-gold"
|
||||
: "text-gray-600 hover:text-gray-500"
|
||||
} ${sizeClasses[size]}`}
|
||||
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
|
||||
>
|
||||
★
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{showValue && value > 0 && (
|
||||
<p className="text-gray-500 text-xs text-center">
|
||||
{value}/5
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
49
components/ui/Textarea.tsx
Normal file
49
components/ui/Textarea.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { TextareaHTMLAttributes, forwardRef } from "react";
|
||||
|
||||
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
showCharCount?: boolean;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ label, error, showCharCount, maxLength, className = "", value, ...props }, ref) => {
|
||||
const charCount = typeof value === "string" ? value.length : 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
ref={ref}
|
||||
maxLength={maxLength}
|
||||
value={value}
|
||||
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{showCharCount && maxLength && (
|
||||
<p className="text-gray-500 text-xs mt-1 text-right">
|
||||
{charCount}/{maxLength} caractères
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p className="text-red-400 text-xs mt-1">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export default Textarea;
|
||||
|
||||
14
components/ui/index.ts
Normal file
14
components/ui/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export { default as Avatar } from "./Avatar";
|
||||
export { default as Button } from "./Button";
|
||||
export { default as Input } from "./Input";
|
||||
export { default as Textarea } from "./Textarea";
|
||||
export { default as Card } from "./Card";
|
||||
export { default as Modal } from "./Modal";
|
||||
export { default as Badge } from "./Badge";
|
||||
export { default as ProgressBar } from "./ProgressBar";
|
||||
export { default as StarRating } from "./StarRating";
|
||||
export { default as SectionTitle } from "./SectionTitle";
|
||||
export { default as BackgroundSection } from "./BackgroundSection";
|
||||
export { default as Alert } from "./Alert";
|
||||
export { default as CloseButton } from "./CloseButton";
|
||||
|
||||
Reference in New Issue
Block a user