brackt/app/lib/draft-order.ts

83 lines
2.4 KiB
TypeScript
Raw Permalink Normal View History

Fix draft order initialization for teams added mid-season (#449) * Fix draft order showing only new team when league size increased before order was set When a commissioner increased the team count on a league that had teams but no draft order set yet, the server created draft slots only for the newly-added teams. This left the DB with N+K teams but only K slots, causing the drag-and-drop list to display only the K new teams. Two fixes: 1. Server: only append new draft slots when an order was already set (existingSlots.length > 0). If no order exists yet, skip slot creation so the page correctly treats the order as unset for all teams. 2. Frontend: buildDraftOrderTeams() appends any unslotted teams after the slotted ones, so a partially-corrupt DB state still shows all teams. https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo * Address code review feedback on draft order bug fix - Move buildDraftOrderTeams to app/lib/draft-order.ts so it is importable and testable; remove the local copy from the settings component - Add unit tests for buildDraftOrderTeams covering the empty, full, and partial-slot cases - Tighten the draft slot guard from existingSlots.length > 0 to existingSlots.length === currentTeamCount so partial legacy states are treated the same as "order not set" - Condense the 3-line server comment to a single line per project style - Rename getNumTeamsInSeason → getNumDraftSlotsBySeasonId to reflect what the function actually counts, and update its one call site - Add tests for the server-side slot-creation guard logic https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo * Fix lint: move shouldAppendDraftSlots to outer scope oxlint (consistent-function-scoping) requires functions that don't close over any variables to be defined at the outer scope rather than inside a describe block. https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 18:22:47 -07:00
/**
* Builds the ordered team-ID list for the draft order UI.
* Slotted teams come first (in their configured order); any teams without a
* slot are appended at the end so all teams are always visible.
*/
export function buildDraftOrderTeams(
slots: { teamId: string }[],
allTeams: { id: string }[]
): string[] {
if (slots.length === 0) return allTeams.map((t) => t.id);
const slottedIds = new Set(slots.map((s) => s.teamId));
const unslotted = allTeams.filter((t) => !slottedIds.has(t.id)).map((t) => t.id);
return [...slots.map((s) => s.teamId), ...unslotted];
}
Add browser push notifications toggle to draft page (#11) * Add push notifications implementation plan Documents the approach for adding a browser Notification API toggle to the draft page sidebar, including files to create/modify and edge cases to handle. https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69 * Add browser push notifications toggle to draft page Adds a Notifications toggle in the draft sidebar (below Autodraft) that uses the Browser Notification API to alert users when a pick is made or when it's their turn, while the tab is not focused. - New useDraftNotifications hook for state, permission, and localStorage - New NotificationSettings component with Switch toggle - Fires "It's your turn to pick!" when the next pick is the user's - Fires "{Team} picked {Player}" for all other picks - Gracefully hides toggle when Notification API is unavailable https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69 * Fix all code review issues with push notifications - Extract snake draft order calculation to shared getTeamForPick() helper in lib/draft-order.ts, removing the duplicated logic - Fix notification firing on user's own picks (guard with team id check) - Fix notification firing after draft is complete (early return) - Use a ref for sendNotification to prevent full socket handler re-registration every time the toggle is changed - Add permission drift listener via Permissions API so the UI reacts if the user revokes notification permission in browser settings - Add SSR guard for document.hidden in sendNotification - Remove redundant isSupported prop from NotificationSettings; derive unsupported state from permissionState === "unsupported" - Move NotificationSettings out of QueueSection prop-drilling path; render it via a new settingsSection slot on DraftSidebar instead https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69 * Fix second round of notification code review issues - Add My Turn Only / All Picks mode granularity so users can choose to only be notified when it's their pick, or for every pick - Remove double top-border by stripping leftover border-t/pt-4/mt-4 wrapper from NotificationSettings (now provided by DraftSidebar) - Remove unused isSupported from hook return value - Add n.onclick = () => window.focus() so clicking a notification brings the draft tab back into focus - Scope localStorage keys to userId to avoid shared-browser conflicts (keys now: draftNotifications-{userId}-{seasonId}) - Add notificationsModeRef alongside sendNotificationRef so mode changes don't trigger full socket handler re-registration https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 19:30:53 -08:00
/**
* Returns the draft slot for a given pick number in a snake draft.
*/
export function getTeamForPick<T extends { draftOrder: number }>(
pickNumber: number,
draftSlots: T[]
): T | undefined {
const totalTeams = draftSlots.length;
if (totalTeams === 0) return undefined;
const round = Math.ceil(pickNumber / totalTeams);
const isEvenRound = round % 2 === 0;
let pickInRound = ((pickNumber - 1) % totalTeams) + 1;
if (isEvenRound) {
pickInRound = totalTeams - pickInRound + 1;
}
return draftSlots.find((slot) => slot.draftOrder === pickInRound);
}
export interface ProjectedPick {
round: number;
pickNumber: number;
picksFromNow: number;
}
export function getProjectedPicks(options: {
draftSlots: Array<{ draftOrder: number; teamId: string }>;
userTeamId: string;
currentPick: number;
totalRounds: number;
existingPicks: Array<{ pickNumber: number; teamId: string }>;
}): ProjectedPick[] {
const { draftSlots, userTeamId, currentPick, totalRounds, existingPicks } = options;
const totalTeams = draftSlots.length;
if (totalTeams === 0) return [];
const userSlot = draftSlots.find((slot) => slot.teamId === userTeamId);
if (!userSlot) return [];
const userDraftOrder = userSlot.draftOrder;
const pickedNumbersForTeam = new Set(
existingPicks
.filter((p) => p.teamId === userTeamId)
.map((p) => p.pickNumber)
);
const result: ProjectedPick[] = [];
for (let round = 1; round <= totalRounds; round++) {
const isEvenRound = round % 2 === 0;
const pickInRound = isEvenRound
? totalTeams - userDraftOrder + 1
: userDraftOrder;
const pickNumber = (round - 1) * totalTeams + pickInRound;
if (pickNumber < currentPick) continue;
if (pickedNumbersForTeam.has(pickNumber)) continue;
result.push({
round,
pickNumber,
picksFromNow: pickNumber - currentPick,
});
}
return result;
}