Add opencode oxlint plugin and UI refinements (#323)

* Add opencode oxlint plugin and UI refinements

- Add .opencode/plugins/oxlint.ts to run oxlint automatically on file edits
- Refactor RankingDisplay into shared StatHelpers component
- Add tied rank display support in home loader
- Update navbar with NavLink for active state styling
- Replace .reverse() with .toReversed() in RecentPicksFeed

* Remove unused RankChangeIndicator import in LeagueRow
This commit is contained in:
Chris Parsons 2026-04-24 11:11:20 -07:00 committed by GitHub
parent 3566a97a1e
commit e8e4981465
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 105 additions and 28 deletions

View file

@ -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}`)
}
},
}
}

View file

@ -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<string | undefined>(picks.at(-1)?.id);
const [animatingId, setAnimatingId] = useState<string | undefined>(undefined);

View file

@ -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 && (
<div className="flex items-center gap-4 border-t border-border/50 pt-2 sm:border-0 sm:pt-0 sm:shrink-0">
{currentRank !== undefined && (
<StatColumn label="Ranking">
<span className="text-2xl font-bold leading-none">#{currentRank}</span>
{previousRank !== undefined && previousRank !== currentRank && (
<RankChangeIndicator delta={previousRank - currentRank} />
)}
</StatColumn>
{displayRank !== undefined && (
<RankingDisplay
displayRank={displayRank}
rankChange={previousRank !== undefined && currentRank !== undefined && previousRank !== currentRank ? previousRank - currentRank : undefined}
/>
)}
{currentRank !== undefined && totalPoints !== undefined && <StatDivider />}
{totalPoints !== undefined && (

View file

@ -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 */}
<div className="flex items-center gap-4 w-full border-t border-border/30 pt-2 mt-1 sm:w-auto sm:border-0 sm:pt-0 sm:mt-0 sm:shrink-0">
<StatColumn label="Ranking">
<span className="text-2xl font-bold leading-none">#{String(entry.displayRank).replace(/^T/, "")}</span>
{entry.rankChange !== undefined && entry.rankChange !== 0 && (
<RankChangeIndicator delta={entry.rankChange} />
)}
</StatColumn>
<RankingDisplay displayRank={entry.displayRank} rankChange={entry.rankChange} />
<StatDivider />
<StatColumn label="Points">
<span className="text-2xl font-bold leading-none text-electric">

View file

@ -39,6 +39,23 @@ export function RankChangeIndicator({ delta }: { delta: number }) {
);
}
export function RankingDisplay({
displayRank,
rankChange,
}: {
displayRank: string | number;
rankChange?: number;
}) {
return (
<StatColumn label="Ranking">
<span className="text-2xl font-bold leading-none">{displayRank}</span>
{rankChange !== undefined && rankChange !== 0 && (
<RankChangeIndicator delta={rankChange} />
)}
</StatColumn>
);
}
export function PointChangeIndicator({ delta }: { delta: number }) {
if (delta >= 0) {
return (

View file

@ -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 */}
<div className="flex items-center gap-2">
<Link to="/" className="flex items-center mr-2">
<img src={logoUrl} alt="Brackt" className="h-14" />
<img src="/logo.svg" alt="Brackt" className="h-14" />
</Link>
<nav aria-label="Main navigation" className="hidden md:flex items-center gap-1">
<Link to="/how-to-play" className="text-sm font-semibold uppercase tracking-wide text-muted-foreground transition-colors hover:bg-transparent hover:text-transparent hover:bg-clip-text hover:[background:var(--brackt-gradient)] px-3 py-2">How To Play</Link>
<Link to="/rules" className="text-sm font-semibold uppercase tracking-wide text-muted-foreground transition-colors hover:bg-transparent hover:text-transparent hover:bg-clip-text hover:[background:var(--brackt-gradient)] px-3 py-2">Rules</Link>
<SignedOut>
<NavLink to="/" end className={navLinkClass}>Home</NavLink>
</SignedOut>
<SignedIn>
<NavLink to="/" end className={navLinkClass}>Dashboard</NavLink>
</SignedIn>
<NavLink to="/how-to-play" className={navLinkClass}>How To Play</NavLink>
<NavLink to="/rules" className={navLinkClass}>Rules</NavLink>
</nav>
</div>
@ -102,20 +120,40 @@ export function Navbar({ isAdmin }: NavbarProps) {
<SheetTitle>Menu</SheetTitle>
</SheetHeader>
<nav aria-label="Mobile navigation" className="flex flex-col gap-4 mt-8">
<Link
<SignedOut>
<NavLink
to="/"
end
className={mobileNavLinkClass}
onClick={() => setOpen(false)}
>
Home
</NavLink>
</SignedOut>
<SignedIn>
<NavLink
to="/"
end
className={mobileNavLinkClass}
onClick={() => setOpen(false)}
>
Dashboard
</NavLink>
</SignedIn>
<NavLink
to="/how-to-play"
className="block px-4 py-2 text-lg font-medium hover:bg-accent rounded-md transition-colors"
className={mobileNavLinkClass}
onClick={() => setOpen(false)}
>
How To Play
</Link>
<Link
</NavLink>
<NavLink
to="/rules"
className="block px-4 py-2 text-lg font-medium hover:bg-accent rounded-md transition-colors"
className={mobileNavLinkClass}
onClick={() => setOpen(false)}
>
Rules
</Link>
</NavLink>
</nav>
</SheetContent>
</Sheet>

View file

@ -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,