Refactor evaluation and admin pages to use server actions for data fetching, enhancing performance and simplifying state management. Update README to reflect API route changes and remove deprecated API endpoints for users and evaluations.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m7s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m7s
This commit is contained in:
174
src/components/AdminClient.tsx
Normal file
174
src/components/AdminClient.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { setUserRole, deleteUser } from "@/actions/admin";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { ConfirmModal } from "@/components/ConfirmModal";
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
dimensions: { id: string; title: string; orderIndex: number }[];
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
role: string;
|
||||
createdAt: Date | string;
|
||||
}
|
||||
|
||||
interface AdminClientProps {
|
||||
templates: Template[];
|
||||
users: User[];
|
||||
}
|
||||
|
||||
export function AdminClient({ templates, users: initialUsers }: AdminClientProps) {
|
||||
const { data: session } = useSession();
|
||||
const [users, setUsers] = useState(initialUsers);
|
||||
const [updatingId, setUpdatingId] = useState<string | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<User | null>(null);
|
||||
|
||||
async function handleSetRole(userId: string, role: "admin" | "evaluator") {
|
||||
setUpdatingId(userId);
|
||||
try {
|
||||
const result = await setUserRole(userId, role);
|
||||
if (result.success) {
|
||||
setUsers((prev) => prev.map((u) => (u.id === userId ? { ...u, role } : u)));
|
||||
} else {
|
||||
alert(result.error);
|
||||
}
|
||||
} finally {
|
||||
setUpdatingId(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-6 font-mono text-lg font-medium text-zinc-800 dark:text-zinc-200">Admin</h1>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-4 font-mono text-xs text-zinc-600 dark:text-zinc-500">Users</h2>
|
||||
<div className="overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 shadow-sm dark:shadow-none">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-700/80">
|
||||
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Email</th>
|
||||
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Nom</th>
|
||||
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Rôle</th>
|
||||
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Créé le</th>
|
||||
<th className="px-4 py-2.5 text-right font-mono text-xs text-zinc-600 dark:text-zinc-400">—</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((u) => (
|
||||
<tr key={u.id} className="border-b border-zinc-200 dark:border-zinc-600/50 last:border-0">
|
||||
<td className="px-4 py-2.5 text-sm text-zinc-800 dark:text-zinc-200">{u.email}</td>
|
||||
<td className="px-4 py-2.5 text-sm text-zinc-600 dark:text-zinc-400">{u.name ?? "—"}</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<span
|
||||
className={`font-mono text-xs px-1.5 py-0.5 rounded ${
|
||||
u.role === "admin" ? "bg-cyan-500/20 text-cyan-600 dark:text-cyan-400" : "bg-zinc-200 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-400"
|
||||
}`}
|
||||
>
|
||||
{u.role}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{format(new Date(u.createdAt), "yyyy-MM-dd HH:mm")}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-right">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{u.role === "admin" ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSetRole(u.id, "evaluator")}
|
||||
disabled={updatingId === u.id}
|
||||
className="font-mono text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 disabled:opacity-50"
|
||||
title="Rétrograder en évaluateur"
|
||||
>
|
||||
{updatingId === u.id ? "..." : "rétrograder"}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSetRole(u.id, "admin")}
|
||||
disabled={updatingId === u.id}
|
||||
className="font-mono text-xs text-cyan-600 dark:text-cyan-400 hover:text-cyan-500 disabled:opacity-50"
|
||||
title="Promouvoir admin"
|
||||
>
|
||||
{updatingId === u.id ? "..." : "promouvoir admin"}
|
||||
</button>
|
||||
)}
|
||||
{u.id !== session?.user?.id && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteTarget(u)}
|
||||
className="font-mono text-xs text-red-500 hover:text-red-400"
|
||||
title="Supprimer"
|
||||
>
|
||||
supprimer
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={!!deleteTarget}
|
||||
title="Supprimer l'utilisateur"
|
||||
message={
|
||||
deleteTarget
|
||||
? `Supprimer ${deleteTarget.name || deleteTarget.email} ? Les évaluations créées par cet utilisateur resteront (évaluateur mis à null).`
|
||||
: ""
|
||||
}
|
||||
confirmLabel="Supprimer"
|
||||
cancelLabel="Annuler"
|
||||
variant="danger"
|
||||
onConfirm={async () => {
|
||||
if (!deleteTarget) return;
|
||||
const result = await deleteUser(deleteTarget.id);
|
||||
if (result.success) {
|
||||
setUsers((prev) => prev.filter((u) => u.id !== deleteTarget.id));
|
||||
setDeleteTarget(null);
|
||||
} else {
|
||||
alert(result.error);
|
||||
}
|
||||
}}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
/>
|
||||
|
||||
<section className="mt-8">
|
||||
<h2 className="mb-4 font-mono text-xs text-zinc-600 dark:text-zinc-500">Modèles</h2>
|
||||
<div className="space-y-3">
|
||||
{templates.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
className="rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 p-4 shadow-sm dark:shadow-none"
|
||||
>
|
||||
<h3 className="font-medium text-zinc-800 dark:text-zinc-200">{t.name}</h3>
|
||||
<p className="mt-1 font-mono text-xs text-zinc-600 dark:text-zinc-500">
|
||||
{t.dimensions.length} dim.
|
||||
</p>
|
||||
<ul className="mt-2 space-y-0.5 font-mono text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{t.dimensions.slice(0, 5).map((d) => (
|
||||
<li key={d.id}>• {d.title}</li>
|
||||
))}
|
||||
{t.dimensions.length > 5 && (
|
||||
<li className="text-zinc-600 dark:text-zinc-500">+{t.dimensions.length - 5}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user