Artur Mukhamadiev 7d6429aca9 frontend: queue-aware submit flow and queued loading view
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.
2026-09-15 13:38:59 +03:00

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";
}