Add oxlint and fix all lint errors
- Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bcca8b76fa
commit
bf0ddaa8aa
81 changed files with 606 additions and 200 deletions
15
.claude/settings.json
Normal file
15
.claude/settings.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "jq -r '.tool_input.file_path // empty' | xargs -I{} sh -c 'case \"{}\" in *.ts|*.tsx) ./node_modules/.bin/oxlint \"{}\" 2>&1 ;; esac'"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
58
.oxlintrc.json
Normal file
58
.oxlintrc.json
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "vitest", "unicorn", "import"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es2022": true
|
||||
},
|
||||
"categories": {
|
||||
"correctness": "error",
|
||||
"suspicious": "warn"
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"eqeqeq": ["error", "always"],
|
||||
"no-unused-vars": "off",
|
||||
"no-undef": "off",
|
||||
|
||||
"typescript/no-unused-vars": [
|
||||
"error",
|
||||
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
|
||||
],
|
||||
"typescript/no-explicit-any": "warn",
|
||||
"typescript/no-non-null-assertion": "warn",
|
||||
"typescript/consistent-type-imports": [
|
||||
"error",
|
||||
{ "prefer": "type-imports" }
|
||||
],
|
||||
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/exhaustive-deps": "error",
|
||||
"react/jsx-key": "error",
|
||||
"react/no-array-index-key": "error",
|
||||
"react/self-closing-comp": "warn",
|
||||
|
||||
"import/no-duplicates": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx", "cypress/**"],
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"build/**",
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
"drizzle/**",
|
||||
".react-router/**",
|
||||
"cypress/fixtures/**"
|
||||
]
|
||||
}
|
||||
|
|
@ -92,11 +92,11 @@ export function DraftGrid({
|
|||
const round = roundIndex + 1;
|
||||
const isEvenRound = round % 2 === 0;
|
||||
const displayPicks = isEvenRound
|
||||
? [...roundPicks].reverse()
|
||||
? [...roundPicks].toReversed()
|
||||
: roundPicks;
|
||||
|
||||
return (
|
||||
<div key={roundIndex} className="flex gap-2">
|
||||
<div key={round} className="flex gap-2">
|
||||
{displayPicks.map((cell, index) => {
|
||||
const actualIndex = isEvenRound
|
||||
? roundPicks.length - 1 - index
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ export function ConnectionOverlay({
|
|||
</div>
|
||||
) : (
|
||||
<div className="relative w-16 h-16">
|
||||
<div className="absolute inset-0 border-4 border-primary/30 rounded-full"></div>
|
||||
<div className="absolute inset-0 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
||||
<div className="absolute inset-0 border-4 border-primary/30 rounded-full" />
|
||||
<div className="absolute inset-0 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -85,15 +85,15 @@ export function ConnectionOverlay({
|
|||
<div
|
||||
className="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||
style={{ animationDelay: "0ms" }}
|
||||
></div>
|
||||
/>
|
||||
<div
|
||||
className="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||
style={{ animationDelay: "150ms" }}
|
||||
></div>
|
||||
/>
|
||||
<div
|
||||
className="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||
style={{ animationDelay: "300ms" }}
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -168,11 +168,11 @@ export const DraftGridSection = memo(function DraftGridSection({
|
|||
const round = roundIndex + 1;
|
||||
const isEvenRound = round % 2 === 0;
|
||||
const displayPicks = isEvenRound
|
||||
? [...roundPicks].reverse()
|
||||
? [...roundPicks].toReversed()
|
||||
: roundPicks;
|
||||
|
||||
return (
|
||||
<div key={roundIndex} className="flex gap-2 items-stretch">
|
||||
<div key={round} className="flex gap-2 items-stretch">
|
||||
{/* Round label */}
|
||||
<div className="w-8 flex-shrink-0 sticky left-0 z-[5] bg-background/95 flex items-center justify-center">
|
||||
<span className="text-xs font-mono text-muted-foreground">R{round}</span>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function RecentPicksSection({ picks }: RecentPicksSectionProps) {
|
|||
<div className="space-y-2 overflow-y-auto">
|
||||
{picks
|
||||
.slice()
|
||||
.reverse()
|
||||
.toReversed()
|
||||
.slice(0, 10)
|
||||
.map((pick) => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const SidebarRecentPicks = memo(function SidebarRecentPicks({ picks }: Si
|
|||
<div className="space-y-1.5">
|
||||
{picks
|
||||
.slice()
|
||||
.reverse()
|
||||
.toReversed()
|
||||
.map((pick) => (
|
||||
<div
|
||||
key={pick.id}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ export function PlayoffBracket({
|
|||
rounds,
|
||||
preEliminatedParticipants = [],
|
||||
participantPoints = [],
|
||||
partialScoreParticipantIds = [],
|
||||
partialScoreParticipantIds: _partialScoreParticipantIds = [],
|
||||
teamOwnerships = [],
|
||||
userParticipantIds = [],
|
||||
showOwnership = true,
|
||||
|
|
@ -189,7 +189,7 @@ export function PlayoffBracket({
|
|||
// In double-chance brackets (e.g. AFL), a participant may lose one round but
|
||||
// win a later one — only count them as eliminated at their FINAL losing match.
|
||||
const losersByRound = new Map<string, Array<{ participant: Participant; score: string | null; ownership: TeamOwnership | null }>>();
|
||||
let hasScore = false;
|
||||
let _hasScore = false;
|
||||
let bracketWinner: Participant | null = null;
|
||||
|
||||
const lastRound = rounds[rounds.length - 1];
|
||||
|
|
@ -209,7 +209,7 @@ export function PlayoffBracket({
|
|||
match.loserId === match.participant1Id
|
||||
? match.participant1Score
|
||||
: match.participant2Score;
|
||||
if (loserScore) hasScore = true;
|
||||
if (loserScore) _hasScore = true;
|
||||
if (!losersByRound.has(match.round)) losersByRound.set(match.round, []);
|
||||
losersByRound.get(match.round)!.push({
|
||||
participant: match.loser,
|
||||
|
|
@ -574,12 +574,12 @@ export function PlayoffBracket({
|
|||
</TableRow>
|
||||
)}
|
||||
|
||||
{rankedEntries.filter((entry) => isDraftedOrScoring(entry.participant.id)).map((entry, i) => {
|
||||
{rankedEntries.filter((entry) => isDraftedOrScoring(entry.participant.id)).map((entry) => {
|
||||
const isOwned = userParticipantSet.has(entry.participant.id);
|
||||
const pts = pointsMap.get(entry.participant.id);
|
||||
return (
|
||||
<TableRow
|
||||
key={`${entry.participant.id}-${i}`}
|
||||
key={entry.participant.id}
|
||||
className={isOwned ? "bg-electric/5 border-l-2 border-l-electric opacity-80" : "opacity-60"}
|
||||
>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export function SeasonStandings({
|
|||
showOwnership = true,
|
||||
isFinalized = false,
|
||||
title = "Championship Standings",
|
||||
description,
|
||||
description: _description,
|
||||
}: SeasonStandingsProps) {
|
||||
const userParticipantSet = new Set(userParticipantIds);
|
||||
// Create ownership map for fast lookup
|
||||
|
|
@ -191,7 +191,7 @@ export function SeasonStandings({
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{standings
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.toSorted((a, b) => a.position - b.position)
|
||||
.map((standing) => {
|
||||
const ownership = getOwnership(standing.participant.id);
|
||||
const isTied = checkIfTied(standing);
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ interface SportSeasonDisplayProps {
|
|||
export function SportSeasonDisplay({
|
||||
scoringPattern,
|
||||
sportSeasonName,
|
||||
sportName,
|
||||
sportName: _sportName,
|
||||
playoffMatches = [],
|
||||
playoffRounds = [],
|
||||
preEliminatedParticipants = [],
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export function EventSchedule({ upcomingEvents, recentEvents }: EventSchedulePro
|
|||
!event.isComplete && (() => {
|
||||
const isPast = event.eventStartsAt
|
||||
? new Date(event.eventStartsAt) < new Date()
|
||||
: event.eventDate != null && event.eventDate < new Date().toISOString().split("T")[0];
|
||||
: event.eventDate !== null && event.eventDate < new Date().toISOString().split("T")[0];
|
||||
return isPast && (
|
||||
<Badge variant="outline" className="text-xs shrink-0 border-amber-500/30 text-amber-400">
|
||||
Results Pending
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ interface TableSection {
|
|||
// ─── Formatting ───────────────────────────────────────────────────────────────
|
||||
|
||||
function formatWinPct(winPct: string | null, wins: number, gamesPlayed: number): string {
|
||||
if (winPct != null) {
|
||||
if (winPct !== null) {
|
||||
const pct = parseFloat(winPct);
|
||||
if (!isNaN(pct)) return pct.toFixed(3);
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ function formatWinPct(winPct: string | null, wins: number, gamesPlayed: number):
|
|||
}
|
||||
|
||||
function formatGB(gamesBack: string | null): string {
|
||||
if (gamesBack == null) return "—";
|
||||
if (gamesBack === null) return "—";
|
||||
const gb = parseFloat(gamesBack);
|
||||
if (isNaN(gb) || gb === 0) return "—";
|
||||
return gb % 1 === 0 ? gb.toString() : gb.toFixed(1);
|
||||
|
|
@ -96,7 +96,7 @@ function buildFlatSections(
|
|||
conference: "",
|
||||
sections: [
|
||||
{
|
||||
rows: [...standings].sort(
|
||||
rows: [...standings].toSorted(
|
||||
(a, b) => (a.leagueRank ?? 99) - (b.leagueRank ?? 99)
|
||||
),
|
||||
rankMode: "conference",
|
||||
|
|
@ -118,7 +118,7 @@ function buildFlatSections(
|
|||
conference,
|
||||
sections: [
|
||||
{
|
||||
rows: [...rows].sort(
|
||||
rows: [...rows].toSorted(
|
||||
(a, b) =>
|
||||
(a.conferenceRank ?? a.leagueRank ?? 99) -
|
||||
(b.conferenceRank ?? b.leagueRank ?? 99)
|
||||
|
|
@ -154,9 +154,9 @@ function buildNhlSections(
|
|||
name,
|
||||
qualifiers: divRows
|
||||
.filter((r) => (r.divisionRank ?? 99) <= 3)
|
||||
.sort((a, b) => (a.divisionRank ?? 99) - (b.divisionRank ?? 99)),
|
||||
.toSorted((a, b) => (a.divisionRank ?? 99) - (b.divisionRank ?? 99)),
|
||||
}))
|
||||
.sort(
|
||||
.toSorted(
|
||||
(a, b) =>
|
||||
(a.qualifiers[0]?.conferenceRank ?? 99) -
|
||||
(b.qualifiers[0]?.conferenceRank ?? 99)
|
||||
|
|
@ -164,7 +164,7 @@ function buildNhlSections(
|
|||
|
||||
const wildCard = rows
|
||||
.filter((r) => (r.divisionRank ?? 99) > 3)
|
||||
.sort(
|
||||
.toSorted(
|
||||
(a, b) =>
|
||||
(a.conferenceRank ?? a.leagueRank ?? 99) -
|
||||
(b.conferenceRank ?? b.leagueRank ?? 99)
|
||||
|
|
@ -243,7 +243,7 @@ function StandingsTable({
|
|||
|
||||
if (section.heading) {
|
||||
sectionRows.push(
|
||||
<tr key={`h-${sIdx}`}>
|
||||
<tr key={`h-${section.heading}`}>
|
||||
<td
|
||||
colSpan={totalCols}
|
||||
className={`text-xs font-medium text-muted-foreground/60 uppercase tracking-wider pb-1 ${
|
||||
|
|
@ -350,7 +350,7 @@ function StandingsTable({
|
|||
</td>
|
||||
{hasEvs && (
|
||||
<td className="py-2 pl-2 text-right tabular-nums">
|
||||
{ev != null ? (
|
||||
{ev !== null ? (
|
||||
<span className="text-blue-500 dark:text-blue-400 font-medium">
|
||||
{parseFloat(ev).toFixed(1)}
|
||||
</span>
|
||||
|
|
@ -388,7 +388,7 @@ export function RegularSeasonStandings({
|
|||
const lastSyncedAt = standings
|
||||
.map((s) => s.syncedAt)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.toSorted()
|
||||
.at(-1);
|
||||
|
||||
const groups =
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export function TeamScoreBreakdown({
|
|||
// Flatten all picks sorted by sport name then pick number
|
||||
const allPicks = breakdown.picks
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
.toSorted((a, b) => {
|
||||
const sportCmp = a.participant.sport.localeCompare(b.participant.sport);
|
||||
return sportCmp !== 0 ? sportCmp : a.pickNumber - b.pickNumber;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@ async function handleRequest(
|
|||
routerContext: EntryContext,
|
||||
// If you have middleware enabled:
|
||||
// loadContext: RouterContextProvider
|
||||
loadContext: AppLoadContext
|
||||
_loadContext: AppLoadContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
let userAgent = request.headers.get("user-agent");
|
||||
const userAgent = request.headers.get("user-agent");
|
||||
|
||||
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
|
||||
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
||||
let readyOption: keyof RenderToPipeableStreamOptions =
|
||||
const readyOption: keyof RenderToPipeableStreamOptions =
|
||||
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
|
||||
? "onAllReady"
|
||||
: "onShellReady";
|
||||
|
|
|
|||
|
|
@ -599,7 +599,7 @@ describe("calculateDraftEligibility", () => {
|
|||
|
||||
// Team1 picks NFL
|
||||
allPicks = [createPick("team1", "nfl1", "nfl", "NFL")];
|
||||
let team2Eligibility = calculateDraftEligibility(
|
||||
const team2Eligibility = calculateDraftEligibility(
|
||||
"team2",
|
||||
[],
|
||||
allPicks,
|
||||
|
|
@ -619,7 +619,7 @@ describe("calculateDraftEligibility", () => {
|
|||
createPick("team1", "nfl1", "nfl", "NFL"),
|
||||
createPick("team2", "nfl2", "nfl", "NFL"),
|
||||
];
|
||||
let team3Eligibility = calculateDraftEligibility(
|
||||
const team3Eligibility = calculateDraftEligibility(
|
||||
"team3",
|
||||
[],
|
||||
allPicks,
|
||||
|
|
@ -642,7 +642,7 @@ describe("calculateDraftEligibility", () => {
|
|||
];
|
||||
|
||||
// Now team1 wants a second NFL (flex pick)
|
||||
let team1Eligibility = calculateDraftEligibility(
|
||||
const team1Eligibility = calculateDraftEligibility(
|
||||
"team1",
|
||||
[createPick("team1", "nfl1", "nfl", "NFL")],
|
||||
allPicks,
|
||||
|
|
|
|||
|
|
@ -603,7 +603,7 @@ export function getOrderedRoundsFromMatches(
|
|||
for (const m of matches) {
|
||||
roundMatchCounts.set(m.round, (roundMatchCounts.get(m.round) || 0) + 1);
|
||||
}
|
||||
return Array.from(roundMatchCounts.keys()).sort(
|
||||
return Array.from(roundMatchCounts.keys()).toSorted(
|
||||
(a, b) => (roundMatchCounts.get(b) || 0) - (roundMatchCounts.get(a) || 0)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export async function buildOwnerMap(
|
|||
const userByClerkId = new Map(
|
||||
userRows
|
||||
.map((u) => [u.clerkId, getUserDisplayName(u)] as const)
|
||||
.filter((entry): entry is [string, string] => entry[1] != null)
|
||||
.filter((entry): entry is [string, string] => entry[1] !== null && entry[1] !== undefined)
|
||||
);
|
||||
|
||||
const ownerMap: Record<string, string> = {};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Check if a season is complete
|
||||
|
|
|
|||
|
|
@ -182,18 +182,6 @@ describe("AFL Finals System - Phase 3.3", () => {
|
|||
|
||||
describe("AFL Point Distribution", () => {
|
||||
it("has all 8 scoring placements covered", () => {
|
||||
// Verify that all placements 1-8 are accounted for in AFL system
|
||||
const placements = {
|
||||
1: "Grand Final winner",
|
||||
2: "Grand Final loser",
|
||||
3: "Preliminary Final loser (shared with 4th)",
|
||||
4: "Preliminary Final loser (shared with 3rd)",
|
||||
5: "Semi-Final loser (shared with 6th)",
|
||||
6: "Semi-Final loser (shared with 5th)",
|
||||
7: "Elimination Final loser (shared with 8th)",
|
||||
8: "Elimination Final loser (shared with 7th)",
|
||||
};
|
||||
|
||||
// Count scoring rounds
|
||||
const scoringRounds = AFL_10.rounds.filter(r => r.isScoring);
|
||||
expect(scoringRounds).toHaveLength(4); // EF, SF, PF, GF
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { SIMPLE_16, SIMPLE_32, NCAA_68 } from "~/lib/bracket-templates";
|
||||
import { calculateFantasyPoints, calculateAveragedPoints, type ScoringRules } from "../scoring-rules";
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ describe("Bracket Seeding", () => {
|
|||
|
||||
it("uses all 16 seeds exactly once", () => {
|
||||
const pairs = generateStandardSeeding(16);
|
||||
const allSeeds = pairs.flat().map(s => s + 1).sort((a, b) => a - b);
|
||||
const allSeeds = pairs.flat().map(s => s + 1).toSorted((a, b) => a - b);
|
||||
|
||||
expect(allSeeds).toEqual(Array.from({ length: 16 }, (_, i) => i + 1));
|
||||
});
|
||||
|
|
@ -114,7 +114,7 @@ describe("Bracket Seeding", () => {
|
|||
const seed1 = firstMatch[0] + 1;
|
||||
const seed2 = firstMatch[1] + 1;
|
||||
|
||||
expect([seed1, seed2].sort()).toEqual([1, 16]);
|
||||
expect([seed1, seed2].toSorted()).toEqual([1, 16]);
|
||||
});
|
||||
|
||||
it("has proper matchup sums for balance", () => {
|
||||
|
|
@ -167,12 +167,12 @@ describe("Bracket Seeding", () => {
|
|||
const seed1 = firstMatch[0] + 1;
|
||||
const seed2 = firstMatch[1] + 1;
|
||||
|
||||
expect([seed1, seed2].sort()).toEqual([1, 32]);
|
||||
expect([seed1, seed2].toSorted()).toEqual([1, 32]);
|
||||
});
|
||||
|
||||
it("uses all 32 seeds exactly once", () => {
|
||||
const pairs = generateStandardSeeding(32);
|
||||
const allSeeds = pairs.flat().map(s => s + 1).sort((a, b) => a - b);
|
||||
const allSeeds = pairs.flat().map(s => s + 1).toSorted((a, b) => a - b);
|
||||
|
||||
expect(allSeeds).toEqual(Array.from({ length: 32 }, (_, i) => i + 1));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
SIMPLE_4,
|
||||
SIMPLE_8,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calcul
|
|||
*/
|
||||
|
||||
describe("participant-expected-value model", () => {
|
||||
const defaultScoring: ScoringRules = {
|
||||
const _defaultScoring: ScoringRules = {
|
||||
pointsFor1st: 100,
|
||||
pointsFor2nd: 70,
|
||||
pointsFor3rd: 50,
|
||||
|
|
@ -23,7 +23,7 @@ describe("participant-expected-value model", () => {
|
|||
pointsFor8th: 15,
|
||||
};
|
||||
|
||||
const validProbabilities: ProbabilityDistribution = {
|
||||
const _validProbabilities: ProbabilityDistribution = {
|
||||
probFirst: 20,
|
||||
probSecond: 20,
|
||||
probThird: 15,
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ describe("processMatchResult", () => {
|
|||
it("does not overwrite a finalized row with a partial score", async () => {
|
||||
// findFirst returns a finalized row for the first participant (loser)
|
||||
const existing = { id: "result-1", isPartialScore: false };
|
||||
const { db, insertedRows, updatedRows } = makeDb(existing);
|
||||
const { db, updatedRows } = makeDb(existing);
|
||||
|
||||
await processMatchResult(
|
||||
{ ...BASE, round: "Quarterfinals", isScoring: true },
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ describe("Qualifying Points Configuration", () => {
|
|||
{ name: "Player C", qp: 38 },
|
||||
];
|
||||
|
||||
const sorted = standings.sort((a, b) => b.qp - a.qp);
|
||||
const sorted = standings.toSorted((a, b) => b.qp - a.qp);
|
||||
|
||||
expect(sorted[0].name).toBe("Player B"); // 50 QP
|
||||
expect(sorted[1].name).toBe("Player A"); // 44 QP
|
||||
|
|
@ -107,7 +107,7 @@ describe("Qualifying Points Configuration", () => {
|
|||
{ name: "Player C", qp: 20 },
|
||||
];
|
||||
|
||||
const sorted = standings.sort((a, b) => b.qp - a.qp);
|
||||
const sorted = standings.toSorted((a, b) => b.qp - a.qp);
|
||||
|
||||
// Both players with 30 QP should be before Player C
|
||||
expect(sorted[2].name).toBe("Player C");
|
||||
|
|
|
|||
|
|
@ -6,11 +6,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||
// ── DB mock ──────────────────────────────────────────────────────────────────
|
||||
// selectDistinct returns a fluent chain; findMany is used in step 2.
|
||||
const mockFindMany = vi.fn();
|
||||
const mockSelectChainBase = {
|
||||
from: vi.fn(),
|
||||
leftJoin: vi.fn(),
|
||||
where: vi.fn(),
|
||||
};
|
||||
|
||||
function makeSelectChain(rows: { id: string }[]) {
|
||||
const chain = {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ describe("Season Standings (F1 Pattern)", () => {
|
|||
|
||||
it("should correctly score season with tied positions", () => {
|
||||
// Simulate standings where 3rd, 4th, and 5th are tied
|
||||
const standings = [
|
||||
const _standings = [
|
||||
{ position: 1, expected: 100 },
|
||||
{ position: 2, expected: 70 },
|
||||
// Three tied for 3rd place - each gets placement 3
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ describe("Standings Snapshots", () => {
|
|||
];
|
||||
|
||||
// Should be sorted by date descending (newest first)
|
||||
const sorted = [...snapshots].sort((a, b) =>
|
||||
const sorted = [...snapshots].toSorted((a, b) =>
|
||||
b.date.localeCompare(a.date)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ function assignRanks(
|
|||
}>
|
||||
): Map<string, number> {
|
||||
// Sort teams by ranking criteria
|
||||
const sorted = [...teams].sort(compareTeamsForRanking);
|
||||
const sorted = [...teams].toSorted(compareTeamsForRanking);
|
||||
|
||||
const ranks = new Map<string, number>();
|
||||
let currentRank = 1;
|
||||
|
|
|
|||
|
|
@ -345,7 +345,7 @@ export function getTeamForPick(
|
|||
pickNumber: number,
|
||||
draftOrder: { teamId: string; draftOrder: number }[]
|
||||
): string | null {
|
||||
const sortedOrder = [...draftOrder].sort((a, b) => a.draftOrder - b.draftOrder);
|
||||
const sortedOrder = [...draftOrder].toSorted((a, b) => a.draftOrder - b.draftOrder);
|
||||
const teamCount = sortedOrder.length;
|
||||
|
||||
if (teamCount === 0) return null;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { eq, desc, and } from "drizzle-orm";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ export async function findLeaguesWithActiveSeasonsByUserId(
|
|||
for (const league of [...leaguesWithTeam, ...leaguesAsCommissioner]) {
|
||||
leagueMap.set(league.id, league);
|
||||
}
|
||||
return [...leagueMap.values()].sort(
|
||||
return [...leagueMap.values()].toSorted(
|
||||
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, and, inArray, desc } from "drizzle-orm";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
|
||||
export interface UpsertSeasonResultData {
|
||||
participantId: string;
|
||||
|
|
@ -135,7 +135,7 @@ export async function getSeasonResults(
|
|||
});
|
||||
|
||||
// Sort by position (nulls last), then by points descending
|
||||
return results.sort((a, b) => {
|
||||
return results.toSorted((a, b) => {
|
||||
if (a.currentPosition !== null && b.currentPosition !== null) {
|
||||
return a.currentPosition - b.currentPosition;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,7 +149,6 @@ export async function generateSingleEliminationBracket(
|
|||
eventId: string,
|
||||
participantIds: string[]
|
||||
): Promise<PlayoffMatch[]> {
|
||||
const db = database();
|
||||
const numParticipants = participantIds.length;
|
||||
|
||||
// Validate that we have 4 or 8 participants for standard bracket
|
||||
|
|
@ -218,8 +217,6 @@ export async function advanceWinner(
|
|||
matchId: string,
|
||||
winnerId: string
|
||||
): Promise<void> {
|
||||
const db = database();
|
||||
|
||||
// Get the match
|
||||
const match = await findPlayoffMatchById(matchId);
|
||||
if (!match) throw new Error("Match not found");
|
||||
|
|
@ -523,17 +520,6 @@ async function generateNFL14Bracket(
|
|||
): Promise<PlayoffMatch[]> {
|
||||
const matches: NewPlayoffMatch[] = [];
|
||||
|
||||
// Wild Card: 6 games for seeds 3-14 (12 teams)
|
||||
// Seeds 1-2 get byes and automatically advance to Divisional
|
||||
const wildCardSeeding = [
|
||||
[6, 10], // #7 seed vs #11 seed (0-indexed: 6 vs 10)
|
||||
[5, 11], // #6 seed vs #12 seed
|
||||
[4, 12], // #5 seed vs #13 seed
|
||||
[7, 9], // #8 seed vs #10 seed
|
||||
[3, 13], // #4 seed vs #14 seed
|
||||
[2, 14], // #3 seed vs #15 seed (Note: 14 is index for seed 15, but we only have 14 teams, so this is 3 vs 14)
|
||||
];
|
||||
|
||||
// Actually for 14 teams (seeds 1-14), Wild Card should be:
|
||||
// Match 1: #7 (index 6) vs #10 (index 9)
|
||||
// Match 2: #6 (index 5) vs #11 (index 10)
|
||||
|
|
@ -541,7 +527,7 @@ async function generateNFL14Bracket(
|
|||
// Match 4: #8 (index 7) vs #9 (index 8)
|
||||
// Match 5: #4 (index 3) vs #13 (index 12)
|
||||
// Match 6: #3 (index 2) vs #14 (index 13)
|
||||
const correctedWildCardSeeding = [
|
||||
const wildCardSeeding = [
|
||||
[6, 9], // #7 vs #10
|
||||
[5, 10], // #6 vs #11
|
||||
[4, 11], // #5 vs #12
|
||||
|
|
@ -550,8 +536,8 @@ async function generateNFL14Bracket(
|
|||
[2, 13], // #3 vs #14
|
||||
];
|
||||
|
||||
for (let i = 0; i < correctedWildCardSeeding.length; i++) {
|
||||
const [seed1, seed2] = correctedWildCardSeeding[i];
|
||||
for (let i = 0; i < wildCardSeeding.length; i++) {
|
||||
const [seed1, seed2] = wildCardSeeding[i];
|
||||
matches.push({
|
||||
scoringEventId: eventId,
|
||||
round: "Wild Card",
|
||||
|
|
@ -572,7 +558,7 @@ async function generateNFL14Bracket(
|
|||
if (divisionalRound) {
|
||||
for (let i = 0; i < divisionalRound.matchCount; i++) {
|
||||
let participant1Id: string | null = null;
|
||||
let participant2Id: string | null = null;
|
||||
const participant2Id: string | null = null;
|
||||
let seedInfo: string | null = null;
|
||||
|
||||
// Assign bye teams to first two Divisional matches
|
||||
|
|
@ -768,7 +754,6 @@ async function advanceAFLWinner(
|
|||
winnerId: string,
|
||||
loserId: string
|
||||
): Promise<void> {
|
||||
const db = database();
|
||||
const eventId = match.scoringEventId;
|
||||
|
||||
// Wildcard Round: Winner advances to Elimination Finals
|
||||
|
|
@ -871,8 +856,6 @@ export async function advanceWinnerTemplate(
|
|||
winnerId: string,
|
||||
template: BracketTemplate
|
||||
): Promise<void> {
|
||||
const db = database();
|
||||
|
||||
// Get the match
|
||||
const match = await findPlayoffMatchById(matchId);
|
||||
if (!match) throw new Error("Match not found");
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ export async function updateFinalRankings(
|
|||
const updates = [];
|
||||
|
||||
// Sort by points descending
|
||||
const sortedGroups = Array.from(groupedByPoints.entries()).sort((a, b) => {
|
||||
const sortedGroups = Array.from(groupedByPoints.entries()).toSorted((a, b) => {
|
||||
return parseFloat(b[0]) - parseFloat(a[0]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,9 @@ export async function upsertRegularSeasonStandings(
|
|||
losses: r.losses,
|
||||
otLosses: r.otLosses ?? null,
|
||||
ties: r.ties ?? null,
|
||||
winPct: r.winPct != null ? r.winPct.toString() : null,
|
||||
winPct: r.winPct !== null && r.winPct !== undefined ? r.winPct.toString() : null,
|
||||
gamesPlayed: r.gamesPlayed,
|
||||
gamesBack: r.gamesBack != null ? r.gamesBack.toString() : null,
|
||||
gamesBack: r.gamesBack !== null && r.gamesBack !== undefined ? r.gamesBack.toString() : null,
|
||||
conference: r.conference ?? null,
|
||||
division: r.division ?? null,
|
||||
conferenceRank: r.conferenceRank ?? null,
|
||||
|
|
@ -133,7 +133,7 @@ export async function getRegularSeasonStandings(
|
|||
});
|
||||
|
||||
// Sort: conference → division → divisionRank ?? leagueRank ?? leagueRank, nulls last
|
||||
return rows.sort((a, b) => {
|
||||
return rows.toSorted((a, b) => {
|
||||
const confA = a.conference ?? "ZZZ";
|
||||
const confB = b.conference ?? "ZZZ";
|
||||
if (confA !== confB) return confA.localeCompare(confB);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import { getScoringRules, calculateFantasyPoints, calculateAveragedPoints, calculateBracketPoints } from "./scoring-rules";
|
||||
import { getScoringRules, calculateFantasyPoints, calculateBracketPoints } from "./scoring-rules";
|
||||
import { getSeasonResults } from "./participant-season-result";
|
||||
import { updateProbabilitiesAfterResult } from "~/services/probability-updater";
|
||||
import { sendStandingsUpdateNotification, type ScoredMatch } from "~/services/discord";
|
||||
|
|
@ -597,7 +597,7 @@ export async function finalizeQualifyingPoints(
|
|||
}
|
||||
|
||||
// Sort groups by QP (descending)
|
||||
const sortedGroups = Array.from(groupedByQP.entries()).sort((a, b) => {
|
||||
const sortedGroups = Array.from(groupedByQP.entries()).toSorted((a, b) => {
|
||||
return parseFloat(b[0]) - parseFloat(a[0]);
|
||||
});
|
||||
|
||||
|
|
@ -722,7 +722,7 @@ export async function processSeasonStandings(
|
|||
// Get sorted positions
|
||||
const positions = Object.keys(positionGroups)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
.toSorted((a, b) => a - b);
|
||||
|
||||
// Assign fantasy placements (1-8) based on standings
|
||||
let currentPlacement = 1;
|
||||
|
|
@ -874,7 +874,7 @@ export async function calculateTeamScore(
|
|||
// If duplicates exist due to data corruption, the first row wins — acceptable trade-off.
|
||||
const result = pick.participant.results[0];
|
||||
|
||||
if (result && result.finalPosition != null && result.finalPosition > 0) {
|
||||
if (result && result.finalPosition !== null && result.finalPosition > 0) {
|
||||
const isBracket = pick.participant.sportsSeason?.scoringPattern === "playoff_bracket";
|
||||
let points: number;
|
||||
if (isBracket) {
|
||||
|
|
@ -1100,7 +1100,7 @@ function assignRanks(
|
|||
}>
|
||||
): Map<string, number> {
|
||||
// Sort teams by ranking criteria
|
||||
const sorted = [...teams].sort(compareTeamsForRanking);
|
||||
const sorted = [...teams].toSorted(compareTeamsForRanking);
|
||||
|
||||
const ranks = new Map<string, number>();
|
||||
let currentRank = 1;
|
||||
|
|
@ -1330,7 +1330,7 @@ export async function recalculateAffectedLeagues(
|
|||
const usernameByClerkId = new Map(
|
||||
teamOwnerUsers
|
||||
.map((u) => [u.clerkId, getUserDisplayName(u)])
|
||||
.filter((entry): entry is [string, string] => entry[1] != null)
|
||||
.filter((entry): entry is [string, string] => entry[1] !== null)
|
||||
);
|
||||
|
||||
// Build teamId → ownerId map from standings data
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ export async function getUpcomingEventsForDraftedParticipants(
|
|||
pMap.set(row.participant2Id, draftedMap.get(row.participant2Id)!);
|
||||
}
|
||||
|
||||
if (row.round && row.gameNumber != null) {
|
||||
if (row.round && row.gameNumber !== null) {
|
||||
gameKeysByEvent.get(row.id)!.add(`${row.round}|${row.gameNumber}`);
|
||||
}
|
||||
|
||||
|
|
@ -578,7 +578,7 @@ export async function getEventsForDates(
|
|||
|
||||
// Build timestamp bounds covering the full span of the requested dates so we
|
||||
// can range-check the playoffMatchGames.scheduledAt timestamp column.
|
||||
const sortedDates = [...dates].sort();
|
||||
const sortedDates = [...dates].toSorted();
|
||||
const dateFromTimestamp = new Date(sortedDates[0] + "T00:00:00.000Z");
|
||||
const dateToTimestamp = new Date(sortedDates[sortedDates.length - 1] + "T23:59:59.999Z");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, and, desc } from "drizzle-orm";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import type { TeamStanding, TeamStandingSnapshot, TeamStandingWithChange } from "~/types/standings";
|
||||
import { calculateBracketPoints, calculateFantasyPoints } from "~/models/scoring-rules";
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ export async function getSeasonStandings(
|
|||
});
|
||||
|
||||
// Sort by currentRank ascending (1 is best, 2 is second, etc.)
|
||||
const sorted = standings.sort((a, b) => a.currentRank - b.currentRank);
|
||||
const sorted = standings.toSorted((a, b) => a.currentRank - b.currentRank);
|
||||
|
||||
return sorted.map((standing) => ({
|
||||
teamId: standing.teamId,
|
||||
|
|
@ -197,7 +197,7 @@ export async function getTeamScoreBreakdown(
|
|||
);
|
||||
};
|
||||
|
||||
if (result && result.finalPosition != null && result.finalPosition > 0) {
|
||||
if (result && result.finalPosition !== null && result.finalPosition > 0) {
|
||||
// Calculate points using bracket-averaged scoring for bracket sports
|
||||
if (isBracket) {
|
||||
const templateId = await getBracketTemplate(pick.participant.sportsSeasonId);
|
||||
|
|
@ -416,7 +416,7 @@ export async function getSeasonPointProgression(
|
|||
}
|
||||
|
||||
// Convert to array and sort by date
|
||||
const chartData = Array.from(dateMap.values()).sort((a, b) =>
|
||||
const chartData = Array.from(dateMap.values()).toSorted((a, b) =>
|
||||
new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket";
|
||||
import { redirect } from "react-router";
|
||||
|
||||
import { findSportsSeasonById } from "~/models/sports-season";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||||
import { getScoringEventById, updateScoringEvent } from "~/models/scoring-event";
|
||||
|
|
@ -42,7 +42,7 @@ import {
|
|||
} from "~/models/tournament-group";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const sportsSeason = await findSportsSeasonById(params.id);
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export default function EventBracket({
|
|||
const template = templates.find((t) => t.id === selectedTemplate);
|
||||
|
||||
// Determine if this is a group-stage event
|
||||
const hasGroupStage = template?.groupStage != null;
|
||||
const hasGroupStage = template?.groupStage !== null;
|
||||
|
||||
// Determine if this template uses named regions (e.g. NCAA 68)
|
||||
const hasRegions = (template?.regions?.length ?? 0) > 0;
|
||||
|
|
@ -455,6 +455,7 @@ export default function EventBracket({
|
|||
</p>
|
||||
</div>
|
||||
{regionConfig.map((region, r) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={r} className="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
className="w-36 h-8 text-sm"
|
||||
|
|
@ -463,6 +464,7 @@ export default function EventBracket({
|
|||
placeholder={`Region ${r + 1}`}
|
||||
/>
|
||||
{region.playInSeeds.map((seed, pi) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={pi} className="flex items-center gap-1">
|
||||
<span className="text-xs text-muted-foreground">play-in seed:</span>
|
||||
<Select
|
||||
|
|
@ -505,6 +507,7 @@ export default function EventBracket({
|
|||
))}
|
||||
{/* Hidden fields so server receives the region config */}
|
||||
{regionConfig.map((region, r) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Fragment key={r}>
|
||||
<input type="hidden" name={`regionName${r}`} value={region.name} />
|
||||
<input type="hidden" name={`regionPlayInSeeds${r}`} value={region.playInSeeds.join(",")} />
|
||||
|
|
@ -520,6 +523,7 @@ export default function EventBracket({
|
|||
{effectiveRegions.map((region, r) => {
|
||||
const directOffset = regionSlotMap.directOffsets[r];
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={r} className="border rounded-lg p-4 space-y-2">
|
||||
<h4 className="font-semibold text-sm">{region.name || `Region ${r + 1}`}</h4>
|
||||
{region.directSeeds.map((seedNum, pos) => {
|
||||
|
|
@ -573,7 +577,7 @@ export default function EventBracket({
|
|||
const idx1 = pi.startIndex;
|
||||
const idx2 = pi.startIndex + 1;
|
||||
return (
|
||||
<div key={ffIdx} className="space-y-2">
|
||||
<div key={`${pi.regionIndex}-${pi.seedSlot}`} className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Play-In {ffIdx + 1} — {regionName} {pi.seedSlot}-seed
|
||||
</p>
|
||||
|
|
@ -617,6 +621,7 @@ export default function EventBracket({
|
|||
const availableParticipants = getAvailableParticipants(i);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<Label className="w-20 text-sm text-muted-foreground shrink-0">
|
||||
Seed {i + 1}
|
||||
|
|
@ -829,7 +834,7 @@ export default function EventBracket({
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-8"></TableHead>
|
||||
<TableHead className="w-8" />
|
||||
<TableHead className="w-16">Match</TableHead>
|
||||
<TableHead>Participant 1</TableHead>
|
||||
<TableHead>Score</TableHead>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function EditEventCard({ event }: { event: { name: string; eventDate?: string |
|
|||
if (event.eventStartsAt) {
|
||||
setDisplayValue(format(new Date(event.eventStartsAt), "yyyy-MM-dd'T'HH:mm"));
|
||||
}
|
||||
}, []);
|
||||
}, [event.eventStartsAt]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
|
@ -107,7 +107,7 @@ export default function EventResults({
|
|||
);
|
||||
|
||||
// Sort results by placement
|
||||
const sortedResults = [...results].sort((a, b) => (a.placement || 999) - (b.placement || 999));
|
||||
const sortedResults = [...results].toSorted((a, b) => (a.placement || 999) - (b.placement || 999));
|
||||
|
||||
// Create a map of season results for easy lookup
|
||||
const seasonResultsMap = seasonResults
|
||||
|
|
@ -120,7 +120,7 @@ export default function EventResults({
|
|||
if (event.eventType === "schedule_event") {
|
||||
const isPast = event.eventStartsAt
|
||||
? new Date(event.eventStartsAt) < new Date()
|
||||
: event.eventDate != null && String(event.eventDate).slice(0, 10) < new Date().toISOString().split("T")[0];
|
||||
: event.eventDate !== null && event.eventDate !== undefined && String(event.eventDate).slice(0, 10) < new Date().toISOString().split("T")[0];
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
|
|
@ -695,7 +695,7 @@ export default function EventResults({
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{[...participantResults]
|
||||
.sort((a: any, b: any) => {
|
||||
.toSorted((a: any, b: any) => {
|
||||
const posA = a.finalPosition ?? 999;
|
||||
const posB = b.finalPosition ?? 999;
|
||||
return posA - posB;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from "~/models/scoring-event";
|
||||
import { getQPStandings } from "~/models/qualifying-points";
|
||||
import { finalizeQualifyingPoints } from "~/models/scoring-calculator";
|
||||
import { getScoringRules } from "~/models/scoring-rules";
|
||||
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const sportsSeason = await findSportsSeasonById(params.id);
|
||||
|
|
@ -23,7 +23,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||
|
||||
// For qualifying sports seasons, get QP standings
|
||||
let qpStandings = null;
|
||||
let scoringRules = null;
|
||||
const scoringRules = null;
|
||||
if (sportsSeason.scoringPattern === "qualifying_points") {
|
||||
qpStandings = await getQPStandings(params.id);
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export default function SportsSeasonEvents({
|
|||
if (eventType === "schedule_event") {
|
||||
const isPast = eventStartsAt
|
||||
? new Date(eventStartsAt) < new Date()
|
||||
: eventDate != null && eventDate < new Date().toISOString().split("T")[0];
|
||||
: eventDate !== null && eventDate < new Date().toISOString().split("T")[0];
|
||||
return isPast ? (
|
||||
<Badge variant="outline" className="text-muted-foreground">Results Pending</Badge>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export default function ExpectedValuesPage({ loaderData }: Route.ComponentProps)
|
|||
// so the total EV should always equal the sum of scoring values (340).
|
||||
// Sum only over participants shown in the table — excludes orphan EV records
|
||||
// from prior simulation runs for participants no longer in this season.
|
||||
const sorted = [...participants].sort((a, b) => {
|
||||
const sorted = [...participants].toSorted((a, b) => {
|
||||
const evA = existingEVs.has(a.id) ? evFromProbs(existingEVs.get(a.id)!) : 0;
|
||||
const evB = existingEVs.has(b.id) ? evFromProbs(existingEVs.get(b.id)!) : 0;
|
||||
return evB - evA;
|
||||
|
|
|
|||
|
|
@ -316,8 +316,8 @@ export default function AdminSportsSeasonFuturesOdds() {
|
|||
Not matched ({parseResults.unmatched.length}) — enter manually below
|
||||
</div>
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 divide-y divide-amber-100 text-sm">
|
||||
{parseResults.unmatched.map((u, i) => (
|
||||
<div key={`${u.inputName}-${i}`} className="flex justify-between px-3 py-1.5">
|
||||
{parseResults.unmatched.map((u) => (
|
||||
<div key={u.inputName} className="flex justify-between px-3 py-1.5">
|
||||
<span>{u.inputName}</span>
|
||||
<span className="font-medium">{u.odds > 0 ? '+' : ''}{u.odds}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
|
|||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
<TableHead className="w-[50px]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||
export async function action({ request, params }: Route.ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
|
||||
const isNhl = formData.get("_isNhl") === "true";
|
||||
|
||||
// Parse updates from form fields: wins_<id>, losses_<id>, otLosses_<id>, conference_<id>, division_<id>
|
||||
const byId = new Map<
|
||||
string,
|
||||
|
|
@ -87,7 +85,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
|
||||
try {
|
||||
for (const [participantId, data] of byId.entries()) {
|
||||
if (data.wins == null && data.losses == null) continue;
|
||||
if (data.wins === null && data.losses === null) continue;
|
||||
const wins = data.wins ?? 0;
|
||||
const losses = data.losses ?? 0;
|
||||
const gamesPlayed = wins + losses + (data.otLosses ?? 0);
|
||||
|
|
@ -116,7 +114,7 @@ export default function ManageRegularStandings({ loaderData, actionData }: Route
|
|||
sportsSeason.sport?.name?.toLowerCase().includes("hockey");
|
||||
|
||||
// Sort participants: those with records first (by wins desc), then unrecorded
|
||||
const sorted = [...participants].sort((a, b) => {
|
||||
const sorted = [...participants].toSorted((a, b) => {
|
||||
const recA = standingsMap[a.id];
|
||||
const recB = standingsMap[b.id];
|
||||
if (recA && recB) return (recB.wins ?? 0) - (recA.wins ?? 0);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default function ManageStandings({ loaderData, actionData }: Route.Compon
|
|||
const { sportsSeason, participants, resultsMap } = loaderData;
|
||||
|
||||
// Sort participants by current position (unranked at bottom)
|
||||
const sorted = [...participants].sort((a, b) => {
|
||||
const sorted = [...participants].toSorted((a, b) => {
|
||||
const posA = resultsMap[a.id]?.currentPosition ?? Infinity;
|
||||
const posB = resultsMap[b.id]?.currentPosition ?? Infinity;
|
||||
return posA - posB;
|
||||
|
|
@ -138,7 +138,7 @@ export default function ManageStandings({ loaderData, actionData }: Route.Compon
|
|||
<span>Points</span>
|
||||
</div>
|
||||
|
||||
{sorted.map((participant, index) => {
|
||||
{sorted.map((participant) => {
|
||||
const result = resultsMap[participant.id];
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { isUserAdminByClerkId } from "~/models/user";
|
|||
import type { Route } from "./+types/admin.sports-seasons.$id";
|
||||
|
||||
import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason } from "~/models/sports-season";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||||
import { processSeasonStandings, recalculateStandings } from "~/models/scoring-calculator";
|
||||
import { createDailySnapshot } from "~/models/standings";
|
||||
import { database } from "~/database/context";
|
||||
|
|
@ -12,13 +11,12 @@ import { participantEvSnapshots, seasonSports } from "~/database/schema";
|
|||
import { eq, desc } from "drizzle-orm";
|
||||
import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry";
|
||||
import { syncStandings } from "~/services/standings-sync/index";
|
||||
import { getLastSyncedAt } from "~/models/regular-season-standings";
|
||||
import {
|
||||
getPendingStandingsMappings,
|
||||
deletePendingStandingsMapping,
|
||||
} from "~/models/pending-standings-mappings";
|
||||
import { updateParticipant } from "~/models/participant";
|
||||
import { upsertRegularSeasonStandings } from "~/models/regular-season-standings";
|
||||
import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/participant";
|
||||
import { getLastSyncedAt, upsertRegularSeasonStandings } from "~/models/regular-season-standings";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useLoaderData, useActionData, Form } from "react-router";
|
||||
import { redirect } from "react-router";
|
||||
import { Form } from "react-router";
|
||||
import type { Route } from "./+types/admin.standings-snapshots";
|
||||
|
||||
import { database } from "~/database/context";
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
|
||||
const upcomingCalendarEvents = leaguesWithData
|
||||
.flatMap((l) => l.calendarEvents)
|
||||
.sort((a, b) => toEventSortKey(a).localeCompare(toEventSortKey(b)));
|
||||
.toSorted((a, b) => toEventSortKey(a).localeCompare(toEventSortKey(b)));
|
||||
|
||||
return {
|
||||
leagues: leaguesWithData,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import { database } from "~/database/context";
|
|||
import * as schema from "~/database/schema";
|
||||
import { useDraftSocket } from "~/hooks/useDraftSocket";
|
||||
import { useCallback, useEffect, useState, useMemo, useRef } from "react";
|
||||
import { Card } from "~/components/ui/card";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "~/components/ui/dialog";
|
||||
|
|
@ -221,7 +219,6 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
userAutodraftSettings,
|
||||
isCommissioner: !!isCommissioner,
|
||||
currentUserId: userId,
|
||||
numFlexPicks: Math.max(0, season.draftRounds - seasonSports.length),
|
||||
ownerMap,
|
||||
};
|
||||
}
|
||||
|
|
@ -239,7 +236,6 @@ export default function DraftRoom() {
|
|||
userAutodraftSettings,
|
||||
isCommissioner,
|
||||
currentUserId,
|
||||
numFlexPicks,
|
||||
ownerMap,
|
||||
} = useLoaderData<typeof loader>();
|
||||
const { revalidate, state: revalidatorState } = useRevalidator();
|
||||
|
|
@ -640,7 +636,7 @@ export default function DraftRoom() {
|
|||
sportsMap.set(p.sport.id, { id: p.sport.id, name: p.sport.name });
|
||||
}
|
||||
});
|
||||
return Array.from(sportsMap.values()).sort((a, b) =>
|
||||
return Array.from(sportsMap.values()).toSorted((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
}, [availableParticipants]);
|
||||
|
|
@ -753,7 +749,7 @@ export default function DraftRoom() {
|
|||
// if the HTTP revalidation hasn't completed yet.
|
||||
// React's useState already bails out when the new value equals the old
|
||||
// one (Object.is comparison on primitives), so no manual guard needed.
|
||||
if (data.currentPickNumber != null) {
|
||||
if (data.currentPickNumber !== null) {
|
||||
setCurrentPick(data.currentPickNumber);
|
||||
}
|
||||
};
|
||||
|
|
@ -989,7 +985,7 @@ export default function DraftRoom() {
|
|||
const error = await response.json();
|
||||
toast.error(error.error || "Failed to add to queue");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Revert the optimistic update on network error
|
||||
setQueue((prev) => prev.filter((item) => item.id !== tempQueueItem.id));
|
||||
toast.error("Network error - failed to add to queue");
|
||||
|
|
@ -1621,9 +1617,9 @@ export default function DraftRoom() {
|
|||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -85,14 +85,14 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
const ownerMap = new Map(
|
||||
ownerIds
|
||||
.map((id) => [id, userByClerkId.get(id)] as const)
|
||||
.filter((entry): entry is [string, NonNullable<typeof entry[1]>] => entry[1] != null)
|
||||
.filter((entry): entry is [string, NonNullable<typeof entry[1]>] => entry[1] !== undefined)
|
||||
.map(([id, user]) => [id, getUserDisplayName(user)])
|
||||
);
|
||||
|
||||
const commissionerMap = new Map(
|
||||
commissionerIds
|
||||
.map((id) => [id, userByClerkId.get(id)] as const)
|
||||
.filter((entry): entry is [string, NonNullable<typeof entry[1]>] => entry[1] != null)
|
||||
.filter((entry): entry is [string, NonNullable<typeof entry[1]>] => entry[1] !== undefined)
|
||||
.map(([id, user]) => [id, getUserDisplayName(user)])
|
||||
);
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
sportsSeasonPageUrl: `/leagues/${leagueId}/sports-seasons/${ss.id}`,
|
||||
}))
|
||||
)
|
||||
.sort((a, b) => toEventSortKey(a).localeCompare(toEventSortKey(b)));
|
||||
.toSorted((a, b) => toEventSortKey(a).localeCompare(toEventSortKey(b)));
|
||||
|
||||
// Check if draft order is set
|
||||
const isDraftOrderSet = draftSlots.length > 0;
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export default function SportSeasonDetail({
|
|||
|
||||
const hasBracket = playoffMatches && playoffMatches.length > 0;
|
||||
const hasStandings = regularSeasonStandings && regularSeasonStandings.length > 0;
|
||||
const showOtLosses = hasStandings && regularSeasonStandings.some((s: any) => s.otLosses != null);
|
||||
const showOtLosses = hasStandings && regularSeasonStandings.some((s: any) => s.otLosses !== null);
|
||||
|
||||
const simulatorType = sportsSeason.sport?.simulatorType;
|
||||
const standingsDisplayMode =
|
||||
|
|
|
|||
|
|
@ -85,12 +85,12 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
]);
|
||||
|
||||
// Build teamId -> ownerName map
|
||||
const ownerIds = [...new Set(teams.map((t) => t.ownerId).filter((id): id is string => id != null))];
|
||||
const ownerIds = [...new Set(teams.map((t) => t.ownerId).filter((id): id is string => id !== null))];
|
||||
const userRows = await findUsersByClerkIds(ownerIds);
|
||||
const userByClerkId = new Map(userRows.map((u) => [u.clerkId, u]));
|
||||
const ownerNameByTeamId = new Map(
|
||||
teams
|
||||
.filter((t): t is typeof t & { ownerId: string } => t.ownerId != null)
|
||||
.filter((t): t is typeof t & { ownerId: string } => t.ownerId !== null)
|
||||
.map((t) => {
|
||||
const user = userByClerkId.get(t.ownerId);
|
||||
return [t.id, user ? getUserDisplayName(user) : null] as const;
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
const standingsMap = new Map(standings.map((s) => [s.teamId, s]));
|
||||
|
||||
// Pre-compute sorted standings list
|
||||
const sortedTeams = [...teams].sort((a, b) => {
|
||||
const sortedTeams = [...teams].toSorted((a, b) => {
|
||||
const rankA = standingsMap.get(a.id)?.currentRank ?? Infinity;
|
||||
const rankB = standingsMap.get(b.id)?.currentRank ?? Infinity;
|
||||
return rankA - rankB;
|
||||
|
|
@ -95,7 +95,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
|
||||
// Pre-compute sorted sports seasons: active → upcoming → completed,
|
||||
// season_standings last within active, then alphabetical by sport name
|
||||
const sortedSportsSeasons = [...sportsSeasons].sort((a, b) => {
|
||||
const sortedSportsSeasons = [...sportsSeasons].toSorted((a, b) => {
|
||||
const statusOrder = { active: 0, upcoming: 1, completed: 2 } as const;
|
||||
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
||||
if (statusDiff !== 0) return statusDiff;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ describe("timer-update handler – currentPickNumber sync", () => {
|
|||
...prev,
|
||||
[data.teamId]: data.timeRemaining,
|
||||
}));
|
||||
if (data.currentPickNumber != null) {
|
||||
if (data.currentPickNumber !== null) {
|
||||
setCurrentPick(data.currentPickNumber);
|
||||
}
|
||||
};
|
||||
|
|
@ -132,7 +132,7 @@ describe("timer-update handler – currentPickNumber sync", () => {
|
|||
const setCurrentPick = (value: number) => { currentPick = value; };
|
||||
|
||||
const handleTimerUpdate = (data: any) => {
|
||||
if (data.currentPickNumber != null) {
|
||||
if (data.currentPickNumber !== null) {
|
||||
setCurrentPick(data.currentPickNumber);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { updateLeague, findLeagueById } from '~/models/league';
|
||||
import { isCommissioner } from '~/models/commissioner';
|
||||
import { updateLeague } from '~/models/league';
|
||||
import { findCurrentSeasonWithSports, updateSeason } from '~/models/season';
|
||||
import { findTeamsBySeasonId } from '~/models/team';
|
||||
|
||||
// Mock all models used in the settings update intent
|
||||
vi.mock('~/models/league', () => ({
|
||||
|
|
@ -175,7 +173,7 @@ describe('Settings Update - League Save', () => {
|
|||
let result: { error: string } | undefined;
|
||||
try {
|
||||
await updateLeague('league-1', { name: 'Test League', isPublicDraftBoard: false });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
result = { error: 'Failed to update league. Please try again.' };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export default function TestSocket() {
|
|||
) : (
|
||||
<ul className="space-y-2">
|
||||
{messages.map((msg, idx) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<li key={idx} className="text-sm font-mono bg-card p-2 rounded">
|
||||
{msg}
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe('bracket-simulator', () => {
|
|||
expect(results.size).toBe(8);
|
||||
|
||||
// With equal ratings, each team should have roughly equal probability
|
||||
results.forEach((distribution, participantId) => {
|
||||
results.forEach((distribution, _participantId) => {
|
||||
// Sum of probabilities should be 1.0
|
||||
const sum = distribution.reduce((acc, p) => acc + p, 0);
|
||||
expect(sum).toBeCloseTo(1.0, 1);
|
||||
|
|
@ -96,7 +96,7 @@ describe('bracket-simulator', () => {
|
|||
|
||||
const results = simulateBracketSync(teams, 'nhl-8', 5000);
|
||||
|
||||
results.forEach((distribution, participantId) => {
|
||||
results.forEach((distribution, _participantId) => {
|
||||
const sum = distribution.reduce((acc, p) => acc + p, 0);
|
||||
expect(sum).toBeCloseTo(1.0, 1);
|
||||
});
|
||||
|
|
@ -154,7 +154,7 @@ describe('bracket-simulator', () => {
|
|||
|
||||
const results = simulateBracketSync(teams, 'nhl-8', 1000);
|
||||
|
||||
results.forEach((distribution, participantId) => {
|
||||
results.forEach((distribution, _participantId) => {
|
||||
// Should be array of 8 numbers
|
||||
expect(distribution).toHaveLength(8);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
eloWinProbability,
|
||||
convertFuturesToElo,
|
||||
calculatePredictionError,
|
||||
DEFAULT_CALIBRATION,
|
||||
} from '../probability-engine';
|
||||
|
||||
describe('probability-engine', () => {
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ describe("NCAAM bracket advancement path", () => {
|
|||
|
||||
it("E8 match i uses 0-indexed S16 winners at (i-1)*2 and (i-1)*2+1", () => {
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const p1 = (i - 1) * 2;
|
||||
const _p1 = (i - 1) * 2;
|
||||
const p2 = (i - 1) * 2 + 1;
|
||||
expect(p2).toBeLessThan(8);
|
||||
}
|
||||
|
|
@ -154,7 +154,7 @@ describe("NCAAM bracket advancement path", () => {
|
|||
|
||||
it("FF match i uses 0-indexed E8 winners at (i-1)*2 and (i-1)*2+1", () => {
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
const p1 = (i - 1) * 2;
|
||||
const _p1 = (i - 1) * 2;
|
||||
const p2 = (i - 1) * 2 + 1;
|
||||
expect(p2).toBeLessThan(4);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ describe("NCAAW bracket advancement path", () => {
|
|||
|
||||
it("E8 match i uses 0-indexed S16 winners at (i-1)*2 and (i-1)*2+1", () => {
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const p1 = (i - 1) * 2;
|
||||
const _p1 = (i - 1) * 2;
|
||||
const p2 = (i - 1) * 2 + 1;
|
||||
expect(p2).toBeLessThan(8);
|
||||
}
|
||||
|
|
@ -169,7 +169,7 @@ describe("NCAAW bracket advancement path", () => {
|
|||
|
||||
it("FF match i uses 0-indexed E8 winners at (i-1)*2 and (i-1)*2+1", () => {
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
const p1 = (i - 1) * 2;
|
||||
const _p1 = (i - 1) * 2;
|
||||
const p2 = (i - 1) * 2 + 1;
|
||||
expect(p2).toBeLessThan(4);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ describe("UCLSimulator.simulate()", () => {
|
|||
// Full tournament completed: 8 R16 losers, 4 QF losers, 2 SF losers, 1 finalist, 1 champion
|
||||
// Use team-1 as champion path: R16m1→QFm1→SFm1→Final
|
||||
const champion = PARTICIPANT_IDS[0]; // team-1, participant1 of R16 match 1
|
||||
const finalist = PARTICIPANT_IDS[2]; // team-3, participant1 of R16 match 2
|
||||
const _finalist = PARTICIPANT_IDS[2]; // team-3, participant1 of R16 match 2
|
||||
|
||||
const r16Matches = Array.from({ length: 8 }, (_, i) => {
|
||||
const matchNum = i + 1;
|
||||
|
|
@ -301,8 +301,8 @@ describe("UCLSimulator.simulate()", () => {
|
|||
// QF: R16 match 1 winner (team-1) + R16 match 2 winner (team-4) → QF match 1: team-1 wins
|
||||
// R16 match 3 winner (team-6) + R16 match 4 winner (team-8) → QF match 2: team-6 wins
|
||||
// QF match 3: team-10 wins, QF match 4: team-14 wins
|
||||
const qfWinners = [champion, PARTICIPANT_IDS[3], PARTICIPANT_IDS[9], PARTICIPANT_IDS[13]];
|
||||
const qfLosers = [PARTICIPANT_IDS[3], PARTICIPANT_IDS[5], PARTICIPANT_IDS[7], PARTICIPANT_IDS[11]];
|
||||
const _qfWinners = [champion, PARTICIPANT_IDS[3], PARTICIPANT_IDS[9], PARTICIPANT_IDS[13]];
|
||||
const _qfLosers = [PARTICIPANT_IDS[3], PARTICIPANT_IDS[5], PARTICIPANT_IDS[7], PARTICIPANT_IDS[11]];
|
||||
// Actually: QF1: team-1 beats team-4; QF2: team-6 beats team-8; etc.
|
||||
const qfMatches = Array.from({ length: 4 }, (_, i) => {
|
||||
const matchNum = i + 1;
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export class AutoRacingSimulator implements Simulator {
|
|||
|
||||
for (const p of participants) {
|
||||
const ev = evMap.get(p.id);
|
||||
rawProbs.set(p.id, ev?.sourceOdds != null ? americanToImpliedProb(ev.sourceOdds) : fallbackProb);
|
||||
rawProbs.set(p.id, ev?.sourceOdds !== null ? americanToImpliedProb(ev.sourceOdds) : fallbackProb);
|
||||
}
|
||||
|
||||
// Normalize to remove vig
|
||||
|
|
@ -164,7 +164,7 @@ export class AutoRacingSimulator implements Simulator {
|
|||
// rankCounts[id][0..7] = number of times driver finished 1st..8th
|
||||
const rankCounts = new Map<string, number[]>();
|
||||
for (const id of ids) {
|
||||
rankCounts.set(id, new Array(8).fill(0));
|
||||
rankCounts.set(id, Array.from({ length: 8 }, () => 0));
|
||||
}
|
||||
|
||||
if (remainingRaces === 0) {
|
||||
|
|
@ -215,7 +215,7 @@ export class AutoRacingSimulator implements Simulator {
|
|||
|
||||
// 7d. Sort by final championship points, record top-8 finishes
|
||||
const finalOrder = [...simPoints.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.toSorted((a, b) => b[1] - a[1])
|
||||
.map(([id]) => id);
|
||||
|
||||
for (let rank = 0; rank < Math.min(8, finalOrder.length); rank++) {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
*/
|
||||
|
||||
import { database } from "~/database/context";
|
||||
import { participantExpectedValues, participants } from "~/database/schema";
|
||||
import { eq, and, isNotNull } from "drizzle-orm";
|
||||
import { participantExpectedValues } from "~/database/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { simulateBracket } from "~/services/bracket-simulator";
|
||||
import { convertFuturesToElo } from "~/services/probability-engine";
|
||||
import type { Simulator, SimulationResult } from "./types";
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ export class NCAAMSimulator implements Simulator {
|
|||
}
|
||||
|
||||
const sortByMatchNumber = (matches: typeof allMatches) =>
|
||||
[...matches].sort((a, b) => a.matchNumber - b.matchNumber);
|
||||
[...matches].toSorted((a, b) => a.matchNumber - b.matchNumber);
|
||||
|
||||
const firstFourMatches = byRound.has("First Four") ? sortByMatchNumber(byRound.get("First Four")!) : [];
|
||||
const r64Matches = byRound.has("Round of 64") ? sortByMatchNumber(byRound.get("Round of 64")!) : null;
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ export class NCAAWSimulator implements Simulator {
|
|||
}
|
||||
|
||||
const sortByMatchNumber = (matches: typeof allMatches) =>
|
||||
[...matches].sort((a, b) => a.matchNumber - b.matchNumber);
|
||||
[...matches].toSorted((a, b) => a.matchNumber - b.matchNumber);
|
||||
|
||||
const firstFourMatches = byRound.has("First Four") ? sortByMatchNumber(byRound.get("First Four")!) : [];
|
||||
const r64Matches = byRound.has("Round of 64") ? sortByMatchNumber(byRound.get("Round of 64")!) : null;
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export class UCLSimulator implements Simulator {
|
|||
}
|
||||
|
||||
const sortedRoundMatches = [...byRound.values()]
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.toSorted((a, b) => b.length - a.length)
|
||||
.map((matches) => matches.sort((a, b) => a.matchNumber - b.matchNumber));
|
||||
|
||||
if (sortedRoundMatches.length < 4) {
|
||||
|
|
@ -178,7 +178,7 @@ export class UCLSimulator implements Simulator {
|
|||
const ev = evMap.get(id);
|
||||
rawProbs.set(
|
||||
id,
|
||||
ev?.sourceOdds != null ? americanToImpliedProb(ev.sourceOdds) : fallbackProb
|
||||
ev?.sourceOdds !== null ? americanToImpliedProb(ev.sourceOdds) : fallbackProb
|
||||
);
|
||||
}
|
||||
const rawSum = [...rawProbs.values()].reduce((a, b) => a + b, 0);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { findMatchingTeamName } from "~/lib/normalize-team-name";
|
||||
|
||||
// Pure unit tests for the name-matching logic used by syncStandings.
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export class NbaStandingsAdapter implements StandingsSyncAdapter {
|
|||
}
|
||||
|
||||
// Assign league rank by sorting on wins desc, then losses asc
|
||||
const sorted = [...flattened].sort((a, b) => {
|
||||
const sorted = [...flattened].toSorted((a, b) => {
|
||||
const winsA = statsMap(a.entry.stats).get("wins")?.value ?? 0;
|
||||
const winsB = statsMap(b.entry.stats).get("wins")?.value ?? 0;
|
||||
if (winsB !== winsA) return winsB - winsA;
|
||||
|
|
@ -122,7 +122,7 @@ export class NbaStandingsAdapter implements StandingsSyncAdapter {
|
|||
// Seed within conference — ESPN key is "playoffSeed", value is a float (1.0, 2.0 ...)
|
||||
const playoffSeedStat = sm.get("playoffSeed");
|
||||
const conferenceRank =
|
||||
playoffSeedStat?.value != null
|
||||
playoffSeedStat?.value !== null
|
||||
? Math.round(playoffSeedStat.value)
|
||||
: playoffSeedStat?.displayValue
|
||||
? parseInt(playoffSeedStat.displayValue, 10) || undefined
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ export async function importSportsDataFromJSON(
|
|||
importData = ImportDataSchema.parse(JSON.parse(jsonData));
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Invalid import file: ${err instanceof Error ? err.message : "unknown validation error"}`
|
||||
`Invalid import file: ${err instanceof Error ? err.message : "unknown validation error"}`, { cause: err }
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|||
|
||||
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
||||
|
||||
import * as schema from "./schema";
|
||||
import type * as schema from "./schema";
|
||||
|
||||
export const DatabaseContext = new AsyncLocalStorage<
|
||||
PostgresJsDatabase<typeof schema>
|
||||
|
|
|
|||
369
package-lock.json
generated
369
package-lock.json
generated
|
|
@ -76,6 +76,7 @@
|
|||
"dotenv-cli": "^8.0.0",
|
||||
"esbuild": "^0.25.11",
|
||||
"jsdom": "^27.0.1",
|
||||
"oxlint": "^1.56.0",
|
||||
"shadcn": "^4.0.8",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tsx": "^4.20.6",
|
||||
|
|
@ -3227,6 +3228,329 @@
|
|||
"@opentelemetry/api": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-android-arm-eabi": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.56.0.tgz",
|
||||
"integrity": "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-android-arm64": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.56.0.tgz",
|
||||
"integrity": "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-darwin-arm64": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.56.0.tgz",
|
||||
"integrity": "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-darwin-x64": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.56.0.tgz",
|
||||
"integrity": "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-freebsd-x64": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.56.0.tgz",
|
||||
"integrity": "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.56.0.tgz",
|
||||
"integrity": "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.56.0.tgz",
|
||||
"integrity": "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm64-gnu": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.56.0.tgz",
|
||||
"integrity": "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm64-musl": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.56.0.tgz",
|
||||
"integrity": "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.56.0.tgz",
|
||||
"integrity": "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.56.0.tgz",
|
||||
"integrity": "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-riscv64-musl": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.56.0.tgz",
|
||||
"integrity": "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-s390x-gnu": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.56.0.tgz",
|
||||
"integrity": "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-x64-gnu": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.56.0.tgz",
|
||||
"integrity": "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-x64-musl": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.56.0.tgz",
|
||||
"integrity": "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-openharmony-arm64": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.56.0.tgz",
|
||||
"integrity": "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-arm64-msvc": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.56.0.tgz",
|
||||
"integrity": "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-ia32-msvc": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.56.0.tgz",
|
||||
"integrity": "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-x64-msvc": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.56.0.tgz",
|
||||
"integrity": "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
|
|
@ -12391,6 +12715,51 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oxlint": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.56.0.tgz",
|
||||
"integrity": "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"oxlint": "bin/oxlint"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxlint/binding-android-arm-eabi": "1.56.0",
|
||||
"@oxlint/binding-android-arm64": "1.56.0",
|
||||
"@oxlint/binding-darwin-arm64": "1.56.0",
|
||||
"@oxlint/binding-darwin-x64": "1.56.0",
|
||||
"@oxlint/binding-freebsd-x64": "1.56.0",
|
||||
"@oxlint/binding-linux-arm-gnueabihf": "1.56.0",
|
||||
"@oxlint/binding-linux-arm-musleabihf": "1.56.0",
|
||||
"@oxlint/binding-linux-arm64-gnu": "1.56.0",
|
||||
"@oxlint/binding-linux-arm64-musl": "1.56.0",
|
||||
"@oxlint/binding-linux-ppc64-gnu": "1.56.0",
|
||||
"@oxlint/binding-linux-riscv64-gnu": "1.56.0",
|
||||
"@oxlint/binding-linux-riscv64-musl": "1.56.0",
|
||||
"@oxlint/binding-linux-s390x-gnu": "1.56.0",
|
||||
"@oxlint/binding-linux-x64-gnu": "1.56.0",
|
||||
"@oxlint/binding-linux-x64-musl": "1.56.0",
|
||||
"@oxlint/binding-openharmony-arm64": "1.56.0",
|
||||
"@oxlint/binding-win32-arm64-msvc": "1.56.0",
|
||||
"@oxlint/binding-win32-ia32-msvc": "1.56.0",
|
||||
"@oxlint/binding-win32-x64-msvc": "1.56.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"oxlint-tsgolint": ">=0.15.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"oxlint-tsgolint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@
|
|||
"test:e2e": "cypress open",
|
||||
"test:e2e:headless": "cypress run",
|
||||
"test:all": "npm run test:run && npm run test:e2e:headless",
|
||||
"typecheck": "react-router typegen && tsc -b && tsc -p tsconfig.server.json --noEmit"
|
||||
"typecheck": "react-router typegen && tsc -b && tsc -p tsconfig.server.json --noEmit",
|
||||
"lint": "oxlint app/ server/ database/",
|
||||
"lint:fix": "oxlint app/ server/ database/ --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/react-router": "^2.1.0",
|
||||
|
|
@ -91,6 +93,7 @@
|
|||
"dotenv-cli": "^8.0.0",
|
||||
"esbuild": "^0.25.11",
|
||||
"jsdom": "^27.0.1",
|
||||
"oxlint": "^1.56.0",
|
||||
"shadcn": "^4.0.8",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tsx": "^4.20.6",
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ export default {
|
|||
|
||||
buildEnd: async (
|
||||
{
|
||||
viteConfig: viteConfig,
|
||||
reactRouterConfig: reactRouterConfig,
|
||||
buildManifest: buildManifest
|
||||
viteConfig,
|
||||
reactRouterConfig,
|
||||
buildManifest
|
||||
}
|
||||
) => {
|
||||
await sentryOnBuildEnd({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, inArray, or } from "drizzle-orm";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { createDailySnapshot } from "~/models/standings";
|
||||
|
||||
// Create a dedicated database connection for the snapshot system
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Server as SocketIOServer, Socket } from "socket.io";
|
||||
import type { Socket } from "socket.io";
|
||||
import { Server as SocketIOServer } from "socket.io";
|
||||
import type { Server as HTTPServer } from "http";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
||||
|
|
|
|||
2
server/types.d.ts
vendored
2
server/types.d.ts
vendored
|
|
@ -4,4 +4,4 @@ declare global {
|
|||
var __socketIO: SocketIOServer | undefined;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue