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
20 changes: 20 additions & 0 deletions client/src/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -251,6 +270,7 @@ export function DashboardPage() {
leagueName={selectedLeague?.name ?? ""}
week={week}
oldestYear={oldestYear}
subheading={mastheadSubheading}
/>

<div className="px-3 sm:px-7 pt-4 pb-6 flex-1">
Expand Down
13 changes: 13 additions & 0 deletions client/src/utils/providerLabels.ts
Original file line number Diff line number Diff line change
@@ -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<ProviderId, string> = {
sleeper: "Sleeper",
espn: "ESPN",
yahoo: "Yahoo",
};

export function getProviderLabel(provider: ProviderId | undefined): string {
return provider ? PROVIDER_LABELS[provider] : "";
}
51 changes: 51 additions & 0 deletions client/src/utils/sleeperNormalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>): 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, number>): 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, unknown>): string {
const type = Number(settings["type"] ?? 0);
if (type === 3) return "Chopped";
if (type === 2) return "Dynasty";
if (type === 1) return "Keeper";
return "Redraft";
}
13 changes: 6 additions & 7 deletions client/src/widgets/dashboard/Masthead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -39,7 +38,7 @@ export function Masthead({
{leagueName}
</div>
<div className="mt-1 font-serif italic text-[13px] text-muted text-center">
[PLATFORM] [# OF TEAMS] [FORMAT]
{subheading}
</div>
</div>
<div className="text-[11px] text-muted tracking-wide font-sans text-center sm:text-right">
Expand Down
13 changes: 12 additions & 1 deletion server/src/providers/sleeper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -253,7 +263,7 @@ export const sleeperProvider: FantasyProvider = {

async getUserLeagues(userId: string, year: string): Promise<League[]> {
const leagues = await getSleeperLeagues(userId, year);
return leagues.map(toLeague);
return leagues.filter((l) => !isChoppedLeague(l)).map(toLeague);
},

async getAllUserLeagues(userId: string): Promise<League[]> {
Expand All @@ -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),
Expand Down