diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 2973f93..fdfc631 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -32,6 +32,13 @@ import { } from "../hooks/useSleeper"; import { usePowerRankings } from "../hooks/usePowerRankings"; import { getFamilySeasons } from "../utils/leagueFamily"; +import { getProviderLabel } from "../utils/providerLabels"; +import { + isSuperflex as isSuperflexLeague, + isTightEndPremium, + getScoringType, + getLeagueFormat, +} from "../utils/sleeperNormalize"; import { useMyClaimedTeam } from "../hooks/useMyClaimedTeam"; import { useMyHuddles } from "../hooks/useHuddles"; import { Ticker } from "../widgets/dashboard/Ticker"; @@ -128,6 +135,18 @@ export function DashboardPage() { ? (leagueSettings["last_scored_leg"] as number) : null; + // Masthead subheading — e.g. "Sleeper 12-team PPR TEP Superflex Dynasty League". + const mastheadSubheading = [ + getProviderLabel(selectedLeague?.ref.provider), + leagueSettings["num_teams"] ? `${leagueSettings["num_teams"]}-team` : null, + getScoringType(selectedLeague?.scoringSettings ?? {}), + isTightEndPremium(selectedLeague?.scoringSettings ?? {}) ? "TEP" : null, + isSuperflexLeague(selectedLeague?.rosterPositions ?? []) ? "Superflex" : null, + `${getLeagueFormat(leagueSettings)} League`, + ] + .filter(Boolean) + .join(" "); + // Sleeper omits last_scored_leg for pre-draft / drafting leagues, so falling // back to playoff_week_start would (incorrectly) advertise weeks 17/18 as // navigable. Pin to 0 when the league hasn't kicked off so Scoreboard locks @@ -251,6 +270,7 @@ export function DashboardPage() { leagueName={selectedLeague?.name ?? ""} week={week} oldestYear={oldestYear} + subheading={mastheadSubheading} />
diff --git a/client/src/utils/providerLabels.ts b/client/src/utils/providerLabels.ts new file mode 100644 index 0000000..9d9a918 --- /dev/null +++ b/client/src/utils/providerLabels.ts @@ -0,0 +1,13 @@ +import type { ProviderId } from "../types/fantasy"; + +// Display copy per provider — not always a naive capitalize (e.g. ESPN is an +// acronym, not a titlecased word). +const PROVIDER_LABELS: Record = { + sleeper: "Sleeper", + espn: "ESPN", + yahoo: "Yahoo", +}; + +export function getProviderLabel(provider: ProviderId | undefined): string { + return provider ? PROVIDER_LABELS[provider] : ""; +} diff --git a/client/src/utils/sleeperNormalize.ts b/client/src/utils/sleeperNormalize.ts index 78087b8..e1df855 100644 --- a/client/src/utils/sleeperNormalize.ts +++ b/client/src/utils/sleeperNormalize.ts @@ -177,3 +177,54 @@ export function buildDefStatsKeyMap( return map; } + +// ── League format ──────────────────────────────────────────────────────────── + +/** + * A league is superflex when its roster_positions include a SUPER_FLEX slot + * (a flex spot that can also start a QB), as opposed to a plain FLEX slot. + * + * @param rosterPositions - League's `rosterPositions` field. + */ +export function isSuperflex(rosterPositions: string[]): boolean { + return rosterPositions.includes("SUPER_FLEX"); +} + +/** + * Tight end premium (TEP) leagues award tight ends extra points per + * reception on top of the standard `rec` value, via Sleeper's + * `bonus_rec_te` scoring key. + * + * @param scoringSettings - League's `scoringSettings` field. + */ +export function isTightEndPremium(scoringSettings: Record): boolean { + return (scoringSettings["bonus_rec_te"] ?? 0) > 0; +} + +/** + * Derive the league's reception scoring format from the `rec` key in + * scoring_settings: 1 point/reception is full PPR, 0.5 is half-PPR, and + * 0 (or unset) is standard. + * + * @param scoringSettings - League's `scoringSettings` field. + */ +export function getScoringType(scoringSettings: Record): string { + const rec = scoringSettings["rec"] ?? 0; + if (rec >= 1) return "PPR"; + if (rec > 0) return "Half-PPR"; + return "Standard"; +} + +/** + * League format comes from Sleeper's `settings.type` field: + * 0 = redraft, 1 = keeper, 2 = dynasty. + * + * @param settings - League's `settings` field. + */ +export function getLeagueFormat(settings: Record): string { + const type = Number(settings["type"] ?? 0); + if (type === 3) return "Chopped"; + if (type === 2) return "Dynasty"; + if (type === 1) return "Keeper"; + return "Redraft"; +} diff --git a/client/src/widgets/dashboard/Masthead.tsx b/client/src/widgets/dashboard/Masthead.tsx index 9c6a89e..5be38dd 100644 --- a/client/src/widgets/dashboard/Masthead.tsx +++ b/client/src/widgets/dashboard/Masthead.tsx @@ -10,15 +10,14 @@ * Wiring: `oldestYear` and `week` come from `DashboardPage`'s display-week * derivation; `leagueName` is the selected league's display name. */ -export function Masthead({ - leagueName, - week, - oldestYear, -}: { +type MastheadProps = { leagueName: string; week: number; oldestYear: string | null; -}) { + subheading?: string; +}; + +export function Masthead({ leagueName, week, oldestYear, subheading }: MastheadProps) { const now = new Date(); const dateStr = now.toLocaleDateString("en-US", { weekday: "long", @@ -39,7 +38,7 @@ export function Masthead({ {leagueName}
- [PLATFORM] [# OF TEAMS] [FORMAT] + {subheading}
diff --git a/server/src/providers/sleeper/index.ts b/server/src/providers/sleeper/index.ts index 3c5695f..385cd0a 100644 --- a/server/src/providers/sleeper/index.ts +++ b/server/src/providers/sleeper/index.ts @@ -44,6 +44,16 @@ import type { SleeperDraftPick, } from "../../services/sleeperService.js"; +// Sleeper's `settings.type`: 0 = redraft, 1 = keeper, 2 = dynasty, 3 = chopped. +// Chopped leagues play by different elimination rules we don't support yet, +// so they're filtered out of league lookup at the import boundary rather +// than handled (or half-handled) throughout the rest of the app. +const CHOPPED_LEAGUE_TYPE = 3; + +function isChoppedLeague(s: SleeperLeague): boolean { + return Number(s.settings?.type) === CHOPPED_LEAGUE_TYPE; +} + function toLeague(s: SleeperLeague): League { return { ref: { provider: "sleeper", leagueId: s.league_id }, @@ -253,7 +263,7 @@ export const sleeperProvider: FantasyProvider = { async getUserLeagues(userId: string, year: string): Promise { const leagues = await getSleeperLeagues(userId, year); - return leagues.map(toLeague); + return leagues.filter((l) => !isChoppedLeague(l)).map(toLeague); }, async getAllUserLeagues(userId: string): Promise { @@ -273,6 +283,7 @@ export const sleeperProvider: FantasyProvider = { seen.add(l.league_id); return true; }) + .filter((l) => !isChoppedLeague(l)) .sort( (a, b) => Number(b.season) - Number(a.season) || a.name.localeCompare(b.name),