diff --git a/CLAUDE.md b/CLAUDE.md index 9ce9caa..7a62548 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ npm run lint # Run ESLint Fanteasy is a fantasy sports league application supporting NFL, NBA, and NCAAM leagues with real-time drafting. -**Tech Stack:** Next.js 15 (App Router) + React 19 + Supabase (auth, database, realtime) + Tailwind CSS +**Tech Stack:** Next.js 15 (App Router) + React 19 + Supabase (auth, database, realtime) + Tailwind CSS + shadcn/ui + Lucide icons ### Key Directories - `app/` - Next.js App Router pages and components (colocated by feature) @@ -40,7 +40,7 @@ Auto-generated types from Supabase are in `lib/database.types.ts`. Extended rela ## Supabase Auth - Critical Pattern -**MUST use `@supabase/ssr` with `getAll`/`setAll` pattern. NEVER use deprecated `get`/`set`/`remove` methods or `@supabase/auth-helpers-nextjs`.** +**MUST use `@supabase/ssr` with `getAll`/`setAll` pattern. NEVER use deprecated `get`/`set`/`remove` methods or `@supabase/auth-helpers-nextjs`.** Use `getUser()` (validates JWT server-side) — NEVER `getSession()` (trusts unverified JWT from cookie). See `documentation/development/auth.md` for correct implementation patterns. @@ -50,11 +50,24 @@ Middleware: `middleware.ts` ## Theme System -Uses CSS custom properties with `data-theme` attribute on ``: -- Dark mode (default): `--background`, `--surface`, `--primary-text` etc. -- Light mode: `html[data-theme="light"]` overrides -- Theme state persisted in localStorage -- Custom Tailwind colors defined in `tailwind.config.ts`: liquid-lava, dark-void, snow, dusty-grey, gluon-grey, slate-grey +Uses CSS custom properties with `data-theme` attribute on ``. Theme state persisted in localStorage. + +**7 themes:** dark (default), light, midnight, forest, crimson, sunset, arctic — defined in `app/globals.css` + +**Color tokens — always use shadcn semantic classes, NOT raw brand colors:** +- `text-foreground` / `text-muted-foreground` (not `text-primary-text` or `text-secondary-text` — removed) +- `bg-background` / `bg-card` / `bg-muted` (not `bg-surface` — removed) +- `text-primary` / `text-accent` for interactive/highlight elements (not `text-liquid-lava`) +- `text-destructive` for errors/delete actions +- `border-border` (not `border-slate-grey`) +- Raw brand colors (`liquid-lava`, `dark-void`, etc.) exist in Tailwind config but should only be used for truly brand-specific needs + +**UI component conventions:** +- Use shadcn/ui components (`Card`, `Button`, `Input`, `Label`, `Tabs`, etc.) — not raw HTML equivalents +- Use Lucide React for icons — no emojis as UI elements +- Use `sonner` toast (`toast.success/error/warning`) — not `alert()` +- Global `NavBar` in `layout.tsx` handles navigation, theme toggle, and auth — pages should NOT render their own nav bars +- `min-h-screen bg-background` is on `` — pages should NOT add it themselves ## Environment Variables diff --git a/app/league/[id]/league-home.tsx b/app/league/[id]/league-home.tsx index 1d5d62a..f7d5ce1 100644 --- a/app/league/[id]/league-home.tsx +++ b/app/league/[id]/league-home.tsx @@ -34,6 +34,8 @@ interface TeamWithOwnerAndScores { league_id: string; user_id?: string | null; is_commish?: boolean | null; + activePlayers: number; + totalPlayers: number; } interface LeagueHomeProps { @@ -45,6 +47,11 @@ interface LeagueHomeProps { weeks: number[]; } +function weekLabel(week: number, league: League): string { + if (league.league === 'NFL') return `Wk ${week}`; + return `G${week}`; +} + export default function LeagueHome({ teams, league_id, league, draftSettings, isCommissioner, weeks }: LeagueHomeProps) { const [sortedTeams, setSortedTeams] = useState([]); const [showAddTeam, setShowAddTeam] = useState(false); @@ -204,7 +211,7 @@ export default function LeagueHome({ teams, league_id, league, draftSettings, is {weeks.length > 0 && (
{weeks.map(week => ( - Wk {week} + {weekLabel(week, league)} ))} Total
@@ -226,6 +233,13 @@ export default function LeagueHome({ teams, league_id, league, draftSettings, is

{team.owner || 'Unclaimed'} + {league.league === 'NCAAM' && team.totalPlayers > 0 && ( + + · + {team.activePlayers}/{team.totalPlayers} active + + + )}

diff --git a/app/league/[id]/page.tsx b/app/league/[id]/page.tsx index 26a54df..107dedc 100644 --- a/app/league/[id]/page.tsx +++ b/app/league/[id]/page.tsx @@ -191,20 +191,42 @@ export default async function League(props: { params: Promise<{ id: LeagueID }> }; })); - // Collect all weeks that have scores across all teams + // Collect all weeks that have scores across all teams (filter out NaN from null week_numbers) const allWeeks = new Set(); teamScores.forEach(ts => { - Object.keys(ts.weeklyScores).forEach(week => allWeeks.add(parseInt(week))); + Object.keys(ts.weeklyScores).forEach(week => { + const parsed = parseInt(week); + if (!isNaN(parsed)) allWeeks.add(parsed); + }); }); const sortedWeeks = Array.from(allWeeks).sort((a, b) => a - b); + // For NCAAM Best Ball, fetch eliminated status for all rostered players + let playerEliminated: Record = {}; + if (leagueData.league === 'NCAAM') { + const allPlayerIds = teamsData.flatMap(t => t.team_players || []); + if (allPlayerIds.length > 0) { + const { data: playerStatuses } = await supabase + .from('players') + .select('id, eliminated') + .in('id', allPlayerIds); + (playerStatuses || []).forEach(p => { + playerEliminated[p.id] = p.eliminated ?? false; + }); + } + } + const teams = teamsData.map(team => { const scoreData = teamScores.find(score => score.teamId === team.id); + const playerIds = team.team_players || []; + const activePlayers = playerIds.filter((id: string) => !playerEliminated[id]).length; return { ...team, owner: team.profiles?.full_name, totalScore: scoreData?.totalScore || 0, - weeklyScores: scoreData?.weeklyScores || {} + weeklyScores: scoreData?.weeklyScores || {}, + activePlayers, + totalPlayers: playerIds.length, }; }); diff --git a/app/league/[id]/team/[teamid]/one-team.tsx b/app/league/[id]/team/[teamid]/one-team.tsx index 4a78dee..4222a96 100644 --- a/app/league/[id]/team/[teamid]/one-team.tsx +++ b/app/league/[id]/team/[teamid]/one-team.tsx @@ -47,6 +47,13 @@ export default function OneTeam({ team }: { team: TeamWithPlayers & { leagues: L

Team Total Score

{totalTeamScore.toFixed(1)}
+ {team.leagues?.league === 'NCAAM' && team.players && team.players.length > 0 && ( +
+ + {team.players.filter((p: Player) => !p.eliminated).length}/{team.players.length} + players active +
+ )} {orderedPlayers().map((player: Player) => { @@ -57,7 +64,7 @@ export default function OneTeam({ team }: { team: TeamWithPlayers & { leagues: L return (
-
+
{player.position} + {player.eliminated && ( + + Eliminated + + )}
diff --git a/lib/database.types.ts b/lib/database.types.ts index 1e680d7..2410b2c 100644 --- a/lib/database.types.ts +++ b/lib/database.types.ts @@ -539,6 +539,7 @@ export type Database = { carries: number | null completions: number | null created_at: string + eliminated: boolean | null external_id: string | null fumbles: number | null games: number | null @@ -570,6 +571,7 @@ export type Database = { carries?: number | null completions?: number | null created_at?: string + eliminated?: boolean | null external_id?: string | null fumbles?: number | null games?: number | null @@ -601,6 +603,7 @@ export type Database = { carries?: number | null completions?: number | null created_at?: string + eliminated?: boolean | null external_id?: string | null fumbles?: number | null games?: number | null