Add admin leagues page and mobile-friendly admin layout
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m45s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m21s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped

- Make admin sidebar responsive: hidden on mobile with a Sheet drawer
  triggered by a hamburger menu button; fixed top bar on mobile
- Reduce dashboard padding to p-4 on mobile (p-8 on md+)
- Add listLeaguesWithStatus() to league model (left-joins seasons for status/year)
- New /admin/leagues page: table of all leagues with link, season year, status badge
- Register admin/leagues route and add Leagues nav link to admin sidebar

https://claude.ai/code/session_01GWWdJ5m8cBK1Es355YNt18
This commit is contained in:
Claude 2026-06-11 00:14:31 +00:00
parent 97a92aba7d
commit ddb646402c
No known key found for this signature in database
5 changed files with 247 additions and 53 deletions

View file

@ -1,6 +1,7 @@
import { eq, and } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import type { SeasonStatus } from "~/models/season";
export type League = typeof schema.leagues.$inferSelect;
export type NewLeague = typeof schema.leagues.$inferInsert;
@ -54,6 +55,38 @@ export async function setCurrentSeason(
return await updateLeague(leagueId, { currentSeasonId: seasonId });
}
export type LeagueWithStatus = League & {
currentSeasonStatus: SeasonStatus | null;
currentSeasonYear: number | null;
};
export async function listLeaguesWithStatus(): Promise<LeagueWithStatus[]> {
const db = database();
const rows = await db
.select({
id: schema.leagues.id,
name: schema.leagues.name,
createdBy: schema.leagues.createdBy,
currentSeasonId: schema.leagues.currentSeasonId,
isPublicDraftBoard: schema.leagues.isPublicDraftBoard,
discordWebhookUrl: schema.leagues.discordWebhookUrl,
discordPicksAnnouncementEnabled: schema.leagues.discordPicksAnnouncementEnabled,
createdAt: schema.leagues.createdAt,
updatedAt: schema.leagues.updatedAt,
currentSeasonStatus: schema.seasons.status,
currentSeasonYear: schema.seasons.year,
})
.from(schema.leagues)
.leftJoin(schema.seasons, eq(schema.seasons.id, schema.leagues.currentSeasonId))
.orderBy(schema.leagues.createdAt);
return rows.map((r) => ({
...r,
currentSeasonStatus: (r.currentSeasonStatus as SeasonStatus) ?? null,
currentSeasonYear: r.currentSeasonYear ?? null,
}));
}
export async function listLeagues(options?: {
limit?: number;
offset?: number;

View file

@ -159,6 +159,7 @@ export default [
route("data-sync", "routes/admin.data-sync.tsx"),
route("standings-snapshots", "routes/admin.standings-snapshots.tsx"),
route("users", "routes/admin.users.tsx"),
route("leagues", "routes/admin.leagues.tsx"),
]),
route(
"api/admin/export-sports-data",

View file

@ -273,7 +273,7 @@ export default function AdminDashboard({ loaderData, actionData }: Route.Compone
const systemIsPopulated = stats.sportsCount > 0;
return (
<div className="p-8">
<div className="p-4 md:p-8">
<div className="mb-8">
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
<p className="text-muted-foreground mt-2">

View file

@ -0,0 +1,116 @@
import { Link } from "react-router";
import type { Route } from "./+types/admin.leagues";
import { listLeaguesWithStatus } from "~/models/league";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import { Badge } from "~/components/ui/badge";
import type { SeasonStatus } from "~/models/season";
export function meta(): Route.MetaDescriptors {
return [{ title: "Leagues — Brackt Admin" }];
}
export async function loader() {
const leagues = await listLeaguesWithStatus();
return { leagues };
}
function StatusBadge({ status }: { status: SeasonStatus | null }) {
if (!status) {
return <Badge variant="outline" className="text-muted-foreground">No Season</Badge>;
}
switch (status) {
case "pre_draft":
return <Badge variant="secondary">Pre-Draft</Badge>;
case "draft":
return (
<Badge className="border-amber-300 text-amber-700 dark:border-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30" variant="outline">
Drafting
</Badge>
);
case "active":
return (
<Badge className="border-green-300 text-green-700 dark:border-green-700 dark:text-green-400 bg-green-50 dark:bg-green-950/30" variant="outline">
Active
</Badge>
);
case "completed":
return <Badge variant="outline">Completed</Badge>;
}
}
export default function AdminLeagues({ loaderData }: Route.ComponentProps) {
const { leagues } = loaderData;
return (
<div className="p-4 md:p-8">
<div className="mb-6">
<h1 className="text-2xl font-bold">Leagues</h1>
<p className="text-muted-foreground text-sm mt-1">{leagues.length} total leagues</p>
</div>
<Card>
<CardHeader>
<CardTitle>All Leagues</CardTitle>
<CardDescription>Every league in the system with their current season status.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>League Name</TableHead>
<TableHead>Season Year</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{leagues.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
No leagues found.
</TableCell>
</TableRow>
)}
{leagues.map((league) => (
<TableRow key={league.id}>
<TableCell className="font-medium">
<Link
to={`/leagues/${league.id}`}
className="hover:underline text-foreground"
>
{league.name}
</Link>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{league.currentSeasonYear ?? "—"}
</TableCell>
<TableCell>
<StatusBadge status={league.currentSeasonStatus} />
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{new Date(league.createdAt).toLocaleDateString()}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View file

@ -4,6 +4,13 @@ import type { Route } from "./+types/admin";
import { isUserAdmin } from "~/models/user";
import { Button } from "~/components/ui/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "~/components/ui/sheet";
import {
LayoutDashboard,
Trophy,
@ -13,6 +20,8 @@ import {
Award,
Activity,
Users,
Menu,
Shield,
} from "lucide-react";
export function meta(): Route.MetaDescriptors {
@ -35,21 +44,21 @@ export async function loader(args: Route.LoaderArgs) {
return { isAdmin };
}
export default function AdminLayout() {
function AdminNavLinks() {
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 aria-label="Admin navigation" 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/leagues">
<Shield className="mr-2 h-4 w-4" />
Leagues
</Link>
</Button>
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to="/admin/sports">
<Trophy className="mr-2 h-4 w-4" />
@ -93,11 +102,46 @@ export default function AdminLayout() {
Data Sync
</Link>
</Button>
</>
);
}
export default function AdminLayout() {
return (
<div className="flex min-h-screen">
{/* Mobile top bar */}
<div className="md:hidden fixed top-0 left-0 right-0 z-10 flex h-14 items-center border-b px-4 gap-3 bg-muted/40">
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="shrink-0">
<Menu className="h-5 w-5" />
<span className="sr-only">Open menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-64 p-0">
<SheetHeader className="border-b px-6 h-16 flex justify-center">
<SheetTitle>Admin Panel</SheetTitle>
</SheetHeader>
<nav aria-label="Admin navigation" className="space-y-1 p-4">
<AdminNavLinks />
</nav>
</SheetContent>
</Sheet>
<h2 className="text-lg font-semibold">Admin Panel</h2>
</div>
{/* Desktop sidebar */}
<aside className="hidden md:block w-64 shrink-0 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 aria-label="Admin navigation" className="space-y-1 p-4">
<AdminNavLinks />
</nav>
</aside>
{/* Main Content */}
<main className="flex-1">
<main className="flex-1 min-w-0 pt-14 md:pt-0">
<Outlet />
</main>
</div>