import { useState, useEffect } from "react"; import { Form, redirect, useSearchParams } from "react-router"; import type { Route } from "./+types/admin.users"; import { auth } from "~/lib/auth.server"; import { isUserAdmin, findUsersPaginated, findUserByUsername, updateUser, USERNAME_RE, } from "~/models/user"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Badge } from "~/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { Pencil, Check, X } from "lucide-react"; const PAGE_SIZE = 25; export function meta(): Route.MetaDescriptors { return [{ title: "Users — Brackt Admin" }]; } export async function loader(args: Route.LoaderArgs) { const session = await auth.api.getSession({ headers: args.request.headers }); const userId = session?.user.id ?? null; if (!userId) throw redirect("/"); const isAdmin = await isUserAdmin(userId); if (!isAdmin) throw redirect("/"); const searchParams = new URL(args.request.url).searchParams; const page = Math.max(0, parseInt(searchParams.get("page") ?? "0", 10) || 0); const search = searchParams.get("search") ?? ""; const { users, total } = await findUsersPaginated({ limit: PAGE_SIZE, offset: page * PAGE_SIZE, search: search || undefined, }); const totalPages = Math.ceil(total / PAGE_SIZE); return { users, page, totalPages, total, search }; } export async function action(args: Route.ActionArgs) { const session = await auth.api.getSession({ headers: args.request.headers }); const userId = session?.user.id ?? null; if (!userId) throw redirect("/"); const isAdmin = await isUserAdmin(userId); if (!isAdmin) throw redirect("/"); const formData = await args.request.formData(); const intent = formData.get("intent"); if (intent === "update-username") { const targetUserId = formData.get("userId"); const username = formData.get("username"); if (typeof targetUserId !== "string" || typeof username !== "string") { return { error: "Invalid request" }; } const trimmed = username.trim(); if (trimmed !== "" && !USERNAME_RE.test(trimmed)) { return { error: "Username must be 3–30 characters and contain only letters, numbers, underscores, or hyphens.", userId: targetUserId, }; } if (trimmed !== "") { const existing = await findUserByUsername(trimmed); if (existing && existing.id !== targetUserId) { return { error: "That username is already taken.", userId: targetUserId }; } } await updateUser(targetUserId, { username: trimmed || null }); return { success: true }; } return {}; } export default function AdminUsers({ loaderData, actionData }: Route.ComponentProps) { const { users, page, totalPages, total, search } = loaderData; const [searchParams] = useSearchParams(); const [editingId, setEditingId] = useState(null); const [editValue, setEditValue] = useState(""); useEffect(() => { if (actionData && "success" in actionData && actionData.success) { setEditingId(null); setEditValue(""); } }, [actionData]); function startEdit(userId: string, currentUsername: string | null) { setEditingId(userId); setEditValue(currentUsername ?? ""); } function cancelEdit() { setEditingId(null); setEditValue(""); } function buildPageUrl(newPage: number) { const p = new URLSearchParams(searchParams); p.set("page", String(newPage)); return `?${p.toString()}`; } return (

Users

{total} total users

All Users Search and manage user accounts.
{search && ( )}
Email Username Display Name Admin Joined {users.length === 0 && ( No users found. )} {users.map((user) => { const isEditing = editingId === user.id; const hasError = actionData && "error" in actionData && actionData.userId === user.id; return ( {user.email} {isEditing ? (
setEditValue(e.target.value)} className="h-7 w-40 text-sm" autoFocus placeholder="leave blank to clear" /> {hasError && (

{actionData.error}

)}
) : (
{user.username ?? "—"}
)}
{user.displayName ?? "—"} {user.isAdmin && Admin} {new Date(user.createdAt).toLocaleDateString()}
); })}
{totalPages > 1 && (
Page {page + 1} of {totalPages}
{page > 0 && ( )} {page < totalPages - 1 && ( )}
)}
); }