diff --git a/.opencode/plugins/oxlint.ts b/.opencode/plugins/oxlint.ts new file mode 100644 index 0000000..365eaa9 --- /dev/null +++ b/.opencode/plugins/oxlint.ts @@ -0,0 +1,20 @@ +export const OxlintPlugin = async ({ $ }: { $: any }) => { + return { + "file.edited": async ({ filePath }: { filePath: string }) => { + if ( + !filePath.endsWith(".ts") && + !filePath.endsWith(".tsx") && + !filePath.endsWith(".js") && + !filePath.endsWith(".jsx") + ) { + return + } + + try { + await $`npm run lint:path -- ${filePath}`.quiet() + } catch (e: any) { + throw new Error(`oxlint found issues in ${filePath}:\n${e}`) + } + }, + } +} diff --git a/app/components/draft/RecentPicksFeed.tsx b/app/components/draft/RecentPicksFeed.tsx index 13ca77f..959836b 100644 --- a/app/components/draft/RecentPicksFeed.tsx +++ b/app/components/draft/RecentPicksFeed.tsx @@ -9,7 +9,7 @@ type Pick = { }; export const RecentPicksFeed = memo(function RecentPicksFeed({ picks }: { picks: Pick[] }) { - const recentPicks = picks.slice(-3).reverse(); + const recentPicks = picks.slice(-3).toReversed(); const prevNewestIdRef = useRef(picks.at(-1)?.id); const [animatingId, setAnimatingId] = useState(undefined); diff --git a/app/components/league/LeagueRow.tsx b/app/components/league/LeagueRow.tsx index a0414b6..0784d69 100644 --- a/app/components/league/LeagueRow.tsx +++ b/app/components/league/LeagueRow.tsx @@ -2,7 +2,7 @@ import { formatDistanceToNow } from "date-fns"; import { Link } from "react-router"; import { Button } from "~/components/ui/button"; import { LeagueAvatar } from "./LeagueAvatar"; -import { StatColumn, StatDivider, RankChangeIndicator } from "./StatHelpers"; +import { StatColumn, StatDivider, RankingDisplay } from "./StatHelpers"; export interface LeagueRowProps { leagueId: string; @@ -10,6 +10,7 @@ export interface LeagueRowProps { numSports: number; status: "draft" | "active" | "pre_draft" | "completed"; seasonId?: string; + displayRank?: string | number; currentRank?: number; totalPoints?: number; previousRank?: number; @@ -93,6 +94,7 @@ function DraftRow({ leagueId, leagueName, seasonId, picksUntilMyTurn }: LeagueRo function ActiveRow({ leagueId, leagueName, + displayRank, currentRank, totalPoints, previousRank, @@ -116,13 +118,11 @@ function ActiveRow({ {/* Stats — second row on mobile, right side on desktop */} {showStats && (
- {currentRank !== undefined && ( - - #{currentRank} - {previousRank !== undefined && previousRank !== currentRank && ( - - )} - + {displayRank !== undefined && ( + )} {currentRank !== undefined && totalPoints !== undefined && } {totalPoints !== undefined && ( diff --git a/app/components/league/StandingsPreview.tsx b/app/components/league/StandingsPreview.tsx index 1d313ee..c6b5df2 100644 --- a/app/components/league/StandingsPreview.tsx +++ b/app/components/league/StandingsPreview.tsx @@ -4,7 +4,7 @@ import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; import { GradientIcon } from "~/components/ui/GradientIcon"; import { TeamAvatar } from "~/components/TeamAvatar"; -import { StatColumn, StatDivider, RankChangeIndicator, PointChangeIndicator } from "./StatHelpers"; +import { StatColumn, StatDivider, PointChangeIndicator, RankingDisplay } from "./StatHelpers"; // ─── Row styles ─────────────────────────────────────────────────────────────── @@ -38,12 +38,7 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) { {/* Right: stats — second row on mobile */}
- - #{String(entry.displayRank).replace(/^T/, "")} - {entry.rankChange !== undefined && entry.rankChange !== 0 && ( - - )} - + diff --git a/app/components/league/StatHelpers.tsx b/app/components/league/StatHelpers.tsx index e1db42a..a21525c 100644 --- a/app/components/league/StatHelpers.tsx +++ b/app/components/league/StatHelpers.tsx @@ -39,6 +39,23 @@ export function RankChangeIndicator({ delta }: { delta: number }) { ); } +export function RankingDisplay({ + displayRank, + rankChange, +}: { + displayRank: string | number; + rankChange?: number; +}) { + return ( + + {displayRank} + {rankChange !== undefined && rankChange !== 0 && ( + + )} + + ); +} + export function PointChangeIndicator({ delta }: { delta: number }) { if (delta >= 0) { return ( diff --git a/app/components/navbar.tsx b/app/components/navbar.tsx index 836ea2f..8555596 100644 --- a/app/components/navbar.tsx +++ b/app/components/navbar.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Link } from "react-router"; +import { Link, NavLink } from "react-router"; import { Sheet, SheetContent, @@ -10,7 +10,19 @@ import { import { Button } from "~/components/ui/button"; import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/react-router"; import { HelpCircle, MenuIcon, Settings } from "lucide-react"; -import logoUrl from "../../public/logo.svg?url"; + +const NAV_LINK_CLASS = + "relative text-sm font-semibold uppercase tracking-wide text-muted-foreground px-3 py-2 transition-colors " + + "after:content-[''] after:absolute after:bottom-0 after:left-3 after:right-3 after:h-[3px] " + + "after:scale-x-0 hover:after:scale-x-100 after:transition-transform after:[background:var(--brackt-gradient)]"; + +function navLinkClass({ isActive }: { isActive: boolean }) { + return `${NAV_LINK_CLASS}${isActive ? " !text-emerald-400" : ""}`; +} + +function mobileNavLinkClass({ isActive }: { isActive: boolean }) { + return `block px-4 py-2 text-lg font-medium rounded-md transition-colors${isActive ? " text-emerald-400" : " text-muted-foreground"}`; +} interface NavbarProps { isAdmin: boolean; @@ -25,11 +37,17 @@ export function Navbar({ isAdmin }: NavbarProps) { {/* Left: Logo + Nav links */}
- Brackt + Brackt
@@ -102,20 +120,40 @@ export function Navbar({ isAdmin }: NavbarProps) { Menu diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 84045c6..690dbc5 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -11,8 +11,9 @@ import { findSeasonById, findCurrentSeasonWithSports } from "~/models/season"; import { findTeamByOwnerAndSeason } from "~/models/team"; import { getDraftedParticipantsBySportsSeason } from "~/models/draft-pick"; import { getUpcomingEventsForDraftedParticipants } from "~/models/scoring-event"; -import { getTeamStanding } from "~/models/standings"; +import { getTeamStanding, getSeasonStandings } from "~/models/standings"; import { getSeasonCompletionPercentage } from "~/lib/season-helpers.server"; +import { buildTiedRankChecker, getDisplayRank } from "~/lib/standings-display"; import { findDraftSlotsBySeasonId } from "~/models/draft-slot"; import { getTeamForPick } from "~/lib/draft-order"; import type { CalendarPanelEvent } from "~/components/sport-season/UpcomingCalendarPanel"; @@ -57,6 +58,7 @@ export async function loader(args: Route.LoaderArgs) { let currentRank: number | undefined; let totalPoints: number | undefined; let previousRank: number | undefined; + let displayRank: string | number | undefined; let completionPercentage = 0; let picksUntilMyTurn: number | undefined; let draftPosition: number | undefined; @@ -100,6 +102,9 @@ export async function loader(args: Route.LoaderArgs) { currentRank = standing.currentRank ?? undefined; totalPoints = standing.totalPoints ?? undefined; previousRank = standing.previousRank ?? undefined; + const allStandings = await getSeasonStandings(league.currentSeasonId); + const isTied = buildTiedRankChecker(allStandings.map((s) => s.currentRank)); + displayRank = getDisplayRank(standing, allStandings.length, isTied(standing.currentRank)); } else if (myTeam && season?.status === "active") { // No standing row yet (no scoring events) — default to rank 1, 0 points currentRank = 1; @@ -138,6 +143,7 @@ export async function loader(args: Route.LoaderArgs) { ...league, currentSeason: season, numSports, + displayRank, currentRank, totalPoints, previousRank, @@ -195,6 +201,7 @@ export default function Home({ loaderData }: Route.ComponentProps) { status: isValidStatus(rawStatus) ? rawStatus : "pre_draft", seasonId: league.currentSeasonId ?? undefined, currentRank: league.currentRank, + displayRank: league.displayRank, totalPoints: league.totalPoints, previousRank: league.previousRank, completionPercentage: league.completionPercentage,