15 "js/wix/events-core.js": "// Declarations of events-core.ts: what this file exports, their arguments and what they return.\n// import type { EventCategory, EventDetail, EventStatus, EventSummary, RsvpForm, TaxSettings } from \"./events-types.js\";\n// export type Raw = Record<string, any>;\n// export type ImgSrc = (value: any, width: number, height: number) => string;\n// export declare const EVENTS_APP_ID = \"140603ad-af8d-84a5-2c80-a0f60cb47351\";\n// export declare const rawId: (raw: Raw | undefined | null) => string;\n// export declare const LIST_FIELDS: readonly [\"DETAILS\", \"REGISTRATION\", \"CATEGORIES\"];\n// export declare const DETAIL_FIELDS: readonly [\"DETAILS\", \"TEXTS\", \"REGISTRATION\", \"URLS\", \"CATEGORIES\", \"FORM\"];\n// export declare const LIVE_STATUSES: readonly [\"UPCOMING\", \"STARTED\"];\n// export declare const PAST_STATUSES: readonly [\"ENDED\"];\n// export type StatusFilter = \"upcoming\" | \"past\" | \"all\";\n// export declare function statusesFor(status?: StatusFilter): EventStatus[];\n// export declare const START_DATE_FIELD = \"dateAndTimeSettings.startDate\";\n// export declare const CREATED_DATE_FIELD_SDK = \"_createdDate\";\n// export declare const CREATED_DATE_FIELD_REST = \"createdDate\";\n// export declare const CATEGORY_ID_FIELD_SDK = \"categories._id\";\n// export declare const CATEGORY_ID_FIELD_REST = \"categories.id\";\n// export declare const DEFAULT_PAGE_SIZE = 20;\n// export interface ListOptions {\n// limit?: number;\n// offset?: number;\n// categoryId?: string;\n// status?: StatusFilter;\n// }\n// export declare function listQuery({ limit, offset, categoryId, status }?: ListOptions): Raw;\n// export declare const CATEGORY_STATES: readonly [\"MANUAL\"];\n// export declare function categoriesQuery(): Raw;\n// export declare function hasMorePages(loaded: number, total: number | undefined | null): boolean;\n// export declare function formatAmount(value: string | number | null | undefined, currency: string | null | undefined): string;\n// export declare function formatMoney(money: Raw | null | undefined): string;\n// export declare function isoOf(value: unknown): string;\n// export declare function formatInZone(value: unknown, timeZoneId: string | null | undefined): string;\n// export declare function lowestPriceLabel(reg: Raw): string;\n// export declare function toParagraphs(rich: Raw | undefined | null, legacy: string | undefined): string[];\n// export declare function mainImageOf(raw: Raw): unknown;\n// export declare function toCategory(raw: Raw): EventCategory;\n// export declare function manualCategories(raw: Raw): EventCategory[];\n// export declare function toCategories(list: Raw[] | undefined): EventCategory[];\n// export declare function toSummary(raw: Raw, imgSrc: ImgSrc): EventSummary;\n// export declare function toRsvpForm(form: Raw | undefined | null): RsvpForm | null;\n// export declare function toTaxSettings(ts: Raw | undefined | null): TaxSettings | null;\n// export declare function formattedAddress(address: Raw | undefined | null): string;\n// export declare function coordinatesOf(address: Raw | undefined | null): {\n// lat: number;\n// lng: number;\n// } | null;\n// export declare function pageUrlOf(value: unknown): string;\n// export declare function toDetail(raw: Raw, imgSrc: ImgSrc): EventDetail;\n// Event rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./events.ts (the\n// SDK, managed Astro and React) and the REST twin in templates/events/rest/events.ts (fetch, a\n// static site or a port to another language). Every rule about statuses, fieldsets, prices,\n// dates, images, categories, and the organizer's RSVP form lives HERE, once. A raw event may come\n// from the SDK (`_id`, mainImage as a wix:image string, Date objects) or from REST (`id`, mainImage\n// as { id, url }, ISO strings); the mappers accept both. Its one runtime import is the shared Ricos\n// renderer, deployed beside it on every stack.\nimport { richContentToHtml } from \"./rich-content.js\";\n/** The Wix Events app id (reference only — frontend calls need no app-id constant). */\nexport const EVENTS_APP_ID = \"140603ad-af8d-84a5-2c80-a0f60cb47351\";\nexport const rawId = (raw) => raw?._id ?? raw?.id ?? \"\";\n// Fieldsets are opt-in: without REGISTRATION there's no registration type to branch on;\n// without CATEGORIES the category names never arrive; DETAILS carries the formatted date\n// and mainImage; TEXTS carries the long description; URLS the Wix event page url; FORM the\n// organizer's RSVP form (createRsvp validates submissions against it).\nexport const LIST_FIELDS = [\"DETAILS\", \"REGISTRATION\", \"CATEGORIES\"];\nexport const DETAIL_FIELDS = [\"DETAILS\", \"TEXTS\", \"REGISTRATION\", \"URLS\", \"CATEGORIES\", \"FORM\"];\n/** Default listing: only these are registerable. `past` is ENDED; `all` is the three (never CANCELED/DRAFT). */\nexport const LIVE_STATUSES = [\"UPCOMING\", \"STARTED\"];\nexport const PAST_STATUSES = [\"ENDED\"];\nexport function statusesFor(status = \"upcoming\") {\n if (status === \"past\")\n return [...PAST_STATUSES];\n if (status === \"all\")\n return [...LIVE_STATUSES, ...PAST_STATUSES];\n return [...LIVE_STATUSES];\n}\nexport const START_DATE_FIELD = \"dateAndTimeSettings.startDate\";\n/** The stable secondary sort. The SDK spells it `_createdDate` and renames on the wire; REST is `createdDate`. */\nexport const CREATED_DATE_FIELD_SDK = \"_createdDate\";\nexport const CREATED_DATE_FIELD_REST = \"createdDate\";\n/** Server-side category filter field. SDK `categories._id` → REST `categories.id` (the same rename). */\nexport const CATEGORY_ID_FIELD_SDK = \"categories._id\";\nexport const CATEGORY_ID_FIELD_REST = \"categories.id\";\n/** Wix's page size; a listing loads more pages on demand. */\nexport const DEFAULT_PAGE_SIZE = 20;\n/**\n * The Query Events body for the listing in REST spelling: statuses, soonest first then newest\n * first, a positive limit, an offset. `paging.limit` defaults to 0 — a bare query answers\n * `total: N, events: []` with no error — so the limit is always sent. The SDK builder spells\n * the same parts with `_createdDate` / `categories._id`.\n */\nexport function listQuery({ limit = DEFAULT_PAGE_SIZE, offset = 0, categoryId, status } = {}) {\n if (!Number.isInteger(limit) || limit < 1 || limit > 1000)\n throw new Error(\"limit must be between 1 and 1000.\");\n if (!Number.isInteger(offset) || offset < 0)\n throw new Error(\"offset must be a non-negative integer.\");\n const filter = { status: { $in: statusesFor(status) } };\n if (categoryId)\n filter[CATEGORY_ID_FIELD_REST] = { $in: [categoryId] };\n return {\n filter,\n sort: [\n { fieldName: START_DATE_FIELD, order: \"ASC\" },\n { fieldName: CREATED_DATE_FIELD_REST, order: \"DESC\" },\n ],\n paging: { limit, offset },\n };\n}\n/** Query Categories body: only MANUAL categories (AUTO / RECURRING_EVENT / HIDDEN never become pills). */\nexport const CATEGORY_STATES = [\"MANUAL\"];\nexport function categoriesQuery() {\n return { filter: { states: { $hasSome: [...CATEGORY_STATES] } }, paging: { limit: 100 } };\n}\n/** How many more pages: a page is the last when offset + page length reaches the total. */\nexport function hasMorePages(loaded, total) {\n return typeof total === \"number\" && loaded < total;\n}\n/**\n * Format a decimal amount in a currency with the visitor's locale — the one money formatter for\n * this vertical. No currency → the bare amount (never a made-up \"$\"). \"\" when there is no value.\n */\nexport function formatAmount(value, currency) {\n if (value == null || value === \"\")\n return \"\";\n const n = Number(value);\n if (!Number.isFinite(n))\n return String(value);\n if (!currency)\n return n.toFixed(2);\n try {\n return new Intl.NumberFormat(undefined, { style: \"currency\", currency }).format(n);\n }\n catch {\n return `${n.toFixed(2)} ${currency}`;\n }\n}\n/** A Wix Money `{ value, amount (deprecated), currency }` → formatted string (value read before the deprecated amount). */\nexport function formatMoney(money) {\n if (!money)\n return \"\";\n return formatAmount(money.value ?? money.amount, money.currency);\n}\n/** Any date the API hands back (Date on the SDK, ISO string on REST) → ISO string, \"\" when absent/invalid. */\nexport function isoOf(value) {\n if (!value)\n return \"\";\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? \"\" : d.toISOString();\n}\n/**\n * A moment formatted in the EVENT's time zone (medium date + short time) — sale periods,\n * registration openings. The browser's zone is wrong for a venue event; a missing/invalid zone\n * falls back to the visitor's.\n */\nexport function formatInZone(value, timeZoneId) {\n const iso = isoOf(value);\n if (!iso)\n return \"\";\n const d = new Date(iso);\n const opts = { dateStyle: \"medium\", timeStyle: \"short\" };\n try {\n return new Intl.DateTimeFormat(undefined, timeZoneId ? { ...opts, timeZone: timeZoneId } : opts).format(d);\n }\n catch {\n return new Intl.DateTimeFormat(undefined, opts).format(d);\n }\n}\n/** \"From €45\" (ticketed), \"Free\" (RSVP / free tickets), \"\" when nothing applies. */\nexport function lowestPriceLabel(reg) {\n const type = reg.type ?? reg.initialType;\n if (type === \"RSVP\")\n return \"Free\";\n if (type !== \"TICKETING\")\n return \"\";\n const lowest = reg.tickets?.lowestPrice;\n if (!lowest)\n return \"\";\n if (Number(lowest.value ?? lowest.amount ?? 0) === 0)\n return \"Free\";\n const formatted = lowest.formattedValue || formatMoney(lowest);\n return formatted ? `From ${formatted}` : \"\";\n}\n// event.description is Ricos rich content ({ nodes: [...] }), NOT a string — calling string\n// methods on it crashes the page. Extract plain paragraphs; detailedDescription (legacy plain\n// text) is the fallback. A description of one empty PARAGRAPH node (what the dashboard saves\n// for \"no description\") yields [] here, matching Wix's hasDescription() rule.\nexport function toParagraphs(rich, legacy) {\n const collect = (nodes) => (nodes ?? [])\n .map((n) => (typeof n.textData?.text === \"string\" ? n.textData.text : collect(n.nodes)))\n .join(\"\");\n const out = [];\n for (const node of rich?.nodes ?? []) {\n const text = collect([node]).trim();\n if (text)\n out.push(text);\n }\n if (!out.length && legacy)\n out.push(legacy);\n return out;\n}\n/**\n * The main image in the form imgSrc scales: the SDK carries it as a `wix:image://` string; REST\n * as `{ id, url, altText }` — rebuilt into the id form so both transports resolve one URL shape.\n */\nexport function mainImageOf(raw) {\n const m = raw.mainImage;\n if (m && typeof m === \"object\" && typeof m.id === \"string\" && m.id) {\n return `wix:image://v1/${m.id}/${encodeURIComponent(String(m.altText || \"image\"))}`;\n }\n return m;\n}\n/** A category entity (Categories API or the event's CATEGORIES fieldset) → pill DTO. */\nexport function toCategory(raw) {\n return { id: rawId(raw), name: raw.name ?? \"\" };\n}\n/**\n * The event's assigned categories that are real, visitor-facing groupings: the CATEGORIES\n * fieldset also lists the RECURRING_EVENT category every series gets and hidden ones — neither\n * is a filter pill. (`type` and `hidden` per the SDK's EventCategory.)\n */\nexport function manualCategories(raw) {\n return (raw.categories?.categories ?? [])\n .filter((c) => c.type !== \"RECURRING_EVENT\" && c.hidden !== true)\n .map(toCategory)\n .filter((c) => c.id);\n}\n/** Categories from Query Categories, MANUAL only (the query already filters; this is the safety net). */\nexport function toCategories(list) {\n return (list ?? [])\n .filter((c) => !Array.isArray(c.states) || c.states.includes(\"MANUAL\"))\n .map(toCategory)\n .filter((c) => c.id);\n}\nexport function toSummary(raw, imgSrc) {\n const reg = raw.registration ?? {};\n const dts = raw.dateAndTimeSettings ?? {};\n const dateTbd = dts.dateAndTimeTbd === true;\n const registrationType = (reg.type ?? reg.initialType ?? \"NONE\");\n return {\n id: rawId(raw), // _id on the SDK, id on REST — never a bare .id on the SDK (undefined there)\n slug: raw.slug ?? \"\",\n title: raw.title ?? \"\",\n shortDescription: raw.shortDescription ?? \"\",\n status: (raw.status ?? \"UPCOMING\"),\n // Wix's server-formatted schedule (site locale, event zone); the organizer's TBD message when there is no date.\n dateLabel: dateTbd ? dts.dateAndTimeTbdMessage || dts.formatted?.dateAndTime || \"\" : (dts.formatted?.dateAndTime ?? \"\"),\n startDateIso: dateTbd ? \"\" : isoOf(dts.startDate),\n endDateIso: dateTbd ? \"\" : isoOf(dts.endDate),\n timeZoneId: dts.timeZoneId ?? \"\",\n dateTbd,\n dateTbdMessage: dts.dateAndTimeTbdMessage ?? \"\",\n hideEndDate: dts.hideEndDate === true,\n showTimeZone: dts.showTimeZone !== false,\n locationName: raw.location?.name ?? \"\",\n locationType: raw.location?.locationTbd\n ? \"TBD\"\n : (raw.location?.type ?? \"TBD\"),\n imageUrl: imgSrc(mainImageOf(raw), 1200, 800),\n // `type` is the current flavor (can become EXTERNAL later); initialType is the immutable\n // seeded one — read type first.\n registrationType,\n priceLabel: lowestPriceLabel(reg),\n // The API reports tickets.soldOut true on an RSVP event too (it has no tickets at all) — sold\n // out is a ticketing signal only.\n soldOut: registrationType === \"TICKETING\" && reg.tickets?.soldOut === true,\n // `categories` is absent on the typed Event (an SDK type gap) — the Raw boundary reads\n // the runtime field the CATEGORIES fieldset populates.\n categories: manualCategories(raw),\n recurrenceStatus: dts.recurrenceStatus ?? \"ONE_TIME\",\n recurringCategoryId: dts.recurringEvents?.categoryId ?? \"\",\n };\n}\nconst INPUT_TYPES = new Set([\"TEXT\", \"NUMBER\", \"TEXT_ARRAY\", \"DATE_TIME\", \"ADDRESS\"]);\nfunction toFormInput(raw) {\n const type = INPUT_TYPES.has(raw.type) ? raw.type : raw.array === true ? \"TEXT_ARRAY\" : \"TEXT\";\n return {\n name: raw.name ?? \"\",\n label: raw.label ?? raw.name ?? \"\",\n mandatory: raw.mandatory === true,\n type,\n options: Array.isArray(raw.options) ? raw.options.map(String) : [],\n maxLength: Number(raw.maxLength) > 0 ? Number(raw.maxLength) : 0,\n };\n}\n/**\n * The organizer's RSVP form from the FORM fieldset. createRsvp validates `form.inputValues`\n * against `form.controls[].inputs`: an unknown input name, a missing mandatory value (for a YES),\n * or a value outside the predefined options is INVALID_FORM_RESPONSE — so the form is rendered\n * from this, never hardcoded. Deleted controls are skipped; controls keep `orderIndex` order.\n * GUEST_CONTROL is lifted out: its answer travels as `additionalGuestDetails`, not as an input\n * value (the Create RSVP contract). Its max is read from the count input's numeric options, else\n * the names input's `maxSize`.\n */\nexport function toRsvpForm(form) {\n if (!form || !Array.isArray(form.controls))\n return null;\n const live = form.controls\n .filter((c) => c.deleted !== true)\n .slice()\n .sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0));\n const controls = [];\n let guestControl = null;\n for (const c of live) {\n const inputs = (c.inputs ?? []).map(toFormInput).filter((i) => i.name);\n if (c.type === \"GUEST_CONTROL\") {\n const count = inputs.find((i) => i.type === \"NUMBER\") ?? inputs[0];\n const names = inputs.find((i) => i.type === \"TEXT_ARRAY\");\n const numeric = (count?.options ?? []).map(Number).filter((n) => Number.isFinite(n));\n const fromOptions = numeric.length ? Math.max(...numeric) : 0;\n const rawNames = (c.inputs ?? []).find((i) => i.type === \"TEXT_ARRAY\" || i.array === true);\n const maxGuests = fromOptions || Number(rawNames?.maxSize ?? 0) || 0;\n guestControl = { label: count?.label ?? \"Guests\", maxGuests, namesLabel: names?.label ?? \"Guest names\" };\n continue;\n }\n controls.push({ id: rawId(c) || c.name || `${controls.length}`, type: c.type ?? \"INPUT\", system: c.system === true, inputs });\n }\n return { controls, guestControl };\n}\n/** `registration.tickets.taxSettings` → DTO (null when the organizer configured no tax). */\nexport function toTaxSettings(ts) {\n if (!ts || !ts.type)\n return null;\n const rate = Number(ts.rate);\n return {\n name: ts.name ?? \"Tax\",\n ratePercent: Number.isFinite(rate) ? rate : 0,\n includedInPrice: ts.type === \"INCLUDED_IN_PRICE\",\n appliedToDonations: ts.appliedToDonations === true,\n };\n}\n/** The venue's one-line address: Wix's `formatted` (a runtime field), else the parts joined. */\nexport function formattedAddress(address) {\n if (!address)\n return \"\";\n if (typeof address.formatted === \"string\" && address.formatted)\n return address.formatted;\n if (typeof address.formattedAddress === \"string\" && address.formattedAddress)\n return address.formattedAddress;\n return [address.addressLine1 ?? address.addressLine, address.addressLine2, address.city, address.subdivision, address.postalCode, address.country]\n .filter((p) => typeof p === \"string\" && p.trim())\n .join(\", \");\n}\n/** Coordinates when Wix geocoded the venue (`address.location` on events; `geocode` on ticket definitions). */\nexport function coordinatesOf(address) {\n const geo = address?.location ?? address?.geocode;\n const lat = Number(geo?.latitude);\n const lng = Number(geo?.longitude);\n return Number.isFinite(lat) && Number.isFinite(lng) && (lat !== 0 || lng !== 0) ? { lat, lng } : null;\n}\n/** A Wix PageUrl: the SDK hands a string; REST hands `{ base, path }` (joined the way the SDK does). */\nexport function pageUrlOf(value) {\n if (!value)\n return \"\";\n if (typeof value === \"string\")\n return value;\n const v = value;\n if (typeof v.base !== \"string\")\n return \"\";\n const base = /^https?:\\/\\//.test(v.base) ? v.base : `https://${v.base}`;\n return `${base}${v.path ?? \"\"}`;\n}\nexport function toDetail(raw, imgSrc) {\n const summary = toSummary(raw, imgSrc);\n const reg = raw.registration ?? {};\n const status = (typeof reg.status === \"string\" ? reg.status : \"UNKNOWN_REGISTRATION_STATUS\");\n const paused = reg.registrationPaused === true || reg.registrationDisabled === true;\n const opensAt = status === \"SCHEDULED_RSVP\" ? isoOf(reg.rsvp?.startDate) : \"\";\n const formatted = raw.dateAndTimeSettings?.formatted ?? {};\n const limit = Number(reg.tickets?.ticketLimitPerOrder);\n return {\n ...summary,\n aboutParagraphs: toParagraphs(raw.description, raw.detailedDescription),\n // An empty description (the dashboard saves one empty paragraph) is \"\", as aboutParagraphs is [].\n aboutHtml: richContentToHtml(raw.description, imgSrc).replace(/<p>\\s*<\\/p>/g, \"\"),\n formatted: {\n startDate: formatted.startDate ?? \"\",\n startTime: formatted.startTime ?? \"\",\n endDate: formatted.endDate ?? \"\",\n endTime: formatted.endTime ?? \"\",\n },\n address: summary.locationType === \"VENUE\" ? formattedAddress(raw.location?.address) : \"\",\n coordinates: summary.locationType === \"VENUE\" ? coordinatesOf(raw.location?.address) : null,\n registrationStatus: status,\n // Wix: closed is exactly the two CLOSED_* statuses. Open = an OPEN_* status the organizer hasn't paused;\n // SCHEDULED_RSVP is neither (it opens at rsvp.startDate).\n registrationOpen: status.startsWith(\"OPEN_\") && !paused && summary.registrationType !== \"NONE\",\n registrationClosed: status === \"CLOSED_MANUALLY\" || status === \"CLOSED_AUTOMATICALLY\",\n registrationOpensAtIso: opensAt,\n registrationOpensAtLabel: formatInZone(opensAt, summary.timeZoneId),\n registrationPaused: paused && status.startsWith(\"OPEN_\"),\n waitlistOnly: status === \"OPEN_RSVP_WAITLIST_ONLY\",\n membersOnly: reg.allowedGuestTypes === \"MEMBER\",\n rsvpResponseType: reg.rsvp?.responseType === \"YES_AND_NO\" ? \"YES_AND_NO\" : \"YES_ONLY\",\n rsvpForm: summary.registrationType === \"RSVP\" ? toRsvpForm(raw.form) : null,\n externalUrl: reg.external?.url ?? \"\",\n eventPageUrl: pageUrlOf(raw.eventPageUrl),\n addToCalendar: { google: raw.calendarUrls?.google ?? \"\", ics: raw.calendarUrls?.ics ?? \"\" },\n taxSettings: toTaxSettings(reg.tickets?.taxSettings),\n // Wix default 20, max 50 — the reservation rejects a larger order with an opaque error.\n ticketLimitPerOrder: Number.isInteger(limit) && limit > 0 ? Math.min(limit, 50) : 20,\n };\n}\n",
16 "js/wix/events-order-store.js": "// Declarations of events-order-store.ts: what this file exports, their arguments and what they return.\n// import type { EventDetail, OrderSummary } from \"./events-types.js\";\n// export interface OrderStoreOptions {\n// orderNumber: string;\n// eventId: string;\n// }\n// export interface OrderState {\n// orderNumber: string;\n// eventId: string;\n// event: EventDetail | null;\n// order: OrderSummary | null;\n// orderUnavailable: boolean;\n// polling: boolean;\n// loading: boolean;\n// }\n// export interface OrderStore {\n// getState(): OrderState;\n// subscribe(listener: (state: OrderState) => void): () => void;\n// start(): void;\n// stop(): void;\n// }\n// export declare function createOrderStore({ orderNumber, eventId }: OrderStoreOptions): OrderStore;\n// The post-checkout confirmation as a framework-free store — the logic behind\n// useEventConfirmation, usable from React (the hook wraps it), from a static confirmation page,\n// from Vue/Svelte, or as the specification for a port. The Wix-hosted checkout redirects to the\n// thank-you URL with `?orderNumber=&eventId=`; this store fetches the event for context and the\n// order for its details, then polls the order every 2 s for up to 15 s until the tickets are\n// generated (Wix's own thank-you page does the same) so the PDF link can appear.\n//\n// The order read runs on the visitor token, as Wix's headless demo does. It isn't guaranteed for\n// every site's visitor scope: when it is refused, `orderUnavailable` is set and the surface shows\n// the confirmation without order details — landing here IS the success signal (Wix redirects only\n// after checkout completes); the tickets arrive by email regardless.\nimport { fetchEventById } from \"./events.js\";\nimport { fetchOrder } from \"./orders.js\";\nimport { ORDER_POLL_INTERVAL_MS, ORDER_POLL_TOTAL_MS } from \"./orders-core.js\";\nexport function createOrderStore({ orderNumber, eventId }) {\n let event = null;\n let order = null;\n let orderUnavailable = false;\n let polling = false;\n let loading = Boolean(orderNumber && eventId);\n let started = false;\n let timer = null;\n const listeners = new Set();\n let snapshot = null;\n const emit = () => {\n snapshot = null;\n for (const fn of listeners)\n fn();\n };\n function getState() {\n if (snapshot)\n return snapshot;\n snapshot = { orderNumber, eventId, event, order, orderUnavailable, polling, loading };\n return snapshot;\n }\n async function readOrder() {\n const next = await fetchOrder(eventId, orderNumber);\n if (!started)\n return true;\n if (!next) {\n orderUnavailable = order === null;\n return true; // nothing to poll for\n }\n order = next;\n return next.ticketsReady;\n }\n function schedule(elapsed) {\n if (!started || elapsed >= ORDER_POLL_TOTAL_MS) {\n polling = false;\n emit();\n return;\n }\n polling = true;\n emit();\n timer = setTimeout(async () => {\n timer = null;\n let ready = true;\n try {\n ready = await readOrder();\n }\n catch {\n /* a transient failure: keep the last order, try again until the budget is spent */\n ready = false;\n }\n if (!started)\n return;\n if (ready) {\n polling = false;\n emit();\n }\n else {\n schedule(elapsed + ORDER_POLL_INTERVAL_MS);\n }\n }, ORDER_POLL_INTERVAL_MS);\n }\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"events-order-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n if (!orderNumber || !eventId)\n return;\n fetchEventById(eventId)\n .then((e) => {\n if (!started)\n return;\n event = e;\n emit();\n })\n .catch(() => { });\n readOrder()\n .then((ready) => {\n if (!started)\n return;\n loading = false;\n emit();\n if (!ready)\n schedule(0);\n })\n .catch(() => {\n if (!started)\n return;\n // Refused for this visitor scope (or a transient failure) — degrade, never fake details.\n orderUnavailable = true;\n loading = false;\n emit();\n });\n },\n stop() {\n started = false;\n if (timer)\n clearTimeout(timer);\n timer = null;\n },\n };\n}\n",
17 "js/wix/events-store.js": "// Declarations of events-store.ts: what this file exports, their arguments and what they return.\n// import { type StatusFilter } from \"./events-core.js\";\n// import type { EventCategory, EventSummary } from \"./events-types.js\";\n// export interface EventsStoreOptions {\n// initialEvents?: EventSummary[];\n// initialTotal?: number;\n// pageSize?: number;\n// status?: StatusFilter;\n// }\n// export interface EventsState {\n// events: EventSummary[] | null;\n// categories: EventCategory[];\n// activeCategoryId: string | null;\n// hasMore: boolean;\n// loadingMore: boolean;\n// error: string | null;\n// }\n// export interface EventsStore {\n// getState(): EventsState;\n// subscribe(listener: (state: EventsState) => void): () => void;\n// start(): void;\n// stop(): void;\n// setActiveCategoryId(id: string | null): void;\n// loadMore(): Promise<void>;\n// }\n// export declare function createEventsStore({ initialEvents, initialTotal, pageSize, status }?: EventsStoreOptions): EventsStore;\n// The events listing as a framework-free store — the logic behind useEvents, usable from React\n// (useEvents wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and\n// render), or as the specification for a port. State, actions, subscribe/getState, emit after\n// every change.\n//\n// SSR-friendly: seed with `initialEvents` (+ `initialTotal` from fetchEventsPage) and they render\n// at once with no client fetch; a SPA passes nothing and `start()` fetches the first page. Pages\n// are Wix's 20; `loadMore()` appends the next. The category pills are the site's MANUAL categories\n// (Query Categories on the visitor token, as Wix's listing does); when that read is refused for\n// this site's visitor scope the pills fall back to the manual categories on the loaded events.\n// Filtering by category is client-side while the whole list is loaded, server-side (a refetch)\n// once there are more pages than loaded — so a filter never hides events on unloaded pages.\n//\n// One store per mounted listing (a page can hold the index and a \"next up\" rail):\n// createEventsStore(), not a singleton.\nimport { fetchCategories, fetchEventsPage } from \"./events.js\";\nimport { DEFAULT_PAGE_SIZE } from \"./events-core.js\";\nexport function createEventsStore({ initialEvents, initialTotal, pageSize = DEFAULT_PAGE_SIZE, status } = {}) {\n // `all` is the unfiltered list; `filtered` is the server-filtered list for the active category (null = not in use).\n let all = initialEvents ?? null;\n let allTotal = initialTotal ?? initialEvents?.length ?? 0;\n let filtered = null;\n let filteredTotal = 0;\n let fetchedCategories = null;\n let activeCategoryId = null;\n let loadingMore = false;\n let error = null;\n let started = false;\n let generation = 0; // bumps on every filter change so a late page for an old filter is dropped\n const listeners = new Set();\n let snapshot = null;\n const emit = () => {\n snapshot = null;\n for (const fn of listeners)\n fn();\n };\n const message = (e) => (e instanceof Error ? e.message : String(e));\n const allLoaded = () => all !== null && all.length >= allTotal;\n const serverFiltering = () => activeCategoryId !== null && !allLoaded();\n function derivedCategories() {\n const seen = new Map();\n for (const e of all ?? [])\n for (const c of e.categories)\n if (!seen.has(c.id))\n seen.set(c.id, c.name);\n return [...seen.entries()].map(([id, name]) => ({ id, name }));\n }\n function getState() {\n if (snapshot)\n return snapshot;\n const events = serverFiltering()\n ? filtered\n : all === null\n ? null\n : activeCategoryId\n ? all.filter((e) => e.categories.some((c) => c.id === activeCategoryId))\n : all;\n // Never before the first page is in: a \"load more\" under skeletons would be a lie.\n const hasMore = serverFiltering() ? filtered !== null && filtered.length < filteredTotal : activeCategoryId === null && all !== null && !allLoaded();\n snapshot = {\n events,\n categories: fetchedCategories ?? derivedCategories(),\n activeCategoryId,\n hasMore,\n loadingMore,\n error,\n };\n return snapshot;\n }\n async function loadPage(categoryId, offset) {\n const gen = generation;\n loadingMore = true;\n error = null;\n emit();\n try {\n const page = await fetchEventsPage({ limit: pageSize, offset, categoryId: categoryId ?? undefined, status });\n if (!started || gen !== generation)\n return;\n if (categoryId === null) {\n all = offset === 0 ? page.events : [...(all ?? []), ...page.events];\n allTotal = page.total;\n }\n else {\n filtered = offset === 0 ? page.events : [...(filtered ?? []), ...page.events];\n filteredTotal = page.total;\n }\n }\n catch (e) {\n if (!started || gen !== generation)\n return;\n if (categoryId === null && all === null)\n all = [];\n if (categoryId !== null && filtered === null)\n filtered = [];\n error = message(e);\n }\n finally {\n if (started && gen === generation) {\n loadingMore = false;\n emit();\n }\n }\n }\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"events-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n // The visitor-scope category read — a refusal is not an error state, the pills derive from the events instead.\n fetchCategories()\n .then((list) => {\n if (!started)\n return;\n fetchedCategories = list;\n emit();\n })\n .catch(() => { });\n if (!initialEvents)\n void loadPage(null, 0);\n },\n stop() {\n started = false;\n },\n setActiveCategoryId(id) {\n if (id === activeCategoryId)\n return;\n activeCategoryId = id;\n generation++;\n filtered = null;\n filteredTotal = 0;\n loadingMore = false;\n emit();\n if (serverFiltering())\n void loadPage(id, 0);\n },\n async loadMore() {\n if (loadingMore || !getState().hasMore)\n return;\n if (serverFiltering())\n await loadPage(activeCategoryId, (filtered ?? []).length);\n else\n await loadPage(null, (all ?? []).length);\n },\n };\n}\n",
18 "js/wix/events-types.js": "// Declarations of events-types.ts: what this file exports, their arguments and what they return.\n// /** The current registration flavor — every registration surface branches on this. */\n// export type RegistrationType = \"RSVP\" | \"TICKETING\" | \"EXTERNAL\" | \"NONE\";\n// /** Event lifecycle (Events V3 `status`) — only UPCOMING/STARTED are listed by default. */\n// export type EventStatus = \"UPCOMING\" | \"STARTED\" | \"ENDED\" | \"CANCELED\" | \"DRAFT\";\n// /**\n// * Events V3 `registration.status`. Closed means exactly CLOSED_MANUALLY / CLOSED_AUTOMATICALLY;\n// * SCHEDULED_RSVP opens later; OPEN_RSVP_WAITLIST_ONLY accepts registrations onto a waitlist.\n// */\n// export type RegistrationStatus = \"OPEN_RSVP\" | \"OPEN_RSVP_WAITLIST_ONLY\" | \"OPEN_TICKETS\" | \"OPEN_EXTERNAL\" | \"SCHEDULED_RSVP\" | \"CLOSED_MANUALLY\" | \"CLOSED_AUTOMATICALLY\" | \"UNKNOWN_REGISTRATION_STATUS\";\n// /** An event as a listing/grid tile needs it. */\n// export interface EventSummary {\n// id: string;\n// slug: string;\n// title: string;\n// /** Plain-text teaser, safe to render directly (the rich description never leaves the data layer). */\n// shortDescription: string;\n// status: EventStatus;\n// /**\n// * Human-formatted date/time from Wix in the event's zone, e.g. \"Sep 26, 2026, 7:00 PM\"; the\n// * organizer's TBD message when the date is TBD (\"\" only when both are missing).\n// */\n// dateLabel: string;\n// /** ISO start/end instants for custom formatting/grouping (\"\" when the date is TBD). */\n// startDateIso: string;\n// endDateIso: string;\n// /** IANA zone of the event (\"\" when unknown) — pass as `timeZone` to Intl for any date you format yourself. */\n// timeZoneId: string;\n// /** True when the organizer marked the schedule TBD; `dateTbdMessage` is their copy for it. */\n// dateTbd: boolean;\n// dateTbdMessage: string;\n// /** Organizer display flags for the schedule. */\n// hideEndDate: boolean;\n// showTimeZone: boolean;\n// locationName: string;\n// locationType: \"VENUE\" | \"ONLINE\" | \"TBD\";\n// /** Resolved https URL (\"\" when the event has no image). */\n// imageUrl: string;\n// registrationType: RegistrationType;\n// /** \"From €25.00\" (ticketed), \"Free\" (RSVP / free tickets), \"\" when nothing applies. */\n// priceLabel: string;\n// soldOut: boolean;\n// /** Manual categories only (the per-series RECURRING_EVENT and hidden ones are dropped). */\n// categories: {\n// id: string;\n// name: string;\n// }[];\n// /** \"RECURRING\" / \"RECURRING_UPCOMING\" for one date of a series; \"ONE_TIME\" otherwise. */\n// recurrenceStatus: string;\n// /** Every date of a recurring series shares this id (\"\" for a one-time event) — `fetchOccurrences(id)` lists them. */\n// recurringCategoryId: string;\n// }\n// /** A category as the filter pills need it. */\n// export interface EventCategory {\n// id: string;\n// name: string;\n// }\n// /** One field of the organizer's RSVP form (Events V3 `form.controls[].inputs[]`). */\n// export interface RsvpFormInput {\n// /** The `inputName` to submit under. */\n// name: string;\n// label: string;\n// mandatory: boolean;\n// /** Wix value type: TEXT (also DROPDOWN/RADIO values), NUMBER, TEXT_ARRAY (CHECKBOX), DATE_TIME, ADDRESS. */\n// type: \"TEXT\" | \"NUMBER\" | \"TEXT_ARRAY\" | \"DATE_TIME\" | \"ADDRESS\";\n// /** Predefined choices (DROPDOWN / RADIO / CHECKBOX); the submitted value must be one of them. */\n// options: string[];\n// /** Max characters for text inputs (0 = unlimited). */\n// maxLength: number;\n// }\n// /** One control of the organizer's RSVP form; the built-in name and email controls carry `system: true`. */\n// export interface RsvpFormControl {\n// id: string;\n// type: \"INPUT\" | \"TEXTAREA\" | \"DROPDOWN\" | \"RADIO\" | \"CHECKBOX\" | \"NAME\" | \"GUEST_CONTROL\" | \"ADDRESS_SHORT\" | \"ADDRESS_FULL\" | \"DATE\";\n// system: boolean;\n// inputs: RsvpFormInput[];\n// }\n// /** The organizer's RSVP form: controls in display order, plus the \"+guests\" control when enabled. */\n// export interface RsvpForm {\n// /** Every non-deleted control except GUEST_CONTROL, in `orderIndex` order. */\n// controls: RsvpFormControl[];\n// /** Present when the organizer lets a guest bring others — rendered as a 0..maxGuests picker plus names. */\n// guestControl: {\n// label: string;\n// maxGuests: number;\n// namesLabel: string;\n// } | null;\n// }\n// /** Event-level tax settings (`registration.tickets.taxSettings`) — what the hosted checkout charges. */\n// export interface TaxSettings {\n// name: string;\n// /** e.g. 21 for \"21%\". */\n// ratePercent: number;\n// /** INCLUDED_IN_PRICE → prices already contain it; else it is added at checkout. */\n// includedInPrice: boolean;\n// appliedToDonations: boolean;\n// }\n// /** An event as the detail/registration page needs it. */\n// export interface EventDetail extends EventSummary {\n// /** Long description as plain paragraphs (extracted from the rich content; may be empty). */\n// aboutParagraphs: string[];\n// /** The same description as sanitized HTML (\"\" for none): headings, lists, links, images. Set it as innerHTML. */\n// aboutHtml: string;\n// /** Wix's formatted schedule parts (\"\" for a TBD schedule; end parts \"\" when the end date is hidden). */\n// formatted: {\n// startDate: string;\n// startTime: string;\n// endDate: string;\n// endTime: string;\n// };\n// /** Full formatted street address (\"\" for ONLINE/TBD). */\n// address: string;\n// /** Venue coordinates when Wix geocoded the address — for a map embed. */\n// coordinates: {\n// lat: number;\n// lng: number;\n// } | null;\n// /** The raw Wix registration status — branch on the booleans below, keep this for copy. */\n// registrationStatus: RegistrationStatus;\n// /** Accepting registrations RIGHT NOW (an OPEN_* status, not paused) — the form/picker renders only then. */\n// registrationOpen: boolean;\n// /** CLOSED_MANUALLY / CLOSED_AUTOMATICALLY — the honest closed state (sold out when `soldOut`). */\n// registrationClosed: boolean;\n// /** SCHEDULED_RSVP: when registration opens (\"\" otherwise), raw and formatted in the event's zone. */\n// registrationOpensAtIso: string;\n// registrationOpensAtLabel: string;\n// /** The organizer paused registration — open status, but nothing is accepted. */\n// registrationPaused: boolean;\n// /** OPEN_RSVP_WAITLIST_ONLY: the guest list is full; an RSVP joins the waitlist. */\n// waitlistOnly: boolean;\n// /** `allowedGuestTypes === \"MEMBER\"`: only signed-in site members may register. */\n// membersOnly: boolean;\n// /** RSVP events: \"YES_AND_NO\" also allows a \"can't make it\" reply. */\n// rsvpResponseType: \"YES_ONLY\" | \"YES_AND_NO\";\n// /** RSVP events: the organizer's form (from the FORM fieldset); null for other types. */\n// rsvpForm: RsvpForm | null;\n// /** EXTERNAL events: the outbound registration URL (\"\" otherwise). */\n// externalUrl: string;\n// /** The event's page on the organizer's Wix site (\"\" when unavailable). */\n// eventPageUrl: string;\n// addToCalendar: {\n// google: string;\n// ics: string;\n// };\n// /** TICKETING: tax the hosted checkout applies (null when none is configured). */\n// taxSettings: TaxSettings | null;\n// /** TICKETING: max tickets in one order (Wix default 20, max 50) — the picker caps the total here. */\n// ticketLimitPerOrder: number;\n// }\n// /** A purchasable ticket tier (TICKETING events, Ticket Definitions V3). */\n// export interface TicketTier {\n// id: string;\n// name: string;\n// description: string;\n// /**\n// * FIXED: one price. FREE: no charge. GUEST: the guest names the amount (a donation, at least\n// * `minPrice`). OPTIONS: several named prices, each picked with its own quantity.\n// */\n// pricingType: \"FIXED\" | \"FREE\" | \"GUEST\" | \"OPTIONS\";\n// /** Formatted: \"€45.00\" (FIXED), \"Free\" (FREE), \"€10.00 – €25.00\" (OPTIONS range), \"\" (GUEST). */\n// price: string;\n// /** FIXED: the raw decimal (\"45.00\") behind `price` — for totals only, never displayed (\"0\" otherwise). */\n// priceValue: string;\n// free: boolean;\n// /** GUEST: formatted minimum (\"€5.00\") and its raw decimal (\"5.00\") for validation; \"\" otherwise. */\n// minPrice: string;\n// minPriceValue: string;\n// /** ISO currency of the tier (\"\" when unknown). */\n// currency: string;\n// /** OPTIONS: the named prices (`value` is the raw decimal for totals, never displayed); `notes` are that option's tax/fee lines. Empty for other types. */\n// options: {\n// id: string;\n// name: string;\n// price: string;\n// value: string;\n// notes: string[];\n// }[];\n// /** Tax and Wix fee lines the checkout adds, e.g. [\"+€9.45 VAT\", \"+€1.36 ticket service fee\"] or [\"VAT included\"]. */\n// notes: string[];\n// /** Max quantity per order for this tier RIGHT NOW; 0 means sold out (Wix lowers it to the unsold count). */\n// limitPerCheckout: number;\n// soldOut: boolean;\n// /** \"SALE_STARTED\" is the only buyable status. */\n// saleStatus: \"SALE_SCHEDULED\" | \"SALE_STARTED\" | \"SALE_ENDED\";\n// /** Buyable now: on sale and not sold out — enable the controls on this. */\n// available: boolean;\n// /** SALE_SCHEDULED: \"Goes on sale\" moment, formatted in the event's zone (\"\" otherwise). */\n// saleStartsLabel: string;\n// /** Sale end moment, formatted in the event's zone (\"\" when there is none or the sale hasn't started). */\n// saleEndsLabel: string;\n// /** Wix fee handling: FEE_ADDED_AT_CHECKOUT adds 2.5% at checkout; FEE_INCLUDED/NO_FEE add nothing. */\n// feeType: \"FEE_INCLUDED\" | \"FEE_ADDED_AT_CHECKOUT\" | \"NO_FEE\" | \"\";\n// }\n// /** One line of the picker's selection: a tier, optionally one of its pricing options, and for GUEST tiers the named price. */\n// export interface TicketSelection {\n// tierId: string;\n// quantity: number;\n// optionId?: string;\n// /** GUEST tiers: the decimal the guest typed (\"25.00\"). */\n// guestPrice?: string;\n// }\n// /** The picker's running totals, mirroring Wix's own picker; the hosted checkout is authoritative. */\n// export interface SelectionTotals {\n// subtotal: string;\n// tax: string;\n// fee: string;\n// total: string;\n// }\n// /** The outcome of a registration: the browser is redirecting to Wix checkout, or the RSVP is in. */\n// export type RegistrationResult = {\n// kind: \"redirect\";\n// url: string;\n// } | {\n// kind: \"rsvpConfirmed\";\n// status: \"YES\" | \"NO\" | \"WAITLIST\";\n// };\n// /** A ticket order as the confirmation page shows it (Orders `getOrder`, TICKETS + DETAILS + INVOICE). */\n// export interface OrderSummary {\n// number: string;\n// /** Wix order status: FREE, PAID, PENDING, OFFLINE_PENDING, INITIATED, CANCELED, DECLINED, … */\n// status: string;\n// /** Visitor copy for the status (\"Paid\", \"Awaiting payment\", …). */\n// statusLabel: string;\n// /** FREE or PAID — the order is settled. */\n// settled: boolean;\n// email: string;\n// /** \"Sep 26, 2026\" — the order date. */\n// createdLabel: string;\n// items: {\n// name: string;\n// price: string;\n// quantity: number;\n// total: string;\n// }[];\n// subtotal: string;\n// /** \"\" when no coupon applied. */\n// couponDiscount: string;\n// paidPlanDiscount: {\n// amount: string;\n// ratePercent: number;\n// } | null;\n// tax: {\n// name: string;\n// ratePercent: number;\n// amount: string;\n// } | null;\n// /** The Wix ticket service fee when it was added at checkout. */\n// fee: {\n// amount: string;\n// ratePercent: number;\n// } | null;\n// total: string;\n// /** Download link for the ticket PDF (\"\" until the tickets are generated). */\n// ticketsPdfUrl: string;\n// ticketCount: number;\n// /** Tickets are generated asynchronously after payment — true once every ticket exists. */\n// ticketsReady: boolean;\n// }\n// Events DTOs — the serializable shapes every hook, component, and page consumes.\n// Plain JSON: safe as Astro island props or across server/client boundaries. Images are\n// resolved https URLs; every displayable price is a ready formatted string; every date label is\n// formatted in the EVENT's time zone (a venue event shown in the browser's zone is wrong).\nexport {};\n",
19 "js/wix/events.js": "// Declarations of events.ts: what this file exports, their arguments and what they return.\n// import { EVENTS_APP_ID, type ListOptions } from \"./events-core.js\";\n// import type { EventCategory, EventDetail, EventSummary } from \"./events-types.js\";\n// export { EVENTS_APP_ID, type ListOptions };\n// export interface EventsPage {\n// events: EventSummary[];\n// total: number;\n// hasMore: boolean;\n// }\n// export declare function fetchEventsPage(options?: ListOptions): Promise<EventsPage>;\n// export declare function fetchEvents(options?: ListOptions): Promise<EventSummary[]>;\n// export declare function fetchOccurrences(recurringCategoryId: string, { limit }?: {\n// limit?: number;\n// }): Promise<EventSummary[]>;\n// export declare function fetchCategories(): Promise<EventCategory[]>;\n// export declare function fetchEventBySlug(slug: string): Promise<EventDetail | null>;\n// export declare function fetchEventById(eventId: string): Promise<EventDetail | null>;\n// Event reads over REST — the twin of app/wix/events/events.ts. Same exports, same DTOs; the rules\n// and mappers come from events-core (the SAME file the SDK transport uses, deployed flat next to\n// this one by deploy.mjs --stack static), so this file is only the transport: one fetch with a\n// literal body per function. Every call here is safe from a browser with a visitor token.\n// Porting: keep the paths, keep the bodies, port the core once.\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/events-v3/query-events.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/events-v3/get-event-by-slug.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/events-v3/get-event.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/categories/query-categories.md\nimport { WixApiError, wixRequest } from \"./client.js\";\nimport { imgSrc } from \"./media.js\";\nimport { DEFAULT_PAGE_SIZE, DETAIL_FIELDS, EVENTS_APP_ID, LIST_FIELDS, categoriesQuery, hasMorePages, listQuery, rawId, toCategories, toDetail, toSummary, } from \"./events-core.js\";\nexport { EVENTS_APP_ID };\nconst notFound = (e) => e instanceof WixApiError && e.status === 404;\n/**\n * One page of events: UPCOMING/STARTED by default (`status: \"past\"` for ENDED, `\"all\"` for both),\n * soonest first then newest first, optionally one category (server-side), `limit` (default 20)\n * from `offset`. The limit is always sent: `paging.limit` defaults to 0 and a bare query answers\n * `total: N, events: []` with no error.\n * POST /events/v3/events/query { query: { filter: { status: { $in }, \"categories.id\"?: { $in } }, sort, paging: { limit, offset } }, fields }\n * → { events, pagingMetadata: { total } }\n */\nexport async function fetchEventsPage(options = {}) {\n const { offset = 0 } = options;\n const res = await wixRequest(\"/events/v3/events/query\", { body: { query: listQuery({ limit: DEFAULT_PAGE_SIZE, ...options }), fields: LIST_FIELDS } });\n const items = (res?.events ?? []).map((e) => toSummary(e, imgSrc));\n const total = typeof res?.pagingMetadata?.total === \"number\" ? res.pagingMetadata.total : offset + items.length;\n return { events: items, total, hasMore: hasMorePages(offset + items.length, total) };\n}\n/** The events of one page as a plain list (live events, soonest first, by default). */\nexport async function fetchEvents(options = {}) {\n return (await fetchEventsPage(options)).events;\n}\n/**\n * Every live date of a recurring series — the series' `recurringCategoryId` is a category every\n * occurrence carries, so this is the listing filtered to it.\n */\nexport async function fetchOccurrences(recurringCategoryId, { limit = 100 } = {}) {\n if (!recurringCategoryId)\n return [];\n return fetchEvents({ categoryId: recurringCategoryId, limit });\n}\n/**\n * The site's MANUAL categories (the filter pills). Runs on the visitor token: needs\n * WIX_EVENTS.READ_CATEGORIES, which Wix's own headless demo exercises client-side but which isn't\n * guaranteed for every site — callers catch a failure (403) and fall back to the categories on the\n * loaded events.\n * POST /events/v1/categories/query { query: { filter: { states: { $hasSome: [\"MANUAL\"] } }, paging: { limit: 100 } } } → { categories }\n */\nexport async function fetchCategories() {\n const res = await wixRequest(\"/events/v1/categories/query\", { body: { query: categoriesQuery() } });\n return toCategories(res?.categories);\n}\n/**\n * One event by URL slug; null when the slug doesn't resolve (EVENT_NOT_FOUND, 404) — a real 404,\n * never a fallback to another event. The response is WRAPPED: { event }.\n * GET /events/v3/events/slug/{slug}?fields=DETAILS&fields=TEXTS&fields=REGISTRATION&fields=URLS&fields=CATEGORIES&fields=FORM\n */\nexport async function fetchEventBySlug(slug) {\n try {\n const res = await wixRequest(`/events/v3/events/slug/${encodeURIComponent(slug)}`, { method: \"GET\", query: { fields: DETAIL_FIELDS } });\n return res?.event ? toDetail(res.event, imgSrc) : null;\n }\n catch (e) {\n if (notFound(e))\n return null;\n throw e;\n }\n}\n/**\n * One event by id — the post-checkout confirmation read (the thank-you URL carries\n * `?orderNumber=&eventId=`). On REST this response is WRAPPED too: { event } (the SDK's getEvent\n * is the unwrapped one). GET /events/v3/events/{eventId}?fields=…\n */\nexport async function fetchEventById(eventId) {\n try {\n const res = await wixRequest(`/events/v3/events/${encodeURIComponent(eventId)}`, { method: \"GET\", query: { fields: DETAIL_FIELDS } });\n const raw = res?.event;\n return raw && rawId(raw) ? toDetail(raw, imgSrc) : null;\n }\n catch (e) {\n if (notFound(e))\n return null;\n throw e;\n }\n}\n",
20 "js/wix/orders-core.js": "// Declarations of orders-core.ts: what this file exports, their arguments and what they return.\n// import type { OrderSummary } from \"./events-types.js\";\n// import { type Raw } from \"./events-core.js\";\n// export declare const ORDER_FIELDSETS: readonly [\"TICKETS\", \"DETAILS\", \"INVOICE\"];\n// export declare const ORDER_POLL_INTERVAL_MS = 2000;\n// export declare const ORDER_POLL_TOTAL_MS = 15000;\n// export declare const ORDER_STATUS_COPY: Record<string, string>;\n// export declare function orderReady(raw: Raw | null | undefined): boolean;\n// export declare function formatOrderDate(value: unknown): string;\n// export declare function toOrder(raw: Raw): OrderSummary;\nimport { formatMoney, isoOf } from \"./events-core.js\";\n/** Get Order fieldsets: TICKETS (the generated tickets), DETAILS (status, created, email, ticketsPdf), INVOICE. */\nexport const ORDER_FIELDSETS = [\"TICKETS\", \"DETAILS\", \"INVOICE\"];\n/** Wix polls the order every 2 s for up to 15 s until the tickets exist (they are generated after payment). */\nexport const ORDER_POLL_INTERVAL_MS = 2000;\nexport const ORDER_POLL_TOTAL_MS = 15000;\n/** Visitor copy per Wix order status. */\nexport const ORDER_STATUS_COPY = {\n FREE: \"Confirmed\",\n PAID: \"Paid\",\n PENDING: \"Payment pending\",\n OFFLINE_PENDING: \"Payment due at the event\",\n INITIATED: \"Awaiting payment\",\n AUTHORIZED: \"Payment authorized\",\n PARTIALLY_PAID: \"Partially paid\",\n CANCELED: \"Canceled\",\n DECLINED: \"Payment declined\",\n VOIDED: \"Voided\",\n};\n/** Wix's isOrderReady: every ordered ticket has been generated. */\nexport function orderReady(raw) {\n const quantity = Number(raw?.ticketsQuantity);\n return Number.isFinite(quantity) && quantity === (raw?.tickets ?? []).length;\n}\n/** \"Sep 26, 2026\" — the order date in the visitor's locale (Wix formats it without a zone). */\nexport function formatOrderDate(value) {\n const iso = isoOf(value);\n if (!iso)\n return \"\";\n try {\n return new Intl.DateTimeFormat(undefined, { dateStyle: \"medium\" }).format(new Date(iso));\n }\n catch {\n return iso.slice(0, 10);\n }\n}\nconst percent = (rate) => {\n const n = Number(rate);\n return Number.isFinite(n) ? n : 0;\n};\n/** A raw Order (TICKETS + DETAILS + INVOICE) → the confirmation DTO. */\nexport function toOrder(raw) {\n const invoice = raw.invoice ?? {};\n const discounts = invoice.discount?.discounts ?? [];\n const coupon = discounts.find((d) => d.coupon);\n const paidPlan = discounts.find((d) => d.paidPlan);\n const tax = invoice.tax;\n const fee = (invoice.fees ?? []).find((f) => f.type === \"FEE_ADDED_AT_CHECKOUT\");\n const status = raw.status ?? \"\";\n const ticketCount = Number(raw.ticketsQuantity);\n return {\n number: raw.orderNumber ?? \"\",\n status,\n statusLabel: ORDER_STATUS_COPY[status] ?? \"\",\n settled: status === \"FREE\" || status === \"PAID\",\n email: raw.email ?? \"\",\n createdLabel: formatOrderDate(raw.created),\n items: (invoice.items ?? []).map((i) => ({\n name: i.name ?? \"\",\n price: formatMoney(i.price),\n quantity: Number(i.quantity) || 0,\n total: formatMoney(i.total),\n })),\n subtotal: formatMoney(invoice.subTotal),\n couponDiscount: coupon ? formatMoney(coupon.amount) : \"\",\n paidPlanDiscount: paidPlan ? { amount: formatMoney(paidPlan.amount), ratePercent: percent(paidPlan.paidPlan?.percentDiscount?.rate) } : null,\n tax: tax?.amount ? { name: tax.name ?? \"Tax\", ratePercent: percent(tax.rate), amount: formatMoney(tax.amount) } : null,\n fee: fee?.amount ? { amount: formatMoney(fee.amount), ratePercent: percent(fee.rate) } : null,\n total: formatMoney(invoice.grandTotal ?? raw.totalPrice),\n ticketsPdfUrl: typeof raw.ticketsPdf === \"string\" ? raw.ticketsPdf : \"\",\n ticketCount: Number.isFinite(ticketCount) ? ticketCount : (raw.tickets ?? []).length,\n ticketsReady: orderReady(raw),\n };\n}\n",
21 "js/wix/orders.js": "// Declarations of orders.ts: what this file exports, their arguments and what they return.\n// import type { OrderSummary } from \"./events-types.js\";\n// export declare function fetchOrder(eventId: string, orderNumber: string): Promise<OrderSummary | null>;\n// The ticket order behind the confirmation page over REST — the twin of app/wix/events/orders.ts.\n// Same export, same DTO; the mapper from orders-core (the SAME file the SDK transport uses,\n// deployed flat next to this one). Runs on the visitor token — the hosted checkout created the\n// order on this visitor's session. Not guaranteed for every site's visitor scope: a 403 throws\n// and the confirmation surface degrades to a confirmation without order details.\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/events/registration/ticketing/orders/get-order.md\nimport { WixApiError, wixRequest } from \"./client.js\";\nimport { ORDER_FIELDSETS, toOrder } from \"./orders-core.js\";\n/**\n * One order by the pair the thank-you URL carries. Null on 404; throws on anything else.\n * GET /events/v1/events/{eventId}/orders/{orderNumber}?fieldset=TICKETS&fieldset=DETAILS&fieldset=INVOICE → { order }\n */\nexport async function fetchOrder(eventId, orderNumber) {\n try {\n const res = await wixRequest(`/events/v1/events/${encodeURIComponent(eventId)}/orders/${encodeURIComponent(orderNumber)}`, {\n method: \"GET\",\n query: { fieldset: ORDER_FIELDSETS },\n });\n const raw = res?.order;\n return raw?.orderNumber ? toOrder(raw) : null;\n }\n catch (e) {\n if (e instanceof WixApiError && e.status === 404)\n return null;\n throw e;\n }\n}\n",
22 "js/wix/registration-core.js": "// Declarations of registration-core.ts: what this file exports, their arguments and what they return.\n// import type { EventDetail, RegistrationResult, RsvpForm, RsvpFormInput, SelectionTotals, TaxSettings, TicketSelection, TicketTier } from \"./events-types.js\";\n// import { type Raw } from \"./events-core.js\";\n// export declare const WIX_FEE_RATE = 2.5;\n// export type TierEvent = Pick<EventDetail, \"id\" | \"timeZoneId\" | \"taxSettings\">;\n// export declare function availableTiersQuery(eventId: string): Raw;\n// export declare function formatTierPrice(price: Raw | undefined): string;\n// export declare function tierTax(tax: TaxSettings, price: number): {\n// taxableValue: number;\n// taxValue: number;\n// };\n// export declare function tierFee(tax: TaxSettings | null, price: number, guestPricing: boolean): number;\n// export declare function priceNotes(price: number, currency: string, tax: TaxSettings | null, feeType: TicketTier[\"feeType\"], { free, guestPricing }: {\n// free: boolean;\n// guestPricing: boolean;\n// }): string[];\n// export declare function toTier(raw: Raw, event: TierEvent): TicketTier;\n// export declare function toTiers(definitions: Raw[] | undefined, event: TierEvent): TicketTier[];\n// export declare function selectionKey(tierId: string, optionId?: string): string;\n// export declare function clampQuantity(tier: TicketTier, quantity: number): number;\n// export declare function guestPriceValid(tier: TicketTier, value: string | undefined): boolean;\n// export declare function ticketCount(selections: TicketSelection[]): number;\n// export declare function selectionTotals(tiers: TicketTier[], selections: TicketSelection[], tax: TaxSettings | null): SelectionTotals | null;\n// export declare function reservationTickets(eventId: string, selections: TicketSelection[], tiers?: TicketTier[]): Raw[];\n// export declare function errorMessage(e: unknown): string;\n// export declare function reservationError(e: unknown): Error;\n// export declare function reservationId(reservation: Raw | null | undefined): string;\n// export interface CheckoutPaths {\n// confirmation?: string;\n// event?: string;\n// }\n// export declare function checkoutCallbacks(origin: string, slug: string, paths?: CheckoutPaths): Raw;\n// export declare function redirectResult(session: Raw | null | undefined): RegistrationResult;\n// export type RsvpValues = Record<string, string | string[]>;\n// export declare const DEFAULT_RSVP_FORM: RsvpForm;\n// export declare function rsvpFormOf(event: Pick<EventDetail, \"rsvpForm\">): RsvpForm;\n// export declare function builtInInputs(form: RsvpForm): {\n// firstName: string;\n// lastName: string;\n// email: string;\n// };\n// export declare function canSubmitRsvp(form: RsvpForm, values: RsvpValues, guestCount: number, attending?: boolean): boolean;\n// export declare function inputValueOf(input: RsvpFormInput, values: RsvpValues): Raw | null;\n// export declare function rsvpBody(eventId: string, form: RsvpForm, values: RsvpValues, status: \"YES\" | \"NO\", guests?: {\n// guestCount: number;\n// guestNames: string[];\n// }): Raw;\n// export declare function rsvpResult(rsvp: Raw | null | undefined, sent: \"YES\" | \"NO\"): RegistrationResult;\n// export type CheckoutEvent = Pick<EventDetail, \"id\" | \"slug\">;\nimport { formatAmount, formatInZone, formatMoney } from \"./events-core.js\";\nconst tierId = (raw) => raw?._id ?? raw?.id ?? \"\";\n/** Wix's ticket service fee, added at checkout when a tier's `feeType` is FEE_ADDED_AT_CHECKOUT. */\nexport const WIX_FEE_RATE = 2.5;\nconst round2 = (n) => Number(n.toFixed(2));\n/**\n * Query Available Ticket Definitions (V3) body: this event's tiers in the organizer's order. The\n * endpoint is visitor-callable and already excludes hidden tiers; no paging needed (default 100).\n */\nexport function availableTiersQuery(eventId) {\n return { filter: { eventId }, sort: [{ fieldName: \"sortIndex\", order: \"ASC\" }] };\n}\n/** Formatted price of a Money (`value` first; the deprecated `amount` as the v1 fallback). */\nexport function formatTierPrice(price) {\n return formatMoney(price);\n}\n/**\n * Tax on one ticket at `price` (Wix's getTicketDefinitionTax): INCLUDED_IN_PRICE backs the tax out\n * of the price; ADDED_AT_CHECKOUT adds `rate%` on top.\n */\nexport function tierTax(tax, price) {\n if (tax.includedInPrice) {\n const taxableValue = round2((price * 100) / (100 + tax.ratePercent));\n return { taxableValue, taxValue: round2(price - taxableValue) };\n }\n return { taxableValue: price, taxValue: round2(price * (tax.ratePercent / 100)) };\n}\n/** The Wix fee on one ticket (getTicketDefinitionFee): 2.5% of the price plus any tax added at checkout. */\nexport function tierFee(tax, price, guestPricing) {\n const addedTax = tax && !tax.includedInPrice && (!guestPricing || tax.appliedToDonations) ? tierTax(tax, price).taxValue : 0;\n return round2((price + addedTax) * (WIX_FEE_RATE / 100));\n}\n/**\n * The lines under a price that say what checkout adds (Wix's TicketDefinition Tax/Fee): tax is\n * hidden for free tiers and for a guest price unless `appliedToDonations`; the fee only when the\n * organizer passes it on (FEE_ADDED_AT_CHECKOUT) and the tier isn't free.\n */\nexport function priceNotes(price, currency, tax, feeType, { free, guestPricing }) {\n const notes = [];\n if (free)\n return notes;\n if (tax && (!guestPricing || tax.appliedToDonations)) {\n const { taxValue } = tierTax(tax, price);\n notes.push(tax.includedInPrice ? `${tax.name} included` : `+${formatAmount(taxValue, currency)} ${tax.name}`);\n }\n if (feeType === \"FEE_ADDED_AT_CHECKOUT\")\n notes.push(`+${formatAmount(tierFee(tax, price, guestPricing), currency)} ticket service fee`);\n return notes;\n}\nconst FEE_TYPES = new Set([\"FEE_INCLUDED\", \"FEE_ADDED_AT_CHECKOUT\", \"NO_FEE\"]);\nconst SALE_STATUSES = new Set([\"SALE_SCHEDULED\", \"SALE_STARTED\", \"SALE_ENDED\"]);\n/**\n * One raw ticket definition (V3 `pricingMethod`, or the v1 `price` shape) → tier DTO. Sold out is\n * `limitPerCheckout === 0` (Wix lowers the per-checkout limit to the unsold count); a missing field\n * means the API didn't say, never 0.\n */\nexport function toTier(raw, event) {\n const pm = raw.pricingMethod ?? {};\n const fixed = pm.fixedPrice ?? raw.price;\n const guest = pm.guestPrice;\n const optionDetails = pm.pricingOptions?.optionDetails ?? [];\n const currency = fixed?.currency ?? guest?.currency ?? optionDetails[0]?.price?.currency ?? \"\";\n const tax = event.taxSettings;\n const feeType = (FEE_TYPES.has(raw.feeType) ? raw.feeType : \"\");\n const free = pm.free === true || raw.free === true || (fixed && Number(fixed.value ?? fixed.amount ?? 0) === 0 && !guest && !optionDetails.length);\n const pricingType = free ? \"FREE\" : guest ? \"GUEST\" : optionDetails.length ? \"OPTIONS\" : \"FIXED\";\n const limit = Number(raw.limitPerCheckout);\n const limitPerCheckout = Number.isInteger(limit) && limit >= 0 ? limit : 20;\n const saleStatus = (SALE_STATUSES.has(raw.saleStatus) ? raw.saleStatus : \"SALE_STARTED\");\n const options = optionDetails\n .filter((o) => o.optionId)\n .map((o) => ({\n id: String(o.optionId),\n name: o.name ?? \"\",\n price: formatMoney(o.price),\n value: String(o.price?.value ?? o.price?.amount ?? \"0\"),\n notes: priceNotes(Number(o.price?.value ?? o.price?.amount ?? 0), currency, tax, feeType, { free: false, guestPricing: false }),\n }));\n const optionValues = optionDetails.map((o) => Number(o.price?.value ?? o.price?.amount ?? 0)).filter((n) => Number.isFinite(n));\n const fixedValue = Number(fixed?.value ?? fixed?.amount ?? 0);\n const price = pricingType === \"FREE\"\n ? \"Free\"\n : pricingType === \"FIXED\"\n ? formatMoney(fixed)\n : pricingType === \"OPTIONS\" && optionValues.length\n ? `${formatAmount(Math.min(...optionValues), currency)} – ${formatAmount(Math.max(...optionValues), currency)}`\n : \"\";\n // Sale dates read in the EVENT's zone: \"goes on sale\" only while scheduled; \"ends\" once the sale started (Wix).\n const salePeriod = raw.salePeriod;\n return {\n id: tierId(raw),\n name: raw.name ?? \"\",\n description: raw.description ?? \"\",\n pricingType,\n price,\n priceValue: pricingType === \"FIXED\" ? String(fixed?.value ?? fixed?.amount ?? \"0\") : \"0\",\n free: pricingType === \"FREE\",\n minPrice: pricingType === \"GUEST\" ? formatMoney(guest) : \"\",\n minPriceValue: pricingType === \"GUEST\" ? String(guest?.value ?? guest?.amount ?? \"0\") : \"\",\n currency,\n options: pricingType === \"OPTIONS\" ? options : [],\n notes: pricingType === \"OPTIONS\" ? [] : priceNotes(fixedValue, currency, tax, feeType, { free: pricingType === \"FREE\", guestPricing: pricingType === \"GUEST\" }),\n limitPerCheckout,\n soldOut: limitPerCheckout === 0,\n saleStatus,\n available: limitPerCheckout > 0 && saleStatus === \"SALE_STARTED\",\n saleStartsLabel: saleStatus === \"SALE_SCHEDULED\" ? formatInZone(salePeriod?.startDate, event.timeZoneId) : \"\",\n saleEndsLabel: saleStatus !== \"SALE_SCHEDULED\" && salePeriod?.endDate ? formatInZone(salePeriod.endDate, event.timeZoneId) : \"\",\n feeType,\n };\n}\n/**\n * Tiers in display order (V3 `sortIndex`, v1 `orderIndex`), skipping any without an id and the\n * ones the organizer hides while not on sale (`salePeriod.displayNotOnSale === false`; v1 `hideNotOnSale`).\n */\nexport function toTiers(definitions, event) {\n return (definitions ?? [])\n .filter((d) => d.hidden !== true)\n .filter((d) => {\n const onSale = (d.saleStatus ?? \"SALE_STARTED\") === \"SALE_STARTED\";\n const hideWhenNotOnSale = d.salePeriod?.displayNotOnSale === false || d.hideNotOnSale === true;\n return onSale || !hideWhenNotOnSale;\n })\n .slice()\n .sort((a, b) => (a.sortIndex ?? a.orderIndex ?? 0) - (b.sortIndex ?? b.orderIndex ?? 0))\n .map((d) => toTier(d, event))\n .filter((t) => t.id);\n}\n/** The store keys a selection by tier, or tier + pricing option. */\nexport function selectionKey(tierId, optionId) {\n return optionId ? `${tierId}:${optionId}` : tierId;\n}\n/** Wix's clamp: 0..limitPerCheckout, where a limit of 0 (sold out) allows nothing — never a default of 20. */\nexport function clampQuantity(tier, quantity) {\n return Math.max(0, Math.min(Math.trunc(quantity) || 0, tier.limitPerCheckout || 0));\n}\n/** A guest-priced tier needs a decimal at or above its minimum before it can be reserved. */\nexport function guestPriceValid(tier, value) {\n if (tier.pricingType !== \"GUEST\")\n return true;\n const n = Number(value);\n return value != null && value.trim() !== \"\" && Number.isFinite(n) && n >= Number(tier.minPriceValue || 0);\n}\nexport function ticketCount(selections) {\n return selections.reduce((sum, s) => sum + (s.quantity > 0 ? s.quantity : 0), 0);\n}\n/**\n * The picker's running totals — Wix's getTicketReservationTotals verbatim, returned formatted:\n * subtotal = Σ price × qty (minus tax when INCLUDED_IN_PRICE), tax per the settings (skipped for\n * free tiers and for donations unless appliedToDonations), fee per tier with FEE_ADDED_AT_CHECKOUT,\n * total = subtotal + added tax + fee. Null with no selection. The hosted checkout is authoritative.\n */\nexport function selectionTotals(tiers, selections, tax) {\n const picked = selections.filter((s) => s.quantity > 0);\n if (!picked.length)\n return null;\n const currency = tiers.find((t) => t.currency)?.currency ?? \"\";\n let subtotal = 0, taxTotal = 0, fee = 0, total = 0;\n for (const s of picked) {\n const tier = tiers.find((t) => t.id === s.tierId);\n if (!tier)\n continue;\n const guestPricing = tier.pricingType === \"GUEST\";\n const price = guestPricing\n ? Number(s.guestPrice || \"0\")\n : s.optionId\n ? Number(tier.options.find((o) => o.id === s.optionId)?.value ?? 0)\n : Number(tier.priceValue || 0);\n const unit = Number.isFinite(price) ? price : 0;\n subtotal = round2(subtotal + unit * s.quantity);\n total = round2(total + unit * s.quantity);\n if (tax && !tier.free && (!guestPricing || tax.appliedToDonations))\n taxTotal = round2(taxTotal + tierTax(tax, unit).taxValue * s.quantity);\n if (tier.feeType === \"FEE_ADDED_AT_CHECKOUT\" && !tier.free)\n fee = round2(fee + tierFee(tax, unit, guestPricing) * s.quantity);\n }\n if (tax && !tax.includedInPrice)\n total = round2(total + taxTotal);\n else if (tax && tax.includedInPrice)\n subtotal = round2(subtotal - taxTotal);\n total = round2(total + fee);\n return {\n subtotal: formatAmount(subtotal, currency),\n tax: formatAmount(taxTotal, currency),\n fee: formatAmount(fee, currency),\n total: formatAmount(total, currency),\n };\n}\n/**\n * The reservation's line items (Create Ticket Reservation): one per selection with `eventId`,\n * `ticketDefinitionId`, `quantity` (1..50) and, where the tier needs it, `ticketInfo` — the\n * `pricingOptionId` of an OPTIONS tier or the `guestPrice` of a GUEST tier. Throws when nothing is\n * picked or a guest price is missing/below the minimum.\n */\nexport function reservationTickets(eventId, selections, tiers = []) {\n const tickets = selections\n .filter((s) => s.quantity > 0)\n .map((s) => {\n const tier = tiers.find((t) => t.id === s.tierId);\n if (tier && !guestPriceValid(tier, s.guestPrice)) {\n throw new Error(tier.minPrice ? `Enter an amount of at least ${tier.minPrice} for ${tier.name}.` : `Enter an amount for ${tier.name}.`);\n }\n const ticketInfo = {};\n if (s.optionId)\n ticketInfo.pricingOptionId = s.optionId;\n if (tier?.pricingType === \"GUEST\" && s.guestPrice)\n ticketInfo.guestPrice = String(Number(s.guestPrice).toFixed(2));\n return { eventId, ticketDefinitionId: s.tierId, quantity: s.quantity, ...(Object.keys(ticketInfo).length ? { ticketInfo } : {}) };\n });\n if (!tickets.length)\n throw new Error(\"Pick at least one ticket first.\");\n return tickets;\n}\n/** Visitor copy for the application error codes registration can answer with. */\nconst ERROR_COPY = {\n GUEST_LIMIT_EXCEEDED: \"This event is full.\",\n MEMBER_ALREADY_REGISTERED: \"You're already registered for this event.\",\n MEMBER_EMAIL_ALREADY_REGISTERED: \"You're already registered for this event.\",\n INVALID_FORM_RESPONSE: \"Please fill in the required fields.\",\n UNEXPECTED_RSVP_STATUS: \"Registration isn't open for this event.\",\n EVENT_NOT_FOUND: \"This event is no longer available.\",\n};\n/**\n * One visitor-facing message out of any failure. SDK errors carry a JSON envelope as their message\n * (`{ message, details: { applicationError: { code } } }`) plus `details` on the error object; the\n * REST client sets `code`/`details` the same way. Known codes get copy; the payment gate (403\n * NO_PAYMENT_METHOD_CONFIGURED until the site has a premium plan AND a payment method — not a\n * permissions bug, never elevate) gets its owner-facing line; anything else is Wix's message.\n */\nexport function errorMessage(e) {\n const err = e;\n let message = e instanceof Error ? e.message : typeof e === \"string\" ? e : \"\";\n let code = err?.details?.applicationError?.code ?? err?.code;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") {\n message = typeof parsed.message === \"string\" ? parsed.message : message;\n code = code ?? parsed.details?.applicationError?.code;\n }\n }\n catch {\n /* a plain message */\n }\n if (code && ERROR_COPY[code])\n return ERROR_COPY[code];\n if (/payment method|not configured|premium/i.test(message) || code === \"NO_PAYMENT_METHOD_CONFIGURED\") {\n return \"Ticket sales aren't switched on yet — the organizer needs to connect a payment method in the dashboard.\";\n }\n return message || \"Something went wrong — please try again.\";\n}\n/** The reservation failure as an Error with visitor copy (see errorMessage). */\nexport function reservationError(e) {\n return new Error(errorMessage(e));\n}\n/** The reservation id out of either transport's response; throws when the hold didn't happen. */\nexport function reservationId(reservation) {\n const id = tierId(reservation);\n if (!id)\n throw new Error(\"Those tickets couldn't be reserved — they may have just sold out.\");\n return id;\n}\n/**\n * The redirect session's callbacks. `origin` is the published https host (window.location.origin\n * in a browser) — an http or server-derived origin isn't on the redirect allowlist and 403s the\n * return; {} when unknown (SSR).\n */\nexport function checkoutCallbacks(origin, slug, paths = {}) {\n if (!origin)\n return {};\n return {\n thankYouPageUrl: `${origin}${paths.confirmation ?? \"/event-confirmation\"}`,\n postFlowUrl: `${origin}${paths.event ?? `/events/${encodeURIComponent(slug)}`}`,\n };\n}\n/** The redirect result; throws when Wix returned no URL. */\nexport function redirectResult(session) {\n const url = session?.redirectSession?.fullUrl;\n if (!url)\n throw new Error(\"Checkout couldn't start — please try again.\");\n return { kind: \"redirect\", url };\n}\n/** What every RSVP form has (the system controls Wix creates) — used when the FORM fieldset is missing. */\nexport const DEFAULT_RSVP_FORM = {\n controls: [\n {\n id: \"name\",\n type: \"NAME\",\n system: true,\n inputs: [\n { name: \"firstName\", label: \"First name\", mandatory: true, type: \"TEXT\", options: [], maxLength: 0 },\n { name: \"lastName\", label: \"Last name\", mandatory: true, type: \"TEXT\", options: [], maxLength: 0 },\n ],\n },\n { id: \"email\", type: \"INPUT\", system: true, inputs: [{ name: \"email\", label: \"Email\", mandatory: true, type: \"TEXT\", options: [], maxLength: 0 }] },\n ],\n guestControl: null,\n};\n/** The form to render: the organizer's, else the built-in one. */\nexport function rsvpFormOf(event) {\n return event.rsvpForm ?? DEFAULT_RSVP_FORM;\n}\n/**\n * Which inputs are the built-in guest identity: the system NAME control's two inputs (first, last)\n * and the system email input. Create RSVP requires them at the top level of the body.\n */\nexport function builtInInputs(form) {\n const name = form.controls.find((c) => c.type === \"NAME\") ?? form.controls.find((c) => c.system && c.inputs.length >= 2);\n const emailControl = form.controls.find((c) => c.system && c.type !== \"NAME\" && c.inputs.some((i) => /mail/i.test(i.name) || /mail/i.test(i.label))) ??\n form.controls.find((c) => c.system && c.type !== \"NAME\");\n const emailInput = emailControl?.inputs.find((i) => /mail/i.test(i.name) || /mail/i.test(i.label)) ?? emailControl?.inputs[0];\n return { firstName: name?.inputs[0]?.name ?? \"firstName\", lastName: name?.inputs[1]?.name ?? \"lastName\", email: emailInput?.name ?? \"email\" };\n}\nconst filled = (v) => (Array.isArray(v) ? v.some((x) => x.trim()) : typeof v === \"string\" && v.trim().length > 0);\n/**\n * Whether the answers can be submitted: the built-in identity always; every mandatory input when\n * attending (Create RSVP enforces mandatory values for `YES` only); a guest count within the\n * control's range.\n */\nexport function canSubmitRsvp(form, values, guestCount, attending = true) {\n const ids = builtInInputs(form);\n if (![ids.firstName, ids.lastName, ids.email].every((n) => filled(values[n])))\n return false;\n if (attending) {\n for (const c of form.controls)\n for (const i of c.inputs)\n if (i.mandatory && !filled(values[i.name]))\n return false;\n }\n if (form.guestControl && (guestCount < 0 || guestCount > form.guestControl.maxGuests))\n return false;\n return true;\n}\n/** One form input's answer as Create RSVP wants it (`values` for TEXT_ARRAY inputs, `value` otherwise); null when unanswered. */\nexport function inputValueOf(input, values) {\n const v = values[input.name];\n if (!filled(v))\n return null;\n if (input.type === \"TEXT_ARRAY\")\n return { inputName: input.name, values: (Array.isArray(v) ? v : [v]).filter((x) => x.trim()) };\n return { inputName: input.name, value: Array.isArray(v) ? v.join(\", \") : v };\n}\n/**\n * The Create RSVP body: the built-in identity at the top level (required fields), the whole form\n * as `form.inputValues` (only defined input names, only answered ones), `additionalGuestDetails`\n * only when the form has a guest control. \"NO\" only when the event's rsvpResponseType is\n * \"YES_AND_NO\".\n */\nexport function rsvpBody(eventId, form, values, status, guests = { guestCount: 0, guestNames: [] }) {\n const ids = builtInInputs(form);\n const str = (v) => (Array.isArray(v) ? v.join(\" \") : (v ?? \"\")).trim();\n const inputValues = form.controls.flatMap((c) => c.inputs.map((i) => inputValueOf(i, values))).filter((x) => x !== null);\n const body = {\n eventId,\n status,\n firstName: str(values[ids.firstName]),\n lastName: str(values[ids.lastName]),\n email: str(values[ids.email]),\n form: { inputValues },\n };\n if (form.guestControl) {\n const guestCount = Math.max(0, Math.min(Math.trunc(guests.guestCount) || 0, form.guestControl.maxGuests));\n body.additionalGuestDetails = { guestCount, guestNames: guests.guestNames.slice(0, guestCount).map((n) => n.trim()).filter(Boolean) };\n }\n return body;\n}\n/** A full event with a waitlist answers status \"WAITLIST\" — tell the guest they're waitlisted, not confirmed. */\nexport function rsvpResult(rsvp, sent) {\n const status = rsvp?.status;\n return { kind: \"rsvpConfirmed\", status: status === \"YES\" || status === \"NO\" || status === \"WAITLIST\" ? status : sent };\n}\n",
23 "js/wix/registration-store.js": "// Declarations of registration-store.ts: what this file exports, their arguments and what they return.\n// import { type CheckoutPaths } from \"./registration.js\";\n// import { type RsvpValues } from \"./registration-core.js\";\n// import type { EventDetail, RegistrationResult, RsvpForm, SelectionTotals, TicketSelection, TicketTier } from \"./events-types.js\";\n// export type { RsvpValues };\n// export interface RegistrationStoreOptions {\n// paths?: CheckoutPaths;\n// }\n// export interface RegistrationState {\n// tiers: TicketTier[] | null;\n// quantities: Record<string, number>;\n// guestPrices: Record<string, string>;\n// selections: TicketSelection[];\n// ticketCount: number;\n// ticketLimitPerOrder: number;\n// totals: SelectionTotals | null;\n// canCheckout: boolean;\n// rsvpForm: RsvpForm;\n// rsvpValues: RsvpValues;\n// guestCount: number;\n// guestNames: string[];\n// canRsvp: boolean;\n// submitting: boolean;\n// confirmed: RegistrationResult | null;\n// error: string | null;\n// }\n// export interface RegistrationStore {\n// getState(): RegistrationState;\n// subscribe(listener: (state: RegistrationState) => void): () => void;\n// start(): void;\n// stop(): void;\n// setQuantity(tierId: string, quantity: number, optionId?: string): void;\n// setGuestPrice(tierId: string, value: string): void;\n// setRsvpValue(inputName: string, value: string | string[]): void;\n// setGuestCount(count: number): void;\n// setGuestName(index: number, name: string): void;\n// checkout(): Promise<RegistrationResult>;\n// rsvp(attending?: boolean): Promise<RegistrationResult>;\n// }\n// export declare function createRegistrationStore(event: EventDetail, { paths }?: RegistrationStoreOptions): RegistrationStore;\n// The registration flow for one event as a framework-free store — the logic behind\n// useEventRegistration, usable from React (the hook wraps it), from a static page's event view,\n// from Vue/Svelte, or as the specification for a port. Branched on event.registrationType:\n// TICKETING loads the tier picker (quantities per tier or per pricing option, a named price for\n// donation tiers, Wix's running totals) and checkout() reserves → redirects to Wix's hosted\n// checkout; RSVP renders the ORGANIZER'S form (event.rsvpForm) and submits it in place. All\n// correctness (the visitor-public tier read, the reservation payload, the redirect callbacks,\n// rsvpV2, the form body) lives in the data layer — this store orchestrates; the page owns how it\n// looks.\n//\n// One store per event surface: createRegistrationStore(event), not a singleton. A static site\n// passes `paths` so the hosted checkout returns to its files (`event-confirmation.html`).\nimport { fetchTicketTiers, startTicketCheckout, submitRsvp } from \"./registration.js\";\nimport { canSubmitRsvp, clampQuantity, errorMessage, guestPriceValid, rsvpFormOf, selectionKey, selectionTotals, ticketCount as countTickets, } from \"./registration-core.js\";\nexport function createRegistrationStore(event, { paths } = {}) {\n const ticketed = event.registrationType === \"TICKETING\";\n const rsvpForm = rsvpFormOf(event);\n let tiers = ticketed ? null : [];\n let quantities = {};\n let guestPrices = {};\n let rsvpValues = {};\n let guestCount = 0;\n let guestNames = [];\n let submitting = false;\n let confirmed = null;\n let error = null;\n let started = false;\n const listeners = new Set();\n let snapshot = null;\n const emit = () => {\n snapshot = null;\n for (const fn of listeners)\n fn();\n };\n function currentSelections() {\n return Object.entries(quantities)\n .filter(([, q]) => q > 0)\n .map(([key, quantity]) => {\n const [tierId, optionId] = key.split(\":\");\n const tier = (tiers ?? []).find((t) => t.id === tierId);\n return {\n tierId,\n quantity,\n ...(optionId ? { optionId } : {}),\n ...(tier?.pricingType === \"GUEST\" ? { guestPrice: guestPrices[tierId] ?? \"\" } : {}),\n };\n });\n }\n function getState() {\n if (snapshot)\n return snapshot;\n const selections = currentSelections();\n const ticketCount = countTickets(selections);\n const guestPricesValid = selections.every((s) => {\n const tier = (tiers ?? []).find((t) => t.id === s.tierId);\n return !tier || guestPriceValid(tier, s.guestPrice);\n });\n snapshot = {\n tiers,\n quantities,\n guestPrices,\n selections,\n ticketCount,\n ticketLimitPerOrder: event.ticketLimitPerOrder,\n totals: selectionTotals(tiers ?? [], selections, event.taxSettings),\n canCheckout: ticketCount > 0 && ticketCount <= event.ticketLimitPerOrder && guestPricesValid,\n rsvpForm,\n rsvpValues,\n guestCount,\n guestNames,\n canRsvp: canSubmitRsvp(rsvpForm, rsvpValues, guestCount, true),\n submitting,\n confirmed,\n error,\n };\n return snapshot;\n }\n async function run(op) {\n submitting = true;\n error = null;\n emit();\n try {\n const result = await op();\n if (result.kind === \"redirect\") {\n if (typeof window !== \"undefined\")\n window.location.href = result.url;\n }\n else {\n confirmed = result;\n }\n return result;\n }\n catch (e) {\n error = errorMessage(e);\n throw e;\n }\n finally {\n submitting = false;\n emit();\n }\n }\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"registration-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n if (!ticketed)\n return;\n fetchTicketTiers(event)\n .then((t) => {\n if (!started)\n return;\n tiers = t;\n emit();\n })\n .catch((e) => {\n if (!started)\n return;\n tiers = [];\n error = errorMessage(e);\n emit();\n });\n },\n stop() {\n started = false;\n },\n setQuantity(tierId, quantity, optionId) {\n const tier = (tiers ?? []).find((t) => t.id === tierId);\n if (!tier || !tier.available)\n return;\n if (optionId && !tier.options.some((o) => o.id === optionId))\n return;\n const key = selectionKey(tierId, optionId);\n const clamped = clampQuantity(tier, quantity);\n // The event caps one order: never let the total pass it (Wix's reservation would refuse with an opaque error).\n const others = Object.entries(quantities).reduce((sum, [k, q]) => (k === key ? sum : sum + q), 0);\n const capped = Math.max(0, Math.min(clamped, event.ticketLimitPerOrder - others));\n quantities = { ...quantities, [key]: capped };\n error = capped < clamped ? `Up to ${event.ticketLimitPerOrder} tickets per order.` : null; // a stale error never outlives a selection change\n emit();\n },\n setGuestPrice(tierId, value) {\n guestPrices = { ...guestPrices, [tierId]: value };\n error = null;\n emit();\n },\n setRsvpValue(inputName, value) {\n rsvpValues = { ...rsvpValues, [inputName]: value };\n emit();\n },\n setGuestCount(count) {\n const max = rsvpForm.guestControl?.maxGuests ?? 0;\n guestCount = Math.max(0, Math.min(Math.trunc(count) || 0, max));\n guestNames = guestNames.slice(0, guestCount);\n emit();\n },\n setGuestName(index, name) {\n if (index < 0 || index >= guestCount)\n return;\n const next = guestNames.slice();\n while (next.length < guestCount)\n next.push(\"\");\n next[index] = name;\n guestNames = next;\n emit();\n },\n checkout: () => run(() => startTicketCheckout(event, currentSelections(), paths, tiers ?? [])),\n rsvp: (attending = true) => run(() => submitRsvp(event, rsvpValues, attending ? \"YES\" : \"NO\", { guestCount, guestNames })),\n };\n}\n",
24 "js/wix/registration.js": "// Declarations of registration.ts: what this file exports, their arguments and what they return.\n// import { type CheckoutEvent, type CheckoutPaths, type RsvpValues, type TierEvent } from \"./registration-core.js\";\n// import type { EventDetail, RegistrationResult, TicketSelection, TicketTier } from \"./events-types.js\";\n// export { type CheckoutPaths, type RsvpValues, type TierEvent };\n// export declare function fetchTicketTiers(event: TierEvent): Promise<TicketTier[]>;\n// export declare function submitRsvp(event: Pick<EventDetail, \"id\" | \"rsvpForm\">, values: RsvpValues, status?: \"YES\" | \"NO\", guests?: {\n// guestCount: number;\n// guestNames: string[];\n// }): Promise<RegistrationResult>;\n// export declare function startTicketCheckout(event: CheckoutEvent, selections: TicketSelection[], paths?: CheckoutPaths, tiers?: TicketTier[], origin?: string): Promise<RegistrationResult>;\n// Ticket tiers, the reserve → hosted-checkout redirect, and RSVP over REST — the twin of\n// app/wix/events/registration.ts. Same exports, same DTOs; rules and mappers from\n// registration-core (the SAME file the SDK transport uses, deployed flat next to this one). The\n// request shapes are exact and rewriting them is how registrations break. Failures are loud.\n// All calls run with the visitor token — the reservation and the RSVP are the token's.\n// Query Available Ticket Definitions: wix.events.ticketdef.v3.TicketDefinitionManagement.QueryAvailableTicketDefinitions\n// (POST /events/v3/ticket-definitions/available/query, as the @wix/events SDK issues it; the V3 family's reference:)\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/ticket-definitions-v3/create-ticket-definition.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/events/registration/ticketing/ticket-reservations/create-ticket-reservation.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/events/registration/rsvp-v2/create-rsvp.md\n// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md\nimport { wixRequest } from \"./client.js\";\nimport { availableTiersQuery, checkoutCallbacks, redirectResult, reservationError, reservationId, reservationTickets, rsvpBody, rsvpFormOf, rsvpResult, toTiers, } from \"./registration-core.js\";\n/**\n * Ticket tiers for a TICKETING event, in the organizer's order — the VISITOR-public V3 read (hidden\n * tiers excluded server-side), never the ticket-definitions management query (403 for a visitor).\n * Pass the EventDetail: sale dates format in its zone, its tax settings price the notes.\n * POST /events/v3/ticket-definitions/available/query { query: { filter: { eventId }, sort: [{ fieldName: \"sortIndex\", order: \"ASC\" }] } } → { ticketDefinitions }\n */\nexport async function fetchTicketTiers(event) {\n const res = await wixRequest(\"/events/v3/ticket-definitions/available/query\", { body: { query: availableTiersQuery(event.id) } });\n return toTiers(res?.ticketDefinitions, event);\n}\n/**\n * RSVP to a free (RSVP-type) event — completes fully client-side: no reservation, no redirect, no\n * payment. `values` answer the ORGANIZER'S form (event.rsvpForm), keyed by input name; the body\n * carries the built-in identity at the top level, the answers as `form.inputValues`, and\n * `additionalGuestDetails` when the form has a guest control. \"NO\" only when the event's\n * rsvpResponseType is \"YES_AND_NO\". Throws on closed registration, a guest limit, a missing\n * mandatory answer (INVALID_FORM_RESPONSE), or an invalid email — surface the message. A full\n * event with a waitlist answers \"WAITLIST\".\n * POST /events/v2/rsvps { rsvp: { eventId, status, firstName, lastName, email, form: { inputValues: [{ inputName, value | values }] }, additionalGuestDetails?: { guestCount, guestNames } } } → { rsvp: { id, status } }\n */\nexport async function submitRsvp(event, values, status = \"YES\", guests = { guestCount: 0, guestNames: [] }) {\n const res = await wixRequest(\"/events/v2/rsvps\", { body: { rsvp: rsvpBody(event.id, rsvpFormOf(event), values, status, guests) } });\n return rsvpResult(res?.rsvp, status);\n}\n/**\n * Ticketed checkout, the exact sequence: reserve the selected tiers (a PENDING hold that\n * auto-expires), then mint the Wix-hosted checkout redirect and return its URL — the caller\n * navigates the FULL document to it; Wix collects guest details + payment and emails the tickets.\n * Selections carry the pricing option or the guest's price where the tier needs one (`ticketInfo`);\n * pass `tiers` so a missing guest price is refused before the request. `origin` must be the site's\n * real https origin as registered on the OAuth app's allowed domains (browser:\n * window.location.origin). Never hand-build the checkout URL. `paths` overrides the return routes —\n * a static site passes `{ confirmation: \"/event-confirmation.html\", event: \"/event.html?slug=…\" }`.\n * POST /events/v1/ticket-reservations { ticketReservation: { tickets: [{ eventId, ticketDefinitionId, quantity, ticketInfo?: { pricingOptionId?, guestPrice? } }] } } → { ticketReservation: { id, status: \"PENDING\", expirationDate } }\n * POST /headless/v1/redirect-session { eventsCheckout: { reservationId, eventSlug }, callbacks: { thankYouPageUrl, postFlowUrl } }\n */\nexport async function startTicketCheckout(event, selections, paths = {}, tiers = [], origin = typeof window !== \"undefined\" ? window.location.origin : \"\") {\n const tickets = reservationTickets(event.id, selections, tiers);\n let reservation;\n try {\n // Until the site has a premium plan AND a payment method this is a 403 NO_PAYMENT_METHOD_CONFIGURED — a business gate, not a bug.\n reservation = (await wixRequest(\"/events/v1/ticket-reservations\", { body: { ticketReservation: { tickets } } }))?.ticketReservation;\n }\n catch (e) {\n throw reservationError(e);\n }\n const session = await wixRequest(\"/headless/v1/redirect-session\", {\n body: { eventsCheckout: { reservationId: reservationId(reservation), eventSlug: event.slug }, callbacks: checkoutCallbacks(origin, event.slug, paths) },\n });\n return redirectResult(session);\n}\n"