* Add overnight pause feature for draft timers Protects players from having their pick timer expire while asleep. Admins configure a nightly window (league-wide or per-user timezone); the timer freezes during that window while autodraft can still fire. Fixes #66 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TS error: guard getUserDisplayName against undefined user Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
/**
|
||
* Utilities for overnight pause windows.
|
||
* Works in both Node.js and browser — uses only Intl, no extra dependencies.
|
||
*/
|
||
|
||
function getLocalMinutes(timezone: string, now: Date): number {
|
||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||
timeZone: timezone,
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
hour12: false,
|
||
});
|
||
const parts = formatter.formatToParts(now);
|
||
const hour = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10);
|
||
const minute = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10);
|
||
// Intl hour12:false can return "24" for midnight — normalize to 0
|
||
return (hour === 24 ? 0 : hour) * 60 + minute;
|
||
}
|
||
|
||
/**
|
||
* Build a UTC Date representing "the given date at HH:MM in the given timezone."
|
||
* Corrects for the UTC offset by comparing what local time the naive UTC produces.
|
||
*/
|
||
function utcForLocalTime(
|
||
timezone: string,
|
||
year: number,
|
||
month: number, // 1-indexed
|
||
day: number,
|
||
hour: number,
|
||
minute: number
|
||
): Date {
|
||
// Start with a naive UTC (treating target H:M as UTC)
|
||
const approxUTC = Date.UTC(year, month - 1, day, hour, minute, 0);
|
||
const probe = new Date(approxUTC);
|
||
const localMins = getLocalMinutes(timezone, probe);
|
||
const targetMins = hour * 60 + minute;
|
||
// Shift: if local is behind target, add the gap; if ahead, subtract it
|
||
return new Date(approxUTC + (targetMins - localMins) * 60 * 1000);
|
||
}
|
||
|
||
/**
|
||
* Returns true if `now` falls inside the overnight pause window defined by
|
||
* startHHMM–endHHMM in the given IANA timezone.
|
||
*
|
||
* The window almost always spans midnight (e.g. "23:00"–"07:00"), but a
|
||
* same-day window (e.g. "10:00"–"18:00") is also handled correctly.
|
||
*/
|
||
export function isInOvernightWindow(
|
||
timezone: string,
|
||
startHHMM: string,
|
||
endHHMM: string,
|
||
now = new Date()
|
||
): boolean {
|
||
const currentMinutes = getLocalMinutes(timezone, now);
|
||
const [sh, sm] = startHHMM.split(":").map(Number);
|
||
const [eh, em] = endHHMM.split(":").map(Number);
|
||
const startMinutes = sh * 60 + sm;
|
||
const endMinutes = eh * 60 + em;
|
||
|
||
if (startMinutes > endMinutes) {
|
||
// Spans midnight: in window if current >= start OR current < end
|
||
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
|
||
}
|
||
// Same-day window: in window if start <= current < end
|
||
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
||
}
|
||
|
||
/**
|
||
* Returns the next UTC Date when endHHMM will occur in the given timezone.
|
||
* Used to tell clients when the overnight window will end.
|
||
*/
|
||
export function getOvernightResumeUTC(
|
||
timezone: string,
|
||
endHHMM: string,
|
||
now = new Date()
|
||
): Date {
|
||
const [eh, em] = endHHMM.split(":").map(Number);
|
||
|
||
// Get current calendar date in the target timezone
|
||
const dateFmt = new Intl.DateTimeFormat("en-CA", {
|
||
timeZone: timezone,
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
});
|
||
const [year, month, day] = dateFmt.format(now).split("-").map(Number);
|
||
|
||
// Candidate: today at endHHMM in the target timezone
|
||
const candidate = utcForLocalTime(timezone, year, month, day, eh, em);
|
||
|
||
// If this moment has already passed, advance one day
|
||
if (candidate.getTime() <= now.getTime()) {
|
||
return new Date(candidate.getTime() + 24 * 60 * 60 * 1000);
|
||
}
|
||
return candidate;
|
||
}
|