Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.

Expand All @@ -50,11 +50,24 @@ Middleware: `middleware.ts`

## Theme System

Uses CSS custom properties with `data-theme` attribute on `<html>`:
- 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 `<html>`. 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 `<body>` — pages should NOT add it themselves

## Environment Variables

Expand Down
16 changes: 15 additions & 1 deletion app/league/[id]/league-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ interface TeamWithOwnerAndScores {
league_id: string;
user_id?: string | null;
is_commish?: boolean | null;
activePlayers: number;
totalPlayers: number;
}

interface LeagueHomeProps {
Expand All @@ -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<TeamWithOwnerAndScores[]>([]);
const [showAddTeam, setShowAddTeam] = useState(false);
Expand Down Expand Up @@ -204,7 +211,7 @@ export default function LeagueHome({ teams, league_id, league, draftSettings, is
{weeks.length > 0 && (
<div className="flex justify-end items-center gap-4 px-6 py-2 text-sm text-muted-foreground">
{weeks.map(week => (
<span key={week} className="hidden sm:inline-block w-16 text-center">Wk {week}</span>
<span key={week} className="hidden sm:inline-block w-16 text-center">{weekLabel(week, league)}</span>
))}
<span className="w-20 text-center">Total</span>
</div>
Expand All @@ -226,6 +233,13 @@ export default function LeagueHome({ teams, league_id, league, draftSettings, is
</Link>
<p className="text-muted-foreground text-sm">
{team.owner || 'Unclaimed'}
{league.league === 'NCAAM' && team.totalPlayers > 0 && (
<span className="ml-2">
· <span className={team.activePlayers === 0 ? 'text-destructive' : 'text-primary'}>
{team.activePlayers}/{team.totalPlayers} active
</span>
</span>
)}
</p>
</div>
<div className="flex items-center gap-4">
Expand Down
28 changes: 25 additions & 3 deletions app/league/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>();
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<string, boolean> = {};
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,
};
});

Expand Down
16 changes: 14 additions & 2 deletions app/league/[id]/team/[teamid]/one-team.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ export default function OneTeam({ team }: { team: TeamWithPlayers & { leagues: L
<h2 className="text-lg font-semibold text-foreground">Team Total Score</h2>
<div className="text-2xl font-bold text-accent">{totalTeamScore.toFixed(1)}</div>
</div>
{team.leagues?.league === 'NCAAM' && team.players && team.players.length > 0 && (
<div className="mt-2 text-sm text-muted-foreground">
<span className="text-primary font-medium">
{team.players.filter((p: Player) => !p.eliminated).length}/{team.players.length}
</span> players active
</div>
)}
</CardContent>
</Card>
{orderedPlayers().map((player: Player) => {
Expand All @@ -57,7 +64,7 @@ export default function OneTeam({ team }: { team: TeamWithPlayers & { leagues: L
return (
<div
key={player.id}
className="flex items-center p-4 bg-background rounded-lg border border-border hover:border-accent transition-colors"
className={`flex items-center p-4 bg-background rounded-lg border border-border hover:border-accent transition-colors ${player.eliminated ? 'opacity-50' : ''}`}
>
<div className="flex-shrink-0">
<Image
Expand All @@ -80,10 +87,15 @@ export default function OneTeam({ team }: { team: TeamWithPlayers & { leagues: L
</p>
</div>

<div className="text-right">
<div className="text-right flex items-center gap-2">
<span className="inline-block px-2 py-1 bg-card rounded text-xs font-medium text-accent">
{player.position}
</span>
{player.eliminated && (
<span className="inline-block px-2 py-1 bg-destructive/10 rounded text-xs font-medium text-destructive">
Eliminated
</span>
)}
</div>
</div>

Expand Down
3 changes: 3 additions & 0 deletions lib/database.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading