feat: add current season tracking and active league listing by user

This commit is contained in:
Chris Parsons 2025-10-11 21:10:01 -07:00
parent 226cdd0ea7
commit 5f32ec05af
7 changed files with 503 additions and 9 deletions

View file

@ -1,4 +1,4 @@
import { eq } from "drizzle-orm";
import { eq, desc } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
@ -47,6 +47,13 @@ export async function deleteLeague(id: string): Promise<void> {
await db.delete(schema.leagues).where(eq(schema.leagues.id, id));
}
export async function setCurrentSeason(
leagueId: string,
seasonId: string | null
): Promise<League> {
return await updateLeague(leagueId, { currentSeasonId: seasonId });
}
export async function listLeagues(options?: {
limit?: number;
offset?: number;
@ -58,3 +65,26 @@ export async function listLeagues(options?: {
orderBy: (leagues, { desc }) => [desc(leagues.createdAt)],
});
}
export async function findLeaguesWithActiveSeasonsByUserId(
userId: string
): Promise<League[]> {
const db = database();
// Query to find all leagues where user has a team in the current season
const leagues = await db
.selectDistinct({
id: schema.leagues.id,
name: schema.leagues.name,
createdBy: schema.leagues.createdBy,
currentSeasonId: schema.leagues.currentSeasonId,
createdAt: schema.leagues.createdAt,
updatedAt: schema.leagues.updatedAt,
})
.from(schema.leagues)
.innerJoin(schema.teams, eq(schema.teams.seasonId, schema.leagues.currentSeasonId))
.where(eq(schema.teams.ownerId, userId))
.orderBy(desc(schema.leagues.createdAt));
return leagues;
}

View file

@ -1,10 +1,19 @@
import { useEffect } from "react";
import { useSearchParams } from "react-router";
import { Link, useSearchParams } from "react-router";
import { toast } from "sonner";
import { getAuth } from "@clerk/react-router/server";
import type { Route } from "./+types/home";
import { Welcome } from "../welcome/welcome";
import { expressValueContext } from "~/contexts/express";
import { findLeaguesWithActiveSeasonsByUserId } from "~/models/league";
import { findSeasonById } from "~/models/season";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
export function meta() {
return [
@ -16,22 +25,118 @@ export function meta() {
];
}
export async function loader({ context }: Route.LoaderArgs) {
export async function loader(args: Route.LoaderArgs) {
const { userId } = await getAuth(args);
if (!userId) {
return { leagues: [], isLoggedIn: false };
}
// Fetch leagues where user has a team in the current season
const leagues = await findLeaguesWithActiveSeasonsByUserId(userId);
// Fetch season details for each league's current season
const leaguesWithSeasons = await Promise.all(
leagues.map(async (league) => {
const season = league.currentSeasonId
? await findSeasonById(league.currentSeasonId)
: null;
return {
...league,
currentSeason: season,
};
})
);
return {
message: context.get(expressValueContext) || "No message from Express",
leagues: leaguesWithSeasons,
isLoggedIn: true,
};
}
export default function Home({ loaderData }: Route.ComponentProps) {
const { leagues, isLoggedIn } = loaderData;
const [searchParams, setSearchParams] = useSearchParams();
useEffect(() => {
if (searchParams.get("deleted") === "true") {
toast.success("League deleted successfully");
// Remove the query parameter
setSearchParams({});
}
}, [searchParams, setSearchParams]);
return <Welcome message={loaderData.message} />;
if (!isLoggedIn) {
return (
<div className="container mx-auto py-16 px-4 text-center">
<h1 className="text-4xl font-bold mb-4">Welcome to Brackt</h1>
<p className="text-xl text-muted-foreground mb-8">
Please sign in to view your leagues
</p>
</div>
);
}
return (
<div className="container mx-auto py-8 px-4">
<div className="flex items-center justify-between mb-8">
<h1 className="text-4xl font-bold">My Leagues</h1>
<Button asChild>
<Link to="/leagues/new">Create New League</Link>
</Button>
</div>
{leagues.length === 0 ? (
<Card>
<CardHeader>
<CardTitle>No Active Leagues</CardTitle>
<CardDescription>
You don't have any teams in leagues with active seasons yet
</CardDescription>
</CardHeader>
<CardContent>
<Button asChild>
<Link to="/leagues/new">Create a New League</Link>
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{leagues.map((league) => (
<Link key={league.id} to={`/leagues/${league.id}`}>
<Card className="hover:border-primary transition-colors cursor-pointer h-full">
<CardHeader>
<CardTitle>{league.name}</CardTitle>
<CardDescription>
Created {new Date(league.createdAt).toLocaleDateString()}
</CardDescription>
</CardHeader>
<CardContent>
{league.currentSeason ? (
<div className="space-y-2">
<div>
<p className="text-sm text-muted-foreground">
Current Season
</p>
<p className="font-medium">{league.currentSeason.year}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Status</p>
<p className="font-medium capitalize">
{league.currentSeason.status.replace("_", " ")}
</p>
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">
No active season
</p>
)}
</CardContent>
</Card>
</Link>
))}
</div>
)}
</div>
);
}

View file

@ -1,6 +1,6 @@
import { Form, Link, redirect } from "react-router";
import { getAuth } from "@clerk/react-router/server";
import { createLeague } from "~/models/league";
import { createLeague, setCurrentSeason } from "~/models/league";
import { createSeason } from "~/models/season";
import { createManyTeams } from "~/models/team";
import { createCommissioner } from "~/models/commissioner";
@ -70,6 +70,9 @@ export async function action(args: Route.ActionArgs) {
status: "pre_draft",
});
// Set this as the current season for the league
await setCurrentSeason(league.id, season.id);
// Create teams with placeholder names
const teams = Array.from({ length: teamCountNum }, (_, i) => ({
seasonId: season.id,

View file

@ -25,6 +25,7 @@ export const leagues = pgTable("leagues", {
id: uuid("id").primaryKey().defaultRandom(),
name: varchar("name", { length: 255 }).notNull(),
createdBy: varchar("created_by", { length: 255 }).notNull(), // Clerk user ID
currentSeasonId: uuid("current_season_id"), // References the active season
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

View file

@ -0,0 +1 @@
ALTER TABLE "leagues" ADD COLUMN "current_season_id" uuid;

View file

@ -0,0 +1,347 @@
{
"id": "afd92b21-6a89-4b8c-80ac-414a320db897",
"prevId": "6839ce43-d04c-4246-9b00-7820935e1166",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.commissioners": {
"name": "commissioners",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"league_id": {
"name": "league_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"commissioners_league_id_leagues_id_fk": {
"name": "commissioners_league_id_leagues_id_fk",
"tableFrom": "commissioners",
"tableTo": "leagues",
"columnsFrom": [
"league_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.leagues": {
"name": "leagues",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"created_by": {
"name": "created_by",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"current_season_id": {
"name": "current_season_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.seasons": {
"name": "seasons",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"league_id": {
"name": "league_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"year": {
"name": "year",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "season_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'pre_draft'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"seasons_league_id_leagues_id_fk": {
"name": "seasons_league_id_leagues_id_fk",
"tableFrom": "seasons",
"tableTo": "leagues",
"columnsFrom": [
"league_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.teams": {
"name": "teams",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"season_id": {
"name": "season_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"owner_id": {
"name": "owner_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"teams_season_id_seasons_id_fk": {
"name": "teams_season_id_seasons_id_fk",
"tableFrom": "teams",
"tableTo": "seasons",
"columnsFrom": [
"season_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"clerk_id": {
"name": "clerk_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"display_name": {
"name": "display_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"first_name": {
"name": "first_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"last_name": {
"name": "last_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"image_url": {
"name": "image_url",
"type": "varchar(512)",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_clerk_id_unique": {
"name": "users_clerk_id_unique",
"nullsNotDistinct": false,
"columns": [
"clerk_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.season_status": {
"name": "season_status",
"schema": "public",
"values": [
"pre_draft",
"draft",
"active",
"completed"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -43,6 +43,13 @@
"when": 1760169537260,
"tag": "0005_abnormal_thunderbird",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1760241924414,
"tag": "0006_needy_junta",
"breakpoints": true
}
]
}