admin fixes #85
5 changed files with 247 additions and 53 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
|
import type { SeasonStatus } from "~/models/season";
|
||||||
|
|
||||||
export type League = typeof schema.leagues.$inferSelect;
|
export type League = typeof schema.leagues.$inferSelect;
|
||||||
export type NewLeague = typeof schema.leagues.$inferInsert;
|
export type NewLeague = typeof schema.leagues.$inferInsert;
|
||||||
|
|
@ -54,6 +55,38 @@ export async function setCurrentSeason(
|
||||||
return await updateLeague(leagueId, { currentSeasonId: seasonId });
|
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?: {
|
export async function listLeagues(options?: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,7 @@ export default [
|
||||||
route("data-sync", "routes/admin.data-sync.tsx"),
|
route("data-sync", "routes/admin.data-sync.tsx"),
|
||||||
route("standings-snapshots", "routes/admin.standings-snapshots.tsx"),
|
route("standings-snapshots", "routes/admin.standings-snapshots.tsx"),
|
||||||
route("users", "routes/admin.users.tsx"),
|
route("users", "routes/admin.users.tsx"),
|
||||||
|
route("leagues", "routes/admin.leagues.tsx"),
|
||||||
]),
|
]),
|
||||||
route(
|
route(
|
||||||
"api/admin/export-sports-data",
|
"api/admin/export-sports-data",
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ export default function AdminDashboard({ loaderData, actionData }: Route.Compone
|
||||||
const systemIsPopulated = stats.sportsCount > 0;
|
const systemIsPopulated = stats.sportsCount > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-4 md:p-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
|
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
|
||||||
<p className="text-muted-foreground mt-2">
|
<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 { isUserAdmin } from "~/models/user";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "~/components/ui/sheet";
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Trophy,
|
Trophy,
|
||||||
|
|
@ -13,6 +20,8 @@ import {
|
||||||
Award,
|
Award,
|
||||||
Activity,
|
Activity,
|
||||||
Users,
|
Users,
|
||||||
|
Menu,
|
||||||
|
Shield,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export function meta(): Route.MetaDescriptors {
|
export function meta(): Route.MetaDescriptors {
|
||||||
|
|
@ -35,69 +44,104 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
return { isAdmin };
|
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() {
|
export default function AdminLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
{/* Sidebar */}
|
{/* Mobile top bar */}
|
||||||
<aside className="w-64 border-r bg-muted/40">
|
<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">
|
<div className="flex h-16 items-center border-b px-6">
|
||||||
<h2 className="text-lg font-semibold">Admin Panel</h2>
|
<h2 className="text-lg font-semibold">Admin Panel</h2>
|
||||||
</div>
|
</div>
|
||||||
<nav aria-label="Admin navigation" className="space-y-1 p-4">
|
<nav aria-label="Admin navigation" className="space-y-1 p-4">
|
||||||
<Button variant="ghost" className="w-full justify-start" asChild>
|
<AdminNavLinks />
|
||||||
<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>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1">
|
<main className="flex-1 min-w-0 pt-14 md:pt-0">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue