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:
Chris Parsons 2026-03-21 09:30:59 -07:00
parent bf0ddaa8aa
commit 2b9ffc0b62
32 changed files with 167 additions and 131 deletions

View file

@ -6,6 +6,13 @@ import {
ContextMenuTrigger,
} from "~/components/ui/context-menu";
interface DraftCell {
pickNumber: number;
participant: { id: string; name: string };
sport: { id: string; name: string };
team: { id: string; name: string };
}
interface DraftGridProps {
draftSlots: Array<{
id: string;
@ -15,7 +22,7 @@ interface DraftGridProps {
name: string;
};
}>;
draftGrid: any[][];
draftGrid: Array<Array<DraftCell | null>>;
currentPick: number;
teamTimers?: Record<string, number>;
formatTime?: (seconds: number | undefined) => string;

View file

@ -6,12 +6,12 @@ import { mockDraftSlots } from '~/test/fixtures/team';
describe('DraftGrid Component', () => {
const mockGrid = [
[
{ participant: { name: 'Player 1' }, sport: { name: 'NFL' } },
{ pickNumber: 1, participant: { id: 'p1', name: 'Player 1' }, sport: { id: 's1', name: 'NFL' }, team: { id: 't1', name: 'Team 1' } },
null,
],
[
null,
{ participant: { name: 'Player 2' }, sport: { name: 'NBA' } },
{ pickNumber: 4, participant: { id: 'p2', name: 'Player 2' }, sport: { id: 's2', name: 'NBA' }, team: { id: 't2', name: 'Team 2' } },
],
];
@ -143,12 +143,12 @@ describe('DraftGrid Component', () => {
it('should reverse picks on even rounds', () => {
const snakeGrid = [
[
{ participant: { name: 'Pick 1' }, sport: { name: 'NFL' } },
{ participant: { name: 'Pick 2' }, sport: { name: 'NBA' } },
{ pickNumber: 1, participant: { id: 'p1', name: 'Pick 1' }, sport: { id: 's1', name: 'NFL' }, team: { id: 't1', name: 'Team 1' } },
{ pickNumber: 2, participant: { id: 'p2', name: 'Pick 2' }, sport: { id: 's2', name: 'NBA' }, team: { id: 't2', name: 'Team 2' } },
],
[
{ participant: { name: 'Pick 4' }, sport: { name: 'MLB' } },
{ participant: { name: 'Pick 3' }, sport: { name: 'NHL' } },
{ pickNumber: 3, participant: { id: 'p3', name: 'Pick 4' }, sport: { id: 's1', name: 'MLB' }, team: { id: 't2', name: 'Team 2' } },
{ pickNumber: 4, participant: { id: 'p4', name: 'Pick 3' }, sport: { id: 's2', name: 'NHL' }, team: { id: 't1', name: 'Team 1' } },
],
];

View file

@ -26,7 +26,7 @@ interface StandingRow {
lastTen: string | null;
homeRecord: string | null;
awayRecord: string | null;
syncedAt: string | null;
syncedAt: Date | string | null;
participant: { id: string; name: string; shortName?: string | null };
}

View file

@ -9,9 +9,11 @@ interface UseDraftSocketReturn {
/** Increments each time a new socket instance is created. Include in
* useEffect deps so handler effects re-register on socket recreation. */
socketVersion: number;
// eslint-disable-next-line typescript/no-explicit-any -- socket.io callbacks are untyped at the hook level
on: (event: string, callback: (...args: any[]) => void) => void;
// eslint-disable-next-line typescript/no-explicit-any
off: (event: string, callback?: (...args: any[]) => void) => void;
emit: (event: string, ...args: any[]) => void;
emit: (event: string, ...args: unknown[]) => void;
}
export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocketReturn {
@ -131,15 +133,17 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke
// has run (socketRef.current === null) are silently no-ops. Consumers should
// only call them inside their own useEffect, not during render or synchronously
// after mount.
// eslint-disable-next-line typescript/no-explicit-any -- socket.io callbacks are untyped at the hook level
const on = useCallback((event: string, callback: (...args: any[]) => void) => {
socketRef.current?.on(event, callback);
}, []);
// eslint-disable-next-line typescript/no-explicit-any
const off = useCallback((event: string, callback?: (...args: any[]) => void) => {
socketRef.current?.off(event, callback);
}, []);
const emit = useCallback((event: string, ...args: any[]) => {
const emit = useCallback((event: string, ...args: unknown[]) => {
socketRef.current?.emit(event, ...args);
}, []);

View file

@ -3,6 +3,9 @@ import { database } from "~/database/context";
import * as schema from "~/database/schema";
export type ParticipantResult = typeof schema.participantResults.$inferSelect;
export type ParticipantResultWithParticipant = ParticipantResult & {
participant: { id: string; name: string } | null;
};
export type NewParticipantResult = typeof schema.participantResults.$inferInsert;
export async function createParticipantResult(
@ -62,7 +65,7 @@ export async function findParticipantResultByParticipantId(
export async function findParticipantResultsBySportsSeasonId(
sportsSeasonId: string
): Promise<ParticipantResult[]> {
): Promise<ParticipantResultWithParticipant[]> {
const db = database();
return await db.query.participantResults.findMany({
where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId),

View file

@ -157,7 +157,7 @@ export async function updateScoringEvent(
) {
const db = providedDb || database();
const updateData: any = { updatedAt: new Date() };
const updateData: Partial<typeof schema.scoringEvents.$inferInsert> = { updatedAt: new Date() };
if (data.name !== undefined) updateData.name = data.name;
if (data.eventDate !== undefined) {
@ -450,6 +450,7 @@ export async function getUpcomingEventsForDraftedParticipants(
inArray(schema.playoffMatches.participant1Id, draftedIds),
// participant2Id is nullable UUID — same underlying type as participant1Id;
// the notNull difference only exists at the TypeScript layer
// oxlint-disable-next-line typescript/no-explicit-any -- participant2Id is nullable UUID; notNull difference only exists at the TypeScript layer
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inArray(schema.playoffMatches.participant2Id as any, draftedIds)
),

View file

@ -402,7 +402,7 @@ export async function getSeasonPointProgression(
});
// Organize data by date
const dateMap = new Map<string, any>();
const dateMap = new Map<string, { date: string; [teamName: string]: string | number }>();
for (const snapshot of snapshots) {
const date = snapshot.snapshotDate;
@ -411,7 +411,7 @@ export async function getSeasonPointProgression(
dateMap.set(date, { date });
}
const dateData = dateMap.get(date);
const dateData = dateMap.get(date)!;
dateData[snapshot.team.name] = parseFloat(snapshot.totalPoints);
}

View file

@ -79,6 +79,8 @@ export async function loader({ params }: Route.LoaderArgs) {
participant2: { id: string; name: string } | null;
winner: { id: string; name: string } | null;
loser: { id: string; name: string } | null;
games: Array<{ id: string; gameNumber: number; scheduledAt: Date | null; status: string; winner?: { name: string } | null }>;
odds: Array<{ id: string; participantId: string; moneylineOdds: number; impliedProbability: string; oddsSource?: string | null; participant: { id: string; name: string } | null }>;
}>,
tournamentGroups,
};

View file

@ -151,8 +151,8 @@ export default function EventBracket({
const knockoutPopulated = useMemo(() => {
if (!isGroupStageEvent) return false;
// Check if any Round of 32 match has participants assigned
const r32Matches = matches.filter((m: any) => m.round === "Round of 32");
return r32Matches.some((m: any) => m.participant1Id || m.participant2Id);
const r32Matches = matches.filter((m) => m.round === "Round of 32");
return r32Matches.some((m) => m.participant1Id || m.participant2Id);
}, [isGroupStageEvent, matches]);
// Group stage stats
@ -161,7 +161,7 @@ export default function EventBracket({
let eliminated = 0;
let total = 0;
for (const group of tournamentGroups) {
for (const member of (group as any).members || []) {
for (const member of group.members || []) {
total++;
if (member.eliminated) eliminated++;
}
@ -174,7 +174,7 @@ export default function EventBracket({
if (!isGroupStageEvent) return [];
const advancing: Array<{ id: string; name: string }> = [];
for (const group of tournamentGroups) {
for (const member of (group as any).members || []) {
for (const member of group.members || []) {
if (!member.eliminated && member.participant) {
advancing.push({
id: member.participant.id,
@ -258,7 +258,7 @@ export default function EventBracket({
const getPendingWinnersInRound = (round: string) => {
return Object.entries(selectedWinners)
.filter(([matchId]) => {
const match = matches.find((m: any) => m.id === matchId);
const match = matches.find((m) => m.id === matchId);
return match && match.round === round && !match.isComplete;
});
};
@ -283,7 +283,7 @@ export default function EventBracket({
// Check if all matches are complete
const allMatchesComplete = useMemo(() => {
return matches.length > 0 && matches.every((m: any) => m.isComplete);
return matches.length > 0 && matches.every((m) => m.isComplete);
}, [matches]);
// No bracket and no groups yet = setup phase
@ -685,13 +685,13 @@ export default function EventBracket({
{/* Group Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{tournamentGroups.map((group: any) => (
{tournamentGroups.map((group) => (
<Card key={group.id}>
<CardHeader className="py-3 px-4">
<CardTitle className="text-base">Group {group.groupName}</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2">
{(group.members || []).map((member: any) => (
{(group.members || []).map((member) => (
<div
key={member.id}
className={`flex items-center justify-between gap-2 p-2 rounded-md border ${
@ -929,9 +929,9 @@ export default function EventBracket({
</div>
{/* Existing games */}
{(match as any).games?.length > 0 ? (
{match.games?.length > 0 ? (
<div className="space-y-2 mb-3">
{(match as any).games.map((game: any) => (
{match.games.map((game) => (
<div key={game.id} className="flex items-center gap-2 text-sm bg-background rounded p-2 border">
<span className="font-medium w-12 shrink-0">Game {game.gameNumber}</span>
<span className="text-muted-foreground flex-1">
@ -987,7 +987,7 @@ export default function EventBracket({
type="number"
min={1}
max={9}
defaultValue={((match as any).games?.length ?? 0) + 1}
defaultValue={(match.games?.length ?? 0) + 1}
className="h-8 w-20"
required
/>
@ -1014,9 +1014,9 @@ export default function EventBracket({
<span className="text-sm font-semibold">Moneyline Odds</span>
</div>
{(match as any).odds?.length > 0 ? (
{match.odds?.length > 0 ? (
<div className="space-y-2 mb-3">
{(match as any).odds.map((odd: any) => (
{match.odds.map((odd) => (
<div key={odd.id} className="flex items-center gap-2 text-sm bg-background rounded p-2 border">
<span className="flex-1 font-medium">{odd.participant?.name}</span>
<span className={`font-mono ${odd.moneylineOdds > 0 ? "text-emerald-500" : "text-muted-foreground"}`}>

View file

@ -695,12 +695,12 @@ export default function EventResults({
</TableHeader>
<TableBody>
{[...participantResults]
.toSorted((a: any, b: any) => {
.toSorted((a: { finalPosition?: number | null }, b: { finalPosition?: number | null }) => {
const posA = a.finalPosition ?? 999;
const posB = b.finalPosition ?? 999;
return posA - posB;
})
.map((result: any) => (
.map((result) => (
<TableRow key={result.id}>
<TableCell>
<div className="flex items-center gap-2">
@ -730,7 +730,7 @@ export default function EventResults({
</span>
</div>
</TableCell>
<TableCell>{result.participant.name}</TableCell>
<TableCell>{result.participant?.name}</TableCell>
<TableCell className="text-right font-semibold">
{result.qualifyingPoints ? (
<span className={result.qualifyingPoints === "0.00" ? "text-muted-foreground" : "text-emerald-400"}>

View file

@ -71,7 +71,7 @@ export default function SportsSeasonEvents({
if (eventType === "schedule_event") {
const isPast = eventStartsAt
? new Date(eventStartsAt) < new Date()
: eventDate !== null && eventDate < new Date().toISOString().split("T")[0];
: eventDate !== null && eventDate !== undefined && eventDate < new Date().toISOString().split("T")[0];
return isPast ? (
<Badge variant="outline" className="text-muted-foreground">Results Pending</Badge>
) : (

View file

@ -3,7 +3,7 @@ import { getAuth } from "@clerk/react-router/server";
import { isUserAdminByClerkId } from "~/models/user";
import type { Route } from "./+types/admin.sports-seasons.$id";
import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason } from "~/models/sports-season";
import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason, type NewSportsSeason } from "~/models/sports-season";
import { processSeasonStandings, recalculateStandings } from "~/models/scoring-calculator";
import { createDailySnapshot } from "~/models/standings";
import { database } from "~/database/context";
@ -252,7 +252,7 @@ export async function action(args: Route.ActionArgs) {
}
try {
const updateData: any = {
const updateData: Partial<NewSportsSeason> = {
name: name.trim(),
year: yearNum,
startDate: typeof startDate === "string" && startDate ? startDate : null,
@ -263,7 +263,7 @@ export async function action(args: Route.ActionArgs) {
};
if (scoringPattern && typeof scoringPattern === "string") {
updateData.scoringPattern = scoringPattern;
updateData.scoringPattern = scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points";
}
if (totalMajors && typeof totalMajors === "string") {

View file

@ -1,7 +1,7 @@
import { Form, Link, redirect } from "react-router";
import type { Route } from "./+types/admin.sports-seasons.new";
import { createSportsSeason } from "~/models/sports-season";
import { createSportsSeason, type NewSportsSeason } from "~/models/sports-season";
import { findAllSports } from "~/models/sport";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
@ -75,7 +75,7 @@ export async function action({ request }: Route.ActionArgs) {
}
try {
const seasonData: any = {
const seasonData: Partial<NewSportsSeason> = {
sportId,
name: name.trim(),
year: yearNum,
@ -86,7 +86,7 @@ export async function action({ request }: Route.ActionArgs) {
};
if (scoringPattern && typeof scoringPattern === "string") {
seasonData.scoringPattern = scoringPattern;
seasonData.scoringPattern = scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points";
}
if (totalMajors && typeof totalMajors === "string") {
@ -96,7 +96,7 @@ export async function action({ request }: Route.ActionArgs) {
}
}
await createSportsSeason(seasonData);
await createSportsSeason(seasonData as NewSportsSeason);
return redirect("/admin/sports-seasons");
} catch (error) {

View file

@ -26,7 +26,7 @@ export async function action({ request }: Route.ActionArgs) {
// Create a new Svix instance with your webhook secret
const wh = new Webhook(WEBHOOK_SECRET);
let evt: any;
let evt: { type: string; data: { id: string; email_addresses?: Array<{ email_address: string }>; username?: string; first_name?: string; last_name?: string; image_url?: string } };
// Verify the webhook
try {
@ -34,7 +34,7 @@ export async function action({ request }: Route.ActionArgs) {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
});
}) as typeof evt;
} catch (err) {
console.error("Error verifying webhook:", err);
return new Response("Error: Verification failed", { status: 400 });
@ -53,7 +53,7 @@ export async function action({ request }: Route.ActionArgs) {
const user = await findOrCreateUser({
id,
emailAddresses:
email_addresses?.map((e: any) => ({
email_addresses?.map((e: { email_address: string }) => ({
emailAddress: e.email_address,
})) || [],
username,
@ -69,7 +69,7 @@ export async function action({ request }: Route.ActionArgs) {
const user = await findOrCreateUser({
id,
emailAddresses:
email_addresses?.map((e: any) => ({
email_addresses?.map((e: { email_address: string }) => ({
emailAddress: e.email_address,
})) || [],
username,

View file

@ -127,9 +127,11 @@ export default function DraftBoard() {
useEffect(() => {
if (season.status !== "draft") return;
const handlePickMade = (data: any) => {
setPicks((prev: any) => [...prev, data.pick]);
setCurrentPick(data.nextPickNumber);
type PickShape = (typeof initialPicks)[number];
const handlePickMade = (data: unknown) => {
const pickData = data as { pick: PickShape; nextPickNumber: number };
setPicks((prev) => [...prev, pickData.pick]);
setCurrentPick(pickData.nextPickNumber);
};
on("pick-made", handlePickMade);
@ -142,13 +144,14 @@ export default function DraftBoard() {
// Generate draft grid
const totalTeams = draftSlots.length;
const totalRounds = season.draftRounds || 1;
const draftGrid: any[][] = [];
type PickItem = (typeof initialPicks)[number];
const draftGrid: Array<Array<PickItem | null>> = [];
for (let round = 0; round < totalRounds; round++) {
const roundPicks: any[] = [];
const roundPicks: Array<PickItem | null> = [];
for (let teamIndex = 0; teamIndex < totalTeams; teamIndex++) {
const pickNumber = round * totalTeams + teamIndex + 1;
const pick = picks.find((p: any) => p.pickNumber === pickNumber);
const pick = picks.find((p) => p.pickNumber === pickNumber);
roundPicks.push(pick || null);
}
draftGrid.push(roundPicks);

View file

@ -79,7 +79,7 @@ export async function loader(args: Route.LoaderArgs) {
// Resolve user's team and commissioner status (null userId means neither)
const userTeam = userId
? season.teams.find((team: any) => team.ownerId === userId)
? season.teams.find((team) => team.ownerId === userId)
: undefined;
const isCommissioner = userId
@ -427,7 +427,7 @@ export default function DraftRoom() {
// (deduplicated by ID) after the DB snapshot lands.
const revalidatorStateRef = useRef(revalidatorState);
const isRevalidatingRef = useRef(false);
const pendingPicksDuringRevalidationRef = useRef<any[]>([]);
const pendingPicksDuringRevalidationRef = useRef<(typeof draftPicks)[number][]>([]);
// Track the draftPicks reference when revalidation starts. If it hasn't
// changed by the time revalidation completes, the loader didn't return new
// data (e.g. network failure) and we should NOT overwrite state that may
@ -467,9 +467,9 @@ export default function DraftRoom() {
return;
}
const dbPickIds = new Set(draftPicks.map((p: any) => p.id));
const dbPickIds = new Set(draftPicks.map((p) => p.id));
const missedPicks = pendingPicksDuringRevalidationRef.current.filter(
(p: any) => !dbPickIds.has(p.id)
(p) => !dbPickIds.has(p.id)
);
pendingPicksDuringRevalidationRef.current = [];
@ -496,7 +496,7 @@ export default function DraftRoom() {
if (timers.length > 0) {
setTeamTimers((prev) => {
const updated = { ...prev };
timers.forEach((timer: any) => {
timers.forEach((timer) => {
updated[timer.teamId] = timer.timeRemaining;
});
return updated;
@ -506,7 +506,7 @@ export default function DraftRoom() {
// we were away.
setAutodraftStatus(() => {
const status: Record<string, AutodraftStatusEntry> = {};
autodraftSettings.forEach((setting: any) => {
autodraftSettings.forEach((setting) => {
status[setting.teamId] = {
isEnabled: setting.isEnabled,
mode: setting.mode,
@ -547,7 +547,7 @@ export default function DraftRoom() {
// Track autodraft status for all teams
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, AutodraftStatusEntry>>(() => {
const status: Record<string, AutodraftStatusEntry> = {};
autodraftSettings.forEach((setting: any) => {
autodraftSettings.forEach((setting) => {
status[setting.teamId] = {
isEnabled: setting.isEnabled,
mode: setting.mode,
@ -567,7 +567,7 @@ export default function DraftRoom() {
if (timers.length > 0) {
// Draft has started, use actual timers
timers.forEach((timer: any) => {
timers.forEach((timer) => {
timersMap[timer.teamId] = timer.timeRemaining;
});
} else {
@ -610,7 +610,7 @@ export default function DraftRoom() {
// Shared transforms for eligibility calculations
const transformedPicks = useMemo(
() =>
picks.map((p: any) => ({
picks.map((p) => ({
teamId: p.team.id,
participant: {
id: p.participant.id,
@ -622,7 +622,7 @@ export default function DraftRoom() {
const transformedParticipants = useMemo(
() =>
availableParticipants.map((p: any) => ({
availableParticipants.map((p) => ({
id: p.id,
sport: { id: p.sport.id, name: p.sport.name },
})),
@ -645,7 +645,7 @@ export default function DraftRoom() {
const eligibility = useMemo(() => {
if (!userTeam) return null;
const userTeamPicks = transformedPicks.filter(
(p: any) => p.teamId === userTeam.id
(p) => p.teamId === userTeam.id
);
return calculateDraftEligibility(
userTeam.id,
@ -662,7 +662,7 @@ export default function DraftRoom() {
const forcePickEligibility = useMemo(() => {
if (!selectedPickSlot) return null;
const selectedTeamPicks = transformedPicks.filter(
(p: any) => p.teamId === selectedPickSlot.teamId
(p) => p.teamId === selectedPickSlot.teamId
);
return calculateDraftEligibility(
selectedPickSlot.teamId,
@ -679,10 +679,10 @@ export default function DraftRoom() {
const replacePickEligibility = useMemo(() => {
if (!replacePickSlot) return null;
const allPicksExcluding = transformedPicks.filter(
(p: any) => p.participant.id !== replacePickSlot.oldParticipantId
(p) => p.participant.id !== replacePickSlot.oldParticipantId
);
const selectedTeamPicks = allPicksExcluding.filter(
(p: any) => p.teamId === replacePickSlot.teamId
(p) => p.teamId === replacePickSlot.teamId
);
return calculateDraftEligibility(
replacePickSlot.teamId,
@ -697,7 +697,8 @@ export default function DraftRoom() {
// Listen for new picks from other users
useEffect(() => {
const handlePickMade = (data: any) => {
type PickMadePayload = { pick: (typeof draftPicks)[number] & { team?: { id?: string; name?: string }; participant?: { name?: string } }; nextPickNumber: number; isDraftComplete?: boolean };
const handlePickMade = (data: PickMadePayload) => {
if (isRevalidatingRef.current) {
// Buffer this pick; the revalidation-complete handler will merge it
// into the DB snapshot so it isn't silently dropped.
@ -706,7 +707,7 @@ export default function DraftRoom() {
// pick counter and the picks list in sync with each other.
pendingPicksDuringRevalidationRef.current.push(data.pick);
} else {
setPicks((prev: any) => [...prev, data.pick]);
setPicks((prev) => [...prev, data.pick]);
setCurrentPick(data.nextPickNumber);
}
@ -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.
// 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.
@ -819,35 +820,35 @@ export default function DraftRoom() {
};
const handleParticipantRemovedFromQueues = (data: { participantId: string }) => {
setQueue((prev: any) =>
prev.filter((item: any) => item.participantId !== data.participantId)
setQueue((prev) =>
prev.filter((item) => item.participantId !== data.participantId)
);
};
const handleQueueEligibilityPruned = (data: { teamId: string; removedParticipantIds: string[] }) => {
if (data.teamId !== userTeam?.id) return;
const removed = new Set(data.removedParticipantIds);
setQueue((prev: any) => prev.filter((item: any) => !removed.has(item.participantId)));
setQueue((prev) => prev.filter((item) => !removed.has(item.participantId)));
};
// Fired when another window/session changes the user's queue (add/remove/reorder/clear).
// Only received by this team's own sockets (emitted to team-${teamId} private room).
// Ignored while this window has in-flight mutations to prevent the server echo
// from clobbering a more-recent optimistic update.
const handleQueueUpdated = (data: { queue: any[] }) => {
const handleQueueUpdated = (data: { queue: QueueItem[] }) => {
if (pendingQueueMutationsRef.current > 0) return;
setQueue(data.queue);
};
const handlePickReplaced = (data: any) => {
setPicks((prev: any) =>
prev.map((p: any) => (p.pickNumber === data.pickNumber ? data.pick : p))
const handlePickReplaced = (data: { pickNumber: number; pick: (typeof draftPicks)[number] }) => {
setPicks((prev) =>
prev.map((p) => (p.pickNumber === data.pickNumber ? data.pick : p))
);
};
const handleDraftRolledBack = (data: any) => {
setPicks((prev: any) =>
prev.filter((p: any) => p.pickNumber < data.pickNumber)
const handleDraftRolledBack = (data: { pickNumber: number }) => {
setPicks((prev) =>
prev.filter((p) => p.pickNumber < data.pickNumber)
);
setCurrentPick(data.pickNumber);
setIsDraftComplete(false);
@ -857,7 +858,7 @@ export default function DraftRoom() {
// Full state sync sent by the server on join-draft. Provides an immediate,
// socket-based path to bring the client up to date — critical for mobile
// reconnection where the HTTP revalidation may be delayed or fail entirely.
const handleDraftStateSync = (data: any) => {
const handleDraftStateSync = (data: { picks: (typeof draftPicks)[number][]; currentPickNumber: number; isPaused: boolean; status: string; timers?: Array<{ teamId: string; timeRemaining: number }>; queue?: QueueItem[] }) => {
// If a revalidation is in-flight, let it complete and handle the merge.
// The revalidation path already buffers picks and deduplicates — layering
// this on top would risk conflicts. But if NOT revalidating, apply the
@ -872,7 +873,7 @@ export default function DraftRoom() {
if (data.timers) {
setTeamTimers((prev) => {
const updated = { ...prev };
data.timers.forEach((t: any) => {
data.timers?.forEach((t) => {
updated[t.teamId] = t.timeRemaining;
});
return updated;
@ -1214,7 +1215,7 @@ export default function DraftRoom() {
};
const handleReplacePickOpen = useCallback((pickNumber: number, teamId: string) => {
const pick = picks.find((p: any) => p.pickNumber === pickNumber);
const pick = picks.find((p) => p.pickNumber === pickNumber);
if (!pick) {
toast.error("Pick not found — try refreshing the page");
return;
@ -1387,7 +1388,7 @@ export default function DraftRoom() {
round: number;
pickInRound: number;
teamId: string;
pick?: any;
pick?: (typeof draftPicks)[number];
}>
> = [];
@ -1401,7 +1402,7 @@ export default function DraftRoom() {
const pickNumber = (round - 1) * teamCount + pickInRound;
const teamId = draftSlots[teamIndex]?.team.id;
const pick = picks.find((p: any) => p.pickNumber === pickNumber);
const pick = picks.find((p) => p.pickNumber === pickNumber);
roundPicks.push({ pickNumber, round, pickInRound, teamId, pick });
}
@ -1413,7 +1414,7 @@ export default function DraftRoom() {
// Get drafted participant IDs for filtering
const draftedParticipantIds = useMemo(
() => new Set(picks.map((p: any) => p.participant.id)),
() => new Set(picks.map((p) => p.participant.id)),
[picks]
);
@ -1422,8 +1423,8 @@ export default function DraftRoom() {
if (!userTeam) return new Set<string>();
return new Set(
picks
.filter((p: any) => p.team.id === userTeam.id)
.map((p: any) => p.sport.id)
.filter((p) => p.team.id === userTeam.id)
.map((p) => p.sport.id)
);
}, [picks, userTeam]);
@ -1432,8 +1433,8 @@ export default function DraftRoom() {
if (!userTeam) return new Set<string>();
return new Set(
picks
.filter((p: any) => p.team.id === userTeam.id)
.map((p: any) => p.sport.name)
.filter((p) => p.team.id === userTeam.id)
.map((p) => p.sport.name)
);
}, [picks, userTeam]);
@ -1441,7 +1442,7 @@ export default function DraftRoom() {
const uniqueSports = useMemo(
() =>
Array.from(
new Set(availableParticipants.map((p: any) => p.sport.name))
new Set(availableParticipants.map((p) => p.sport.name))
).sort(),
[availableParticipants]
);
@ -1449,7 +1450,7 @@ export default function DraftRoom() {
// Filter participants based on search, sport, drafted status, and eligibility
const filteredParticipants = useMemo(() => {
const sportFilterSet = new Set(sportFilters);
return availableParticipants.filter((participant: any) => {
return availableParticipants.filter((participant) => {
// Drafted filter - hide drafted participants by default
if (hideDrafted && draftedParticipantIds.has(participant.id)) {
return false;
@ -1958,7 +1959,7 @@ export default function DraftRoom() {
<DialogTitle>Roll Back Draft</DialogTitle>
<DialogDescription>
{rollbackPickNumber !== null && (() => {
const count = picks.filter((p: any) => p.pickNumber >= rollbackPickNumber).length;
const count = picks.filter((p) => p.pickNumber >= rollbackPickNumber).length;
return `This will erase ${count} pick${count === 1 ? "" : "s"} (pick #${rollbackPickNumber} through the most recent) and return the draft to pick #${rollbackPickNumber}. This cannot be undone.`;
})()}
</DialogDescription>

View file

@ -13,7 +13,7 @@ import {
countCommissionersByLeagueId,
removeCommissionerByLeagueAndUser,
} from "~/models/commissioner";
import { findCurrentSeasonWithSports, updateSeason } from "~/models/season";
import { findCurrentSeasonWithSports, updateSeason, type NewSeason } from "~/models/season";
import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team";
import { findAllUsers, findUserByClerkId, findUsersByClerkIds, getUserDisplayName, isUserAdminByClerkId } from "~/models/user";
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
@ -281,7 +281,7 @@ export async function action(args: Route.ActionArgs) {
const selectedSports = formData.getAll("sportsSeasons");
const currentSportIds = new Set(
season.seasonSports?.map((s: any) => s.sportsSeason.id) || []
season.seasonSports?.map((s) => s.sportsSeason.id) || []
);
const newSportIds = new Set(selectedSports as string[]);
@ -465,7 +465,7 @@ export async function action(args: Route.ActionArgs) {
// Now handle season-specific updates.
try {
// Update season settings
const seasonUpdates: any = {};
const seasonUpdates: Partial<NewSeason> = {};
// Handle draft rounds
if (typeof draftRounds === "string") {
@ -514,7 +514,7 @@ export async function action(args: Route.ActionArgs) {
if (points < 0 || points > 1000) {
return { error: `${field} must be between 0 and 1000 points` };
}
seasonUpdates[field] = points;
(seasonUpdates as Record<string, unknown>)[field] = points;
}
}
}
@ -652,7 +652,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [timerMode, setTimerMode] = useState<"chess_clock" | "standard">(season?.draftTimerMode ?? "chess_clock");
const [selectedSports, setSelectedSports] = useState<Set<string>>(
new Set(season?.seasonSports?.map((s: any) => s.sportsSeason.id) || [])
new Set(season?.seasonSports?.map((s) => s.sportsSeason.id) || [])
);
const [draftOrderTeams, setDraftOrderTeams] = useState<string[]>(
draftSlots.length > 0

View file

@ -18,6 +18,7 @@ import {
import { getRegularSeasonStandings } from "~/models/regular-season-standings";
import { getAllParticipantEVsForSeason } from "~/models/participant-expected-value";
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
import type { PlayoffMatch } from "~/models/playoff-match";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, inArray } from "drizzle-orm";
@ -136,13 +137,20 @@ export async function loader(args: Route.LoaderArgs) {
type SeasonStanding = { id: string; position: number; championshipPoints: string; participant: { id: string; name: string } };
let playoffMatches: any[] = [];
type PlayoffMatchWithRelations = PlayoffMatch & {
participant1: { id: string; name: string } | null;
participant2: { id: string; name: string } | null;
winner: { id: string; name: string } | null;
loser: { id: string; name: string } | null;
};
let playoffMatches: PlayoffMatchWithRelations[] = [];
let playoffRounds: string[] = [];
let preEliminatedParticipants: { id: string; name: string }[] = [];
let participantPoints: { participantId: string; points: number }[] = [];
let partialScoreParticipantIds: string[] = [];
let seasonStandings: SeasonStanding[] = [];
let qpStandings: any[] = [];
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number];
let qpStandings: QPStanding[] = [];
if (scoringPattern === "playoff_bracket") {
// Fetch playoff matches via scoring events
@ -164,7 +172,7 @@ export async function loader(args: Route.LoaderArgs) {
loser: true,
},
});
playoffMatches = matches;
playoffMatches = matches as PlayoffMatchWithRelations[];
templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId ?? undefined;
const template = templateId ? getBracketTemplate(templateId) : undefined;
@ -186,8 +194,8 @@ export async function loader(args: Route.LoaderArgs) {
]);
preEliminatedParticipants = eliminatedResults
.filter((r) => r.participant)
.map((r) => ({ id: (r as any).participant.id, name: (r as any).participant.name }));
.filter((r): r is typeof r & { participant: NonNullable<typeof r.participant> } => r.participant !== null && r.participant !== undefined)
.map((r) => ({ id: r.participant.id, name: r.participant.name }));
// Compute per-participant fantasy points directly from participant_results.
// This covers both finalized placements and progressive floor scores for still-alive participants.

View file

@ -69,7 +69,7 @@ export default function SportSeasonDetail({
const hasBracket = playoffMatches && playoffMatches.length > 0;
const hasStandings = regularSeasonStandings && regularSeasonStandings.length > 0;
const showOtLosses = hasStandings && regularSeasonStandings.some((s: any) => s.otLosses !== null);
const showOtLosses = hasStandings && regularSeasonStandings.some((s) => s.otLosses !== null);
const simulatorType = sportsSeason.sport?.simulatorType;
const standingsDisplayMode =
@ -80,7 +80,7 @@ export default function SportSeasonDetail({
// Build ownership map for RegularSeasonStandings
const ownershipMap = Object.fromEntries(
teamOwnerships.map((o: any) => [
teamOwnerships.map((o) => [
o.participantId,
{ teamName: o.teamName, ownerName: o.ownerName ?? "", teamId: o.teamId },
])
@ -113,12 +113,12 @@ export default function SportSeasonDetail({
{hasStandings && !hasBracket && (
<div className="mb-6">
<RegularSeasonStandings
standings={regularSeasonStandings as any}
standings={regularSeasonStandings}
teamOwnerships={ownershipMap}
userParticipantIds={userParticipantIds}
showOtLosses={showOtLosses}
participantEvs={participantEvs}
displayMode={standingsDisplayMode as any}
displayMode={standingsDisplayMode as "flat" | "nhl-divisions"}
playoffSpots={playoffSpots}
/>
</div>
@ -129,25 +129,25 @@ export default function SportSeasonDetail({
(upcomingEvents.length > 0 || recentEvents.length > 0) && (
<div className="mb-6">
<EventSchedule
upcomingEvents={upcomingEvents as any}
recentEvents={recentEvents as any}
upcomingEvents={upcomingEvents}
recentEvents={recentEvents}
/>
</div>
)}
{/* Hide bracket display when standings exist but no bracket matches have been set yet */}
{!(scoringPattern === "playoff_bracket" && hasStandings && !hasBracket) && <SportSeasonDisplay
scoringPattern={scoringPattern as any}
scoringPattern={scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points"}
sportSeasonName={sportsSeason.name}
sportName={sportsSeason.sport.name}
playoffMatches={playoffMatches as any}
playoffMatches={playoffMatches}
playoffRounds={playoffRounds}
preEliminatedParticipants={preEliminatedParticipants}
participantPoints={participantPoints}
partialScoreParticipantIds={partialScoreParticipantIds}
seasonStandings={seasonStandings as any}
seasonStandings={seasonStandings}
seasonIsFinalized={seasonIsFinalized}
qpStandings={qpStandings as any}
qpStandings={qpStandings}
qpIsFinalized={sportsSeason.qualifyingPointsFinalized || false}
totalMajors={sportsSeason.totalMajors}
majorsCompleted={sportsSeason.majorsCompleted || 0}
@ -166,12 +166,12 @@ export default function SportSeasonDetail({
{hasStandings && hasBracket && (
<div className="mt-8">
<RegularSeasonStandings
standings={regularSeasonStandings as any}
standings={regularSeasonStandings}
teamOwnerships={ownershipMap}
userParticipantIds={userParticipantIds}
showOtLosses={showOtLosses}
participantEvs={participantEvs}
displayMode={standingsDisplayMode as any}
displayMode={standingsDisplayMode as "flat" | "nhl-divisions"}
playoffSpots={playoffSpots}
/>
</div>

View file

@ -57,7 +57,7 @@ export async function loader() {
return {
...template,
sportsSeasonIds: templateWithSports?.seasonTemplateSports?.map(
(ts: any) => ts.sportsSeason.id
(ts: { sportsSeason: { id: string } }) => ts.sportsSeason.id
) || []
};
})

View file

@ -11,7 +11,7 @@ export default function TestSocket() {
useEffect(() => {
// Listen for test messages
const handleTestMessage = (data: any) => {
const handleTestMessage = (data: unknown) => {
console.log("Received test message:", data);
setMessages((prev) => [...prev, JSON.stringify(data)]);
};

View file

@ -37,6 +37,7 @@ describe("probability-updater", () => {
notes: null,
createdAt: new Date(),
updatedAt: new Date(),
participant: null,
},
]);
@ -108,6 +109,7 @@ describe("probability-updater", () => {
notes: null,
createdAt: new Date(),
updatedAt: new Date(),
participant: null,
},
{
id: "result-2",
@ -119,6 +121,7 @@ describe("probability-updater", () => {
notes: null,
createdAt: new Date(),
updatedAt: new Date(),
participant: null,
},
]);
@ -195,6 +198,7 @@ describe("probability-updater", () => {
notes: null,
createdAt: new Date(),
updatedAt: new Date(),
participant: null,
},
]);
@ -219,6 +223,7 @@ describe("probability-updater", () => {
notes: null,
createdAt: new Date(),
updatedAt: new Date(),
participant: null,
},
]);

View file

@ -140,7 +140,7 @@ export class AutoRacingSimulator implements Simulator {
for (const p of participants) {
const ev = evMap.get(p.id);
rawProbs.set(p.id, ev?.sourceOdds !== null ? americanToImpliedProb(ev.sourceOdds) : fallbackProb);
rawProbs.set(p.id, ev !== undefined && ev.sourceOdds !== null && ev.sourceOdds !== undefined ? americanToImpliedProb(ev.sourceOdds) : fallbackProb);
}
// Normalize to remove vig

View file

@ -178,7 +178,7 @@ export class UCLSimulator implements Simulator {
const ev = evMap.get(id);
rawProbs.set(
id,
ev?.sourceOdds !== null ? americanToImpliedProb(ev.sourceOdds) : fallbackProb
ev !== undefined && ev.sourceOdds !== null && ev.sourceOdds !== undefined ? americanToImpliedProb(ev.sourceOdds) : fallbackProb
);
}
const rawSum = [...rawProbs.values()].reduce((a, b) => a + b, 0);

View file

@ -122,7 +122,7 @@ export class NbaStandingsAdapter implements StandingsSyncAdapter {
// Seed within conference — ESPN key is "playoffSeed", value is a float (1.0, 2.0 ...)
const playoffSeedStat = sm.get("playoffSeed");
const conferenceRank =
playoffSeedStat?.value !== null
playoffSeedStat !== undefined && playoffSeedStat.value !== null && playoffSeedStat.value !== undefined
? Math.round(playoffSeedStat.value)
: playoffSeedStat?.displayValue
? parseInt(playoffSeedStat.displayValue, 10) || undefined

View file

@ -13,7 +13,7 @@ process.env.NODE_ENV = 'test';
// Mock Clerk
vi.mock('@clerk/react-router/server', () => ({
getAuth: vi.fn(() => Promise.resolve({ userId: 'test-user-id' })),
clerkMiddleware: vi.fn(() => (req: any, res: any, next: any) => next()),
clerkMiddleware: vi.fn(() => (req: unknown, res: unknown, next: () => void) => next()),
rootAuthLoader: vi.fn(() => Promise.resolve({ userId: 'test-user-id' })),
}));

View file

@ -2,7 +2,7 @@
// The real implementation uses Node.js AsyncLocalStorage which is server-only.
// This stub is used during the client build to avoid bundling server-only code.
export const DatabaseContext = null as any;
export const DatabaseContext = null as unknown;
export function database(): never {
throw new Error("database() can only be called on the server");

View file

@ -2,6 +2,7 @@ import { createRequestHandler } from "@react-router/express";
import { drizzle } from "drizzle-orm/postgres-js";
import express from "express";
import postgres from "postgres";
import type { ServerBuild } from "react-router";
import { RouterContextProvider } from "react-router";
import { DatabaseContext } from "~/database/context";
@ -18,11 +19,12 @@ app.use((_, __, next) => DatabaseContext.run(db, next));
app.use(
createRequestHandler({
build: () => import("virtual:react-router/server-build") as any,
build: () => import("virtual:react-router/server-build") as unknown as Promise<ServerBuild>,
// @ts-ignore -- RouterContextProvider is the correct runtime type but tsconfig.server.json can't resolve the conditional type statically
getLoadContext() {
const provider = new RouterContextProvider();
provider.set(expressValueContext, "Hello from Express");
return provider as any; // Type assertion needed - RouterContextProvider is the context
return provider as unknown as RouterContextProvider;
},
}),
);

View file

@ -21,12 +21,12 @@ function getSocketDb(): PostgresJsDatabase<typeof schema> {
// Socket event types
interface ServerToClientEvents {
"test-message": (data: {
originalMessage: any;
originalMessage: unknown;
serverResponse: string;
serverTimestamp: string;
socketId: string;
}) => void;
"pick-made": (data: any) => void;
"pick-made": (data: unknown) => void;
"draft-started": (data: { seasonId: string; currentPickNumber: number }) => void;
"draft-completed": () => void;
"timer-update": (data: {
@ -59,9 +59,9 @@ interface ServerToClientEvents {
round: number;
pickInRound: number;
timeUsed: number;
team: any;
participant: any;
sport: any;
team: unknown;
participant: unknown;
sport: unknown;
}>;
timers: Array<{
teamId: string;
@ -80,7 +80,7 @@ interface ServerToClientEvents {
interface ClientToServerEvents {
"join-draft": (seasonId: string, teamId?: string) => void;
"leave-draft": (seasonId: string) => void;
"test-event": (data: any) => void;
"test-event": (data: unknown) => void;
}
// Global type augmentation
@ -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);
socket.emit("test-message", {

View file

@ -17,8 +17,8 @@
"composite": true,
"strict": true,
"types": ["node"],
"lib": ["ES2022"],
"target": "ES2022",
"lib": ["ES2023"],
"target": "ES2023",
"module": "ES2022",
"moduleResolution": "bundler"
}

View file

@ -5,10 +5,10 @@
"noEmit": false,
"outDir": "./dist/server",
"rootDir": ".",
"target": "ES2022",
"target": "ES2023",
"module": "ES2022",
"moduleResolution": "node",
"lib": ["ES2022"],
"lib": ["ES2023"],
"types": ["node"],
"strict": true,
"esModuleInterop": true,

View file

@ -14,7 +14,7 @@
"compilerOptions": {
"composite": true,
"strict": true,
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"lib": ["DOM", "DOM.Iterable", "ES2023"],
"types": ["vite/client"],
"target": "ES2022",
"module": "ES2022",