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>
This commit is contained in:
parent
bf0ddaa8aa
commit
2b9ffc0b62
32 changed files with 167 additions and 131 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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' } },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,7 +411,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -685,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 ${
|
||||||
|
|
@ -929,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">
|
||||||
|
|
@ -987,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
|
||||||
/>
|
/>
|
||||||
|
|
@ -1014,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"}`}>
|
||||||
|
|
|
||||||
|
|
@ -695,12 +695,12 @@ export default function EventResults({
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{[...participantResults]
|
{[...participantResults]
|
||||||
.toSorted((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"}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -3,7 +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 { 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";
|
||||||
|
|
@ -252,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,
|
||||||
|
|
@ -263,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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -79,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
|
||||||
|
|
@ -427,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
|
||||||
|
|
@ -467,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 = [];
|
||||||
|
|
||||||
|
|
@ -496,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;
|
||||||
|
|
@ -506,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,
|
||||||
|
|
@ -547,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,
|
||||||
|
|
@ -567,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 {
|
||||||
|
|
@ -610,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,
|
||||||
|
|
@ -622,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 },
|
||||||
})),
|
})),
|
||||||
|
|
@ -645,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,
|
||||||
|
|
@ -662,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,
|
||||||
|
|
@ -679,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,
|
||||||
|
|
@ -697,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.
|
||||||
|
|
@ -706,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -735,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.
|
||||||
|
|
@ -819,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);
|
||||||
|
|
@ -857,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
|
||||||
|
|
@ -872,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;
|
||||||
|
|
@ -1214,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;
|
||||||
|
|
@ -1387,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];
|
||||||
}>
|
}>
|
||||||
> = [];
|
> = [];
|
||||||
|
|
||||||
|
|
@ -1401,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 });
|
||||||
}
|
}
|
||||||
|
|
@ -1413,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]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1422,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]);
|
||||||
|
|
||||||
|
|
@ -1432,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]);
|
||||||
|
|
||||||
|
|
@ -1441,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]
|
||||||
);
|
);
|
||||||
|
|
@ -1449,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;
|
||||||
|
|
@ -1958,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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)]);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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' })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,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;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,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: {
|
||||||
|
|
@ -59,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;
|
||||||
|
|
@ -80,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
|
||||||
|
|
@ -274,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", {
|
||||||
|
|
|
||||||
|
|
@ -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