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:
parent
3566a97a1e
commit
e8e4981465
7 changed files with 105 additions and 28 deletions
20
.opencode/plugins/oxlint.ts
Normal file
20
.opencode/plugins/oxlint.ts
Normal 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}`)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue