From b9743aacc1fb0566a2dde21744ea9753ba4c8b4c Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Thu, 16 Oct 2025 10:17:31 -0700 Subject: [PATCH] Revert Phase 8 and 9 implementations back to 0bef91e --- app/components/draft/CommissionerControls.tsx | 107 ----- app/components/draft/DraftGrid.tsx | 183 -------- app/components/draft/DraftQueue.tsx | 125 ------ app/components/draft/DraftTimer.tsx | 37 -- app/components/draft/PlayerList.tsx | 195 --------- app/hooks/useDraftSocket.ts | 101 ----- app/models/season.ts | 1 - app/routes.ts | 2 - app/routes/api/draft-actions.ts | 95 ---- app/routes/api/draft-commissioner.ts | 203 --------- app/routes/api/draft-pick.ts | 175 -------- app/routes/leagues/$leagueId.draft.tsx | 412 ------------------ app/routes/leagues/$leagueId.tsx | 9 - package-lock.json | 367 +--------------- package.json | 10 +- server/index.ts => server.js | 11 +- server/app.ts | 8 +- server/socket.ts | 75 ---- server/types.d.ts | 7 - tsconfig.node.json | 8 +- vite.config.ts | 5 - 21 files changed, 13 insertions(+), 2123 deletions(-) delete mode 100644 app/components/draft/CommissionerControls.tsx delete mode 100644 app/components/draft/DraftGrid.tsx delete mode 100644 app/components/draft/DraftQueue.tsx delete mode 100644 app/components/draft/DraftTimer.tsx delete mode 100644 app/components/draft/PlayerList.tsx delete mode 100644 app/hooks/useDraftSocket.ts delete mode 100644 app/routes/api/draft-actions.ts delete mode 100644 app/routes/api/draft-commissioner.ts delete mode 100644 app/routes/api/draft-pick.ts delete mode 100644 app/routes/leagues/$leagueId.draft.tsx rename server/index.ts => server.js (79%) delete mode 100644 server/socket.ts delete mode 100644 server/types.d.ts diff --git a/app/components/draft/CommissionerControls.tsx b/app/components/draft/CommissionerControls.tsx deleted file mode 100644 index d00f7c9..0000000 --- a/app/components/draft/CommissionerControls.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Button } from "~/components/ui/button"; -import { Play, Pause, FastForward } from "lucide-react"; - -interface CommissionerControlsProps { - isDraftStarted: boolean; - isDraftPaused: boolean; - isDraftComplete: boolean; - currentTeamName: string; - onStartDraft: () => void; - onPauseDraft: () => void; - onResumeDraft: () => void; - onForcePick: () => void; -} - -export function CommissionerControls({ - isDraftStarted, - isDraftPaused, - isDraftComplete, - currentTeamName, - onStartDraft, - onPauseDraft, - onResumeDraft, - onForcePick, -}: CommissionerControlsProps) { - if (isDraftComplete) { - return ( -
-
-

- Draft Complete -

-
-
- ); - } - - if (!isDraftStarted) { - return ( -
-

- The draft has not started yet. Click below to begin the draft. -

- -
- ); - } - - return ( -
- {/* Draft Status */} -
-
- Draft Status: - - {isDraftPaused ? "Paused" : "Active"} - -
-
- Current Pick: - {currentTeamName} -
-
- - {/* Control Buttons */} -
- {isDraftPaused ? ( - - ) : ( - - )} - - -
- - {/* Help Text */} -
-

- Force Pick: Automatically selects the top player from the - current team's queue, or the highest EV available player if queue is empty. -

-
-
- ); -} diff --git a/app/components/draft/DraftGrid.tsx b/app/components/draft/DraftGrid.tsx deleted file mode 100644 index 5206db8..0000000 --- a/app/components/draft/DraftGrid.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { useMemo } from "react"; -import { DraftTimer } from "./DraftTimer"; - -interface Team { - id: string; - name: string; - draftOrder: number; - ownerId: string | null; -} - -interface DraftPick { - id: string; - teamId: string; - participantId: string; - pickNumber: number; - round: number; - pickInRound: number; - pickedByType: "owner" | "commissioner" | "auto"; -} - -interface Participant { - id: string; - name: string; - sport: string; -} - -interface Timer { - teamId: string; - timeRemaining: number; -} - -interface DraftGridProps { - teams: Team[]; - draftPicks: DraftPick[]; - participants: Participant[]; - draftRounds: number; - currentPickNumber: number; - userTeamId?: string; - timers?: Timer[]; -} - -export function DraftGrid({ - teams, - draftPicks, - participants, - draftRounds, - currentPickNumber, - userTeamId, - timers = [], -}: DraftGridProps) { - // Create a map of participant IDs to participant data for quick lookup - const participantMap = useMemo(() => { - return new Map(participants.map((p) => [p.id, p])); - }, [participants]); - - // Create a map of pick numbers to draft picks - const pickMap = useMemo(() => { - return new Map(draftPicks.map((p) => [p.pickNumber, p])); - }, [draftPicks]); - - // Create a map of team IDs to timers - const timerMap = useMemo(() => { - return new Map(timers.map((t) => [t.teamId, t])); - }, [timers]); - - // Calculate pick number for a given round and team - const getPickNumber = (round: number, teamIndex: number): number => { - const isOddRound = round % 2 === 1; - const pickInRound = isOddRound ? teamIndex + 1 : teams.length - teamIndex; - return (round - 1) * teams.length + pickInRound; - }; - - - return ( -
-
- {/* Header Row - Team Names */} -
-
- Round -
- {teams.map((team) => { - const timer = timerMap.get(team.id); - // Determine if this team is on the clock - const currentPick = Math.ceil(currentPickNumber / teams.length); - const pickInRound = ((currentPickNumber - 1) % teams.length) + 1; - const isOddRound = currentPick % 2 === 1; - const currentTeamIndex = isOddRound ? pickInRound - 1 : teams.length - pickInRound; - const isOnClock = teams[currentTeamIndex]?.id === team.id; - - return ( -
-
- {team.name} -
- {timer && ( -
- -
- )} -
- ); - })} -
- - {/* Draft Rounds */} - {Array.from({ length: draftRounds }, (_, roundIndex) => { - const round = roundIndex + 1; - const isOddRound = round % 2 === 1; - - return ( -
- {/* Round Number */} -
- {round} -
- - {/* Picks for this round */} - {teams.map((team, teamIndex) => { - const pickNumber = getPickNumber(round, teamIndex); - const pick = pickMap.get(pickNumber); - const participant = pick ? participantMap.get(pick.participantId) : null; - const isCurrentPick = pickNumber === currentPickNumber; - const isPastPick = pickNumber < currentPickNumber; - const isUserTeam = team.id === userTeamId; - - return ( -
-
- {/* Pick Number */} -
- {round}.{String(isOddRound ? teamIndex + 1 : teams.length - teamIndex).padStart(2, "0")} -
- - {/* Participant or Status */} - {participant ? ( -
-
- {participant.name} -
-
- {participant.sport} -
- {pick && pick.pickedByType !== "owner" && ( -
- ({pick.pickedByType}) -
- )} -
- ) : isCurrentPick ? ( -
- On the clock -
- ) : isPastPick ? ( -
- Skipped -
- ) : ( -
- - -
- )} -
-
- ); - })} -
- ); - })} -
-
- ); -} diff --git a/app/components/draft/DraftQueue.tsx b/app/components/draft/DraftQueue.tsx deleted file mode 100644 index a83eeac..0000000 --- a/app/components/draft/DraftQueue.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useState } from "react"; -import { Button } from "~/components/ui/button"; -import { Badge } from "~/components/ui/badge"; -import { GripVertical, X } from "lucide-react"; - -interface Participant { - id: string; - name: string; - sport: string; - expectedValue: number; -} - -interface QueueItem { - id: string; - participantId: string; - queuePosition: number; -} - -interface DraftQueueProps { - queueItems: QueueItem[]; - participants: Participant[]; - onReorder?: (participantIds: string[]) => void; - onRemove?: (queueItemId: string) => void; -} - -export function DraftQueue({ - queueItems, - participants, - onReorder, - onRemove, -}: DraftQueueProps) { - const [draggedIndex, setDraggedIndex] = useState(null); - - // Create a map for quick participant lookup - const participantMap = new Map(participants.map((p) => [p.id, p])); - - // Sort queue items by position - const sortedQueue = [...queueItems].sort((a, b) => a.queuePosition - b.queuePosition); - - const handleDragStart = (index: number) => { - setDraggedIndex(index); - }; - - const handleDragOver = (e: React.DragEvent, index: number) => { - e.preventDefault(); - if (draggedIndex === null || draggedIndex === index) return; - - // Reorder the items - const newQueue = [...sortedQueue]; - const draggedItem = newQueue[draggedIndex]; - newQueue.splice(draggedIndex, 1); - newQueue.splice(index, 0, draggedItem); - - // Update positions and notify parent - const participantIds = newQueue.map((item) => item.participantId); - onReorder?.(participantIds); - setDraggedIndex(index); - }; - - const handleDragEnd = () => { - setDraggedIndex(null); - }; - - if (sortedQueue.length === 0) { - return ( -
-

Your queue is empty

-

Add players from the Available Players list

-
- ); - } - - return ( -
- {sortedQueue.map((item, index) => { - const participant = participantMap.get(item.participantId); - if (!participant) return null; - - return ( -
handleDragStart(index)} - onDragOver={(e) => handleDragOver(e, index)} - onDragEnd={handleDragEnd} - className={`flex items-center gap-2 p-3 rounded-lg border bg-card transition-all ${ - draggedIndex === index ? "opacity-50" : "hover:bg-accent cursor-move" - }`} - > - {/* Drag Handle */} -
- - {index + 1} -
- - {/* Player Info */} -
-
{participant.name}
-
- - {participant.sport} - - - EV: {participant.expectedValue.toFixed(1)} - -
-
- - {/* Remove Button */} - {onRemove && ( - - )} -
- ); - })} -
- ); -} diff --git a/app/components/draft/DraftTimer.tsx b/app/components/draft/DraftTimer.tsx deleted file mode 100644 index d8ef01f..0000000 --- a/app/components/draft/DraftTimer.tsx +++ /dev/null @@ -1,37 +0,0 @@ -interface DraftTimerProps { - timeRemaining: number; // in seconds - isActive?: boolean; -} - -export function DraftTimer({ timeRemaining, isActive = false }: DraftTimerProps) { - // Format time as M:SS or H:MM:SS - const formatTime = (seconds: number): string => { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - - if (hours > 0) { - return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; - } - return `${minutes}:${String(secs).padStart(2, "0")}`; - }; - - // Determine color based on time remaining - const getColorClass = (): string => { - if (timeRemaining > 60) { - return "text-green-600 dark:text-green-400"; - } else if (timeRemaining > 30) { - return "text-yellow-600 dark:text-yellow-400"; - } else if (timeRemaining > 10) { - return "text-red-600 dark:text-red-400"; - } else { - return "text-red-600 dark:text-red-400 animate-pulse"; - } - }; - - return ( -
- {formatTime(timeRemaining)} -
- ); -} diff --git a/app/components/draft/PlayerList.tsx b/app/components/draft/PlayerList.tsx deleted file mode 100644 index ce142fe..0000000 --- a/app/components/draft/PlayerList.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useState, useMemo } from "react"; -import { Input } from "~/components/ui/input"; -import { Button } from "~/components/ui/button"; -import { Badge } from "~/components/ui/badge"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "~/components/ui/select"; -import { Search, ArrowUpDown } from "lucide-react"; - -interface Participant { - id: string; - name: string; - sport: string; - expectedValue: number; -} - -interface DraftPick { - participantId: string; -} - -interface PlayerListProps { - participants: Participant[]; - draftPicks: DraftPick[]; - onAddToQueue?: (participantId: string) => void; - onDraftPlayer?: (participantId: string) => void; - canDraft: boolean; -} - -export function PlayerList({ - participants, - draftPicks, - onAddToQueue, - onDraftPlayer, - canDraft, -}: PlayerListProps) { - const [searchQuery, setSearchQuery] = useState(""); - const [sportFilter, setSportFilter] = useState("all"); - const [sortBy, setSortBy] = useState<"ev" | "name">("ev"); - - // Get unique sports - const sports = useMemo(() => { - const uniqueSports = new Set(participants.map((p) => p.sport)); - return Array.from(uniqueSports).sort(); - }, [participants]); - - // Get drafted participant IDs - const draftedIds = useMemo(() => { - return new Set(draftPicks.map((p) => p.participantId)); - }, [draftPicks]); - - // Filter and sort participants - const filteredParticipants = useMemo(() => { - let filtered = participants.filter((p) => { - // Filter out drafted players - if (draftedIds.has(p.id)) return false; - - // Filter by search query - if (searchQuery && !p.name.toLowerCase().includes(searchQuery.toLowerCase())) { - return false; - } - - // Filter by sport - if (sportFilter !== "all" && p.sport !== sportFilter) { - return false; - } - - return true; - }); - - // Sort - if (sortBy === "ev") { - filtered.sort((a, b) => b.expectedValue - a.expectedValue); - } else { - filtered.sort((a, b) => a.name.localeCompare(b.name)); - } - - return filtered; - }, [participants, draftedIds, searchQuery, sportFilter, sortBy]); - - return ( -
- {/* Filters */} -
- {/* Search */} -
- - setSearchQuery(e.target.value)} - className="pl-9" - /> -
- - {/* Sport Filter and Sort */} -
- - - -
-
- - {/* Player Count */} -
- {filteredParticipants.length} player{filteredParticipants.length !== 1 ? "s" : ""} available -
- - {/* Player Table */} -
- {filteredParticipants.length === 0 ? ( -
- No players found -
- ) : ( - - - - - - - - - - - {filteredParticipants.map((participant) => ( - - - - - - - ))} - -
PlayerSportEVActions
{participant.name} - - {participant.sport} - - - {participant.expectedValue.toFixed(1)} - -
- {onAddToQueue && ( - - )} - {canDraft && onDraftPlayer && ( - - )} -
-
- )} -
-
- ); -} diff --git a/app/hooks/useDraftSocket.ts b/app/hooks/useDraftSocket.ts deleted file mode 100644 index 88f771f..0000000 --- a/app/hooks/useDraftSocket.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { useEffect, useState, useCallback } from "react"; -import { io, Socket } from "socket.io-client"; - -const SOCKET_URL = typeof window !== "undefined" - ? window.location.origin - : "http://localhost:3000"; - -export interface DraftSocketEvents { - // Events emitted by server - "pick-made": (data: any) => void; - "timer-update": (data: any) => void; - "current-pick-changed": (data: any) => void; - "draft-started": (data: any) => void; - "draft-paused": (data: any) => void; - "draft-completed": (data: any) => void; - "queue-updated": (data: any) => void; - "user-joined": (data: any) => void; -} - -export function useDraftSocket(seasonId: string | null) { - const [socket, setSocket] = useState(null); - const [isConnected, setIsConnected] = useState(false); - - useEffect(() => { - if (!seasonId) return; - - // Create socket connection - const newSocket = io(SOCKET_URL, { - transports: ["websocket", "polling"], - }); - - newSocket.on("connect", () => { - console.log("Socket connected:", newSocket.id); - setIsConnected(true); - - // Join the draft room - newSocket.emit("join-draft", seasonId); - }); - - newSocket.on("disconnect", () => { - console.log("Socket disconnected"); - setIsConnected(false); - }); - - newSocket.on("connect_error", (error) => { - console.error("Socket connection error:", error); - setIsConnected(false); - }); - - setSocket(newSocket); - - // Cleanup on unmount - return () => { - if (newSocket) { - newSocket.emit("leave-draft", seasonId); - newSocket.close(); - } - }; - }, [seasonId]); - - // Helper to emit events - const emit = useCallback((event: string, data?: any) => { - if (socket && isConnected) { - socket.emit(event, data); - } else { - console.warn(`Cannot emit ${event}: socket not connected`); - } - }, [socket, isConnected]); - - // Helper to listen to events - const on = useCallback(( - event: K, - handler: DraftSocketEvents[K] - ) => { - if (socket) { - socket.on(event as string, handler as any); - } - }, [socket]); - - // Helper to remove event listeners - const off = useCallback(( - event: K, - handler?: DraftSocketEvents[K] - ) => { - if (socket) { - if (handler) { - socket.off(event as string, handler as any); - } else { - socket.off(event as string); - } - } - }, [socket]); - - return { - socket, - isConnected, - emit, - on, - off, - }; -} diff --git a/app/models/season.ts b/app/models/season.ts index 1b4167b..46d9a8c 100644 --- a/app/models/season.ts +++ b/app/models/season.ts @@ -139,7 +139,6 @@ export async function findSeasonWithSportsSeasons( sportsSeason: { with: { sport: true, - participants: true, }, }, }, diff --git a/app/routes.ts b/app/routes.ts index 6b7a80b..1e08d0c 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -6,10 +6,8 @@ export default [ route("leagues/new", "routes/leagues/new.tsx"), route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"), route("leagues/:leagueId/settings", "routes/leagues/$leagueId.settings.tsx"), - route("leagues/:leagueId/draft", "routes/leagues/$leagueId.draft.tsx"), route("teams/:teamId/settings", "routes/teams/$teamId.settings.tsx"), route("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"), - route("api/draft-actions", "routes/api/draft-actions.ts"), route("user-profile", "routes/user-profile.tsx"), route("how-to-play", "routes/how-to-play.tsx"), diff --git a/app/routes/api/draft-actions.ts b/app/routes/api/draft-actions.ts deleted file mode 100644 index 269dacb..0000000 --- a/app/routes/api/draft-actions.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { data, type ActionFunctionArgs } from "react-router"; -import { getAuth } from "@clerk/react-router/server"; -import { - addToQueue, - removeFromQueue, - reorderQueueWithSeason, - findTeamById, - getTeamQueue, -} from "~/models"; - -export async function action(args: ActionFunctionArgs) { - const { userId } = await getAuth(args); - if (!userId) { - return data({ error: "Unauthorized" }, { status: 401 }); - } - - const { request } = args; - - const formData = await request.formData(); - const actionType = formData.get("action"); - const teamId = formData.get("teamId"); - - if (!teamId || typeof teamId !== "string") { - return data({ error: "Team ID is required" }, { status: 400 }); - } - - // Verify user owns this team - const team = await findTeamById(teamId); - if (!team || team.ownerId !== userId) { - return data({ error: "You do not own this team" }, { status: 403 }); - } - - try { - switch (actionType) { - case "add-to-queue": { - const participantId = formData.get("participantId"); - const seasonId = formData.get("seasonId"); - - if (!participantId || typeof participantId !== "string") { - return data({ error: "Participant ID is required" }, { status: 400 }); - } - if (!seasonId || typeof seasonId !== "string") { - return data({ error: "Season ID is required" }, { status: 400 }); - } - - // Get current queue to determine position - const currentQueue = await getTeamQueue(teamId); - const queuePosition = currentQueue.length + 1; - - await addToQueue({ - seasonId, - teamId, - participantId, - queuePosition, - }); - - return data({ success: true }); - } - - case "remove-from-queue": { - const queueItemId = formData.get("queueItemId"); - - if (!queueItemId || typeof queueItemId !== "string") { - return data({ error: "Queue item ID is required" }, { status: 400 }); - } - - await removeFromQueue(queueItemId); - return data({ success: true }); - } - - case "reorder-queue": { - const participantIds = formData.get("participantIds"); - const seasonId = formData.get("seasonId"); - - if (!participantIds || typeof participantIds !== "string") { - return data({ error: "Participant IDs are required" }, { status: 400 }); - } - if (!seasonId || typeof seasonId !== "string") { - return data({ error: "Season ID is required" }, { status: 400 }); - } - - const idsArray = JSON.parse(participantIds); - await reorderQueueWithSeason(seasonId, teamId, idsArray); - - return data({ success: true }); - } - - default: - return data({ error: "Invalid action" }, { status: 400 }); - } - } catch (error) { - console.error("Draft action error:", error); - return data({ error: "Failed to perform action" }, { status: 500 }); - } -} diff --git a/app/routes/api/draft-commissioner.ts b/app/routes/api/draft-commissioner.ts deleted file mode 100644 index ec311a1..0000000 --- a/app/routes/api/draft-commissioner.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { data, type ActionFunctionArgs } from "react-router"; -import { getAuth } from "@clerk/react-router/server"; -import { - findSeasonById, - updateSeason, - updateSeasonStatus, - findTeamsBySeasonId, - isCommissioner, - createDraftPick, - initializeDraftTimers, - updateTeamTimer, - getSeasonTimers, - autoPickForTeam, - calculatePickInfo, - removeFromQueue, - getTeamQueue, -} from "~/models"; -import { getSocketIO } from "../../../server/socket.js"; - -export async function action(args: ActionFunctionArgs) { - const { userId } = await getAuth(args); - if (!userId) { - return data({ error: "Unauthorized" }, { status: 401 }); - } - - const { request } = args; - const formData = await request.formData(); - const actionType = formData.get("action"); - const seasonId = formData.get("seasonId"); - - if (!seasonId || typeof seasonId !== "string") { - return data({ error: "Season ID is required" }, { status: 400 }); - } - - // Get season - const season = await findSeasonById(seasonId); - if (!season) { - return data({ error: "Season not found" }, { status: 404 }); - } - - // Verify user is commissioner - const isUserCommissioner = await isCommissioner(season.leagueId, userId); - if (!isUserCommissioner) { - return data({ error: "Only commissioners can perform this action" }, { status: 403 }); - } - - try { - const io = getSocketIO(); - - switch (actionType) { - case "start-draft": { - // Check if draft already started - if (season.draftStartedAt) { - return data({ error: "Draft has already started" }, { status: 400 }); - } - - // Get teams - const teams = await findTeamsBySeasonId(seasonId); - - // Initialize timers - await initializeDraftTimers(seasonId, teams, season.draftInitialTime || 120); - - // Update season - await updateSeason(seasonId, { - draftStartedAt: new Date(), - currentPickNumber: 1, - draftPaused: false, - }); - await updateSeasonStatus(seasonId, "draft"); - - // Get updated timers - const timers = await getSeasonTimers(seasonId); - - // Broadcast to all clients - io.to(`draft-${seasonId}`).emit("draft-started", { - seasonId, - startedAt: new Date(), - timers, - }); - - return data({ success: true }); - } - - case "pause-draft": { - const pause = formData.get("pause") === "true"; - - // Check if draft has started - if (!season.draftStartedAt) { - return data({ error: "Draft has not started yet" }, { status: 400 }); - } - - // Update season - await updateSeason(seasonId, { draftPaused: pause }); - - // Broadcast to all clients - io.to(`draft-${seasonId}`).emit("draft-paused", { - seasonId, - isPaused: pause, - }); - - return data({ success: true }); - } - - case "force-pick": { - const teamId = formData.get("teamId"); - - if (!teamId || typeof teamId !== "string") { - return data({ error: "Team ID is required" }, { status: 400 }); - } - - // Check if draft has started - if (!season.draftStartedAt) { - return data({ error: "Draft has not started" }, { status: 400 }); - } - - // Check if draft is complete - const teams = await findTeamsBySeasonId(seasonId); - const totalPicks = season.draftRounds * teams.length; - const currentPickNumber = season.currentPickNumber || 1; - - if (currentPickNumber > totalPicks) { - return data({ error: "Draft is already complete" }, { status: 400 }); - } - - // Auto-pick for the team - const participantId = await autoPickForTeam(seasonId, teamId); - - if (!participantId) { - return data({ error: "No available participants to pick" }, { status: 400 }); - } - - // Calculate pick info - const { round, pickInRound } = calculatePickInfo(currentPickNumber, teams.length); - - // Create the draft pick - const pick = await createDraftPick({ - seasonId, - teamId, - participantId, - pickNumber: currentPickNumber, - round, - pickInRound, - pickedByUserId: userId, - pickedByType: "commissioner", - timeUsed: 0, - }); - - // Remove from queue if it was queued - const queue = await getTeamQueue(teamId); - const queueItem = queue.find((item: any) => item.participantId === participantId); - if (queueItem) { - await removeFromQueue(queueItem.id); - } - - // Update season to next pick - const nextPickNumber = currentPickNumber + 1; - await updateSeason(seasonId, { currentPickNumber: nextPickNumber }); - - // Add increment time to current team's timer - if (season.draftIncrementTime) { - const timers = await getSeasonTimers(seasonId); - const currentTimer = timers.find((t: any) => t.teamId === teamId); - if (currentTimer) { - const newTime = currentTimer.timeRemaining + season.draftIncrementTime; - await updateTeamTimer(teamId, newTime); - } - } - - // Check if draft is complete - if (nextPickNumber > totalPicks) { - await updateSeasonStatus(seasonId, "active"); - io.to(`draft-${seasonId}`).emit("draft-completed", { - seasonId, - }); - } - - // Broadcast pick to all clients in the room - io.to(`draft-${seasonId}`).emit("pick-made", { - pick, - nextPickNumber, - isDraftComplete: nextPickNumber > totalPicks, - }); - - // Emit queue update to team owner - const updatedQueue = await getTeamQueue(teamId); - io.to(`draft-${seasonId}`).emit("queue-updated", { - teamId, - queue: updatedQueue, - }); - - return data({ success: true, pick }); - } - - default: - return data({ error: "Invalid action" }, { status: 400 }); - } - } catch (error) { - console.error("Commissioner action error:", error); - return data({ - error: error instanceof Error ? error.message : "Failed to perform action" - }, { status: 500 }); - } -} diff --git a/app/routes/api/draft-pick.ts b/app/routes/api/draft-pick.ts deleted file mode 100644 index 0293154..0000000 --- a/app/routes/api/draft-pick.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { data, type ActionFunctionArgs } from "react-router"; -import { getAuth } from "@clerk/react-router/server"; -import { - findSeasonById, - updateSeason, - updateSeasonStatus, - findTeamsBySeasonId, - isCommissioner, - createDraftPick, - isParticipantDrafted, - updateTeamTimer, - getSeasonTimers, - calculatePickInfo, - getTeamForPick, - findDraftSlotsBySeasonId, - removeFromQueue, - getTeamQueue, -} from "~/models"; -import { getSocketIO } from "../../../server/socket.js"; - -export async function action(args: ActionFunctionArgs) { - const { userId } = await getAuth(args); - if (!userId) { - return data({ error: "Unauthorized" }, { status: 401 }); - } - - const { request } = args; - const formData = await request.formData(); - - const seasonId = formData.get("seasonId"); - const teamId = formData.get("teamId"); - const participantId = formData.get("participantId"); - - if (!seasonId || typeof seasonId !== "string") { - return data({ error: "Season ID is required" }, { status: 400 }); - } - - if (!teamId || typeof teamId !== "string") { - return data({ error: "Team ID is required" }, { status: 400 }); - } - - if (!participantId || typeof participantId !== "string") { - return data({ error: "Participant ID is required" }, { status: 400 }); - } - - try { - // Get season - const season = await findSeasonById(seasonId); - if (!season) { - return data({ error: "Season not found" }, { status: 404 }); - } - - // Check if draft has started - if (!season.draftStartedAt) { - return data({ error: "Draft has not started" }, { status: 400 }); - } - - // Check if draft is paused - if (season.draftPaused) { - return data({ error: "Draft is currently paused" }, { status: 400 }); - } - - // Check if draft is complete - const teams = await findTeamsBySeasonId(seasonId); - const totalPicks = season.draftRounds * teams.length; - const currentPickNumber = season.currentPickNumber || 1; - - if (currentPickNumber > totalPicks) { - return data({ error: "Draft is already complete" }, { status: 400 }); - } - - // Get draft slots to determine current team - const draftSlots = await findDraftSlotsBySeasonId(seasonId); - const draftOrder = draftSlots.map((slot: any) => ({ - teamId: slot.teamId, - draftOrder: slot.draftOrder, - })); - - const currentTeamId = getTeamForPick(currentPickNumber, draftOrder); - - if (!currentTeamId) { - return data({ error: "Could not determine current team" }, { status: 400 }); - } - - // Verify it's the correct team's turn - if (teamId !== currentTeamId) { - return data({ error: "It is not this team's turn" }, { status: 400 }); - } - - // Verify user has permission (team owner or commissioner) - const team = teams.find((t: any) => t.id === teamId); - const isTeamOwner = team?.ownerId === userId; - const isUserCommissioner = await isCommissioner(season.leagueId, userId); - - if (!isTeamOwner && !isUserCommissioner) { - return data({ error: "You do not have permission to make this pick" }, { status: 403 }); - } - - // Check if participant is already drafted - const isDrafted = await isParticipantDrafted(seasonId, participantId); - if (isDrafted) { - return data({ error: "This participant has already been drafted" }, { status: 400 }); - } - - // Calculate pick info - const { round, pickInRound } = calculatePickInfo(currentPickNumber, teams.length); - - // Determine picked by type - const pickedByType = isUserCommissioner && !isTeamOwner ? "commissioner" : "owner"; - - // Create the draft pick - const pick = await createDraftPick({ - seasonId, - teamId, - participantId, - pickNumber: currentPickNumber, - round, - pickInRound, - pickedByUserId: userId, - pickedByType, - timeUsed: 0, - }); - - // Remove from queue if it was queued - const queue = await getTeamQueue(teamId); - const queueItem = queue.find((item: any) => item.participantId === participantId); - if (queueItem) { - await removeFromQueue(queueItem.id); - } - - // Update season to next pick - const nextPickNumber = currentPickNumber + 1; - await updateSeason(seasonId, { currentPickNumber: nextPickNumber }); - - // Add increment time to current team's timer - if (season.draftIncrementTime) { - const timers = await getSeasonTimers(seasonId); - const currentTimer = timers.find((t: any) => t.teamId === teamId); - if (currentTimer) { - const newTime = currentTimer.timeRemaining + season.draftIncrementTime; - await updateTeamTimer(teamId, newTime); - } - } - - // Check if draft is complete - const io = getSocketIO(); - if (nextPickNumber > totalPicks) { - await updateSeasonStatus(seasonId, "active"); - io.to(`draft-${seasonId}`).emit("draft-completed", { - seasonId, - }); - } - - // Broadcast pick to all clients in the room - io.to(`draft-${seasonId}`).emit("pick-made", { - pick, - nextPickNumber, - isDraftComplete: nextPickNumber > totalPicks, - }); - - // Emit queue update to team owner - const updatedQueue = await getTeamQueue(teamId); - io.to(`draft-${seasonId}`).emit("queue-updated", { - teamId, - queue: updatedQueue, - }); - - return data({ success: true, pick }); - } catch (error) { - console.error("Error making pick:", error); - return data({ - error: error instanceof Error ? error.message : "Failed to make pick", - }, { status: 500 }); - } -} diff --git a/app/routes/leagues/$leagueId.draft.tsx b/app/routes/leagues/$leagueId.draft.tsx deleted file mode 100644 index 9860b28..0000000 --- a/app/routes/leagues/$leagueId.draft.tsx +++ /dev/null @@ -1,412 +0,0 @@ -import { data, redirect } from "react-router"; -import type { LoaderFunctionArgs } from "react-router"; -import { getAuth } from "@clerk/react-router/server"; -import { useEffect, useState } from "react"; -import { - findLeagueById, - findCurrentSeason, - findTeamsBySeasonId, - isCommissioner, - findDraftSlotsBySeasonId, - getDraftPicks, - getSeasonTimers, - findSeasonWithSportsSeasons, - getTeamQueue, - getTeamForPick, -} from "~/models"; -import { DraftGrid } from "~/components/draft/DraftGrid"; -import { PlayerList } from "~/components/draft/PlayerList"; -import { DraftQueue } from "~/components/draft/DraftQueue"; -import { CommissionerControls } from "~/components/draft/CommissionerControls"; -import { useDraftSocket } from "~/hooks/useDraftSocket"; - -export async function loader(args: LoaderFunctionArgs) { - const { userId } = await getAuth(args); - if (!userId) { - throw redirect("/"); - } - - const { leagueId } = args.params; - if (!leagueId) { - throw new Response("League ID is required", { status: 400 }); - } - - // Get league and season - const league = await findLeagueById(leagueId); - if (!league) { - throw new Response("League not found", { status: 404 }); - } - - const season = await findCurrentSeason(leagueId); - if (!season) { - throw new Response("No active season found", { status: 404 }); - } - - // Get season with sports and participants - const seasonWithSports = await findSeasonWithSportsSeasons(season.id); - const participants = seasonWithSports?.seasonSports?.flatMap( - (ss: any) => { - const sportName = ss.sportsSeason?.sport?.name || "Unknown"; - return (ss.sportsSeason?.participants || []).map((p: any) => ({ - ...p, - sport: sportName, - })); - } - ) || []; - - // Get teams and draft order - const teams = await findTeamsBySeasonId(season.id); - const draftSlots = await findDraftSlotsBySeasonId(season.id); - - // Sort teams by draft order - const teamsWithOrder = teams.map((team) => { - const slot = draftSlots.find((s: { teamId: string }) => s.teamId === team.id); - return { - ...team, - draftOrder: slot?.draftOrder || 0, - }; - }).sort((a, b) => a.draftOrder - b.draftOrder); - - // Check user permissions - const isUserCommissioner = await isCommissioner(leagueId, userId); - const userTeam = teams.find((team) => team.ownerId === userId); - - // Get draft data - const draftPicks = await getDraftPicks(season.id); - let timers = await getSeasonTimers(season.id); - - // If no timers exist, create default timers for display - if (timers.length === 0) { - timers = teams.map((team) => ({ - id: team.id, - seasonId: season.id, - teamId: team.id, - timeRemaining: season.draftInitialTime, - createdAt: new Date(), - updatedAt: new Date(), - })); - } - - // Get user's queue if they have a team - const queueItems = userTeam ? await getTeamQueue(userTeam.id) : []; - - // Calculate current pick info - const totalPicks = season.draftRounds * teams.length; - const currentPickNumber = season.currentPickNumber || 1; - const isDraftComplete = currentPickNumber > totalPicks; - - return data({ - league, - season, - teams: teamsWithOrder, - draftSlots, - isCommissioner: isUserCommissioner, - userTeam, - draftPicks, - participants, - queueItems, - timers, - currentPickNumber, - totalPicks, - isDraftComplete, - userId, - }); -} - -export default function DraftRoom({ loaderData }: { loaderData: Awaited>["data"] }) { - const { - league, - season, - teams, - draftPicks: initialDraftPicks, - participants, - queueItems: initialQueueItems, - timers: initialTimers, - isCommissioner, - userTeam, - currentPickNumber: initialPickNumber, - totalPicks, - isDraftComplete: initialIsDraftComplete, - draftSlots, - } = loaderData; - - // Local state for real-time updates - const [draftPicks, setDraftPicks] = useState(initialDraftPicks); - const [queueItems, setQueueItems] = useState(initialQueueItems); - const [timers, setTimers] = useState(initialTimers); - const [currentPickNumber, setCurrentPickNumber] = useState(initialPickNumber); - const [isDraftComplete, setIsDraftComplete] = useState(initialIsDraftComplete); - const [isDraftStarted, setIsDraftStarted] = useState(!!season.draftStartedAt); - const [isDraftPaused, setIsDraftPaused] = useState(season.draftPaused || false); - - // Socket connection - const { on, off } = useDraftSocket(season.id); - - // Calculate current team - const draftOrder = draftSlots.map((slot: any) => ({ - teamId: slot.teamId, - draftOrder: slot.draftOrder, - })); - const currentTeamId = getTeamForPick(currentPickNumber, draftOrder); - const currentTeam = teams.find((t: any) => t.id === currentTeamId); - const currentTeamName = currentTeam?.name || "Unknown"; - - // Socket event handlers - useEffect(() => { - // Listen for pick made - const handlePickMade = (data: any) => { - setDraftPicks((prev: any) => [...prev, data.pick]); - setCurrentPickNumber(data.nextPickNumber); - setIsDraftComplete(data.isDraftComplete); - }; - - // Listen for draft started - const handleDraftStarted = (data: any) => { - setIsDraftStarted(true); - setIsDraftPaused(false); - setTimers(data.timers); - }; - - // Listen for draft paused - const handleDraftPaused = (data: any) => { - setIsDraftPaused(data.isPaused); - }; - - // Listen for draft completed - const handleDraftCompleted = () => { - setIsDraftComplete(true); - }; - - // Listen for queue updates - const handleQueueUpdated = (data: any) => { - if (userTeam && data.teamId === userTeam.id) { - setQueueItems(data.queue); - } - }; - - on("pick-made", handlePickMade); - on("draft-started", handleDraftStarted); - on("draft-paused", handleDraftPaused); - on("draft-completed", handleDraftCompleted); - on("queue-updated", handleQueueUpdated); - - return () => { - off("pick-made", handlePickMade); - off("draft-started", handleDraftStarted); - off("draft-paused", handleDraftPaused); - off("draft-completed", handleDraftCompleted); - off("queue-updated", handleQueueUpdated); - }; - }, [on, off, userTeam]); - - // Commissioner action handlers - const handleStartDraft = async () => { - try { - const formData = new FormData(); - formData.append("action", "start-draft"); - formData.append("seasonId", season.id); - - const response = await fetch("/api/draft-commissioner", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - const error = await response.json(); - console.error("Failed to start draft:", error); - } - } catch (error) { - console.error("Error starting draft:", error); - } - }; - - const handlePauseDraft = async () => { - try { - const formData = new FormData(); - formData.append("action", "pause-draft"); - formData.append("seasonId", season.id); - formData.append("pause", "true"); - - const response = await fetch("/api/draft-commissioner", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - const error = await response.json(); - console.error("Failed to pause draft:", error); - } - } catch (error) { - console.error("Error pausing draft:", error); - } - }; - - const handleResumeDraft = async () => { - try { - const formData = new FormData(); - formData.append("action", "pause-draft"); - formData.append("seasonId", season.id); - formData.append("pause", "false"); - - const response = await fetch("/api/draft-commissioner", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - const error = await response.json(); - console.error("Failed to resume draft:", error); - } - } catch (error) { - console.error("Error resuming draft:", error); - } - }; - - const handleForcePick = async () => { - if (!currentTeamId) return; - - try { - const formData = new FormData(); - formData.append("action", "force-pick"); - formData.append("seasonId", season.id); - formData.append("teamId", currentTeamId); - - const response = await fetch("/api/draft-commissioner", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - const error = await response.json(); - console.error("Failed to force pick:", error); - } - } catch (error) { - console.error("Error forcing pick:", error); - } - }; - - return ( -
- {/* Header */} -
-
-
-
-

{league.name} Draft

-

- {season.year} Season • Pick {currentPickNumber} of {totalPicks} -

-
-
- {isCommissioner && ( - - Commissioner - - )} - {userTeam && ( - - {userTeam.name} - - )} -
-
-
-
- - {/* Main Content */} -
- {isDraftComplete ? ( -
-

Draft Complete!

-

- All picks have been made. The season is ready to begin. -

-
- ) : ( -
- {/* Draft Board - Full Width */} -
-

Draft Board

- -
- - {/* 2x2 Grid Below */} -
- {/* Available Players */} -
-

Available Players

- -
- - {/* Your Queue */} -
-

Your Queue

-
- -
-
- - {/* Draft Timer */} -
-

Draft Timer

-
- Timer will be implemented in Phase 8 -
-
- - {/* Commissioner Controls */} - {isCommissioner ? ( -
-

Commissioner Controls

- -
- ) : ( -
-

Draft Info

-
-
- Current Pick: - {currentPickNumber} of {totalPicks} -
-
- Draft Rounds: - {season.draftRounds} -
-
- Current Team: - {currentTeamName} -
-
-
- )} -
-
- )} -
-
- ); -} diff --git a/app/routes/leagues/$leagueId.tsx b/app/routes/leagues/$leagueId.tsx index 6d3393a..aef86b6 100644 --- a/app/routes/leagues/$leagueId.tsx +++ b/app/routes/leagues/$leagueId.tsx @@ -372,15 +372,6 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { )} - {season && ( - - - - - - )} diff --git a/package-lock.json b/package-lock.json index c7f983d..8f81a3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,8 +34,6 @@ "react-day-picker": "^9.11.1", "react-dom": "^19.1.0", "react-router": "^7.7.1", - "socket.io": "^4.8.1", - "socket.io-client": "^4.8.1", "sonner": "^2.0.7", "svix": "^1.77.0", "tailwind-merge": "^3.3.1" @@ -53,7 +51,7 @@ "@types/react-dom": "^19.1.2", "dotenv-cli": "^8.0.0", "tailwindcss": "^4.1.4", - "tsx": "^4.20.6", + "tsx": "^4.19.2", "tw-animate-css": "^1.4.0", "typescript": "^5.8.3", "vite": "^6.3.3", @@ -2865,12 +2863,6 @@ "win32" ] }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, "node_modules/@stablelib/base64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", @@ -3186,15 +3178,6 @@ "@types/node": "*" } }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3255,6 +3238,7 @@ "version": "20.19.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz", "integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==", + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3426,15 +3410,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, "node_modules/baseline-browser-mapping": { "version": "2.8.16", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", @@ -3760,19 +3735,6 @@ "node": ">=6.6.0" } }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4121,125 +4083,6 @@ "node": ">= 0.8" } }, - "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -5414,15 +5257,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6231,173 +6065,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -7232,6 +6899,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, "license": "MIT" }, "node_modules/unpipe": { @@ -8062,35 +7730,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 38e9a3a..3de31db 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "build": "react-router build", "db:generate": "dotenv -- drizzle-kit generate", "db:migrate": "dotenv -- drizzle-kit migrate", - "dev": "dotenv -- tsx server/index.ts", - "start": "tsx server/index.ts", - "start:production": "drizzle-kit migrate && rm -rf drizzle drizzle.config.ts database && tsx server/index.ts", + "dev": "dotenv -- node server.js", + "start": "node server.js", + "start:production": "drizzle-kit migrate && rm -rf drizzle drizzle.config.ts database && node server.js", "typecheck": "react-router typegen && tsc -b" }, "dependencies": { @@ -40,8 +40,6 @@ "react-day-picker": "^9.11.1", "react-dom": "^19.1.0", "react-router": "^7.7.1", - "socket.io": "^4.8.1", - "socket.io-client": "^4.8.1", "sonner": "^2.0.7", "svix": "^1.77.0", "tailwind-merge": "^3.3.1" @@ -59,7 +57,7 @@ "@types/react-dom": "^19.1.2", "dotenv-cli": "^8.0.0", "tailwindcss": "^4.1.4", - "tsx": "^4.20.6", + "tsx": "^4.19.2", "tw-animate-css": "^1.4.0", "typescript": "^5.8.3", "vite": "^6.3.3", diff --git a/server/index.ts b/server.js similarity index 79% rename from server/index.ts rename to server.js index 2d10f29..50fea2b 100644 --- a/server/index.ts +++ b/server.js @@ -1,6 +1,5 @@ import compression from "compression"; import express from "express"; -import { createServer } from "http"; import morgan from "morgan"; // Short-circuit the type-checking of the built output. @@ -9,7 +8,6 @@ const DEVELOPMENT = process.env.NODE_ENV === "development"; const PORT = Number.parseInt(process.env.PORT || "3000"); const app = express(); -const httpServer = createServer(app); app.use(compression()); app.disable("x-powered-by"); @@ -44,13 +42,6 @@ if (DEVELOPMENT) { app.use(await import(BUILD_PATH).then((mod) => mod.app)); } -// Initialize Socket.IO -const socketModule = await import("./socket.js"); -const io = socketModule.setupSocketIO(httpServer); -socketModule.setSocketIO(io); -console.log("Socket.IO initialized"); - -httpServer.listen(PORT, () => { +app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); - console.log(`Socket.IO is ready for connections`); }); diff --git a/server/app.ts b/server/app.ts index bca8cca..9a60fc7 100644 --- a/server/app.ts +++ b/server/app.ts @@ -4,9 +4,9 @@ import express from "express"; import postgres from "postgres"; import { RouterContextProvider } from "react-router"; -import { DatabaseContext } from "../database/context.js"; -import * as schema from "../database/schema.js"; -import { expressValueContext } from "../app/contexts/express.js"; +import { DatabaseContext } from "~/database/context"; +import * as schema from "~/database/schema"; +import { expressValueContext } from "~/contexts/express"; export const app = express(); @@ -22,7 +22,7 @@ app.use( getLoadContext() { const context = new RouterContextProvider(); context.set(expressValueContext, "Hello from Express"); - return context as any; + return context; }, }), ); diff --git a/server/socket.ts b/server/socket.ts deleted file mode 100644 index daa3cbb..0000000 --- a/server/socket.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Server as HTTPServer } from "http"; -import { Server, Socket } from "socket.io"; - -export type SocketServer = Server; -export type SocketClient = Socket; - -export function setupSocketIO(httpServer: HTTPServer): SocketServer { - const io = new Server(httpServer, { - cors: { - origin: process.env.CLIENT_URL || "*", - methods: ["GET", "POST"], - }, - }); - - io.on("connection", (socket: SocketClient) => { - console.log(`Client connected: ${socket.id}`); - - // Join a draft room - socket.on("join-draft", (seasonId: string) => { - const roomName = `draft-${seasonId}`; - socket.join(roomName); - console.log(`Socket ${socket.id} joined draft room: ${roomName}`); - - // Notify others in the room - socket.to(roomName).emit("user-joined", { socketId: socket.id }); - }); - - // Leave a draft room - socket.on("leave-draft", (seasonId: string) => { - const roomName = `draft-${seasonId}`; - socket.leave(roomName); - console.log(`Socket ${socket.id} left draft room: ${roomName}`); - }); - - // Draft Actions - Phase 9 - // These events will be handled by API routes and then broadcast back via socket - // The client will call API routes which will then use getSocketIO() to broadcast - socket.on("make-pick", (_data) => { - console.log("make-pick event received, should use API route instead"); - }); - - socket.on("commissioner-start-draft", (_data) => { - console.log("commissioner-start-draft event received, should use API route instead"); - }); - - socket.on("commissioner-pause-draft", (_data) => { - console.log("commissioner-pause-draft event received, should use API route instead"); - }); - - socket.on("commissioner-force-pick", (_data) => { - console.log("commissioner-force-pick event received, should use API route instead"); - }); - - // Handle disconnection - socket.on("disconnect", () => { - console.log(`Client disconnected: ${socket.id}`); - }); - }); - - return io; -} - -// Export a singleton instance that will be set by the server -let ioInstance: SocketServer | null = null; - -export function setSocketIO(io: SocketServer) { - ioInstance = io; -} - -export function getSocketIO(): SocketServer { - if (!ioInstance) { - throw new Error("Socket.IO has not been initialized"); - } - return ioInstance; -} diff --git a/server/types.d.ts b/server/types.d.ts deleted file mode 100644 index 65fd064..0000000 --- a/server/types.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Type declarations for server-side modules - -declare module "virtual:react-router/server-build" { - import type { ServerBuild } from "@react-router/node"; - const serverBuild: ServerBuild; - export = serverBuild; -} diff --git a/tsconfig.node.json b/tsconfig.node.json index a717121..12107b4 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,12 +1,6 @@ { "extends": "./tsconfig.json", - "include": [ - "server/**/*.ts", - "vite.config.ts", - "database/**/*.ts", - "app/contexts/**/*.ts", - "app/models/**/*.ts" - ], + "include": ["server.js", "vite.config.ts"], "compilerOptions": { "composite": true, "strict": true, diff --git a/vite.config.ts b/vite.config.ts index 8f02a76..36dea4b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,9 +15,4 @@ export default defineConfig(({ isSsrBuild }) => ({ server: { allowedHosts: true, }, - resolve: { - alias: { - "~/database": "/database", - }, - }, }));