admin fixes (#85)
This commit is contained in:
parent
ea84ffd915
commit
0034adaa9a
5 changed files with 247 additions and 53 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
116
app/routes/admin.leagues.tsx
Normal file
116
app/routes/admin.leagues.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<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" />
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminLayout() {
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 border-r bg-muted/40">
|
||||
{/* 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">
|
||||
<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>
|
||||
<AdminNavLinks />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1">
|
||||
<main className="flex-1 min-w-0 pt-14 md:pt-0">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue