From ddb646402c3163144d7b93fee4286d7a6f405770 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 00:14:31 +0000 Subject: [PATCH] Add admin leagues page and mobile-friendly admin layout - 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 --- app/models/league.ts | 33 ++++++++ app/routes.ts | 1 + app/routes/admin._index.tsx | 2 +- app/routes/admin.leagues.tsx | 116 +++++++++++++++++++++++++++ app/routes/admin.tsx | 148 +++++++++++++++++++++++------------ 5 files changed, 247 insertions(+), 53 deletions(-) create mode 100644 app/routes/admin.leagues.tsx diff --git a/app/models/league.ts b/app/models/league.ts index 3946661..cca1981 100644 --- a/app/models/league.ts +++ b/app/models/league.ts @@ -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 { + 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; diff --git a/app/routes.ts b/app/routes.ts index 1afea04..2ffa9a4 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -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", diff --git a/app/routes/admin._index.tsx b/app/routes/admin._index.tsx index 0438a5e..b84b432 100644 --- a/app/routes/admin._index.tsx +++ b/app/routes/admin._index.tsx @@ -273,7 +273,7 @@ export default function AdminDashboard({ loaderData, actionData }: Route.Compone const systemIsPopulated = stats.sportsCount > 0; return ( -
+

Admin Dashboard

diff --git a/app/routes/admin.leagues.tsx b/app/routes/admin.leagues.tsx new file mode 100644 index 0000000..61a4675 --- /dev/null +++ b/app/routes/admin.leagues.tsx @@ -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 No Season; + } + switch (status) { + case "pre_draft": + return Pre-Draft; + case "draft": + return ( + + Drafting + + ); + case "active": + return ( + + Active + + ); + case "completed": + return Completed; + } +} + +export default function AdminLeagues({ loaderData }: Route.ComponentProps) { + const { leagues } = loaderData; + + return ( +

+
+

Leagues

+

{leagues.length} total leagues

+
+ + + + All Leagues + Every league in the system with their current season status. + + + + + + League Name + Season Year + Status + Created + + + + {leagues.length === 0 && ( + + + No leagues found. + + + )} + {leagues.map((league) => ( + + + + {league.name} + + + + {league.currentSeasonYear ?? "—"} + + + + + + {new Date(league.createdAt).toLocaleDateString()} + + + ))} + +
+
+
+
+ ); +} diff --git a/app/routes/admin.tsx b/app/routes/admin.tsx index a7338f8..3cef405 100644 --- a/app/routes/admin.tsx +++ b/app/routes/admin.tsx @@ -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,69 +44,104 @@ export async function loader(args: Route.LoaderArgs) { return { isAdmin }; } +function AdminNavLinks() { + return ( + <> + + + + + + + + +
+ + + ); +} + export default function AdminLayout() { return (
- {/* Sidebar */} -