36 Commits
Author SHA1 Message Date
Michilis a4945a87cf Merge pull request 'Dev' (#36) from dev into main
Reviewed-on: #36
2026-09-17 15:41:05 +00:00
Michilis 183a2c99ed Merge pull request 'Restrict whole-event door takings to admin and organizer.' (#35) from dev into main
Reviewed-on: #35
2026-08-23 06:38:57 +00:00
Michilis 279cd19308 Merge pull request 'Fix the backend build against hono's narrowed route param type.' (#34) from dev into main
Reviewed-on: #34
2026-08-23 06:16:27 +00:00
Michilis de791fd17d Merge pull request 'Dev' (#33) from dev into main
Reviewed-on: #33
2026-08-23 06:04:26 +00:00
Michilis d06f80e572 Merge pull request 'Add mobile save sheet with same-origin photo downloads.' (#32) from dev into main
Reviewed-on: #32
2026-08-06 20:20:15 +00:00
Michilis e0f0b2f1e9 Merge pull request 'Dev' (#31) from dev into main
Reviewed-on: #31
2026-08-05 23:18:50 +00:00
Michilis 82857b5aa8 Merge pull request 'Fix post-login redirect when the session cookie is off-origin' (#30) from dev into main
Reviewed-on: #30
2026-07-29 20:46:11 +00:00
Michilis 5b478c327d Merge pull request 'Dev' (#29) from dev into main
Reviewed-on: #29
2026-07-29 19:08:28 +00:00
Michilis 6ec8b8400f Merge pull request 'dev' (#28) from dev into main
Reviewed-on: #28
2026-07-26 05:38:23 +00:00
Michilis 19461098a0 Merge pull request 'Stop auto-failing manual payments (TPago/bank/cash) after the pending TTL.' (#27) from tpago-fix into main
Reviewed-on: #27
2026-07-20 03:55:01 +00:00
Michilis 74dcc8e5ac Merge pull request 'Normalize RUC formatting to base-checkdigit across booking and admin views.' (#26) from RUC-fix into main
Reviewed-on: #26
2026-07-13 03:48:13 +00:00
Michilis bd1043a3f4 Merge pull request 'speed' (#25) from speed into main
Reviewed-on: #25
2026-07-13 01:18:23 +00:00
Michilis 107cd9753e Merge pull request 'Redesign user dashboard with overview tab and i18n payment copy.' (#24) from dashboard-update into main
Reviewed-on: #24
2026-06-29 17:00:15 +00:00
Michilis 194604004e Merge pull request 'security-update' (#23) from security-update into main
Reviewed-on: #23
2026-06-29 06:47:19 +00:00
Michilis bb920ce6f0 Merge pull request 'Add per-quantity TPago payment links for multi-ticket checkout.' (#22) from tpago-update into main
Reviewed-on: #22
2026-06-19 00:24:00 +00:00
Michilis c0315a705d Merge pull request 'Add human-readable event URL slugs with legacy redirect support.' (#21) from new-slugs into main
Reviewed-on: #21
2026-06-05 04:16:13 +00:00
Michilis fbc437a670 Merge pull request 'Fix booking flow scroll position on mobile step changes.' (#20) from dev into main
Reviewed-on: #20
2026-06-05 04:06:39 +00:00
Michilis e0f0700398 Merge pull request 'dev' (#19) from dev into main
Reviewed-on: #19
2026-06-04 23:37:15 +00:00
Michilis defd9685e0 Merge pull request 'dev' (#18) from dev into main
Reviewed-on: #18
2026-04-27 20:42:36 +00:00
Michilis 1ed62b0d3f Merge pull request 'Fix db:export ENOBUFS by streaming pg_dump output to file' (#17) from dev into main
Reviewed-on: #17
2026-03-12 19:18:57 +00:00
Michilis 91de6df04d Merge pull request 'feat(emails): add re-send for all emails, failed tab, and resend indicators' (#16) from dev into main
Reviewed-on: #16
2026-03-12 19:14:36 +00:00
Michilis a5d97d65e1 Merge pull request 'Admin: stats privacy toggle, clickable event rows, fix payment method display' (#15) from dev into main
Reviewed-on: #15
2026-03-10 01:14:36 +00:00
Michilis f0128f66b0 Merge pull request 'Bug fixes and improvements' (#14) from dev into main
Reviewed-on: #14
2026-03-07 22:53:13 +00:00
Michilis b33c68feb0 Merge pull request 'dev' (#13) from dev into main
Reviewed-on: #13
2026-02-19 02:23:19 +00:00
Michilis 15655e3987 Merge pull request 'dev' (#12) from dev into main
Reviewed-on: #12
2026-02-16 23:11:52 +00:00
Michilis d8b3864411 Merge pull request 'Fix stale featured event on homepage: revalidate cache when featured event changes' (#11) from dev into main
Reviewed-on: #11
2026-02-16 22:44:19 +00:00
Michilis 194cbd6ca8 Merge pull request 'Scanner: close button on valid ticket, camera lifecycle fix' (#10) from dev into main
Reviewed-on: #10
2026-02-14 19:04:42 +00:00
Michilis d5445c2282 Merge pull request 'Admin event page: redesign UI, export endpoints, mobile fixes' (#9) from dev into main
Reviewed-on: #9
2026-02-14 18:38:57 +00:00
Michilis dcfefc8371 Merge pull request 'feat(admin): add event attendees export (CSV) with status filters' (#8) from dev into main
Reviewed-on: #8
2026-02-14 05:28:24 +00:00
Michilis b5f14335c4 Merge pull request 'Mobile scanner redesign + backend live search' (#7) from dev into main
Reviewed-on: #7
2026-02-14 04:28:44 +00:00
Michilis d44ac949b5 Merge pull request 'Email queue + async sending; legal settings and placeholders' (#6) from dev into main
Reviewed-on: #6
2026-02-12 21:04:58 +00:00
Michilis a5e939221d Merge pull request 'dev' (#5) from dev into main
Reviewed-on: #5
2026-02-12 07:56:37 +00:00
Michilis 833e3e5a9c Merge pull request 'Fix llms.txt event times: format in America/Asuncion timezone' (#4) from dev into main
Reviewed-on: #4
2026-02-12 06:28:51 +00:00
Michilis ba1975dd6d Merge pull request 'dev' (#3) from dev into main
Reviewed-on: #3
2026-02-12 04:55:39 +00:00
Michilis 3025ef3d21 Merge pull request 'dev' (#2) from dev into main
Reviewed-on: #2
2026-02-12 03:19:06 +00:00
Michilis 8564f8af83 Merge pull request 'dev' (#1) from dev into main
Reviewed-on: #1
2026-02-12 02:18:08 +00:00
2 changed files with 12 additions and 46 deletions
@@ -1,5 +1,3 @@
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.
@@ -41,18 +39,3 @@ 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;
}
+8 -25
View File
@@ -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, dismissAfter } from './_lib/feedback'; import { playSuccessSound, playErrorSound, vibrate } 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,10 +48,6 @@ 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([], {
@@ -254,18 +250,12 @@ export default function AdminDoorPage() {
try { try {
await undoDoorAction(eventId, entry.idempotencyKey); await undoDoorAction(eventId, entry.idempotencyKey);
dismissAfter( toast.success(`Undone — ${entry.name}`, { position: 'bottom-center' });
toast.success(`Undone — ${entry.name}`, { position: 'bottom-center', duration: Infinity }),
TOAST_MS,
);
} catch (error: any) { } catch (error: any) {
dismissAfter(
toast.error(`Could not undo ${entry.name}: ${error?.message || 'request failed'}`, { toast.error(`Could not undo ${entry.name}: ${error?.message || 'request failed'}`, {
position: 'bottom-center', position: 'bottom-center',
duration: Infinity, duration: 8000,
}), });
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 });
@@ -303,9 +293,8 @@ export default function AdminDoorPage() {
</button> </button>
</div> </div>
), ),
{ duration: Infinity, position: 'bottom-center' }, { duration: durationMs, position: 'bottom-center' },
); );
dismissAfter(id, durationMs);
undoToastIdsRef.current.set(entry.idempotencyKey, id); undoToastIdsRef.current.set(entry.idempotencyKey, id);
}, },
[handleUndo], [handleUndo],
@@ -441,10 +430,7 @@ 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;
dismissAfter( toast('Event at capacity, added anyway', { icon: '⚠️', position: 'bottom-center', duration: 6000 });
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
@@ -471,13 +457,10 @@ 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.',
}); });
} }
dismissAfter(
toast.error(`FAILED — ${opts.displayName} is NOT checked in. ${error?.message || ''}`.trim(), { toast.error(`FAILED — ${opts.displayName} is NOT checked in. ${error?.message || ''}`.trim(), {
duration: Infinity, duration: 12000,
position: 'bottom-center', 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));