Booking flow: required terms and privacy checkbox with i18n
Also includes admin, dashboard, and API updates; PWA icon assets; and assorted layout and utility changes on dev.
This commit is contained in:
@@ -179,6 +179,20 @@ export const ticketsApi = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
guestCreate: (data: {
|
||||
eventId: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
preferredLanguage?: 'en' | 'es';
|
||||
adminNote?: string;
|
||||
}) =>
|
||||
fetchApi<{ ticket: Ticket; payment: Payment; message: string }>('/api/tickets/admin/guest', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
checkPaymentStatus: (ticketId: string) =>
|
||||
fetchApi<{ ticketStatus: string; paymentStatus: string; lnbitsStatus?: string; isPaid: boolean }>(
|
||||
@@ -550,6 +564,7 @@ export interface Ticket {
|
||||
checkedInByAdminId?: string;
|
||||
qrCode: string;
|
||||
adminNote?: string;
|
||||
isGuest?: boolean;
|
||||
createdAt: string;
|
||||
event?: Event;
|
||||
payment?: Payment;
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
// All helpers pin the timezone to America/Asuncion so the output is identical
|
||||
// on the server (often UTC) and the client (user's local TZ). This prevents
|
||||
// React hydration mismatches like "07:20 PM" (server) vs "04:20 PM" (client).
|
||||
//
|
||||
// IMPORTANT — parseDate() must be used instead of raw `new Date(str)` so that
|
||||
// ISO-like strings without a timezone suffix (e.g. "2026-04-02T14:00:00") are
|
||||
// always treated as UTC. Without this, the same string produces a different
|
||||
// instant on server (Node TZ) vs client (browser / DevTools TZ).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const EVENT_TIMEZONE = 'America/Asuncion';
|
||||
export const EVENT_TIMEZONE = 'America/Asuncion';
|
||||
|
||||
type Locale = 'en' | 'es';
|
||||
|
||||
@@ -14,11 +19,29 @@ function pickLocale(locale: Locale): string {
|
||||
return locale === 'es' ? 'es-ES' : 'en-US';
|
||||
}
|
||||
|
||||
// Matches ISO-like strings that have NO timezone indicator (Z, +HH:MM, etc.)
|
||||
const NAIVE_ISO_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/;
|
||||
|
||||
/**
|
||||
* Parse a date string into a deterministic Date object.
|
||||
*
|
||||
* If the string looks like an ISO datetime but lacks a timezone suffix it is
|
||||
* ambiguous — `new Date()` would interpret it in the environment's local
|
||||
* timezone which differs between Node (SSR) and the browser (hydration).
|
||||
* We normalise by appending "Z" so parsing always targets UTC.
|
||||
*/
|
||||
export function parseDate(dateStr: string): Date {
|
||||
if (NAIVE_ISO_RE.test(dateStr)) {
|
||||
return new Date(dateStr + 'Z');
|
||||
}
|
||||
return new Date(dateStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Sat, Feb 14" / "sáb, 14 feb"
|
||||
*/
|
||||
export function formatDateShort(dateStr: string, locale: Locale = 'en'): string {
|
||||
return new Date(dateStr).toLocaleDateString(pickLocale(locale), {
|
||||
return parseDate(dateStr).toLocaleDateString(pickLocale(locale), {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
@@ -30,7 +53,7 @@ export function formatDateShort(dateStr: string, locale: Locale = 'en'): string
|
||||
* "Saturday, February 14, 2026" / "sábado, 14 de febrero de 2026"
|
||||
*/
|
||||
export function formatDateLong(dateStr: string, locale: Locale = 'en'): string {
|
||||
return new Date(dateStr).toLocaleDateString(pickLocale(locale), {
|
||||
return parseDate(dateStr).toLocaleDateString(pickLocale(locale), {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@@ -43,7 +66,7 @@ export function formatDateLong(dateStr: string, locale: Locale = 'en'): string {
|
||||
* "February 14, 2026" / "14 de febrero de 2026" (no weekday)
|
||||
*/
|
||||
export function formatDateMedium(dateStr: string, locale: Locale = 'en'): string {
|
||||
return new Date(dateStr).toLocaleDateString(pickLocale(locale), {
|
||||
return parseDate(dateStr).toLocaleDateString(pickLocale(locale), {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
@@ -55,7 +78,7 @@ export function formatDateMedium(dateStr: string, locale: Locale = 'en'): string
|
||||
* "Feb 14, 2026" / "14 feb 2026"
|
||||
*/
|
||||
export function formatDateCompact(dateStr: string, locale: Locale = 'en'): string {
|
||||
return new Date(dateStr).toLocaleDateString(pickLocale(locale), {
|
||||
return parseDate(dateStr).toLocaleDateString(pickLocale(locale), {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
@@ -67,7 +90,7 @@ export function formatDateCompact(dateStr: string, locale: Locale = 'en'): strin
|
||||
* "04:30 PM" / "16:30"
|
||||
*/
|
||||
export function formatTime(dateStr: string, locale: Locale = 'en'): string {
|
||||
return new Date(dateStr).toLocaleTimeString(pickLocale(locale), {
|
||||
return parseDate(dateStr).toLocaleTimeString(pickLocale(locale), {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: EVENT_TIMEZONE,
|
||||
@@ -78,7 +101,7 @@ export function formatTime(dateStr: string, locale: Locale = 'en'): string {
|
||||
* "Feb 14, 2026, 04:30 PM" — compact date + time combined
|
||||
*/
|
||||
export function formatDateTime(dateStr: string, locale: Locale = 'en'): string {
|
||||
return new Date(dateStr).toLocaleString(pickLocale(locale), {
|
||||
return parseDate(dateStr).toLocaleString(pickLocale(locale), {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
@@ -92,7 +115,7 @@ export function formatDateTime(dateStr: string, locale: Locale = 'en'): string {
|
||||
* "Sat, Feb 14, 04:30 PM" — short date + time combined
|
||||
*/
|
||||
export function formatDateTimeShort(dateStr: string, locale: Locale = 'en'): string {
|
||||
return new Date(dateStr).toLocaleString(pickLocale(locale), {
|
||||
return parseDate(dateStr).toLocaleString(pickLocale(locale), {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
|
||||
Reference in New Issue
Block a user