Merge pull request 'Close door-screen toasts on our own timer so they dismiss on touchscreens.' (#37) from dev into main
Reviewed-on: #37
This commit was merged in pull request #37.
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
// Haptic + audio confirmation. At a loud, dark door the sound and the buzz are
|
// Haptic + audio confirmation. At a loud, dark door the sound and the buzz are
|
||||||
// what staff actually register — the green flash is confirmation for the person
|
// what staff actually register — the green flash is confirmation for the person
|
||||||
// standing in front of them.
|
// standing in front of them.
|
||||||
@@ -39,3 +41,18 @@ export function vibrate(pattern: number | number[]) {
|
|||||||
if (navigator.vibrate) navigator.vibrate(pattern);
|
if (navigator.vibrate) navigator.vibrate(pattern);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss a toast after `ms` regardless of react-hot-toast's hover/touch pause.
|
||||||
|
*
|
||||||
|
* The <Toaster> pauses every countdown while the pointer is "inside" its
|
||||||
|
* container, and schedules no dismissal at all for toasts created while paused.
|
||||||
|
* On a touchscreen, tapping a toast button (the Undo toast) synthesizes a
|
||||||
|
* mouseenter with no mouseleave until the next tap somewhere else — so the
|
||||||
|
* "Undone" toast that follows would sit there indefinitely. Callers create the
|
||||||
|
* toast with `duration: Infinity` and let this timer close it.
|
||||||
|
*/
|
||||||
|
export function dismissAfter(id: string, ms: number): string {
|
||||||
|
window.setTimeout(() => toast.dismiss(id), ms);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { formatCurrency, parseDate, EVENT_TIMEZONE } from '@/lib/utils';
|
|||||||
|
|
||||||
import { buildIndex, searchAttendees, hasExactMatch } from './_lib/search';
|
import { buildIndex, searchAttendees, hasExactMatch } from './_lib/search';
|
||||||
import { newIdempotencyKey, submitDoorAction, undoDoorAction } from './_lib/doorActions';
|
import { newIdempotencyKey, submitDoorAction, undoDoorAction } from './_lib/doorActions';
|
||||||
import { playSuccessSound, playErrorSound, vibrate } from './_lib/feedback';
|
import { playSuccessSound, playErrorSound, vibrate, dismissAfter } from './_lib/feedback';
|
||||||
import { QRScannerOverlay } from './_components/QRScannerOverlay';
|
import { QRScannerOverlay } from './_components/QRScannerOverlay';
|
||||||
import { AttendeeRow } from './_components/AttendeeRow';
|
import { AttendeeRow } from './_components/AttendeeRow';
|
||||||
import { WalkInRow, emptyWalkIn, type WalkInDraft } from './_components/WalkInRow';
|
import { WalkInRow, emptyWalkIn, type WalkInDraft } from './_components/WalkInRow';
|
||||||
@@ -48,6 +48,10 @@ import { ResultScreen, type DoorResult } from './_components/ResultScreen';
|
|||||||
const REFRESH_INTERVAL_MS = 30_000;
|
const REFRESH_INTERVAL_MS = 30_000;
|
||||||
const FLASH_MS = 900;
|
const FLASH_MS = 900;
|
||||||
const UNDO_WINDOW_MS = 10_000;
|
const UNDO_WINDOW_MS = 10_000;
|
||||||
|
// Every toast on this screen is closed by our own timer (dismissAfter) rather
|
||||||
|
// than react-hot-toast's countdown, which pauses while a toast is touched and
|
||||||
|
// never resumes on a touchscreen until the next tap elsewhere.
|
||||||
|
const TOAST_MS = 4000;
|
||||||
|
|
||||||
const clockTime = (value: Date | string) =>
|
const clockTime = (value: Date | string) =>
|
||||||
(typeof value === 'string' ? parseDate(value) : value).toLocaleTimeString([], {
|
(typeof value === 'string' ? parseDate(value) : value).toLocaleTimeString([], {
|
||||||
@@ -250,12 +254,18 @@ export default function AdminDoorPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await undoDoorAction(eventId, entry.idempotencyKey);
|
await undoDoorAction(eventId, entry.idempotencyKey);
|
||||||
toast.success(`Undone — ${entry.name}`, { position: 'bottom-center' });
|
dismissAfter(
|
||||||
|
toast.success(`Undone — ${entry.name}`, { position: 'bottom-center', duration: Infinity }),
|
||||||
|
TOAST_MS,
|
||||||
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(`Could not undo ${entry.name}: ${error?.message || 'request failed'}`, {
|
dismissAfter(
|
||||||
position: 'bottom-center',
|
toast.error(`Could not undo ${entry.name}: ${error?.message || 'request failed'}`, {
|
||||||
duration: 8000,
|
position: 'bottom-center',
|
||||||
});
|
duration: Infinity,
|
||||||
|
}),
|
||||||
|
8000,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// Whatever happened, the server is the truth about who is inside.
|
// Whatever happened, the server is the truth about who is inside.
|
||||||
loadAttendees(eventId, { silent: true });
|
loadAttendees(eventId, { silent: true });
|
||||||
@@ -293,8 +303,9 @@ export default function AdminDoorPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
{ duration: durationMs, position: 'bottom-center' },
|
{ duration: Infinity, position: 'bottom-center' },
|
||||||
);
|
);
|
||||||
|
dismissAfter(id, durationMs);
|
||||||
undoToastIdsRef.current.set(entry.idempotencyKey, id);
|
undoToastIdsRef.current.set(entry.idempotencyKey, id);
|
||||||
},
|
},
|
||||||
[handleUndo],
|
[handleUndo],
|
||||||
@@ -430,7 +441,10 @@ export default function AdminDoorPage() {
|
|||||||
);
|
);
|
||||||
if (res.warnings?.includes('at_capacity') && !capacityWarnedRef.current) {
|
if (res.warnings?.includes('at_capacity') && !capacityWarnedRef.current) {
|
||||||
capacityWarnedRef.current = true;
|
capacityWarnedRef.current = true;
|
||||||
toast('Event at capacity, added anyway', { icon: '⚠️', position: 'bottom-center', duration: 6000 });
|
dismissAfter(
|
||||||
|
toast('Event at capacity, added anyway', { icon: '⚠️', position: 'bottom-center', duration: Infinity }),
|
||||||
|
6000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Retries are exhausted: make the failure loud and name the person, so
|
// Retries are exhausted: make the failure loud and name the person, so
|
||||||
@@ -457,10 +471,13 @@ export default function AdminDoorPage() {
|
|||||||
detail: error?.message || 'The check-in did not reach the server. Try again.',
|
detail: error?.message || 'The check-in did not reach the server. Try again.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
toast.error(`FAILED — ${opts.displayName} is NOT checked in. ${error?.message || ''}`.trim(), {
|
dismissAfter(
|
||||||
duration: 12000,
|
toast.error(`FAILED — ${opts.displayName} is NOT checked in. ${error?.message || ''}`.trim(), {
|
||||||
position: 'bottom-center',
|
duration: Infinity,
|
||||||
});
|
position: 'bottom-center',
|
||||||
|
}),
|
||||||
|
12000,
|
||||||
|
);
|
||||||
loadAttendees(eventId, { silent: true });
|
loadAttendees(eventId, { silent: true });
|
||||||
} finally {
|
} finally {
|
||||||
setBusyId((current) => (current === opts.busyKey ? null : current));
|
setBusyId((current) => (current === opts.busyKey ? null : current));
|
||||||
|
|||||||
Reference in New Issue
Block a user