brackt/app/routes/admin.tsx
Chris Parsons 6f70a19b73
Add admin user management interface with search and username editing (#431)
* Add admin users management page with pagination, search, and inline username editing

https://claude.ai/code/session_01E4UUQqQedhFP2F5YcBRArs

* Fix five issues from admin users page code review

- admin.users: use useEffect to close inline edit on success so validation
  errors are not silently discarded when the save button fires onClick
- root: add /admin to ONBOARDING_EXEMPT so admin users without a username
  are not redirect-looped away from admin pages
- settings: skip username validation when the username field is empty so
  updating timezone alone does not break for accounts without a username
- models/user: exclude soft-deleted users from findUsersPaginated
- admin.users: remove unused hasMore from loader return; simplify header div

https://claude.ai/code/session_01E4UUQqQedhFP2F5YcBRArs

* Fix lint: rename shadowed variables in user model

- findUserByUsername: use imported sql directly instead of destructuring
  it from the callback (shadowed the top-level import)
- findUsersPaginated: rename orderBy callback param from users to t
  (shadowed the outer users result variable)

https://claude.ai/code/session_01E4UUQqQedhFP2F5YcBRArs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 12:07:51 -07:00

105 lines
3.1 KiB
TypeScript

import { Link, Outlet, redirect } from "react-router";
import { auth } from "~/lib/auth.server";
import type { Route } from "./+types/admin";
import { isUserAdmin } from "~/models/user";
import { Button } from "~/components/ui/button";
import {
LayoutDashboard,
Trophy,
Calendar,
FolderKanban,
RefreshCw,
Award,
Activity,
Users,
} from "lucide-react";
export function meta(): Route.MetaDescriptors {
return [{ title: "Admin - Brackt" }];
}
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("/");
}
return { isAdmin };
}
export default function AdminLayout() {
return (
<div className="flex min-h-screen">
{/* Sidebar */}
<aside className="w-64 border-r bg-muted/40">
<div className="flex h-16 items-center border-b px-6">
<h2 className="text-lg font-semibold">Admin Panel</h2>
</div>
<nav className="space-y-1 p-4">
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to="/admin">
<LayoutDashboard className="mr-2 h-4 w-4" />
Dashboard
</Link>
</Button>
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to="/admin/sports">
<Trophy className="mr-2 h-4 w-4" />
Sports
</Link>
</Button>
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to="/admin/sports-seasons">
<Calendar className="mr-2 h-4 w-4" />
Sports Seasons
</Link>
</Button>
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to="/admin/simulators">
<Activity className="mr-2 h-4 w-4" />
Simulators
</Link>
</Button>
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to="/admin/tournaments">
<Award className="mr-2 h-4 w-4" />
Tournaments
</Link>
</Button>
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to="/admin/templates">
<FolderKanban className="mr-2 h-4 w-4" />
Season Templates
</Link>
</Button>
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to="/admin/users">
<Users className="mr-2 h-4 w-4" />
Users
</Link>
</Button>
<div className="border-t my-2" />
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to="/admin/data-sync">
<RefreshCw className="mr-2 h-4 w-4" />
Data Sync
</Link>
</Button>
</nav>
</aside>
{/* Main Content */}
<main className="flex-1">
<Outlet />
</main>
</div>
);
}