Refactor component imports and structure: Update import paths for various components to improve organization, moving them into appropriate subdirectories. Remove unused components related to user and event management, enhancing code clarity and maintainability across the application.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
This commit is contained in:
755
components/admin/UserManagement.tsx
Normal file
755
components/admin/UserManagement.tsx
Normal file
@@ -0,0 +1,755 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useTransition } from "react";
|
||||
import { Avatar, Input, Button, Card } from "@/components/ui";
|
||||
import { updateUser, deleteUser } from "@/actions/admin/users";
|
||||
|
||||
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;
|
||||
username: string | null;
|
||||
avatar: string | null;
|
||||
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);
|
||||
const [deletingUserId, setDeletingUserId] = useState<string | null>(null);
|
||||
const [, startTransition] = useTransition();
|
||||
const [uploadingAvatar, setUploadingAvatar] = useState<string | null>(null);
|
||||
|
||||
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,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
hpDelta: 0,
|
||||
xpDelta: 0,
|
||||
score: user.score,
|
||||
level: user.level,
|
||||
role: user.role,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editingUser) return;
|
||||
|
||||
setSaving(true);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const body: {
|
||||
username?: string;
|
||||
avatar?: string | null;
|
||||
hpDelta?: number;
|
||||
xpDelta?: number;
|
||||
score?: number;
|
||||
level?: number;
|
||||
role?: string;
|
||||
} = {};
|
||||
|
||||
if (editingUser.username !== null) {
|
||||
body.username = editingUser.username;
|
||||
}
|
||||
if (editingUser.avatar !== undefined) {
|
||||
body.avatar = editingUser.avatar;
|
||||
}
|
||||
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 result = await updateUser(editingUser.userId, body);
|
||||
|
||||
if (result.success) {
|
||||
await fetchUsers();
|
||||
setEditingUser(null);
|
||||
} else {
|
||||
alert(result.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 handleDelete = async (userId: string) => {
|
||||
if (
|
||||
!confirm(
|
||||
"Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible."
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingUserId(userId);
|
||||
try {
|
||||
const result = await deleteUser(userId);
|
||||
|
||||
if (result.success) {
|
||||
await fetchUsers();
|
||||
} else {
|
||||
alert(result.error || "Erreur lors de la suppression");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting user:", error);
|
||||
alert("Erreur lors de la suppression");
|
||||
} finally {
|
||||
setDeletingUserId(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;
|
||||
const displayAvatar = isEditing ? editingUser.avatar : user.avatar;
|
||||
const displayUsername = isEditing
|
||||
? editingUser.username || user.username
|
||||
: user.username;
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<Avatar
|
||||
src={displayAvatar}
|
||||
username={displayUsername}
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
borderClassName="border-2 border-pixel-gold/50"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 flex-wrap">
|
||||
<h3 className="text-pixel-gold font-bold text-sm sm:text-base break-words">
|
||||
{displayUsername}
|
||||
</h3>
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 whitespace-nowrap">
|
||||
Niveau {user.level}
|
||||
</span>
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 whitespace-nowrap">
|
||||
Score: {formatNumber(user.score)}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[10px] sm:text-xs whitespace-nowrap ${
|
||||
user.role === "ADMIN"
|
||||
? "text-pixel-gold"
|
||||
: "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{user.role}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-[10px] sm:text-xs truncate">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<div className="flex gap-2 flex-shrink-0 sm:ml-2">
|
||||
<button
|
||||
onClick={() => handleEdit(user)}
|
||||
className="px-2 sm:px-3 py-1.5 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(user.id)}
|
||||
disabled={deletingUserId === user.id}
|
||||
className="px-2 sm:px-3 py-1.5 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 disabled:opacity-50 whitespace-nowrap"
|
||||
>
|
||||
{deletingUserId === user.id
|
||||
? "Suppression..."
|
||||
: "Supprimer"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="space-y-4">
|
||||
{/* Username Section */}
|
||||
<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">
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-2">
|
||||
Avatar
|
||||
</label>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
src={editingUser.avatar}
|
||||
username={editingUser.username || user.username}
|
||||
size="lg"
|
||||
borderClassName="border-2 border-pixel-gold/50"
|
||||
/>
|
||||
{uploadingAvatar === user.id && (
|
||||
<div className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-full">
|
||||
<div className="text-pixel-gold text-xs">
|
||||
Upload...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Avatars par défaut */}
|
||||
<div className="flex flex-col items-center gap-2 w-full">
|
||||
<label className="block text-pixel-gold text-[10px] sm:text-xs uppercase tracking-widest">
|
||||
Avatars par défaut
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
{[
|
||||
"/avatar-1.jpg",
|
||||
"/avatar-2.jpg",
|
||||
"/avatar-3.jpg",
|
||||
"/avatar-4.jpg",
|
||||
"/avatar-5.jpg",
|
||||
"/avatar-6.jpg",
|
||||
].map((defaultAvatar) => (
|
||||
<button
|
||||
key={defaultAvatar}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
avatar: defaultAvatar,
|
||||
})
|
||||
}
|
||||
className={`w-12 h-12 sm:w-14 sm:h-14 rounded-full border-2 overflow-hidden transition ${
|
||||
editingUser.avatar === defaultAvatar
|
||||
? "border-pixel-gold scale-110"
|
||||
: "border-pixel-gold/30 hover:border-pixel-gold/50"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={defaultAvatar}
|
||||
alt="Avatar par défaut"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Upload */}
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploadingAvatar(user.id);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await fetch(
|
||||
"/api/admin/avatars/upload",
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
avatar: data.url,
|
||||
});
|
||||
} else {
|
||||
alert("Erreur lors de l'upload de l'image");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error uploading image:", error);
|
||||
alert("Erreur lors de l'upload de l'image");
|
||||
} finally {
|
||||
setUploadingAvatar(null);
|
||||
if (e.target) {
|
||||
e.target.value = "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
id={`avatar-upload-${user.id}`}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* HP Section */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-xs sm:text-sm text-gray-300">
|
||||
Points de Vie (HP)
|
||||
</label>
|
||||
<span className="text-[10px] sm:text-xs text-gray-400">
|
||||
{previewHp} / {user.maxHp}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-1 sm:gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: editingUser.hpDelta - 100,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-[10px] sm:text-xs rounded hover:bg-red-900/30 transition flex-shrink-0"
|
||||
>
|
||||
-100
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: editingUser.hpDelta - 10,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-[10px] sm:text-xs rounded hover:bg-red-900/30 transition flex-shrink-0"
|
||||
>
|
||||
-10
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
value={editingUser.hpDelta || 0}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: parseInt(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="flex-1 min-w-[60px] px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: editingUser.hpDelta + 10,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-[10px] sm:text-xs rounded hover:bg-green-900/30 transition flex-shrink-0"
|
||||
>
|
||||
+10
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: editingUser.hpDelta + 100,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-[10px] sm:text-xs rounded hover:bg-green-900/30 transition flex-shrink-0"
|
||||
>
|
||||
+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-xs sm:text-sm text-gray-300">
|
||||
Expérience (XP)
|
||||
</label>
|
||||
<span className="text-[10px] sm:text-xs text-gray-400">
|
||||
{formatNumber(previewXp)} / {formatNumber(user.maxXp)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-1 sm:gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: editingUser.xpDelta - 1000,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-[10px] sm:text-xs rounded hover:bg-red-900/30 transition flex-shrink-0"
|
||||
>
|
||||
-1000
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: editingUser.xpDelta - 100,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-[10px] sm:text-xs rounded hover:bg-red-900/30 transition flex-shrink-0"
|
||||
>
|
||||
-100
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
value={editingUser.xpDelta || 0}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: parseInt(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="flex-1 min-w-[60px] px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: editingUser.xpDelta + 100,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-[10px] sm:text-xs rounded hover:bg-green-900/30 transition flex-shrink-0"
|
||||
>
|
||||
+100
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: editingUser.xpDelta + 1000,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-[10px] sm:text-xs rounded hover:bg-green-900/30 transition flex-shrink-0"
|
||||
>
|
||||
+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-xs sm:text-sm text-gray-300 mb-2">
|
||||
Score
|
||||
</label>
|
||||
<div className="flex gap-1 sm:gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: (editingUser.score || 0) - 1000,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-[10px] sm:text-xs rounded hover:bg-red-900/30 transition flex-shrink-0"
|
||||
>
|
||||
-1000
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: (editingUser.score || 0) - 100,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-[10px] sm:text-xs rounded hover:bg-red-900/30 transition flex-shrink-0"
|
||||
>
|
||||
-100
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
value={editingUser.score ?? 0}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: parseInt(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="flex-1 min-w-[60px] px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: (editingUser.score || 0) + 100,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-[10px] sm:text-xs rounded hover:bg-green-900/30 transition flex-shrink-0"
|
||||
>
|
||||
+100
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: (editingUser.score || 0) + 1000,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-[10px] sm:text-xs rounded hover:bg-green-900/30 transition flex-shrink-0"
|
||||
>
|
||||
+1000
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Level Section */}
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-2">
|
||||
Niveau
|
||||
</label>
|
||||
<div className="flex gap-1 sm:gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
level: Math.max(1, (editingUser.level || 1) - 1),
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-[10px] sm:text-xs rounded hover:bg-red-900/30 transition flex-shrink-0"
|
||||
>
|
||||
-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 min-w-[60px] px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
level: (editingUser.level || 1) + 1,
|
||||
})
|
||||
}
|
||||
className="px-2 sm:px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-[10px] sm:text-xs rounded hover:bg-green-900/30 transition flex-shrink-0"
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Role Section */}
|
||||
<div>
|
||||
<label className="block text-xs sm: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-3 sm:px-4 py-2 border rounded text-[10px] sm: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-3 sm:px-4 py-2 border rounded text-[10px] sm: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 flex-col sm:flex-row gap-2 pt-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="success"
|
||||
size="md"
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? "Enregistrement..." : "Enregistrer"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
variant="secondary"
|
||||
size="md"
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 text-[10px] sm:text-xs">
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-center mb-0.5">
|
||||
<span className="text-gray-400">HP</span>
|
||||
<span className="text-gray-400">
|
||||
{user.hp}/{user.maxHp}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 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 className="flex-1">
|
||||
<div className="flex justify-between items-center mb-0.5">
|
||||
<span className="text-gray-400">XP</span>
|
||||
<span className="text-gray-400">
|
||||
{formatNumber(user.xp)}/{formatNumber(user.maxXp)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 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>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user