Files
FullStack-Blog-Nestjs-Nextj…/frontend/app/dashboard/components/users-table.tsx
2026-02-18 22:27:05 +09:00

192 lines
6.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { clientFetch } from "@/lib/api";
import { toast } from "sonner";
import { User, UserRole } from "@/lib/types";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2, Pencil, X, Check } from "lucide-react";
interface Props {
initialUsers: User[];
}
const roleColor: Record<UserRole, string> = {
[UserRole.ADMIN]: "bg-red-100 text-red-800 border-red-200",
[UserRole.MANAGER]: "bg-amber-100 text-amber-800 border-amber-200",
[UserRole.MEMBER]: "bg-blue-100 text-blue-700 border-blue-200",
};
export function UsersTable({ initialUsers }: Props) {
const [users, setUsers] = useState<User[]>(initialUsers);
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState("");
const [editRole, setEditRole] = useState<UserRole>(UserRole.MEMBER);
const [loading, setLoading] = useState(false);
const startEdit = (user: User) => {
setEditingId(user.id);
setEditName(user.name || "");
setEditRole(user.role);
};
const cancelEdit = () => {
setEditingId(null);
};
const handleUpdate = async (user: User) => {
setLoading(true);
try {
// Update name if changed
let updated = user;
if (editName !== (user.name || "")) {
const res = await clientFetch<{ success: boolean; user: User }>(
`/users/${user.id}`,
{
method: "PATCH",
body: JSON.stringify({ name: editName || undefined }),
}
);
updated = res.user;
}
// Update role if changed
if (editRole !== user.role) {
const res = await clientFetch<{ success: boolean; user: User }>(
`/users/${user.id}/role`,
{
method: "PATCH",
body: JSON.stringify({ role: editRole }),
}
);
updated = res.user;
}
toast.success("User updated.");
setUsers((prev) => prev.map((u) => (u.id === user.id ? updated : u)));
setEditingId(null);
} catch (err: unknown) {
toast.error(err instanceof Error ? err.message : "Update failed");
} finally {
setLoading(false);
}
};
return (
<div className="space-y-2">
{users.length === 0 ? (
<div className="rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground">
No users found.
</div>
) : (
users.map((user) => (
<div
key={user.id}
className="rounded-xl border bg-card p-4 shadow-sm flex flex-wrap items-center gap-3"
>
{editingId === user.id ? (
/* ── Edit row ─────────────────────────────────────────────── */
<div className="flex flex-1 flex-wrap items-center gap-2">
<Input
className="h-8 w-40"
placeholder="Name"
value={editName}
onChange={(e) => setEditName(e.target.value)}
/>
<Select
value={editRole}
onValueChange={(v) => setEditRole(v as UserRole)}
>
<SelectTrigger className="h-8 w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={UserRole.MEMBER}>MEMBER</SelectItem>
<SelectItem value={UserRole.MANAGER}>MANAGER</SelectItem>
<SelectItem value={UserRole.ADMIN}>ADMIN</SelectItem>
</SelectContent>
</Select>
<div className="flex gap-1">
<Button
size="sm"
onClick={() => handleUpdate(user)}
disabled={loading}
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Check className="h-4 w-4" />
)}
</Button>
<Button size="sm" variant="ghost" onClick={cancelEdit}>
<X className="h-4 w-4" />
</Button>
</div>
</div>
) : (
/* ── View row ─────────────────────────────────────────────── */
<>
<div className="flex flex-1 flex-wrap items-center gap-3 min-w-0">
{/* Avatar placeholder */}
<div className="h-8 w-8 shrink-0 rounded-full bg-muted flex items-center justify-center text-xs font-semibold uppercase">
{(user.name || user.email).charAt(0)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">
{user.name ?? (
<span className="italic text-muted-foreground">
No name
</span>
)}
</p>
<p className="text-xs text-muted-foreground truncate">
{user.email}
</p>
</div>
<span
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${roleColor[user.role]}`}
>
{user.role}
</span>
{!user.isActive && (
<Badge
variant="outline"
className="text-xs text-muted-foreground"
>
Inactive
</Badge>
)}
<p className="text-xs text-muted-foreground shrink-0">
Joined {new Date(user.createdAt).toLocaleDateString()}
</p>
</div>
{/* Actions */}
<Button
variant="outline"
size="sm"
onClick={() => startEdit(user)}
>
<Pencil className="h-4 w-4" />
</Button>
</>
)}
</div>
))
)}
</div>
);
}