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,
|
||||
} 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;
|
||||
|
|
|
|||
|
|
@ -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' } },
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"}`}>
|
||||
|
|
|
|||
|
|
@ -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"}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
countCommissionersByLeagueId,
|
||||
removeCommissionerByLeagueAndUser,
|
||||
} from "~/models/commissioner";
|
||||
import { findCurrentSeasonWithSports, updateSeason } from "~/models/season";
|
||||
import { findCurrentSeasonWithSports, updateSeason, type NewSeason } from "~/models/season";
|
||||
import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team";
|
||||
import { findAllUsers, findUserByClerkId, findUsersByClerkIds, getUserDisplayName, isUserAdminByClerkId } from "~/models/user";
|
||||
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
||||
|
|
@ -281,7 +281,7 @@ export async function action(args: Route.ActionArgs) {
|
|||
|
||||
const selectedSports = formData.getAll("sportsSeasons");
|
||||
const currentSportIds = new Set(
|
||||
season.seasonSports?.map((s: any) => s.sportsSeason.id) || []
|
||||
season.seasonSports?.map((s) => s.sportsSeason.id) || []
|
||||
);
|
||||
const newSportIds = new Set(selectedSports as string[]);
|
||||
|
||||
|
|
@ -465,7 +465,7 @@ export async function action(args: Route.ActionArgs) {
|
|||
// Now handle season-specific updates.
|
||||
try {
|
||||
// Update season settings
|
||||
const seasonUpdates: any = {};
|
||||
const seasonUpdates: Partial<NewSeason> = {};
|
||||
|
||||
// Handle draft rounds
|
||||
if (typeof draftRounds === "string") {
|
||||
|
|
@ -514,7 +514,7 @@ export async function action(args: Route.ActionArgs) {
|
|||
if (points < 0 || points > 1000) {
|
||||
return { error: `${field} must be between 0 and 1000 points` };
|
||||
}
|
||||
seasonUpdates[field] = points;
|
||||
(seasonUpdates as Record<string, unknown>)[field] = points;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -652,7 +652,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
|||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [timerMode, setTimerMode] = useState<"chess_clock" | "standard">(season?.draftTimerMode ?? "chess_clock");
|
||||
const [selectedSports, setSelectedSports] = useState<Set<string>>(
|
||||
new Set(season?.seasonSports?.map((s: any) => s.sportsSeason.id) || [])
|
||||
new Set(season?.seasonSports?.map((s) => s.sportsSeason.id) || [])
|
||||
);
|
||||
const [draftOrderTeams, setDraftOrderTeams] = useState<string[]>(
|
||||
draftSlots.length > 0
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { getRegularSeasonStandings } from "~/models/regular-season-standings";
|
||||
import { getAllParticipantEVsForSeason } from "~/models/participant-expected-value";
|
||||
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
|
||||
import type { PlayoffMatch } from "~/models/playoff-match";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
|
|
@ -136,13 +137,20 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
|
||||
type SeasonStanding = { id: string; position: number; championshipPoints: string; participant: { id: string; name: string } };
|
||||
|
||||
let playoffMatches: any[] = [];
|
||||
type PlayoffMatchWithRelations = PlayoffMatch & {
|
||||
participant1: { id: string; name: string } | null;
|
||||
participant2: { id: string; name: string } | null;
|
||||
winner: { id: string; name: string } | null;
|
||||
loser: { id: string; name: string } | null;
|
||||
};
|
||||
let playoffMatches: PlayoffMatchWithRelations[] = [];
|
||||
let playoffRounds: string[] = [];
|
||||
let preEliminatedParticipants: { id: string; name: string }[] = [];
|
||||
let participantPoints: { participantId: string; points: number }[] = [];
|
||||
let partialScoreParticipantIds: string[] = [];
|
||||
let seasonStandings: SeasonStanding[] = [];
|
||||
let qpStandings: any[] = [];
|
||||
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number];
|
||||
let qpStandings: QPStanding[] = [];
|
||||
|
||||
if (scoringPattern === "playoff_bracket") {
|
||||
// Fetch playoff matches via scoring events
|
||||
|
|
@ -164,7 +172,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
loser: true,
|
||||
},
|
||||
});
|
||||
playoffMatches = matches;
|
||||
playoffMatches = matches as PlayoffMatchWithRelations[];
|
||||
|
||||
templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId ?? undefined;
|
||||
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
||||
|
|
@ -186,8 +194,8 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
]);
|
||||
|
||||
preEliminatedParticipants = eliminatedResults
|
||||
.filter((r) => r.participant)
|
||||
.map((r) => ({ id: (r as any).participant.id, name: (r as any).participant.name }));
|
||||
.filter((r): r is typeof r & { participant: NonNullable<typeof r.participant> } => r.participant !== null && r.participant !== undefined)
|
||||
.map((r) => ({ id: r.participant.id, name: r.participant.name }));
|
||||
|
||||
// Compute per-participant fantasy points directly from participant_results.
|
||||
// This covers both finalized placements and progressive floor scores for still-alive participants.
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export default function SportSeasonDetail({
|
|||
|
||||
const hasBracket = playoffMatches && playoffMatches.length > 0;
|
||||
const hasStandings = regularSeasonStandings && regularSeasonStandings.length > 0;
|
||||
const showOtLosses = hasStandings && regularSeasonStandings.some((s: any) => s.otLosses !== null);
|
||||
const showOtLosses = hasStandings && regularSeasonStandings.some((s) => s.otLosses !== null);
|
||||
|
||||
const simulatorType = sportsSeason.sport?.simulatorType;
|
||||
const standingsDisplayMode =
|
||||
|
|
@ -80,7 +80,7 @@ export default function SportSeasonDetail({
|
|||
|
||||
// Build ownership map for RegularSeasonStandings
|
||||
const ownershipMap = Object.fromEntries(
|
||||
teamOwnerships.map((o: any) => [
|
||||
teamOwnerships.map((o) => [
|
||||
o.participantId,
|
||||
{ teamName: o.teamName, ownerName: o.ownerName ?? "", teamId: o.teamId },
|
||||
])
|
||||
|
|
@ -113,12 +113,12 @@ export default function SportSeasonDetail({
|
|||
{hasStandings && !hasBracket && (
|
||||
<div className="mb-6">
|
||||
<RegularSeasonStandings
|
||||
standings={regularSeasonStandings as any}
|
||||
standings={regularSeasonStandings}
|
||||
teamOwnerships={ownershipMap}
|
||||
userParticipantIds={userParticipantIds}
|
||||
showOtLosses={showOtLosses}
|
||||
participantEvs={participantEvs}
|
||||
displayMode={standingsDisplayMode as any}
|
||||
displayMode={standingsDisplayMode as "flat" | "nhl-divisions"}
|
||||
playoffSpots={playoffSpots}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -129,25 +129,25 @@ export default function SportSeasonDetail({
|
|||
(upcomingEvents.length > 0 || recentEvents.length > 0) && (
|
||||
<div className="mb-6">
|
||||
<EventSchedule
|
||||
upcomingEvents={upcomingEvents as any}
|
||||
recentEvents={recentEvents as any}
|
||||
upcomingEvents={upcomingEvents}
|
||||
recentEvents={recentEvents}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hide bracket display when standings exist but no bracket matches have been set yet */}
|
||||
{!(scoringPattern === "playoff_bracket" && hasStandings && !hasBracket) && <SportSeasonDisplay
|
||||
scoringPattern={scoringPattern as any}
|
||||
scoringPattern={scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points"}
|
||||
sportSeasonName={sportsSeason.name}
|
||||
sportName={sportsSeason.sport.name}
|
||||
playoffMatches={playoffMatches as any}
|
||||
playoffMatches={playoffMatches}
|
||||
playoffRounds={playoffRounds}
|
||||
preEliminatedParticipants={preEliminatedParticipants}
|
||||
participantPoints={participantPoints}
|
||||
partialScoreParticipantIds={partialScoreParticipantIds}
|
||||
seasonStandings={seasonStandings as any}
|
||||
seasonStandings={seasonStandings}
|
||||
seasonIsFinalized={seasonIsFinalized}
|
||||
qpStandings={qpStandings as any}
|
||||
qpStandings={qpStandings}
|
||||
qpIsFinalized={sportsSeason.qualifyingPointsFinalized || false}
|
||||
totalMajors={sportsSeason.totalMajors}
|
||||
majorsCompleted={sportsSeason.majorsCompleted || 0}
|
||||
|
|
@ -166,12 +166,12 @@ export default function SportSeasonDetail({
|
|||
{hasStandings && hasBracket && (
|
||||
<div className="mt-8">
|
||||
<RegularSeasonStandings
|
||||
standings={regularSeasonStandings as any}
|
||||
standings={regularSeasonStandings}
|
||||
teamOwnerships={ownershipMap}
|
||||
userParticipantIds={userParticipantIds}
|
||||
showOtLosses={showOtLosses}
|
||||
participantEvs={participantEvs}
|
||||
displayMode={standingsDisplayMode as any}
|
||||
displayMode={standingsDisplayMode as "flat" | "nhl-divisions"}
|
||||
playoffSpots={playoffSpots}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export async function loader() {
|
|||
return {
|
||||
...template,
|
||||
sportsSeasonIds: templateWithSports?.seasonTemplateSports?.map(
|
||||
(ts: any) => ts.sportsSeason.id
|
||||
(ts: { sportsSeason: { id: string } }) => ts.sportsSeason.id
|
||||
) || []
|
||||
};
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default function TestSocket() {
|
|||
|
||||
useEffect(() => {
|
||||
// Listen for test messages
|
||||
const handleTestMessage = (data: any) => {
|
||||
const handleTestMessage = (data: unknown) => {
|
||||
console.log("Received test message:", data);
|
||||
setMessages((prev) => [...prev, JSON.stringify(data)]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export class NbaStandingsAdapter implements StandingsSyncAdapter {
|
|||
// Seed within conference — ESPN key is "playoffSeed", value is a float (1.0, 2.0 ...)
|
||||
const playoffSeedStat = sm.get("playoffSeed");
|
||||
const conferenceRank =
|
||||
playoffSeedStat?.value !== null
|
||||
playoffSeedStat !== undefined && playoffSeedStat.value !== null && playoffSeedStat.value !== undefined
|
||||
? Math.round(playoffSeedStat.value)
|
||||
: playoffSeedStat?.displayValue
|
||||
? parseInt(playoffSeedStat.displayValue, 10) || undefined
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ process.env.NODE_ENV = 'test';
|
|||
// Mock Clerk
|
||||
vi.mock('@clerk/react-router/server', () => ({
|
||||
getAuth: vi.fn(() => Promise.resolve({ userId: 'test-user-id' })),
|
||||
clerkMiddleware: vi.fn(() => (req: any, res: any, next: any) => next()),
|
||||
clerkMiddleware: vi.fn(() => (req: unknown, res: unknown, next: () => void) => next()),
|
||||
rootAuthLoader: vi.fn(() => Promise.resolve({ userId: 'test-user-id' })),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// The real implementation uses Node.js AsyncLocalStorage which is server-only.
|
||||
// This stub is used during the client build to avoid bundling server-only code.
|
||||
|
||||
export const DatabaseContext = null as any;
|
||||
export const DatabaseContext = null as unknown;
|
||||
|
||||
export function database(): never {
|
||||
throw new Error("database() can only be called on the server");
|
||||
|
|
|
|||
|
|
@ -2,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;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
"composite": true,
|
||||
"strict": true,
|
||||
"types": ["node"],
|
||||
"lib": ["ES2022"],
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"target": "ES2023",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
"noEmit": false,
|
||||
"outDir": "./dist/server",
|
||||
"rootDir": ".",
|
||||
"target": "ES2022",
|
||||
"target": "ES2023",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"lib": ["ES2023"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
||||
"types": ["vite/client"],
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue