Add oxlint linting setup with zero errors (#194)
* 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> * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial<NewSeason> indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bcca8b76fa
commit
e2b178221a
97 changed files with 767 additions and 325 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": "error",
|
||||
"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/**"
|
||||
]
|
||||
}
|
||||
|
|
@ -6,6 +6,13 @@ import {
|
|||
ContextMenuTrigger,
|
||||
} from "~/components/ui/context-menu";
|
||||
|
||||
interface DraftCell {
|
||||
pickNumber: number;
|
||||
participant: { id: string; name: string };
|
||||
sport: { id: string; name: string };
|
||||
team: { id: string; name: string };
|
||||
}
|
||||
|
||||
interface DraftGridProps {
|
||||
draftSlots: Array<{
|
||||
id: string;
|
||||
|
|
@ -15,7 +22,7 @@ interface DraftGridProps {
|
|||
name: string;
|
||||
};
|
||||
}>;
|
||||
draftGrid: any[][];
|
||||
draftGrid: Array<Array<DraftCell | null>>;
|
||||
currentPick: number;
|
||||
teamTimers?: Record<string, number>;
|
||||
formatTime?: (seconds: number | undefined) => string;
|
||||
|
|
@ -92,11 +99,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
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import { mockDraftSlots } from '~/test/fixtures/team';
|
|||
describe('DraftGrid Component', () => {
|
||||
const mockGrid = [
|
||||
[
|
||||
{ participant: { name: 'Player 1' }, sport: { name: 'NFL' } },
|
||||
{ pickNumber: 1, participant: { id: 'p1', name: 'Player 1' }, sport: { id: 's1', name: 'NFL' }, team: { id: 't1', name: 'Team 1' } },
|
||||
null,
|
||||
],
|
||||
[
|
||||
null,
|
||||
{ participant: { name: 'Player 2' }, sport: { name: 'NBA' } },
|
||||
{ pickNumber: 4, participant: { id: 'p2', name: 'Player 2' }, sport: { id: 's2', name: 'NBA' }, team: { id: 't2', name: 'Team 2' } },
|
||||
],
|
||||
];
|
||||
|
||||
|
|
@ -143,12 +143,12 @@ describe('DraftGrid Component', () => {
|
|||
it('should reverse picks on even rounds', () => {
|
||||
const snakeGrid = [
|
||||
[
|
||||
{ participant: { name: 'Pick 1' }, sport: { name: 'NFL' } },
|
||||
{ participant: { name: 'Pick 2' }, sport: { name: 'NBA' } },
|
||||
{ pickNumber: 1, participant: { id: 'p1', name: 'Pick 1' }, sport: { id: 's1', name: 'NFL' }, team: { id: 't1', name: 'Team 1' } },
|
||||
{ pickNumber: 2, participant: { id: 'p2', name: 'Pick 2' }, sport: { id: 's2', name: 'NBA' }, team: { id: 't2', name: 'Team 2' } },
|
||||
],
|
||||
[
|
||||
{ participant: { name: 'Pick 4' }, sport: { name: 'MLB' } },
|
||||
{ participant: { name: 'Pick 3' }, sport: { name: 'NHL' } },
|
||||
{ pickNumber: 3, participant: { id: 'p3', name: 'Pick 4' }, sport: { id: 's1', name: 'MLB' }, team: { id: 't2', name: 'Team 2' } },
|
||||
{ pickNumber: 4, participant: { id: 'p4', name: 'Pick 3' }, sport: { id: 's2', name: 'NHL' }, team: { id: 't1', name: 'Team 1' } },
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ interface StandingRow {
|
|||
lastTen: string | null;
|
||||
homeRecord: string | null;
|
||||
awayRecord: string | null;
|
||||
syncedAt: string | null;
|
||||
syncedAt: Date | string | null;
|
||||
participant: { id: string; name: string; shortName?: string | null };
|
||||
}
|
||||
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ interface UseDraftSocketReturn {
|
|||
/** Increments each time a new socket instance is created. Include in
|
||||
* useEffect deps so handler effects re-register on socket recreation. */
|
||||
socketVersion: number;
|
||||
// eslint-disable-next-line typescript/no-explicit-any -- socket.io callbacks are untyped at the hook level
|
||||
on: (event: string, callback: (...args: any[]) => void) => void;
|
||||
// eslint-disable-next-line typescript/no-explicit-any
|
||||
off: (event: string, callback?: (...args: any[]) => void) => void;
|
||||
emit: (event: string, ...args: any[]) => void;
|
||||
emit: (event: string, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocketReturn {
|
||||
|
|
@ -131,15 +133,17 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke
|
|||
// has run (socketRef.current === null) are silently no-ops. Consumers should
|
||||
// only call them inside their own useEffect, not during render or synchronously
|
||||
// after mount.
|
||||
// eslint-disable-next-line typescript/no-explicit-any -- socket.io callbacks are untyped at the hook level
|
||||
const on = useCallback((event: string, callback: (...args: any[]) => void) => {
|
||||
socketRef.current?.on(event, callback);
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line typescript/no-explicit-any
|
||||
const off = useCallback((event: string, callback?: (...args: any[]) => void) => {
|
||||
socketRef.current?.off(event, callback);
|
||||
}, []);
|
||||
|
||||
const emit = useCallback((event: string, ...args: any[]) => {
|
||||
const emit = useCallback((event: string, ...args: unknown[]) => {
|
||||
socketRef.current?.emit(event, ...args);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { database } from "~/database/context";
|
|||
import * as schema from "~/database/schema";
|
||||
|
||||
export type ParticipantResult = typeof schema.participantResults.$inferSelect;
|
||||
export type ParticipantResultWithParticipant = ParticipantResult & {
|
||||
participant: { id: string; name: string } | null;
|
||||
};
|
||||
export type NewParticipantResult = typeof schema.participantResults.$inferInsert;
|
||||
|
||||
export async function createParticipantResult(
|
||||
|
|
@ -62,7 +65,7 @@ export async function findParticipantResultByParticipantId(
|
|||
|
||||
export async function findParticipantResultsBySportsSeasonId(
|
||||
sportsSeasonId: string
|
||||
): Promise<ParticipantResult[]> {
|
||||
): Promise<ParticipantResultWithParticipant[]> {
|
||||
const db = database();
|
||||
return await db.query.participantResults.findMany({
|
||||
where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export async function updateScoringEvent(
|
|||
) {
|
||||
const db = providedDb || database();
|
||||
|
||||
const updateData: any = { updatedAt: new Date() };
|
||||
const updateData: Partial<typeof schema.scoringEvents.$inferInsert> = { updatedAt: new Date() };
|
||||
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
if (data.eventDate !== undefined) {
|
||||
|
|
@ -450,6 +450,7 @@ export async function getUpcomingEventsForDraftedParticipants(
|
|||
inArray(schema.playoffMatches.participant1Id, draftedIds),
|
||||
// participant2Id is nullable UUID — same underlying type as participant1Id;
|
||||
// the notNull difference only exists at the TypeScript layer
|
||||
// oxlint-disable-next-line typescript/no-explicit-any -- participant2Id is nullable UUID; notNull difference only exists at the TypeScript layer
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
inArray(schema.playoffMatches.participant2Id as any, draftedIds)
|
||||
),
|
||||
|
|
@ -503,7 +504,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 +579,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);
|
||||
|
|
@ -402,7 +402,7 @@ export async function getSeasonPointProgression(
|
|||
});
|
||||
|
||||
// Organize data by date
|
||||
const dateMap = new Map<string, any>();
|
||||
const dateMap = new Map<string, { date: string; [teamName: string]: string | number }>();
|
||||
|
||||
for (const snapshot of snapshots) {
|
||||
const date = snapshot.snapshotDate;
|
||||
|
|
@ -411,12 +411,12 @@ export async function getSeasonPointProgression(
|
|||
dateMap.set(date, { date });
|
||||
}
|
||||
|
||||
const dateData = dateMap.get(date);
|
||||
const dateData = dateMap.get(date)!;
|
||||
dateData[snapshot.team.name] = parseFloat(snapshot.totalPoints);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
@ -79,6 +79,8 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||
participant2: { id: string; name: string } | null;
|
||||
winner: { id: string; name: string } | null;
|
||||
loser: { id: string; name: string } | null;
|
||||
games: Array<{ id: string; gameNumber: number; scheduledAt: Date | null; status: string; winner?: { name: string } | null }>;
|
||||
odds: Array<{ id: string; participantId: string; moneylineOdds: number; impliedProbability: string; oddsSource?: string | null; participant: { id: string; name: string } | null }>;
|
||||
}>,
|
||||
tournamentGroups,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -151,8 +151,8 @@ export default function EventBracket({
|
|||
const knockoutPopulated = useMemo(() => {
|
||||
if (!isGroupStageEvent) return false;
|
||||
// Check if any Round of 32 match has participants assigned
|
||||
const r32Matches = matches.filter((m: any) => m.round === "Round of 32");
|
||||
return r32Matches.some((m: any) => m.participant1Id || m.participant2Id);
|
||||
const r32Matches = matches.filter((m) => m.round === "Round of 32");
|
||||
return r32Matches.some((m) => m.participant1Id || m.participant2Id);
|
||||
}, [isGroupStageEvent, matches]);
|
||||
|
||||
// Group stage stats
|
||||
|
|
@ -161,7 +161,7 @@ export default function EventBracket({
|
|||
let eliminated = 0;
|
||||
let total = 0;
|
||||
for (const group of tournamentGroups) {
|
||||
for (const member of (group as any).members || []) {
|
||||
for (const member of group.members || []) {
|
||||
total++;
|
||||
if (member.eliminated) eliminated++;
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ export default function EventBracket({
|
|||
if (!isGroupStageEvent) return [];
|
||||
const advancing: Array<{ id: string; name: string }> = [];
|
||||
for (const group of tournamentGroups) {
|
||||
for (const member of (group as any).members || []) {
|
||||
for (const member of group.members || []) {
|
||||
if (!member.eliminated && member.participant) {
|
||||
advancing.push({
|
||||
id: member.participant.id,
|
||||
|
|
@ -258,7 +258,7 @@ export default function EventBracket({
|
|||
const getPendingWinnersInRound = (round: string) => {
|
||||
return Object.entries(selectedWinners)
|
||||
.filter(([matchId]) => {
|
||||
const match = matches.find((m: any) => m.id === matchId);
|
||||
const match = matches.find((m) => m.id === matchId);
|
||||
return match && match.round === round && !match.isComplete;
|
||||
});
|
||||
};
|
||||
|
|
@ -283,7 +283,7 @@ export default function EventBracket({
|
|||
|
||||
// Check if all matches are complete
|
||||
const allMatchesComplete = useMemo(() => {
|
||||
return matches.length > 0 && matches.every((m: any) => m.isComplete);
|
||||
return matches.length > 0 && matches.every((m) => m.isComplete);
|
||||
}, [matches]);
|
||||
|
||||
// No bracket and no groups yet = setup phase
|
||||
|
|
@ -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}
|
||||
|
|
@ -680,13 +685,13 @@ export default function EventBracket({
|
|||
|
||||
{/* Group Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{tournamentGroups.map((group: any) => (
|
||||
{tournamentGroups.map((group) => (
|
||||
<Card key={group.id}>
|
||||
<CardHeader className="py-3 px-4">
|
||||
<CardTitle className="text-base">Group {group.groupName}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4 space-y-2">
|
||||
{(group.members || []).map((member: any) => (
|
||||
{(group.members || []).map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={`flex items-center justify-between gap-2 p-2 rounded-md border ${
|
||||
|
|
@ -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>
|
||||
|
|
@ -924,9 +929,9 @@ export default function EventBracket({
|
|||
</div>
|
||||
|
||||
{/* Existing games */}
|
||||
{(match as any).games?.length > 0 ? (
|
||||
{match.games?.length > 0 ? (
|
||||
<div className="space-y-2 mb-3">
|
||||
{(match as any).games.map((game: any) => (
|
||||
{match.games.map((game) => (
|
||||
<div key={game.id} className="flex items-center gap-2 text-sm bg-background rounded p-2 border">
|
||||
<span className="font-medium w-12 shrink-0">Game {game.gameNumber}</span>
|
||||
<span className="text-muted-foreground flex-1">
|
||||
|
|
@ -982,7 +987,7 @@ export default function EventBracket({
|
|||
type="number"
|
||||
min={1}
|
||||
max={9}
|
||||
defaultValue={((match as any).games?.length ?? 0) + 1}
|
||||
defaultValue={(match.games?.length ?? 0) + 1}
|
||||
className="h-8 w-20"
|
||||
required
|
||||
/>
|
||||
|
|
@ -1009,9 +1014,9 @@ export default function EventBracket({
|
|||
<span className="text-sm font-semibold">Moneyline Odds</span>
|
||||
</div>
|
||||
|
||||
{(match as any).odds?.length > 0 ? (
|
||||
{match.odds?.length > 0 ? (
|
||||
<div className="space-y-2 mb-3">
|
||||
{(match as any).odds.map((odd: any) => (
|
||||
{match.odds.map((odd) => (
|
||||
<div key={odd.id} className="flex items-center gap-2 text-sm bg-background rounded p-2 border">
|
||||
<span className="flex-1 font-medium">{odd.participant?.name}</span>
|
||||
<span className={`font-mono ${odd.moneylineOdds > 0 ? "text-emerald-500" : "text-muted-foreground"}`}>
|
||||
|
|
|
|||
|
|
@ -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,12 +695,12 @@ export default function EventResults({
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{[...participantResults]
|
||||
.sort((a: any, b: any) => {
|
||||
.toSorted((a: { finalPosition?: number | null }, b: { finalPosition?: number | null }) => {
|
||||
const posA = a.finalPosition ?? 999;
|
||||
const posB = b.finalPosition ?? 999;
|
||||
return posA - posB;
|
||||
})
|
||||
.map((result: any) => (
|
||||
.map((result) => (
|
||||
<TableRow key={result.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -730,7 +730,7 @@ export default function EventResults({
|
|||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{result.participant.name}</TableCell>
|
||||
<TableCell>{result.participant?.name}</TableCell>
|
||||
<TableCell className="text-right font-semibold">
|
||||
{result.qualifyingPoints ? (
|
||||
<span className={result.qualifyingPoints === "0.00" ? "text-muted-foreground" : "text-emerald-400"}>
|
||||
|
|
|
|||
|
|
@ -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 !== undefined && 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
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import { getAuth } from "@clerk/react-router/server";
|
|||
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 { findSportsSeasonById, updateSportsSeason, deleteSportsSeason, type NewSportsSeason } from "~/models/sports-season";
|
||||
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";
|
||||
|
|
@ -254,7 +252,7 @@ export async function action(args: Route.ActionArgs) {
|
|||
}
|
||||
|
||||
try {
|
||||
const updateData: any = {
|
||||
const updateData: Partial<NewSportsSeason> = {
|
||||
name: name.trim(),
|
||||
year: yearNum,
|
||||
startDate: typeof startDate === "string" && startDate ? startDate : null,
|
||||
|
|
@ -265,7 +263,7 @@ export async function action(args: Route.ActionArgs) {
|
|||
};
|
||||
|
||||
if (scoringPattern && typeof scoringPattern === "string") {
|
||||
updateData.scoringPattern = scoringPattern;
|
||||
updateData.scoringPattern = scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points";
|
||||
}
|
||||
|
||||
if (totalMajors && typeof totalMajors === "string") {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Form, Link, redirect } from "react-router";
|
||||
import type { Route } from "./+types/admin.sports-seasons.new";
|
||||
|
||||
import { createSportsSeason } from "~/models/sports-season";
|
||||
import { createSportsSeason, type NewSportsSeason } from "~/models/sports-season";
|
||||
import { findAllSports } from "~/models/sport";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
|
|
@ -75,7 +75,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
}
|
||||
|
||||
try {
|
||||
const seasonData: any = {
|
||||
const seasonData: Partial<NewSportsSeason> = {
|
||||
sportId,
|
||||
name: name.trim(),
|
||||
year: yearNum,
|
||||
|
|
@ -86,7 +86,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
};
|
||||
|
||||
if (scoringPattern && typeof scoringPattern === "string") {
|
||||
seasonData.scoringPattern = scoringPattern;
|
||||
seasonData.scoringPattern = scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points";
|
||||
}
|
||||
|
||||
if (totalMajors && typeof totalMajors === "string") {
|
||||
|
|
@ -96,7 +96,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
}
|
||||
}
|
||||
|
||||
await createSportsSeason(seasonData);
|
||||
await createSportsSeason(seasonData as NewSportsSeason);
|
||||
|
||||
return redirect("/admin/sports-seasons");
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
// Create a new Svix instance with your webhook secret
|
||||
const wh = new Webhook(WEBHOOK_SECRET);
|
||||
|
||||
let evt: any;
|
||||
let evt: { type: string; data: { id: string; email_addresses?: Array<{ email_address: string }>; username?: string; first_name?: string; last_name?: string; image_url?: string } };
|
||||
|
||||
// Verify the webhook
|
||||
try {
|
||||
|
|
@ -34,7 +34,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
"svix-id": svix_id,
|
||||
"svix-timestamp": svix_timestamp,
|
||||
"svix-signature": svix_signature,
|
||||
});
|
||||
}) as typeof evt;
|
||||
} catch (err) {
|
||||
console.error("Error verifying webhook:", err);
|
||||
return new Response("Error: Verification failed", { status: 400 });
|
||||
|
|
@ -53,7 +53,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
const user = await findOrCreateUser({
|
||||
id,
|
||||
emailAddresses:
|
||||
email_addresses?.map((e: any) => ({
|
||||
email_addresses?.map((e: { email_address: string }) => ({
|
||||
emailAddress: e.email_address,
|
||||
})) || [],
|
||||
username,
|
||||
|
|
@ -69,7 +69,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
const user = await findOrCreateUser({
|
||||
id,
|
||||
emailAddresses:
|
||||
email_addresses?.map((e: any) => ({
|
||||
email_addresses?.map((e: { email_address: string }) => ({
|
||||
emailAddress: e.email_address,
|
||||
})) || [],
|
||||
username,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -127,9 +127,11 @@ export default function DraftBoard() {
|
|||
useEffect(() => {
|
||||
if (season.status !== "draft") return;
|
||||
|
||||
const handlePickMade = (data: any) => {
|
||||
setPicks((prev: any) => [...prev, data.pick]);
|
||||
setCurrentPick(data.nextPickNumber);
|
||||
type PickShape = (typeof initialPicks)[number];
|
||||
const handlePickMade = (data: unknown) => {
|
||||
const pickData = data as { pick: PickShape; nextPickNumber: number };
|
||||
setPicks((prev) => [...prev, pickData.pick]);
|
||||
setCurrentPick(pickData.nextPickNumber);
|
||||
};
|
||||
|
||||
on("pick-made", handlePickMade);
|
||||
|
|
@ -142,13 +144,14 @@ export default function DraftBoard() {
|
|||
// Generate draft grid
|
||||
const totalTeams = draftSlots.length;
|
||||
const totalRounds = season.draftRounds || 1;
|
||||
const draftGrid: any[][] = [];
|
||||
type PickItem = (typeof initialPicks)[number];
|
||||
const draftGrid: Array<Array<PickItem | null>> = [];
|
||||
|
||||
for (let round = 0; round < totalRounds; round++) {
|
||||
const roundPicks: any[] = [];
|
||||
const roundPicks: Array<PickItem | null> = [];
|
||||
for (let teamIndex = 0; teamIndex < totalTeams; teamIndex++) {
|
||||
const pickNumber = round * totalTeams + teamIndex + 1;
|
||||
const pick = picks.find((p: any) => p.pickNumber === pickNumber);
|
||||
const pick = picks.find((p) => p.pickNumber === pickNumber);
|
||||
roundPicks.push(pick || null);
|
||||
}
|
||||
draftGrid.push(roundPicks);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -81,7 +79,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
|
||||
// Resolve user's team and commissioner status (null userId means neither)
|
||||
const userTeam = userId
|
||||
? season.teams.find((team: any) => team.ownerId === userId)
|
||||
? season.teams.find((team) => team.ownerId === userId)
|
||||
: undefined;
|
||||
|
||||
const isCommissioner = userId
|
||||
|
|
@ -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();
|
||||
|
|
@ -431,7 +427,7 @@ export default function DraftRoom() {
|
|||
// (deduplicated by ID) after the DB snapshot lands.
|
||||
const revalidatorStateRef = useRef(revalidatorState);
|
||||
const isRevalidatingRef = useRef(false);
|
||||
const pendingPicksDuringRevalidationRef = useRef<any[]>([]);
|
||||
const pendingPicksDuringRevalidationRef = useRef<(typeof draftPicks)[number][]>([]);
|
||||
// Track the draftPicks reference when revalidation starts. If it hasn't
|
||||
// changed by the time revalidation completes, the loader didn't return new
|
||||
// data (e.g. network failure) and we should NOT overwrite state that may
|
||||
|
|
@ -471,9 +467,9 @@ export default function DraftRoom() {
|
|||
return;
|
||||
}
|
||||
|
||||
const dbPickIds = new Set(draftPicks.map((p: any) => p.id));
|
||||
const dbPickIds = new Set(draftPicks.map((p) => p.id));
|
||||
const missedPicks = pendingPicksDuringRevalidationRef.current.filter(
|
||||
(p: any) => !dbPickIds.has(p.id)
|
||||
(p) => !dbPickIds.has(p.id)
|
||||
);
|
||||
pendingPicksDuringRevalidationRef.current = [];
|
||||
|
||||
|
|
@ -500,7 +496,7 @@ export default function DraftRoom() {
|
|||
if (timers.length > 0) {
|
||||
setTeamTimers((prev) => {
|
||||
const updated = { ...prev };
|
||||
timers.forEach((timer: any) => {
|
||||
timers.forEach((timer) => {
|
||||
updated[timer.teamId] = timer.timeRemaining;
|
||||
});
|
||||
return updated;
|
||||
|
|
@ -510,7 +506,7 @@ export default function DraftRoom() {
|
|||
// we were away.
|
||||
setAutodraftStatus(() => {
|
||||
const status: Record<string, AutodraftStatusEntry> = {};
|
||||
autodraftSettings.forEach((setting: any) => {
|
||||
autodraftSettings.forEach((setting) => {
|
||||
status[setting.teamId] = {
|
||||
isEnabled: setting.isEnabled,
|
||||
mode: setting.mode,
|
||||
|
|
@ -551,7 +547,7 @@ export default function DraftRoom() {
|
|||
// Track autodraft status for all teams
|
||||
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, AutodraftStatusEntry>>(() => {
|
||||
const status: Record<string, AutodraftStatusEntry> = {};
|
||||
autodraftSettings.forEach((setting: any) => {
|
||||
autodraftSettings.forEach((setting) => {
|
||||
status[setting.teamId] = {
|
||||
isEnabled: setting.isEnabled,
|
||||
mode: setting.mode,
|
||||
|
|
@ -571,7 +567,7 @@ export default function DraftRoom() {
|
|||
|
||||
if (timers.length > 0) {
|
||||
// Draft has started, use actual timers
|
||||
timers.forEach((timer: any) => {
|
||||
timers.forEach((timer) => {
|
||||
timersMap[timer.teamId] = timer.timeRemaining;
|
||||
});
|
||||
} else {
|
||||
|
|
@ -614,7 +610,7 @@ export default function DraftRoom() {
|
|||
// Shared transforms for eligibility calculations
|
||||
const transformedPicks = useMemo(
|
||||
() =>
|
||||
picks.map((p: any) => ({
|
||||
picks.map((p) => ({
|
||||
teamId: p.team.id,
|
||||
participant: {
|
||||
id: p.participant.id,
|
||||
|
|
@ -626,7 +622,7 @@ export default function DraftRoom() {
|
|||
|
||||
const transformedParticipants = useMemo(
|
||||
() =>
|
||||
availableParticipants.map((p: any) => ({
|
||||
availableParticipants.map((p) => ({
|
||||
id: p.id,
|
||||
sport: { id: p.sport.id, name: p.sport.name },
|
||||
})),
|
||||
|
|
@ -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]);
|
||||
|
|
@ -649,7 +645,7 @@ export default function DraftRoom() {
|
|||
const eligibility = useMemo(() => {
|
||||
if (!userTeam) return null;
|
||||
const userTeamPicks = transformedPicks.filter(
|
||||
(p: any) => p.teamId === userTeam.id
|
||||
(p) => p.teamId === userTeam.id
|
||||
);
|
||||
return calculateDraftEligibility(
|
||||
userTeam.id,
|
||||
|
|
@ -666,7 +662,7 @@ export default function DraftRoom() {
|
|||
const forcePickEligibility = useMemo(() => {
|
||||
if (!selectedPickSlot) return null;
|
||||
const selectedTeamPicks = transformedPicks.filter(
|
||||
(p: any) => p.teamId === selectedPickSlot.teamId
|
||||
(p) => p.teamId === selectedPickSlot.teamId
|
||||
);
|
||||
return calculateDraftEligibility(
|
||||
selectedPickSlot.teamId,
|
||||
|
|
@ -683,10 +679,10 @@ export default function DraftRoom() {
|
|||
const replacePickEligibility = useMemo(() => {
|
||||
if (!replacePickSlot) return null;
|
||||
const allPicksExcluding = transformedPicks.filter(
|
||||
(p: any) => p.participant.id !== replacePickSlot.oldParticipantId
|
||||
(p) => p.participant.id !== replacePickSlot.oldParticipantId
|
||||
);
|
||||
const selectedTeamPicks = allPicksExcluding.filter(
|
||||
(p: any) => p.teamId === replacePickSlot.teamId
|
||||
(p) => p.teamId === replacePickSlot.teamId
|
||||
);
|
||||
return calculateDraftEligibility(
|
||||
replacePickSlot.teamId,
|
||||
|
|
@ -701,7 +697,8 @@ export default function DraftRoom() {
|
|||
|
||||
// Listen for new picks from other users
|
||||
useEffect(() => {
|
||||
const handlePickMade = (data: any) => {
|
||||
type PickMadePayload = { pick: (typeof draftPicks)[number] & { team?: { id?: string; name?: string }; participant?: { name?: string } }; nextPickNumber: number; isDraftComplete?: boolean };
|
||||
const handlePickMade = (data: PickMadePayload) => {
|
||||
if (isRevalidatingRef.current) {
|
||||
// Buffer this pick; the revalidation-complete handler will merge it
|
||||
// into the DB snapshot so it isn't silently dropped.
|
||||
|
|
@ -710,7 +707,7 @@ export default function DraftRoom() {
|
|||
// pick counter and the picks list in sync with each other.
|
||||
pendingPicksDuringRevalidationRef.current.push(data.pick);
|
||||
} else {
|
||||
setPicks((prev: any) => [...prev, data.pick]);
|
||||
setPicks((prev) => [...prev, data.pick]);
|
||||
setCurrentPick(data.nextPickNumber);
|
||||
}
|
||||
|
||||
|
|
@ -739,7 +736,7 @@ export default function DraftRoom() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleTimerUpdate = (data: any) => {
|
||||
const handleTimerUpdate = (data: { teamId: string; timeRemaining: number; currentPickNumber: number | null }) => {
|
||||
// Bail out without creating a new object reference if nothing changed.
|
||||
// Without this guard the spread `{ ...prev }` always returns a new object,
|
||||
// causing a full re-render of the DraftRoom tree on every 1-second tick.
|
||||
|
|
@ -753,7 +750,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);
|
||||
}
|
||||
};
|
||||
|
|
@ -823,35 +820,35 @@ export default function DraftRoom() {
|
|||
};
|
||||
|
||||
const handleParticipantRemovedFromQueues = (data: { participantId: string }) => {
|
||||
setQueue((prev: any) =>
|
||||
prev.filter((item: any) => item.participantId !== data.participantId)
|
||||
setQueue((prev) =>
|
||||
prev.filter((item) => item.participantId !== data.participantId)
|
||||
);
|
||||
};
|
||||
|
||||
const handleQueueEligibilityPruned = (data: { teamId: string; removedParticipantIds: string[] }) => {
|
||||
if (data.teamId !== userTeam?.id) return;
|
||||
const removed = new Set(data.removedParticipantIds);
|
||||
setQueue((prev: any) => prev.filter((item: any) => !removed.has(item.participantId)));
|
||||
setQueue((prev) => prev.filter((item) => !removed.has(item.participantId)));
|
||||
};
|
||||
|
||||
// Fired when another window/session changes the user's queue (add/remove/reorder/clear).
|
||||
// Only received by this team's own sockets (emitted to team-${teamId} private room).
|
||||
// Ignored while this window has in-flight mutations to prevent the server echo
|
||||
// from clobbering a more-recent optimistic update.
|
||||
const handleQueueUpdated = (data: { queue: any[] }) => {
|
||||
const handleQueueUpdated = (data: { queue: QueueItem[] }) => {
|
||||
if (pendingQueueMutationsRef.current > 0) return;
|
||||
setQueue(data.queue);
|
||||
};
|
||||
|
||||
const handlePickReplaced = (data: any) => {
|
||||
setPicks((prev: any) =>
|
||||
prev.map((p: any) => (p.pickNumber === data.pickNumber ? data.pick : p))
|
||||
const handlePickReplaced = (data: { pickNumber: number; pick: (typeof draftPicks)[number] }) => {
|
||||
setPicks((prev) =>
|
||||
prev.map((p) => (p.pickNumber === data.pickNumber ? data.pick : p))
|
||||
);
|
||||
};
|
||||
|
||||
const handleDraftRolledBack = (data: any) => {
|
||||
setPicks((prev: any) =>
|
||||
prev.filter((p: any) => p.pickNumber < data.pickNumber)
|
||||
const handleDraftRolledBack = (data: { pickNumber: number }) => {
|
||||
setPicks((prev) =>
|
||||
prev.filter((p) => p.pickNumber < data.pickNumber)
|
||||
);
|
||||
setCurrentPick(data.pickNumber);
|
||||
setIsDraftComplete(false);
|
||||
|
|
@ -861,7 +858,7 @@ export default function DraftRoom() {
|
|||
// Full state sync sent by the server on join-draft. Provides an immediate,
|
||||
// socket-based path to bring the client up to date — critical for mobile
|
||||
// reconnection where the HTTP revalidation may be delayed or fail entirely.
|
||||
const handleDraftStateSync = (data: any) => {
|
||||
const handleDraftStateSync = (data: { picks: (typeof draftPicks)[number][]; currentPickNumber: number; isPaused: boolean; status: string; timers?: Array<{ teamId: string; timeRemaining: number }>; queue?: QueueItem[] }) => {
|
||||
// If a revalidation is in-flight, let it complete and handle the merge.
|
||||
// The revalidation path already buffers picks and deduplicates — layering
|
||||
// this on top would risk conflicts. But if NOT revalidating, apply the
|
||||
|
|
@ -876,7 +873,7 @@ export default function DraftRoom() {
|
|||
if (data.timers) {
|
||||
setTeamTimers((prev) => {
|
||||
const updated = { ...prev };
|
||||
data.timers.forEach((t: any) => {
|
||||
data.timers?.forEach((t) => {
|
||||
updated[t.teamId] = t.timeRemaining;
|
||||
});
|
||||
return updated;
|
||||
|
|
@ -989,7 +986,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");
|
||||
|
|
@ -1218,7 +1215,7 @@ export default function DraftRoom() {
|
|||
};
|
||||
|
||||
const handleReplacePickOpen = useCallback((pickNumber: number, teamId: string) => {
|
||||
const pick = picks.find((p: any) => p.pickNumber === pickNumber);
|
||||
const pick = picks.find((p) => p.pickNumber === pickNumber);
|
||||
if (!pick) {
|
||||
toast.error("Pick not found — try refreshing the page");
|
||||
return;
|
||||
|
|
@ -1391,7 +1388,7 @@ export default function DraftRoom() {
|
|||
round: number;
|
||||
pickInRound: number;
|
||||
teamId: string;
|
||||
pick?: any;
|
||||
pick?: (typeof draftPicks)[number];
|
||||
}>
|
||||
> = [];
|
||||
|
||||
|
|
@ -1405,7 +1402,7 @@ export default function DraftRoom() {
|
|||
const pickNumber = (round - 1) * teamCount + pickInRound;
|
||||
const teamId = draftSlots[teamIndex]?.team.id;
|
||||
|
||||
const pick = picks.find((p: any) => p.pickNumber === pickNumber);
|
||||
const pick = picks.find((p) => p.pickNumber === pickNumber);
|
||||
|
||||
roundPicks.push({ pickNumber, round, pickInRound, teamId, pick });
|
||||
}
|
||||
|
|
@ -1417,7 +1414,7 @@ export default function DraftRoom() {
|
|||
|
||||
// Get drafted participant IDs for filtering
|
||||
const draftedParticipantIds = useMemo(
|
||||
() => new Set(picks.map((p: any) => p.participant.id)),
|
||||
() => new Set(picks.map((p) => p.participant.id)),
|
||||
[picks]
|
||||
);
|
||||
|
||||
|
|
@ -1426,8 +1423,8 @@ export default function DraftRoom() {
|
|||
if (!userTeam) return new Set<string>();
|
||||
return new Set(
|
||||
picks
|
||||
.filter((p: any) => p.team.id === userTeam.id)
|
||||
.map((p: any) => p.sport.id)
|
||||
.filter((p) => p.team.id === userTeam.id)
|
||||
.map((p) => p.sport.id)
|
||||
);
|
||||
}, [picks, userTeam]);
|
||||
|
||||
|
|
@ -1436,8 +1433,8 @@ export default function DraftRoom() {
|
|||
if (!userTeam) return new Set<string>();
|
||||
return new Set(
|
||||
picks
|
||||
.filter((p: any) => p.team.id === userTeam.id)
|
||||
.map((p: any) => p.sport.name)
|
||||
.filter((p) => p.team.id === userTeam.id)
|
||||
.map((p) => p.sport.name)
|
||||
);
|
||||
}, [picks, userTeam]);
|
||||
|
||||
|
|
@ -1445,7 +1442,7 @@ export default function DraftRoom() {
|
|||
const uniqueSports = useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
new Set(availableParticipants.map((p: any) => p.sport.name))
|
||||
new Set(availableParticipants.map((p) => p.sport.name))
|
||||
).sort(),
|
||||
[availableParticipants]
|
||||
);
|
||||
|
|
@ -1453,7 +1450,7 @@ export default function DraftRoom() {
|
|||
// Filter participants based on search, sport, drafted status, and eligibility
|
||||
const filteredParticipants = useMemo(() => {
|
||||
const sportFilterSet = new Set(sportFilters);
|
||||
return availableParticipants.filter((participant: any) => {
|
||||
return availableParticipants.filter((participant) => {
|
||||
// Drafted filter - hide drafted participants by default
|
||||
if (hideDrafted && draftedParticipantIds.has(participant.id)) {
|
||||
return false;
|
||||
|
|
@ -1621,9 +1618,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>
|
||||
)}
|
||||
|
|
@ -1962,7 +1959,7 @@ export default function DraftRoom() {
|
|||
<DialogTitle>Roll Back Draft</DialogTitle>
|
||||
<DialogDescription>
|
||||
{rollbackPickNumber !== null && (() => {
|
||||
const count = picks.filter((p: any) => p.pickNumber >= rollbackPickNumber).length;
|
||||
const count = picks.filter((p) => p.pickNumber >= rollbackPickNumber).length;
|
||||
return `This will erase ${count} pick${count === 1 ? "" : "s"} (pick #${rollbackPickNumber} through the most recent) and return the draft to pick #${rollbackPickNumber}. This cannot be undone.`;
|
||||
})()}
|
||||
</DialogDescription>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
countCommissionersByLeagueId,
|
||||
removeCommissionerByLeagueAndUser,
|
||||
} from "~/models/commissioner";
|
||||
import { findCurrentSeasonWithSports, updateSeason } from "~/models/season";
|
||||
import { findCurrentSeasonWithSports, updateSeason, type NewSeason } from "~/models/season";
|
||||
import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team";
|
||||
import { findAllUsers, findUserByClerkId, findUsersByClerkIds, getUserDisplayName, isUserAdminByClerkId } from "~/models/user";
|
||||
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
||||
|
|
@ -281,7 +281,7 @@ export async function action(args: Route.ActionArgs) {
|
|||
|
||||
const selectedSports = formData.getAll("sportsSeasons");
|
||||
const currentSportIds = new Set(
|
||||
season.seasonSports?.map((s: any) => s.sportsSeason.id) || []
|
||||
season.seasonSports?.map((s) => s.sportsSeason.id) || []
|
||||
);
|
||||
const newSportIds = new Set(selectedSports as string[]);
|
||||
|
||||
|
|
@ -465,7 +465,7 @@ export async function action(args: Route.ActionArgs) {
|
|||
// Now handle season-specific updates.
|
||||
try {
|
||||
// Update season settings
|
||||
const seasonUpdates: any = {};
|
||||
const seasonUpdates: Partial<NewSeason> = {};
|
||||
|
||||
// Handle draft rounds
|
||||
if (typeof draftRounds === "string") {
|
||||
|
|
@ -514,7 +514,7 @@ export async function action(args: Route.ActionArgs) {
|
|||
if (points < 0 || points > 1000) {
|
||||
return { error: `${field} must be between 0 and 1000 points` };
|
||||
}
|
||||
seasonUpdates[field] = points;
|
||||
(seasonUpdates as Record<string, unknown>)[field] = points;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -652,7 +652,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
|||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [timerMode, setTimerMode] = useState<"chess_clock" | "standard">(season?.draftTimerMode ?? "chess_clock");
|
||||
const [selectedSports, setSelectedSports] = useState<Set<string>>(
|
||||
new Set(season?.seasonSports?.map((s: any) => s.sportsSeason.id) || [])
|
||||
new Set(season?.seasonSports?.map((s) => s.sportsSeason.id) || [])
|
||||
);
|
||||
const [draftOrderTeams, setDraftOrderTeams] = useState<string[]>(
|
||||
draftSlots.length > 0
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { getRegularSeasonStandings } from "~/models/regular-season-standings";
|
||||
import { getAllParticipantEVsForSeason } from "~/models/participant-expected-value";
|
||||
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
|
||||
import type { PlayoffMatch } from "~/models/playoff-match";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
|
|
@ -136,13 +137,20 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
|
||||
type SeasonStanding = { id: string; position: number; championshipPoints: string; participant: { id: string; name: string } };
|
||||
|
||||
let playoffMatches: any[] = [];
|
||||
type PlayoffMatchWithRelations = PlayoffMatch & {
|
||||
participant1: { id: string; name: string } | null;
|
||||
participant2: { id: string; name: string } | null;
|
||||
winner: { id: string; name: string } | null;
|
||||
loser: { id: string; name: string } | null;
|
||||
};
|
||||
let playoffMatches: PlayoffMatchWithRelations[] = [];
|
||||
let playoffRounds: string[] = [];
|
||||
let preEliminatedParticipants: { id: string; name: string }[] = [];
|
||||
let participantPoints: { participantId: string; points: number }[] = [];
|
||||
let partialScoreParticipantIds: string[] = [];
|
||||
let seasonStandings: SeasonStanding[] = [];
|
||||
let qpStandings: any[] = [];
|
||||
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number];
|
||||
let qpStandings: QPStanding[] = [];
|
||||
|
||||
if (scoringPattern === "playoff_bracket") {
|
||||
// Fetch playoff matches via scoring events
|
||||
|
|
@ -164,7 +172,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
loser: true,
|
||||
},
|
||||
});
|
||||
playoffMatches = matches;
|
||||
playoffMatches = matches as PlayoffMatchWithRelations[];
|
||||
|
||||
templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId ?? undefined;
|
||||
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
||||
|
|
@ -186,8 +194,8 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
]);
|
||||
|
||||
preEliminatedParticipants = eliminatedResults
|
||||
.filter((r) => r.participant)
|
||||
.map((r) => ({ id: (r as any).participant.id, name: (r as any).participant.name }));
|
||||
.filter((r): r is typeof r & { participant: NonNullable<typeof r.participant> } => r.participant !== null && r.participant !== undefined)
|
||||
.map((r) => ({ id: r.participant.id, name: r.participant.name }));
|
||||
|
||||
// Compute per-participant fantasy points directly from participant_results.
|
||||
// This covers both finalized placements and progressive floor scores for still-alive participants.
|
||||
|
|
|
|||
|
|
@ -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) => s.otLosses !== null);
|
||||
|
||||
const simulatorType = sportsSeason.sport?.simulatorType;
|
||||
const standingsDisplayMode =
|
||||
|
|
@ -80,7 +80,7 @@ export default function SportSeasonDetail({
|
|||
|
||||
// Build ownership map for RegularSeasonStandings
|
||||
const ownershipMap = Object.fromEntries(
|
||||
teamOwnerships.map((o: any) => [
|
||||
teamOwnerships.map((o) => [
|
||||
o.participantId,
|
||||
{ teamName: o.teamName, ownerName: o.ownerName ?? "", teamId: o.teamId },
|
||||
])
|
||||
|
|
@ -113,12 +113,12 @@ export default function SportSeasonDetail({
|
|||
{hasStandings && !hasBracket && (
|
||||
<div className="mb-6">
|
||||
<RegularSeasonStandings
|
||||
standings={regularSeasonStandings as any}
|
||||
standings={regularSeasonStandings}
|
||||
teamOwnerships={ownershipMap}
|
||||
userParticipantIds={userParticipantIds}
|
||||
showOtLosses={showOtLosses}
|
||||
participantEvs={participantEvs}
|
||||
displayMode={standingsDisplayMode as any}
|
||||
displayMode={standingsDisplayMode as "flat" | "nhl-divisions"}
|
||||
playoffSpots={playoffSpots}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -129,25 +129,25 @@ export default function SportSeasonDetail({
|
|||
(upcomingEvents.length > 0 || recentEvents.length > 0) && (
|
||||
<div className="mb-6">
|
||||
<EventSchedule
|
||||
upcomingEvents={upcomingEvents as any}
|
||||
recentEvents={recentEvents as any}
|
||||
upcomingEvents={upcomingEvents}
|
||||
recentEvents={recentEvents}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hide bracket display when standings exist but no bracket matches have been set yet */}
|
||||
{!(scoringPattern === "playoff_bracket" && hasStandings && !hasBracket) && <SportSeasonDisplay
|
||||
scoringPattern={scoringPattern as any}
|
||||
scoringPattern={scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points"}
|
||||
sportSeasonName={sportsSeason.name}
|
||||
sportName={sportsSeason.sport.name}
|
||||
playoffMatches={playoffMatches as any}
|
||||
playoffMatches={playoffMatches}
|
||||
playoffRounds={playoffRounds}
|
||||
preEliminatedParticipants={preEliminatedParticipants}
|
||||
participantPoints={participantPoints}
|
||||
partialScoreParticipantIds={partialScoreParticipantIds}
|
||||
seasonStandings={seasonStandings as any}
|
||||
seasonStandings={seasonStandings}
|
||||
seasonIsFinalized={seasonIsFinalized}
|
||||
qpStandings={qpStandings as any}
|
||||
qpStandings={qpStandings}
|
||||
qpIsFinalized={sportsSeason.qualifyingPointsFinalized || false}
|
||||
totalMajors={sportsSeason.totalMajors}
|
||||
majorsCompleted={sportsSeason.majorsCompleted || 0}
|
||||
|
|
@ -166,12 +166,12 @@ export default function SportSeasonDetail({
|
|||
{hasStandings && hasBracket && (
|
||||
<div className="mt-8">
|
||||
<RegularSeasonStandings
|
||||
standings={regularSeasonStandings as any}
|
||||
standings={regularSeasonStandings}
|
||||
teamOwnerships={ownershipMap}
|
||||
userParticipantIds={userParticipantIds}
|
||||
showOtLosses={showOtLosses}
|
||||
participantEvs={participantEvs}
|
||||
displayMode={standingsDisplayMode as any}
|
||||
displayMode={standingsDisplayMode as "flat" | "nhl-divisions"}
|
||||
playoffSpots={playoffSpots}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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.' };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export async function loader() {
|
|||
return {
|
||||
...template,
|
||||
sportsSeasonIds: templateWithSports?.seasonTemplateSports?.map(
|
||||
(ts: any) => ts.sportsSeason.id
|
||||
(ts: { sportsSeason: { id: string } }) => ts.sportsSeason.id
|
||||
) || []
|
||||
};
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default function TestSocket() {
|
|||
|
||||
useEffect(() => {
|
||||
// Listen for test messages
|
||||
const handleTestMessage = (data: any) => {
|
||||
const handleTestMessage = (data: unknown) => {
|
||||
console.log("Received test message:", data);
|
||||
setMessages((prev) => [...prev, JSON.stringify(data)]);
|
||||
};
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ describe("probability-updater", () => {
|
|||
notes: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
participant: null,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
@ -108,6 +109,7 @@ describe("probability-updater", () => {
|
|||
notes: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
participant: null,
|
||||
},
|
||||
{
|
||||
id: "result-2",
|
||||
|
|
@ -119,6 +121,7 @@ describe("probability-updater", () => {
|
|||
notes: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
participant: null,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
@ -195,6 +198,7 @@ describe("probability-updater", () => {
|
|||
notes: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
participant: null,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
@ -219,6 +223,7 @@ describe("probability-updater", () => {
|
|||
notes: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
participant: null,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 !== undefined && ev.sourceOdds !== null && ev.sourceOdds !== undefined ? 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 !== undefined && ev.sourceOdds !== null && ev.sourceOdds !== undefined ? 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 !== undefined && playoffSeedStat.value !== null && playoffSeedStat.value !== undefined
|
||||
? Math.round(playoffSeedStat.value)
|
||||
: playoffSeedStat?.displayValue
|
||||
? parseInt(playoffSeedStat.displayValue, 10) || undefined
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ process.env.NODE_ENV = 'test';
|
|||
// Mock Clerk
|
||||
vi.mock('@clerk/react-router/server', () => ({
|
||||
getAuth: vi.fn(() => Promise.resolve({ userId: 'test-user-id' })),
|
||||
clerkMiddleware: vi.fn(() => (req: any, res: any, next: any) => next()),
|
||||
clerkMiddleware: vi.fn(() => (req: unknown, res: unknown, next: () => void) => next()),
|
||||
rootAuthLoader: vi.fn(() => Promise.resolve({ userId: 'test-user-id' })),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
// The real implementation uses Node.js AsyncLocalStorage which is server-only.
|
||||
// This stub is used during the client build to avoid bundling server-only code.
|
||||
|
||||
export const DatabaseContext = null as any;
|
||||
export const DatabaseContext = null as unknown;
|
||||
|
||||
export function database(): never {
|
||||
throw new Error("database() can only be called on the server");
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { createRequestHandler } from "@react-router/express";
|
|||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import express from "express";
|
||||
import postgres from "postgres";
|
||||
import type { ServerBuild } from "react-router";
|
||||
import { RouterContextProvider } from "react-router";
|
||||
|
||||
import { DatabaseContext } from "~/database/context";
|
||||
|
|
@ -18,11 +19,12 @@ app.use((_, __, next) => DatabaseContext.run(db, next));
|
|||
|
||||
app.use(
|
||||
createRequestHandler({
|
||||
build: () => import("virtual:react-router/server-build") as any,
|
||||
build: () => import("virtual:react-router/server-build") as unknown as Promise<ServerBuild>,
|
||||
// @ts-ignore -- RouterContextProvider is the correct runtime type but tsconfig.server.json can't resolve the conditional type statically
|
||||
getLoadContext() {
|
||||
const provider = new RouterContextProvider();
|
||||
provider.set(expressValueContext, "Hello from Express");
|
||||
return provider as any; // Type assertion needed - RouterContextProvider is the context
|
||||
return provider as unknown as RouterContextProvider;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -20,12 +21,12 @@ function getSocketDb(): PostgresJsDatabase<typeof schema> {
|
|||
// Socket event types
|
||||
interface ServerToClientEvents {
|
||||
"test-message": (data: {
|
||||
originalMessage: any;
|
||||
originalMessage: unknown;
|
||||
serverResponse: string;
|
||||
serverTimestamp: string;
|
||||
socketId: string;
|
||||
}) => void;
|
||||
"pick-made": (data: any) => void;
|
||||
"pick-made": (data: unknown) => void;
|
||||
"draft-started": (data: { seasonId: string; currentPickNumber: number }) => void;
|
||||
"draft-completed": () => void;
|
||||
"timer-update": (data: {
|
||||
|
|
@ -58,9 +59,9 @@ interface ServerToClientEvents {
|
|||
round: number;
|
||||
pickInRound: number;
|
||||
timeUsed: number;
|
||||
team: any;
|
||||
participant: any;
|
||||
sport: any;
|
||||
team: unknown;
|
||||
participant: unknown;
|
||||
sport: unknown;
|
||||
}>;
|
||||
timers: Array<{
|
||||
teamId: string;
|
||||
|
|
@ -79,7 +80,7 @@ interface ServerToClientEvents {
|
|||
interface ClientToServerEvents {
|
||||
"join-draft": (seasonId: string, teamId?: string) => void;
|
||||
"leave-draft": (seasonId: string) => void;
|
||||
"test-event": (data: any) => void;
|
||||
"test-event": (data: unknown) => void;
|
||||
}
|
||||
|
||||
// Global type augmentation
|
||||
|
|
@ -273,7 +274,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
|||
}
|
||||
});
|
||||
|
||||
socket.on("test-event", (data: any) => {
|
||||
socket.on("test-event", (data: unknown) => {
|
||||
console.log("📨 Received test-event from client:", socket.id, data);
|
||||
|
||||
socket.emit("test-message", {
|
||||
|
|
|
|||
2
server/types.d.ts
vendored
2
server/types.d.ts
vendored
|
|
@ -4,4 +4,4 @@ declare global {
|
|||
var __socketIO: SocketIOServer | undefined;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
"composite": true,
|
||||
"strict": true,
|
||||
"types": ["node"],
|
||||
"lib": ["ES2022"],
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"target": "ES2023",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
"noEmit": false,
|
||||
"outDir": "./dist/server",
|
||||
"rootDir": ".",
|
||||
"target": "ES2022",
|
||||
"target": "ES2023",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"lib": ["ES2023"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
||||
"types": ["vite/client"],
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue