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,
|
ContextMenuTrigger,
|
||||||
} from "~/components/ui/context-menu";
|
} 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 {
|
interface DraftGridProps {
|
||||||
draftSlots: Array<{
|
draftSlots: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -15,7 +22,7 @@ interface DraftGridProps {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
draftGrid: any[][];
|
draftGrid: Array<Array<DraftCell | null>>;
|
||||||
currentPick: number;
|
currentPick: number;
|
||||||
teamTimers?: Record<string, number>;
|
teamTimers?: Record<string, number>;
|
||||||
formatTime?: (seconds: number | undefined) => string;
|
formatTime?: (seconds: number | undefined) => string;
|
||||||
|
|
@ -92,11 +99,11 @@ export function DraftGrid({
|
||||||
const round = roundIndex + 1;
|
const round = roundIndex + 1;
|
||||||
const isEvenRound = round % 2 === 0;
|
const isEvenRound = round % 2 === 0;
|
||||||
const displayPicks = isEvenRound
|
const displayPicks = isEvenRound
|
||||||
? [...roundPicks].reverse()
|
? [...roundPicks].toReversed()
|
||||||
: roundPicks;
|
: roundPicks;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={roundIndex} className="flex gap-2">
|
<div key={round} className="flex gap-2">
|
||||||
{displayPicks.map((cell, index) => {
|
{displayPicks.map((cell, index) => {
|
||||||
const actualIndex = isEvenRound
|
const actualIndex = isEvenRound
|
||||||
? roundPicks.length - 1 - index
|
? roundPicks.length - 1 - index
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import { mockDraftSlots } from '~/test/fixtures/team';
|
||||||
describe('DraftGrid Component', () => {
|
describe('DraftGrid Component', () => {
|
||||||
const mockGrid = [
|
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,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
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', () => {
|
it('should reverse picks on even rounds', () => {
|
||||||
const snakeGrid = [
|
const snakeGrid = [
|
||||||
[
|
[
|
||||||
{ participant: { name: 'Pick 1' }, sport: { name: 'NFL' } },
|
{ pickNumber: 1, participant: { id: 'p1', name: 'Pick 1' }, sport: { id: 's1', name: 'NFL' }, team: { id: 't1', name: 'Team 1' } },
|
||||||
{ participant: { name: 'Pick 2' }, sport: { name: 'NBA' } },
|
{ 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' } },
|
{ pickNumber: 3, participant: { id: 'p3', name: 'Pick 4' }, sport: { id: 's1', name: 'MLB' }, team: { id: 't2', name: 'Team 2' } },
|
||||||
{ participant: { name: 'Pick 3' }, sport: { name: 'NHL' } },
|
{ 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>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative w-16 h-16">
|
<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/30 rounded-full" />
|
||||||
<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 border-t-transparent rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -85,15 +85,15 @@ export function ConnectionOverlay({
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 bg-primary rounded-full animate-bounce"
|
className="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||||
style={{ animationDelay: "0ms" }}
|
style={{ animationDelay: "0ms" }}
|
||||||
></div>
|
/>
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 bg-primary rounded-full animate-bounce"
|
className="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||||
style={{ animationDelay: "150ms" }}
|
style={{ animationDelay: "150ms" }}
|
||||||
></div>
|
/>
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 bg-primary rounded-full animate-bounce"
|
className="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||||
style={{ animationDelay: "300ms" }}
|
style={{ animationDelay: "300ms" }}
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -168,11 +168,11 @@ export const DraftGridSection = memo(function DraftGridSection({
|
||||||
const round = roundIndex + 1;
|
const round = roundIndex + 1;
|
||||||
const isEvenRound = round % 2 === 0;
|
const isEvenRound = round % 2 === 0;
|
||||||
const displayPicks = isEvenRound
|
const displayPicks = isEvenRound
|
||||||
? [...roundPicks].reverse()
|
? [...roundPicks].toReversed()
|
||||||
: roundPicks;
|
: roundPicks;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={roundIndex} className="flex gap-2 items-stretch">
|
<div key={round} className="flex gap-2 items-stretch">
|
||||||
{/* Round label */}
|
{/* Round label */}
|
||||||
<div className="w-8 flex-shrink-0 sticky left-0 z-[5] bg-background/95 flex items-center justify-center">
|
<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>
|
<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">
|
<div className="space-y-2 overflow-y-auto">
|
||||||
{picks
|
{picks
|
||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.toReversed()
|
||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map((pick) => (
|
.map((pick) => (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const SidebarRecentPicks = memo(function SidebarRecentPicks({ picks }: Si
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{picks
|
{picks
|
||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.toReversed()
|
||||||
.map((pick) => (
|
.map((pick) => (
|
||||||
<div
|
<div
|
||||||
key={pick.id}
|
key={pick.id}
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ export function PlayoffBracket({
|
||||||
rounds,
|
rounds,
|
||||||
preEliminatedParticipants = [],
|
preEliminatedParticipants = [],
|
||||||
participantPoints = [],
|
participantPoints = [],
|
||||||
partialScoreParticipantIds = [],
|
partialScoreParticipantIds: _partialScoreParticipantIds = [],
|
||||||
teamOwnerships = [],
|
teamOwnerships = [],
|
||||||
userParticipantIds = [],
|
userParticipantIds = [],
|
||||||
showOwnership = true,
|
showOwnership = true,
|
||||||
|
|
@ -189,7 +189,7 @@ export function PlayoffBracket({
|
||||||
// In double-chance brackets (e.g. AFL), a participant may lose one round but
|
// 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.
|
// 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 }>>();
|
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;
|
let bracketWinner: Participant | null = null;
|
||||||
|
|
||||||
const lastRound = rounds[rounds.length - 1];
|
const lastRound = rounds[rounds.length - 1];
|
||||||
|
|
@ -209,7 +209,7 @@ export function PlayoffBracket({
|
||||||
match.loserId === match.participant1Id
|
match.loserId === match.participant1Id
|
||||||
? match.participant1Score
|
? match.participant1Score
|
||||||
: match.participant2Score;
|
: match.participant2Score;
|
||||||
if (loserScore) hasScore = true;
|
if (loserScore) _hasScore = true;
|
||||||
if (!losersByRound.has(match.round)) losersByRound.set(match.round, []);
|
if (!losersByRound.has(match.round)) losersByRound.set(match.round, []);
|
||||||
losersByRound.get(match.round)!.push({
|
losersByRound.get(match.round)!.push({
|
||||||
participant: match.loser,
|
participant: match.loser,
|
||||||
|
|
@ -574,12 +574,12 @@ export function PlayoffBracket({
|
||||||
</TableRow>
|
</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 isOwned = userParticipantSet.has(entry.participant.id);
|
||||||
const pts = pointsMap.get(entry.participant.id);
|
const pts = pointsMap.get(entry.participant.id);
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<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"}
|
className={isOwned ? "bg-electric/5 border-l-2 border-l-electric opacity-80" : "opacity-60"}
|
||||||
>
|
>
|
||||||
<TableCell className="font-mono text-xs text-muted-foreground">
|
<TableCell className="font-mono text-xs text-muted-foreground">
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export function SeasonStandings({
|
||||||
showOwnership = true,
|
showOwnership = true,
|
||||||
isFinalized = false,
|
isFinalized = false,
|
||||||
title = "Championship Standings",
|
title = "Championship Standings",
|
||||||
description,
|
description: _description,
|
||||||
}: SeasonStandingsProps) {
|
}: SeasonStandingsProps) {
|
||||||
const userParticipantSet = new Set(userParticipantIds);
|
const userParticipantSet = new Set(userParticipantIds);
|
||||||
// Create ownership map for fast lookup
|
// Create ownership map for fast lookup
|
||||||
|
|
@ -191,7 +191,7 @@ export function SeasonStandings({
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{standings
|
{standings
|
||||||
.sort((a, b) => a.position - b.position)
|
.toSorted((a, b) => a.position - b.position)
|
||||||
.map((standing) => {
|
.map((standing) => {
|
||||||
const ownership = getOwnership(standing.participant.id);
|
const ownership = getOwnership(standing.participant.id);
|
||||||
const isTied = checkIfTied(standing);
|
const isTied = checkIfTied(standing);
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ interface SportSeasonDisplayProps {
|
||||||
export function SportSeasonDisplay({
|
export function SportSeasonDisplay({
|
||||||
scoringPattern,
|
scoringPattern,
|
||||||
sportSeasonName,
|
sportSeasonName,
|
||||||
sportName,
|
sportName: _sportName,
|
||||||
playoffMatches = [],
|
playoffMatches = [],
|
||||||
playoffRounds = [],
|
playoffRounds = [],
|
||||||
preEliminatedParticipants = [],
|
preEliminatedParticipants = [],
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ export function EventSchedule({ upcomingEvents, recentEvents }: EventSchedulePro
|
||||||
!event.isComplete && (() => {
|
!event.isComplete && (() => {
|
||||||
const isPast = event.eventStartsAt
|
const isPast = event.eventStartsAt
|
||||||
? new Date(event.eventStartsAt) < new Date()
|
? 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 && (
|
return isPast && (
|
||||||
<Badge variant="outline" className="text-xs shrink-0 border-amber-500/30 text-amber-400">
|
<Badge variant="outline" className="text-xs shrink-0 border-amber-500/30 text-amber-400">
|
||||||
Results Pending
|
Results Pending
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ interface StandingRow {
|
||||||
lastTen: string | null;
|
lastTen: string | null;
|
||||||
homeRecord: string | null;
|
homeRecord: string | null;
|
||||||
awayRecord: string | null;
|
awayRecord: string | null;
|
||||||
syncedAt: string | null;
|
syncedAt: Date | string | null;
|
||||||
participant: { id: string; name: string; shortName?: string | null };
|
participant: { id: string; name: string; shortName?: string | null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ interface TableSection {
|
||||||
// ─── Formatting ───────────────────────────────────────────────────────────────
|
// ─── Formatting ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function formatWinPct(winPct: string | null, wins: number, gamesPlayed: number): string {
|
function formatWinPct(winPct: string | null, wins: number, gamesPlayed: number): string {
|
||||||
if (winPct != null) {
|
if (winPct !== null) {
|
||||||
const pct = parseFloat(winPct);
|
const pct = parseFloat(winPct);
|
||||||
if (!isNaN(pct)) return pct.toFixed(3);
|
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 {
|
function formatGB(gamesBack: string | null): string {
|
||||||
if (gamesBack == null) return "—";
|
if (gamesBack === null) return "—";
|
||||||
const gb = parseFloat(gamesBack);
|
const gb = parseFloat(gamesBack);
|
||||||
if (isNaN(gb) || gb === 0) return "—";
|
if (isNaN(gb) || gb === 0) return "—";
|
||||||
return gb % 1 === 0 ? gb.toString() : gb.toFixed(1);
|
return gb % 1 === 0 ? gb.toString() : gb.toFixed(1);
|
||||||
|
|
@ -96,7 +96,7 @@ function buildFlatSections(
|
||||||
conference: "",
|
conference: "",
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
rows: [...standings].sort(
|
rows: [...standings].toSorted(
|
||||||
(a, b) => (a.leagueRank ?? 99) - (b.leagueRank ?? 99)
|
(a, b) => (a.leagueRank ?? 99) - (b.leagueRank ?? 99)
|
||||||
),
|
),
|
||||||
rankMode: "conference",
|
rankMode: "conference",
|
||||||
|
|
@ -118,7 +118,7 @@ function buildFlatSections(
|
||||||
conference,
|
conference,
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
rows: [...rows].sort(
|
rows: [...rows].toSorted(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.conferenceRank ?? a.leagueRank ?? 99) -
|
(a.conferenceRank ?? a.leagueRank ?? 99) -
|
||||||
(b.conferenceRank ?? b.leagueRank ?? 99)
|
(b.conferenceRank ?? b.leagueRank ?? 99)
|
||||||
|
|
@ -154,9 +154,9 @@ function buildNhlSections(
|
||||||
name,
|
name,
|
||||||
qualifiers: divRows
|
qualifiers: divRows
|
||||||
.filter((r) => (r.divisionRank ?? 99) <= 3)
|
.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, b) =>
|
||||||
(a.qualifiers[0]?.conferenceRank ?? 99) -
|
(a.qualifiers[0]?.conferenceRank ?? 99) -
|
||||||
(b.qualifiers[0]?.conferenceRank ?? 99)
|
(b.qualifiers[0]?.conferenceRank ?? 99)
|
||||||
|
|
@ -164,7 +164,7 @@ function buildNhlSections(
|
||||||
|
|
||||||
const wildCard = rows
|
const wildCard = rows
|
||||||
.filter((r) => (r.divisionRank ?? 99) > 3)
|
.filter((r) => (r.divisionRank ?? 99) > 3)
|
||||||
.sort(
|
.toSorted(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.conferenceRank ?? a.leagueRank ?? 99) -
|
(a.conferenceRank ?? a.leagueRank ?? 99) -
|
||||||
(b.conferenceRank ?? b.leagueRank ?? 99)
|
(b.conferenceRank ?? b.leagueRank ?? 99)
|
||||||
|
|
@ -243,7 +243,7 @@ function StandingsTable({
|
||||||
|
|
||||||
if (section.heading) {
|
if (section.heading) {
|
||||||
sectionRows.push(
|
sectionRows.push(
|
||||||
<tr key={`h-${sIdx}`}>
|
<tr key={`h-${section.heading}`}>
|
||||||
<td
|
<td
|
||||||
colSpan={totalCols}
|
colSpan={totalCols}
|
||||||
className={`text-xs font-medium text-muted-foreground/60 uppercase tracking-wider pb-1 ${
|
className={`text-xs font-medium text-muted-foreground/60 uppercase tracking-wider pb-1 ${
|
||||||
|
|
@ -350,7 +350,7 @@ function StandingsTable({
|
||||||
</td>
|
</td>
|
||||||
{hasEvs && (
|
{hasEvs && (
|
||||||
<td className="py-2 pl-2 text-right tabular-nums">
|
<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">
|
<span className="text-blue-500 dark:text-blue-400 font-medium">
|
||||||
{parseFloat(ev).toFixed(1)}
|
{parseFloat(ev).toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -388,7 +388,7 @@ export function RegularSeasonStandings({
|
||||||
const lastSyncedAt = standings
|
const lastSyncedAt = standings
|
||||||
.map((s) => s.syncedAt)
|
.map((s) => s.syncedAt)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.sort()
|
.toSorted()
|
||||||
.at(-1);
|
.at(-1);
|
||||||
|
|
||||||
const groups =
|
const groups =
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export function TeamScoreBreakdown({
|
||||||
// Flatten all picks sorted by sport name then pick number
|
// Flatten all picks sorted by sport name then pick number
|
||||||
const allPicks = breakdown.picks
|
const allPicks = breakdown.picks
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => {
|
.toSorted((a, b) => {
|
||||||
const sportCmp = a.participant.sport.localeCompare(b.participant.sport);
|
const sportCmp = a.participant.sport.localeCompare(b.participant.sport);
|
||||||
return sportCmp !== 0 ? sportCmp : a.pickNumber - b.pickNumber;
|
return sportCmp !== 0 ? sportCmp : a.pickNumber - b.pickNumber;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,15 @@ async function handleRequest(
|
||||||
routerContext: EntryContext,
|
routerContext: EntryContext,
|
||||||
// If you have middleware enabled:
|
// If you have middleware enabled:
|
||||||
// loadContext: RouterContextProvider
|
// loadContext: RouterContextProvider
|
||||||
loadContext: AppLoadContext
|
_loadContext: AppLoadContext
|
||||||
) {
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let shellRendered = false;
|
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
|
// 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
|
// 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
|
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
|
||||||
? "onAllReady"
|
? "onAllReady"
|
||||||
: "onShellReady";
|
: "onShellReady";
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ interface UseDraftSocketReturn {
|
||||||
/** Increments each time a new socket instance is created. Include in
|
/** Increments each time a new socket instance is created. Include in
|
||||||
* useEffect deps so handler effects re-register on socket recreation. */
|
* useEffect deps so handler effects re-register on socket recreation. */
|
||||||
socketVersion: number;
|
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;
|
on: (event: string, callback: (...args: any[]) => void) => void;
|
||||||
|
// eslint-disable-next-line typescript/no-explicit-any
|
||||||
off: (event: string, callback?: (...args: any[]) => void) => void;
|
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 {
|
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
|
// has run (socketRef.current === null) are silently no-ops. Consumers should
|
||||||
// only call them inside their own useEffect, not during render or synchronously
|
// only call them inside their own useEffect, not during render or synchronously
|
||||||
// after mount.
|
// 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) => {
|
const on = useCallback((event: string, callback: (...args: any[]) => void) => {
|
||||||
socketRef.current?.on(event, callback);
|
socketRef.current?.on(event, callback);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// eslint-disable-next-line typescript/no-explicit-any
|
||||||
const off = useCallback((event: string, callback?: (...args: any[]) => void) => {
|
const off = useCallback((event: string, callback?: (...args: any[]) => void) => {
|
||||||
socketRef.current?.off(event, callback);
|
socketRef.current?.off(event, callback);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const emit = useCallback((event: string, ...args: any[]) => {
|
const emit = useCallback((event: string, ...args: unknown[]) => {
|
||||||
socketRef.current?.emit(event, ...args);
|
socketRef.current?.emit(event, ...args);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -599,7 +599,7 @@ describe("calculateDraftEligibility", () => {
|
||||||
|
|
||||||
// Team1 picks NFL
|
// Team1 picks NFL
|
||||||
allPicks = [createPick("team1", "nfl1", "nfl", "NFL")];
|
allPicks = [createPick("team1", "nfl1", "nfl", "NFL")];
|
||||||
let team2Eligibility = calculateDraftEligibility(
|
const team2Eligibility = calculateDraftEligibility(
|
||||||
"team2",
|
"team2",
|
||||||
[],
|
[],
|
||||||
allPicks,
|
allPicks,
|
||||||
|
|
@ -619,7 +619,7 @@ describe("calculateDraftEligibility", () => {
|
||||||
createPick("team1", "nfl1", "nfl", "NFL"),
|
createPick("team1", "nfl1", "nfl", "NFL"),
|
||||||
createPick("team2", "nfl2", "nfl", "NFL"),
|
createPick("team2", "nfl2", "nfl", "NFL"),
|
||||||
];
|
];
|
||||||
let team3Eligibility = calculateDraftEligibility(
|
const team3Eligibility = calculateDraftEligibility(
|
||||||
"team3",
|
"team3",
|
||||||
[],
|
[],
|
||||||
allPicks,
|
allPicks,
|
||||||
|
|
@ -642,7 +642,7 @@ describe("calculateDraftEligibility", () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
// Now team1 wants a second NFL (flex pick)
|
// Now team1 wants a second NFL (flex pick)
|
||||||
let team1Eligibility = calculateDraftEligibility(
|
const team1Eligibility = calculateDraftEligibility(
|
||||||
"team1",
|
"team1",
|
||||||
[createPick("team1", "nfl1", "nfl", "NFL")],
|
[createPick("team1", "nfl1", "nfl", "NFL")],
|
||||||
allPicks,
|
allPicks,
|
||||||
|
|
|
||||||
|
|
@ -603,7 +603,7 @@ export function getOrderedRoundsFromMatches(
|
||||||
for (const m of matches) {
|
for (const m of matches) {
|
||||||
roundMatchCounts.set(m.round, (roundMatchCounts.get(m.round) || 0) + 1);
|
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)
|
(a, b) => (roundMatchCounts.get(b) || 0) - (roundMatchCounts.get(a) || 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export async function buildOwnerMap(
|
||||||
const userByClerkId = new Map(
|
const userByClerkId = new Map(
|
||||||
userRows
|
userRows
|
||||||
.map((u) => [u.clerkId, getUserDisplayName(u)] as const)
|
.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> = {};
|
const ownerMap: Record<string, string> = {};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a season is complete
|
* Check if a season is complete
|
||||||
|
|
|
||||||
|
|
@ -182,18 +182,6 @@ describe("AFL Finals System - Phase 3.3", () => {
|
||||||
|
|
||||||
describe("AFL Point Distribution", () => {
|
describe("AFL Point Distribution", () => {
|
||||||
it("has all 8 scoring placements covered", () => {
|
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
|
// Count scoring rounds
|
||||||
const scoringRounds = AFL_10.rounds.filter(r => r.isScoring);
|
const scoringRounds = AFL_10.rounds.filter(r => r.isScoring);
|
||||||
expect(scoringRounds).toHaveLength(4); // EF, SF, PF, GF
|
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 { SIMPLE_16, SIMPLE_32, NCAA_68 } from "~/lib/bracket-templates";
|
||||||
import { calculateFantasyPoints, calculateAveragedPoints, type ScoringRules } from "../scoring-rules";
|
import { calculateFantasyPoints, calculateAveragedPoints, type ScoringRules } from "../scoring-rules";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ describe("Bracket Seeding", () => {
|
||||||
|
|
||||||
it("uses all 16 seeds exactly once", () => {
|
it("uses all 16 seeds exactly once", () => {
|
||||||
const pairs = generateStandardSeeding(16);
|
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));
|
expect(allSeeds).toEqual(Array.from({ length: 16 }, (_, i) => i + 1));
|
||||||
});
|
});
|
||||||
|
|
@ -114,7 +114,7 @@ describe("Bracket Seeding", () => {
|
||||||
const seed1 = firstMatch[0] + 1;
|
const seed1 = firstMatch[0] + 1;
|
||||||
const seed2 = firstMatch[1] + 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", () => {
|
it("has proper matchup sums for balance", () => {
|
||||||
|
|
@ -167,12 +167,12 @@ describe("Bracket Seeding", () => {
|
||||||
const seed1 = firstMatch[0] + 1;
|
const seed1 = firstMatch[0] + 1;
|
||||||
const seed2 = firstMatch[1] + 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", () => {
|
it("uses all 32 seeds exactly once", () => {
|
||||||
const pairs = generateStandardSeeding(32);
|
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));
|
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 {
|
import {
|
||||||
SIMPLE_4,
|
SIMPLE_4,
|
||||||
SIMPLE_8,
|
SIMPLE_8,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calcul
|
||||||
*/
|
*/
|
||||||
|
|
||||||
describe("participant-expected-value model", () => {
|
describe("participant-expected-value model", () => {
|
||||||
const defaultScoring: ScoringRules = {
|
const _defaultScoring: ScoringRules = {
|
||||||
pointsFor1st: 100,
|
pointsFor1st: 100,
|
||||||
pointsFor2nd: 70,
|
pointsFor2nd: 70,
|
||||||
pointsFor3rd: 50,
|
pointsFor3rd: 50,
|
||||||
|
|
@ -23,7 +23,7 @@ describe("participant-expected-value model", () => {
|
||||||
pointsFor8th: 15,
|
pointsFor8th: 15,
|
||||||
};
|
};
|
||||||
|
|
||||||
const validProbabilities: ProbabilityDistribution = {
|
const _validProbabilities: ProbabilityDistribution = {
|
||||||
probFirst: 20,
|
probFirst: 20,
|
||||||
probSecond: 20,
|
probSecond: 20,
|
||||||
probThird: 15,
|
probThird: 15,
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ describe("processMatchResult", () => {
|
||||||
it("does not overwrite a finalized row with a partial score", async () => {
|
it("does not overwrite a finalized row with a partial score", async () => {
|
||||||
// findFirst returns a finalized row for the first participant (loser)
|
// findFirst returns a finalized row for the first participant (loser)
|
||||||
const existing = { id: "result-1", isPartialScore: false };
|
const existing = { id: "result-1", isPartialScore: false };
|
||||||
const { db, insertedRows, updatedRows } = makeDb(existing);
|
const { db, updatedRows } = makeDb(existing);
|
||||||
|
|
||||||
await processMatchResult(
|
await processMatchResult(
|
||||||
{ ...BASE, round: "Quarterfinals", isScoring: true },
|
{ ...BASE, round: "Quarterfinals", isScoring: true },
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ describe("Qualifying Points Configuration", () => {
|
||||||
{ name: "Player C", qp: 38 },
|
{ 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[0].name).toBe("Player B"); // 50 QP
|
||||||
expect(sorted[1].name).toBe("Player A"); // 44 QP
|
expect(sorted[1].name).toBe("Player A"); // 44 QP
|
||||||
|
|
@ -107,7 +107,7 @@ describe("Qualifying Points Configuration", () => {
|
||||||
{ name: "Player C", qp: 20 },
|
{ 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
|
// Both players with 30 QP should be before Player C
|
||||||
expect(sorted[2].name).toBe("Player C");
|
expect(sorted[2].name).toBe("Player C");
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
// ── DB mock ──────────────────────────────────────────────────────────────────
|
// ── DB mock ──────────────────────────────────────────────────────────────────
|
||||||
// selectDistinct returns a fluent chain; findMany is used in step 2.
|
// selectDistinct returns a fluent chain; findMany is used in step 2.
|
||||||
const mockFindMany = vi.fn();
|
const mockFindMany = vi.fn();
|
||||||
const mockSelectChainBase = {
|
|
||||||
from: vi.fn(),
|
|
||||||
leftJoin: vi.fn(),
|
|
||||||
where: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
function makeSelectChain(rows: { id: string }[]) {
|
function makeSelectChain(rows: { id: string }[]) {
|
||||||
const chain = {
|
const chain = {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ describe("Season Standings (F1 Pattern)", () => {
|
||||||
|
|
||||||
it("should correctly score season with tied positions", () => {
|
it("should correctly score season with tied positions", () => {
|
||||||
// Simulate standings where 3rd, 4th, and 5th are tied
|
// Simulate standings where 3rd, 4th, and 5th are tied
|
||||||
const standings = [
|
const _standings = [
|
||||||
{ position: 1, expected: 100 },
|
{ position: 1, expected: 100 },
|
||||||
{ position: 2, expected: 70 },
|
{ position: 2, expected: 70 },
|
||||||
// Three tied for 3rd place - each gets placement 3
|
// Three tied for 3rd place - each gets placement 3
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ describe("Standings Snapshots", () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
// Should be sorted by date descending (newest first)
|
// 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)
|
b.date.localeCompare(a.date)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ function assignRanks(
|
||||||
}>
|
}>
|
||||||
): Map<string, number> {
|
): Map<string, number> {
|
||||||
// Sort teams by ranking criteria
|
// Sort teams by ranking criteria
|
||||||
const sorted = [...teams].sort(compareTeamsForRanking);
|
const sorted = [...teams].toSorted(compareTeamsForRanking);
|
||||||
|
|
||||||
const ranks = new Map<string, number>();
|
const ranks = new Map<string, number>();
|
||||||
let currentRank = 1;
|
let currentRank = 1;
|
||||||
|
|
|
||||||
|
|
@ -345,7 +345,7 @@ export function getTeamForPick(
|
||||||
pickNumber: number,
|
pickNumber: number,
|
||||||
draftOrder: { teamId: string; draftOrder: number }[]
|
draftOrder: { teamId: string; draftOrder: number }[]
|
||||||
): string | null {
|
): 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;
|
const teamCount = sortedOrder.length;
|
||||||
|
|
||||||
if (teamCount === 0) return null;
|
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 { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ export async function findLeaguesWithActiveSeasonsByUserId(
|
||||||
for (const league of [...leaguesWithTeam, ...leaguesAsCommissioner]) {
|
for (const league of [...leaguesWithTeam, ...leaguesAsCommissioner]) {
|
||||||
leagueMap.set(league.id, league);
|
leagueMap.set(league.id, league);
|
||||||
}
|
}
|
||||||
return [...leagueMap.values()].sort(
|
return [...leagueMap.values()].toSorted(
|
||||||
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
|
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
|
|
||||||
export type ParticipantResult = typeof schema.participantResults.$inferSelect;
|
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 type NewParticipantResult = typeof schema.participantResults.$inferInsert;
|
||||||
|
|
||||||
export async function createParticipantResult(
|
export async function createParticipantResult(
|
||||||
|
|
@ -62,7 +65,7 @@ export async function findParticipantResultByParticipantId(
|
||||||
|
|
||||||
export async function findParticipantResultsBySportsSeasonId(
|
export async function findParticipantResultsBySportsSeasonId(
|
||||||
sportsSeasonId: string
|
sportsSeasonId: string
|
||||||
): Promise<ParticipantResult[]> {
|
): Promise<ParticipantResultWithParticipant[]> {
|
||||||
const db = database();
|
const db = database();
|
||||||
return await db.query.participantResults.findMany({
|
return await db.query.participantResults.findMany({
|
||||||
where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId),
|
where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { eq, and, inArray, desc } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
|
|
||||||
export interface UpsertSeasonResultData {
|
export interface UpsertSeasonResultData {
|
||||||
participantId: string;
|
participantId: string;
|
||||||
|
|
@ -135,7 +135,7 @@ export async function getSeasonResults(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort by position (nulls last), then by points descending
|
// 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) {
|
if (a.currentPosition !== null && b.currentPosition !== null) {
|
||||||
return a.currentPosition - b.currentPosition;
|
return a.currentPosition - b.currentPosition;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,6 @@ export async function generateSingleEliminationBracket(
|
||||||
eventId: string,
|
eventId: string,
|
||||||
participantIds: string[]
|
participantIds: string[]
|
||||||
): Promise<PlayoffMatch[]> {
|
): Promise<PlayoffMatch[]> {
|
||||||
const db = database();
|
|
||||||
const numParticipants = participantIds.length;
|
const numParticipants = participantIds.length;
|
||||||
|
|
||||||
// Validate that we have 4 or 8 participants for standard bracket
|
// Validate that we have 4 or 8 participants for standard bracket
|
||||||
|
|
@ -218,8 +217,6 @@ export async function advanceWinner(
|
||||||
matchId: string,
|
matchId: string,
|
||||||
winnerId: string
|
winnerId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = database();
|
|
||||||
|
|
||||||
// Get the match
|
// Get the match
|
||||||
const match = await findPlayoffMatchById(matchId);
|
const match = await findPlayoffMatchById(matchId);
|
||||||
if (!match) throw new Error("Match not found");
|
if (!match) throw new Error("Match not found");
|
||||||
|
|
@ -523,17 +520,6 @@ async function generateNFL14Bracket(
|
||||||
): Promise<PlayoffMatch[]> {
|
): Promise<PlayoffMatch[]> {
|
||||||
const matches: NewPlayoffMatch[] = [];
|
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:
|
// Actually for 14 teams (seeds 1-14), Wild Card should be:
|
||||||
// Match 1: #7 (index 6) vs #10 (index 9)
|
// Match 1: #7 (index 6) vs #10 (index 9)
|
||||||
// Match 2: #6 (index 5) vs #11 (index 10)
|
// 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 4: #8 (index 7) vs #9 (index 8)
|
||||||
// Match 5: #4 (index 3) vs #13 (index 12)
|
// Match 5: #4 (index 3) vs #13 (index 12)
|
||||||
// Match 6: #3 (index 2) vs #14 (index 13)
|
// Match 6: #3 (index 2) vs #14 (index 13)
|
||||||
const correctedWildCardSeeding = [
|
const wildCardSeeding = [
|
||||||
[6, 9], // #7 vs #10
|
[6, 9], // #7 vs #10
|
||||||
[5, 10], // #6 vs #11
|
[5, 10], // #6 vs #11
|
||||||
[4, 11], // #5 vs #12
|
[4, 11], // #5 vs #12
|
||||||
|
|
@ -550,8 +536,8 @@ async function generateNFL14Bracket(
|
||||||
[2, 13], // #3 vs #14
|
[2, 13], // #3 vs #14
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < correctedWildCardSeeding.length; i++) {
|
for (let i = 0; i < wildCardSeeding.length; i++) {
|
||||||
const [seed1, seed2] = correctedWildCardSeeding[i];
|
const [seed1, seed2] = wildCardSeeding[i];
|
||||||
matches.push({
|
matches.push({
|
||||||
scoringEventId: eventId,
|
scoringEventId: eventId,
|
||||||
round: "Wild Card",
|
round: "Wild Card",
|
||||||
|
|
@ -572,7 +558,7 @@ async function generateNFL14Bracket(
|
||||||
if (divisionalRound) {
|
if (divisionalRound) {
|
||||||
for (let i = 0; i < divisionalRound.matchCount; i++) {
|
for (let i = 0; i < divisionalRound.matchCount; i++) {
|
||||||
let participant1Id: string | null = null;
|
let participant1Id: string | null = null;
|
||||||
let participant2Id: string | null = null;
|
const participant2Id: string | null = null;
|
||||||
let seedInfo: string | null = null;
|
let seedInfo: string | null = null;
|
||||||
|
|
||||||
// Assign bye teams to first two Divisional matches
|
// Assign bye teams to first two Divisional matches
|
||||||
|
|
@ -768,7 +754,6 @@ async function advanceAFLWinner(
|
||||||
winnerId: string,
|
winnerId: string,
|
||||||
loserId: string
|
loserId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = database();
|
|
||||||
const eventId = match.scoringEventId;
|
const eventId = match.scoringEventId;
|
||||||
|
|
||||||
// Wildcard Round: Winner advances to Elimination Finals
|
// Wildcard Round: Winner advances to Elimination Finals
|
||||||
|
|
@ -871,8 +856,6 @@ export async function advanceWinnerTemplate(
|
||||||
winnerId: string,
|
winnerId: string,
|
||||||
template: BracketTemplate
|
template: BracketTemplate
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = database();
|
|
||||||
|
|
||||||
// Get the match
|
// Get the match
|
||||||
const match = await findPlayoffMatchById(matchId);
|
const match = await findPlayoffMatchById(matchId);
|
||||||
if (!match) throw new Error("Match not found");
|
if (!match) throw new Error("Match not found");
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@ export async function updateFinalRankings(
|
||||||
const updates = [];
|
const updates = [];
|
||||||
|
|
||||||
// Sort by points descending
|
// 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]);
|
return parseFloat(b[0]) - parseFloat(a[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,9 @@ export async function upsertRegularSeasonStandings(
|
||||||
losses: r.losses,
|
losses: r.losses,
|
||||||
otLosses: r.otLosses ?? null,
|
otLosses: r.otLosses ?? null,
|
||||||
ties: r.ties ?? 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,
|
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,
|
conference: r.conference ?? null,
|
||||||
division: r.division ?? null,
|
division: r.division ?? null,
|
||||||
conferenceRank: r.conferenceRank ?? null,
|
conferenceRank: r.conferenceRank ?? null,
|
||||||
|
|
@ -133,7 +133,7 @@ export async function getRegularSeasonStandings(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort: conference → division → divisionRank ?? leagueRank ?? leagueRank, nulls last
|
// Sort: conference → division → divisionRank ?? leagueRank ?? leagueRank, nulls last
|
||||||
return rows.sort((a, b) => {
|
return rows.toSorted((a, b) => {
|
||||||
const confA = a.conference ?? "ZZZ";
|
const confA = a.conference ?? "ZZZ";
|
||||||
const confB = b.conference ?? "ZZZ";
|
const confB = b.conference ?? "ZZZ";
|
||||||
if (confA !== confB) return confA.localeCompare(confB);
|
if (confA !== confB) return confA.localeCompare(confB);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
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 { getSeasonResults } from "./participant-season-result";
|
||||||
import { updateProbabilitiesAfterResult } from "~/services/probability-updater";
|
import { updateProbabilitiesAfterResult } from "~/services/probability-updater";
|
||||||
import { sendStandingsUpdateNotification, type ScoredMatch } from "~/services/discord";
|
import { sendStandingsUpdateNotification, type ScoredMatch } from "~/services/discord";
|
||||||
|
|
@ -597,7 +597,7 @@ export async function finalizeQualifyingPoints(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort groups by QP (descending)
|
// 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]);
|
return parseFloat(b[0]) - parseFloat(a[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -722,7 +722,7 @@ export async function processSeasonStandings(
|
||||||
// Get sorted positions
|
// Get sorted positions
|
||||||
const positions = Object.keys(positionGroups)
|
const positions = Object.keys(positionGroups)
|
||||||
.map(Number)
|
.map(Number)
|
||||||
.sort((a, b) => a - b);
|
.toSorted((a, b) => a - b);
|
||||||
|
|
||||||
// Assign fantasy placements (1-8) based on standings
|
// Assign fantasy placements (1-8) based on standings
|
||||||
let currentPlacement = 1;
|
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.
|
// If duplicates exist due to data corruption, the first row wins — acceptable trade-off.
|
||||||
const result = pick.participant.results[0];
|
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";
|
const isBracket = pick.participant.sportsSeason?.scoringPattern === "playoff_bracket";
|
||||||
let points: number;
|
let points: number;
|
||||||
if (isBracket) {
|
if (isBracket) {
|
||||||
|
|
@ -1100,7 +1100,7 @@ function assignRanks(
|
||||||
}>
|
}>
|
||||||
): Map<string, number> {
|
): Map<string, number> {
|
||||||
// Sort teams by ranking criteria
|
// Sort teams by ranking criteria
|
||||||
const sorted = [...teams].sort(compareTeamsForRanking);
|
const sorted = [...teams].toSorted(compareTeamsForRanking);
|
||||||
|
|
||||||
const ranks = new Map<string, number>();
|
const ranks = new Map<string, number>();
|
||||||
let currentRank = 1;
|
let currentRank = 1;
|
||||||
|
|
@ -1330,7 +1330,7 @@ export async function recalculateAffectedLeagues(
|
||||||
const usernameByClerkId = new Map(
|
const usernameByClerkId = new Map(
|
||||||
teamOwnerUsers
|
teamOwnerUsers
|
||||||
.map((u) => [u.clerkId, getUserDisplayName(u)])
|
.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
|
// Build teamId → ownerId map from standings data
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ export async function updateScoringEvent(
|
||||||
) {
|
) {
|
||||||
const db = providedDb || database();
|
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.name !== undefined) updateData.name = data.name;
|
||||||
if (data.eventDate !== undefined) {
|
if (data.eventDate !== undefined) {
|
||||||
|
|
@ -450,6 +450,7 @@ export async function getUpcomingEventsForDraftedParticipants(
|
||||||
inArray(schema.playoffMatches.participant1Id, draftedIds),
|
inArray(schema.playoffMatches.participant1Id, draftedIds),
|
||||||
// participant2Id is nullable UUID — same underlying type as participant1Id;
|
// participant2Id is nullable UUID — same underlying type as participant1Id;
|
||||||
// the notNull difference only exists at the TypeScript layer
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
inArray(schema.playoffMatches.participant2Id as any, draftedIds)
|
inArray(schema.playoffMatches.participant2Id as any, draftedIds)
|
||||||
),
|
),
|
||||||
|
|
@ -503,7 +504,7 @@ export async function getUpcomingEventsForDraftedParticipants(
|
||||||
pMap.set(row.participant2Id, draftedMap.get(row.participant2Id)!);
|
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}`);
|
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
|
// Build timestamp bounds covering the full span of the requested dates so we
|
||||||
// can range-check the playoffMatchGames.scheduledAt timestamp column.
|
// 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 dateFromTimestamp = new Date(sortedDates[0] + "T00:00:00.000Z");
|
||||||
const dateToTimestamp = new Date(sortedDates[sortedDates.length - 1] + "T23:59:59.999Z");
|
const dateToTimestamp = new Date(sortedDates[sortedDates.length - 1] + "T23:59:59.999Z");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
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 type { TeamStanding, TeamStandingSnapshot, TeamStandingWithChange } from "~/types/standings";
|
||||||
import { calculateBracketPoints, calculateFantasyPoints } from "~/models/scoring-rules";
|
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.)
|
// 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) => ({
|
return sorted.map((standing) => ({
|
||||||
teamId: standing.teamId,
|
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
|
// Calculate points using bracket-averaged scoring for bracket sports
|
||||||
if (isBracket) {
|
if (isBracket) {
|
||||||
const templateId = await getBracketTemplate(pick.participant.sportsSeasonId);
|
const templateId = await getBracketTemplate(pick.participant.sportsSeasonId);
|
||||||
|
|
@ -402,7 +402,7 @@ export async function getSeasonPointProgression(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Organize data by date
|
// 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) {
|
for (const snapshot of snapshots) {
|
||||||
const date = snapshot.snapshotDate;
|
const date = snapshot.snapshotDate;
|
||||||
|
|
@ -411,12 +411,12 @@ export async function getSeasonPointProgression(
|
||||||
dateMap.set(date, { date });
|
dateMap.set(date, { date });
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateData = dateMap.get(date);
|
const dateData = dateMap.get(date)!;
|
||||||
dateData[snapshot.team.name] = parseFloat(snapshot.totalPoints);
|
dateData[snapshot.team.name] = parseFloat(snapshot.totalPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to array and sort by date
|
// 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()
|
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 { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket";
|
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket";
|
||||||
import { redirect } from "react-router";
|
|
||||||
import { findSportsSeasonById } from "~/models/sports-season";
|
import { findSportsSeasonById } from "~/models/sports-season";
|
||||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||||||
import { getScoringEventById, updateScoringEvent } from "~/models/scoring-event";
|
import { getScoringEventById, updateScoringEvent } from "~/models/scoring-event";
|
||||||
|
|
@ -42,7 +42,7 @@ import {
|
||||||
} from "~/models/tournament-group";
|
} from "~/models/tournament-group";
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export async function loader({ params }: Route.LoaderArgs) {
|
export async function loader({ params }: Route.LoaderArgs) {
|
||||||
const sportsSeason = await findSportsSeasonById(params.id);
|
const sportsSeason = await findSportsSeasonById(params.id);
|
||||||
|
|
@ -79,6 +79,8 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||||
participant2: { id: string; name: string } | null;
|
participant2: { id: string; name: string } | null;
|
||||||
winner: { id: string; name: string } | null;
|
winner: { id: string; name: string } | null;
|
||||||
loser: { 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,
|
tournamentGroups,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export default function EventBracket({
|
||||||
const template = templates.find((t) => t.id === selectedTemplate);
|
const template = templates.find((t) => t.id === selectedTemplate);
|
||||||
|
|
||||||
// Determine if this is a group-stage event
|
// 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)
|
// Determine if this template uses named regions (e.g. NCAA 68)
|
||||||
const hasRegions = (template?.regions?.length ?? 0) > 0;
|
const hasRegions = (template?.regions?.length ?? 0) > 0;
|
||||||
|
|
@ -151,8 +151,8 @@ export default function EventBracket({
|
||||||
const knockoutPopulated = useMemo(() => {
|
const knockoutPopulated = useMemo(() => {
|
||||||
if (!isGroupStageEvent) return false;
|
if (!isGroupStageEvent) return false;
|
||||||
// Check if any Round of 32 match has participants assigned
|
// Check if any Round of 32 match has participants assigned
|
||||||
const r32Matches = matches.filter((m: any) => m.round === "Round of 32");
|
const r32Matches = matches.filter((m) => m.round === "Round of 32");
|
||||||
return r32Matches.some((m: any) => m.participant1Id || m.participant2Id);
|
return r32Matches.some((m) => m.participant1Id || m.participant2Id);
|
||||||
}, [isGroupStageEvent, matches]);
|
}, [isGroupStageEvent, matches]);
|
||||||
|
|
||||||
// Group stage stats
|
// Group stage stats
|
||||||
|
|
@ -161,7 +161,7 @@ export default function EventBracket({
|
||||||
let eliminated = 0;
|
let eliminated = 0;
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (const group of tournamentGroups) {
|
for (const group of tournamentGroups) {
|
||||||
for (const member of (group as any).members || []) {
|
for (const member of group.members || []) {
|
||||||
total++;
|
total++;
|
||||||
if (member.eliminated) eliminated++;
|
if (member.eliminated) eliminated++;
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +174,7 @@ export default function EventBracket({
|
||||||
if (!isGroupStageEvent) return [];
|
if (!isGroupStageEvent) return [];
|
||||||
const advancing: Array<{ id: string; name: string }> = [];
|
const advancing: Array<{ id: string; name: string }> = [];
|
||||||
for (const group of tournamentGroups) {
|
for (const group of tournamentGroups) {
|
||||||
for (const member of (group as any).members || []) {
|
for (const member of group.members || []) {
|
||||||
if (!member.eliminated && member.participant) {
|
if (!member.eliminated && member.participant) {
|
||||||
advancing.push({
|
advancing.push({
|
||||||
id: member.participant.id,
|
id: member.participant.id,
|
||||||
|
|
@ -258,7 +258,7 @@ export default function EventBracket({
|
||||||
const getPendingWinnersInRound = (round: string) => {
|
const getPendingWinnersInRound = (round: string) => {
|
||||||
return Object.entries(selectedWinners)
|
return Object.entries(selectedWinners)
|
||||||
.filter(([matchId]) => {
|
.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;
|
return match && match.round === round && !match.isComplete;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -283,7 +283,7 @@ export default function EventBracket({
|
||||||
|
|
||||||
// Check if all matches are complete
|
// Check if all matches are complete
|
||||||
const allMatchesComplete = useMemo(() => {
|
const allMatchesComplete = useMemo(() => {
|
||||||
return matches.length > 0 && matches.every((m: any) => m.isComplete);
|
return matches.length > 0 && matches.every((m) => m.isComplete);
|
||||||
}, [matches]);
|
}, [matches]);
|
||||||
|
|
||||||
// No bracket and no groups yet = setup phase
|
// No bracket and no groups yet = setup phase
|
||||||
|
|
@ -455,6 +455,7 @@ export default function EventBracket({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{regionConfig.map((region, r) => (
|
{regionConfig.map((region, r) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<div key={r} className="flex flex-wrap items-center gap-2">
|
<div key={r} className="flex flex-wrap items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
className="w-36 h-8 text-sm"
|
className="w-36 h-8 text-sm"
|
||||||
|
|
@ -463,6 +464,7 @@ export default function EventBracket({
|
||||||
placeholder={`Region ${r + 1}`}
|
placeholder={`Region ${r + 1}`}
|
||||||
/>
|
/>
|
||||||
{region.playInSeeds.map((seed, pi) => (
|
{region.playInSeeds.map((seed, pi) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<div key={pi} className="flex items-center gap-1">
|
<div key={pi} className="flex items-center gap-1">
|
||||||
<span className="text-xs text-muted-foreground">play-in seed:</span>
|
<span className="text-xs text-muted-foreground">play-in seed:</span>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -505,6 +507,7 @@ export default function EventBracket({
|
||||||
))}
|
))}
|
||||||
{/* Hidden fields so server receives the region config */}
|
{/* Hidden fields so server receives the region config */}
|
||||||
{regionConfig.map((region, r) => (
|
{regionConfig.map((region, r) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<Fragment key={r}>
|
<Fragment key={r}>
|
||||||
<input type="hidden" name={`regionName${r}`} value={region.name} />
|
<input type="hidden" name={`regionName${r}`} value={region.name} />
|
||||||
<input type="hidden" name={`regionPlayInSeeds${r}`} value={region.playInSeeds.join(",")} />
|
<input type="hidden" name={`regionPlayInSeeds${r}`} value={region.playInSeeds.join(",")} />
|
||||||
|
|
@ -520,6 +523,7 @@ export default function EventBracket({
|
||||||
{effectiveRegions.map((region, r) => {
|
{effectiveRegions.map((region, r) => {
|
||||||
const directOffset = regionSlotMap.directOffsets[r];
|
const directOffset = regionSlotMap.directOffsets[r];
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<div key={r} className="border rounded-lg p-4 space-y-2">
|
<div key={r} className="border rounded-lg p-4 space-y-2">
|
||||||
<h4 className="font-semibold text-sm">{region.name || `Region ${r + 1}`}</h4>
|
<h4 className="font-semibold text-sm">{region.name || `Region ${r + 1}`}</h4>
|
||||||
{region.directSeeds.map((seedNum, pos) => {
|
{region.directSeeds.map((seedNum, pos) => {
|
||||||
|
|
@ -573,7 +577,7 @@ export default function EventBracket({
|
||||||
const idx1 = pi.startIndex;
|
const idx1 = pi.startIndex;
|
||||||
const idx2 = pi.startIndex + 1;
|
const idx2 = pi.startIndex + 1;
|
||||||
return (
|
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">
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
Play-In {ffIdx + 1} — {regionName} {pi.seedSlot}-seed
|
Play-In {ffIdx + 1} — {regionName} {pi.seedSlot}-seed
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -617,6 +621,7 @@ export default function EventBracket({
|
||||||
const availableParticipants = getAvailableParticipants(i);
|
const availableParticipants = getAvailableParticipants(i);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<div key={i} className="flex items-center gap-2">
|
<div key={i} className="flex items-center gap-2">
|
||||||
<Label className="w-20 text-sm text-muted-foreground shrink-0">
|
<Label className="w-20 text-sm text-muted-foreground shrink-0">
|
||||||
Seed {i + 1}
|
Seed {i + 1}
|
||||||
|
|
@ -680,13 +685,13 @@ export default function EventBracket({
|
||||||
|
|
||||||
{/* Group Cards */}
|
{/* Group Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
<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}>
|
<Card key={group.id}>
|
||||||
<CardHeader className="py-3 px-4">
|
<CardHeader className="py-3 px-4">
|
||||||
<CardTitle className="text-base">Group {group.groupName}</CardTitle>
|
<CardTitle className="text-base">Group {group.groupName}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-4 pb-4 space-y-2">
|
<CardContent className="px-4 pb-4 space-y-2">
|
||||||
{(group.members || []).map((member: any) => (
|
{(group.members || []).map((member) => (
|
||||||
<div
|
<div
|
||||||
key={member.id}
|
key={member.id}
|
||||||
className={`flex items-center justify-between gap-2 p-2 rounded-md border ${
|
className={`flex items-center justify-between gap-2 p-2 rounded-md border ${
|
||||||
|
|
@ -829,7 +834,7 @@ export default function EventBracket({
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-8"></TableHead>
|
<TableHead className="w-8" />
|
||||||
<TableHead className="w-16">Match</TableHead>
|
<TableHead className="w-16">Match</TableHead>
|
||||||
<TableHead>Participant 1</TableHead>
|
<TableHead>Participant 1</TableHead>
|
||||||
<TableHead>Score</TableHead>
|
<TableHead>Score</TableHead>
|
||||||
|
|
@ -924,9 +929,9 @@ export default function EventBracket({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Existing games */}
|
{/* Existing games */}
|
||||||
{(match as any).games?.length > 0 ? (
|
{match.games?.length > 0 ? (
|
||||||
<div className="space-y-2 mb-3">
|
<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">
|
<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="font-medium w-12 shrink-0">Game {game.gameNumber}</span>
|
||||||
<span className="text-muted-foreground flex-1">
|
<span className="text-muted-foreground flex-1">
|
||||||
|
|
@ -982,7 +987,7 @@ export default function EventBracket({
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={9}
|
max={9}
|
||||||
defaultValue={((match as any).games?.length ?? 0) + 1}
|
defaultValue={(match.games?.length ?? 0) + 1}
|
||||||
className="h-8 w-20"
|
className="h-8 w-20"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -1009,9 +1014,9 @@ export default function EventBracket({
|
||||||
<span className="text-sm font-semibold">Moneyline Odds</span>
|
<span className="text-sm font-semibold">Moneyline Odds</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(match as any).odds?.length > 0 ? (
|
{match.odds?.length > 0 ? (
|
||||||
<div className="space-y-2 mb-3">
|
<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">
|
<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="flex-1 font-medium">{odd.participant?.name}</span>
|
||||||
<span className={`font-mono ${odd.moneylineOdds > 0 ? "text-emerald-500" : "text-muted-foreground"}`}>
|
<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) {
|
if (event.eventStartsAt) {
|
||||||
setDisplayValue(format(new Date(event.eventStartsAt), "yyyy-MM-dd'T'HH:mm"));
|
setDisplayValue(format(new Date(event.eventStartsAt), "yyyy-MM-dd'T'HH:mm"));
|
||||||
}
|
}
|
||||||
}, []);
|
}, [event.eventStartsAt]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -107,7 +107,7 @@ export default function EventResults({
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort results by placement
|
// 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
|
// Create a map of season results for easy lookup
|
||||||
const seasonResultsMap = seasonResults
|
const seasonResultsMap = seasonResults
|
||||||
|
|
@ -120,7 +120,7 @@ export default function EventResults({
|
||||||
if (event.eventType === "schedule_event") {
|
if (event.eventType === "schedule_event") {
|
||||||
const isPast = event.eventStartsAt
|
const isPast = event.eventStartsAt
|
||||||
? new Date(event.eventStartsAt) < new Date()
|
? 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 (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
|
|
@ -695,12 +695,12 @@ export default function EventResults({
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{[...participantResults]
|
{[...participantResults]
|
||||||
.sort((a: any, b: any) => {
|
.toSorted((a: { finalPosition?: number | null }, b: { finalPosition?: number | null }) => {
|
||||||
const posA = a.finalPosition ?? 999;
|
const posA = a.finalPosition ?? 999;
|
||||||
const posB = b.finalPosition ?? 999;
|
const posB = b.finalPosition ?? 999;
|
||||||
return posA - posB;
|
return posA - posB;
|
||||||
})
|
})
|
||||||
.map((result: any) => (
|
.map((result) => (
|
||||||
<TableRow key={result.id}>
|
<TableRow key={result.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -730,7 +730,7 @@ export default function EventResults({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{result.participant.name}</TableCell>
|
<TableCell>{result.participant?.name}</TableCell>
|
||||||
<TableCell className="text-right font-semibold">
|
<TableCell className="text-right font-semibold">
|
||||||
{result.qualifyingPoints ? (
|
{result.qualifyingPoints ? (
|
||||||
<span className={result.qualifyingPoints === "0.00" ? "text-muted-foreground" : "text-emerald-400"}>
|
<span className={result.qualifyingPoints === "0.00" ? "text-muted-foreground" : "text-emerald-400"}>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from "~/models/scoring-event";
|
} from "~/models/scoring-event";
|
||||||
import { getQPStandings } from "~/models/qualifying-points";
|
import { getQPStandings } from "~/models/qualifying-points";
|
||||||
import { finalizeQualifyingPoints } from "~/models/scoring-calculator";
|
import { finalizeQualifyingPoints } from "~/models/scoring-calculator";
|
||||||
import { getScoringRules } from "~/models/scoring-rules";
|
|
||||||
|
|
||||||
export async function loader({ params }: Route.LoaderArgs) {
|
export async function loader({ params }: Route.LoaderArgs) {
|
||||||
const sportsSeason = await findSportsSeasonById(params.id);
|
const sportsSeason = await findSportsSeasonById(params.id);
|
||||||
|
|
@ -23,7 +23,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||||
|
|
||||||
// For qualifying sports seasons, get QP standings
|
// For qualifying sports seasons, get QP standings
|
||||||
let qpStandings = null;
|
let qpStandings = null;
|
||||||
let scoringRules = null;
|
const scoringRules = null;
|
||||||
if (sportsSeason.scoringPattern === "qualifying_points") {
|
if (sportsSeason.scoringPattern === "qualifying_points") {
|
||||||
qpStandings = await getQPStandings(params.id);
|
qpStandings = await getQPStandings(params.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export default function SportsSeasonEvents({
|
||||||
if (eventType === "schedule_event") {
|
if (eventType === "schedule_event") {
|
||||||
const isPast = eventStartsAt
|
const isPast = eventStartsAt
|
||||||
? new Date(eventStartsAt) < new Date()
|
? 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 ? (
|
return isPast ? (
|
||||||
<Badge variant="outline" className="text-muted-foreground">Results Pending</Badge>
|
<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).
|
// 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
|
// Sum only over participants shown in the table — excludes orphan EV records
|
||||||
// from prior simulation runs for participants no longer in this season.
|
// 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 evA = existingEVs.has(a.id) ? evFromProbs(existingEVs.get(a.id)!) : 0;
|
||||||
const evB = existingEVs.has(b.id) ? evFromProbs(existingEVs.get(b.id)!) : 0;
|
const evB = existingEVs.has(b.id) ? evFromProbs(existingEVs.get(b.id)!) : 0;
|
||||||
return evB - evA;
|
return evB - evA;
|
||||||
|
|
|
||||||
|
|
@ -316,8 +316,8 @@ export default function AdminSportsSeasonFuturesOdds() {
|
||||||
Not matched ({parseResults.unmatched.length}) — enter manually below
|
Not matched ({parseResults.unmatched.length}) — enter manually below
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-amber-200 bg-amber-50 divide-y divide-amber-100 text-sm">
|
<div className="rounded-md border border-amber-200 bg-amber-50 divide-y divide-amber-100 text-sm">
|
||||||
{parseResults.unmatched.map((u, i) => (
|
{parseResults.unmatched.map((u) => (
|
||||||
<div key={`${u.inputName}-${i}`} className="flex justify-between px-3 py-1.5">
|
<div key={u.inputName} className="flex justify-between px-3 py-1.5">
|
||||||
<span>{u.inputName}</span>
|
<span>{u.inputName}</span>
|
||||||
<span className="font-medium">{u.odds > 0 ? '+' : ''}{u.odds}</span>
|
<span className="font-medium">{u.odds > 0 ? '+' : ''}{u.odds}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Name</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead className="w-[50px]"></TableHead>
|
<TableHead className="w-[50px]" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,6 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||||
export async function action({ request, params }: Route.ActionArgs) {
|
export async function action({ request, params }: Route.ActionArgs) {
|
||||||
const formData = await request.formData();
|
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>
|
// Parse updates from form fields: wins_<id>, losses_<id>, otLosses_<id>, conference_<id>, division_<id>
|
||||||
const byId = new Map<
|
const byId = new Map<
|
||||||
string,
|
string,
|
||||||
|
|
@ -87,7 +85,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const [participantId, data] of byId.entries()) {
|
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 wins = data.wins ?? 0;
|
||||||
const losses = data.losses ?? 0;
|
const losses = data.losses ?? 0;
|
||||||
const gamesPlayed = wins + losses + (data.otLosses ?? 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");
|
sportsSeason.sport?.name?.toLowerCase().includes("hockey");
|
||||||
|
|
||||||
// Sort participants: those with records first (by wins desc), then unrecorded
|
// 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 recA = standingsMap[a.id];
|
||||||
const recB = standingsMap[b.id];
|
const recB = standingsMap[b.id];
|
||||||
if (recA && recB) return (recB.wins ?? 0) - (recA.wins ?? 0);
|
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;
|
const { sportsSeason, participants, resultsMap } = loaderData;
|
||||||
|
|
||||||
// Sort participants by current position (unranked at bottom)
|
// 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 posA = resultsMap[a.id]?.currentPosition ?? Infinity;
|
||||||
const posB = resultsMap[b.id]?.currentPosition ?? Infinity;
|
const posB = resultsMap[b.id]?.currentPosition ?? Infinity;
|
||||||
return posA - posB;
|
return posA - posB;
|
||||||
|
|
@ -138,7 +138,7 @@ export default function ManageStandings({ loaderData, actionData }: Route.Compon
|
||||||
<span>Points</span>
|
<span>Points</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sorted.map((participant, index) => {
|
{sorted.map((participant) => {
|
||||||
const result = resultsMap[participant.id];
|
const result = resultsMap[participant.id];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import { getAuth } from "@clerk/react-router/server";
|
||||||
import { isUserAdminByClerkId } from "~/models/user";
|
import { isUserAdminByClerkId } from "~/models/user";
|
||||||
import type { Route } from "./+types/admin.sports-seasons.$id";
|
import type { Route } from "./+types/admin.sports-seasons.$id";
|
||||||
|
|
||||||
import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason } from "~/models/sports-season";
|
import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason, type NewSportsSeason } from "~/models/sports-season";
|
||||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
|
||||||
import { processSeasonStandings, recalculateStandings } from "~/models/scoring-calculator";
|
import { processSeasonStandings, recalculateStandings } from "~/models/scoring-calculator";
|
||||||
import { createDailySnapshot } from "~/models/standings";
|
import { createDailySnapshot } from "~/models/standings";
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
|
|
@ -12,13 +11,12 @@ import { participantEvSnapshots, seasonSports } from "~/database/schema";
|
||||||
import { eq, desc } from "drizzle-orm";
|
import { eq, desc } from "drizzle-orm";
|
||||||
import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry";
|
import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry";
|
||||||
import { syncStandings } from "~/services/standings-sync/index";
|
import { syncStandings } from "~/services/standings-sync/index";
|
||||||
import { getLastSyncedAt } from "~/models/regular-season-standings";
|
|
||||||
import {
|
import {
|
||||||
getPendingStandingsMappings,
|
getPendingStandingsMappings,
|
||||||
deletePendingStandingsMapping,
|
deletePendingStandingsMapping,
|
||||||
} from "~/models/pending-standings-mappings";
|
} from "~/models/pending-standings-mappings";
|
||||||
import { updateParticipant } from "~/models/participant";
|
import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/participant";
|
||||||
import { upsertRegularSeasonStandings } from "~/models/regular-season-standings";
|
import { getLastSyncedAt, upsertRegularSeasonStandings } from "~/models/regular-season-standings";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Label } from "~/components/ui/label";
|
import { Label } from "~/components/ui/label";
|
||||||
|
|
@ -254,7 +252,7 @@ export async function action(args: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateData: any = {
|
const updateData: Partial<NewSportsSeason> = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
year: yearNum,
|
year: yearNum,
|
||||||
startDate: typeof startDate === "string" && startDate ? startDate : null,
|
startDate: typeof startDate === "string" && startDate ? startDate : null,
|
||||||
|
|
@ -265,7 +263,7 @@ export async function action(args: Route.ActionArgs) {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (scoringPattern && typeof scoringPattern === "string") {
|
if (scoringPattern && typeof scoringPattern === "string") {
|
||||||
updateData.scoringPattern = scoringPattern;
|
updateData.scoringPattern = scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalMajors && typeof totalMajors === "string") {
|
if (totalMajors && typeof totalMajors === "string") {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Form, Link, redirect } from "react-router";
|
import { Form, Link, redirect } from "react-router";
|
||||||
import type { Route } from "./+types/admin.sports-seasons.new";
|
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 { findAllSports } from "~/models/sport";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
|
|
@ -75,7 +75,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const seasonData: any = {
|
const seasonData: Partial<NewSportsSeason> = {
|
||||||
sportId,
|
sportId,
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
year: yearNum,
|
year: yearNum,
|
||||||
|
|
@ -86,7 +86,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (scoringPattern && typeof scoringPattern === "string") {
|
if (scoringPattern && typeof scoringPattern === "string") {
|
||||||
seasonData.scoringPattern = scoringPattern;
|
seasonData.scoringPattern = scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalMajors && typeof totalMajors === "string") {
|
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");
|
return redirect("/admin/sports-seasons");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useLoaderData, useActionData, Form } from "react-router";
|
import { Form } from "react-router";
|
||||||
import { redirect } from "react-router";
|
|
||||||
import type { Route } from "./+types/admin.standings-snapshots";
|
import type { Route } from "./+types/admin.standings-snapshots";
|
||||||
|
|
||||||
import { database } from "~/database/context";
|
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
|
// Create a new Svix instance with your webhook secret
|
||||||
const wh = new Webhook(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
|
// Verify the webhook
|
||||||
try {
|
try {
|
||||||
|
|
@ -34,7 +34,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
"svix-id": svix_id,
|
"svix-id": svix_id,
|
||||||
"svix-timestamp": svix_timestamp,
|
"svix-timestamp": svix_timestamp,
|
||||||
"svix-signature": svix_signature,
|
"svix-signature": svix_signature,
|
||||||
});
|
}) as typeof evt;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error verifying webhook:", err);
|
console.error("Error verifying webhook:", err);
|
||||||
return new Response("Error: Verification failed", { status: 400 });
|
return new Response("Error: Verification failed", { status: 400 });
|
||||||
|
|
@ -53,7 +53,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
const user = await findOrCreateUser({
|
const user = await findOrCreateUser({
|
||||||
id,
|
id,
|
||||||
emailAddresses:
|
emailAddresses:
|
||||||
email_addresses?.map((e: any) => ({
|
email_addresses?.map((e: { email_address: string }) => ({
|
||||||
emailAddress: e.email_address,
|
emailAddress: e.email_address,
|
||||||
})) || [],
|
})) || [],
|
||||||
username,
|
username,
|
||||||
|
|
@ -69,7 +69,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
const user = await findOrCreateUser({
|
const user = await findOrCreateUser({
|
||||||
id,
|
id,
|
||||||
emailAddresses:
|
emailAddresses:
|
||||||
email_addresses?.map((e: any) => ({
|
email_addresses?.map((e: { email_address: string }) => ({
|
||||||
emailAddress: e.email_address,
|
emailAddress: e.email_address,
|
||||||
})) || [],
|
})) || [],
|
||||||
username,
|
username,
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
|
|
||||||
const upcomingCalendarEvents = leaguesWithData
|
const upcomingCalendarEvents = leaguesWithData
|
||||||
.flatMap((l) => l.calendarEvents)
|
.flatMap((l) => l.calendarEvents)
|
||||||
.sort((a, b) => toEventSortKey(a).localeCompare(toEventSortKey(b)));
|
.toSorted((a, b) => toEventSortKey(a).localeCompare(toEventSortKey(b)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
leagues: leaguesWithData,
|
leagues: leaguesWithData,
|
||||||
|
|
|
||||||
|
|
@ -127,9 +127,11 @@ export default function DraftBoard() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (season.status !== "draft") return;
|
if (season.status !== "draft") return;
|
||||||
|
|
||||||
const handlePickMade = (data: any) => {
|
type PickShape = (typeof initialPicks)[number];
|
||||||
setPicks((prev: any) => [...prev, data.pick]);
|
const handlePickMade = (data: unknown) => {
|
||||||
setCurrentPick(data.nextPickNumber);
|
const pickData = data as { pick: PickShape; nextPickNumber: number };
|
||||||
|
setPicks((prev) => [...prev, pickData.pick]);
|
||||||
|
setCurrentPick(pickData.nextPickNumber);
|
||||||
};
|
};
|
||||||
|
|
||||||
on("pick-made", handlePickMade);
|
on("pick-made", handlePickMade);
|
||||||
|
|
@ -142,13 +144,14 @@ export default function DraftBoard() {
|
||||||
// Generate draft grid
|
// Generate draft grid
|
||||||
const totalTeams = draftSlots.length;
|
const totalTeams = draftSlots.length;
|
||||||
const totalRounds = season.draftRounds || 1;
|
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++) {
|
for (let round = 0; round < totalRounds; round++) {
|
||||||
const roundPicks: any[] = [];
|
const roundPicks: Array<PickItem | null> = [];
|
||||||
for (let teamIndex = 0; teamIndex < totalTeams; teamIndex++) {
|
for (let teamIndex = 0; teamIndex < totalTeams; teamIndex++) {
|
||||||
const pickNumber = round * totalTeams + teamIndex + 1;
|
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);
|
roundPicks.push(pick || null);
|
||||||
}
|
}
|
||||||
draftGrid.push(roundPicks);
|
draftGrid.push(roundPicks);
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { useDraftSocket } from "~/hooks/useDraftSocket";
|
import { useDraftSocket } from "~/hooks/useDraftSocket";
|
||||||
import { useCallback, useEffect, useState, useMemo, useRef } from "react";
|
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 { Button } from "~/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "~/components/ui/dialog";
|
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)
|
// Resolve user's team and commissioner status (null userId means neither)
|
||||||
const userTeam = userId
|
const userTeam = userId
|
||||||
? season.teams.find((team: any) => team.ownerId === userId)
|
? season.teams.find((team) => team.ownerId === userId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const isCommissioner = userId
|
const isCommissioner = userId
|
||||||
|
|
@ -221,7 +219,6 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
userAutodraftSettings,
|
userAutodraftSettings,
|
||||||
isCommissioner: !!isCommissioner,
|
isCommissioner: !!isCommissioner,
|
||||||
currentUserId: userId,
|
currentUserId: userId,
|
||||||
numFlexPicks: Math.max(0, season.draftRounds - seasonSports.length),
|
|
||||||
ownerMap,
|
ownerMap,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +236,6 @@ export default function DraftRoom() {
|
||||||
userAutodraftSettings,
|
userAutodraftSettings,
|
||||||
isCommissioner,
|
isCommissioner,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
numFlexPicks,
|
|
||||||
ownerMap,
|
ownerMap,
|
||||||
} = useLoaderData<typeof loader>();
|
} = useLoaderData<typeof loader>();
|
||||||
const { revalidate, state: revalidatorState } = useRevalidator();
|
const { revalidate, state: revalidatorState } = useRevalidator();
|
||||||
|
|
@ -431,7 +427,7 @@ export default function DraftRoom() {
|
||||||
// (deduplicated by ID) after the DB snapshot lands.
|
// (deduplicated by ID) after the DB snapshot lands.
|
||||||
const revalidatorStateRef = useRef(revalidatorState);
|
const revalidatorStateRef = useRef(revalidatorState);
|
||||||
const isRevalidatingRef = useRef(false);
|
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
|
// Track the draftPicks reference when revalidation starts. If it hasn't
|
||||||
// changed by the time revalidation completes, the loader didn't return new
|
// 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
|
// data (e.g. network failure) and we should NOT overwrite state that may
|
||||||
|
|
@ -471,9 +467,9 @@ export default function DraftRoom() {
|
||||||
return;
|
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(
|
const missedPicks = pendingPicksDuringRevalidationRef.current.filter(
|
||||||
(p: any) => !dbPickIds.has(p.id)
|
(p) => !dbPickIds.has(p.id)
|
||||||
);
|
);
|
||||||
pendingPicksDuringRevalidationRef.current = [];
|
pendingPicksDuringRevalidationRef.current = [];
|
||||||
|
|
||||||
|
|
@ -500,7 +496,7 @@ export default function DraftRoom() {
|
||||||
if (timers.length > 0) {
|
if (timers.length > 0) {
|
||||||
setTeamTimers((prev) => {
|
setTeamTimers((prev) => {
|
||||||
const updated = { ...prev };
|
const updated = { ...prev };
|
||||||
timers.forEach((timer: any) => {
|
timers.forEach((timer) => {
|
||||||
updated[timer.teamId] = timer.timeRemaining;
|
updated[timer.teamId] = timer.timeRemaining;
|
||||||
});
|
});
|
||||||
return updated;
|
return updated;
|
||||||
|
|
@ -510,7 +506,7 @@ export default function DraftRoom() {
|
||||||
// we were away.
|
// we were away.
|
||||||
setAutodraftStatus(() => {
|
setAutodraftStatus(() => {
|
||||||
const status: Record<string, AutodraftStatusEntry> = {};
|
const status: Record<string, AutodraftStatusEntry> = {};
|
||||||
autodraftSettings.forEach((setting: any) => {
|
autodraftSettings.forEach((setting) => {
|
||||||
status[setting.teamId] = {
|
status[setting.teamId] = {
|
||||||
isEnabled: setting.isEnabled,
|
isEnabled: setting.isEnabled,
|
||||||
mode: setting.mode,
|
mode: setting.mode,
|
||||||
|
|
@ -551,7 +547,7 @@ export default function DraftRoom() {
|
||||||
// Track autodraft status for all teams
|
// Track autodraft status for all teams
|
||||||
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, AutodraftStatusEntry>>(() => {
|
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, AutodraftStatusEntry>>(() => {
|
||||||
const status: Record<string, AutodraftStatusEntry> = {};
|
const status: Record<string, AutodraftStatusEntry> = {};
|
||||||
autodraftSettings.forEach((setting: any) => {
|
autodraftSettings.forEach((setting) => {
|
||||||
status[setting.teamId] = {
|
status[setting.teamId] = {
|
||||||
isEnabled: setting.isEnabled,
|
isEnabled: setting.isEnabled,
|
||||||
mode: setting.mode,
|
mode: setting.mode,
|
||||||
|
|
@ -571,7 +567,7 @@ export default function DraftRoom() {
|
||||||
|
|
||||||
if (timers.length > 0) {
|
if (timers.length > 0) {
|
||||||
// Draft has started, use actual timers
|
// Draft has started, use actual timers
|
||||||
timers.forEach((timer: any) => {
|
timers.forEach((timer) => {
|
||||||
timersMap[timer.teamId] = timer.timeRemaining;
|
timersMap[timer.teamId] = timer.timeRemaining;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -614,7 +610,7 @@ export default function DraftRoom() {
|
||||||
// Shared transforms for eligibility calculations
|
// Shared transforms for eligibility calculations
|
||||||
const transformedPicks = useMemo(
|
const transformedPicks = useMemo(
|
||||||
() =>
|
() =>
|
||||||
picks.map((p: any) => ({
|
picks.map((p) => ({
|
||||||
teamId: p.team.id,
|
teamId: p.team.id,
|
||||||
participant: {
|
participant: {
|
||||||
id: p.participant.id,
|
id: p.participant.id,
|
||||||
|
|
@ -626,7 +622,7 @@ export default function DraftRoom() {
|
||||||
|
|
||||||
const transformedParticipants = useMemo(
|
const transformedParticipants = useMemo(
|
||||||
() =>
|
() =>
|
||||||
availableParticipants.map((p: any) => ({
|
availableParticipants.map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
sport: { id: p.sport.id, name: p.sport.name },
|
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 });
|
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)
|
a.name.localeCompare(b.name)
|
||||||
);
|
);
|
||||||
}, [availableParticipants]);
|
}, [availableParticipants]);
|
||||||
|
|
@ -649,7 +645,7 @@ export default function DraftRoom() {
|
||||||
const eligibility = useMemo(() => {
|
const eligibility = useMemo(() => {
|
||||||
if (!userTeam) return null;
|
if (!userTeam) return null;
|
||||||
const userTeamPicks = transformedPicks.filter(
|
const userTeamPicks = transformedPicks.filter(
|
||||||
(p: any) => p.teamId === userTeam.id
|
(p) => p.teamId === userTeam.id
|
||||||
);
|
);
|
||||||
return calculateDraftEligibility(
|
return calculateDraftEligibility(
|
||||||
userTeam.id,
|
userTeam.id,
|
||||||
|
|
@ -666,7 +662,7 @@ export default function DraftRoom() {
|
||||||
const forcePickEligibility = useMemo(() => {
|
const forcePickEligibility = useMemo(() => {
|
||||||
if (!selectedPickSlot) return null;
|
if (!selectedPickSlot) return null;
|
||||||
const selectedTeamPicks = transformedPicks.filter(
|
const selectedTeamPicks = transformedPicks.filter(
|
||||||
(p: any) => p.teamId === selectedPickSlot.teamId
|
(p) => p.teamId === selectedPickSlot.teamId
|
||||||
);
|
);
|
||||||
return calculateDraftEligibility(
|
return calculateDraftEligibility(
|
||||||
selectedPickSlot.teamId,
|
selectedPickSlot.teamId,
|
||||||
|
|
@ -683,10 +679,10 @@ export default function DraftRoom() {
|
||||||
const replacePickEligibility = useMemo(() => {
|
const replacePickEligibility = useMemo(() => {
|
||||||
if (!replacePickSlot) return null;
|
if (!replacePickSlot) return null;
|
||||||
const allPicksExcluding = transformedPicks.filter(
|
const allPicksExcluding = transformedPicks.filter(
|
||||||
(p: any) => p.participant.id !== replacePickSlot.oldParticipantId
|
(p) => p.participant.id !== replacePickSlot.oldParticipantId
|
||||||
);
|
);
|
||||||
const selectedTeamPicks = allPicksExcluding.filter(
|
const selectedTeamPicks = allPicksExcluding.filter(
|
||||||
(p: any) => p.teamId === replacePickSlot.teamId
|
(p) => p.teamId === replacePickSlot.teamId
|
||||||
);
|
);
|
||||||
return calculateDraftEligibility(
|
return calculateDraftEligibility(
|
||||||
replacePickSlot.teamId,
|
replacePickSlot.teamId,
|
||||||
|
|
@ -701,7 +697,8 @@ export default function DraftRoom() {
|
||||||
|
|
||||||
// Listen for new picks from other users
|
// Listen for new picks from other users
|
||||||
useEffect(() => {
|
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) {
|
if (isRevalidatingRef.current) {
|
||||||
// Buffer this pick; the revalidation-complete handler will merge it
|
// Buffer this pick; the revalidation-complete handler will merge it
|
||||||
// into the DB snapshot so it isn't silently dropped.
|
// 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.
|
// pick counter and the picks list in sync with each other.
|
||||||
pendingPicksDuringRevalidationRef.current.push(data.pick);
|
pendingPicksDuringRevalidationRef.current.push(data.pick);
|
||||||
} else {
|
} else {
|
||||||
setPicks((prev: any) => [...prev, data.pick]);
|
setPicks((prev) => [...prev, data.pick]);
|
||||||
setCurrentPick(data.nextPickNumber);
|
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.
|
// Bail out without creating a new object reference if nothing changed.
|
||||||
// Without this guard the spread `{ ...prev }` always returns a new object,
|
// 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.
|
// 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.
|
// if the HTTP revalidation hasn't completed yet.
|
||||||
// React's useState already bails out when the new value equals the old
|
// React's useState already bails out when the new value equals the old
|
||||||
// one (Object.is comparison on primitives), so no manual guard needed.
|
// one (Object.is comparison on primitives), so no manual guard needed.
|
||||||
if (data.currentPickNumber != null) {
|
if (data.currentPickNumber !== null) {
|
||||||
setCurrentPick(data.currentPickNumber);
|
setCurrentPick(data.currentPickNumber);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -823,35 +820,35 @@ export default function DraftRoom() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleParticipantRemovedFromQueues = (data: { participantId: string }) => {
|
const handleParticipantRemovedFromQueues = (data: { participantId: string }) => {
|
||||||
setQueue((prev: any) =>
|
setQueue((prev) =>
|
||||||
prev.filter((item: any) => item.participantId !== data.participantId)
|
prev.filter((item) => item.participantId !== data.participantId)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQueueEligibilityPruned = (data: { teamId: string; removedParticipantIds: string[] }) => {
|
const handleQueueEligibilityPruned = (data: { teamId: string; removedParticipantIds: string[] }) => {
|
||||||
if (data.teamId !== userTeam?.id) return;
|
if (data.teamId !== userTeam?.id) return;
|
||||||
const removed = new Set(data.removedParticipantIds);
|
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).
|
// 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).
|
// 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
|
// Ignored while this window has in-flight mutations to prevent the server echo
|
||||||
// from clobbering a more-recent optimistic update.
|
// from clobbering a more-recent optimistic update.
|
||||||
const handleQueueUpdated = (data: { queue: any[] }) => {
|
const handleQueueUpdated = (data: { queue: QueueItem[] }) => {
|
||||||
if (pendingQueueMutationsRef.current > 0) return;
|
if (pendingQueueMutationsRef.current > 0) return;
|
||||||
setQueue(data.queue);
|
setQueue(data.queue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePickReplaced = (data: any) => {
|
const handlePickReplaced = (data: { pickNumber: number; pick: (typeof draftPicks)[number] }) => {
|
||||||
setPicks((prev: any) =>
|
setPicks((prev) =>
|
||||||
prev.map((p: any) => (p.pickNumber === data.pickNumber ? data.pick : p))
|
prev.map((p) => (p.pickNumber === data.pickNumber ? data.pick : p))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDraftRolledBack = (data: any) => {
|
const handleDraftRolledBack = (data: { pickNumber: number }) => {
|
||||||
setPicks((prev: any) =>
|
setPicks((prev) =>
|
||||||
prev.filter((p: any) => p.pickNumber < data.pickNumber)
|
prev.filter((p) => p.pickNumber < data.pickNumber)
|
||||||
);
|
);
|
||||||
setCurrentPick(data.pickNumber);
|
setCurrentPick(data.pickNumber);
|
||||||
setIsDraftComplete(false);
|
setIsDraftComplete(false);
|
||||||
|
|
@ -861,7 +858,7 @@ export default function DraftRoom() {
|
||||||
// Full state sync sent by the server on join-draft. Provides an immediate,
|
// 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
|
// socket-based path to bring the client up to date — critical for mobile
|
||||||
// reconnection where the HTTP revalidation may be delayed or fail entirely.
|
// 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.
|
// If a revalidation is in-flight, let it complete and handle the merge.
|
||||||
// The revalidation path already buffers picks and deduplicates — layering
|
// The revalidation path already buffers picks and deduplicates — layering
|
||||||
// this on top would risk conflicts. But if NOT revalidating, apply the
|
// this on top would risk conflicts. But if NOT revalidating, apply the
|
||||||
|
|
@ -876,7 +873,7 @@ export default function DraftRoom() {
|
||||||
if (data.timers) {
|
if (data.timers) {
|
||||||
setTeamTimers((prev) => {
|
setTeamTimers((prev) => {
|
||||||
const updated = { ...prev };
|
const updated = { ...prev };
|
||||||
data.timers.forEach((t: any) => {
|
data.timers?.forEach((t) => {
|
||||||
updated[t.teamId] = t.timeRemaining;
|
updated[t.teamId] = t.timeRemaining;
|
||||||
});
|
});
|
||||||
return updated;
|
return updated;
|
||||||
|
|
@ -989,7 +986,7 @@ export default function DraftRoom() {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
toast.error(error.error || "Failed to add to queue");
|
toast.error(error.error || "Failed to add to queue");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Revert the optimistic update on network error
|
// Revert the optimistic update on network error
|
||||||
setQueue((prev) => prev.filter((item) => item.id !== tempQueueItem.id));
|
setQueue((prev) => prev.filter((item) => item.id !== tempQueueItem.id));
|
||||||
toast.error("Network error - failed to add to queue");
|
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 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) {
|
if (!pick) {
|
||||||
toast.error("Pick not found — try refreshing the page");
|
toast.error("Pick not found — try refreshing the page");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1391,7 +1388,7 @@ export default function DraftRoom() {
|
||||||
round: number;
|
round: number;
|
||||||
pickInRound: number;
|
pickInRound: number;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
pick?: any;
|
pick?: (typeof draftPicks)[number];
|
||||||
}>
|
}>
|
||||||
> = [];
|
> = [];
|
||||||
|
|
||||||
|
|
@ -1405,7 +1402,7 @@ export default function DraftRoom() {
|
||||||
const pickNumber = (round - 1) * teamCount + pickInRound;
|
const pickNumber = (round - 1) * teamCount + pickInRound;
|
||||||
const teamId = draftSlots[teamIndex]?.team.id;
|
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 });
|
roundPicks.push({ pickNumber, round, pickInRound, teamId, pick });
|
||||||
}
|
}
|
||||||
|
|
@ -1417,7 +1414,7 @@ export default function DraftRoom() {
|
||||||
|
|
||||||
// Get drafted participant IDs for filtering
|
// Get drafted participant IDs for filtering
|
||||||
const draftedParticipantIds = useMemo(
|
const draftedParticipantIds = useMemo(
|
||||||
() => new Set(picks.map((p: any) => p.participant.id)),
|
() => new Set(picks.map((p) => p.participant.id)),
|
||||||
[picks]
|
[picks]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1426,8 +1423,8 @@ export default function DraftRoom() {
|
||||||
if (!userTeam) return new Set<string>();
|
if (!userTeam) return new Set<string>();
|
||||||
return new Set(
|
return new Set(
|
||||||
picks
|
picks
|
||||||
.filter((p: any) => p.team.id === userTeam.id)
|
.filter((p) => p.team.id === userTeam.id)
|
||||||
.map((p: any) => p.sport.id)
|
.map((p) => p.sport.id)
|
||||||
);
|
);
|
||||||
}, [picks, userTeam]);
|
}, [picks, userTeam]);
|
||||||
|
|
||||||
|
|
@ -1436,8 +1433,8 @@ export default function DraftRoom() {
|
||||||
if (!userTeam) return new Set<string>();
|
if (!userTeam) return new Set<string>();
|
||||||
return new Set(
|
return new Set(
|
||||||
picks
|
picks
|
||||||
.filter((p: any) => p.team.id === userTeam.id)
|
.filter((p) => p.team.id === userTeam.id)
|
||||||
.map((p: any) => p.sport.name)
|
.map((p) => p.sport.name)
|
||||||
);
|
);
|
||||||
}, [picks, userTeam]);
|
}, [picks, userTeam]);
|
||||||
|
|
||||||
|
|
@ -1445,7 +1442,7 @@ export default function DraftRoom() {
|
||||||
const uniqueSports = useMemo(
|
const uniqueSports = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Array.from(
|
Array.from(
|
||||||
new Set(availableParticipants.map((p: any) => p.sport.name))
|
new Set(availableParticipants.map((p) => p.sport.name))
|
||||||
).sort(),
|
).sort(),
|
||||||
[availableParticipants]
|
[availableParticipants]
|
||||||
);
|
);
|
||||||
|
|
@ -1453,7 +1450,7 @@ export default function DraftRoom() {
|
||||||
// Filter participants based on search, sport, drafted status, and eligibility
|
// Filter participants based on search, sport, drafted status, and eligibility
|
||||||
const filteredParticipants = useMemo(() => {
|
const filteredParticipants = useMemo(() => {
|
||||||
const sportFilterSet = new Set(sportFilters);
|
const sportFilterSet = new Set(sportFilters);
|
||||||
return availableParticipants.filter((participant: any) => {
|
return availableParticipants.filter((participant) => {
|
||||||
// Drafted filter - hide drafted participants by default
|
// Drafted filter - hide drafted participants by default
|
||||||
if (hideDrafted && draftedParticipantIds.has(participant.id)) {
|
if (hideDrafted && draftedParticipantIds.has(participant.id)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1621,9 +1618,9 @@ export default function DraftRoom() {
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
>
|
>
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
<line x1="3" y1="12" x2="21" y2="12" />
|
||||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
<line x1="3" y1="6" x2="21" y2="6" />
|
||||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
<line x1="3" y1="18" x2="21" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1962,7 +1959,7 @@ export default function DraftRoom() {
|
||||||
<DialogTitle>Roll Back Draft</DialogTitle>
|
<DialogTitle>Roll Back Draft</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{rollbackPickNumber !== null && (() => {
|
{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.`;
|
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>
|
</DialogDescription>
|
||||||
|
|
|
||||||
|
|
@ -85,14 +85,14 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
const ownerMap = new Map(
|
const ownerMap = new Map(
|
||||||
ownerIds
|
ownerIds
|
||||||
.map((id) => [id, userByClerkId.get(id)] as const)
|
.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)])
|
.map(([id, user]) => [id, getUserDisplayName(user)])
|
||||||
);
|
);
|
||||||
|
|
||||||
const commissionerMap = new Map(
|
const commissionerMap = new Map(
|
||||||
commissionerIds
|
commissionerIds
|
||||||
.map((id) => [id, userByClerkId.get(id)] as const)
|
.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)])
|
.map(([id, user]) => [id, getUserDisplayName(user)])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -149,7 +149,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
sportsSeasonPageUrl: `/leagues/${leagueId}/sports-seasons/${ss.id}`,
|
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
|
// Check if draft order is set
|
||||||
const isDraftOrderSet = draftSlots.length > 0;
|
const isDraftOrderSet = draftSlots.length > 0;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
countCommissionersByLeagueId,
|
countCommissionersByLeagueId,
|
||||||
removeCommissionerByLeagueAndUser,
|
removeCommissionerByLeagueAndUser,
|
||||||
} from "~/models/commissioner";
|
} 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 { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team";
|
||||||
import { findAllUsers, findUserByClerkId, findUsersByClerkIds, getUserDisplayName, isUserAdminByClerkId } from "~/models/user";
|
import { findAllUsers, findUserByClerkId, findUsersByClerkIds, getUserDisplayName, isUserAdminByClerkId } from "~/models/user";
|
||||||
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
||||||
|
|
@ -281,7 +281,7 @@ export async function action(args: Route.ActionArgs) {
|
||||||
|
|
||||||
const selectedSports = formData.getAll("sportsSeasons");
|
const selectedSports = formData.getAll("sportsSeasons");
|
||||||
const currentSportIds = new Set(
|
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[]);
|
const newSportIds = new Set(selectedSports as string[]);
|
||||||
|
|
||||||
|
|
@ -465,7 +465,7 @@ export async function action(args: Route.ActionArgs) {
|
||||||
// Now handle season-specific updates.
|
// Now handle season-specific updates.
|
||||||
try {
|
try {
|
||||||
// Update season settings
|
// Update season settings
|
||||||
const seasonUpdates: any = {};
|
const seasonUpdates: Partial<NewSeason> = {};
|
||||||
|
|
||||||
// Handle draft rounds
|
// Handle draft rounds
|
||||||
if (typeof draftRounds === "string") {
|
if (typeof draftRounds === "string") {
|
||||||
|
|
@ -514,7 +514,7 @@ export async function action(args: Route.ActionArgs) {
|
||||||
if (points < 0 || points > 1000) {
|
if (points < 0 || points > 1000) {
|
||||||
return { error: `${field} must be between 0 and 1000 points` };
|
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 [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [timerMode, setTimerMode] = useState<"chess_clock" | "standard">(season?.draftTimerMode ?? "chess_clock");
|
const [timerMode, setTimerMode] = useState<"chess_clock" | "standard">(season?.draftTimerMode ?? "chess_clock");
|
||||||
const [selectedSports, setSelectedSports] = useState<Set<string>>(
|
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[]>(
|
const [draftOrderTeams, setDraftOrderTeams] = useState<string[]>(
|
||||||
draftSlots.length > 0
|
draftSlots.length > 0
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import { getRegularSeasonStandings } from "~/models/regular-season-standings";
|
import { getRegularSeasonStandings } from "~/models/regular-season-standings";
|
||||||
import { getAllParticipantEVsForSeason } from "~/models/participant-expected-value";
|
import { getAllParticipantEVsForSeason } from "~/models/participant-expected-value";
|
||||||
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
|
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
|
||||||
|
import type { PlayoffMatch } from "~/models/playoff-match";
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
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 } };
|
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 playoffRounds: string[] = [];
|
||||||
let preEliminatedParticipants: { id: string; name: string }[] = [];
|
let preEliminatedParticipants: { id: string; name: string }[] = [];
|
||||||
let participantPoints: { participantId: string; points: number }[] = [];
|
let participantPoints: { participantId: string; points: number }[] = [];
|
||||||
let partialScoreParticipantIds: string[] = [];
|
let partialScoreParticipantIds: string[] = [];
|
||||||
let seasonStandings: SeasonStanding[] = [];
|
let seasonStandings: SeasonStanding[] = [];
|
||||||
let qpStandings: any[] = [];
|
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number];
|
||||||
|
let qpStandings: QPStanding[] = [];
|
||||||
|
|
||||||
if (scoringPattern === "playoff_bracket") {
|
if (scoringPattern === "playoff_bracket") {
|
||||||
// Fetch playoff matches via scoring events
|
// Fetch playoff matches via scoring events
|
||||||
|
|
@ -164,7 +172,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
loser: true,
|
loser: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
playoffMatches = matches;
|
playoffMatches = matches as PlayoffMatchWithRelations[];
|
||||||
|
|
||||||
templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId ?? undefined;
|
templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId ?? undefined;
|
||||||
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
||||||
|
|
@ -186,8 +194,8 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
preEliminatedParticipants = eliminatedResults
|
preEliminatedParticipants = eliminatedResults
|
||||||
.filter((r) => r.participant)
|
.filter((r): r is typeof r & { participant: NonNullable<typeof r.participant> } => r.participant !== null && r.participant !== undefined)
|
||||||
.map((r) => ({ id: (r as any).participant.id, name: (r as any).participant.name }));
|
.map((r) => ({ id: r.participant.id, name: r.participant.name }));
|
||||||
|
|
||||||
// Compute per-participant fantasy points directly from participant_results.
|
// Compute per-participant fantasy points directly from participant_results.
|
||||||
// This covers both finalized placements and progressive floor scores for still-alive participants.
|
// 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 hasBracket = playoffMatches && playoffMatches.length > 0;
|
||||||
const hasStandings = regularSeasonStandings && regularSeasonStandings.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 simulatorType = sportsSeason.sport?.simulatorType;
|
||||||
const standingsDisplayMode =
|
const standingsDisplayMode =
|
||||||
|
|
@ -80,7 +80,7 @@ export default function SportSeasonDetail({
|
||||||
|
|
||||||
// Build ownership map for RegularSeasonStandings
|
// Build ownership map for RegularSeasonStandings
|
||||||
const ownershipMap = Object.fromEntries(
|
const ownershipMap = Object.fromEntries(
|
||||||
teamOwnerships.map((o: any) => [
|
teamOwnerships.map((o) => [
|
||||||
o.participantId,
|
o.participantId,
|
||||||
{ teamName: o.teamName, ownerName: o.ownerName ?? "", teamId: o.teamId },
|
{ teamName: o.teamName, ownerName: o.ownerName ?? "", teamId: o.teamId },
|
||||||
])
|
])
|
||||||
|
|
@ -113,12 +113,12 @@ export default function SportSeasonDetail({
|
||||||
{hasStandings && !hasBracket && (
|
{hasStandings && !hasBracket && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<RegularSeasonStandings
|
<RegularSeasonStandings
|
||||||
standings={regularSeasonStandings as any}
|
standings={regularSeasonStandings}
|
||||||
teamOwnerships={ownershipMap}
|
teamOwnerships={ownershipMap}
|
||||||
userParticipantIds={userParticipantIds}
|
userParticipantIds={userParticipantIds}
|
||||||
showOtLosses={showOtLosses}
|
showOtLosses={showOtLosses}
|
||||||
participantEvs={participantEvs}
|
participantEvs={participantEvs}
|
||||||
displayMode={standingsDisplayMode as any}
|
displayMode={standingsDisplayMode as "flat" | "nhl-divisions"}
|
||||||
playoffSpots={playoffSpots}
|
playoffSpots={playoffSpots}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -129,25 +129,25 @@ export default function SportSeasonDetail({
|
||||||
(upcomingEvents.length > 0 || recentEvents.length > 0) && (
|
(upcomingEvents.length > 0 || recentEvents.length > 0) && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<EventSchedule
|
<EventSchedule
|
||||||
upcomingEvents={upcomingEvents as any}
|
upcomingEvents={upcomingEvents}
|
||||||
recentEvents={recentEvents as any}
|
recentEvents={recentEvents}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hide bracket display when standings exist but no bracket matches have been set yet */}
|
{/* Hide bracket display when standings exist but no bracket matches have been set yet */}
|
||||||
{!(scoringPattern === "playoff_bracket" && hasStandings && !hasBracket) && <SportSeasonDisplay
|
{!(scoringPattern === "playoff_bracket" && hasStandings && !hasBracket) && <SportSeasonDisplay
|
||||||
scoringPattern={scoringPattern as any}
|
scoringPattern={scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points"}
|
||||||
sportSeasonName={sportsSeason.name}
|
sportSeasonName={sportsSeason.name}
|
||||||
sportName={sportsSeason.sport.name}
|
sportName={sportsSeason.sport.name}
|
||||||
playoffMatches={playoffMatches as any}
|
playoffMatches={playoffMatches}
|
||||||
playoffRounds={playoffRounds}
|
playoffRounds={playoffRounds}
|
||||||
preEliminatedParticipants={preEliminatedParticipants}
|
preEliminatedParticipants={preEliminatedParticipants}
|
||||||
participantPoints={participantPoints}
|
participantPoints={participantPoints}
|
||||||
partialScoreParticipantIds={partialScoreParticipantIds}
|
partialScoreParticipantIds={partialScoreParticipantIds}
|
||||||
seasonStandings={seasonStandings as any}
|
seasonStandings={seasonStandings}
|
||||||
seasonIsFinalized={seasonIsFinalized}
|
seasonIsFinalized={seasonIsFinalized}
|
||||||
qpStandings={qpStandings as any}
|
qpStandings={qpStandings}
|
||||||
qpIsFinalized={sportsSeason.qualifyingPointsFinalized || false}
|
qpIsFinalized={sportsSeason.qualifyingPointsFinalized || false}
|
||||||
totalMajors={sportsSeason.totalMajors}
|
totalMajors={sportsSeason.totalMajors}
|
||||||
majorsCompleted={sportsSeason.majorsCompleted || 0}
|
majorsCompleted={sportsSeason.majorsCompleted || 0}
|
||||||
|
|
@ -166,12 +166,12 @@ export default function SportSeasonDetail({
|
||||||
{hasStandings && hasBracket && (
|
{hasStandings && hasBracket && (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<RegularSeasonStandings
|
<RegularSeasonStandings
|
||||||
standings={regularSeasonStandings as any}
|
standings={regularSeasonStandings}
|
||||||
teamOwnerships={ownershipMap}
|
teamOwnerships={ownershipMap}
|
||||||
userParticipantIds={userParticipantIds}
|
userParticipantIds={userParticipantIds}
|
||||||
showOtLosses={showOtLosses}
|
showOtLosses={showOtLosses}
|
||||||
participantEvs={participantEvs}
|
participantEvs={participantEvs}
|
||||||
displayMode={standingsDisplayMode as any}
|
displayMode={standingsDisplayMode as "flat" | "nhl-divisions"}
|
||||||
playoffSpots={playoffSpots}
|
playoffSpots={playoffSpots}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -85,12 +85,12 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Build teamId -> ownerName map
|
// 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 userRows = await findUsersByClerkIds(ownerIds);
|
||||||
const userByClerkId = new Map(userRows.map((u) => [u.clerkId, u]));
|
const userByClerkId = new Map(userRows.map((u) => [u.clerkId, u]));
|
||||||
const ownerNameByTeamId = new Map(
|
const ownerNameByTeamId = new Map(
|
||||||
teams
|
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) => {
|
.map((t) => {
|
||||||
const user = userByClerkId.get(t.ownerId);
|
const user = userByClerkId.get(t.ownerId);
|
||||||
return [t.id, user ? getUserDisplayName(user) : null] as const;
|
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]));
|
const standingsMap = new Map(standings.map((s) => [s.teamId, s]));
|
||||||
|
|
||||||
// Pre-compute sorted standings list
|
// 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 rankA = standingsMap.get(a.id)?.currentRank ?? Infinity;
|
||||||
const rankB = standingsMap.get(b.id)?.currentRank ?? Infinity;
|
const rankB = standingsMap.get(b.id)?.currentRank ?? Infinity;
|
||||||
return rankA - rankB;
|
return rankA - rankB;
|
||||||
|
|
@ -95,7 +95,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
|
|
||||||
// Pre-compute sorted sports seasons: active → upcoming → completed,
|
// Pre-compute sorted sports seasons: active → upcoming → completed,
|
||||||
// season_standings last within active, then alphabetical by sport name
|
// 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 statusOrder = { active: 0, upcoming: 1, completed: 2 } as const;
|
||||||
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
||||||
if (statusDiff !== 0) return statusDiff;
|
if (statusDiff !== 0) return statusDiff;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ describe("timer-update handler – currentPickNumber sync", () => {
|
||||||
...prev,
|
...prev,
|
||||||
[data.teamId]: data.timeRemaining,
|
[data.teamId]: data.timeRemaining,
|
||||||
}));
|
}));
|
||||||
if (data.currentPickNumber != null) {
|
if (data.currentPickNumber !== null) {
|
||||||
setCurrentPick(data.currentPickNumber);
|
setCurrentPick(data.currentPickNumber);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -132,7 +132,7 @@ describe("timer-update handler – currentPickNumber sync", () => {
|
||||||
const setCurrentPick = (value: number) => { currentPick = value; };
|
const setCurrentPick = (value: number) => { currentPick = value; };
|
||||||
|
|
||||||
const handleTimerUpdate = (data: any) => {
|
const handleTimerUpdate = (data: any) => {
|
||||||
if (data.currentPickNumber != null) {
|
if (data.currentPickNumber !== null) {
|
||||||
setCurrentPick(data.currentPickNumber);
|
setCurrentPick(data.currentPickNumber);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { updateLeague, findLeagueById } from '~/models/league';
|
import { updateLeague } from '~/models/league';
|
||||||
import { isCommissioner } from '~/models/commissioner';
|
|
||||||
import { findCurrentSeasonWithSports, updateSeason } from '~/models/season';
|
import { findCurrentSeasonWithSports, updateSeason } from '~/models/season';
|
||||||
import { findTeamsBySeasonId } from '~/models/team';
|
|
||||||
|
|
||||||
// Mock all models used in the settings update intent
|
// Mock all models used in the settings update intent
|
||||||
vi.mock('~/models/league', () => ({
|
vi.mock('~/models/league', () => ({
|
||||||
|
|
@ -175,7 +173,7 @@ describe('Settings Update - League Save', () => {
|
||||||
let result: { error: string } | undefined;
|
let result: { error: string } | undefined;
|
||||||
try {
|
try {
|
||||||
await updateLeague('league-1', { name: 'Test League', isPublicDraftBoard: false });
|
await updateLeague('league-1', { name: 'Test League', isPublicDraftBoard: false });
|
||||||
} catch (e) {
|
} catch {
|
||||||
result = { error: 'Failed to update league. Please try again.' };
|
result = { error: 'Failed to update league. Please try again.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export async function loader() {
|
||||||
return {
|
return {
|
||||||
...template,
|
...template,
|
||||||
sportsSeasonIds: templateWithSports?.seasonTemplateSports?.map(
|
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(() => {
|
useEffect(() => {
|
||||||
// Listen for test messages
|
// Listen for test messages
|
||||||
const handleTestMessage = (data: any) => {
|
const handleTestMessage = (data: unknown) => {
|
||||||
console.log("Received test message:", data);
|
console.log("Received test message:", data);
|
||||||
setMessages((prev) => [...prev, JSON.stringify(data)]);
|
setMessages((prev) => [...prev, JSON.stringify(data)]);
|
||||||
};
|
};
|
||||||
|
|
@ -64,6 +64,7 @@ export default function TestSocket() {
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{messages.map((msg, idx) => (
|
{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">
|
<li key={idx} className="text-sm font-mono bg-card p-2 rounded">
|
||||||
{msg}
|
{msg}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ describe('bracket-simulator', () => {
|
||||||
expect(results.size).toBe(8);
|
expect(results.size).toBe(8);
|
||||||
|
|
||||||
// With equal ratings, each team should have roughly equal probability
|
// 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
|
// Sum of probabilities should be 1.0
|
||||||
const sum = distribution.reduce((acc, p) => acc + p, 0);
|
const sum = distribution.reduce((acc, p) => acc + p, 0);
|
||||||
expect(sum).toBeCloseTo(1.0, 1);
|
expect(sum).toBeCloseTo(1.0, 1);
|
||||||
|
|
@ -96,7 +96,7 @@ describe('bracket-simulator', () => {
|
||||||
|
|
||||||
const results = simulateBracketSync(teams, 'nhl-8', 5000);
|
const results = simulateBracketSync(teams, 'nhl-8', 5000);
|
||||||
|
|
||||||
results.forEach((distribution, participantId) => {
|
results.forEach((distribution, _participantId) => {
|
||||||
const sum = distribution.reduce((acc, p) => acc + p, 0);
|
const sum = distribution.reduce((acc, p) => acc + p, 0);
|
||||||
expect(sum).toBeCloseTo(1.0, 1);
|
expect(sum).toBeCloseTo(1.0, 1);
|
||||||
});
|
});
|
||||||
|
|
@ -154,7 +154,7 @@ describe('bracket-simulator', () => {
|
||||||
|
|
||||||
const results = simulateBracketSync(teams, 'nhl-8', 1000);
|
const results = simulateBracketSync(teams, 'nhl-8', 1000);
|
||||||
|
|
||||||
results.forEach((distribution, participantId) => {
|
results.forEach((distribution, _participantId) => {
|
||||||
// Should be array of 8 numbers
|
// Should be array of 8 numbers
|
||||||
expect(distribution).toHaveLength(8);
|
expect(distribution).toHaveLength(8);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
eloWinProbability,
|
eloWinProbability,
|
||||||
convertFuturesToElo,
|
convertFuturesToElo,
|
||||||
calculatePredictionError,
|
calculatePredictionError,
|
||||||
DEFAULT_CALIBRATION,
|
|
||||||
} from '../probability-engine';
|
} from '../probability-engine';
|
||||||
|
|
||||||
describe('probability-engine', () => {
|
describe('probability-engine', () => {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ describe("probability-updater", () => {
|
||||||
notes: null,
|
notes: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
participant: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -108,6 +109,7 @@ describe("probability-updater", () => {
|
||||||
notes: null,
|
notes: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
participant: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "result-2",
|
id: "result-2",
|
||||||
|
|
@ -119,6 +121,7 @@ describe("probability-updater", () => {
|
||||||
notes: null,
|
notes: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
participant: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -195,6 +198,7 @@ describe("probability-updater", () => {
|
||||||
notes: null,
|
notes: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
participant: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -219,6 +223,7 @@ describe("probability-updater", () => {
|
||||||
notes: null,
|
notes: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: 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", () => {
|
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++) {
|
for (let i = 1; i <= 4; i++) {
|
||||||
const p1 = (i - 1) * 2;
|
const _p1 = (i - 1) * 2;
|
||||||
const p2 = (i - 1) * 2 + 1;
|
const p2 = (i - 1) * 2 + 1;
|
||||||
expect(p2).toBeLessThan(8);
|
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", () => {
|
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++) {
|
for (let i = 1; i <= 2; i++) {
|
||||||
const p1 = (i - 1) * 2;
|
const _p1 = (i - 1) * 2;
|
||||||
const p2 = (i - 1) * 2 + 1;
|
const p2 = (i - 1) * 2 + 1;
|
||||||
expect(p2).toBeLessThan(4);
|
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", () => {
|
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++) {
|
for (let i = 1; i <= 4; i++) {
|
||||||
const p1 = (i - 1) * 2;
|
const _p1 = (i - 1) * 2;
|
||||||
const p2 = (i - 1) * 2 + 1;
|
const p2 = (i - 1) * 2 + 1;
|
||||||
expect(p2).toBeLessThan(8);
|
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", () => {
|
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++) {
|
for (let i = 1; i <= 2; i++) {
|
||||||
const p1 = (i - 1) * 2;
|
const _p1 = (i - 1) * 2;
|
||||||
const p2 = (i - 1) * 2 + 1;
|
const p2 = (i - 1) * 2 + 1;
|
||||||
expect(p2).toBeLessThan(4);
|
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
|
// 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
|
// Use team-1 as champion path: R16m1→QFm1→SFm1→Final
|
||||||
const champion = PARTICIPANT_IDS[0]; // team-1, participant1 of R16 match 1
|
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 r16Matches = Array.from({ length: 8 }, (_, i) => {
|
||||||
const matchNum = i + 1;
|
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
|
// 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
|
// 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
|
// 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 _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 _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.
|
// Actually: QF1: team-1 beats team-4; QF2: team-6 beats team-8; etc.
|
||||||
const qfMatches = Array.from({ length: 4 }, (_, i) => {
|
const qfMatches = Array.from({ length: 4 }, (_, i) => {
|
||||||
const matchNum = i + 1;
|
const matchNum = i + 1;
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ export class AutoRacingSimulator implements Simulator {
|
||||||
|
|
||||||
for (const p of participants) {
|
for (const p of participants) {
|
||||||
const ev = evMap.get(p.id);
|
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
|
// Normalize to remove vig
|
||||||
|
|
@ -164,7 +164,7 @@ export class AutoRacingSimulator implements Simulator {
|
||||||
// rankCounts[id][0..7] = number of times driver finished 1st..8th
|
// rankCounts[id][0..7] = number of times driver finished 1st..8th
|
||||||
const rankCounts = new Map<string, number[]>();
|
const rankCounts = new Map<string, number[]>();
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
rankCounts.set(id, new Array(8).fill(0));
|
rankCounts.set(id, Array.from({ length: 8 }, () => 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remainingRaces === 0) {
|
if (remainingRaces === 0) {
|
||||||
|
|
@ -215,7 +215,7 @@ export class AutoRacingSimulator implements Simulator {
|
||||||
|
|
||||||
// 7d. Sort by final championship points, record top-8 finishes
|
// 7d. Sort by final championship points, record top-8 finishes
|
||||||
const finalOrder = [...simPoints.entries()]
|
const finalOrder = [...simPoints.entries()]
|
||||||
.sort((a, b) => b[1] - a[1])
|
.toSorted((a, b) => b[1] - a[1])
|
||||||
.map(([id]) => id);
|
.map(([id]) => id);
|
||||||
|
|
||||||
for (let rank = 0; rank < Math.min(8, finalOrder.length); rank++) {
|
for (let rank = 0; rank < Math.min(8, finalOrder.length); rank++) {
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import { participantExpectedValues, participants } from "~/database/schema";
|
import { participantExpectedValues } from "~/database/schema";
|
||||||
import { eq, and, isNotNull } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { simulateBracket } from "~/services/bracket-simulator";
|
import { simulateBracket } from "~/services/bracket-simulator";
|
||||||
import { convertFuturesToElo } from "~/services/probability-engine";
|
import { convertFuturesToElo } from "~/services/probability-engine";
|
||||||
import type { Simulator, SimulationResult } from "./types";
|
import type { Simulator, SimulationResult } from "./types";
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@ export class NCAAMSimulator implements Simulator {
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortByMatchNumber = (matches: typeof allMatches) =>
|
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 firstFourMatches = byRound.has("First Four") ? sortByMatchNumber(byRound.get("First Four")!) : [];
|
||||||
const r64Matches = byRound.has("Round of 64") ? sortByMatchNumber(byRound.get("Round of 64")!) : null;
|
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) =>
|
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 firstFourMatches = byRound.has("First Four") ? sortByMatchNumber(byRound.get("First Four")!) : [];
|
||||||
const r64Matches = byRound.has("Round of 64") ? sortByMatchNumber(byRound.get("Round of 64")!) : null;
|
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()]
|
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));
|
.map((matches) => matches.sort((a, b) => a.matchNumber - b.matchNumber));
|
||||||
|
|
||||||
if (sortedRoundMatches.length < 4) {
|
if (sortedRoundMatches.length < 4) {
|
||||||
|
|
@ -178,7 +178,7 @@ export class UCLSimulator implements Simulator {
|
||||||
const ev = evMap.get(id);
|
const ev = evMap.get(id);
|
||||||
rawProbs.set(
|
rawProbs.set(
|
||||||
id,
|
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);
|
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";
|
import { findMatchingTeamName } from "~/lib/normalize-team-name";
|
||||||
|
|
||||||
// Pure unit tests for the name-matching logic used by syncStandings.
|
// 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
|
// 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 winsA = statsMap(a.entry.stats).get("wins")?.value ?? 0;
|
||||||
const winsB = statsMap(b.entry.stats).get("wins")?.value ?? 0;
|
const winsB = statsMap(b.entry.stats).get("wins")?.value ?? 0;
|
||||||
if (winsB !== winsA) return winsB - winsA;
|
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 ...)
|
// Seed within conference — ESPN key is "playoffSeed", value is a float (1.0, 2.0 ...)
|
||||||
const playoffSeedStat = sm.get("playoffSeed");
|
const playoffSeedStat = sm.get("playoffSeed");
|
||||||
const conferenceRank =
|
const conferenceRank =
|
||||||
playoffSeedStat?.value != null
|
playoffSeedStat !== undefined && playoffSeedStat.value !== null && playoffSeedStat.value !== undefined
|
||||||
? Math.round(playoffSeedStat.value)
|
? Math.round(playoffSeedStat.value)
|
||||||
: playoffSeedStat?.displayValue
|
: playoffSeedStat?.displayValue
|
||||||
? parseInt(playoffSeedStat.displayValue, 10) || undefined
|
? parseInt(playoffSeedStat.displayValue, 10) || undefined
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ process.env.NODE_ENV = 'test';
|
||||||
// Mock Clerk
|
// Mock Clerk
|
||||||
vi.mock('@clerk/react-router/server', () => ({
|
vi.mock('@clerk/react-router/server', () => ({
|
||||||
getAuth: vi.fn(() => Promise.resolve({ userId: 'test-user-id' })),
|
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' })),
|
rootAuthLoader: vi.fn(() => Promise.resolve({ userId: 'test-user-id' })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ export async function importSportsDataFromJSON(
|
||||||
importData = ImportDataSchema.parse(JSON.parse(jsonData));
|
importData = ImportDataSchema.parse(JSON.parse(jsonData));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(
|
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.
|
// 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.
|
// 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 {
|
export function database(): never {
|
||||||
throw new Error("database() can only be called on the server");
|
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 type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
||||||
|
|
||||||
import * as schema from "./schema";
|
import type * as schema from "./schema";
|
||||||
|
|
||||||
export const DatabaseContext = new AsyncLocalStorage<
|
export const DatabaseContext = new AsyncLocalStorage<
|
||||||
PostgresJsDatabase<typeof schema>
|
PostgresJsDatabase<typeof schema>
|
||||||
|
|
|
||||||
369
package-lock.json
generated
369
package-lock.json
generated
|
|
@ -76,6 +76,7 @@
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"esbuild": "^0.25.11",
|
"esbuild": "^0.25.11",
|
||||||
"jsdom": "^27.0.1",
|
"jsdom": "^27.0.1",
|
||||||
|
"oxlint": "^1.56.0",
|
||||||
"shadcn": "^4.0.8",
|
"shadcn": "^4.0.8",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
|
|
@ -3227,6 +3228,329 @@
|
||||||
"@opentelemetry/api": "^1.1.0"
|
"@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": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
|
|
@ -12391,6 +12715,51 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@
|
||||||
"test:e2e": "cypress open",
|
"test:e2e": "cypress open",
|
||||||
"test:e2e:headless": "cypress run",
|
"test:e2e:headless": "cypress run",
|
||||||
"test:all": "npm run test:run && npm run test:e2e:headless",
|
"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": {
|
"dependencies": {
|
||||||
"@clerk/react-router": "^2.1.0",
|
"@clerk/react-router": "^2.1.0",
|
||||||
|
|
@ -91,6 +93,7 @@
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"esbuild": "^0.25.11",
|
"esbuild": "^0.25.11",
|
||||||
"jsdom": "^27.0.1",
|
"jsdom": "^27.0.1",
|
||||||
|
"oxlint": "^1.56.0",
|
||||||
"shadcn": "^4.0.8",
|
"shadcn": "^4.0.8",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ export default {
|
||||||
|
|
||||||
buildEnd: async (
|
buildEnd: async (
|
||||||
{
|
{
|
||||||
viteConfig: viteConfig,
|
viteConfig,
|
||||||
reactRouterConfig: reactRouterConfig,
|
reactRouterConfig,
|
||||||
buildManifest: buildManifest
|
buildManifest
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
await sentryOnBuildEnd({
|
await sentryOnBuildEnd({
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { createRequestHandler } from "@react-router/express";
|
||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
|
import type { ServerBuild } from "react-router";
|
||||||
import { RouterContextProvider } from "react-router";
|
import { RouterContextProvider } from "react-router";
|
||||||
|
|
||||||
import { DatabaseContext } from "~/database/context";
|
import { DatabaseContext } from "~/database/context";
|
||||||
|
|
@ -18,11 +19,12 @@ app.use((_, __, next) => DatabaseContext.run(db, next));
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
createRequestHandler({
|
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() {
|
getLoadContext() {
|
||||||
const provider = new RouterContextProvider();
|
const provider = new RouterContextProvider();
|
||||||
provider.set(expressValueContext, "Hello from Express");
|
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 { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { eq, inArray, or } from "drizzle-orm";
|
import { eq, or } from "drizzle-orm";
|
||||||
import { createDailySnapshot } from "~/models/standings";
|
import { createDailySnapshot } from "~/models/standings";
|
||||||
|
|
||||||
// Create a dedicated database connection for the snapshot system
|
// 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 type { Server as HTTPServer } from "http";
|
||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import type { PostgresJsDatabase } 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
|
// Socket event types
|
||||||
interface ServerToClientEvents {
|
interface ServerToClientEvents {
|
||||||
"test-message": (data: {
|
"test-message": (data: {
|
||||||
originalMessage: any;
|
originalMessage: unknown;
|
||||||
serverResponse: string;
|
serverResponse: string;
|
||||||
serverTimestamp: string;
|
serverTimestamp: string;
|
||||||
socketId: string;
|
socketId: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
"pick-made": (data: any) => void;
|
"pick-made": (data: unknown) => void;
|
||||||
"draft-started": (data: { seasonId: string; currentPickNumber: number }) => void;
|
"draft-started": (data: { seasonId: string; currentPickNumber: number }) => void;
|
||||||
"draft-completed": () => void;
|
"draft-completed": () => void;
|
||||||
"timer-update": (data: {
|
"timer-update": (data: {
|
||||||
|
|
@ -58,9 +59,9 @@ interface ServerToClientEvents {
|
||||||
round: number;
|
round: number;
|
||||||
pickInRound: number;
|
pickInRound: number;
|
||||||
timeUsed: number;
|
timeUsed: number;
|
||||||
team: any;
|
team: unknown;
|
||||||
participant: any;
|
participant: unknown;
|
||||||
sport: any;
|
sport: unknown;
|
||||||
}>;
|
}>;
|
||||||
timers: Array<{
|
timers: Array<{
|
||||||
teamId: string;
|
teamId: string;
|
||||||
|
|
@ -79,7 +80,7 @@ interface ServerToClientEvents {
|
||||||
interface ClientToServerEvents {
|
interface ClientToServerEvents {
|
||||||
"join-draft": (seasonId: string, teamId?: string) => void;
|
"join-draft": (seasonId: string, teamId?: string) => void;
|
||||||
"leave-draft": (seasonId: string) => void;
|
"leave-draft": (seasonId: string) => void;
|
||||||
"test-event": (data: any) => void;
|
"test-event": (data: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global type augmentation
|
// 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);
|
console.log("📨 Received test-event from client:", socket.id, data);
|
||||||
|
|
||||||
socket.emit("test-message", {
|
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;
|
var __socketIO: SocketIOServer | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2023"],
|
||||||
"target": "ES2022",
|
"target": "ES2023",
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
"moduleResolution": "bundler"
|
"moduleResolution": "bundler"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"outDir": "./dist/server",
|
"outDir": "./dist/server",
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"target": "ES2022",
|
"target": "ES2023",
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2023"],
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client"],
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue