Add User Management component to admin page, replacing placeholder text with functional UI for user management.
This commit is contained in:
@@ -5,6 +5,7 @@ import { useSession } from "next-auth/react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Navigation from "@/components/Navigation";
|
import Navigation from "@/components/Navigation";
|
||||||
import ImageSelector from "@/components/ImageSelector";
|
import ImageSelector from "@/components/ImageSelector";
|
||||||
|
import UserManagement from "@/components/UserManagement";
|
||||||
|
|
||||||
interface SitePreferences {
|
interface SitePreferences {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -241,9 +242,7 @@ export default function AdminPage() {
|
|||||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||||
Gestion des Utilisateurs
|
Gestion des Utilisateurs
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-center text-gray-400 py-8">
|
<UserManagement />
|
||||||
Section utilisateurs à venir...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
151
app/api/admin/users/[id]/route.ts
Normal file
151
app/api/admin/users/[id]/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
44
app/api/admin/users/route.ts
Normal file
44
app/api/admin/users/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
555
components/UserManagement.tsx
Normal file
555
components/UserManagement.tsx
Normal file
@@ -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<User[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [editingUser, setEditingUser] = useState<EditingUser | null>(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 (
|
||||||
|
<div className="text-center text-gray-400 py-8">Chargement...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{users.length === 0 ? (
|
||||||
|
<div className="text-center text-gray-400 py-8">
|
||||||
|
Aucun utilisateur trouvé
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
key={user.id}
|
||||||
|
className="bg-black/60 border border-pixel-gold/20 rounded p-4"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<div className="flex gap-4 items-start">
|
||||||
|
{/* Avatar */}
|
||||||
|
<div className="w-16 h-16 rounded-full border-2 border-pixel-gold/50 overflow-hidden bg-black/60 flex-shrink-0">
|
||||||
|
{user.avatar ? (
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.username}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.style.display = "none";
|
||||||
|
e.currentTarget.nextElementSibling?.classList.remove("hidden");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className={`w-full h-full flex items-center justify-center text-pixel-gold text-xl font-bold ${user.avatar ? "hidden" : ""}`}>
|
||||||
|
{user.username.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-pixel-gold font-bold text-lg">
|
||||||
|
{user.username}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-400 text-sm">{user.email}</p>
|
||||||
|
<div className="flex gap-4 mt-2 text-xs text-gray-500">
|
||||||
|
<span>Niveau {user.level}</span>
|
||||||
|
<span>Score: {formatNumber(user.score)}</span>
|
||||||
|
<span className={user.role === "ADMIN" ? "text-pixel-gold" : ""}>
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isEditing && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(user)}
|
||||||
|
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 transition"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* HP Section */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<label className="text-sm text-gray-300">
|
||||||
|
Points de Vie (HP)
|
||||||
|
</label>
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{previewHp} / {user.maxHp}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
hpDelta: editingUser.hpDelta - 100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||||
|
>
|
||||||
|
-100
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
hpDelta: editingUser.hpDelta - 10,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||||
|
>
|
||||||
|
-10
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editingUser.hpDelta || 0}
|
||||||
|
onChange={(e) =>
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
hpDelta: editingUser.hpDelta + 10,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||||
|
>
|
||||||
|
+10
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
hpDelta: editingUser.hpDelta + 100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||||
|
>
|
||||||
|
+100
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 h-2 bg-black/60 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-red-600 to-green-500 transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, (previewHp / user.maxHp) * 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* XP Section */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<label className="text-sm text-gray-300">
|
||||||
|
Expérience (XP)
|
||||||
|
</label>
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{formatNumber(previewXp)} / {formatNumber(user.maxXp)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
xpDelta: editingUser.xpDelta - 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||||
|
>
|
||||||
|
-1000
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
xpDelta: editingUser.xpDelta - 100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||||
|
>
|
||||||
|
-100
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editingUser.xpDelta || 0}
|
||||||
|
onChange={(e) =>
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
xpDelta: editingUser.xpDelta + 100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||||
|
>
|
||||||
|
+100
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
xpDelta: editingUser.xpDelta + 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||||
|
>
|
||||||
|
+1000
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 h-2 bg-black/60 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-blue-600 to-purple-500 transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, (previewXp / user.maxXp) * 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Score Section */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-300 mb-2">
|
||||||
|
Score
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
score: (editingUser.score || 0) - 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||||
|
>
|
||||||
|
-1000
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
score: (editingUser.score || 0) - 100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||||
|
>
|
||||||
|
-100
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editingUser.score ?? 0}
|
||||||
|
onChange={(e) =>
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
score: (editingUser.score || 0) + 100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||||
|
>
|
||||||
|
+100
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
score: (editingUser.score || 0) + 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||||
|
>
|
||||||
|
+1000
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Level Section */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-300 mb-2">
|
||||||
|
Niveau
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
level: Math.max(1, (editingUser.level || 1) - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||||
|
>
|
||||||
|
-1
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={editingUser.level ?? 1}
|
||||||
|
onChange={(e) =>
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
level: (editingUser.level || 1) + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||||
|
>
|
||||||
|
+1
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Role Section */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-300 mb-2">
|
||||||
|
Rôle
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
role: "USER",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className={`flex-1 px-4 py-2 border rounded text-xs uppercase tracking-widest transition ${
|
||||||
|
editingUser.role === "USER"
|
||||||
|
? "border-pixel-gold bg-pixel-gold/20 text-pixel-gold"
|
||||||
|
: "border-gray-600/50 bg-gray-900/20 text-gray-400 hover:bg-gray-900/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
USER
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setEditingUser({
|
||||||
|
...editingUser,
|
||||||
|
role: "ADMIN",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className={`flex-1 px-4 py-2 border rounded text-xs uppercase tracking-widest transition ${
|
||||||
|
editingUser.role === "ADMIN"
|
||||||
|
? "border-pixel-gold bg-pixel-gold/20 text-pixel-gold"
|
||||||
|
: "border-gray-600/50 bg-gray-900/20 text-gray-400 hover:bg-gray-900/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
ADMIN
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<span className="text-xs text-gray-400">HP</span>
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{user.hp} / {user.maxHp}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-black/60 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-red-600 to-green-500"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, (user.hp / user.maxHp) * 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<span className="text-xs text-gray-400">XP</span>
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{formatNumber(user.xp)} / {formatNumber(user.maxXp)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-black/60 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-blue-600 to-purple-500"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, (user.xp / user.maxXp) * 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user