JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState, useEffect } from "react";4import { motion } from "framer-motion";5import { UserPlus, Trash2, ShieldCheck, Edit2, Users, FileText, Folder } from "lucide-react";6import Button from "../ui/Button";7import Modal from "../ui/Modal";8import UserAvatar from "../ui/UserAvatar";9import { EmptyState } from "../ui/PageHeader";10import toast from "react-hot-toast";1112interface UserData {13 id: string;14 username: string | null;15 email: string | null;16 name: string | null;17 role: string;18 twoFactorEnabled: boolean;19 createdAt: string;20 _count?: { files: number; folders: number };21}2223const roleLabels: Record<string, string> = { admin: "Admin", user: "Utilisateur", viewer: "Lecteur" };24const roleColors: Record<string, string> = {25 admin: "bg-rose-500/10 text-rose-600 dark:text-rose-400 ring-rose-500/20",26 user: "bg-blue-500/10 text-blue-700 dark:text-blue-300 ring-blue-500/20",27 viewer: "bg-gray-100 text-gray-600 dark:bg-white/[0.06] dark:text-gray-300 ring-gray-500/15",28};2930const emptyForm = { username: "", name: "", password: "", role: "user" };3132function UserForm({ create, form, setForm }: { create: boolean; form: typeof emptyForm; setForm: (f: typeof emptyForm) => void }) {33return (34 <div className="space-y-3.5">35 <div>36 <label className="block text-[12px] font-medium text-gray-500 dark:text-gray-400 mb-1.5">Nom d'utilisateur{create ? " *" : ""}</label>37 <input type="text" placeholder="ex. simon, marie42…" value={form.username} onChange={(e) => setForm({ ...form, username: e.target.value.toLowerCase().replace(/\s/g, "") })} className="field h-11" autoFocus={create} autoComplete="off" />38 </div>39 <div>40 <label className="block text-[12px] font-medium text-gray-500 dark:text-gray-400 mb-1.5">Nom affiché</label>41 <input type="text" placeholder="ex. Simon Boucher" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className="field h-11" />42 </div>43 <div>44 <label className="block text-[12px] font-medium text-gray-500 dark:text-gray-400 mb-1.5">{create ? "Mot de passe *" : "Nouveau mot de passe"}</label>45 <input type="password" placeholder={create ? "Mot de passe" : "Laisser vide pour ne pas changer"} value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} className="field h-11" autoComplete="new-password" />46 </div>47 <div>48 <label className="block text-[12px] font-medium text-gray-500 dark:text-gray-400 mb-1.5">Rôle</label>49 <select value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value })} className="field h-11">50 <option value="user">Utilisateur</option>51 <option value="viewer">Lecteur</option>52 <option value="admin">Admin</option>53 </select>54 </div>55 </div>56);57}5859export default function UserManagement() {60 const [users, setUsers] = useState<UserData[]>([]);61 const [loading, setLoading] = useState(true);62 const [showCreate, setShowCreate] = useState(false);63 const [editUser, setEditUser] = useState<UserData | null>(null);64 const [deleteUser, setDeleteUser] = useState<UserData | null>(null);65 const [saving, setSaving] = useState(false);66 const [form, setForm] = useState(emptyForm);6768 const fetchUsers = async () => {69 const res = await fetch("/api/users");70 if (res.ok) setUsers(await res.json());71 setLoading(false);72 };7374 useEffect(() => { fetchUsers(); }, []);7576 const handleCreate = async () => {77 setSaving(true);78 const res = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form) });79 setSaving(false);80 if (res.ok) {81 toast.success("Utilisateur créé");82 setShowCreate(false);83 setForm(emptyForm);84 fetchUsers();85 } else {86 const data = await res.json();87 toast.error(data.error || "Erreur");88 }89 };9091 const handleUpdate = async () => {92 if (!editUser) return;93 const data: Record<string, string> = {};94 if (form.name) data.name = form.name;95 if (form.username) data.username = form.username;96 if (form.role) data.role = form.role;97 if (form.password) data.password = form.password;98 setSaving(true);99 const res = await fetch(`/api/users/${editUser.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) });100 setSaving(false);101 if (res.ok) {102 toast.success("Utilisateur modifié");103 setEditUser(null);104 fetchUsers();105 } else toast.error("Erreur lors de la modification");106 };107108 const handleDelete = async () => {109 if (!deleteUser) return;110 setSaving(true);111 const res = await fetch(`/api/users/${deleteUser.id}`, { method: "DELETE" });112 setSaving(false);113 setDeleteUser(null);114 if (res.ok) {115 toast.success("Utilisateur supprimé");116 fetchUsers();117 } else {118 const data = await res.json();119 toast.error(data.error || "Erreur");120 }121 };122123 return (124 <div className="space-y-4">125 <div className="flex items-center justify-between gap-3">126 <p className="text-[13px] text-gray-500 dark:text-gray-400">{loading ? "Chargement…" : `${users.length} compte${users.length !== 1 ? "s" : ""}`}</p>127 <Button variant="accent" size="sm" onClick={() => { setForm(emptyForm); setShowCreate(true); }}>128 <UserPlus className="w-4 h-4" /> Ajouter129 </Button>130 </div>131132 {loading ? (133 <div className="space-y-2">134 {[0, 1, 2].map((i) => (135 <div key={i} className="card p-4 flex items-center gap-3 animate-pulse">136 <div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-white/10" />137 <div className="flex-1 space-y-2"><div className="h-3 w-1/3 rounded bg-gray-200 dark:bg-white/10" /><div className="h-2 w-1/4 rounded bg-gray-200 dark:bg-white/10" /></div>138 </div>139 ))}140 </div>141 ) : users.length === 0 ? (142 <EmptyState icon={Users} title="Aucun utilisateur" description="Créez un premier compte pour partager votre cloud." />143 ) : (144 <div className="card p-0 overflow-hidden">145 {/* En-tête desktop */}146 <div className="hidden md:grid grid-cols-[1fr_120px_140px_110px_96px] items-center gap-3 px-4 h-10 border-b border-line dark:border-white/[0.07] text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 sticky top-0 bg-surface dark:bg-canvas z-10">147 <span>Utilisateur</span><span>Rôle</span><span>Contenu</span><span>2FA</span><span />148 </div>149 <div className="divide-y divide-line dark:divide-white/[0.06]">150 {users.map((user, i) => (151 <motion.div152 key={user.id}153 initial={{ opacity: 0, y: 5 }}154 animate={{ opacity: 1, y: 0 }}155 transition={{ delay: Math.min(i, 10) * 0.03 }}156 className="grid grid-cols-1 md:grid-cols-[1fr_120px_140px_110px_96px] items-center gap-3 px-4 py-3 hover:bg-gray-50 dark:hover:bg-white/[0.03] transition-colors"157 >158 <div className="flex items-center gap-3 min-w-0">159 <UserAvatar userId={user.id} name={user.name || user.username} size="md" />160 <div className="min-w-0">161 <p className="text-[14px] font-medium text-gray-900 dark:text-white truncate">{user.name || user.username || user.id}</p>162 <p className="text-[12px] text-gray-500 truncate">@{user.username}{user.email ? ` · ${user.email}` : ""}</p>163 </div>164 </div>165 <div className="flex items-center gap-2 md:block">166 <span className={`inline-flex px-2 py-0.5 rounded-full text-[11px] font-medium ring-1 ring-inset ${roleColors[user.role] || roleColors.user}`}>{roleLabels[user.role] || user.role}</span>167 <span className="md:hidden text-[12px] text-gray-500">{user._count?.files || 0} fichiers · {user._count?.folders || 0} dossiers</span>168 {user.twoFactorEnabled && <ShieldCheck className="md:hidden w-4 h-4 text-emerald-500" aria-label="2FA activée" />}169 </div>170 <div className="hidden md:flex items-center gap-3 text-[12.5px] text-gray-500 tabular-nums">171 <span className="inline-flex items-center gap-1"><FileText className="w-3.5 h-3.5" />{user._count?.files || 0}</span>172 <span className="inline-flex items-center gap-1"><Folder className="w-3.5 h-3.5" />{user._count?.folders || 0}</span>173 </div>174 <div className="hidden md:block">175 {user.twoFactorEnabled ? (176 <span className="inline-flex items-center gap-1 text-[12px] text-emerald-600 dark:text-emerald-400"><ShieldCheck className="w-3.5 h-3.5" /> Activée</span>177 ) : (178 <span className="text-[12px] text-gray-400">—</span>179 )}180 </div>181 <div className="flex gap-1 md:justify-end -ml-2 md:ml-0">182 <button183 onClick={() => { setEditUser(user); setForm({ username: user.username || "", name: user.name || "", password: "", role: user.role }); }}184 className="w-10 h-10 flex items-center justify-center text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-xl hover:bg-gray-100 dark:hover:bg-white/[0.08] transition-colors"185 title="Modifier"186 >187 <Edit2 className="w-4 h-4" />188 </button>189 <button190 onClick={() => setDeleteUser(user)}191 className="w-10 h-10 flex items-center justify-center text-gray-400 hover:text-rose-500 rounded-xl hover:bg-rose-500/10 transition-colors"192 title="Supprimer"193 >194 <Trash2 className="w-4 h-4" />195 </button>196 </div>197 </motion.div>198 ))}199 </div>200 </div>201 )}202203 <Modal204 isOpen={showCreate}205 onClose={() => setShowCreate(false)}206 title="Nouvel utilisateur"207 footer={<><Button variant="ghost" onClick={() => setShowCreate(false)}>Annuler</Button><Button variant="accent" onClick={handleCreate} loading={saving} disabled={!form.username || !form.password}>Créer</Button></>}208 >209 <UserForm create form={form} setForm={setForm} />210 </Modal>211212 <Modal213 isOpen={!!editUser}214 onClose={() => setEditUser(null)}215 title="Modifier l'utilisateur"216 description={editUser ? `@${editUser.username}` : undefined}217 footer={<><Button variant="ghost" onClick={() => setEditUser(null)}>Annuler</Button><Button variant="accent" onClick={handleUpdate} loading={saving}>Enregistrer</Button></>}218 >219 <UserForm create={false} form={form} setForm={setForm} />220 </Modal>221222 <Modal223 isOpen={!!deleteUser}224 onClose={() => setDeleteUser(null)}225 title="Supprimer cet utilisateur ?"226 description={deleteUser ? `${deleteUser.name || deleteUser.username}` : undefined}227 footer={<><Button variant="ghost" onClick={() => setDeleteUser(null)}>Annuler</Button><Button variant="danger" onClick={handleDelete} loading={saving}><Trash2 className="w-4 h-4" /> Supprimer</Button></>}228 >229 <p className="text-sm text-gray-600 dark:text-gray-300">Le compte sera supprimé et ne pourra plus se connecter.</p>230 </Modal>231 </div>232 );233}234