diff --git a/app/admin/page.tsx b/app/admin/page.tsx index d9755fa..7276221 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -5,6 +5,7 @@ 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"; interface SitePreferences { id: string; @@ -241,9 +242,7 @@ export default function AdminPage() {

Gestion des Utilisateurs

-
- Section utilisateurs à venir... -
+ )} diff --git a/app/api/admin/users/[id]/route.ts b/app/api/admin/users/[id]/route.ts new file mode 100644 index 0000000..6233d0b --- /dev/null +++ b/app/api/admin/users/[id]/route.ts @@ -0,0 +1,151 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { Role } from "@/prisma/generated/prisma/client"; + +export async function PUT( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + + if (!session?.user || session.user.role !== Role.ADMIN) { + return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); + } + + const { id } = await params; + const body = await request.json(); + const { hpDelta, xpDelta, score, level, role } = body; + + // Récupérer l'utilisateur actuel + const user = await prisma.user.findUnique({ + where: { id }, + }); + + if (!user) { + return NextResponse.json( + { error: "Utilisateur non trouvé" }, + { status: 404 } + ); + } + + // Calculer les nouvelles valeurs + let newHp = user.hp; + let newXp = user.xp; + let newLevel = user.level; + let newMaxXp = user.maxXp; + + // Appliquer les changements de HP + if (hpDelta !== undefined) { + newHp = Math.max(0, Math.min(user.maxHp, user.hp + hpDelta)); + } + + // Appliquer les changements de XP + if (xpDelta !== undefined) { + newXp = user.xp + xpDelta; + newLevel = user.level; + newMaxXp = user.maxXp; + + // Gérer le niveau up si nécessaire (quand on ajoute de l'XP) + if (newXp >= newMaxXp && newXp > 0) { + while (newXp >= newMaxXp) { + newXp -= newMaxXp; + newLevel += 1; + // Augmenter le maxXp pour le prochain niveau (formule simple) + newMaxXp = Math.floor(newMaxXp * 1.2); + } + } + + // Gérer le niveau down si nécessaire (quand on enlève de l'XP) + if (newXp < 0 && newLevel > 1) { + while (newXp < 0 && newLevel > 1) { + newLevel -= 1; + // Calculer le maxXp du niveau précédent + newMaxXp = Math.floor(newMaxXp / 1.2); + newXp += newMaxXp; + } + // S'assurer que l'XP ne peut pas être négative + newXp = Math.max(0, newXp); + } + + // S'assurer que le niveau minimum est 1 + if (newLevel < 1) { + newLevel = 1; + newXp = 0; + } + } + + // Appliquer les changements directs (score, level, role) + const updateData: { + hp: number; + xp: number; + level: number; + maxXp: number; + score?: number; + role?: Role; + } = { + hp: newHp, + xp: newXp, + level: newLevel, + maxXp: newMaxXp, + }; + + if (score !== undefined) { + updateData.score = Math.max(0, score); + } + + if (level !== undefined) { + // Si le niveau est modifié directement, utiliser cette valeur + const targetLevel = Math.max(1, level); + updateData.level = targetLevel; + + // Recalculer le maxXp pour le nouveau niveau + // Formule: maxXp = 5000 * (1.2 ^ (level - 1)) + let calculatedMaxXp = 5000; + for (let i = 1; i < targetLevel; i++) { + calculatedMaxXp = Math.floor(calculatedMaxXp * 1.2); + } + updateData.maxXp = calculatedMaxXp; + + // Réinitialiser l'XP si le niveau change directement (sauf si on modifie aussi l'XP) + if (targetLevel !== user.level && xpDelta === undefined) { + updateData.xp = 0; + } + } + + if (role !== undefined) { + if (role === "ADMIN" || role === "USER") { + updateData.role = role as Role; + } + } + + // Mettre à jour l'utilisateur + const updatedUser = await prisma.user.update({ + where: { id }, + data: updateData, + select: { + id: true, + username: true, + email: true, + role: true, + score: true, + level: true, + hp: true, + maxHp: true, + xp: true, + maxXp: true, + avatar: true, + }, + }); + + return NextResponse.json(updatedUser); + } catch (error) { + console.error("Error updating user:", error); + return NextResponse.json( + { error: "Erreur lors de la mise à jour de l'utilisateur" }, + { status: 500 } + ); + } +} + diff --git a/app/api/admin/users/route.ts b/app/api/admin/users/route.ts new file mode 100644 index 0000000..9154d2d --- /dev/null +++ b/app/api/admin/users/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { Role } from "@/prisma/generated/prisma/client"; + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user || session.user.role !== Role.ADMIN) { + return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); + } + + // Récupérer tous les utilisateurs avec leurs stats + const users = await prisma.user.findMany({ + select: { + id: true, + username: true, + email: true, + role: true, + score: true, + level: true, + hp: true, + maxHp: true, + xp: true, + maxXp: true, + avatar: true, + createdAt: true, + }, + orderBy: { + score: "desc", + }, + }); + + return NextResponse.json(users); + } catch (error) { + console.error("Error fetching users:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des utilisateurs" }, + { status: 500 } + ); + } +} + diff --git a/components/UserManagement.tsx b/components/UserManagement.tsx new file mode 100644 index 0000000..3bce639 --- /dev/null +++ b/components/UserManagement.tsx @@ -0,0 +1,555 @@ +"use client"; + +import { useState, useEffect } from "react"; + +interface User { + id: string; + username: string; + email: string; + role: string; + score: number; + level: number; + hp: number; + maxHp: number; + xp: number; + maxXp: number; + avatar: string | null; + createdAt: string; +} + +interface EditingUser { + userId: string; + hpDelta: number; + xpDelta: number; + score: number | null; + level: number | null; + role: string | null; +} + +export default function UserManagement() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [editingUser, setEditingUser] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + fetchUsers(); + }, []); + + const fetchUsers = async () => { + try { + const response = await fetch("/api/admin/users"); + if (response.ok) { + const data = await response.json(); + setUsers(data); + } + } catch (error) { + console.error("Error fetching users:", error); + } finally { + setLoading(false); + } + }; + + const handleEdit = (user: User) => { + setEditingUser({ + userId: user.id, + hpDelta: 0, + xpDelta: 0, + score: user.score, + level: user.level, + role: user.role, + }); + }; + + const handleSave = async () => { + if (!editingUser) return; + + setSaving(true); + try { + const body: { + hpDelta?: number; + xpDelta?: number; + score?: number; + level?: number; + role?: string; + } = {}; + + if (editingUser.hpDelta !== 0) { + body.hpDelta = editingUser.hpDelta; + } + if (editingUser.xpDelta !== 0) { + body.xpDelta = editingUser.xpDelta; + } + if (editingUser.score !== null) { + body.score = editingUser.score; + } + if (editingUser.level !== null) { + body.level = editingUser.level; + } + if (editingUser.role !== null) { + body.role = editingUser.role; + } + + const response = await fetch(`/api/admin/users/${editingUser.userId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (response.ok) { + await fetchUsers(); + setEditingUser(null); + } else { + const error = await response.json(); + alert(error.error || "Erreur lors de la mise à jour"); + } + } catch (error) { + console.error("Error updating user:", error); + alert("Erreur lors de la mise à jour"); + } finally { + setSaving(false); + } + }; + + const handleCancel = () => { + setEditingUser(null); + }; + + const formatNumber = (num: number) => { + return num.toLocaleString("en-US"); + }; + + if (loading) { + return ( +
Chargement...
+ ); + } + + return ( +
+ {users.length === 0 ? ( +
+ Aucun utilisateur trouvé +
+ ) : ( + users.map((user) => { + const isEditing = editingUser?.userId === user.id; + const previewHp = isEditing + ? Math.max(0, Math.min(user.maxHp, user.hp + editingUser.hpDelta)) + : user.hp; + const previewXp = isEditing + ? Math.max(0, user.xp + editingUser.xpDelta) + : user.xp; + + return ( +
+
+
+ {/* Avatar */} +
+ {user.avatar ? ( + {user.username} { + e.currentTarget.style.display = "none"; + e.currentTarget.nextElementSibling?.classList.remove("hidden"); + }} + /> + ) : null} +
+ {user.username.charAt(0).toUpperCase()} +
+
+
+

+ {user.username} +

+

{user.email}

+
+ Niveau {user.level} + Score: {formatNumber(user.score)} + + {user.role} + +
+
+
+ {!isEditing && ( + + )} +
+ + {isEditing ? ( +
+ {/* HP Section */} +
+
+ + + {previewHp} / {user.maxHp} + +
+
+ + + + setEditingUser({ + ...editingUser, + hpDelta: parseInt(e.target.value) || 0, + }) + } + className="flex-1 px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm text-center" + /> + + +
+
+
+
+
+ + {/* XP Section */} +
+
+ + + {formatNumber(previewXp)} / {formatNumber(user.maxXp)} + +
+
+ + + + setEditingUser({ + ...editingUser, + xpDelta: parseInt(e.target.value) || 0, + }) + } + className="flex-1 px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm text-center" + /> + + +
+
+
+
+
+ + {/* Score Section */} +
+ +
+ + + + setEditingUser({ + ...editingUser, + score: parseInt(e.target.value) || 0, + }) + } + className="flex-1 px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm text-center" + /> + + +
+
+ + {/* Level Section */} +
+ +
+ + + setEditingUser({ + ...editingUser, + level: Math.max(1, parseInt(e.target.value) || 1), + }) + } + className="flex-1 px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm text-center" + /> + +
+
+ + {/* Role Section */} +
+ +
+ + +
+
+ +
+ + +
+
+ ) : ( +
+
+
+ HP + + {user.hp} / {user.maxHp} + +
+
+
+
+
+
+
+ XP + + {formatNumber(user.xp)} / {formatNumber(user.maxXp)} + +
+
+
+
+
+
+ )} +
+ ); + }) + )} +
+ ); +} +