Submit joins the admission queue first and sends the PAT only once the reservation is held. Queued state shows the shaping orb, ordinal position with estimate, and an exit button whose label changes with the wait; running switches the orb to solving. Rejoin once on a lost reservation or ticket, best-effort leave on pagehide. Mock server gains the queue endpoints and five scenarios; contract, unit and e2e tests cover them.
61 lines
2.0 KiB
JavaScript
61 lines
2.0 KiB
JavaScript
/**
|
|
* Pure formatting helpers for the admission queue UI (spec `docs/QUEUE_SPECIFICATION.md` §7.2, §7.3).
|
|
* Deliberately framework- and DOM-free so they can be unit tested without a browser; the DOM
|
|
* wiring and polling state machine live in app.js.
|
|
*/
|
|
|
|
export const QUEUE_POLL_INTERVAL_MS = 2000;
|
|
|
|
/**
|
|
* English ordinal suffix for a 1-based rank: 1st, 2nd, 3rd, 4th, 11th-13th, 21st, ...
|
|
* @param {number} n
|
|
* @returns {string}
|
|
*/
|
|
export function ordinal(n) {
|
|
const rem100 = n % 100;
|
|
if (rem100 >= 11 && rem100 <= 13) return `${n}th`;
|
|
switch (n % 10) {
|
|
case 1: return `${n}st`;
|
|
case 2: return `${n}nd`;
|
|
case 3: return `${n}rd`;
|
|
default: return `${n}th`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats the estimate clause: "estimating…" when unknown, "under a minute" below 60s,
|
|
* otherwise "about N min" rounded up (spec §5, §7.2).
|
|
* @param {number|null|undefined} etaSeconds
|
|
* @returns {string}
|
|
*/
|
|
export function formatEta(etaSeconds) {
|
|
if (etaSeconds === null || etaSeconds === undefined) return 'estimating…';
|
|
if (etaSeconds < 60) return 'under a minute';
|
|
const minutes = Math.ceil(etaSeconds / 60);
|
|
return `about ${minutes} min`;
|
|
}
|
|
|
|
/**
|
|
* Formats the queued sub-state status line (spec §7.2 table, `queued` row).
|
|
* Position 1 reads as "You're next" instead of "You're 1st in line".
|
|
* @param {{ position: number, eta_seconds: number|null }} status
|
|
* @returns {string}
|
|
*/
|
|
export function formatQueuedStatus({ position, eta_seconds: etaSeconds }) {
|
|
const place = position === 1 ? "You're next" : `You're ${ordinal(position)} in line`;
|
|
return `${place} · ${formatEta(etaSeconds)}`;
|
|
}
|
|
|
|
/**
|
|
* Formats the "Exit queue" button's visible label based on time waited since the join
|
|
* response (spec §7.3). The button's `aria-label` stays "Leave the queue" regardless.
|
|
* @param {number} waitedMs
|
|
* @returns {string}
|
|
*/
|
|
export function formatExitQueueLabel(waitedMs) {
|
|
const waitedMinutes = waitedMs / 60000;
|
|
if (waitedMinutes < 1) return 'I will try next time';
|
|
if (waitedMinutes <= 3) return "Ohhh, it's so long";
|
|
return "I'm dying in this queue";
|
|
}
|