feat: add autodraft status and team connection indicators to draft grid

This commit is contained in:
Chris Parsons 2025-10-21 23:22:17 -07:00
parent 86ae5014ec
commit ff97e25438
18 changed files with 3646 additions and 117 deletions

View file

@ -0,0 +1,140 @@
import { useState } from "react";
import { Switch } from "~/components/ui/switch";
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
import { Label } from "~/components/ui/label";
interface AutodraftSettingsProps {
seasonId: string;
teamId: string;
isEnabled: boolean;
mode: "next_pick" | "while_on";
isMyTurn: boolean;
onUpdate: (isEnabled: boolean, mode: "next_pick" | "while_on") => void;
}
export function AutodraftSettings({
seasonId,
teamId,
isEnabled,
mode,
isMyTurn,
onUpdate,
}: AutodraftSettingsProps) {
const [localEnabled, setLocalEnabled] = useState(isEnabled);
const [localMode, setLocalMode] = useState<"next_pick" | "while_on">(mode);
const [isUpdating, setIsUpdating] = useState(false);
const handleToggle = async (checked: boolean) => {
setLocalEnabled(checked);
setIsUpdating(true);
try {
const formData = new FormData();
formData.append("seasonId", seasonId);
formData.append("teamId", teamId);
formData.append("isEnabled", checked.toString());
formData.append("mode", localMode);
const response = await fetch("/api/autodraft/update", {
method: "POST",
body: formData,
});
if (response.ok) {
onUpdate(checked, localMode);
} else {
// Revert on error
setLocalEnabled(!checked);
console.error("Failed to update autodraft settings");
}
} catch (error) {
// Revert on error
setLocalEnabled(!checked);
console.error("Error updating autodraft settings:", error);
} finally {
setIsUpdating(false);
}
};
const handleModeChange = async (newMode: "next_pick" | "while_on") => {
setLocalMode(newMode);
setIsUpdating(true);
try {
const formData = new FormData();
formData.append("seasonId", seasonId);
formData.append("teamId", teamId);
formData.append("isEnabled", localEnabled.toString());
formData.append("mode", newMode);
const response = await fetch("/api/autodraft/update", {
method: "POST",
body: formData,
});
if (response.ok) {
onUpdate(localEnabled, newMode);
} else {
// Revert on error
setLocalMode(localMode);
console.error("Failed to update autodraft mode");
}
} catch (error) {
// Revert on error
setLocalMode(localMode);
console.error("Error updating autodraft mode:", error);
} finally {
setIsUpdating(false);
}
};
return (
<div className="border-t pt-4 mt-4">
<div className="flex items-center justify-between mb-3">
<Label htmlFor="autodraft-switch" className="text-sm font-semibold">
Autodraft
</Label>
<Switch
id="autodraft-switch"
checked={localEnabled}
onCheckedChange={handleToggle}
disabled={isMyTurn || isUpdating}
/>
</div>
{localEnabled && (
<RadioGroup
value={localMode}
onValueChange={(value) => handleModeChange(value as "next_pick" | "while_on")}
disabled={isMyTurn || isUpdating}
className="space-y-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="next_pick" id="next_pick" disabled={isMyTurn || isUpdating} />
<Label
htmlFor="next_pick"
className={`text-sm ${isMyTurn || isUpdating ? "text-muted-foreground" : "cursor-pointer"}`}
>
Next Pick
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="while_on" id="while_on" disabled={isMyTurn || isUpdating} />
<Label
htmlFor="while_on"
className={`text-sm ${isMyTurn || isUpdating ? "text-muted-foreground" : "cursor-pointer"}`}
>
While On
</Label>
</div>
</RadioGroup>
)}
{isMyTurn && (
<p className="text-xs text-muted-foreground mt-2">
Autodraft settings are disabled during your pick
</p>
)}
</div>
);
}

View file

@ -23,6 +23,8 @@ interface DraftGridProps {
isCommissioner?: boolean; isCommissioner?: boolean;
onForceAutopick?: (pickNumber: number, teamId: string) => void; onForceAutopick?: (pickNumber: number, teamId: string) => void;
onForceManualPick?: (pickNumber: number, teamId: string) => void; onForceManualPick?: (pickNumber: number, teamId: string) => void;
autodraftStatus?: Record<string, boolean>;
connectedTeams?: Set<string>;
} }
export function DraftGrid({ export function DraftGrid({
@ -35,6 +37,8 @@ export function DraftGrid({
isCommissioner = false, isCommissioner = false,
onForceAutopick, onForceAutopick,
onForceManualPick, onForceManualPick,
autodraftStatus = {},
connectedTeams = new Set(),
}: DraftGridProps) { }: DraftGridProps) {
const totalTeams = draftSlots.length; const totalTeams = draftSlots.length;
@ -46,9 +50,14 @@ export function DraftGrid({
<div className="flex gap-2 mb-2"> <div className="flex gap-2 mb-2">
{draftSlots.map((slot) => { {draftSlots.map((slot) => {
const teamTime = teamTimers?.[slot.team.id]; const teamTime = teamTimers?.[slot.team.id];
const isAutodraft = autodraftStatus[slot.team.id] || false;
const isConnected = connectedTeams.has(slot.team.id);
return ( return (
<div key={slot.id} className="flex-1 min-w-32 text-center"> <div key={slot.id} className="flex-1 min-w-32 text-center">
<div className="font-semibold text-sm truncate px-2"> <div
className={`font-semibold text-sm truncate px-2 ${!isConnected ? "italic text-muted-foreground" : ""}`}
>
{slot.team.name} {slot.team.name}
</div> </div>
{teamTimers && formatTime && ( {teamTimers && formatTime && (
@ -64,6 +73,9 @@ export function DraftGrid({
}`} }`}
> >
{formatTime(teamTime)} {formatTime(teamTime)}
{isAutodraft && (
<span className="ml-1 text-muted-foreground">(auto)</span>
)}
</div> </div>
)} )}
</div> </div>
@ -123,7 +135,13 @@ export function DraftGrid({
); );
// Wrap with context menu if commissioner and current unpicked cell // Wrap with context menu if commissioner and current unpicked cell
if (isCommissioner && !isPicked && isCurrent && onForceAutopick && onForceManualPick) { if (
isCommissioner &&
!isPicked &&
isCurrent &&
onForceAutopick &&
onForceManualPick
) {
return ( return (
<ContextMenu key={pickNumber}> <ContextMenu key={pickNumber}>
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
@ -131,12 +149,16 @@ export function DraftGrid({
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem <ContextMenuItem
onClick={() => onForceAutopick(pickNumber, slot.team.id)} onClick={() =>
onForceAutopick(pickNumber, slot.team.id)
}
> >
Force Auto Pick Force Auto Pick
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem <ContextMenuItem
onClick={() => onForceManualPick(pickNumber, slot.team.id)} onClick={() =>
onForceManualPick(pickNumber, slot.team.id)
}
> >
Force Manual Pick Force Manual Pick
</ContextMenuItem> </ContextMenuItem>

View file

@ -0,0 +1,287 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AutodraftSettings } from '../AutodraftSettings';
// Mock fetch
global.fetch = vi.fn();
describe('AutodraftSettings Component', () => {
const defaultProps = {
seasonId: 'season-123',
teamId: 'team-456',
isEnabled: false,
mode: 'next_pick' as const,
isMyTurn: false,
onUpdate: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => ({ success: true }),
});
});
describe('Rendering', () => {
it('should render the autodraft switch', () => {
render(<AutodraftSettings {...defaultProps} />);
expect(screen.getByText('Autodraft')).toBeInTheDocument();
expect(screen.getByRole('switch')).toBeInTheDocument();
});
it('should not show mode options when autodraft is disabled', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
expect(screen.queryByText('Next Pick')).not.toBeInTheDocument();
expect(screen.queryByText('While On')).not.toBeInTheDocument();
});
it('should show mode options when autodraft is enabled', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} />);
expect(screen.getByText('Next Pick')).toBeInTheDocument();
expect(screen.getByText('While On')).toBeInTheDocument();
});
it('should show disabled message when it is user turn', () => {
render(<AutodraftSettings {...defaultProps} isMyTurn={true} />);
expect(screen.getByText(/Autodraft settings are disabled during your pick/i)).toBeInTheDocument();
});
});
describe('Switch Toggle', () => {
it('should enable autodraft when switch is toggled on', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} onUpdate={onUpdate} />);
const switchElement = screen.getByRole('switch');
fireEvent.click(switchElement);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
'/api/autodraft/update',
expect.objectContaining({
method: 'POST',
})
);
});
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(true, 'next_pick');
});
});
it('should disable autodraft when switch is toggled off', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} isEnabled={true} onUpdate={onUpdate} />);
const switchElement = screen.getByRole('switch');
fireEvent.click(switchElement);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled();
});
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(false, 'next_pick');
});
});
it('should be disabled when it is user turn', () => {
render(<AutodraftSettings {...defaultProps} isMyTurn={true} />);
const switchElement = screen.getByRole('switch');
expect(switchElement).toBeDisabled();
});
it('should handle API errors gracefully', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
(global.fetch as any).mockResolvedValueOnce({
ok: false,
json: async () => ({ error: 'Server error' }),
});
render(<AutodraftSettings {...defaultProps} />);
const switchElement = screen.getByRole('switch');
fireEvent.click(switchElement);
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalled();
});
// Switch should revert to original state
expect(switchElement).not.toBeChecked();
consoleSpy.mockRestore();
});
});
describe('Mode Selection', () => {
it('should select next_pick mode by default', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="next_pick" />);
const nextPickRadio = screen.getByRole('radio', { name: /next pick/i });
expect(nextPickRadio).toBeChecked();
});
it('should select while_on mode when specified', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="while_on" />);
const whileOnRadio = screen.getByRole('radio', { name: /while on/i });
expect(whileOnRadio).toBeChecked();
});
it('should change mode when radio button is clicked', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="next_pick" onUpdate={onUpdate} />);
const whileOnRadio = screen.getByRole('radio', { name: /while on/i });
fireEvent.click(whileOnRadio);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled();
});
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(true, 'while_on');
});
});
it('should disable mode selection when it is user turn', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} isMyTurn={true} />);
const nextPickRadio = screen.getByRole('radio', { name: /next pick/i });
const whileOnRadio = screen.getByRole('radio', { name: /while on/i });
expect(nextPickRadio).toBeDisabled();
expect(whileOnRadio).toBeDisabled();
});
});
describe('API Integration', () => {
it('should send correct data when enabling autodraft', async () => {
render(<AutodraftSettings {...defaultProps} />);
const switchElement = screen.getByRole('switch');
fireEvent.click(switchElement);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
'/api/autodraft/update',
expect.objectContaining({
method: 'POST',
body: expect.any(FormData),
})
);
});
const fetchCall = (global.fetch as any).mock.calls[0];
const formData = fetchCall[1].body as FormData;
expect(formData.get('seasonId')).toBe('season-123');
expect(formData.get('teamId')).toBe('team-456');
expect(formData.get('isEnabled')).toBe('true');
expect(formData.get('mode')).toBe('next_pick');
});
it('should send correct data when changing mode', async () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="next_pick" />);
const whileOnRadio = screen.getByRole('radio', { name: /while on/i });
fireEvent.click(whileOnRadio);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled();
});
const fetchCall = (global.fetch as any).mock.calls[0];
const formData = fetchCall[1].body as FormData;
expect(formData.get('mode')).toBe('while_on');
expect(formData.get('isEnabled')).toBe('true');
});
it('should show loading state during API call', async () => {
let resolvePromise: any;
const fetchPromise = new Promise((resolve) => {
resolvePromise = resolve;
});
(global.fetch as any).mockReturnValueOnce(fetchPromise);
render(<AutodraftSettings {...defaultProps} />);
const switchElement = screen.getByRole('switch');
fireEvent.click(switchElement);
// Switch should be disabled during update
await waitFor(() => {
expect(switchElement).toBeDisabled();
});
// Resolve the promise
resolvePromise({ ok: true, json: async () => ({ success: true }) });
// Switch should be enabled again
await waitFor(() => {
expect(switchElement).not.toBeDisabled();
});
});
});
describe('Edge Cases', () => {
it('should handle rapid toggle clicks', async () => {
render(<AutodraftSettings {...defaultProps} />);
const switchElement = screen.getByRole('switch');
// Click multiple times rapidly
fireEvent.click(switchElement);
fireEvent.click(switchElement);
fireEvent.click(switchElement);
// Should only process the first click while updating
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledTimes(1);
});
});
it('should handle network errors', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
(global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
render(<AutodraftSettings {...defaultProps} />);
const switchElement = screen.getByRole('switch');
fireEvent.click(switchElement);
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalled();
});
consoleSpy.mockRestore();
});
it('should maintain state consistency on error', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false,
json: async () => ({ error: 'Server error' }),
});
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
const switchElement = screen.getByRole('switch');
expect(switchElement).not.toBeChecked();
fireEvent.click(switchElement);
// Should revert to original state on error
await waitFor(() => {
expect(switchElement).not.toBeChecked();
});
});
});
});

View file

@ -0,0 +1,29 @@
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "app/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View file

@ -8,7 +8,7 @@ interface UseDraftSocketReturn {
off: (event: string, callback?: (...args: any[]) => void) => void; off: (event: string, callback?: (...args: any[]) => void) => void;
} }
export function useDraftSocket(seasonId: string): UseDraftSocketReturn { export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocketReturn {
const socketRef = useRef<Socket | null>(null); const socketRef = useRef<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
@ -24,8 +24,8 @@ export function useDraftSocket(seasonId: string): UseDraftSocketReturn {
socket.on("connect", () => { socket.on("connect", () => {
console.log("Connected to Socket.IO:", socket.id); console.log("Connected to Socket.IO:", socket.id);
setIsConnected(true); setIsConnected(true);
// Join the draft room // Join the draft room with optional teamId
socket.emit("join-draft", seasonId); socket.emit("join-draft", seasonId, teamId);
}); });
socket.on("disconnect", () => { socket.on("disconnect", () => {
@ -45,7 +45,7 @@ export function useDraftSocket(seasonId: string): UseDraftSocketReturn {
socket.disconnect(); socket.disconnect();
} }
}; };
}, [seasonId]); }, [seasonId, teamId]);
// Helper to subscribe to events // Helper to subscribe to events
const on = (event: string, callback: (...args: any[]) => void) => { const on = (event: string, callback: (...args: any[]) => void) => {

View file

@ -0,0 +1,79 @@
import { getAuth } from "@clerk/react-router/server";
import { database } from "../../../database/context";
import * as schema from "../../../database/schema";
import { eq, and } from "drizzle-orm";
import { getSocketIO } from "../../../server/socket";
export async function action(args: any) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const formData = await request.formData();
const seasonId = formData.get("seasonId") as string;
const teamId = formData.get("teamId") as string;
const isEnabled = formData.get("isEnabled") === "true";
const mode = formData.get("mode") as "next_pick" | "while_on";
if (!seasonId || !teamId || !mode) {
return Response.json({ error: "Missing required fields" }, { status: 400 });
}
const db = database();
// Verify user owns this team
const team = await db.query.teams.findFirst({
where: eq(schema.teams.id, teamId),
});
if (!team || team.ownerId !== userId) {
return Response.json({ error: "Unauthorized" }, { status: 403 });
}
// Check if autodraft settings exist
const existingSettings = await db.query.autodraftSettings.findFirst({
where: and(
eq(schema.autodraftSettings.seasonId, seasonId),
eq(schema.autodraftSettings.teamId, teamId)
),
});
let settings;
if (existingSettings) {
// Update existing settings
[settings] = await db
.update(schema.autodraftSettings)
.set({
isEnabled,
mode,
updatedAt: new Date(),
})
.where(eq(schema.autodraftSettings.id, existingSettings.id))
.returning();
} else {
// Create new settings
[settings] = await db
.insert(schema.autodraftSettings)
.values({
seasonId,
teamId,
isEnabled,
mode,
})
.returning();
}
// Emit socket event to notify all clients in the draft room
const io = getSocketIO();
io.to(`draft-${seasonId}`).emit("autodraft-updated", {
teamId,
isEnabled,
mode,
});
return Response.json({ success: true, settings });
}

View file

@ -15,6 +15,7 @@ import {
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "~/components/ui/context-menu"; } from "~/components/ui/context-menu";
import { AutodraftSettings } from "~/components/AutodraftSettings";
export async function loader(args: any) { export async function loader(args: any) {
const { params } = args; const { params } = args;
@ -150,6 +151,21 @@ export async function loader(args: any) {
where: eq(schema.draftTimers.seasonId, seasonId), where: eq(schema.draftTimers.seasonId, seasonId),
}); });
// Load autodraft settings for all teams
const autodraftSettings = await db.query.autodraftSettings.findMany({
where: eq(schema.autodraftSettings.seasonId, seasonId),
});
// Load user's autodraft settings if they have a team
const userAutodraftSettings = userTeam
? await db.query.autodraftSettings.findFirst({
where: and(
eq(schema.autodraftSettings.seasonId, seasonId),
eq(schema.autodraftSettings.teamId, userTeam.id)
),
})
: null;
return { return {
season, season,
draftSlots, draftSlots,
@ -158,6 +174,8 @@ export async function loader(args: any) {
userTeam, userTeam,
userQueue, userQueue,
timers, timers,
autodraftSettings,
userAutodraftSettings,
isCommissioner: !!isCommissioner, isCommissioner: !!isCommissioner,
currentUserId: userId, currentUserId: userId,
}; };
@ -172,9 +190,11 @@ export default function DraftRoom() {
userTeam, userTeam,
userQueue, userQueue,
timers, timers,
autodraftSettings,
userAutodraftSettings,
isCommissioner, isCommissioner,
} = useLoaderData<typeof loader>(); } = useLoaderData<typeof loader>();
const { isConnected, on, off } = useDraftSocket(season.id); const { isConnected, on, off } = useDraftSocket(season.id, userTeam?.id);
const [picks, setPicks] = useState(draftPicks); const [picks, setPicks] = useState(draftPicks);
const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1); const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@ -183,6 +203,25 @@ export default function DraftRoom() {
const [queue, setQueue] = useState(userQueue); const [queue, setQueue] = useState(userQueue);
const [isPaused, setIsPaused] = useState(season.draftPaused || false); const [isPaused, setIsPaused] = useState(season.draftPaused || false);
const [isDraftComplete, setIsDraftComplete] = useState(season.status === "active"); const [isDraftComplete, setIsDraftComplete] = useState(season.status === "active");
// Track autodraft status for all teams
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, boolean>>(() => {
const status: Record<string, boolean> = {};
autodraftSettings.forEach((setting: any) => {
status[setting.teamId] = setting.isEnabled;
});
return status;
});
// Track connected teams
const [connectedTeams, setConnectedTeams] = useState<Set<string>>(new Set());
// Track user's autodraft settings
const [userAutodraft, setUserAutodraft] = useState({
isEnabled: userAutodraftSettings?.isEnabled || false,
mode: (userAutodraftSettings?.mode || "next_pick") as "next_pick" | "while_on",
});
const [teamTimers, setTeamTimers] = useState<Record<string, number>>(() => { const [teamTimers, setTeamTimers] = useState<Record<string, number>>(() => {
// Initialize with timers from loader data, or use initial time if draft hasn't started // Initialize with timers from loader data, or use initial time if draft hasn't started
const timersMap: Record<string, number> = {}; const timersMap: Record<string, number> = {};
@ -236,11 +275,44 @@ export default function DraftRoom() {
setIsDraftComplete(true); setIsDraftComplete(true);
}; };
const handleAutodraftUpdated = (data: { teamId: string; isEnabled: boolean; mode: "next_pick" | "while_on" }) => {
console.log("Autodraft updated:", data);
setAutodraftStatus((prev) => ({
...prev,
[data.teamId]: data.isEnabled,
}));
// Update user's local state if it's their team
if (userTeam && data.teamId === userTeam.id) {
setUserAutodraft({
isEnabled: data.isEnabled,
mode: data.mode,
});
}
};
const handleTeamConnected = (data: { teamId: string }) => {
console.log("Team connected:", data.teamId);
setConnectedTeams((prev) => new Set(prev).add(data.teamId));
};
const handleTeamDisconnected = (data: { teamId: string }) => {
console.log("Team disconnected:", data.teamId);
setConnectedTeams((prev) => {
const newSet = new Set(prev);
newSet.delete(data.teamId);
return newSet;
});
};
on("pick-made", handlePickMade); on("pick-made", handlePickMade);
on("timer-update", handleTimerUpdate); on("timer-update", handleTimerUpdate);
on("draft-paused", handleDraftPaused); on("draft-paused", handleDraftPaused);
on("draft-resumed", handleDraftResumed); on("draft-resumed", handleDraftResumed);
on("draft-completed", handleDraftCompleted); on("draft-completed", handleDraftCompleted);
on("autodraft-updated", handleAutodraftUpdated);
on("team-connected", handleTeamConnected);
on("team-disconnected", handleTeamDisconnected);
return () => { return () => {
off("pick-made", handlePickMade); off("pick-made", handlePickMade);
@ -248,8 +320,11 @@ export default function DraftRoom() {
off("draft-paused", handleDraftPaused); off("draft-paused", handleDraftPaused);
off("draft-resumed", handleDraftResumed); off("draft-resumed", handleDraftResumed);
off("draft-completed", handleDraftCompleted); off("draft-completed", handleDraftCompleted);
off("autodraft-updated", handleAutodraftUpdated);
off("team-connected", handleTeamConnected);
off("team-disconnected", handleTeamDisconnected);
}; };
}, [on, off, currentPick]); }, [on, off, currentPick, userTeam]);
// Queue handlers // Queue handlers
const handleAddToQueue = async (participantId: string) => { const handleAddToQueue = async (participantId: string) => {
@ -421,8 +496,9 @@ export default function DraftRoom() {
const currentDraftSlot = draftSlots.find( const currentDraftSlot = draftSlots.find(
(slot) => slot.draftOrder === pickInRound (slot) => slot.draftOrder === pickInRound
); );
const isMyTurn = const isMyTurn = !!(
userTeam && currentDraftSlot && currentDraftSlot.team.id === userTeam.id; userTeam && currentDraftSlot && currentDraftSlot.team.id === userTeam.id
);
const canPick = isMyTurn && season.status === "draft"; // Only team owner on their turn const canPick = isMyTurn && season.status === "draft"; // Only team owner on their turn
// Format timer display // Format timer display
@ -609,13 +685,19 @@ export default function DraftRoom() {
<div className="flex gap-2 mb-2"> <div className="flex gap-2 mb-2">
{draftSlots.map((slot) => { {draftSlots.map((slot) => {
const teamTime = teamTimers[slot.team.id]; const teamTime = teamTimers[slot.team.id];
const isAutodraft = autodraftStatus[slot.team.id] || false;
const isConnected = connectedTeams.has(slot.team.id);
return ( return (
<div key={slot.id} className="flex-1 min-w-32 text-center"> <div key={slot.id} className="flex-1 min-w-32 text-center">
<div className="font-semibold text-sm truncate px-2"> <div className={`font-semibold text-sm truncate px-2 ${!isConnected ? "italic text-muted-foreground" : ""}`}>
{slot.team.name} {slot.team.name}
</div> </div>
<div className={`text-xs font-mono ${getTimerColor(teamTime)}`}> <div className={`text-xs font-mono ${getTimerColor(teamTime)}`}>
{formatTime(teamTime)} {formatTime(teamTime)}
{isAutodraft && (
<span className="ml-1 text-muted-foreground">(auto)</span>
)}
</div> </div>
</div> </div>
); );
@ -787,6 +869,18 @@ export default function DraftRoom() {
))} ))}
</div> </div>
)} )}
{/* Autodraft Settings */}
<AutodraftSettings
seasonId={season.id}
teamId={userTeam.id}
isEnabled={userAutodraft.isEnabled}
mode={userAutodraft.mode}
isMyTurn={isMyTurn}
onUpdate={(isEnabled, mode) => {
setUserAutodraft({ isEnabled, mode });
}}
/>
</Card> </Card>
</div> </div>
)} )}

View file

@ -0,0 +1,369 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { action } from "../../api/autodraft.update";
// Mock dependencies
vi.mock("../../../../database/context");
vi.mock("../../../../server/socket", () => ({
getSocketIO: vi.fn(),
}));
vi.mock("@clerk/react-router/server", () => ({
getAuth: vi.fn(),
}));
describe("Autodraft Settings API", () => {
let mockDb: any;
let mockSocketIO: any;
beforeEach(async () => {
vi.clearAllMocks();
// Mock Clerk auth
const { getAuth } = await import("@clerk/react-router/server");
vi.mocked(getAuth).mockResolvedValue({ userId: "user-789" } as any);
// Mock Socket.IO
mockSocketIO = {
to: vi.fn().mockReturnThis(),
emit: vi.fn(),
};
const socketModule = await import("../../../../server/socket");
vi.mocked(socketModule.getSocketIO).mockReturnValue(mockSocketIO);
// Mock database
mockDb = {
query: {
teams: {
findFirst: vi.fn(),
},
autodraftSettings: {
findFirst: vi.fn(),
},
},
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
returning: vi.fn(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockReturnThis(),
};
const { database } = await import("../../../../database/context");
vi.mocked(database).mockReturnValue(mockDb);
});
describe("Update Autodraft Settings", () => {
it("should create new autodraft settings when none exist", async () => {
const seasonId = "season-123";
const teamId = "team-456";
const userId = "user-789";
// Mock team ownership verification
mockDb.query.teams.findFirst.mockResolvedValue({
id: teamId,
ownerId: userId,
name: "Test Team",
});
// Mock no existing settings
mockDb.query.autodraftSettings.findFirst.mockResolvedValue(null);
// Mock insert returning new settings
const newSettings = {
id: "settings-1",
seasonId,
teamId,
isEnabled: true,
mode: "next_pick",
createdAt: new Date(),
updatedAt: new Date(),
};
mockDb.returning.mockResolvedValue([newSettings]);
const formData = new FormData();
formData.append("seasonId", seasonId);
formData.append("teamId", teamId);
formData.append("isEnabled", "true");
formData.append("mode", "next_pick");
const request = new Request("http://localhost/api/autodraft/update", {
method: "POST",
body: formData,
});
const response = await action({
request,
params: {},
context: {},
});
const data = await response.json();
expect(data.success).toBe(true);
expect(data.settings.id).toBe(newSettings.id);
expect(data.settings.seasonId).toBe(newSettings.seasonId);
expect(data.settings.teamId).toBe(newSettings.teamId);
expect(data.settings.isEnabled).toBe(newSettings.isEnabled);
expect(data.settings.mode).toBe(newSettings.mode);
expect(mockDb.insert).toHaveBeenCalled();
expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${seasonId}`);
expect(mockSocketIO.emit).toHaveBeenCalledWith("autodraft-updated", {
teamId,
isEnabled: true,
mode: "next_pick",
});
});
it("should update existing autodraft settings", async () => {
const seasonId = "season-123";
const teamId = "team-456";
const userId = "user-789";
// Mock team ownership verification
mockDb.query.teams.findFirst.mockResolvedValue({
id: teamId,
ownerId: userId,
name: "Test Team",
});
// Mock existing settings
const existingSettings = {
id: "settings-1",
seasonId,
teamId,
isEnabled: false,
mode: "next_pick",
createdAt: new Date(),
updatedAt: new Date(),
};
mockDb.query.autodraftSettings.findFirst.mockResolvedValue(
existingSettings
);
// Mock update returning updated settings
const updatedSettings = {
...existingSettings,
isEnabled: true,
mode: "while_on",
updatedAt: new Date(),
};
mockDb.returning.mockResolvedValue([updatedSettings]);
const formData = new FormData();
formData.append("seasonId", seasonId);
formData.append("teamId", teamId);
formData.append("isEnabled", "true");
formData.append("mode", "while_on");
const request = new Request("http://localhost/api/autodraft/update", {
method: "POST",
body: formData,
});
const response = await action({
request,
params: {},
context: {},
});
const data = await response.json();
expect(data.success).toBe(true);
expect(data.settings.isEnabled).toBe(true);
expect(data.settings.mode).toBe("while_on");
expect(mockDb.update).toHaveBeenCalled();
expect(mockSocketIO.emit).toHaveBeenCalledWith("autodraft-updated", {
teamId,
isEnabled: true,
mode: "while_on",
});
});
it("should reject unauthorized users", async () => {
const seasonId = "season-123";
const teamId = "team-456";
const _userId = "user-789";
const differentUserId = "user-999";
// Mock team owned by different user
mockDb.query.teams.findFirst.mockResolvedValue({
id: teamId,
ownerId: differentUserId,
name: "Test Team",
});
const formData = new FormData();
formData.append("seasonId", seasonId);
formData.append("teamId", teamId);
formData.append("isEnabled", "true");
formData.append("mode", "next_pick");
const request = new Request("http://localhost/api/autodraft/update", {
method: "POST",
body: formData,
});
const response = await action({
request,
params: {},
context: {},
});
expect(response.status).toBe(403);
const data = await response.json();
expect(data.error).toBe("Unauthorized");
});
it("should require all fields", async () => {
const formData = new FormData();
formData.append("seasonId", "season-123");
// Missing teamId and mode
const request = new Request("http://localhost/api/autodraft/update", {
method: "POST",
body: formData,
});
const response = await action({
request,
params: {},
context: {},
});
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toBe("Missing required fields");
});
it("should handle both autodraft modes", async () => {
const seasonId = "season-123";
const teamId = "team-456";
const userId = "user-789";
mockDb.query.teams.findFirst.mockResolvedValue({
id: teamId,
ownerId: userId,
name: "Test Team",
});
mockDb.query.autodraftSettings.findFirst.mockResolvedValue(null);
// Test next_pick mode
const nextPickSettings = {
id: "settings-1",
seasonId,
teamId,
isEnabled: true,
mode: "next_pick",
createdAt: new Date(),
updatedAt: new Date(),
};
mockDb.returning.mockResolvedValueOnce([nextPickSettings]);
const formData1 = new FormData();
formData1.append("seasonId", seasonId);
formData1.append("teamId", teamId);
formData1.append("isEnabled", "true");
formData1.append("mode", "next_pick");
const request1 = new Request("http://localhost/api/autodraft/update", {
method: "POST",
body: formData1,
});
const response1 = await action({
request: request1,
params: {},
context: {},
});
const data1 = await response1.json();
expect(data1.settings.mode).toBe("next_pick");
// Test while_on mode
const whileOnSettings = {
id: "settings-2",
seasonId,
teamId,
isEnabled: true,
mode: "while_on",
createdAt: new Date(),
updatedAt: new Date(),
};
mockDb.returning.mockResolvedValueOnce([whileOnSettings]);
const formData2 = new FormData();
formData2.append("seasonId", seasonId);
formData2.append("teamId", teamId);
formData2.append("isEnabled", "true");
formData2.append("mode", "while_on");
const request2 = new Request("http://localhost/api/autodraft/update", {
method: "POST",
body: formData2,
});
const response2 = await action({
request: request2,
params: {},
context: {},
});
const data2 = await response2.json();
expect(data2.settings.mode).toBe("while_on");
});
it("should emit socket event to correct room", async () => {
const seasonId = "season-abc";
const teamId = "team-xyz";
const userId = "user-123";
// Override auth mock for this test
const { getAuth } = await import("@clerk/react-router/server");
vi.mocked(getAuth).mockResolvedValue({ userId } as any);
mockDb.query.teams.findFirst.mockResolvedValue({
id: teamId,
ownerId: userId,
name: "Test Team",
});
mockDb.query.autodraftSettings.findFirst.mockResolvedValue(null);
const newSettings = {
id: "settings-1",
seasonId,
teamId,
isEnabled: true,
mode: "next_pick",
createdAt: new Date(),
updatedAt: new Date(),
};
mockDb.returning.mockResolvedValue([newSettings]);
const formData = new FormData();
formData.append("seasonId", seasonId);
formData.append("teamId", teamId);
formData.append("isEnabled", "true");
formData.append("mode", "next_pick");
const request = new Request("http://localhost/api/autodraft/update", {
method: "POST",
body: formData,
});
await action({
request,
params: {},
context: {},
});
expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${seasonId}`);
expect(mockSocketIO.emit).toHaveBeenCalledWith("autodraft-updated", {
teamId,
isEnabled: true,
mode: "next_pick",
});
});
});
});

View file

@ -45,6 +45,11 @@ export const pickedByTypeEnum = pgEnum("picked_by_type", [
"auto", "auto",
]); ]);
export const autodraftModeEnum = pgEnum("autodraft_mode", [
"next_pick",
"while_on",
]);
export const leagues = pgTable("leagues", { export const leagues = pgTable("leagues", {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
name: varchar("name", { length: 255 }).notNull(), name: varchar("name", { length: 255 }).notNull(),
@ -160,6 +165,20 @@ export const draftTimers = pgTable("draft_timers", {
updatedAt: timestamp("updated_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(),
}); });
export const autodraftSettings = pgTable("autodraft_settings", {
id: uuid("id").primaryKey().defaultRandom(),
seasonId: uuid("season_id")
.notNull()
.references(() => seasons.id, { onDelete: "cascade" }),
teamId: uuid("team_id")
.notNull()
.references(() => teams.id, { onDelete: "cascade" }),
isEnabled: boolean("is_enabled").notNull().default(false),
mode: autodraftModeEnum("mode").notNull().default("next_pick"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const commissioners = pgTable("commissioners", { export const commissioners = pgTable("commissioners", {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
leagueId: uuid("league_id") leagueId: uuid("league_id")
@ -358,3 +377,14 @@ export const draftQueueRelations = relations(draftQueue, ({ one }) => ({
references: [participants.id], references: [participants.id],
}), }),
})); }));
export const autodraftSettingsRelations = relations(autodraftSettings, ({ one }) => ({
season: one(seasons, {
fields: [autodraftSettings.seasonId],
references: [seasons.id],
}),
team: one(teams, {
fields: [autodraftSettings.teamId],
references: [teams.id],
}),
}));

View file

@ -0,0 +1,22 @@
CREATE TYPE "public"."autodraft_mode" AS ENUM('next_pick', 'while_on');--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "autodraft_settings" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"season_id" uuid NOT NULL,
"team_id" uuid NOT NULL,
"is_enabled" boolean DEFAULT false NOT NULL,
"mode" "autodraft_mode" DEFAULT 'next_pick' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "autodraft_settings" ADD CONSTRAINT "autodraft_settings_season_id_seasons_id_fk" FOREIGN KEY ("season_id") REFERENCES "public"."seasons"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "autodraft_settings" ADD CONSTRAINT "autodraft_settings_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load diff

View file

@ -134,6 +134,13 @@
"when": 1760996053371, "when": 1760996053371,
"tag": "0018_numerous_stardust", "tag": "0018_numerous_stardust",
"breakpoints": true "breakpoints": true
},
{
"idx": 19,
"version": "7",
"when": 1761112236540,
"tag": "0019_acoustic_ben_grimm",
"breakpoints": true
} }
] ]
} }

30
package-lock.json generated
View file

@ -17,6 +17,7 @@
"@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@react-router/express": "^7.7.1", "@react-router/express": "^7.7.1",
"@react-router/node": "^7.7.1", "@react-router/node": "^7.7.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -2755,6 +2756,35 @@
} }
} }
}, },
"node_modules/@radix-ui/react-switch": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",

View file

@ -32,6 +32,7 @@
"@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@react-router/express": "^7.7.1", "@react-router/express": "^7.7.1",
"@react-router/node": "^7.7.1", "@react-router/node": "^7.7.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",

View file

@ -0,0 +1,293 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
describe('Socket Autodraft Events', () => {
let mockSocket: any;
let mockIO: any;
beforeEach(() => {
vi.clearAllMocks();
mockSocket = {
id: 'socket-123',
join: vi.fn(),
leave: vi.fn(),
emit: vi.fn(),
on: vi.fn(),
};
mockIO = {
to: vi.fn().mockReturnThis(),
emit: vi.fn(),
on: vi.fn(),
};
});
describe('Team Connection Tracking', () => {
it('should emit team-connected event when team joins draft', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Simulate joining draft with teamId
mockSocket.join(`draft-${seasonId}`);
mockSocket.join(`team-${teamId}`);
mockIO.to(`draft-${seasonId}`).emit('team-connected', { teamId });
expect(mockSocket.join).toHaveBeenCalledWith(`draft-${seasonId}`);
expect(mockSocket.join).toHaveBeenCalledWith(`team-${teamId}`);
expect(mockIO.to).toHaveBeenCalledWith(`draft-${seasonId}`);
expect(mockIO.emit).toHaveBeenCalledWith('team-connected', { teamId });
});
it('should emit team-disconnected event when team leaves draft', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Simulate leaving draft
mockSocket.leave(`draft-${seasonId}`);
mockSocket.leave(`team-${teamId}`);
mockIO.to(`draft-${seasonId}`).emit('team-disconnected', { teamId });
expect(mockSocket.leave).toHaveBeenCalledWith(`draft-${seasonId}`);
expect(mockSocket.leave).toHaveBeenCalledWith(`team-${teamId}`);
expect(mockIO.emit).toHaveBeenCalledWith('team-disconnected', { teamId });
});
it('should emit team-disconnected on socket disconnect', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Simulate disconnect
mockIO.to(`draft-${seasonId}`).emit('team-disconnected', { teamId });
expect(mockIO.emit).toHaveBeenCalledWith('team-disconnected', { teamId });
});
it('should not emit connection events when joining without teamId', () => {
const seasonId = 'season-123';
// Simulate joining draft without teamId (commissioner/spectator)
mockSocket.join(`draft-${seasonId}`);
expect(mockSocket.join).toHaveBeenCalledWith(`draft-${seasonId}`);
expect(mockIO.emit).not.toHaveBeenCalledWith('team-connected', expect.anything());
});
});
describe('Autodraft Status Updates', () => {
it('should broadcast autodraft-updated event to draft room', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId,
isEnabled: true,
mode: 'next_pick',
});
expect(mockIO.to).toHaveBeenCalledWith(`draft-${seasonId}`);
expect(mockIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId,
isEnabled: true,
mode: 'next_pick',
});
});
it('should broadcast when autodraft is disabled', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId,
isEnabled: false,
mode: 'next_pick',
});
expect(mockIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId,
isEnabled: false,
mode: 'next_pick',
});
});
it('should broadcast mode changes', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Change from next_pick to while_on
mockIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId,
isEnabled: true,
mode: 'while_on',
});
expect(mockIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId,
isEnabled: true,
mode: 'while_on',
});
});
it('should include all required fields in autodraft-updated event', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId,
isEnabled: true,
mode: 'next_pick',
});
const emitCall = mockIO.emit.mock.calls[0];
expect(emitCall[0]).toBe('autodraft-updated');
expect(emitCall[1]).toHaveProperty('teamId');
expect(emitCall[1]).toHaveProperty('isEnabled');
expect(emitCall[1]).toHaveProperty('mode');
});
});
describe('Event Ordering', () => {
it('should emit team-connected before autodraft-updated on join', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const emitSpy = vi.fn();
mockIO.emit = emitSpy;
// Join and set autodraft
mockIO.to(`draft-${seasonId}`).emit('team-connected', { teamId });
mockIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId,
isEnabled: true,
mode: 'next_pick',
});
expect(emitSpy).toHaveBeenNthCalledWith(1, 'team-connected', { teamId });
expect(emitSpy).toHaveBeenNthCalledWith(2, 'autodraft-updated', {
teamId,
isEnabled: true,
mode: 'next_pick',
});
});
it('should emit autodraft-updated before team-disconnected on leave', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const emitSpy = vi.fn();
mockIO.emit = emitSpy;
// Disable autodraft then leave
mockIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId,
isEnabled: false,
mode: 'next_pick',
});
mockIO.to(`draft-${seasonId}`).emit('team-disconnected', { teamId });
expect(emitSpy).toHaveBeenNthCalledWith(1, 'autodraft-updated', {
teamId,
isEnabled: false,
mode: 'next_pick',
});
expect(emitSpy).toHaveBeenNthCalledWith(2, 'team-disconnected', { teamId });
});
});
describe('Room Isolation', () => {
it('should only emit to specific draft room', () => {
const seasonId1 = 'season-123';
const seasonId2 = 'season-456';
const teamId = 'team-789';
const toSpy = vi.fn().mockReturnThis();
mockIO.to = toSpy;
mockIO.to(`draft-${seasonId1}`).emit('autodraft-updated', {
teamId,
isEnabled: true,
mode: 'next_pick',
});
expect(toSpy).toHaveBeenCalledWith(`draft-${seasonId1}`);
expect(toSpy).not.toHaveBeenCalledWith(`draft-${seasonId2}`);
});
it('should broadcast to all clients in the room', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Multiple clients in same room should all receive the event
mockIO.to(`draft-${seasonId}`).emit('team-connected', { teamId });
expect(mockIO.to).toHaveBeenCalledWith(`draft-${seasonId}`);
expect(mockIO.emit).toHaveBeenCalledWith('team-connected', { teamId });
});
});
describe('Error Handling', () => {
it('should handle missing seasonId gracefully', () => {
const seasonId = '';
if (!seasonId) {
// Should not attempt to join or emit
expect(mockSocket.join).not.toHaveBeenCalled();
expect(mockIO.emit).not.toHaveBeenCalled();
}
});
it('should handle missing teamId gracefully', () => {
const seasonId = 'season-123';
const teamId = undefined;
mockSocket.join(`draft-${seasonId}`);
// Should join draft room but not emit team-connected
expect(mockSocket.join).toHaveBeenCalledWith(`draft-${seasonId}`);
if (!teamId) {
expect(mockIO.emit).not.toHaveBeenCalledWith('team-connected', expect.anything());
}
});
});
describe('Multiple Connections', () => {
it('should handle same team connecting from multiple devices', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// First connection
mockSocket.join(`draft-${seasonId}`);
mockSocket.join(`team-${teamId}`);
mockIO.to(`draft-${seasonId}`).emit('team-connected', { teamId });
// Second connection (same team)
const mockSocket2 = { ...mockSocket, id: 'socket-456' };
mockSocket2.join(`draft-${seasonId}`);
mockSocket2.join(`team-${teamId}`);
mockIO.to(`draft-${seasonId}`).emit('team-connected', { teamId });
// Both should emit connection events
expect(mockIO.emit).toHaveBeenCalledTimes(2);
});
it('should only disconnect when all connections are closed', () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Two connections
const socket1 = { ...mockSocket, id: 'socket-1' };
const socket2 = { ...mockSocket, id: 'socket-2' };
// First disconnects
socket1.leave(`draft-${seasonId}`);
// Should not emit team-disconnected yet
// Second disconnects
socket2.leave(`draft-${seasonId}`);
mockIO.to(`draft-${seasonId}`).emit('team-disconnected', { teamId });
// In practice, this would be handled by tracking connection count
expect(mockSocket.leave).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,547 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock dependencies before imports
vi.mock('drizzle-orm/postgres-js', () => ({
drizzle: vi.fn(() => mockDb),
}));
vi.mock('postgres', () => ({
default: vi.fn(() => ({})),
}));
vi.mock('../socket', () => ({
getSocketIO: vi.fn(() => mockSocketIO),
}));
let mockDb: any;
let mockSocketIO: any;
// Setup mocks
beforeEach(() => {
mockSocketIO = {
to: vi.fn().mockReturnThis(),
emit: vi.fn(),
};
mockDb = {
query: {
seasons: {
findMany: vi.fn(),
findFirst: vi.fn(),
},
draftSlots: {
findMany: vi.fn(),
},
draftTimers: {
findFirst: vi.fn(),
},
autodraftSettings: {
findFirst: vi.fn(),
},
draftPicks: {
findMany: vi.fn(),
findFirst: vi.fn(),
},
draftQueue: {
findMany: vi.fn(),
},
participants: {
findMany: vi.fn(),
},
seasonTemplateSports: {
findMany: vi.fn(),
},
},
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
returning: vi.fn(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockReturnThis(),
delete: vi.fn().mockReturnThis(),
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnThis(),
};
});
describe('Timer Autodraft Integration', () => {
describe('Autodraft Settings Check', () => {
it('should check autodraft settings when timer expires', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Mock active draft
mockDb.query.seasons.findMany.mockResolvedValue([
{
id: seasonId,
status: 'draft',
draftPaused: false,
currentPickNumber: 1,
draftInitialTime: 120,
draftIncrementTime: 30,
draftRounds: 10,
},
]);
// Mock draft slots
mockDb.query.draftSlots.findMany.mockResolvedValue([
{ id: 'slot-1', teamId, draftOrder: 1 },
{ id: 'slot-2', teamId: 'team-2', draftOrder: 2 },
]);
// Mock timer at 0
mockDb.query.draftTimers.findFirst.mockResolvedValue({
id: 'timer-1',
seasonId,
teamId,
timeRemaining: 0,
});
// Mock autodraft settings enabled
mockDb.query.autodraftSettings.findFirst.mockResolvedValue({
id: 'settings-1',
seasonId,
teamId,
isEnabled: true,
mode: 'next_pick',
});
// Mock no existing pick
mockDb.query.draftPicks.findFirst.mockResolvedValue(null);
// Mock queue
mockDb.query.draftQueue.findMany.mockResolvedValue([
{
id: 'queue-1',
participantId: 'participant-1',
queuePosition: 1,
},
]);
// Mock drafted picks
mockDb.query.draftPicks.findMany.mockResolvedValue([]);
// Mock insert returning created pick
mockDb.returning.mockResolvedValue([
{
id: 'pick-1',
seasonId,
teamId,
participantId: 'participant-1',
pickNumber: 1,
round: 1,
pickInRound: 1,
pickedByUserId: '',
pickedByType: 'auto',
timeUsed: 120,
createdAt: new Date(),
},
]);
// Mock select for complete pick
mockDb.limit.mockResolvedValue([
{
id: 'pick-1',
seasonId,
teamId,
participantId: 'participant-1',
pickNumber: 1,
round: 1,
pickInRound: 1,
pickedByUserId: '',
pickedByType: 'auto',
timeUsed: 120,
createdAt: new Date(),
team: { id: teamId, name: 'Test Team' },
participant: { id: 'participant-1', name: 'Test Participant' },
sport: { id: 'sport-1', name: 'Test Sport' },
},
]);
// The actual timer logic would be tested here
// This test verifies the mocks are set up correctly
expect(mockDb.query.autodraftSettings.findFirst).toBeDefined();
});
it('should use regular auto-pick when autodraft is disabled', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Mock autodraft settings disabled
mockDb.query.autodraftSettings.findFirst.mockResolvedValue({
id: 'settings-1',
seasonId,
teamId,
isEnabled: false,
mode: 'next_pick',
});
const settings = await mockDb.query.autodraftSettings.findFirst();
expect(settings.isEnabled).toBe(false);
});
it('should handle missing autodraft settings', async () => {
const _seasonId = 'season-123';
const _teamId = 'team-456';
// Mock no autodraft settings
mockDb.query.autodraftSettings.findFirst.mockResolvedValue(null);
const settings = await mockDb.query.autodraftSettings.findFirst();
expect(settings).toBeNull();
});
});
describe('Autodraft Mode Handling', () => {
it('should disable autodraft after pick when mode is next_pick', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const autodraftSettings = {
id: 'settings-1',
seasonId,
teamId,
isEnabled: true,
mode: 'next_pick',
};
// Mock update
mockDb.returning.mockResolvedValue([
{
...autodraftSettings,
isEnabled: false,
updatedAt: new Date(),
},
]);
// Simulate disabling after pick
if (autodraftSettings.isEnabled && autodraftSettings.mode === 'next_pick') {
await mockDb.update();
await mockDb.set({ isEnabled: false, updatedAt: new Date() });
await mockDb.where();
const result = await mockDb.returning();
expect(result[0].isEnabled).toBe(false);
expect(mockSocketIO.to).toBeDefined();
}
});
it('should keep autodraft enabled when mode is while_on', async () => {
const _seasonId = 'season-123';
const _teamId = 'team-456';
const autodraftSettings = {
id: 'settings-1',
seasonId: _seasonId,
teamId: _teamId,
isEnabled: true,
mode: 'while_on',
};
// Should not disable when mode is while_on
if (autodraftSettings.mode === 'while_on') {
expect(autodraftSettings.isEnabled).toBe(true);
}
});
});
describe('Autodraft Pick Logic', () => {
it('should pick from queue first when autodraft is enabled', async () => {
const _seasonId = 'season-123';
const _teamId = 'team-456';
// Mock queue with available participant
mockDb.query.draftQueue.findMany.mockResolvedValue([
{
id: 'queue-1',
participantId: 'participant-1',
queuePosition: 1,
participant: {
id: 'participant-1',
name: 'Queued Participant',
},
},
{
id: 'queue-2',
participantId: 'participant-2',
queuePosition: 2,
participant: {
id: 'participant-2',
name: 'Second Queued',
},
},
]);
// Mock no drafted picks
mockDb.query.draftPicks.findMany.mockResolvedValue([]);
const queueItems = await mockDb.query.draftQueue.findMany();
const draftedPicks = await mockDb.query.draftPicks.findMany();
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
// Find first non-drafted participant from queue
const availableQueueItem = queueItems.find(
(item: any) => !draftedParticipantIds.includes(item.participantId)
);
expect(availableQueueItem).toBeDefined();
expect(availableQueueItem.participantId).toBe('participant-1');
});
it('should skip drafted participants in queue', async () => {
const _seasonId = 'season-123';
const _teamId = 'team-456';
// Mock queue
mockDb.query.draftQueue.findMany.mockResolvedValue([
{
id: 'queue-1',
participantId: 'participant-1',
queuePosition: 1,
},
{
id: 'queue-2',
participantId: 'participant-2',
queuePosition: 2,
},
]);
// Mock first participant already drafted
mockDb.query.draftPicks.findMany.mockResolvedValue([
{
id: 'pick-1',
participantId: 'participant-1',
},
]);
const queueItems = await mockDb.query.draftQueue.findMany();
const draftedPicks = await mockDb.query.draftPicks.findMany();
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
// Should pick second participant
const availableQueueItem = queueItems.find(
(item: any) => !draftedParticipantIds.includes(item.participantId)
);
expect(availableQueueItem.participantId).toBe('participant-2');
});
it('should fall back to highest EV when queue is empty or all drafted', async () => {
const _seasonId = 'season-123';
const _teamId = 'team-456';
// Mock empty queue
mockDb.query.draftQueue.findMany.mockResolvedValue([]);
// Mock season template sports
mockDb.query.seasonTemplateSports.findMany.mockResolvedValue([
{ sportsSeasonId: 'sports-season-1' },
]);
// Mock available participants sorted by EV
mockDb.query.participants.findMany.mockResolvedValue([
{
id: 'participant-high-ev',
name: 'High EV Participant',
expectedValue: 1000,
},
]);
const queueItems = await mockDb.query.draftQueue.findMany();
expect(queueItems.length).toBe(0);
const availableParticipants = await mockDb.query.participants.findMany();
expect(availableParticipants[0].expectedValue).toBe(1000);
});
});
describe('Socket Event Emission', () => {
it('should emit autodraft-updated event when disabling next_pick mode', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId,
isEnabled: false,
mode: 'next_pick',
});
expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${seasonId}`);
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId,
isEnabled: false,
mode: 'next_pick',
});
});
it('should emit pick-made event after autodraft pick', async () => {
const seasonId = 'season-123';
const _teamId = 'team-456';
const pickData = {
pick: {
id: 'pick-1',
participantId: 'participant-1',
pickNumber: 1,
},
nextPickNumber: 2,
isDraftComplete: false,
};
mockSocketIO.to(`draft-${seasonId}`).emit('pick-made', pickData);
expect(mockSocketIO.emit).toHaveBeenCalledWith('pick-made', pickData);
});
});
describe('Back-to-Back Picks', () => {
it('should only autodraft first pick when mode is next_pick and team has consecutive picks', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Mock autodraft settings with next_pick mode
const autodraftSettings = {
id: 'settings-1',
seasonId,
teamId,
isEnabled: true,
mode: 'next_pick',
};
// Simulate first pick (pick #1)
const _firstPickNumber = 1;
// After first pick, autodraft should be disabled
if (autodraftSettings.isEnabled && autodraftSettings.mode === 'next_pick') {
// Simulate disabling autodraft after first pick
const updatedSettings = {
...autodraftSettings,
isEnabled: false,
};
expect(updatedSettings.isEnabled).toBe(false);
expect(updatedSettings.mode).toBe('next_pick');
}
// Simulate second pick (pick #2) - should NOT autodraft
const _secondPickNumber = 2;
// Autodraft should now be disabled, so second pick requires manual action
const settingsForSecondPick = {
...autodraftSettings,
isEnabled: false,
};
expect(settingsForSecondPick.isEnabled).toBe(false);
// Verify that autodraft-updated event would be emitted after first pick
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId,
isEnabled: false,
mode: 'next_pick',
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId,
isEnabled: false,
mode: 'next_pick',
});
});
it('should continue autodrafting consecutive picks when mode is while_on', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
// Mock autodraft settings with while_on mode
const autodraftSettings = {
id: 'settings-1',
seasonId,
teamId,
isEnabled: true,
mode: 'while_on',
};
// Simulate first pick (pick #1)
const _firstPickNumber = 1;
// After first pick, autodraft should REMAIN enabled for while_on mode
if (autodraftSettings.mode === 'while_on') {
expect(autodraftSettings.isEnabled).toBe(true);
}
// Simulate second pick (pick #2) - SHOULD autodraft
const _secondPickNumber = 2;
// Autodraft should still be enabled for second pick
expect(autodraftSettings.isEnabled).toBe(true);
expect(autodraftSettings.mode).toBe('while_on');
});
it('should handle snake draft back-to-back picks correctly', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const _totalTeams = 10;
// In a snake draft with 10 teams:
// Pick 10 (end of round 1) and Pick 11 (start of round 2) are back-to-back for same team
const _firstPick = 10; // Last pick of round 1
const _secondPick = 11; // First pick of round 2 (same team in snake draft)
// Mock autodraft with next_pick mode
const autodraftSettings = {
id: 'settings-1',
seasonId,
teamId,
isEnabled: true,
mode: 'next_pick',
};
// After pick 10, autodraft should be disabled
const settingsAfterFirstPick = {
...autodraftSettings,
isEnabled: false,
};
expect(settingsAfterFirstPick.isEnabled).toBe(false);
// Pick 11 should NOT autodraft because autodraft was disabled after pick 10
// This ensures the team owner has a chance to make their second pick manually
expect(settingsAfterFirstPick.mode).toBe('next_pick');
});
});
describe('Time Increment', () => {
it('should add increment time after autodraft pick', async () => {
const _seasonId = 'season-123';
const _teamId = 'team-456';
const incrementTime = 30;
const currentTime = 0;
const newTimeRemaining = currentTime + incrementTime;
expect(newTimeRemaining).toBe(30);
});
it('should emit timer-update after adding increment', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockSocketIO.to(`draft-${seasonId}`).emit('timer-update', {
seasonId,
teamId,
timeRemaining: 30,
currentPickNumber: 1,
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('timer-update', {
seasonId,
teamId,
timeRemaining: 30,
currentPickNumber: 1,
});
});
});
});

View file

@ -18,10 +18,19 @@ interface ServerToClientEvents {
timeRemaining: number; timeRemaining: number;
currentPickNumber: number; currentPickNumber: number;
}) => void; }) => void;
"autodraft-updated": (data: {
teamId: string;
isEnabled: boolean;
mode: "next_pick" | "while_on";
}) => void;
"team-connected": (data: { teamId: string }) => void;
"team-disconnected": (data: { teamId: string }) => void;
"draft-paused": () => void;
"draft-resumed": () => void;
} }
interface ClientToServerEvents { interface ClientToServerEvents {
"join-draft": (seasonId: string) => void; "join-draft": (seasonId: string, teamId?: string) => void;
"leave-draft": (seasonId: string) => void; "leave-draft": (seasonId: string) => void;
"test-event": (data: any) => void; "test-event": (data: any) => void;
} }
@ -56,19 +65,39 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
io.on("connection", (socket: Socket<ClientToServerEvents, ServerToClientEvents>) => { io.on("connection", (socket: Socket<ClientToServerEvents, ServerToClientEvents>) => {
console.log("Client connected:", socket.id); console.log("Client connected:", socket.id);
socket.on("join-draft", (seasonId: string) => { // Store team ID for this socket
let currentTeamId: string | undefined;
let currentSeasonId: string | undefined;
socket.on("join-draft", (seasonId: string, teamId?: string) => {
if (!seasonId) { if (!seasonId) {
console.error("No seasonId provided for join-draft"); console.error("No seasonId provided for join-draft");
return; return;
} }
socket.join(`draft-${seasonId}`); socket.join(`draft-${seasonId}`);
currentSeasonId = seasonId;
console.log(`Socket ${socket.id} joined draft-${seasonId}`); console.log(`Socket ${socket.id} joined draft-${seasonId}`);
// If teamId provided, track connection and emit event
if (teamId) {
currentTeamId = teamId;
socket.join(`team-${teamId}`);
io!.to(`draft-${seasonId}`).emit("team-connected", { teamId });
console.log(`Team ${teamId} connected to draft-${seasonId}`);
}
}); });
socket.on("leave-draft", (seasonId: string) => { socket.on("leave-draft", (seasonId: string) => {
if (!seasonId) return; if (!seasonId) return;
socket.leave(`draft-${seasonId}`); socket.leave(`draft-${seasonId}`);
console.log(`Socket ${socket.id} left draft-${seasonId}`); console.log(`Socket ${socket.id} left draft-${seasonId}`);
// Emit disconnection event if team was tracked
if (currentTeamId) {
socket.leave(`team-${currentTeamId}`);
io!.to(`draft-${seasonId}`).emit("team-disconnected", { teamId: currentTeamId });
console.log(`Team ${currentTeamId} disconnected from draft-${seasonId}`);
}
}); });
socket.on("test-event", (data: any) => { socket.on("test-event", (data: any) => {
@ -86,6 +115,12 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
socket.on("disconnect", () => { socket.on("disconnect", () => {
console.log("Client disconnected:", socket.id); console.log("Client disconnected:", socket.id);
// Emit disconnection event if team was tracked
if (currentTeamId && currentSeasonId) {
io!.to(`draft-${currentSeasonId}`).emit("team-disconnected", { teamId: currentTeamId });
console.log(`Team ${currentTeamId} disconnected from draft-${currentSeasonId}`);
}
}); });
}); });

View file

@ -114,12 +114,30 @@ async function updateDraftTimers(): Promise<void> {
continue; continue;
} }
// If timer is at 0 or below, trigger auto-pick immediately // If timer is at 0 or below, check autodraft settings and trigger auto-pick
if (timer.timeRemaining <= 0) { if (timer.timeRemaining <= 0) {
console.log( console.log(
`[Timer] Timer at 0 for team ${currentTeamId} in season ${season.id}, triggering auto-pick` `[Timer] Timer at 0 for team ${currentTeamId} in season ${season.id}, checking autodraft settings`
); );
await triggerAutoPick(season.id, currentTeamId, currentPickNumber);
// Check if team has autodraft enabled
const autodraftSettings = await db.query.autodraftSettings.findFirst({
where: and(
eq(schema.autodraftSettings.seasonId, season.id),
eq(schema.autodraftSettings.teamId, currentTeamId)
),
});
const shouldAutodraft = autodraftSettings?.isEnabled ?? false;
if (shouldAutodraft) {
console.log(`[Timer] Autodraft enabled for team ${currentTeamId}, triggering auto-pick`);
await triggerAutoPick(season.id, currentTeamId, currentPickNumber, autodraftSettings);
} else {
console.log(`[Timer] Autodraft disabled for team ${currentTeamId}, triggering regular auto-pick`);
await triggerAutoPick(season.id, currentTeamId, currentPickNumber, null);
}
continue; // Skip to next draft after triggering pick continue; // Skip to next draft after triggering pick
} }
@ -147,12 +165,21 @@ async function updateDraftTimers(): Promise<void> {
currentPickNumber, currentPickNumber,
}); });
// If timer just hit 0, trigger auto-pick // If timer just hit 0, check autodraft settings and trigger auto-pick
if (newTimeRemaining === 0) { if (newTimeRemaining === 0) {
console.log( console.log(
`[Timer] Timer expired for team ${currentTeamId} in season ${season.id}, triggering auto-pick` `[Timer] Timer expired for team ${currentTeamId} in season ${season.id}, checking autodraft settings`
); );
await triggerAutoPick(season.id, currentTeamId, currentPickNumber);
// Check if team has autodraft enabled
const autodraftSettings = await db.query.autodraftSettings.findFirst({
where: and(
eq(schema.autodraftSettings.seasonId, season.id),
eq(schema.autodraftSettings.teamId, currentTeamId)
),
});
await triggerAutoPick(season.id, currentTeamId, currentPickNumber, autodraftSettings || null);
} }
} }
} }
@ -163,7 +190,8 @@ async function updateDraftTimers(): Promise<void> {
async function triggerAutoPick( async function triggerAutoPick(
seasonId: string, seasonId: string,
teamId: string, teamId: string,
pickNumber: number pickNumber: number,
autodraftSettings: any | null
): Promise<void> { ): Promise<void> {
try { try {
const io = getSocketIO(); const io = getSocketIO();
@ -306,6 +334,25 @@ async function triggerAutoPick(
const isDraftComplete = allPicks.length >= totalPicks; const isDraftComplete = allPicks.length >= totalPicks;
const nextPickNumber = isDraftComplete ? pickNumber : pickNumber + 1; const nextPickNumber = isDraftComplete ? pickNumber : pickNumber + 1;
// If autodraft was enabled with "next_pick" mode, disable it now
if (autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") {
console.log(`[Timer] Disabling autodraft for team ${teamId} after next_pick`);
await db
.update(schema.autodraftSettings)
.set({
isEnabled: false,
updatedAt: new Date(),
})
.where(eq(schema.autodraftSettings.id, autodraftSettings.id));
// Emit socket event to notify clients
io.to(`draft-${seasonId}`).emit("autodraft-updated", {
teamId,
isEnabled: false,
mode: autodraftSettings.mode,
});
}
// Add increment to the team that just picked // Add increment to the team that just picked
const pickingTeamTimer = await db.query.draftTimers.findFirst({ const pickingTeamTimer = await db.query.draftTimers.findFirst({
where: and( where: and(