Revert Phase 8 and 9 implementations back to 0bef91e
This commit is contained in:
parent
95554e2664
commit
b9743aacc1
21 changed files with 13 additions and 2123 deletions
|
|
@ -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 (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="rounded-lg bg-green-500/10 p-4 text-center">
|
|
||||||
<p className="font-medium text-green-600 dark:text-green-400">
|
|
||||||
Draft Complete
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDraftStarted) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
The draft has not started yet. Click below to begin the draft.
|
|
||||||
</p>
|
|
||||||
<Button onClick={onStartDraft} className="w-full" size="lg">
|
|
||||||
<Play className="mr-2 h-4 w-4" />
|
|
||||||
Start Draft
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Draft Status */}
|
|
||||||
<div className="rounded-lg border bg-muted/50 p-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium">Draft Status:</span>
|
|
||||||
<span
|
|
||||||
className={`text-sm font-semibold ${
|
|
||||||
isDraftPaused
|
|
||||||
? "text-yellow-600 dark:text-yellow-400"
|
|
||||||
: "text-green-600 dark:text-green-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isDraftPaused ? "Paused" : "Active"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium">Current Pick:</span>
|
|
||||||
<span className="text-sm text-muted-foreground">{currentTeamName}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Control Buttons */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
{isDraftPaused ? (
|
|
||||||
<Button onClick={onResumeDraft} className="w-full" variant="default">
|
|
||||||
<Play className="mr-2 h-4 w-4" />
|
|
||||||
Resume Draft
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button onClick={onPauseDraft} className="w-full" variant="outline">
|
|
||||||
<Pause className="mr-2 h-4 w-4" />
|
|
||||||
Pause Draft
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={onForcePick}
|
|
||||||
className="w-full"
|
|
||||||
variant="destructive"
|
|
||||||
disabled={isDraftPaused}
|
|
||||||
>
|
|
||||||
<FastForward className="mr-2 h-4 w-4" />
|
|
||||||
Force Pick for {currentTeamName}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Help Text */}
|
|
||||||
<div className="rounded-lg bg-muted/30 p-3">
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
<strong>Force Pick:</strong> Automatically selects the top player from the
|
|
||||||
current team's queue, or the highest EV available player if queue is empty.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<div className="inline-block min-w-full">
|
|
||||||
{/* Header Row - Team Names */}
|
|
||||||
<div className="flex border-b-2 border-border bg-muted/50 sticky top-0 z-10">
|
|
||||||
<div className="w-16 flex-shrink-0 p-2 font-semibold text-sm border-r">
|
|
||||||
Round
|
|
||||||
</div>
|
|
||||||
{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 (
|
|
||||||
<div
|
|
||||||
key={team.id}
|
|
||||||
className={`flex-1 min-w-[140px] p-2 text-center font-semibold text-sm border-r last:border-r-0 ${
|
|
||||||
team.id === userTeamId ? "bg-primary/10" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="truncate" title={team.name}>
|
|
||||||
{team.name}
|
|
||||||
</div>
|
|
||||||
{timer && (
|
|
||||||
<div className="mt-1">
|
|
||||||
<DraftTimer timeRemaining={timer.timeRemaining} isActive={isOnClock} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Draft Rounds */}
|
|
||||||
{Array.from({ length: draftRounds }, (_, roundIndex) => {
|
|
||||||
const round = roundIndex + 1;
|
|
||||||
const isOddRound = round % 2 === 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={round} className="flex border-b border-border hover:bg-muted/30">
|
|
||||||
{/* Round Number */}
|
|
||||||
<div className="w-16 flex-shrink-0 p-2 text-center font-medium text-sm border-r bg-muted/30">
|
|
||||||
{round}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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 (
|
|
||||||
<div
|
|
||||||
key={team.id}
|
|
||||||
className={`flex-1 min-w-[140px] p-2 border-r last:border-r-0 ${
|
|
||||||
isUserTeam ? "bg-primary/5" : ""
|
|
||||||
} ${isCurrentPick ? "ring-2 ring-primary ring-inset" : ""}`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{/* Pick Number */}
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{round}.{String(isOddRound ? teamIndex + 1 : teams.length - teamIndex).padStart(2, "0")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Participant or Status */}
|
|
||||||
{participant ? (
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<div className="font-medium text-sm truncate" title={participant.name}>
|
|
||||||
{participant.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground truncate">
|
|
||||||
{participant.sport}
|
|
||||||
</div>
|
|
||||||
{pick && pick.pickedByType !== "owner" && (
|
|
||||||
<div className="text-xs text-muted-foreground italic">
|
|
||||||
({pick.pickedByType})
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : isCurrentPick ? (
|
|
||||||
<div className="text-sm font-medium text-primary">
|
|
||||||
On the clock
|
|
||||||
</div>
|
|
||||||
) : isPastPick ? (
|
|
||||||
<div className="text-xs text-muted-foreground italic">
|
|
||||||
Skipped
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
-
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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<number | null>(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 (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
<p>Your queue is empty</p>
|
|
||||||
<p className="text-sm mt-2">Add players from the Available Players list</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{sortedQueue.map((item, index) => {
|
|
||||||
const participant = participantMap.get(item.participantId);
|
|
||||||
if (!participant) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
draggable
|
|
||||||
onDragStart={() => 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 */}
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<GripVertical className="h-4 w-4" />
|
|
||||||
<span className="text-sm font-semibold w-6">{index + 1}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Player Info */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="font-medium truncate">{participant.name}</div>
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{participant.sport}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
EV: {participant.expectedValue.toFixed(1)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Remove Button */}
|
|
||||||
{onRemove && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onRemove(item.id)}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className={`text-sm font-mono font-semibold ${getColorClass()} ${isActive ? "font-bold" : ""}`}>
|
|
||||||
{formatTime(timeRemaining)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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<string>("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 (
|
|
||||||
<div className="flex flex-col h-full overflow-hidden">
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="space-y-3 mb-4">
|
|
||||||
{/* Search */}
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search players..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-9"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sport Filter and Sort */}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select value={sportFilter} onValueChange={setSportFilter}>
|
|
||||||
<SelectTrigger className="flex-1">
|
|
||||||
<SelectValue placeholder="All Sports" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All Sports</SelectItem>
|
|
||||||
{sports.map((sport) => (
|
|
||||||
<SelectItem key={sport} value={sport}>
|
|
||||||
{sport}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSortBy(sortBy === "ev" ? "name" : "ev")}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<ArrowUpDown className="h-4 w-4" />
|
|
||||||
{sortBy === "ev" ? "By EV" : "By Name"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Player Count */}
|
|
||||||
<div className="text-sm text-muted-foreground mb-3">
|
|
||||||
{filteredParticipants.length} player{filteredParticipants.length !== 1 ? "s" : ""} available
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Player Table */}
|
|
||||||
<div className="flex-1 overflow-y-auto border rounded-md">
|
|
||||||
{filteredParticipants.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
No players found
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<table className="w-full text-sm border-separate border-spacing-0">
|
|
||||||
<thead className="sticky top-0 bg-muted border-b">
|
|
||||||
<tr>
|
|
||||||
<th className="text-left p-2 font-semibold">Player</th>
|
|
||||||
<th className="text-left p-2 font-semibold">Sport</th>
|
|
||||||
<th className="text-right p-2 font-semibold">EV</th>
|
|
||||||
<th className="text-right p-2 font-semibold">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filteredParticipants.map((participant) => (
|
|
||||||
<tr
|
|
||||||
key={participant.id}
|
|
||||||
className="border-b last:border-b-0 hover:bg-accent/50 transition-colors"
|
|
||||||
>
|
|
||||||
<td className="p-2 font-medium">{participant.name}</td>
|
|
||||||
<td className="p-2">
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{participant.sport}
|
|
||||||
</Badge>
|
|
||||||
</td>
|
|
||||||
<td className="p-2 text-right text-muted-foreground">
|
|
||||||
{participant.expectedValue.toFixed(1)}
|
|
||||||
</td>
|
|
||||||
<td className="p-2 text-right">
|
|
||||||
<div className="flex items-center justify-end gap-1">
|
|
||||||
{onAddToQueue && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onAddToQueue(participant.id)}
|
|
||||||
className="h-7 px-2 text-xs"
|
|
||||||
>
|
|
||||||
Queue
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{canDraft && onDraftPlayer && (
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onDraftPlayer(participant.id)}
|
|
||||||
className="h-7 px-2 text-xs"
|
|
||||||
>
|
|
||||||
Draft
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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<Socket | null>(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(<K extends keyof DraftSocketEvents>(
|
|
||||||
event: K,
|
|
||||||
handler: DraftSocketEvents[K]
|
|
||||||
) => {
|
|
||||||
if (socket) {
|
|
||||||
socket.on(event as string, handler as any);
|
|
||||||
}
|
|
||||||
}, [socket]);
|
|
||||||
|
|
||||||
// Helper to remove event listeners
|
|
||||||
const off = useCallback(<K extends keyof DraftSocketEvents>(
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -139,7 +139,6 @@ export async function findSeasonWithSportsSeasons(
|
||||||
sportsSeason: {
|
sportsSeason: {
|
||||||
with: {
|
with: {
|
||||||
sport: true,
|
sport: true,
|
||||||
participants: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,8 @@ export default [
|
||||||
route("leagues/new", "routes/leagues/new.tsx"),
|
route("leagues/new", "routes/leagues/new.tsx"),
|
||||||
route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"),
|
route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"),
|
||||||
route("leagues/:leagueId/settings", "routes/leagues/$leagueId.settings.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("teams/:teamId/settings", "routes/teams/$teamId.settings.tsx"),
|
||||||
route("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"),
|
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("user-profile", "routes/user-profile.tsx"),
|
||||||
route("how-to-play", "routes/how-to-play.tsx"),
|
route("how-to-play", "routes/how-to-play.tsx"),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<ReturnType<typeof loader>>["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 (
|
|
||||||
<div className="min-h-screen bg-background">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="border-b bg-card">
|
|
||||||
<div className="px-6 py-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold">{league.name} Draft</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{season.year} Season • Pick {currentPickNumber} of {totalPicks}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{isCommissioner && (
|
|
||||||
<span className="rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
|
|
||||||
Commissioner
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{userTeam && (
|
|
||||||
<span className="rounded-full bg-secondary px-3 py-1 text-xs font-medium">
|
|
||||||
{userTeam.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<main className="px-6 py-6">
|
|
||||||
{isDraftComplete ? (
|
|
||||||
<div className="rounded-lg border bg-card p-8 text-center">
|
|
||||||
<h2 className="text-2xl font-bold">Draft Complete!</h2>
|
|
||||||
<p className="mt-2 text-muted-foreground">
|
|
||||||
All picks have been made. The season is ready to begin.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Draft Board - Full Width */}
|
|
||||||
<div className="rounded-lg border bg-card p-6">
|
|
||||||
<h2 className="mb-4 text-xl font-bold">Draft Board</h2>
|
|
||||||
<DraftGrid
|
|
||||||
teams={teams}
|
|
||||||
draftPicks={draftPicks}
|
|
||||||
participants={participants}
|
|
||||||
draftRounds={season.draftRounds}
|
|
||||||
currentPickNumber={currentPickNumber}
|
|
||||||
userTeamId={userTeam?.id}
|
|
||||||
timers={timers}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 2x2 Grid Below */}
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
|
||||||
{/* Available Players */}
|
|
||||||
<div className="rounded-lg border bg-card p-6 flex flex-col" style={{ height: "600px" }}>
|
|
||||||
<h2 className="mb-4 text-xl font-bold">Available Players</h2>
|
|
||||||
<PlayerList
|
|
||||||
participants={participants}
|
|
||||||
draftPicks={draftPicks}
|
|
||||||
canDraft={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Your Queue */}
|
|
||||||
<div className="rounded-lg border bg-card p-6 flex flex-col" style={{ height: "600px" }}>
|
|
||||||
<h2 className="mb-4 text-xl font-bold">Your Queue</h2>
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
<DraftQueue
|
|
||||||
queueItems={queueItems}
|
|
||||||
participants={participants}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Draft Timer */}
|
|
||||||
<div className="rounded-lg border bg-card p-6">
|
|
||||||
<h2 className="mb-4 text-xl font-bold">Draft Timer</h2>
|
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
|
||||||
Timer will be implemented in Phase 8
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Commissioner Controls */}
|
|
||||||
{isCommissioner ? (
|
|
||||||
<div className="rounded-lg border bg-card p-6">
|
|
||||||
<h2 className="mb-4 text-xl font-bold">Commissioner Controls</h2>
|
|
||||||
<CommissionerControls
|
|
||||||
isDraftStarted={isDraftStarted}
|
|
||||||
isDraftPaused={isDraftPaused}
|
|
||||||
isDraftComplete={isDraftComplete}
|
|
||||||
currentTeamName={currentTeamName}
|
|
||||||
onStartDraft={handleStartDraft}
|
|
||||||
onPauseDraft={handlePauseDraft}
|
|
||||||
onResumeDraft={handleResumeDraft}
|
|
||||||
onForcePick={handleForcePick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-lg border bg-card p-6">
|
|
||||||
<h2 className="mb-4 text-xl font-bold">Draft Info</h2>
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Current Pick:</span>
|
|
||||||
<span className="ml-2 font-medium">{currentPickNumber} of {totalPicks}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Draft Rounds:</span>
|
|
||||||
<span className="ml-2 font-medium">{season.draftRounds}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Current Team:</span>
|
|
||||||
<span className="ml-2 font-medium">{currentTeamName}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -372,15 +372,6 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
{season && (
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<Link to={`/leagues/${league.id}/draft`}>
|
|
||||||
<Button className="w-full" variant="default">
|
|
||||||
{season.status === "draft" ? "Go to Draft Room" : "View Draft Room"}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</CardContent>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
|
|
||||||
367
package-lock.json
generated
367
package-lock.json
generated
|
|
@ -34,8 +34,6 @@
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router": "^7.7.1",
|
"react-router": "^7.7.1",
|
||||||
"socket.io": "^4.8.1",
|
|
||||||
"socket.io-client": "^4.8.1",
|
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"svix": "^1.77.0",
|
"svix": "^1.77.0",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
|
|
@ -53,7 +51,7 @@
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.19.2",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.3",
|
"vite": "^6.3.3",
|
||||||
|
|
@ -2865,12 +2863,6 @@
|
||||||
"win32"
|
"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": {
|
"node_modules/@stablelib/base64": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
|
|
@ -3186,15 +3178,6 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
|
@ -3255,6 +3238,7 @@
|
||||||
"version": "20.19.20",
|
"version": "20.19.20",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz",
|
||||||
"integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==",
|
"integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
|
|
@ -3426,15 +3410,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.16",
|
"version": "2.8.16",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
|
||||||
|
|
@ -3760,19 +3735,6 @@
|
||||||
"node": ">=6.6.0"
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|
@ -4121,125 +4083,6 @@
|
||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.3",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
"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": "^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": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|
@ -6231,173 +6065,6 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/sonner": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
||||||
|
|
@ -7232,6 +6899,7 @@
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
|
|
@ -8062,35 +7730,6 @@
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -6,9 +6,9 @@
|
||||||
"build": "react-router build",
|
"build": "react-router build",
|
||||||
"db:generate": "dotenv -- drizzle-kit generate",
|
"db:generate": "dotenv -- drizzle-kit generate",
|
||||||
"db:migrate": "dotenv -- drizzle-kit migrate",
|
"db:migrate": "dotenv -- drizzle-kit migrate",
|
||||||
"dev": "dotenv -- tsx server/index.ts",
|
"dev": "dotenv -- node server.js",
|
||||||
"start": "tsx server/index.ts",
|
"start": "node server.js",
|
||||||
"start:production": "drizzle-kit migrate && rm -rf drizzle drizzle.config.ts database && tsx server/index.ts",
|
"start:production": "drizzle-kit migrate && rm -rf drizzle drizzle.config.ts database && node server.js",
|
||||||
"typecheck": "react-router typegen && tsc -b"
|
"typecheck": "react-router typegen && tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -40,8 +40,6 @@
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router": "^7.7.1",
|
"react-router": "^7.7.1",
|
||||||
"socket.io": "^4.8.1",
|
|
||||||
"socket.io-client": "^4.8.1",
|
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"svix": "^1.77.0",
|
"svix": "^1.77.0",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
|
|
@ -59,7 +57,7 @@
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.19.2",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.3",
|
"vite": "^6.3.3",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { createServer } from "http";
|
|
||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
|
|
||||||
// Short-circuit the type-checking of the built output.
|
// 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 PORT = Number.parseInt(process.env.PORT || "3000");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const httpServer = createServer(app);
|
|
||||||
|
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
app.disable("x-powered-by");
|
app.disable("x-powered-by");
|
||||||
|
|
@ -44,13 +42,6 @@ if (DEVELOPMENT) {
|
||||||
app.use(await import(BUILD_PATH).then((mod) => mod.app));
|
app.use(await import(BUILD_PATH).then((mod) => mod.app));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Socket.IO
|
app.listen(PORT, () => {
|
||||||
const socketModule = await import("./socket.js");
|
|
||||||
const io = socketModule.setupSocketIO(httpServer);
|
|
||||||
socketModule.setSocketIO(io);
|
|
||||||
console.log("Socket.IO initialized");
|
|
||||||
|
|
||||||
httpServer.listen(PORT, () => {
|
|
||||||
console.log(`Server is running on http://localhost:${PORT}`);
|
console.log(`Server is running on http://localhost:${PORT}`);
|
||||||
console.log(`Socket.IO is ready for connections`);
|
|
||||||
});
|
});
|
||||||
|
|
@ -4,9 +4,9 @@ import express from "express";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
import { RouterContextProvider } from "react-router";
|
import { RouterContextProvider } from "react-router";
|
||||||
|
|
||||||
import { DatabaseContext } from "../database/context.js";
|
import { DatabaseContext } from "~/database/context";
|
||||||
import * as schema from "../database/schema.js";
|
import * as schema from "~/database/schema";
|
||||||
import { expressValueContext } from "../app/contexts/express.js";
|
import { expressValueContext } from "~/contexts/express";
|
||||||
|
|
||||||
export const app = express();
|
export const app = express();
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ app.use(
|
||||||
getLoadContext() {
|
getLoadContext() {
|
||||||
const context = new RouterContextProvider();
|
const context = new RouterContextProvider();
|
||||||
context.set(expressValueContext, "Hello from Express");
|
context.set(expressValueContext, "Hello from Express");
|
||||||
return context as any;
|
return context;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
7
server/types.d.ts
vendored
7
server/types.d.ts
vendored
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"include": [
|
"include": ["server.js", "vite.config.ts"],
|
||||||
"server/**/*.ts",
|
|
||||||
"vite.config.ts",
|
|
||||||
"database/**/*.ts",
|
|
||||||
"app/contexts/**/*.ts",
|
|
||||||
"app/models/**/*.ts"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,4 @@ export default defineConfig(({ isSsrBuild }) => ({
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"~/database": "/database",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue