diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 2409d22..a4bbac6 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,328 +1,42 @@ -"use client"; +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { Role } from "@/prisma/generated/prisma/client"; +import NavigationWrapper from "@/components/NavigationWrapper"; +import AdminPanel from "@/components/AdminPanel"; -import { useEffect, useState } from "react"; -import { useSession } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import Navigation from "@/components/Navigation"; -import ImageSelector from "@/components/ImageSelector"; -import UserManagement from "@/components/UserManagement"; -import EventManagement from "@/components/EventManagement"; +export default async function AdminPage() { + const session = await auth(); -interface SitePreferences { - id: string; - homeBackground: string | null; - eventsBackground: string | null; - leaderboardBackground: string | null; -} + if (!session?.user) { + redirect("/login"); + } -type AdminSection = "preferences" | "users" | "events"; + if (session.user.role !== Role.ADMIN) { + redirect("/"); + } -export default function AdminPage() { - const { data: session, status } = useSession(); - const router = useRouter(); - const [activeSection, setActiveSection] = - useState("preferences"); - const [preferences, setPreferences] = useState(null); - const [loading, setLoading] = useState(true); - const [isEditing, setIsEditing] = useState(false); - const [formData, setFormData] = useState({ - homeBackground: "", - eventsBackground: "", - leaderboardBackground: "", + // Récupérer les préférences globales du site + let sitePreferences = await prisma.sitePreferences.findUnique({ + where: { id: "global" }, }); - useEffect(() => { - if (status === "unauthenticated") { - router.push("/login"); - return; - } - - if (status === "authenticated" && session?.user?.role !== "ADMIN") { - router.push("/"); - return; - } - - if (status === "authenticated" && session?.user?.role === "ADMIN") { - fetchPreferences(); - } - }, [status, session, router]); - - const fetchPreferences = async () => { - try { - const response = await fetch("/api/admin/preferences"); - if (response.ok) { - const data = await response.json(); - setPreferences(data); - setFormData({ - homeBackground: data.homeBackground || "", - eventsBackground: data.eventsBackground || "", - leaderboardBackground: data.leaderboardBackground || "", - }); - } - } catch (error) { - console.error("Error fetching preferences:", error); - } finally { - setLoading(false); - } - }; - - const handleEdit = () => { - setIsEditing(true); - }; - - const handleSave = async () => { - try { - const response = await fetch("/api/admin/preferences", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(formData), - }); - - if (response.ok) { - await fetchPreferences(); - setIsEditing(false); - } - } catch (error) { - console.error("Error updating preferences:", error); - } - }; - - const handleCancel = () => { - setIsEditing(false); - if (preferences) { - setFormData({ - homeBackground: preferences.homeBackground || "", - eventsBackground: preferences.eventsBackground || "", - leaderboardBackground: preferences.leaderboardBackground || "", - }); - } - }; - - if (status === "loading" || loading) { - return ( -
- -
- Chargement... -
-
- ); + // Si elles n'existent pas, créer une entrée par défaut + if (!sitePreferences) { + sitePreferences = await prisma.sitePreferences.create({ + data: { + id: "global", + homeBackground: null, + eventsBackground: null, + leaderboardBackground: null, + }, + }); } return (
- -
-
-

- - ADMIN - -

- - {/* Navigation Tabs */} -
- - - -
- - {activeSection === "preferences" && ( -
-

- Préférences UI Globales -

-
-
-
-
-

- Images de fond du site -

-

- Ces préférences s'appliquent à tous les utilisateurs -

-
- {!isEditing && ( - - )} -
- - {isEditing ? ( -
- - setFormData({ - ...formData, - homeBackground: url, - }) - } - label="Background Home" - /> - - setFormData({ - ...formData, - eventsBackground: url, - }) - } - label="Background Events" - /> - - setFormData({ - ...formData, - leaderboardBackground: url, - }) - } - label="Background Leaderboard" - /> -
- - -
-
- ) : ( -
-
- - Home: - - {preferences?.homeBackground ? ( -
- Home background { - e.currentTarget.src = "/got-2.jpg"; - }} - /> - - {preferences.homeBackground} - -
- ) : ( - Par défaut - )} -
-
- - Events: - - {preferences?.eventsBackground ? ( -
- Events background { - e.currentTarget.src = "/got-2.jpg"; - }} - /> - - {preferences.eventsBackground} - -
- ) : ( - Par défaut - )} -
-
- - Leaderboard: - - {preferences?.leaderboardBackground ? ( -
- Leaderboard background { - e.currentTarget.src = "/got-2.jpg"; - }} - /> - - {preferences.leaderboardBackground} - -
- ) : ( - Par défaut - )} -
-
- )} -
-
-
- )} - - {activeSection === "users" && ( -
-

- Gestion des Utilisateurs -

- -
- )} - - {activeSection === "events" && ( -
-

- Gestion des Événements -

- -
- )} -
-
+ +
); } diff --git a/app/events/page.tsx b/app/events/page.tsx index bb7b473..8960bdd 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -1,12 +1,21 @@ -import Navigation from "@/components/Navigation"; +import NavigationWrapper from "@/components/NavigationWrapper"; import EventsPageSection from "@/components/EventsPageSection"; +import { prisma } from "@/lib/prisma"; +import { getBackgroundImage } from "@/lib/preferences"; + +export default async function EventsPage() { + const events = await prisma.event.findMany({ + orderBy: { + date: "asc", + }, + }); + + const backgroundImage = await getBackgroundImage("events", "/got-2.jpg"); -export default function EventsPage() { return (
- - + +
); } - diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx index 315b74b..07f623b 100644 --- a/app/leaderboard/page.tsx +++ b/app/leaderboard/page.tsx @@ -1,12 +1,51 @@ -import Navigation from "@/components/Navigation"; +import NavigationWrapper from "@/components/NavigationWrapper"; import LeaderboardSection from "@/components/LeaderboardSection"; +import { prisma } from "@/lib/prisma"; +import { getBackgroundImage } from "@/lib/preferences"; + +interface LeaderboardEntry { + rank: number; + username: string; + score: number; + level: number; + avatar: string | null; +} + +export default async function LeaderboardPage() { + const users = await prisma.user.findMany({ + orderBy: { + score: "desc", + }, + take: 10, + select: { + id: true, + username: true, + score: true, + level: true, + avatar: true, + }, + }); + + const leaderboard: LeaderboardEntry[] = users.map((user, index) => ({ + rank: index + 1, + username: user.username, + score: user.score, + level: user.level, + avatar: user.avatar, + })); + + const backgroundImage = await getBackgroundImage( + "leaderboard", + "/leaderboard-bg.jpg" + ); -export default function LeaderboardPage() { return (
- - + +
); } - diff --git a/app/page.tsx b/app/page.tsx index cb21750..670f317 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,21 @@ -import Navigation from "@/components/Navigation"; +import NavigationWrapper from "@/components/NavigationWrapper"; import HeroSection from "@/components/HeroSection"; import EventsSection from "@/components/EventsSection"; +import { prisma } from "@/lib/prisma"; + +export default async function Home() { + const events = await prisma.event.findMany({ + orderBy: { + date: "asc", + }, + take: 3, + }); -export default function Home() { return (
- + - +
); } diff --git a/app/profile/page.tsx b/app/profile/page.tsx index 1acbe1f..eedbd92 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -1,498 +1,44 @@ -"use client"; +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { getBackgroundImage } from "@/lib/preferences"; +import NavigationWrapper from "@/components/NavigationWrapper"; +import ProfileForm from "@/components/ProfileForm"; -import { useEffect, useState, useRef } from "react"; -import { useSession } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import Navigation from "@/components/Navigation"; -import { useBackgroundImage } from "@/hooks/usePreferences"; +export default async function ProfilePage() { + const session = await auth(); -interface UserProfile { - id: string; - email: string; - username: string; - avatar: string | null; - hp: number; - maxHp: number; - xp: number; - maxXp: number; - level: number; - score: number; - createdAt: string; -} - -const formatNumber = (num: number): string => { - return num.toLocaleString("en-US"); -}; - -export default function ProfilePage() { - const { data: session, status } = useSession(); - const router = useRouter(); - const backgroundImage = useBackgroundImage("home", "/got-background.jpg"); - const [profile, setProfile] = useState(null); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); - - const [username, setUsername] = useState(""); - const [avatar, setAvatar] = useState(null); - const fileInputRef = useRef(null); - const [uploadingAvatar, setUploadingAvatar] = useState(false); - - // Password change form state - const [showPasswordForm, setShowPasswordForm] = useState(false); - const [currentPassword, setCurrentPassword] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [changingPassword, setChangingPassword] = useState(false); - - useEffect(() => { - if (status === "unauthenticated") { - router.push("/login"); - return; - } - - if (status === "authenticated" && session?.user) { - fetchProfile(); - } - }, [status, session, router]); - - const fetchProfile = async () => { - try { - const response = await fetch("/api/profile"); - if (response.ok) { - const data = await response.json(); - setProfile(data); - setUsername(data.username); - setAvatar(data.avatar); - } else { - setError("Erreur lors du chargement du profil"); - } - } catch (err) { - console.error("Error fetching profile:", err); - setError("Erreur lors du chargement du profil"); - } finally { - setLoading(false); - } - }; - - const handleAvatarUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - - setUploadingAvatar(true); - setError(null); - - try { - const formData = new FormData(); - formData.append("file", file); - - const response = await fetch("/api/profile/avatar", { - method: "POST", - body: formData, - }); - - if (response.ok) { - const data = await response.json(); - setAvatar(data.url); - setSuccess("Avatar mis à jour avec succès"); - setTimeout(() => setSuccess(null), 3000); - } else { - const errorData = await response.json(); - setError(errorData.error || "Erreur lors de l'upload de l'avatar"); - } - } catch (err) { - console.error("Error uploading avatar:", err); - setError("Erreur lors de l'upload de l'avatar"); - } finally { - setUploadingAvatar(false); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - } - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setSaving(true); - setError(null); - setSuccess(null); - - try { - const response = await fetch("/api/profile", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username, - avatar, - }), - }); - - if (response.ok) { - const data = await response.json(); - setProfile(data); - setSuccess("Profil mis à jour avec succès"); - setTimeout(() => setSuccess(null), 3000); - } else { - const errorData = await response.json(); - setError(errorData.error || "Erreur lors de la mise à jour"); - } - } catch (err) { - console.error("Error updating profile:", err); - setError("Erreur lors de la mise à jour du profil"); - } finally { - setSaving(false); - } - }; - - const handlePasswordChange = async (e: React.FormEvent) => { - e.preventDefault(); - setChangingPassword(true); - setError(null); - setSuccess(null); - - try { - const response = await fetch("/api/profile/password", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - currentPassword, - newPassword, - confirmPassword, - }), - }); - - if (response.ok) { - setSuccess("Mot de passe modifié avec succès"); - setCurrentPassword(""); - setNewPassword(""); - setConfirmPassword(""); - setShowPasswordForm(false); - setTimeout(() => setSuccess(null), 3000); - } else { - const errorData = await response.json(); - setError(errorData.error || "Erreur lors de la modification du mot de passe"); - } - } catch (err) { - console.error("Error changing password:", err); - setError("Erreur lors de la modification du mot de passe"); - } finally { - setChangingPassword(false); - } - }; - - if (loading || status === "loading") { - return ( -
- -
-
Chargement...
-
-
- ); + if (!session?.user) { + redirect("/login"); } - if (!profile) { - return ( -
- -
-
Erreur lors du chargement du profil
-
-
- ); + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { + id: true, + email: true, + username: true, + avatar: true, + hp: true, + maxHp: true, + xp: true, + maxXp: true, + level: true, + score: true, + createdAt: true, + }, + }); + + if (!user) { + redirect("/login"); } - const hpPercentage = (profile.hp / profile.maxHp) * 100; - const xpPercentage = (profile.xp / profile.maxXp) * 100; - - const hpColor = - hpPercentage > 60 - ? "from-green-600 to-green-700" - : hpPercentage > 30 - ? "from-yellow-600 to-orange-700" - : "from-red-700 to-red-900"; + const backgroundImage = await getBackgroundImage("home", "/got-background.jpg"); return (
- -
- {/* Background Image */} -
- {/* Dark overlay for readability */} -
-
- - {/* Content */} -
- {/* Title Section */} -
-

- - PROFIL - -

-
- - Gérez votre profil - -
-
- - {/* Profile Card */} -
-
- {/* Messages */} - {error && ( -
- {error} -
- )} - {success && ( -
- {success} -
- )} - - {/* Avatar Section */} -
-
-
- {avatar ? ( - {username} - ) : ( - - {username.charAt(0).toUpperCase()} - - )} -
- {uploadingAvatar && ( -
-
Upload...
-
- )} -
-
- - -
-
- - {/* Username Field */} -
- - 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} - /> -

- 3-20 caractères -

-
- - {/* Stats Display */} -
-

- Statistiques -

- -
-
-
Score
-
- {formatNumber(profile.score)} -
-
-
-
Niveau
-
- Lv.{profile.level} -
-
-
- - {/* HP Bar */} -
-
- HP - {profile.hp} / {profile.maxHp} -
-
-
-
-
- - {/* XP Bar */} -
-
- XP - {formatNumber(profile.xp)} / {formatNumber(profile.maxXp)} -
-
-
-
-
-
- - {/* Email (read-only) */} -
- - -
- - {/* Submit Button */} -
- -
- - - {/* Password Change Section - Separate form */} -
-
-

- Mot de passe -

- {!showPasswordForm && ( - - )} -
- - {showPasswordForm && ( -
-
- - 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 - /> -
- -
- - 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} - /> -

- Minimum 6 caractères -

-
- -
- - 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} - /> -
- -
- - -
-
- )} -
-
-
-
+ +
); } diff --git a/components/AdminPanel.tsx b/components/AdminPanel.tsx new file mode 100644 index 0000000..cabd99e --- /dev/null +++ b/components/AdminPanel.tsx @@ -0,0 +1,281 @@ +"use client"; + +import { useState } from "react"; +import ImageSelector from "@/components/ImageSelector"; +import UserManagement from "@/components/UserManagement"; +import EventManagement from "@/components/EventManagement"; + +interface SitePreferences { + id: string; + homeBackground: string | null; + eventsBackground: string | null; + leaderboardBackground: string | null; +} + +interface AdminPanelProps { + initialPreferences: SitePreferences; +} + +type AdminSection = "preferences" | "users" | "events"; + +export default function AdminPanel({ initialPreferences }: AdminPanelProps) { + const [activeSection, setActiveSection] = + useState("preferences"); + const [preferences, setPreferences] = useState( + initialPreferences + ); + const [isEditing, setIsEditing] = useState(false); + const [formData, setFormData] = useState({ + homeBackground: initialPreferences.homeBackground || "", + eventsBackground: initialPreferences.eventsBackground || "", + leaderboardBackground: initialPreferences.leaderboardBackground || "", + }); + + const handleEdit = () => { + setIsEditing(true); + }; + + const handleSave = async () => { + try { + const response = await fetch("/api/admin/preferences", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + if (response.ok) { + const data = await response.json(); + setPreferences(data); + setIsEditing(false); + } + } catch (error) { + console.error("Error updating preferences:", error); + } + }; + + const handleCancel = () => { + setIsEditing(false); + if (preferences) { + setFormData({ + homeBackground: preferences.homeBackground || "", + eventsBackground: preferences.eventsBackground || "", + leaderboardBackground: preferences.leaderboardBackground || "", + }); + } + }; + + return ( +
+
+

+ + ADMIN + +

+ + {/* Navigation Tabs */} +
+ + + +
+ + {activeSection === "preferences" && ( +
+

+ Préférences UI Globales +

+
+
+
+
+

+ Images de fond du site +

+

+ Ces préférences s'appliquent à tous les utilisateurs +

+
+ {!isEditing && ( + + )} +
+ + {isEditing ? ( +
+ + setFormData({ + ...formData, + homeBackground: url, + }) + } + label="Background Home" + /> + + setFormData({ + ...formData, + eventsBackground: url, + }) + } + label="Background Events" + /> + + setFormData({ + ...formData, + leaderboardBackground: url, + }) + } + label="Background Leaderboard" + /> +
+ + +
+
+ ) : ( +
+
+ + Home: + + {preferences?.homeBackground ? ( +
+ Home background { + e.currentTarget.src = "/got-2.jpg"; + }} + /> + + {preferences.homeBackground} + +
+ ) : ( + Par défaut + )} +
+
+ + Events: + + {preferences?.eventsBackground ? ( +
+ Events background { + e.currentTarget.src = "/got-2.jpg"; + }} + /> + + {preferences.eventsBackground} + +
+ ) : ( + Par défaut + )} +
+
+ + Leaderboard: + + {preferences?.leaderboardBackground ? ( +
+ Leaderboard background { + e.currentTarget.src = "/got-2.jpg"; + }} + /> + + {preferences.leaderboardBackground} + +
+ ) : ( + Par défaut + )} +
+
+ )} +
+
+
+ )} + + {activeSection === "users" && ( +
+

+ Gestion des Utilisateurs +

+ +
+ )} + + {activeSection === "events" && ( +
+

+ Gestion des Événements +

+ +
+ )} +
+
+ ); +} + diff --git a/components/EventsPageSection.tsx b/components/EventsPageSection.tsx index 4e136f4..04b5eac 100644 --- a/components/EventsPageSection.tsx +++ b/components/EventsPageSection.tsx @@ -1,8 +1,5 @@ "use client"; -import { useEffect, useState } from "react"; -import { useBackgroundImage } from "@/hooks/usePreferences"; - interface Event { id: string; date: string; @@ -12,6 +9,11 @@ interface Event { status: "UPCOMING" | "LIVE" | "PAST"; } +interface EventsPageSectionProps { + events: Event[]; + backgroundImage: string; +} + const getEventTypeColor = (type: Event["type"]) => { switch (type) { case "SUMMIT": @@ -65,31 +67,10 @@ const getStatusBadge = (status: Event["status"]) => { } }; -export default function EventsPageSection() { - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const backgroundImage = useBackgroundImage("events", "/got-2.jpg"); - - useEffect(() => { - fetch("/api/events") - .then((res) => res.json()) - .then((data) => { - setEvents(data); - setLoading(false); - }) - .catch((err) => { - console.error("Error fetching events:", err); - setLoading(false); - }); - }, []); - - if (loading) { - return ( -
-
Chargement...
-
- ); - } +export default function EventsPageSection({ + events, + backgroundImage, +}: EventsPageSectionProps) { return (
{/* Background Image */} diff --git a/components/EventsSection.tsx b/components/EventsSection.tsx index bb680c1..e912c45 100644 --- a/components/EventsSection.tsx +++ b/components/EventsSection.tsx @@ -1,41 +1,14 @@ -"use client"; - -import { useEffect, useState } from "react"; - interface Event { id: string; date: string; name: string; } -export default function EventsSection() { - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetch("/api/events") - .then((res) => res.json()) - .then((data) => { - // Prendre seulement les 3 premiers événements pour la section d'accueil - setEvents(data.slice(0, 3)); - setLoading(false); - }) - .catch((err) => { - console.error("Error fetching events:", err); - setLoading(false); - }); - }, []); - - if (loading) { - return ( -
-
- Chargement... -
-
- ); - } +interface EventsSectionProps { + events: Event[]; +} +export default function EventsSection({ events }: EventsSectionProps) { if (events.length === 0) { return null; } diff --git a/components/LeaderboardSection.tsx b/components/LeaderboardSection.tsx index 9ef0260..5a13d79 100644 --- a/components/LeaderboardSection.tsx +++ b/components/LeaderboardSection.tsx @@ -1,8 +1,5 @@ "use client"; -import { useEffect, useState } from "react"; -import { useBackgroundImage } from "@/hooks/usePreferences"; - interface LeaderboardEntry { rank: number; username: string; @@ -11,39 +8,20 @@ interface LeaderboardEntry { avatar?: string | null; } +interface LeaderboardSectionProps { + leaderboard: LeaderboardEntry[]; + backgroundImage: string; +} + // Format number with consistent locale to avoid hydration mismatch const formatScore = (score: number): string => { return score.toLocaleString("en-US"); }; -export default function LeaderboardSection() { - const [leaderboard, setLeaderboard] = useState([]); - const [loading, setLoading] = useState(true); - const backgroundImage = useBackgroundImage( - "leaderboard", - "/leaderboard-bg.jpg" - ); - - useEffect(() => { - fetch("/api/leaderboard") - .then((res) => res.json()) - .then((data) => { - setLeaderboard(data); - setLoading(false); - }) - .catch((err) => { - console.error("Error fetching leaderboard:", err); - setLoading(false); - }); - }, []); - - if (loading) { - return ( -
-
Chargement...
-
- ); - } +export default function LeaderboardSection({ + leaderboard, + backgroundImage, +}: LeaderboardSectionProps) { return (
{/* Background Image */} diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 95cc029..c26d49f 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -4,9 +4,32 @@ import Link from "next/link"; import { useSession, signOut } from "next-auth/react"; import PlayerStats from "./PlayerStats"; -export default function Navigation() { +interface UserData { + username: string; + avatar: string | null; + hp: number; + maxHp: number; + xp: number; + maxXp: number; + level: number; +} + +interface NavigationProps { + initialUserData?: UserData | null; + initialIsAdmin?: boolean; +} + +export default function Navigation({ + initialUserData, + initialIsAdmin, +}: NavigationProps) { const { data: session } = useSession(); + // Utiliser initialUserData pour déterminer l'état de connexion pendant l'hydratation + // Cela évite le clignottement au reload + const isAuthenticated = initialUserData !== null || session !== null; + const isAdmin = initialIsAdmin ?? session?.user?.role === "ADMIN"; + return (