feat: add autodraft status and team connection indicators to draft grid
This commit is contained in:
parent
86ae5014ec
commit
ff97e25438
18 changed files with 3646 additions and 117 deletions
140
app/components/AutodraftSettings.tsx
Normal file
140
app/components/AutodraftSettings.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -42,115 +46,133 @@ export function DraftGrid({
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
{title && <h2 className="text-xl font-semibold mb-4">{title}</h2>}
|
{title && <h2 className="text-xl font-semibold mb-4">{title}</h2>}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* Team Headers */}
|
{/* Team Headers */}
|
||||||
<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];
|
||||||
return (
|
const isAutodraft = autodraftStatus[slot.team.id] || false;
|
||||||
<div key={slot.id} className="flex-1 min-w-32 text-center">
|
const isConnected = connectedTeams.has(slot.team.id);
|
||||||
<div className="font-semibold text-sm truncate px-2">
|
|
||||||
{slot.team.name}
|
return (
|
||||||
</div>
|
<div key={slot.id} className="flex-1 min-w-32 text-center">
|
||||||
{teamTimers && formatTime && (
|
<div
|
||||||
<div
|
className={`font-semibold text-sm truncate px-2 ${!isConnected ? "italic text-muted-foreground" : ""}`}
|
||||||
className={`text-xs font-mono ${
|
>
|
||||||
teamTime === undefined
|
{slot.team.name}
|
||||||
? "text-muted-foreground"
|
</div>
|
||||||
: teamTime > 60
|
{teamTimers && formatTime && (
|
||||||
|
<div
|
||||||
|
className={`text-xs font-mono ${
|
||||||
|
teamTime === undefined
|
||||||
|
? "text-muted-foreground"
|
||||||
|
: teamTime > 60
|
||||||
? "text-green-600"
|
? "text-green-600"
|
||||||
: teamTime > 30
|
: teamTime > 30
|
||||||
? "text-yellow-600"
|
? "text-yellow-600"
|
||||||
: "text-red-600"
|
: "text-red-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{formatTime(teamTime)}
|
{formatTime(teamTime)}
|
||||||
</div>
|
{isAutodraft && (
|
||||||
)}
|
<span className="ml-1 text-muted-foreground">(auto)</span>
|
||||||
</div>
|
)}
|
||||||
);
|
</div>
|
||||||
})}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Draft Grid Rows */}
|
{/* Draft Grid Rows */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{draftGrid.map((roundPicks, roundIndex) => {
|
{draftGrid.map((roundPicks, roundIndex) => {
|
||||||
const round = roundIndex + 1;
|
const round = roundIndex + 1;
|
||||||
const isEvenRound = round % 2 === 0;
|
const isEvenRound = round % 2 === 0;
|
||||||
const displayPicks = isEvenRound
|
const displayPicks = isEvenRound
|
||||||
? [...roundPicks].reverse()
|
? [...roundPicks].reverse()
|
||||||
: roundPicks;
|
: roundPicks;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={roundIndex} className="flex gap-2">
|
<div key={roundIndex} className="flex gap-2">
|
||||||
{displayPicks.map((cell, index) => {
|
{displayPicks.map((cell, index) => {
|
||||||
const actualIndex = isEvenRound
|
const actualIndex = isEvenRound
|
||||||
? roundPicks.length - 1 - index
|
? roundPicks.length - 1 - index
|
||||||
: index;
|
: index;
|
||||||
const slot = draftSlots[actualIndex];
|
const slot = draftSlots[actualIndex];
|
||||||
const pickNumber = roundIndex * totalTeams + actualIndex + 1;
|
const pickNumber = roundIndex * totalTeams + actualIndex + 1;
|
||||||
const isCurrent = pickNumber === currentPick;
|
const isCurrent = pickNumber === currentPick;
|
||||||
const isPicked = !!cell;
|
const isPicked = !!cell;
|
||||||
|
|
||||||
const cellContent = (
|
const cellContent = (
|
||||||
<div
|
<div
|
||||||
className={`flex-1 min-w-0 h-20 border-2 rounded-lg p-2 transition-all ${
|
className={`flex-1 min-w-0 h-20 border-2 rounded-lg p-2 transition-all ${
|
||||||
isCurrent
|
isCurrent
|
||||||
? "border-blue-500 bg-blue-50 dark:bg-blue-950 shadow-lg"
|
? "border-blue-500 bg-blue-50 dark:bg-blue-950 shadow-lg"
|
||||||
: isPicked
|
: isPicked
|
||||||
? "border-green-500 bg-green-50 dark:bg-green-950"
|
? "border-green-500 bg-green-50 dark:bg-green-950"
|
||||||
: "border-gray-300 bg-white dark:bg-gray-900"
|
: "border-gray-300 bg-white dark:bg-gray-900"
|
||||||
}`}
|
}`}
|
||||||
title={`Overall Pick #${pickNumber}`}
|
title={`Overall Pick #${pickNumber}`}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-mono text-muted-foreground mb-1">
|
<div className="text-xs font-mono text-muted-foreground mb-1">
|
||||||
{round}.{String(actualIndex + 1).padStart(2, "0")}
|
{round}.{String(actualIndex + 1).padStart(2, "0")}
|
||||||
</div>
|
|
||||||
{isPicked ? (
|
|
||||||
<div className="text-xs">
|
|
||||||
<div className="font-semibold truncate">
|
|
||||||
{cell.participant.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground truncate">
|
|
||||||
{cell.sport.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : isCurrent ? (
|
|
||||||
<div className="text-xs font-semibold text-blue-600 dark:text-blue-400">
|
|
||||||
On Clock
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
{isPicked ? (
|
||||||
|
<div className="text-xs">
|
||||||
|
<div className="font-semibold truncate">
|
||||||
|
{cell.participant.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground truncate">
|
||||||
|
{cell.sport.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : isCurrent ? (
|
||||||
|
<div className="text-xs font-semibold text-blue-600 dark:text-blue-400">
|
||||||
|
On Clock
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrap with context menu if commissioner and current unpicked cell
|
||||||
|
if (
|
||||||
|
isCommissioner &&
|
||||||
|
!isPicked &&
|
||||||
|
isCurrent &&
|
||||||
|
onForceAutopick &&
|
||||||
|
onForceManualPick
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<ContextMenu key={pickNumber}>
|
||||||
|
<ContextMenuTrigger asChild>
|
||||||
|
{cellContent}
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
onForceAutopick(pickNumber, slot.team.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Force Auto Pick
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
onForceManualPick(pickNumber, slot.team.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Force Manual Pick
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap with context menu if commissioner and current unpicked cell
|
return cellContent;
|
||||||
if (isCommissioner && !isPicked && isCurrent && onForceAutopick && onForceManualPick) {
|
})}
|
||||||
return (
|
</div>
|
||||||
<ContextMenu key={pickNumber}>
|
);
|
||||||
<ContextMenuTrigger asChild>
|
})}
|
||||||
{cellContent}
|
</div>
|
||||||
</ContextMenuTrigger>
|
|
||||||
<ContextMenuContent>
|
|
||||||
<ContextMenuItem
|
|
||||||
onClick={() => onForceAutopick(pickNumber, slot.team.id)}
|
|
||||||
>
|
|
||||||
Force Auto Pick
|
|
||||||
</ContextMenuItem>
|
|
||||||
<ContextMenuItem
|
|
||||||
onClick={() => onForceManualPick(pickNumber, slot.team.id)}
|
|
||||||
>
|
|
||||||
Force Manual Pick
|
|
||||||
</ContextMenuItem>
|
|
||||||
</ContextMenuContent>
|
|
||||||
</ContextMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cellContent;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
287
app/components/__tests__/AutodraftSettings.test.tsx
Normal file
287
app/components/__tests__/AutodraftSettings.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
29
app/components/ui/switch.tsx
Normal file
29
app/components/ui/switch.tsx
Normal 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 }
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
79
app/routes/api/autodraft.update.ts
Normal file
79
app/routes/api/autodraft.update.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
369
app/routes/leagues/__tests__/autodraft.test.ts
Normal file
369
app/routes/leagues/__tests__/autodraft.test.ts
Normal 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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
|
||||||
22
drizzle/0019_acoustic_ben_grimm.sql
Normal file
22
drizzle/0019_acoustic_ben_grimm.sql
Normal 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 $$;
|
||||||
1497
drizzle/meta/0019_snapshot.json
Normal file
1497
drizzle/meta/0019_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
30
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
293
server/__tests__/socket-autodraft.test.ts
Normal file
293
server/__tests__/socket-autodraft.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
547
server/__tests__/timer-autodraft.test.ts
Normal file
547
server/__tests__/timer-autodraft.test.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue