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[] }) {
|
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 prevNewestIdRef = useRef<string | undefined>(picks.at(-1)?.id);
|
||||||
const [animatingId, setAnimatingId] = useState<string | undefined>(undefined);
|
const [animatingId, setAnimatingId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { formatDistanceToNow } from "date-fns";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { LeagueAvatar } from "./LeagueAvatar";
|
import { LeagueAvatar } from "./LeagueAvatar";
|
||||||
import { StatColumn, StatDivider, RankChangeIndicator } from "./StatHelpers";
|
import { StatColumn, StatDivider, RankingDisplay } from "./StatHelpers";
|
||||||
|
|
||||||
export interface LeagueRowProps {
|
export interface LeagueRowProps {
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
|
|
@ -10,6 +10,7 @@ export interface LeagueRowProps {
|
||||||
numSports: number;
|
numSports: number;
|
||||||
status: "draft" | "active" | "pre_draft" | "completed";
|
status: "draft" | "active" | "pre_draft" | "completed";
|
||||||
seasonId?: string;
|
seasonId?: string;
|
||||||
|
displayRank?: string | number;
|
||||||
currentRank?: number;
|
currentRank?: number;
|
||||||
totalPoints?: number;
|
totalPoints?: number;
|
||||||
previousRank?: number;
|
previousRank?: number;
|
||||||
|
|
@ -93,6 +94,7 @@ function DraftRow({ leagueId, leagueName, seasonId, picksUntilMyTurn }: LeagueRo
|
||||||
function ActiveRow({
|
function ActiveRow({
|
||||||
leagueId,
|
leagueId,
|
||||||
leagueName,
|
leagueName,
|
||||||
|
displayRank,
|
||||||
currentRank,
|
currentRank,
|
||||||
totalPoints,
|
totalPoints,
|
||||||
previousRank,
|
previousRank,
|
||||||
|
|
@ -116,13 +118,11 @@ function ActiveRow({
|
||||||
{/* Stats — second row on mobile, right side on desktop */}
|
{/* Stats — second row on mobile, right side on desktop */}
|
||||||
{showStats && (
|
{showStats && (
|
||||||
<div className="flex items-center gap-4 border-t border-border/50 pt-2 sm:border-0 sm:pt-0 sm:shrink-0">
|
<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 && (
|
{displayRank !== undefined && (
|
||||||
<StatColumn label="Ranking">
|
<RankingDisplay
|
||||||
<span className="text-2xl font-bold leading-none">#{currentRank}</span>
|
displayRank={displayRank}
|
||||||
{previousRank !== undefined && previousRank !== currentRank && (
|
rankChange={previousRank !== undefined && currentRank !== undefined && previousRank !== currentRank ? previousRank - currentRank : undefined}
|
||||||
<RankChangeIndicator delta={previousRank - currentRank} />
|
/>
|
||||||
)}
|
|
||||||
</StatColumn>
|
|
||||||
)}
|
)}
|
||||||
{currentRank !== undefined && totalPoints !== undefined && <StatDivider />}
|
{currentRank !== undefined && totalPoints !== undefined && <StatDivider />}
|
||||||
{totalPoints !== undefined && (
|
{totalPoints !== undefined && (
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader } from "~/components/ui/card";
|
import { Card, CardContent, CardHeader } from "~/components/ui/card";
|
||||||
import { GradientIcon } from "~/components/ui/GradientIcon";
|
import { GradientIcon } from "~/components/ui/GradientIcon";
|
||||||
import { TeamAvatar } from "~/components/TeamAvatar";
|
import { TeamAvatar } from "~/components/TeamAvatar";
|
||||||
import { StatColumn, StatDivider, RankChangeIndicator, PointChangeIndicator } from "./StatHelpers";
|
import { StatColumn, StatDivider, PointChangeIndicator, RankingDisplay } from "./StatHelpers";
|
||||||
|
|
||||||
// ─── Row styles ───────────────────────────────────────────────────────────────
|
// ─── Row styles ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -38,12 +38,7 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
|
||||||
|
|
||||||
{/* Right: stats — second row on mobile */}
|
{/* 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">
|
<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">
|
<RankingDisplay displayRank={entry.displayRank} rankChange={entry.rankChange} />
|
||||||
<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>
|
|
||||||
<StatDivider />
|
<StatDivider />
|
||||||
<StatColumn label="Points">
|
<StatColumn label="Points">
|
||||||
<span className="text-2xl font-bold leading-none text-electric">
|
<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 }) {
|
export function PointChangeIndicator({ delta }: { delta: number }) {
|
||||||
if (delta >= 0) {
|
if (delta >= 0) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router";
|
import { Link, NavLink } from "react-router";
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
|
|
@ -10,7 +10,19 @@ import {
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/react-router";
|
import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/react-router";
|
||||||
import { HelpCircle, MenuIcon, Settings } from "lucide-react";
|
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 {
|
interface NavbarProps {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
|
@ -25,11 +37,17 @@ export function Navbar({ isAdmin }: NavbarProps) {
|
||||||
{/* Left: Logo + Nav links */}
|
{/* Left: Logo + Nav links */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link to="/" className="flex items-center mr-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>
|
</Link>
|
||||||
<nav aria-label="Main navigation" className="hidden md:flex items-center gap-1">
|
<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>
|
<SignedOut>
|
||||||
<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>
|
<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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -102,20 +120,40 @@ export function Navbar({ isAdmin }: NavbarProps) {
|
||||||
<SheetTitle>Menu</SheetTitle>
|
<SheetTitle>Menu</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<nav aria-label="Mobile navigation" className="flex flex-col gap-4 mt-8">
|
<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"
|
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)}
|
onClick={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
How To Play
|
How To Play
|
||||||
</Link>
|
</NavLink>
|
||||||
<Link
|
<NavLink
|
||||||
to="/rules"
|
to="/rules"
|
||||||
className="block px-4 py-2 text-lg font-medium hover:bg-accent rounded-md transition-colors"
|
className={mobileNavLinkClass}
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
Rules
|
Rules
|
||||||
</Link>
|
</NavLink>
|
||||||
</nav>
|
</nav>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ import { findSeasonById, findCurrentSeasonWithSports } from "~/models/season";
|
||||||
import { findTeamByOwnerAndSeason } from "~/models/team";
|
import { findTeamByOwnerAndSeason } from "~/models/team";
|
||||||
import { getDraftedParticipantsBySportsSeason } from "~/models/draft-pick";
|
import { getDraftedParticipantsBySportsSeason } from "~/models/draft-pick";
|
||||||
import { getUpcomingEventsForDraftedParticipants } from "~/models/scoring-event";
|
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 { getSeasonCompletionPercentage } from "~/lib/season-helpers.server";
|
||||||
|
import { buildTiedRankChecker, getDisplayRank } from "~/lib/standings-display";
|
||||||
import { findDraftSlotsBySeasonId } from "~/models/draft-slot";
|
import { findDraftSlotsBySeasonId } from "~/models/draft-slot";
|
||||||
import { getTeamForPick } from "~/lib/draft-order";
|
import { getTeamForPick } from "~/lib/draft-order";
|
||||||
import type { CalendarPanelEvent } from "~/components/sport-season/UpcomingCalendarPanel";
|
import type { CalendarPanelEvent } from "~/components/sport-season/UpcomingCalendarPanel";
|
||||||
|
|
@ -57,6 +58,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
let currentRank: number | undefined;
|
let currentRank: number | undefined;
|
||||||
let totalPoints: number | undefined;
|
let totalPoints: number | undefined;
|
||||||
let previousRank: number | undefined;
|
let previousRank: number | undefined;
|
||||||
|
let displayRank: string | number | undefined;
|
||||||
let completionPercentage = 0;
|
let completionPercentage = 0;
|
||||||
let picksUntilMyTurn: number | undefined;
|
let picksUntilMyTurn: number | undefined;
|
||||||
let draftPosition: number | undefined;
|
let draftPosition: number | undefined;
|
||||||
|
|
@ -100,6 +102,9 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
currentRank = standing.currentRank ?? undefined;
|
currentRank = standing.currentRank ?? undefined;
|
||||||
totalPoints = standing.totalPoints ?? undefined;
|
totalPoints = standing.totalPoints ?? undefined;
|
||||||
previousRank = standing.previousRank ?? 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") {
|
} else if (myTeam && season?.status === "active") {
|
||||||
// No standing row yet (no scoring events) — default to rank 1, 0 points
|
// No standing row yet (no scoring events) — default to rank 1, 0 points
|
||||||
currentRank = 1;
|
currentRank = 1;
|
||||||
|
|
@ -138,6 +143,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
...league,
|
...league,
|
||||||
currentSeason: season,
|
currentSeason: season,
|
||||||
numSports,
|
numSports,
|
||||||
|
displayRank,
|
||||||
currentRank,
|
currentRank,
|
||||||
totalPoints,
|
totalPoints,
|
||||||
previousRank,
|
previousRank,
|
||||||
|
|
@ -195,6 +201,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
||||||
status: isValidStatus(rawStatus) ? rawStatus : "pre_draft",
|
status: isValidStatus(rawStatus) ? rawStatus : "pre_draft",
|
||||||
seasonId: league.currentSeasonId ?? undefined,
|
seasonId: league.currentSeasonId ?? undefined,
|
||||||
currentRank: league.currentRank,
|
currentRank: league.currentRank,
|
||||||
|
displayRank: league.displayRank,
|
||||||
totalPoints: league.totalPoints,
|
totalPoints: league.totalPoints,
|
||||||
previousRank: league.previousRank,
|
previousRank: league.previousRank,
|
||||||
completionPercentage: league.completionPercentage,
|
completionPercentage: league.completionPercentage,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue