786 lines
29 KiB
TypeScript
786 lines
29 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useTransition } from "react";
|
|
import {
|
|
Avatar,
|
|
Input,
|
|
Button,
|
|
Card,
|
|
Modal,
|
|
CloseButton,
|
|
} 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");
|
|
};
|
|
|
|
// Trouver l'utilisateur en cours d'édition pour les previews
|
|
const currentEditingUserData = editingUser
|
|
? users.find((u) => u.id === editingUser.userId)
|
|
: null;
|
|
const previewHp =
|
|
currentEditingUserData && editingUser
|
|
? Math.max(
|
|
0,
|
|
Math.min(
|
|
currentEditingUserData.maxHp,
|
|
currentEditingUserData.hp + editingUser.hpDelta
|
|
)
|
|
)
|
|
: 0;
|
|
const previewXp =
|
|
currentEditingUserData && editingUser
|
|
? Math.max(0, currentEditingUserData.xp + editingUser.xpDelta)
|
|
: 0;
|
|
|
|
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>
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4">
|
|
{users.map((user) => {
|
|
return (
|
|
<Card key={user.id} variant="default" className="p-3">
|
|
<div className="flex flex-col gap-2">
|
|
{/* Header avec avatar et nom */}
|
|
<div className="flex items-center gap-2">
|
|
<Avatar
|
|
src={user.avatar}
|
|
username={user.username}
|
|
size="sm"
|
|
className="flex-shrink-0"
|
|
borderClassName="border-2 border-pixel-gold/50"
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-pixel-gold font-bold text-sm truncate">
|
|
{user.username}
|
|
</h3>
|
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
<span className="text-[10px] text-gray-500">
|
|
Niv. {user.level}
|
|
</span>
|
|
<span
|
|
className={`text-[10px] font-bold px-1.5 py-0.5 rounded border ${
|
|
user.role === "ADMIN"
|
|
? "text-pixel-gold border-pixel-gold/50 bg-pixel-gold/10"
|
|
: "text-gray-500 border-gray-500/30 bg-gray-500/10"
|
|
}`}
|
|
>
|
|
{user.role}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Score en évidence */}
|
|
<div className="flex items-baseline gap-1.5 px-1">
|
|
<span className="text-[10px] text-gray-400">Score:</span>
|
|
<span className="text-lg font-bold text-pixel-gold">
|
|
{formatNumber(user.score)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Stats HP et XP */}
|
|
<div className="space-y-1.5 text-[10px]">
|
|
<div>
|
|
<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 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-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 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>
|
|
|
|
{/* Email */}
|
|
<p className="text-gray-400 text-[10px] truncate px-1">
|
|
{user.email}
|
|
</p>
|
|
|
|
{/* Boutons d'action */}
|
|
<div className="flex gap-2 pt-1">
|
|
<button
|
|
onClick={() => handleEdit(user)}
|
|
className="flex-1 px-2 py-1 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] tracking-widest rounded hover:bg-pixel-gold/10 transition"
|
|
>
|
|
Modifier
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(user.id)}
|
|
disabled={deletingUserId === user.id}
|
|
className="flex-1 px-2 py-1 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-[10px] tracking-widest rounded hover:bg-red-900/30 transition disabled:opacity-50"
|
|
>
|
|
{deletingUserId === user.id ? "..." : "Suppr."}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal d'édition */}
|
|
{editingUser && currentEditingUserData && (
|
|
<Modal isOpen={!!editingUser} onClose={handleCancel} size="lg">
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
|
Modifier l'utilisateur
|
|
</h4>
|
|
<CloseButton onClick={handleCancel} size="lg" />
|
|
</div>
|
|
<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 || currentEditingUserData.username
|
|
}
|
|
size="lg"
|
|
borderClassName="border-2 border-pixel-gold/50"
|
|
/>
|
|
{uploadingAvatar === editingUser.userId && (
|
|
<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(editingUser.userId);
|
|
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-${editingUser.userId}`}
|
|
/>
|
|
<label htmlFor={`avatar-upload-${editingUser.userId}`}>
|
|
<Button
|
|
variant="primary"
|
|
size="sm"
|
|
as="span"
|
|
className="cursor-pointer"
|
|
>
|
|
{uploadingAvatar === editingUser.userId
|
|
? "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} / {currentEditingUserData.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 / currentEditingUserData.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(currentEditingUserData.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 / currentEditingUserData.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>
|
|
</Modal>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|