15 "js/wix/menu-core.js": "// Declarations of menu-core.ts: what this file exports, their arguments and what they return.\n// import type { MenuData, MenuItem, MenuModifier, MenuModifierGroup, MenuSection, ModifierRuleType, OrderSelection, SelectionPrice, SelectionValidation, SiteMoney } from \"./restaurants-types.js\";\n// export type Raw = Record<string, any>;\n// export type ImgSrc = (value: any, width: number, height: number) => string;\n// export declare const rawId: (raw: Raw | string | undefined | null) => string;\n// export declare const byId: (arr: Raw[]) => Map<string, Raw>;\n// export declare function chunk<T>(arr: T[], size?: number): T[][];\n// export declare const nextCursor: (res: Raw | null | undefined) => string | null;\n// export declare function toSiteMoney(settingsRes: Raw | null | undefined): SiteMoney;\n// export declare function formatPrice(amount: string | number | null | undefined, money: SiteMoney, platformFormatted?: string | null): string;\n// export declare function collectVariantIds(items: Raw[]): string[];\n// export declare function collectGroupIds(items: Raw[]): string[];\n// export declare function collectModifierIds(groups: Raw[]): string[];\n// export declare function anyLabels(items: Raw[]): boolean;\n// export declare const isSingleSelectRule: (rule: {\n// required: boolean;\n// minSelections: number;\n// maxSelections: number | null;\n// }) => boolean;\n// export declare function ruleType(rule: {\n// required: boolean;\n// minSelections: number;\n// maxSelections: number | null;\n// }): ModifierRuleType;\n// export declare function ruleLabel(group: MenuModifierGroup): string;\n// export declare function requiredGroupOutOfStock(groups: MenuModifierGroup[]): boolean;\n// export declare function defaultVariantId(item: Pick<MenuItem, \"variants\">): string | null;\n// export declare function initialSelection(item: Pick<MenuItem, \"variants\" | \"modifierGroups\">): OrderSelection;\n// export declare function toggleModifier(item: Pick<MenuItem, \"modifierGroups\">, selection: OrderSelection, groupId: string, key: string): OrderSelection;\n// export declare function validateSelection(item: Pick<MenuItem, \"modifierGroups\">, selection: OrderSelection): SelectionValidation;\n// export declare function selectedModifiers(item: Pick<MenuItem, \"modifierGroups\">, selection: OrderSelection): {\n// group: MenuModifierGroup;\n// modifiers: MenuModifier[];\n// }[];\n// export declare function itemPrice(item: Pick<MenuItem, \"priceAmount\" | \"variants\" | \"modifierGroups\">, selection: OrderSelection, quantity: number, money: SiteMoney): SelectionPrice;\n// export declare const needsSelection: (item: Pick<MenuItem, \"variants\" | \"modifierGroups\" | \"acceptsSpecialRequests\">) => boolean;\n// export interface MenuLookups {\n// variantById: Map<string, Raw>;\n// groupById: Map<string, Raw>;\n// modifierById: Map<string, Raw>;\n// labelById: Map<string, Raw>;\n// }\n// export declare function toModifierGroup(g: Raw, lookups: MenuLookups, money: SiteMoney): MenuModifierGroup;\n// export declare function toItem(raw: Raw, lookups: MenuLookups, imgSrc: ImgSrc, money: SiteMoney): MenuItem;\n// export declare function toSection(raw: Raw, itemById: Map<string, MenuItem>, imgSrc: ImgSrc): MenuSection;\n// export declare function toMenu(raw: Raw, sectionById: Map<string, Raw>, itemById: Map<string, MenuItem>, imgSrc: ImgSrc, money: SiteMoney): MenuData;\n// export interface MenuTreeParts {\n// menus: Raw[];\n// sections: Raw[];\n// items: Raw[];\n// variants: Raw[];\n// groups: Raw[];\n// modifiers: Raw[];\n// labels: Raw[];\n// money: SiteMoney;\n// }\n// export declare function assembleMenus(parts: MenuTreeParts, imgSrc: ImgSrc): MenuData[];\n/** An entity's id, or a bare id string (item.labels may be string ids or { _id } refs). */\nexport const rawId = (raw) => typeof raw === \"string\" ? raw : (raw?._id ?? raw?.id ?? \"\");\n/** Index raw entities by id (either spelling). */\nexport const byId = (arr) => new Map(arr.map((e) => [rawId(e), e]));\nconst unique = (ids) => [...new Set(ids.filter(Boolean))];\n/** Split an id array for `?ids=…` reads — 100 per call keeps the query string well under the limit. */\nexport function chunk(arr, size = 100) {\n const out = [];\n for (let i = 0; i < arr.length; i += size)\n out.push(arr.slice(i, i + size));\n return out;\n}\n/** The next page's cursor of a list response (pagingMetadata.cursors.next), null on the last page. */\nexport const nextCursor = (res) => res?.pagingMetadata?.cursors?.next || null;\n// ---- money -------------------------------------------------------------------------------------------\n/**\n * The site's currency + locale from the eCommerce settings' BUSINESS_INFO (ecommerceSettings.\n * businessInfo.currency / .locale.language / .locale.country), the read Wix's own menus code makes.\n * Nothing is assumed: an unreadable response yields currency \"\" and every price formats to \"\".\n */\nexport function toSiteMoney(settingsRes) {\n const info = settingsRes?.ecommerceSettings?.businessInfo ?? {};\n const language = info.locale?.language ?? \"\";\n const country = info.locale?.country ?? \"\";\n return { currency: info.currency ?? \"\", locale: language ? (country ? `${language}-${country}` : language) : \"\" };\n}\n/**\n * Format a decimal amount in the site's currency and locale. Prefers a platform-formatted string\n * when the API sent one; \"\" when the amount is missing or the currency is unknown — never an\n * invented symbol, never a USD default.\n */\nexport function formatPrice(amount, money, platformFormatted) {\n if (platformFormatted)\n return platformFormatted;\n if (amount == null || amount === \"\" || !money.currency)\n return \"\";\n const n = Number(amount);\n if (!Number.isFinite(n))\n return \"\";\n try {\n return new Intl.NumberFormat(money.locale || undefined, { style: \"currency\", currency: money.currency }).format(n);\n }\n catch {\n try {\n return new Intl.NumberFormat(undefined, { style: \"currency\", currency: money.currency }).format(n);\n }\n catch {\n return `${n.toFixed(2)} ${money.currency}`;\n }\n }\n}\n// ---- id collection for the second and third reads ---------------------------------------------\n/** The variant ids the items reference — the second read's `variantIds`. */\nexport function collectVariantIds(items) {\n return unique(items.flatMap((i) => (i.priceVariants?.variants ?? []).map((v) => v.variantId)));\n}\n/** The modifier-group ids the items reference — the second read's `modifierGroupIds`. */\nexport function collectGroupIds(items) {\n return unique(items.flatMap((i) => (i.modifierGroups ?? []).map(rawId)));\n}\n/** The modifier ids the groups reference — the third read's `modifierIds`. */\nexport function collectModifierIds(groups) {\n return unique(groups.flatMap((g) => (g.modifiers ?? []).map(rawId)));\n}\n/** Whether any item carries a label — the labels list is read only then. */\nexport function anyLabels(items) {\n return items.some((i) => (i.labels ?? []).length > 0);\n}\n// ---- modifier rule engine (mirrors Wix's ordering utils) ----------------------------------------\n/** required && min 1 && max 1 — the one rule rendered as radios; a new pick replaces the old. */\nexport const isSingleSelectRule = (rule) => rule.required && rule.minSelections === 1 && rule.maxSelections === 1;\n/** The rule's shape, in Wix's precedence (NO_LIMIT → CHOOSE_ONE → CHOOSE_X → AT_LEAST_ONE → AT_LEAST_X → UP_TO_X → BETWEEN). */\nexport function ruleType(rule) {\n const { required, minSelections: min, maxSelections: max } = rule;\n if (!required && min < 1 && (max ? max < 1 : true))\n return \"NO_LIMIT\";\n if ((required && min === 1 && max === 1) || (!required && min === 0 && max === 1))\n return \"CHOOSE_ONE\";\n if (required && max && min === max)\n return \"CHOOSE_X\";\n if (required && min === 1 && max == null)\n return \"CHOOSE_AT_LEAST_ONE\";\n if (required && min > 1 && max == null)\n return \"CHOOSE_AT_LEAST_X\";\n if (!required && min === 0 && max && max > 1)\n return \"CHOOSE_UP_TO_X\";\n if (required && max && min > 0 && max > min)\n return \"CHOOSE_BETWEEN_X_AND_Y\";\n return \"NO_LIMIT\";\n}\n/** Human wording of a group's rule for the sheet (\"Choose 1\", \"Choose up to 3\", …). */\nexport function ruleLabel(group) {\n const { minSelections: min, maxSelections: max } = group;\n switch (group.rule) {\n case \"CHOOSE_ONE\": return group.required ? \"Choose 1\" : \"Choose up to 1\";\n case \"CHOOSE_X\": return `Choose ${min}`;\n case \"CHOOSE_AT_LEAST_ONE\": return \"Choose at least 1\";\n case \"CHOOSE_AT_LEAST_X\": return `Choose at least ${min}`;\n case \"CHOOSE_UP_TO_X\": return `Choose up to ${max}`;\n case \"CHOOSE_BETWEEN_X_AND_Y\": return `Choose ${min} to ${max}`;\n default: return \"Optional\";\n }\n}\n/** A required group whose in-stock modifiers can't reach minSelections makes the whole item unorderable. */\nexport function requiredGroupOutOfStock(groups) {\n return groups.some((g) => g.required && g.modifiers.filter((m) => m.inStock).length < Math.max(g.minSelections, 0));\n}\n/** The cheapest variant — the default the sheet opens on; null for a flat-priced item. */\nexport function defaultVariantId(item) {\n if (!item.variants.length)\n return null;\n return item.variants.reduce((cheapest, v) => (Number(v.priceAmount) < Number(cheapest.priceAmount) ? v : cheapest)).variantId;\n}\n/** The selection a sheet opens with: preSelected && inStock modifiers (single-select takes the first), the cheapest variant, no note. */\nexport function initialSelection(item) {\n const modifiers = {};\n for (const g of item.modifierGroups) {\n const pre = g.modifiers.filter((m) => m.preSelected && m.inStock).map((m) => m.key);\n modifiers[g.id] = g.singleSelect ? pre.slice(0, 1) : pre;\n }\n return { variantId: defaultVariantId(item), modifiers, specialRequest: \"\" };\n}\n/** A new selection with `key` toggled in `groupId` — single-select replaces, multi-select toggles. Pure. */\nexport function toggleModifier(item, selection, groupId, key) {\n const group = item.modifierGroups.find((g) => g.id === groupId);\n const current = selection.modifiers[groupId] ?? [];\n const next = group?.singleSelect ? [key] : current.includes(key) ? current.filter((k) => k !== key) : [...current, key];\n return { ...selection, modifiers: { ...selection.modifiers, [groupId]: next } };\n}\n/** Wix's checks per group: required → at least one; count >= min; count <= max when a max is set. */\nexport function validateSelection(item, selection) {\n const errors = {};\n for (const g of item.modifierGroups) {\n const count = (selection.modifiers[g.id] ?? []).length;\n if (g.required && count === 0)\n errors[g.id] = g.minSelections > 1 ? `Choose at least ${g.minSelections}` : \"Choose one\";\n else if (count < g.minSelections)\n errors[g.id] = `Choose at least ${g.minSelections}`;\n else if (g.maxSelections && count > g.maxSelections)\n errors[g.id] = `Choose up to ${g.maxSelections}`;\n }\n return { ok: Object.keys(errors).length === 0, errors };\n}\n/** The modifiers a selection names, resolved by KEY within their group (duplicate ids stay distinct). */\nexport function selectedModifiers(item, selection) {\n return item.modifierGroups\n .map((group) => {\n const keys = selection.modifiers[group.id] ?? [];\n return { group, modifiers: keys.map((k) => group.modifiers.find((m) => m.key === k)).filter((m) => !!m) };\n })\n .filter((g) => g.modifiers.length > 0);\n}\n/** (base + variant + modifier charges) × quantity, formatted in the menu's money. */\nexport function itemPrice(item, selection, quantity, money) {\n const base = Number(item.priceAmount ?? 0) || 0;\n const variant = Number(item.variants.find((v) => v.variantId === selection.variantId)?.priceAmount ?? 0) || 0;\n const modifiers = selectedModifiers(item, selection).reduce((sum, g) => sum + g.modifiers.reduce((s, m) => s + (Number(m.additionalChargeAmount) || 0), 0), 0);\n const amount = (base + variant + modifiers) * Math.max(quantity, 0);\n return { amount, formatted: formatPrice(amount, money) };\n}\n/** True when the dish needs a sheet before adding (a variant, a modifier group, or a note to collect). */\nexport const needsSelection = (item) => item.variants.length > 0 || item.modifierGroups.length > 0 || item.acceptsSpecialRequests;\nexport function toModifierGroup(g, lookups, money) {\n const rule = {\n required: g.rule?.required === true,\n minSelections: g.rule?.minSelections ?? 0,\n maxSelections: g.rule?.maxSelections ?? null,\n };\n return {\n id: rawId(g),\n name: g.name ?? \"\",\n ...rule,\n rule: ruleType(rule),\n singleSelect: isSingleSelectRule(rule),\n // The group-level ref carries preSelected + the up-charge; the modifier entity carries name + inStock.\n modifiers: (g.modifiers ?? []).map((m, index) => {\n const id = rawId(m);\n const entity = lookups.modifierById.get(id);\n const charge = m.additionalChargeInfo?.additionalCharge ?? \"0\";\n return {\n id,\n key: `${id}~${index}`,\n name: entity?.name ?? \"\",\n preSelected: m.preSelected === true,\n additionalCharge: Number(charge) > 0 ? formatPrice(charge, money, m.additionalChargeInfo?.formattedAdditionalCharge) : \"\",\n additionalChargeAmount: String(charge),\n inStock: entity?.inStock !== false,\n };\n }),\n };\n}\nexport function toItem(raw, lookups, imgSrc, money) {\n const variants = (raw.priceVariants?.variants ?? [])\n .map((v) => ({\n variantId: v.variantId ?? \"\",\n name: lookups.variantById.get(v.variantId)?.name ?? \"\",\n price: formatPrice(v.priceInfo?.price, money, v.priceInfo?.formattedPrice),\n priceAmount: v.priceInfo?.price ?? \"0\",\n }))\n .filter((v) => v.variantId);\n const modifierGroups = (raw.modifierGroups ?? [])\n .map((ref) => lookups.groupById.get(rawId(ref)))\n .filter((g) => !!g)\n .map((g) => toModifierGroup(g, lookups, money));\n const labels = (raw.labels ?? [])\n .map((ref) => lookups.labelById.get(rawId(ref)))\n .filter((l) => !!l)\n .map((l) => ({ id: rawId(l), name: l.name ?? \"\", iconUrl: imgSrc(l.icon, 48, 48) }));\n const priceAmount = raw.priceInfo?.price ?? null;\n const price = priceAmount === null ? null : formatPrice(priceAmount, money, raw.priceInfo?.formattedPrice);\n const marketPrice = priceAmount === null && variants.length === 0;\n const inStock = raw.orderSettings?.inStock !== false;\n const soldOut = !inStock || requiredGroupOutOfStock(modifierGroups);\n const imageUrl = imgSrc(raw.image, 800, 800);\n return {\n id: rawId(raw),\n name: raw.name ?? \"\",\n description: raw.description ?? \"\",\n price,\n priceAmount,\n marketPrice,\n variants,\n imageUrl,\n gallery: [imageUrl, ...(raw.additionalImages ?? []).map((m) => imgSrc(m, 800, 800))].filter(Boolean),\n labels,\n modifierGroups,\n inStock,\n soldOut,\n orderable: !marketPrice && !soldOut,\n acceptsSpecialRequests: raw.orderSettings?.acceptSpecialRequests === true,\n featured: raw.featured === true,\n };\n}\nexport function toSection(raw, itemById, imgSrc) {\n return {\n id: rawId(raw),\n name: raw.name ?? \"\",\n description: raw.description ?? \"\",\n imageUrl: imgSrc(raw.image, 800, 800),\n items: (raw.itemIds ?? []).map((iid) => itemById.get(iid)).filter((i) => !!i),\n };\n}\nexport function toMenu(raw, sectionById, itemById, imgSrc, money) {\n return {\n id: rawId(raw),\n name: raw.name ?? \"\",\n description: raw.description ?? \"\",\n slug: raw.urlQueryParam ?? \"\",\n sections: (raw.sectionIds ?? [])\n .map((sid) => sectionById.get(sid))\n .filter((s) => !!s)\n .map((s) => toSection(s, itemById, imgSrc)),\n money,\n };\n}\n/** The display-ordered tree: visible menus → their sections → their items, each item enriched. [] when no menus. */\nexport function assembleMenus(parts, imgSrc) {\n if (!parts.menus.length)\n return [];\n const lookups = {\n variantById: byId(parts.variants),\n groupById: byId(parts.groups),\n modifierById: byId(parts.modifiers),\n labelById: byId(parts.labels),\n };\n const sectionById = byId(parts.sections);\n const itemById = new Map(parts.items.map((i) => [rawId(i), toItem(i, lookups, imgSrc, parts.money)]));\n return parts.menus.map((m) => toMenu(m, sectionById, itemById, imgSrc, parts.money));\n}\n",
16 "js/wix/menu-store.js": "// Declarations of menu-store.ts: what this file exports, their arguments and what they return.\n// import type { MenuData } from \"./restaurants-types.js\";\n// export interface MenuStoreOptions {\n// initialMenus?: MenuData[];\n// }\n// export interface MenuState {\n// menus: MenuData[] | null;\n// activeMenuId: string | null;\n// activeMenu: MenuData | null;\n// error: string | null;\n// }\n// export interface MenuStore {\n// getState(): MenuState;\n// subscribe(listener: (state: MenuState) => void): () => void;\n// start(): void;\n// stop(): void;\n// setActiveMenuId(id: string): void;\n// }\n// export declare function createMenuStore({ initialMenus }?: MenuStoreOptions): MenuStore;\n// Menu browsing as a framework-free store — the logic behind useMenus, usable from React (useMenus\n// wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and render), or\n// as the specification for a port. Same shape as order-store.ts: state, actions, subscribe/getState,\n// emit after every change. The whole tree is fetched once; menu switching is local.\n//\n// SSR-friendly: seed with `initialMenus` (Astro frontmatter) and no fetch happens; a SPA passes\n// nothing and `start()` fetches. One store per mounted menu surface: createMenuStore(), not a\n// singleton (the order cart is the singleton — one per visitor).\nimport { fetchMenus } from \"./menu.js\";\nexport function createMenuStore({ initialMenus } = {}) {\n let menus = initialMenus ?? null;\n let selectedId = initialMenus?.[0]?.id ?? 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 getState() {\n if (snapshot)\n return snapshot;\n const activeMenu = menus ? (menus.find((m) => m.id === selectedId) ?? menus[0] ?? null) : null;\n snapshot = { menus, activeMenuId: activeMenu?.id ?? null, activeMenu, error };\n return snapshot;\n }\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"menu-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 (initialMenus)\n return;\n fetchMenus()\n .then((m) => {\n if (!started)\n return;\n menus = m;\n selectedId ??= m[0]?.id ?? null;\n emit();\n })\n .catch((e) => {\n if (!started)\n return;\n menus = [];\n error = e instanceof Error ? e.message : String(e);\n emit();\n });\n },\n stop() {\n started = false;\n },\n setActiveMenuId(id) {\n if (id === selectedId)\n return;\n selectedId = id;\n emit();\n },\n };\n}\n",
17 "js/wix/menu.js": "// Declarations of menu.ts: what this file exports, their arguments and what they return.\n// import type { MenuData, SiteMoney } from \"./restaurants-types.js\";\n// export declare function fetchSiteMoney(): Promise<SiteMoney>;\n// export declare function fetchMenus(): Promise<MenuData[]>;\n// Menu reads over REST (Wix Restaurants Menus V1) — the twin of app/wix/restaurants/menu.ts. Same\n// exports, same MenuData tree; the rules and the id-array stitching come from menu-core (the SAME\n// file the SDK transport uses, deployed flat next to this one by deploy.mjs --stack static), so\n// this file is only the transport: one GET per list page, literal paths and query params. Every\n// call here is safe from a browser with a visitor token. Porting: keep the paths, keep the params,\n// port the core once. Entities carry `id` here (the SDK spells it `_id`); the core accepts both.\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/menus/list-menus.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/items/items/list-items.md\nimport { wixRequest } from \"./client.js\";\nimport { imgSrc } from \"./media.js\";\nimport { anyLabels, assembleMenus, chunk, collectGroupIds, collectModifierIds, collectVariantIds, nextCursor, toSiteMoney, } from \"./menu-core.js\";\nconst get = (path, query) => wixRequest(path, { method: \"GET\", query });\n// The site's currency + locale are site config — read once per page, reused by every price.\nlet moneyPromise = null;\n/**\n * The site's currency and locale from the eCommerce settings' BUSINESS_INFO (the read Wix's own\n * menus code makes before formatting a price). { currency: \"\" } when the read fails: prices then\n * format to \"\" rather than to a guessed symbol.\n * GET /_api/ecommerce-settings/v1/ecommerce-settings?fields=BUSINESS_INFO (the path the SDK resolves on www.wixapis.com)\n */\nexport function fetchSiteMoney() {\n moneyPromise ??= get(\"/_api/ecommerce-settings/v1/ecommerce-settings\", { fields: [\"BUSINESS_INFO\"] })\n .then((res) => toSiteMoney(res))\n .catch(() => {\n moneyPromise = null; // transient failure — allow a retry on the next call\n return { currency: \"\", locale: \"\" };\n });\n return moneyPromise;\n}\n/** Every page of a cursor-paged list (?paging.cursor=…, pagingMetadata.cursors.next), the entities concatenated. */\nasync function allPages(path, query, key) {\n const out = [];\n let cursor = null;\n do {\n const res = await get(path, cursor ? { ...query, \"paging.cursor\": cursor } : query);\n out.push(...(res?.[key] ?? []));\n cursor = nextCursor(res);\n } while (cursor);\n return out;\n}\n/** An id-array read in chunks of 100 ids per call (the key repeats: ?variantIds=a&variantIds=b), the entities concatenated. */\nasync function byIds(path, param, ids, key) {\n const pages = await Promise.all(chunk(ids).map((part) => get(path, { [param]: part })));\n return pages.flatMap((res) => (res?.[key] ?? []));\n}\n/**\n * The site's full menu tree, assembled and display-ordered, every price formatted in the site's\n * currency — the one entry point for every menu surface; [] when no menus exist. The three visible\n * lists are read to the last page, then the referenced variants, modifier groups (100 ids per\n * call), and labels, then the groups' modifiers.\n * GET /restaurants/menus-menu/v1/menus?onlyVisible=true[&paging.cursor=…]\n * GET /restaurants/menus-section/v1/sections?onlyVisible=true[&paging.cursor=…]\n * GET /restaurants/menus-item/v1/items?onlyVisible=true[&paging.cursor=…]\n * GET /restaurants/item-variants/v1/variants?variantIds=…\n * GET /restaurants/item-modifier-group/v1/modifier-groups?modifierGroupIds=…\n * GET /restaurants/item-modifiers/v1/modifiers?modifierIds=…\n * GET /restaurants/item-labels/v1/labels\n */\nexport async function fetchMenus() {\n const VISIBLE = { onlyVisible: \"true\" };\n const [menus, sections, items, money] = await Promise.all([\n allPages(\"/restaurants/menus-menu/v1/menus\", VISIBLE, \"menus\"),\n allPages(\"/restaurants/menus-section/v1/sections\", VISIBLE, \"sections\"),\n allPages(\"/restaurants/menus-item/v1/items\", VISIBLE, \"items\"),\n fetchSiteMoney(),\n ]);\n if (!menus.length)\n return [];\n const variantIds = collectVariantIds(items);\n const groupIds = collectGroupIds(items);\n const [variants, groups, labels] = await Promise.all([\n byIds(\"/restaurants/item-variants/v1/variants\", \"variantIds\", variantIds, \"variants\"),\n byIds(\"/restaurants/item-modifier-group/v1/modifier-groups\", \"modifierGroupIds\", groupIds, \"modifierGroups\"),\n // List Labels has no paging: one call returns them all.\n anyLabels(items) ? get(\"/restaurants/item-labels/v1/labels\").then((res) => (res?.labels ?? [])) : Promise.resolve([]),\n ]);\n const modifiers = await byIds(\"/restaurants/item-modifiers/v1/modifiers\", \"modifierIds\", collectModifierIds(groups), \"modifiers\");\n return assembleMenus({ menus, sections, items, variants, groups, modifiers, labels, money }, imgSrc);\n}\n",
18 "js/wix/order-store.js": "// Declarations of order-store.ts: what this file exports, their arguments and what they return.\n// import type { DeliveryAddress, FulfillmentChoice, FulfillmentMethodInfo, FulfillmentTime, MenuItem, MenuOrderingInfo, OrderCart, OrderSelection, OrderingStatus } from \"./restaurants-types.js\";\n// export interface OrderCartState {\n// cart: OrderCart | null;\n// ordering: boolean | null;\n// orderingStatus: OrderingStatus | null;\n// menuOrdering: Record<string, MenuOrderingInfo> | null;\n// fulfillment: FulfillmentMethodInfo[] | null;\n// fulfillmentTimes: FulfillmentTime[] | null;\n// fulfillmentChoice: FulfillmentChoice | null;\n// busy: boolean;\n// pendingItemId: string | null;\n// error: string | null;\n// open: boolean;\n// }\n// export declare function getOrderCartState(): OrderCartState;\n// export declare function subscribeOrderCart(fn: (state: OrderCartState) => void): () => void;\n// export declare function isMenuOrderable(menuId: string): boolean | null;\n// export declare function refreshOrderCart(): Promise<void>;\n// export declare function addOrderLine(item: MenuItem, context: {\n// menuId: string;\n// sectionId: string;\n// }, quantity?: number, selection?: OrderSelection): Promise<void>;\n// export declare function updateOrderLineQuantity(lineItemId: string, quantity: number): Promise<void>;\n// export declare function removeLineFromOrder(lineItemId: string): Promise<void>;\n// export declare function loadFulfillmentTimes(dayKey?: string, address?: DeliveryAddress): Promise<void>;\n// export declare function chooseFulfillment(choice: FulfillmentChoice): Promise<void>;\n// export declare function goToOrderCheckout(): Promise<void>;\n// export declare function setOrderCartOpen(open: boolean): void;\nimport { menuOrderable } from \"./ordering-core.js\";\nimport { addToOrder as apiAdd, fetchFulfillmentMethods, fetchFulfillmentTimes, fetchMenuOrdering, fetchOrderCart, orderCheckoutUrl, removeOrderLine as apiRemove, resolveOrdering, setOrderFulfillment, updateOrderQuantity as apiUpdate, } from \"./ordering.js\";\nconst EMPTY = { cart: null, ordering: null, orderingStatus: null, menuOrdering: null, fulfillment: null, fulfillmentTimes: null, fulfillmentChoice: null, busy: false, pendingItemId: null, error: null, open: false };\nlet state = EMPTY;\nconst listeners = new Set();\nlet loaded = false;\nfunction setState(patch) {\n state = { ...state, ...patch };\n for (const l of listeners)\n l();\n}\nexport function getOrderCartState() {\n return state;\n}\nexport function subscribeOrderCart(fn) {\n const listener = () => { try {\n fn(getOrderCartState());\n }\n catch (e) {\n console.error(\"order-store listener failed\", e);\n } };\n listeners.add(listener);\n // First subscriber triggers the initial load (browser only — SSR renders the empty state).\n if (!loaded && typeof window !== \"undefined\") {\n loaded = true;\n void refreshOrderCart();\n void resolveOrdering().then((s) => setState({ ordering: s.status === \"ENABLED\", orderingStatus: s }));\n void fetchMenuOrdering().then((m) => setState({ menuOrdering: m })).catch(() => setState({ menuOrdering: null }));\n void fetchFulfillmentMethods().then((f) => setState({ fulfillment: f }));\n void loadFulfillmentTimes();\n }\n return () => listeners.delete(listener);\n}\n/**\n * Can dishes of `menuId` be added right now? false when the menu isn't enabled for online\n * ordering under the operation or is outside its availability window (render no add control);\n * null while the settings are still loading or could not be read — treat null as \"try\" (the add\n * itself is refused server-side with a message).\n */\nexport function isMenuOrderable(menuId) {\n return menuOrderable(state.menuOrdering, menuId);\n}\nasync function run(op) {\n setState({ busy: true, error: null });\n try {\n setState({ cart: await op(), busy: false });\n }\n catch (e) {\n setState({ busy: false, error: e instanceof Error ? e.message : String(e) });\n throw e;\n }\n}\nexport async function refreshOrderCart() {\n try {\n setState({ cart: await fetchOrderCart() });\n }\n catch {\n /* no cart yet — leave the empty state */\n }\n}\n/**\n * Add a dish (the MenuItem DTO as rendered, with the menu/section ids it is rendered under and\n * the visitor's selection) to the order and open the drawer. Throws (and sets .error) on refusal.\n */\nexport async function addOrderLine(item, context, quantity = 1, selection) {\n setState({ pendingItemId: item.id });\n try {\n await run(() => apiAdd(item, context, quantity, selection));\n }\n finally {\n setState({ pendingItemId: null });\n }\n setState({ open: true });\n}\nexport async function updateOrderLineQuantity(lineItemId, quantity) {\n await run(() => apiUpdate(lineItemId, quantity));\n}\nexport async function removeLineFromOrder(lineItemId) {\n await run(() => apiRemove(lineItemId));\n}\n/**\n * Load the fulfillment times for `dayKey` (\"YYYY-MM-DD\" at the restaurant; default today). Pass the\n * delivery address once entered: delivery times then come only from methods that deliver there.\n */\nexport async function loadFulfillmentTimes(dayKey, address) {\n try {\n setState({ fulfillmentTimes: await fetchFulfillmentTimes(dayKey, address) });\n }\n catch {\n setState({ fulfillmentTimes: null }); // unreadable — checkout still works on ASAP\n }\n}\n/**\n * Set pickup or delivery, the time (a fulfillmentTimes entry; omitted → ASAP) and, for delivery,\n * the address on the order. Throws (and sets .error) when the address isn't delivered to or the\n * time is gone.\n */\nexport async function chooseFulfillment(choice) {\n await run(() => setOrderFulfillment(choice));\n setState({ fulfillmentChoice: choice });\n}\n/** Navigate to the Wix-hosted checkout (pickup ASAP when no fulfillment was chosen). Never hand-build a checkout URL. */\nexport async function goToOrderCheckout() {\n setState({ busy: true, error: null });\n try {\n const url = await orderCheckoutUrl();\n window.location.href = url;\n }\n catch (e) {\n setState({ busy: false, error: e instanceof Error ? e.message : String(e) });\n throw e;\n }\n}\nexport function setOrderCartOpen(open) {\n setState({ open });\n}\n",
19 "js/wix/ordering-core.js": "// Declarations of ordering-core.ts: what this file exports, their arguments and what they return.\n// import type { DeliveryAddress, FulfillmentChoice, FulfillmentMethodInfo, FulfillmentTime, MenuItem, MenuOrderingInfo, OrderCart, OrderLine, OrderSelection, OrderingStatus, SiteMoney } from \"./restaurants-types.js\";\n// import type { ImgSrc, Raw } from \"./menu-core.js\";\n// export declare const RESTAURANTS_ORDERS_APP_ID = \"9a5d83fd-8570-482e-81ab-cfa88942ee60\";\n// export declare const rawId: (raw: Raw | undefined | null) => string;\n// export declare function formatMoney(money: Raw | null | undefined, currencyCode: string | null | undefined): string;\n// export declare const cartCurrency: (raw: Raw | null | undefined) => string;\n// export declare function resolveOperation(operations: Raw[]): OrderingStatus;\n// export declare function pickOperationId(operations: Raw[]): string | null;\n// export declare function toMenuOrdering(raw: Raw): MenuOrderingInfo;\n// export declare function toMenuOrderingMap(raws: Raw[]): Record<string, MenuOrderingInfo>;\n// export declare function isMenuAvailable(info: MenuOrderingInfo, now?: Date): boolean;\n// export declare function menuOrderable(map: Record<string, MenuOrderingInfo> | null, menuId: string, now?: Date): boolean | null;\n// export declare function toFulfillmentMethods(raws: Raw[], fulfillmentIds: string[], money: SiteMoney): FulfillmentMethodInfo[];\n// export declare function fulfillmentCode(choice: Pick<FulfillmentChoice, \"type\" | \"time\">): string;\n// export declare function toFulfillmentTimes(asapSlots: Raw | null | undefined, daySlots: Raw | null | undefined): FulfillmentTime[];\n// export declare function slotsDateQuery(dayKey: string): Record<string, string>;\n// export declare function slotsAddressQuery(address: DeliveryAddress | undefined, region: {\n// country: string;\n// subdivision: string;\n// }): Record<string, string>;\n// export declare const restaurantToday: (timeZone: string, now?: Date) => string;\n// export declare function toRestaurantRegion(rawMethods: Raw[]): {\n// country: string;\n// subdivision: string;\n// };\n// export declare function fulfillmentAddress(choice: FulfillmentChoice, region: {\n// country: string;\n// subdivision: string;\n// }): DeliveryAddress | {\n// country: string;\n// subdivision?: string;\n// };\n// export declare const NOT_DELIVERABLE = \"We don't deliver to this address. Choose pickup or another address.\";\n// export declare function toOrderLine(raw: Raw, currency: string, imgSrc: ImgSrc): OrderLine;\n// export declare function toOrderCart(raw: Raw | null, subtotal: string, imgSrc: ImgSrc): OrderCart;\n// export declare function estimateSubtotal(estimate: Raw | null | undefined, raw: Raw): string;\n// export declare function assertOrderContext(itemId: string, menuId: string, sectionId: string): void;\n// export declare function assertOrderable(item: Pick<MenuItem, \"marketPrice\" | \"soldOut\" | \"name\">): void;\n// export declare function orderCatalogItem(item: MenuItem, ctx: {\n// operationId: string;\n// menuId: string;\n// sectionId: string;\n// }, quantity: number, selection: OrderSelection): Raw;\n// export declare function assertOrderAdded(cart: Raw | null | undefined, itemId: string): void;\n// export declare function assertOrderCheckoutable(raw: Raw | null): void;\n// export declare function orderingUnavailableReason(status: OrderingStatus | null): string;\nimport { zonedDayKey, zonedMinutes, zonedParts } from \"./time-core.js\";\n/** The Restaurants Orders app id — every restaurant cart line's catalogReference.appId. */\nexport const RESTAURANTS_ORDERS_APP_ID = \"9a5d83fd-8570-482e-81ab-cfa88942ee60\";\nexport const rawId = (raw) => raw?._id ?? raw?.id ?? \"\";\n/**\n * Cart V2 money is ConvertedMoney { amount, convertedAmount } with NO formatted string, and the\n * currency lives on the cart, not on the money. \"\" when the currency is unknown — never a guessed\n * symbol, never a USD default.\n */\nexport function formatMoney(money, currencyCode) {\n const value = money?.convertedAmount ?? money?.amount;\n if (value == null || value === \"\" || !currencyCode)\n return \"\";\n try {\n return new Intl.NumberFormat(undefined, { style: \"currency\", currency: currencyCode }).format(Number(value));\n }\n catch {\n return `${value} ${currencyCode}`;\n }\n}\n/** A site-currency decimal (\"5\", \"0\") formatted for display; \"\" when zero or the currency is unknown. */\nfunction formatAmount(amount, money) {\n const n = Number(amount ?? 0);\n if (!Number.isFinite(n) || n <= 0 || !money.currency)\n return \"\";\n try {\n return new Intl.NumberFormat(money.locale || undefined, { style: \"currency\", currency: money.currency }).format(n);\n }\n catch {\n return `${n.toFixed(2)} ${money.currency}`;\n }\n}\nexport const cartCurrency = (raw) => raw?.customerInfo?.currencyCode ?? raw?.businessInfo?.currencyCode ?? \"\";\n// ---- operation ----------------------------------------------------------------------------------------------\nconst NO_ORDERING = { operationId: null, status: \"NONE\", pausedUntilIso: null, timeZone: \"\", fulfillmentIds: [], defaultFulfillmentType: null };\nconst isoOf = (v) => {\n if (!v)\n return null;\n const d = v instanceof Date ? v : new Date(String(v));\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n};\n/**\n * The operation to order through and its state: an ENABLED one, else the default, else the first.\n * Only ENABLED accepts orders (Wix: AcceptingOrders = operation.enabled && a fulfillment); DISABLED\n * and PAUSED_UNTIL (with `pausedUntilOptions.time`) refuse every add — surface the reason.\n */\nexport function resolveOperation(operations) {\n const op = operations.find((o) => o.onlineOrderingStatus === \"ENABLED\") ?? operations.find((o) => o.default) ?? operations[0];\n if (!op || !rawId(op))\n return NO_ORDERING;\n const raw = String(op.onlineOrderingStatus ?? \"\");\n const status = raw === \"ENABLED\" ? \"ENABLED\" : raw === \"PAUSED_UNTIL\" ? \"PAUSED_UNTIL\" : \"DISABLED\";\n return {\n operationId: rawId(op),\n status,\n pausedUntilIso: status === \"PAUSED_UNTIL\" ? isoOf(op.pausedUntilOptions?.time) : null,\n timeZone: op.businessLocationDetails?.timeZone ?? \"\",\n fulfillmentIds: (op.fulfillmentIds ?? []).filter(Boolean),\n defaultFulfillmentType: op.defaultFulfillmentType === \"DELIVERY\" ? \"DELIVERY\" : op.defaultFulfillmentType === \"PICKUP\" ? \"PICKUP\" : null,\n };\n}\n/** Kept for callers that only need the id — null when the site has no operation at all. */\nexport function pickOperationId(operations) {\n return resolveOperation(operations).operationId;\n}\n// ---- menu ordering settings (per operation) ---------------------------------------------------------\nconst DAYS = [\"SUN\", \"MON\", \"TUE\", \"WED\", \"THU\", \"FRI\", \"SAT\"];\nconst hhmm = (t) => `${String(t?.hours ?? 0).padStart(2, \"0\")}:${String(t?.minutes ?? 0).padStart(2, \"0\")}`;\n/** One menu's ordering settings entry → the DTO (availability windows normalised to \"HH:mm\" / ISO). */\nexport function toMenuOrdering(raw) {\n const a = raw.availability ?? {};\n const type = String(a.type ?? \"\");\n return {\n menuId: raw.menuId ?? \"\",\n enabled: raw.onlineOrderingEnabled !== false,\n availability: {\n type: type === \"ALWAYS_AVAILABLE\" || type === \"WEEKLY_SCHEDULE\" || type === \"TIMESTAMP_RANGES\" ? type : \"UNSPECIFIED\",\n timeZone: a.timeZone ?? \"\",\n weekly: (a.weeklyScheduleOptions?.availableTimes ?? []).flatMap((d) => (d.timeRanges ?? []).map((r) => ({ day: String(d.dayOfWeek), start: hhmm(r.startTime), end: hhmm(r.endTime) }))),\n ranges: (a.timestampRangesOptions?.ranges ?? [])\n .map((r) => ({ startIso: isoOf(r.startTime) ?? \"\", endIso: isoOf(r.endTime) ?? \"\" }))\n .filter((r) => r.startIso && r.endIso),\n },\n };\n}\n/** The operation's menu settings keyed by menuId. */\nexport function toMenuOrderingMap(raws) {\n const out = {};\n for (const raw of raws) {\n const info = toMenuOrdering(raw);\n if (info.menuId)\n out[info.menuId] = info;\n }\n return out;\n}\nconst toMinutes = (s) => {\n const [h, m] = s.split(\":\").map(Number);\n return (h || 0) * 60 + (m || 0);\n};\n/** Whether the menu's availability window contains `now`, evaluated in the setting's own timezone. */\nexport function isMenuAvailable(info, now = new Date()) {\n const { availability: a } = info;\n if (a.type === \"TIMESTAMP_RANGES\") {\n const t = now.getTime();\n return a.ranges.some((r) => new Date(r.startIso).getTime() <= t && t < new Date(r.endIso).getTime());\n }\n if (a.type !== \"WEEKLY_SCHEDULE\")\n return true; // ALWAYS_AVAILABLE, or an unspecified type: don't block on it\n const parts = zonedParts(now, a.timeZone);\n const minutes = zonedMinutes(now, a.timeZone);\n const today = DAYS[parts.weekday];\n const yesterday = DAYS[(parts.weekday + 6) % 7];\n return a.weekly.some((w) => {\n const start = toMinutes(w.start), end = toMinutes(w.end);\n if (w.day === today)\n return end > start ? start <= minutes && minutes < end : minutes >= start || minutes < end;\n // a window that wraps past midnight also covers the early hours of the next day\n return w.day === yesterday && end <= start && minutes < end;\n });\n}\n/**\n * Can dishes of `menuId` be added right now? true/false once the operation's menu settings are\n * known; null while they are still loading or could not be read (the add itself is still refused\n * server-side, with a message), and null for a menu with no entry under the operation.\n */\nexport function menuOrderable(map, menuId, now = new Date()) {\n if (!map)\n return null;\n const info = map[menuId];\n // A menu with no settings entry is unknown, not disabled: the add itself decides server-side.\n if (!info)\n return null;\n return info.enabled && isMenuAvailable(info, now);\n}\n// ---- fulfillment methods ----------------------------------------------------------------------------------\n/** The operation's enabled pickup/delivery methods (operation.fulfillmentIds, `enabled`), money formatted. */\nexport function toFulfillmentMethods(raws, fulfillmentIds, money) {\n const wanted = new Set(fulfillmentIds);\n return raws\n .filter((m) => m.enabled === true && wanted.has(rawId(m)))\n .map((m) => ({\n id: rawId(m),\n type: m.type === \"DELIVERY\" ? \"DELIVERY\" : \"PICKUP\",\n name: m.name ?? \"\",\n fee: formatAmount(m.fee, money),\n feeAmount: m.fee ?? \"0\",\n minOrderPrice: formatAmount(m.minOrderPrice, money),\n minOrderPriceAmount: m.minOrderPrice ?? \"0\",\n }));\n}\n// ---- fulfillment choice ----------------------------------------------------------------------------------\n// Restaurants reads the order's fulfillment from the cart's delivery method code, and the hosted\n// checkout offers no pickup/delivery choice until one is set: \"<TYPE>|ASAP\" or\n// \"<TYPE>|<startEpochMs>|<endEpochMs>\" for a scheduled slot.\n/** The cart delivery-method code for a choice (no time → ASAP). */\nexport function fulfillmentCode(choice) {\n if (choice.time && choice.time.type !== choice.type)\n throw new Error(\"The chosen time is for a different fulfillment type.\");\n return choice.time?.code ?? `${choice.type}|ASAP`;\n}\nconst epochMs = (iso) => new Date(String(iso ?? \"\")).getTime();\n/**\n * The operation's time slots → FulfillmentTime[] (ASAP first, then earliest first). `asapSlots` is\n * the first-available response (one ASAP entry per type), `daySlots` a per-date response; both carry\n * `timeslotsPerFulfillmentType` (the API spells the type field `fulfilmentType`). The calls return\n * only orderable slots (blocked ones are left out), so there is nothing to filter.\n */\nexport function toFulfillmentTimes(asapSlots, daySlots) {\n const out = [];\n const seen = new Set();\n for (const raw of [...(asapSlots?.timeslotsPerFulfillmentType ?? []), ...(daySlots?.timeslotsPerFulfillmentType ?? [])]) {\n const type = (raw.fulfilmentType ?? raw.fulfillmentType) === \"DELIVERY\" ? \"DELIVERY\" : \"PICKUP\";\n const slot = raw.timeSlot ?? {};\n const start = epochMs(slot.startTime), end = epochMs(slot.endTime ?? slot.startTime);\n if (!Number.isFinite(start) || !Number.isFinite(end))\n continue;\n const asap = slot.orderSchedulingType === \"ASAP\";\n const code = asap ? `${type}|ASAP` : `${type}|${start}|${end}`;\n if (seen.has(code))\n continue;\n seen.add(code);\n out.push({ type, code, asap, startIso: new Date(start).toISOString(), endIso: new Date(end).toISOString() });\n }\n return out.sort((a, b) => Number(b.asap) - Number(a.asap) || a.startIso.localeCompare(b.startIso));\n}\n/** The per-date slots query for a \"YYYY-MM-DD\" day at the restaurant (`date.day`, `date.month`, `date.year`). */\nexport function slotsDateQuery(dayKey) {\n const [year, month, day] = dayKey.split(\"-\").map(Number);\n return { \"date.day\": String(day), \"date.month\": String(month), \"date.year\": String(year) };\n}\n/**\n * The time-slot calls' `deliveryAddress.*` query for a delivery address, so the delivery slots are\n * those of the methods that deliver there ({} when there's no street or city yet).\n */\nexport function slotsAddressQuery(address, region) {\n if (!address?.addressLine?.trim() || !address.city?.trim())\n return {};\n const a = fulfillmentAddress({ type: \"DELIVERY\", address }, region);\n const q = {};\n for (const [k, v] of Object.entries(a))\n if (v)\n q[`deliveryAddress.${k}`] = String(v);\n return q;\n}\n/** Today at the restaurant, \"YYYY-MM-DD\" (the operation's timezone; \"\" → the visitor's). */\nexport const restaurantToday = (timeZone, now = new Date()) => zonedDayKey(now, timeZone);\n/**\n * The restaurant's country and subdivision (\"US-CA\"), from its pickup address or its delivery\n * area's center. The cart needs a country before ANY delivery method — pickup included — is\n * accepted (DELIVERY_METHOD_MISSING_ADDRESS), and Wix pre-fills one only from the visitor's location.\n */\nexport function toRestaurantRegion(rawMethods) {\n for (const m of rawMethods) {\n const a = m.pickupOptions?.address ?? m.deliveryOptions?.deliveryArea?.radiusOptions?.centerPointAddress ?? {};\n if (!a.country)\n continue;\n const sub = String(a.subdivision ?? \"\");\n return { country: a.country, subdivision: sub && !sub.includes(\"-\") ? `${a.country}-${sub}` : sub };\n }\n return { country: \"\", subdivision: \"\" };\n}\n/**\n * The address a choice puts on the cart: the delivery address with the restaurant's country and\n * subdivision filled in, or for pickup just the restaurant's country. Refuses a delivery choice\n * without the fields Restaurants requires (street, city, country).\n */\nexport function fulfillmentAddress(choice, region) {\n if (choice.type !== \"DELIVERY\") {\n if (!region.country)\n throw new Error(\"Pickup isn't set up for online orders. Choose delivery.\");\n return { country: region.country, ...(region.subdivision ? { subdivision: region.subdivision } : {}) };\n }\n const a = choice.address;\n const country = a?.country?.trim() || region.country;\n const subdivision = a?.subdivision?.trim() || (country === region.country ? region.subdivision : \"\");\n if (!a?.addressLine?.trim() || !a.city?.trim() || !country)\n throw new Error(\"Enter a delivery address: street and city.\");\n return { ...a, country, ...(subdivision ? { subdivision } : {}) };\n}\n/** The message for a delivery method the cart refused: the address is outside every delivery area. */\nexport const NOT_DELIVERABLE = \"We don't deliver to this address. Choose pickup or another address.\";\n// ---- cart --------------------------------------------------------------------------------------------------------\nexport function toOrderLine(raw, currency, imgSrc) {\n return {\n lineItemId: rawId(raw), // the LINE id — what update/remove take, never the menu item id\n itemName: raw.name?.original ?? \"\",\n quantity: raw.quantityInfo?.confirmedQuantity ?? 0,\n unitPrice: formatMoney(raw.pricing?.unitPrice, currency),\n linePrice: formatMoney(raw.pricing?.totalPrice, currency),\n imageUrl: imgSrc(raw.attributes?.image, 300, 300),\n descriptionLines: (raw.attributes?.descriptionLines ?? [])\n .map((d) => {\n const label = d.name?.original ?? \"\", value = d.plainText?.original ?? d.colorInfo?.original ?? \"\";\n return label && value ? `${label}: ${value}` : value || label;\n })\n .filter(Boolean),\n status: raw.status ?? \"IN_STOCK\", // not IN_STOCK → the line can't be checked out as-is\n };\n}\nexport function toOrderCart(raw, subtotal, imgSrc) {\n const currency = cartCurrency(raw);\n const lines = (raw?.lineItems ?? []).map((l) => toOrderLine(l, currency, imgSrc));\n const delivery = raw?.deliveryInfo ?? {};\n return { lines, itemCount: lines.reduce((n, l) => n + l.quantity, 0), subtotal, currency, fulfillmentCode: delivery.method?.code ?? \"\" };\n}\n/** The after-discount subtotal from a cart estimate; \"\" when unknown. Fees and tax resolve at checkout. */\nexport function estimateSubtotal(estimate, raw) {\n return formatMoney(estimate?.summary?.priceSummary?.subtotal, cartCurrency(raw));\n}\n/** The line-refusal reasons an add can't recover from — checked before any call. */\nexport function assertOrderContext(itemId, menuId, sectionId) {\n if (!itemId || !menuId || !sectionId)\n throw new Error(\"addToOrder needs the item, menu, and section ids.\");\n}\n/** Reject a dish that can't be ordered before any call is made — the message is what the visitor reads. */\nexport function assertOrderable(item) {\n if (item.marketPrice)\n throw new Error(`${item.name || \"This dish\"} is priced at the counter and can't be ordered online.`);\n if (item.soldOut)\n throw new Error(`${item.name || \"This dish\"} is sold out.`);\n}\n/**\n * The one catalogItems entry of an add — the Orders app id, the three context ids, and the\n * visitor's choices in the exact option keys Wix's ordering sends: `priceVariant` (id + its\n * formatted price), `modifierGroups` (the entity ids — a selection KEY resolves back to its id —\n * each with `price` as the decimal up-charge and `formattedPrice` only when charged), and\n * `specialRequests` when the item accepts one. Formatted strings are the DTO's (site currency).\n */\nexport function orderCatalogItem(item, ctx, quantity, selection) {\n const variant = item.variants.find((v) => v.variantId === selection.variantId);\n const modifierGroups = item.modifierGroups\n .map((g) => ({\n id: g.id,\n modifiers: (selection.modifiers[g.id] ?? [])\n .map((key) => g.modifiers.find((m) => m.key === key))\n .filter((m) => !!m)\n .map((m) => ({ id: m.id, price: m.additionalChargeAmount, ...(Number(m.additionalChargeAmount) > 0 && m.additionalCharge ? { formattedPrice: m.additionalCharge } : {}) })),\n }))\n .filter((g) => g.modifiers.length > 0);\n const note = item.acceptsSpecialRequests ? selection.specialRequest.trim() : \"\";\n return {\n quantity,\n catalogReference: {\n catalogItemId: item.id,\n appId: RESTAURANTS_ORDERS_APP_ID,\n options: {\n operationId: ctx.operationId,\n menuId: ctx.menuId,\n menuSectionId: ctx.sectionId, // what Restaurants' checkout validates the dish against\n sectionId: ctx.sectionId,\n ...(variant ? { priceVariant: { id: variant.variantId, formattedPrice: variant.price } } : {}),\n ...(modifierGroups.length ? { modifierGroups } : {}),\n ...(note ? { specialRequests: note } : {}),\n },\n },\n };\n}\n/** Read the add result — a refused add still returns 200: a line whose status isn't IN_STOCK, or no line, is a refusal. */\nexport function assertOrderAdded(cart, itemId) {\n // V2 nests the reference under `source` — a top-level lineItem.catalogReference no longer exists.\n const line = (cart?.lineItems ?? []).find((l) => l.source?.catalogReference?.catalogItemId === itemId);\n if (line?.status && line.status !== \"IN_STOCK\") {\n throw new Error(`This dish isn't available right now (${String(line.status).toLowerCase().replace(/_/g, \" \")}).`);\n }\n if (!line || line.quantityInfo?.confirmedQuantity === 0) {\n throw new Error(\"The dish couldn't be added to the order — please try again.\");\n }\n}\n/** Refuse checkout for an empty order or any line not IN_STOCK — say which. */\nexport function assertOrderCheckoutable(raw) {\n const lines = raw?.lineItems ?? [];\n if (!lines.length)\n throw new Error(\"Your order is empty.\");\n const unavailable = lines.filter((l) => l.status && l.status !== \"IN_STOCK\");\n if (unavailable.length) {\n throw new Error(`Some dishes are no longer available: ${unavailable.map((l) => l.name?.original).filter(Boolean).join(\", \")}.`);\n }\n if (!rawId(raw))\n throw new Error(\"Checkout couldn't start: the order has no id.\");\n}\n/** The message an add control shows when the operation refuses orders; \"\" when it accepts them. */\nexport function orderingUnavailableReason(status) {\n if (!status)\n return \"\";\n switch (status.status) {\n case \"ENABLED\": return \"\";\n case \"PAUSED_UNTIL\": return \"Ordering is paused right now\";\n case \"DISABLED\": return \"Ordering unavailable\";\n default: return \"Ordering unavailable\";\n }\n}\n",
20 "js/wix/ordering.js": "// Declarations of ordering.ts: what this file exports, their arguments and what they return.\n// import { RESTAURANTS_ORDERS_APP_ID } from \"./ordering-core.js\";\n// import type { DeliveryAddress, FulfillmentChoice, FulfillmentMethodInfo, FulfillmentTime, MenuItem, MenuOrderingInfo, OrderCart, OrderSelection, OrderingStatus } from \"./restaurants-types.js\";\n// export { RESTAURANTS_ORDERS_APP_ID };\n// export declare function resolveOrdering(): Promise<OrderingStatus>;\n// export declare function resolveOperationId(): Promise<string | null>;\n// export declare function fetchMenuOrdering(): Promise<Record<string, MenuOrderingInfo>>;\n// export declare function fetchFulfillmentMethods(): Promise<FulfillmentMethodInfo[]>;\n// export declare function fetchOrderCart(): Promise<OrderCart>;\n// export declare function addToOrder(item: MenuItem, { menuId, sectionId }: {\n// menuId: string;\n// sectionId: string;\n// }, quantity?: number, selection?: OrderSelection): Promise<OrderCart>;\n// export declare function updateOrderQuantity(lineItemId: string, quantity: number): Promise<OrderCart>;\n// export declare function removeOrderLine(lineItemId: string): Promise<OrderCart>;\n// export declare function fetchFulfillmentTimes(dayKey?: string, address?: DeliveryAddress): Promise<FulfillmentTime[]>;\n// export declare function setOrderFulfillment(choice: FulfillmentChoice): Promise<OrderCart>;\n// export declare function orderCheckoutUrl(origin?: string): Promise<string>;\n// Online ordering over REST (Restaurants Orders + Wix eCom Cart V2) — the twin of\n// app/wix/restaurants/ordering.ts. Same exports, same OrderCart/OrderLine DTOs; rules and mappers\n// from ordering-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 carts break: the Orders app id, options\n// { operationId, menuId, menuSectionId } (sectionId rides along) and the visitor's choices in\n// options.priceVariant / options.modifierGroups / options.specialRequests, the keys Wix's own\n// ordering sends; and the fulfillment (pickup/delivery + time) as the cart's delivery method code,\n// set BEFORE the checkout redirect — the hosted checkout has no picker for it and refuses the order\n// without it. Failures are loud: a missing or paused operation, an invalid selection, an\n// unavailable line, an undeliverable address, an empty order at checkout all throw — surface the message.\n// All calls run with the visitor token: the cart is the token's (see ./client).\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/operations/list-operations.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/menu-ordering-settings/query-menu-ordering-settings.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/fulfillment-methods/list-fulfillment-methods.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/add-line-items-to-current-cart.md\n// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/set-delivery-method-for-current-cart.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/update-current-cart.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/operations/calculate-available-time-slots-for-date.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/operations/calculate-first-available-time-slot-per-fulfillment-type.md\nimport { WixApiError, wixRequest } from \"./client.js\";\nimport { imgSrc } from \"./media.js\";\nimport { fetchSiteMoney } from \"./menu.js\";\nimport { initialSelection, nextCursor, validateSelection } from \"./menu-core.js\";\nimport { NOT_DELIVERABLE, RESTAURANTS_ORDERS_APP_ID, assertOrderAdded, assertOrderCheckoutable, assertOrderContext, assertOrderable, estimateSubtotal, fulfillmentAddress, fulfillmentCode, orderCatalogItem, rawId, resolveOperation, restaurantToday, slotsAddressQuery, slotsDateQuery, toFulfillmentMethods, toFulfillmentTimes, toMenuOrderingMap, toOrderCart, toRestaurantRegion, } from \"./ordering-core.js\";\nexport { RESTAURANTS_ORDERS_APP_ID };\nconst CART = \"/ecom/v2/carts/current\";\n// The ordering operation is site config — resolve once, reuse for every add.\n// GET /restaurants-operations/v1/operations\nlet orderingPromise = null;\n/**\n * The operation to order through and its state (ENABLED, else the default, else the first).\n * `status` \"NONE\" when the site has no online ordering; DISABLED / PAUSED_UNTIL refuse every add.\n */\nexport function resolveOrdering() {\n orderingPromise ??= wixRequest(\"/restaurants-operations/v1/operations\", { method: \"GET\" })\n .then((res) => resolveOperation(res?.operations ?? []))\n .catch(() => {\n orderingPromise = null; // transient failure — allow a retry on the next call\n return resolveOperation([]);\n });\n return orderingPromise;\n}\n/** The id of the operation to order through — null when the site has none. */\nexport function resolveOperationId() {\n return resolveOrdering().then((s) => s.operationId);\n}\n/**\n * Every menu's ordering settings under the operation, keyed by menuId (which menus take online\n * orders and when). A menu absent from the map is not orderable. {} when there is no operation.\n * POST /menu-ordering-settings/v1/menu-ordering-settings/query { query: { filter: { operationId }, cursorPaging: { cursor? } } }\n */\nexport async function fetchMenuOrdering() {\n const { operationId } = await resolveOrdering();\n if (!operationId)\n return {};\n const raws = [];\n let cursor = null;\n do {\n const res = await wixRequest(\"/menu-ordering-settings/v1/menu-ordering-settings/query\", {\n body: { query: { filter: { operationId }, ...(cursor ? { cursorPaging: { cursor } } : {}) } },\n });\n raws.push(...(res?.menuOrderingSettings ?? []));\n cursor = nextCursor(res);\n } while (cursor);\n return toMenuOrderingMap(raws);\n}\n/**\n * The operation's enabled pickup/delivery methods (operation.fulfillmentIds only), fees and\n * minimums formatted in the site currency — for display.\n * GET /fulfillment-methods/v1/fulfillment-methods[?cursorPaging.cursor=…]\n */\nexport async function fetchFulfillmentMethods() {\n try {\n const [status, money, raws] = await Promise.all([resolveOrdering(), fetchSiteMoney(), listFulfillmentRaws()]);\n return toFulfillmentMethods(raws, status.fulfillmentIds, money);\n }\n catch {\n return []; // ordering still works without the list (pickup ASAP)\n }\n}\n// The operation's raw fulfillment methods (operation.fulfillmentIds only), read once per session.\nlet fulfillmentRawsPromise = null;\nfunction listFulfillmentRaws() {\n fulfillmentRawsPromise ??= (async () => {\n const status = await resolveOrdering();\n if (!status.fulfillmentIds.length)\n return [];\n const wanted = new Set(status.fulfillmentIds);\n const raws = [];\n let cursor = null;\n do {\n const res = await wixRequest(\"/fulfillment-methods/v1/fulfillment-methods\", { method: \"GET\", query: cursor ? { \"cursorPaging.cursor\": cursor } : {} });\n raws.push(...(res?.fulfillmentMethods ?? []));\n cursor = nextCursor(res);\n } while (cursor);\n return raws.filter((m) => wanted.has(rawId(m)));\n })().catch((e) => {\n fulfillmentRawsPromise = null; // transient failure — allow a retry on the next call\n throw e;\n });\n return fulfillmentRawsPromise;\n}\n// A line's attributes.image may come back absent — join the menu items' images by the line's\n// catalogItemId, once per item, cached for the session. GET /restaurants/menus-item/v1/items?itemIds=…\nconst lineImageCache = new Map();\nasync function fillLineImages(cart, raws) {\n const wanted = new Map();\n raws.forEach((raw, i) => {\n if (cart.lines[i].imageUrl)\n return;\n const itemId = raw.source?.catalogReference?.catalogItemId;\n if (!itemId)\n return;\n const cached = lineImageCache.get(itemId);\n if (cached !== undefined) {\n cart.lines[i].imageUrl = cached;\n return;\n }\n wanted.set(itemId, [...(wanted.get(itemId) ?? []), i]);\n });\n if (!wanted.size)\n return;\n try {\n const res = await wixRequest(\"/restaurants/menus-item/v1/items\", { method: \"GET\", query: { itemIds: [...wanted.keys()] } });\n for (const item of (res?.items ?? [])) {\n const url = imgSrc(item.image, 300, 300);\n lineImageCache.set(rawId(item), url);\n for (const i of wanted.get(rawId(item)) ?? [])\n cart.lines[i].imageUrl = url;\n }\n }\n catch {\n /* images are a nicety — the cart stays correct without them */\n }\n}\nasync function readCartWithSubtotal(raw) {\n // Only estimate a cart WITH lines — on an absent/empty cart the endpoint returns 404.\n if (!raw?.lineItems?.length)\n return toOrderCart(raw, \"\", imgSrc);\n // The after-discount subtotal comes from the estimate, never from hand-summing lines; fees, tax,\n // and delivery resolve at checkout. POST /ecom/v2/carts/current/estimate {}\n let subtotal = \"\";\n try {\n subtotal = estimateSubtotal(await wixRequest(`${CART}/estimate`, { body: {} }), raw);\n }\n catch {\n /* the estimate is a display nicety — the cart itself is still valid */\n }\n const cart = toOrderCart(raw, subtotal, imgSrc);\n await fillLineImages(cart, raw.lineItems);\n return cart;\n}\n// GET /ecom/v2/carts/current — a 404 means no cart yet.\nasync function getCurrentCartRaw() {\n try {\n return (await wixRequest(CART, { method: \"GET\" }))?.cart ?? null;\n }\n catch (e) {\n if (e instanceof WixApiError && e.status === 404)\n return null;\n throw e;\n }\n}\n/** The visitor's current order cart. An empty cart (not an error) when none exists yet. */\nexport async function fetchOrderCart() {\n return readCartWithSubtotal(await getCurrentCartRaw());\n}\n/**\n * Add a dish to the current order. `item` is the MenuItem DTO as rendered; `menuId` and `sectionId`\n * are the ids of the menu and section it is RENDERED UNDER (the fetchMenus tree); `selection` is\n * what the visitor chose (start from initialSelection(item)). Throws when ordering isn't accepting,\n * the dish can't be ordered, a modifier rule fails, or the line is refused (a refused add still returns 200).\n * POST /ecom/v2/carts/current/add-line-items\n * { catalogItems: [{ quantity, catalogReference: { catalogItemId, appId: <Orders app>, options: {\n * operationId, menuId, menuSectionId, sectionId, priceVariant?: { id, formattedPrice },\n * modifierGroups?: [{ id, modifiers: [{ id, price, formattedPrice? }] }], specialRequests? } } }] }\n */\nexport async function addToOrder(item, { menuId, sectionId }, quantity = 1, selection = initialSelection(item)) {\n const { operationId, status } = await resolveOrdering();\n if (!operationId || status !== \"ENABLED\")\n throw new Error(\"Online ordering isn't available right now.\");\n assertOrderContext(item.id, menuId, sectionId);\n assertOrderable(item);\n const check = validateSelection(item, selection);\n if (!check.ok)\n throw new Error(Object.values(check.errors)[0]);\n const res = await wixRequest(`${CART}/add-line-items`, {\n body: { catalogItems: [orderCatalogItem(item, { operationId, menuId, sectionId }, quantity, selection)] },\n });\n assertOrderAdded(res?.cart, item.id);\n return readCartWithSubtotal(res?.cart ?? null);\n}\n/**\n * Change a line's quantity. `lineItemId` is OrderLine.lineItemId, never the menu item id.\n * POST /ecom/v2/carts/current/update-line-items { lineItems: [{ lineItemId, quantity: { newQuantity } }] }\n */\nexport async function updateOrderQuantity(lineItemId, quantity) {\n const res = await wixRequest(`${CART}/update-line-items`, { body: { lineItems: [{ lineItemId, quantity: { newQuantity: quantity } }] } });\n return readCartWithSubtotal(res?.cart ?? null);\n}\n/** Remove a line by OrderLine.lineItemId. POST /ecom/v2/carts/current/remove-line-items { lineItemIds } */\nexport async function removeOrderLine(lineItemId) {\n const res = await wixRequest(`${CART}/remove-line-items`, { body: { lineItemIds: [lineItemId] } });\n return readCartWithSubtotal(res?.cart ?? null);\n}\n/**\n * The times the order can be fulfilled at: ASAP for each type, then the scheduled slots of `dayKey`\n * (\"YYYY-MM-DD\" at the restaurant; default today), ASAP first. Pass the delivery `address` once the\n * visitor has entered it: the delivery times are then those of the methods that deliver there (none\n * when no method does). [] when there is no operation. Pass the chosen entry to setOrderFulfillment as `time`.\n * GET /restaurants-operations/v1/operations/{id}/first-available-time-slot-per-fulfillment-type[?deliveryAddress.*]\n * GET /restaurants-operations/v1/operations/{id}/available-time-slots-per-date?date.day=&date.month=&date.year=[&deliveryAddress.*]\n * deliveryAddress.addressLine / .city / .postalCode / .country / .subdivision\n */\nexport async function fetchFulfillmentTimes(dayKey, address) {\n const { operationId, timeZone } = await resolveOrdering();\n if (!operationId)\n return [];\n const base = `/restaurants-operations/v1/operations/${operationId}`;\n const where = slotsAddressQuery(address, address ? toRestaurantRegion(await listFulfillmentRaws()) : { country: \"\", subdivision: \"\" });\n const [asap, slots] = await Promise.all([\n wixRequest(`${base}/first-available-time-slot-per-fulfillment-type`, { method: \"GET\", query: where }),\n wixRequest(`${base}/available-time-slots-per-date`, { method: \"GET\", query: { ...slotsDateQuery(dayKey ?? restaurantToday(timeZone)), ...where } }),\n ]);\n return toFulfillmentTimes(asap, slots);\n}\n/**\n * Set how and when the order is fulfilled: pickup or delivery, a time from fetchFulfillmentTimes\n * (omitted → ASAP), and for delivery the address (street, city; country and subdivision default to\n * the restaurant's). Pickup puts the restaurant's country on the cart, which Wix requires before\n * any delivery method. Throws NOT_DELIVERABLE when the address is outside every delivery area.\n * PATCH /ecom/v2/carts/current { cart: { deliveryInfo: { address: { addressLine, city, postalCode, country, subdivision } } } }\n * (the street field is addressLine; addressLine1 is dropped)\n * POST /ecom/v2/carts/current/set-delivery-method { deliveryMethod: { code: \"PICKUP|ASAP\" | \"<TYPE>|<startMs>|<endMs>\" } }\n */\nexport async function setOrderFulfillment(choice) {\n const address = fulfillmentAddress(choice, toRestaurantRegion(await listFulfillmentRaws()));\n await wixRequest(CART, { method: \"PATCH\", body: { cart: { deliveryInfo: { address } } } });\n try {\n const res = await wixRequest(`${CART}/set-delivery-method`, { body: { deliveryMethod: { code: fulfillmentCode(choice) } } });\n return readCartWithSubtotal(res?.cart ?? null);\n }\n catch (e) {\n if (e instanceof WixApiError && e.code === \"DELIVERY_METHOD_NOT_FOUND\") {\n throw new Error(choice.type === \"DELIVERY\" ? NOT_DELIVERABLE : \"This pickup time isn't available. Choose another time.\");\n }\n throw e;\n }\n}\n/**\n * Start the Wix-hosted checkout for the current order and return the URL to navigate the FULL\n * document to. The cart's id IS the checkout id. The fulfillment must be on the cart first\n * (setOrderFulfillment); a cart with none gets pickup ASAP. Payment is collected on the hosted\n * page. `origin` must be the site's real https origin as registered on the OAuth app's allowed\n * domains (browser: window.location.origin) — an unlisted or http origin 403s on return.\n * POST /headless/v1/redirect-session { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl, thankYouPageUrl } }\n */\nexport async function orderCheckoutUrl(origin = typeof window !== \"undefined\" ? window.location.origin : \"\") {\n const raw = await getCurrentCartRaw();\n assertOrderCheckoutable(raw);\n if (!raw?.deliveryInfo?.method?.code) {\n await setOrderFulfillment({ type: \"PICKUP\" }).catch(() => {\n throw new Error(\"Choose pickup or delivery before checkout.\");\n });\n }\n const res = await wixRequest(\"/headless/v1/redirect-session\", {\n body: { ecomCheckout: { checkoutId: rawId(raw) }, callbacks: origin ? { postFlowUrl: `${origin}/`, thankYouPageUrl: `${origin}/` } : {} },\n });\n const url = res?.redirectSession?.fullUrl;\n if (!url)\n throw new Error(\"Checkout couldn't start: no redirect URL returned.\");\n return url;\n}\n",
21 "js/wix/reservation-store.js": "// Declarations of reservation-store.ts: what this file exports, their arguments and what they return.\n// import type { ReservationConfirmation, ReservationHold, ReservationLocationInfo, ReservationReservee, ReservationSlot } from \"./restaurants-types.js\";\n// export interface ReservationState {\n// locations: ReservationLocationInfo[] | null;\n// location: ReservationLocationInfo | null;\n// date: string;\n// time: string;\n// partySize: number;\n// approval: \"AUTOMATIC\" | \"MANUAL\";\n// slots: ReservationSlot[] | null;\n// selectedSlot: ReservationSlot | null;\n// held: ReservationHold | null;\n// holdSecondsLeft: number | null;\n// reservee: ReservationReservee;\n// fieldErrors: Record<string, string>;\n// canConfirm: boolean;\n// confirmed: ReservationConfirmation | null;\n// loading: boolean;\n// error: string | null;\n// }\n// export interface ReservationStore {\n// getState(): ReservationState;\n// subscribe(listener: (state: ReservationState) => void): () => void;\n// start(): void;\n// stop(): void;\n// setLocationId(id: string): void;\n// setDate(date: string): void;\n// setTime(time: string): void;\n// setPartySize(size: number): void;\n// findSlots(): Promise<void>;\n// holdSlot(slot: ReservationSlot): Promise<void>;\n// setReserveeField(field: \"firstName\" | \"lastName\" | \"phone\" | \"email\", value: string): void;\n// setMarketingConsent(value: boolean): void;\n// setCustomField(id: string, value: string): void;\n// confirm(): Promise<void>;\n// reset(): void;\n// }\n// export declare function createReservationStore(): ReservationStore;\n// The table-reservation state machine as a framework-free store — the logic behind useReservation,\n// usable from React (useReservation wraps it), from a static page or Vue/Svelte, or as the\n// specification for a port. Location, date + party size (in the LOCATION's timezone), AVAILABLE\n// slots, then one of two paths Wix's own code takes: AUTOMATIC approval → a 10-minute hold with a\n// countdown → the details form → reserve; MANUAL approval (or a large party) → the details form →\n// one createReservation, no hold. All correctness (AVAILABLE-only slots, hold → reserve with the\n// revision, the form rules from the location's configuration, the real status) lives in the data\n// layer; this store orchestrates. Browser-only: availability is time-specific. One store per\n// mounted surface: createReservationStore(), `start()` when mounted, `stop()` when unmounted.\nimport { completeReservation, fetchReservationLocations, fetchReservationSlots, holdReservation, requestReservation } from \"./reservations.js\";\nimport { holdSecondsLeft as secondsLeft, resolveApproval, validateReservee } from \"./reservations-core.js\";\nimport { zonedDayKey, zonedIso } from \"./time-core.js\";\nexport function createReservationStore() {\n let locations = null;\n let locationId = null;\n let date = zonedDayKey(new Date(), \"\");\n let time = \"19:00\";\n let partySize = 2;\n let slots = null;\n let selectedSlot = null;\n let held = null;\n let holdSecondsLeft = null;\n let reservee = { firstName: \"\", phone: \"\", lastName: \"\", email: \"\", customFields: {} };\n let confirmed = null;\n let loading = false;\n let error = null;\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 const currentLocation = () => locations?.find((l) => l.id === locationId) ?? locations?.[0] ?? null;\n const approvalFor = (loc) => (loc ? resolveApproval(loc, partySize) : \"AUTOMATIC\");\n const clearTimer = () => {\n if (timer)\n clearInterval(timer);\n timer = null;\n holdSecondsLeft = null;\n };\n const startTimer = () => {\n clearTimer();\n if (!held)\n return;\n holdSecondsLeft = secondsLeft(held);\n timer = setInterval(() => {\n if (!held)\n return clearTimer();\n holdSecondsLeft = secondsLeft(held);\n if (holdSecondsLeft <= 0) {\n // The hold lapsed: the reservation can't be reserved any more — back to slot picking.\n held = null;\n selectedSlot = null;\n clearTimer();\n error = \"The hold expired — pick a time again.\";\n }\n emit();\n }, 1000);\n };\n /** Apply a (new) location: date in its zone, party clamped to its bounds, the form's defaults. */\n const adoptLocation = (loc) => {\n if (!loc)\n return;\n date = zonedDayKey(new Date(), loc.timeZone);\n partySize = Math.min(Math.max(partySize, loc.partySizeMin), Math.max(loc.partySizeMax, loc.partySizeMin));\n reservee = { ...reservee, marketingConsent: loc.form.marketingCheckbox ? loc.form.marketingCheckbox.checkedByDefault : undefined };\n };\n function getState() {\n if (snapshot)\n return snapshot;\n const location = currentLocation();\n const approval = approvalFor(location);\n const check = location ? validateReservee(reservee, location.form) : { ok: false, errors: {} };\n const picked = approval === \"MANUAL\" ? !!selectedSlot : !!held;\n snapshot = {\n locations,\n location,\n date,\n time,\n partySize,\n approval,\n slots,\n selectedSlot,\n held,\n holdSecondsLeft,\n reservee,\n fieldErrors: check.errors,\n canConfirm: picked && check.ok && !loading,\n confirmed,\n loading,\n error,\n };\n return snapshot;\n }\n const fail = (e) => (e instanceof Error ? e.message : String(e));\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"reservation-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 fetchReservationLocations()\n .then((locs) => {\n if (!started)\n return;\n locations = locs;\n locationId ??= locs[0]?.id ?? null;\n adoptLocation(currentLocation());\n emit();\n })\n .catch((e) => {\n if (!started)\n return;\n locations = [];\n error = fail(e);\n emit();\n });\n },\n stop() {\n started = false;\n clearTimer();\n },\n setLocationId(id) {\n if (id === locationId)\n return;\n locationId = id;\n slots = null;\n selectedSlot = null;\n held = null;\n clearTimer();\n adoptLocation(currentLocation());\n emit();\n },\n setDate(next) { date = next; emit(); },\n setTime(next) { time = next; emit(); },\n setPartySize(size) {\n const loc = currentLocation();\n const min = loc?.partySizeMin ?? 1;\n const max = Math.max(loc?.partySizeMax ?? 1, min);\n partySize = Math.min(Math.max(size, min), max);\n emit();\n },\n async findSlots() {\n const loc = currentLocation();\n if (!loc)\n return;\n error = null;\n slots = null;\n selectedSlot = null;\n held = null;\n confirmed = null;\n loading = true;\n clearTimer();\n emit();\n try {\n // The anchor is the restaurant's wall clock, not the visitor's.\n const aroundIso = zonedIso(date, time, loc.timeZone);\n if (!aroundIso)\n throw new Error(\"Pick a date and a time.\");\n slots = await fetchReservationSlots(loc.id, aroundIso, partySize, { timeZone: loc.timeZone });\n }\n catch (e) {\n slots = [];\n error = fail(e);\n }\n finally {\n loading = false;\n emit();\n }\n },\n async holdSlot(slot) {\n const loc = currentLocation();\n if (!loc)\n return;\n error = null;\n // Manual approval (the location's mode, or the slot says so): no hold — the form leads to a request.\n if (approvalFor(loc) === \"MANUAL\" || slot.manualApproval) {\n selectedSlot = slot;\n held = null;\n clearTimer();\n emit();\n return;\n }\n loading = true;\n emit();\n try {\n held = await holdReservation(loc.id, slot.startIso, partySize);\n selectedSlot = slot;\n startTimer();\n }\n catch (e) {\n error = fail(e);\n }\n finally {\n loading = false;\n emit();\n }\n },\n setReserveeField(field, value) {\n reservee = { ...reservee, [field]: value };\n emit();\n },\n setMarketingConsent(value) {\n reservee = { ...reservee, marketingConsent: value };\n emit();\n },\n setCustomField(id, value) {\n reservee = { ...reservee, customFields: { ...(reservee.customFields ?? {}), [id]: value } };\n emit();\n },\n async confirm() {\n const loc = currentLocation();\n if (!loc || !selectedSlot)\n return;\n const manual = approvalFor(loc) === \"MANUAL\" || selectedSlot.manualApproval || !held;\n if (!manual && !held)\n return;\n error = null;\n loading = true;\n emit();\n try {\n confirmed = manual\n ? await requestReservation(loc.id, selectedSlot.startIso, partySize, reservee, loc.form)\n : await completeReservation(held, reservee, loc.form);\n held = null;\n selectedSlot = null;\n clearTimer();\n }\n catch (e) {\n error = fail(e);\n // A validation message keeps the form; anything else on the hold path means the hold is gone.\n if (!manual && !/required|valid|too long|country code/i.test(error)) {\n held = null;\n selectedSlot = null;\n clearTimer();\n }\n }\n finally {\n loading = false;\n emit();\n }\n },\n reset() {\n held = null;\n selectedSlot = null;\n confirmed = null;\n slots = null;\n error = null;\n clearTimer();\n emit();\n },\n };\n}\n",
22 "js/wix/reservations-core.js": "// Declarations of reservations-core.ts: what this file exports, their arguments and what they return.\n// import type { ReservationConfirmation, ReservationFormConfig, ReservationHold, ReservationLocationInfo, ReservationReservee, ReservationSlot, ReservationStatus } from \"./restaurants-types.js\";\n// import type { Raw } from \"./menu-core.js\";\n// export declare const rawId: (raw: Raw | undefined | null) => string;\n// export declare const HOLD_SECONDS: number;\n// export declare function toFormConfig(raw: Raw | undefined): ReservationFormConfig;\n// export declare function toLocation(raw: Raw): ReservationLocationInfo;\n// export declare function toLocations(raws: Raw[]): ReservationLocationInfo[];\n// export declare function resolveApproval(location: Pick<ReservationLocationInfo, \"approvalMode\" | \"manualApprovalPartySizeThreshold\">, partySize: number): \"AUTOMATIC\" | \"MANUAL\";\n// export declare function toSlot(raw: Raw, timeZone: string): ReservationSlot;\n// export declare function availableSlots(raws: Raw[], timeZone: string): ReservationSlot[];\n// export declare function toHold(res: Raw | null | undefined, startIso: string, partySize: number): ReservationHold;\n// export declare function holdSecondsLeft(hold: Pick<ReservationHold, \"expiresAtIso\">, now?: Date): number;\n// export declare const normalizePhone: (phone: string | undefined) => string;\n// export declare function validateReservee(reservee: ReservationReservee, form: ReservationFormConfig): {\n// ok: boolean;\n// errors: Record<string, string>;\n// };\n// export declare function reserveeBody(reservee: ReservationReservee, form: ReservationFormConfig): Raw;\n// export declare function reservationBody(locationId: string, startIso: string, partySize: number, reservee: ReservationReservee, form: ReservationFormConfig): Raw;\n// export declare function confirmationOutcome(status: ReservationStatus): ReservationConfirmation[\"outcome\"];\n// export declare function toConfirmation(res: Raw | null | undefined): ReservationConfirmation;\nimport { zonedDayKey, zonedTimeLabel } from \"./time-core.js\";\nexport const rawId = (raw) => raw?._id ?? raw?.id ?? \"\";\n/** A hold lasts 10 minutes from its creation (Wix's own countdown: 600 s from _createdDate). */\nexport const HOLD_SECONDS = 10 * 60;\n// ---- locations -----------------------------------------------------------------------------------------------\nconst toPolicy = (raw) => raw && raw.enabled !== false && (raw.url || raw.text) ? { url: raw.url ?? \"\", text: raw.text ?? \"\" } : null;\n/** The owner's form configuration (reservationForm) → what to render and what to require. */\nexport function toFormConfig(raw) {\n const f = raw ?? {};\n return {\n lastNameRequired: f.lastNameRequired === true,\n emailRequired: f.emailRequired === true,\n marketingCheckbox: f.emailMarketingCheckbox?.enabled === true ? { checkedByDefault: f.emailMarketingCheckbox.checkedByDefault === true } : null,\n customFields: (f.customFieldDefinitions ?? [])\n .map((d) => ({ id: rawId(d), name: d.name ?? \"\", required: d.required === true }))\n .filter((d) => d.id),\n terms: toPolicy(f.termsAndConditions),\n privacy: toPolicy(f.privacyPolicy),\n submitMessage: f.submitMessage ?? \"\",\n };\n}\nexport function toLocation(raw) {\n const online = raw.configuration?.onlineReservations ?? {};\n const approval = online.approval ?? {};\n const mode = approval.mode === \"MANUAL\" || approval.mode === \"MANUAL_FOR_LARGE_PARTIES\" ? approval.mode : \"AUTOMATIC\";\n const threshold = approval.manualForLargePartiesOptions?.partySizeThreshold;\n return {\n id: rawId(raw),\n name: raw.location?.name ?? \"\",\n address: raw.location?.address?.formatted ?? \"\",\n timeZone: raw.location?.timeZone ?? \"\",\n default: raw.default === true,\n partySizeMin: online.partySize?.min ?? 1,\n partySizeMax: online.partySize?.max ?? 1, // Wix's default when the owner set no maximum\n approvalMode: mode,\n manualApprovalPartySizeThreshold: mode === \"MANUAL_FOR_LARGE_PARTIES\" && typeof threshold === \"number\" ? threshold : null,\n onlineReservationsEnabled: online.onlineReservationsEnabled === true,\n form: toFormConfig(raw.configuration?.reservationForm),\n };\n}\n/**\n * The locations to offer, Wix's rule: only those with online reservations ENABLED (the default\n * one first when it is enabled); when none is enabled, only the default location, so the surface\n * can say \"online reservations aren't open\" instead of listing dead options. [] when none exist.\n */\nexport function toLocations(raws) {\n const all = raws.filter((l) => l.archived !== true).map(toLocation).filter((l) => l.id);\n const enabled = all.filter((l) => l.onlineReservationsEnabled);\n if (enabled.length)\n return enabled.sort((a, b) => Number(b.default) - Number(a.default));\n const def = all.find((l) => l.default);\n return def ? [def] : [];\n}\n/** AUTOMATIC → hold then reserve; MANUAL → one createReservation, no hold (also MANUAL_FOR_LARGE_PARTIES from the threshold up). */\nexport function resolveApproval(location, partySize) {\n if (location.approvalMode === \"MANUAL\")\n return \"MANUAL\";\n if (location.approvalMode === \"MANUAL_FOR_LARGE_PARTIES\" && partySize >= (location.manualApprovalPartySizeThreshold ?? 0))\n return \"MANUAL\";\n return \"AUTOMATIC\";\n}\n// ---- slots ----------------------------------------------------------------------------------------------------------\nconst toDate = (v) => (v instanceof Date ? v : new Date(String(v)));\n/** A raw slot → DTO with its label and day key in the LOCATION's timezone. */\nexport function toSlot(raw, timeZone) {\n const start = toDate(raw.startDate);\n return {\n startIso: start.toISOString(),\n label: zonedTimeLabel(start, timeZone),\n dayKey: zonedDayKey(start, timeZone),\n durationMinutes: raw.duration ?? 0,\n manualApproval: raw.manualApproval === true,\n };\n}\n/** AVAILABLE slots only — UNAVAILABLE and NON_WORKING_HOURS are dropped (offering them makes the hold fail). */\nexport function availableSlots(raws, timeZone) {\n return raws.filter((s) => s.status === \"AVAILABLE\").map((s) => toSlot(s, timeZone));\n}\n// ---- hold ------------------------------------------------------------------------------------------------------------\n/** The hold's { reservationId, revision } that completeReservation NEEDS, plus its expiry; throws when the slot wasn't held. */\nexport function toHold(res, startIso, partySize) {\n const reservationId = rawId(res?.reservation);\n const revision = res?.reservation?.revision;\n if (!reservationId || !revision) {\n throw new Error(\"That time couldn't be held — it may have just been taken. Pick another slot.\");\n }\n const created = res?.reservation?._createdDate ?? res?.reservation?.createdDate;\n const createdMs = created ? toDate(created).getTime() : NaN;\n const expiresAt = new Date((Number.isFinite(createdMs) ? createdMs : Date.now()) + HOLD_SECONDS * 1000);\n return { reservationId, revision: String(revision), startIso, partySize, expiresAtIso: expiresAt.toISOString() };\n}\n/** Whole seconds until the hold lapses (0 when it has). */\nexport function holdSecondsLeft(hold, now = new Date()) {\n return Math.max(0, Math.ceil((new Date(hold.expiresAtIso).getTime() - now.getTime()) / 1000));\n}\n// ---- reservee form ------------------------------------------------------------------------------------------\nconst STRING_LIMIT = 200; // Wix's COMMON_STRING_LIMIT: every text field shorter than this\nconst EMAIL = /^(([^<>()[\\].,;:\\s@\"]+(\\.[^<>()[\\].,;:\\s@\"]+)*)|(\".+\"))@(([^<>()[\\].,;:\\s@\"]+\\.)+[^<>()[\\].,;:\\s@\"]{2,})$/i;\n// E.164-shaped after stripping formatting: optional +, 7-15 digits, no leading 0 (no libphonenumber dependency).\nconst PHONE = /^\\+?[1-9]\\d{6,14}$/;\nconst okString = (value, required) => {\n const v = (value ?? \"\").trim();\n if (!v)\n return !required;\n return v.length < STRING_LIMIT;\n};\n/** The visitor's phone with spaces, dashes, dots, and parentheses removed — what gets validated and sent. */\nexport const normalizePhone = (phone) => (phone ?? \"\").replace(/[\\s().-]/g, \"\");\n/**\n * Validate the reservee against the location's form config, Wix's rules: firstName and phone\n * always required; lastName/email per config; email by pattern; phone E.164-shaped; every text\n * under 200 characters; each required custom field filled. Empty `errors` means submittable.\n */\nexport function validateReservee(reservee, form) {\n const errors = {};\n if (!okString(reservee.firstName, true))\n errors.firstName = \"First name is required.\";\n if (!okString(reservee.lastName, form.lastNameRequired))\n errors.lastName = form.lastNameRequired ? \"Last name is required.\" : \"Last name is too long.\";\n const phone = normalizePhone(reservee.phone);\n if (!phone)\n errors.phone = \"Phone number is required.\";\n else if (!PHONE.test(phone))\n errors.phone = \"Enter a phone number with its country code, e.g. +15551234567.\";\n const email = (reservee.email ?? \"\").trim();\n if (!email && form.emailRequired)\n errors.email = \"Email is required.\";\n else if (email && !(EMAIL.test(email) && email.length < STRING_LIMIT))\n errors.email = \"Enter a valid email address.\";\n for (const f of form.customFields) {\n if (!okString(reservee.customFields?.[f.id], f.required))\n errors[`custom:${f.id}`] = f.required ? `${f.name || \"This field\"} is required.` : `${f.name || \"This field\"} is too long.`;\n }\n return { ok: Object.keys(errors).length === 0, errors };\n}\n/**\n * The reservee body of a reserve/create: validated against the form, trimmed, optional fields\n * only when filled, `marketingConsent` only when the form shows the checkbox, `customFields` as\n * { id: answer } for the filled ones. Throws the first validation message.\n */\nexport function reserveeBody(reservee, form) {\n const check = validateReservee(reservee, form);\n if (!check.ok)\n throw new Error(Object.values(check.errors)[0]);\n const customFields = Object.fromEntries(form.customFields.map((f) => [f.id, (reservee.customFields?.[f.id] ?? \"\").trim()]).filter(([, v]) => v));\n return {\n firstName: reservee.firstName.trim(),\n phone: normalizePhone(reservee.phone),\n ...(reservee.lastName?.trim() ? { lastName: reservee.lastName.trim() } : {}),\n ...(reservee.email?.trim() ? { email: reservee.email.trim() } : {}),\n ...(form.marketingCheckbox ? { marketingConsent: reservee.marketingConsent === true } : {}),\n ...(Object.keys(customFields).length ? { customFields } : {}),\n };\n}\n/** The body of a MANUAL-approval createReservation: details + reservee, no hold. */\nexport function reservationBody(locationId, startIso, partySize, reservee, form) {\n if (!locationId || !startIso || !partySize)\n throw new Error(\"Pick a time before sending the request.\");\n return {\n details: { reservationLocationId: locationId, startDate: startIso, partySize },\n reservee: reserveeBody(reservee, form),\n };\n}\n// ---- outcome -------------------------------------------------------------------------------------------------------\nconst STATUSES = [\"HELD\", \"RESERVED\", \"REQUESTED\", \"PAYMENT_INFORMATION_PENDING\", \"CANCELED\", \"DECLINED\", \"FINISHED\", \"NO_SHOW\", \"SEATED\"];\n/** RESERVED is the only confirmed status; REQUESTED and PAYMENT_INFORMATION_PENDING are pending; the rest are other. */\nexport function confirmationOutcome(status) {\n if (status === \"RESERVED\")\n return \"confirmed\";\n if (status === \"REQUESTED\" || status === \"PAYMENT_INFORMATION_PENDING\")\n return \"pending\";\n return \"other\";\n}\n/** The API's real status, unmapped. Throws when no reservation came back (an expired hold). */\nexport function toConfirmation(res) {\n const reservation = res?.reservation;\n const reservationId = rawId(reservation);\n if (!reservationId) {\n throw new Error(\"The reservation couldn't be completed — the hold may have expired. Start over.\");\n }\n const raw = String(reservation?.status ?? \"\");\n const status = STATUSES.includes(raw) ? raw : \"UNKNOWN\";\n return { reservationId, status, outcome: confirmationOutcome(status) };\n}\n",
23 "js/wix/reservations.js": "// Declarations of reservations.ts: what this file exports, their arguments and what they return.\n// import type { ReservationConfirmation, ReservationFormConfig, ReservationHold, ReservationLocationInfo, ReservationReservee, ReservationSlot } from \"./restaurants-types.js\";\n// export declare function fetchReservationLocations(): Promise<ReservationLocationInfo[]>;\n// export declare function fetchReservationSlots(locationId: string, aroundIso: string, partySize: number, { timeZone, slotsBefore, slotsAfter }?: {\n// timeZone?: string;\n// slotsBefore?: number;\n// slotsAfter?: number;\n// }): Promise<ReservationSlot[]>;\n// export declare function holdReservation(locationId: string, startIso: string, partySize: number): Promise<ReservationHold>;\n// export declare function completeReservation(hold: Pick<ReservationHold, \"reservationId\" | \"revision\">, reservee: ReservationReservee, form: ReservationFormConfig): Promise<ReservationConfirmation>;\n// export declare function requestReservation(locationId: string, startIso: string, partySize: number, reservee: ReservationReservee, form: ReservationFormConfig): Promise<ReservationConfirmation>;\n// Table reservations over REST — the twin of app/wix/restaurants/reservations.ts. Same exports,\n// same DTOs; rules and mappers from reservations-core (the SAME file the SDK transport uses,\n// deployed flat next to this one). The flow is location → AVAILABLE slots → (AUTOMATIC approval)\n// hold → reserve, or (MANUAL approval) one create with details + reservee and no hold; a\n// reservation is a hold, not a purchase — no cart, no checkout. Every call here is a visitor's own\n// action with the visitor token. The hold, reserve, and create calls are premium-gated on the site:\n// a non-premium site answers 428 \"site must be premium\" — surface it, don't retry.\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/time-slots/get-time-slots.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservations/create-held-reservation.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservations/reserve-reservation.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservations/create-reservation.md\nimport { wixRequest } from \"./client.js\";\nimport { availableSlots, reservationBody, reserveeBody, toConfirmation, toHold, toLocations } from \"./reservations-core.js\";\nconst RESERVATIONS = \"/table-reservations/reservations/v1\";\n/**\n * The locations to offer: those with online reservations ENABLED (default first), or only the\n * default one when none is enabled (then `onlineReservationsEnabled: false` → render an honest\n * notice); [] when Table Reservations isn't set up. Each carries name, address, timezone, approval\n * rule, and the owner's form configuration.\n * GET /table-reservations/reservation-locations/v1/reservation-locations\n */\nexport async function fetchReservationLocations() {\n const res = await wixRequest(\"/table-reservations/reservation-locations/v1/reservation-locations\", { method: \"GET\" });\n return toLocations(res?.reservationLocations ?? []);\n}\n/**\n * AVAILABLE slots around a moment for a party size (UNAVAILABLE / NON_WORKING_HOURS dropped);\n * `aroundIso` is built in the location's zone (`zonedIso`), labels and day keys use `timeZone`.\n * POST /table-reservations/reservations/v1/time-slots { reservationLocationId, date, partySize, slotsBefore, slotsAfter }\n */\nexport async function fetchReservationSlots(locationId, aroundIso, partySize, { timeZone = \"\", slotsBefore = 6, slotsAfter = 6 } = {}) {\n const res = await wixRequest(`${RESERVATIONS}/time-slots`, {\n body: { reservationLocationId: locationId, date: new Date(aroundIso).toISOString(), partySize, slotsBefore, slotsAfter },\n });\n return availableSlots(res?.timeSlots ?? [], timeZone);\n}\n/**\n * Hold a slot for 10 minutes while the visitor enters their details (AUTOMATIC approval); the hold's\n * { reservationId, revision } feed completeReservation, `expiresAtIso` a countdown.\n * POST /table-reservations/reservations/v1/reservations/hold { reservationDetails: { reservationLocationId, startDate, partySize } }\n */\nexport async function holdReservation(locationId, startIso, partySize) {\n const res = await wixRequest(`${RESERVATIONS}/reservations/hold`, {\n body: { reservationDetails: { reservationLocationId: locationId, startDate: new Date(startIso).toISOString(), partySize } },\n });\n return toHold(res, startIso, partySize);\n}\n/**\n * Complete a held reservation with the visitor's details, validated against the location's `form`\n * (firstName + phone always; lastName/email/custom fields as configured); an expired hold can't be\n * reserved — start a fresh hold. The outcome carries the API's real status.\n * POST /table-reservations/reservations/v1/reservations/{reservationId}/reserve\n * { revision, reservee: { firstName, phone, lastName?, email?, marketingConsent?, customFields? } }\n */\nexport async function completeReservation(hold, reservee, form) {\n const body = reserveeBody(reservee, form);\n const res = await wixRequest(`${RESERVATIONS}/reservations/${encodeURIComponent(hold.reservationId)}/reserve`, {\n body: { revision: hold.revision, reservee: body },\n });\n return toConfirmation(res);\n}\n/**\n * Request a reservation that needs the restaurant's approval (MANUAL, or a large party under\n * MANUAL_FOR_LARGE_PARTIES): ONE create with details + reservee, no hold. Normally ends REQUESTED (pending).\n * POST /table-reservations/reservations/v1/reservations\n * { reservation: { details: { reservationLocationId, startDate, partySize }, reservee: { … } } }\n */\nexport async function requestReservation(locationId, startIso, partySize, reservee, form) {\n const reservation = reservationBody(locationId, new Date(startIso).toISOString(), partySize, reservee, form);\n const res = await wixRequest(`${RESERVATIONS}/reservations`, { body: { reservation } });\n return toConfirmation(res);\n}\n",
24 "js/wix/restaurants-types.js": "// Declarations of restaurants-types.ts: what this file exports, their arguments and what they return.\n// /** The site's currency + locale, from the eCommerce settings. `currency` \"\" when unknown. */\n// export interface SiteMoney {\n// /** ISO 4217, e.g. \"USD\"; \"\" when the settings could not be read (prices then format to \"\"). */\n// currency: string;\n// /** BCP 47, e.g. \"en-US\"; \"\" → the runtime's default locale. */\n// locale: string;\n// }\n// /** A dietary/style label on a menu item (e.g. \"Vegan\", \"Spicy\"). */\n// export interface MenuItemLabel {\n// id: string;\n// name: string;\n// /** Resolved https URL (\"\" when the label has no icon). */\n// iconUrl: string;\n// }\n// /** One price variant of an item (\"Small\" / \"Large\"). An item has price OR variants. */\n// export interface MenuItemVariant {\n// variantId: string;\n// name: string;\n// /** Formatted price (\"\" when the currency is unknown). */\n// price: string;\n// /** Decimal amount, site currency, no symbol (\"9.50\"). */\n// priceAmount: string;\n// }\n// /** One choice inside a modifier group (\"Extra cheese\"). */\n// export interface MenuModifier {\n// /** The modifier entity id — what the cart line carries. The same id may appear twice in one group. */\n// id: string;\n// /** Unique within the group: `${id}~${index}` — the selection key (duplicate ids stay distinct). */\n// key: string;\n// name: string;\n// preSelected: boolean;\n// /** Formatted up-charge (\"\" when free or the currency is unknown). */\n// additionalCharge: string;\n// /** Decimal up-charge amount (\"0\" = free), site currency, no symbol. */\n// additionalChargeAmount: string;\n// inStock: boolean;\n// }\n// /**\n// * The shape of a group's rule, derived from required/min/max the way Wix's ordering code does:\n// * NO_LIMIT (optional, any count) · CHOOSE_ONE (exactly or up to one) · CHOOSE_X (required, min = max)\n// * · AT_LEAST_ONE · AT_LEAST_X · UP_TO_X (optional, max > 1) · BETWEEN_X_AND_Y (required, min < max).\n// */\n// export type ModifierRuleType = \"NO_LIMIT\" | \"CHOOSE_ONE\" | \"CHOOSE_X\" | \"CHOOSE_AT_LEAST_ONE\" | \"CHOOSE_AT_LEAST_X\" | \"CHOOSE_UP_TO_X\" | \"CHOOSE_BETWEEN_X_AND_Y\";\n// /** A modifier group on an item (\"Toppings\", choose 0–3). Selections are sent on the cart line. */\n// export interface MenuModifierGroup {\n// id: string;\n// name: string;\n// required: boolean;\n// minSelections: number;\n// maxSelections: number | null;\n// rule: ModifierRuleType;\n// /** required && min 1 && max 1 → render radios (one selection replaces the other); else checkboxes. */\n// singleSelect: boolean;\n// modifiers: MenuModifier[];\n// }\n// /** A dish/drink as the menu page needs it. */\n// export interface MenuItem {\n// id: string;\n// name: string;\n// description: string;\n// /** Formatted price; null when the item is variant-priced or market-priced. */\n// price: string | null;\n// /** Decimal base price (\"12.50\"); null when variant-priced or market-priced. */\n// priceAmount: string | null;\n// /** True → show \"Market price\"; the item can't be ordered online (an ours-only display rule: owners use \"no price\" for market price). */\n// marketPrice: boolean;\n// /** Non-empty exactly when price is null and not marketPrice. The cheapest is the default selection. */\n// variants: MenuItemVariant[];\n// /** Resolved https URL (\"\" when the item has no image). */\n// imageUrl: string;\n// /** The main image followed by the additional images, resolved (may be empty). */\n// gallery: string[];\n// labels: MenuItemLabel[];\n// modifierGroups: MenuModifierGroup[];\n// /** The item's own stock flag (orderSettings.inStock). */\n// inStock: boolean;\n// /** !inStock, OR a required group can't be satisfied from in-stock modifiers → render \"Sold out\". */\n// soldOut: boolean;\n// /** Can be added online: not market-priced and not sold out. Menu- and operation-level gates are on the order cart. */\n// orderable: boolean;\n// /** The kitchen accepts a free-text note on this item (orderSettings.acceptSpecialRequests). */\n// acceptsSpecialRequests: boolean;\n// featured: boolean;\n// }\n// /** A section of a menu (\"Appetizers\"), items already in display order. */\n// export interface MenuSection {\n// id: string;\n// name: string;\n// description: string;\n// /** Resolved https URL (\"\" when none). */\n// imageUrl: string;\n// items: MenuItem[];\n// }\n// /** A whole menu (\"Dinner\"), sections already in display order. */\n// export interface MenuData {\n// id: string;\n// name: string;\n// description: string;\n// /** URL fragment (urlQueryParam), e.g. \"dinner\". */\n// slug: string;\n// sections: MenuSection[];\n// /** The currency + locale every price on this menu was formatted with — for live price math (`itemPrice`). */\n// money: SiteMoney;\n// }\n// /**\n// * What a visitor chose on a dish before adding it: the variant (variant-priced items only), the\n// * modifier KEYS per group (MenuModifier.key — never the id), and the special request.\n// * Start from `initialSelection(item)`; change with `toggleModifier`; check with `validateSelection`.\n// */\n// export interface OrderSelection {\n// variantId: string | null;\n// /** groupId → selected MenuModifier.key list. */\n// modifiers: Record<string, string[]>;\n// specialRequest: string;\n// }\n// /** The result of validating a selection: one message per group that fails its rule. */\n// export interface SelectionValidation {\n// ok: boolean;\n// /** groupId → message (\"Choose at least 2\", …). */\n// errors: Record<string, string>;\n// }\n// /** The live price of a selection: amount and the formatted string. */\n// export interface SelectionPrice {\n// amount: number;\n// formatted: string;\n// }\n// /** The online-ordering operation's state, resolved once per session. */\n// export interface OrderingStatus {\n// /** The operation to order through; null when the site has none. */\n// operationId: string | null;\n// /** ENABLED accepts orders. DISABLED and PAUSED_UNTIL refuse them; NONE = no operation. */\n// status: \"ENABLED\" | \"DISABLED\" | \"PAUSED_UNTIL\" | \"NONE\";\n// /** When status is PAUSED_UNTIL: the moment ordering resumes (ISO). */\n// pausedUntilIso: string | null;\n// /** The operation's location timezone (for rendering pausedUntilIso), \"\" when unknown. */\n// timeZone: string;\n// /** The fulfillment methods this operation offers (ids) — fetchFulfillmentMethods is limited to them. */\n// fulfillmentIds: string[];\n// defaultFulfillmentType: \"PICKUP\" | \"DELIVERY\" | null;\n// }\n// /** One weekly availability window, wall-clock in the setting's timezone. */\n// export interface WeeklyWindow {\n// day: \"MON\" | \"TUE\" | \"WED\" | \"THU\" | \"FRI\" | \"SAT\" | \"SUN\";\n// /** \"HH:mm\" (24h). An end before the start wraps past midnight. */\n// start: string;\n// end: string;\n// }\n// /**\n// * A menu's ordering settings under the resolved operation (one entry per menu). A menu with no\n// * entry, or `enabled: false`, or outside its availability, isn't orderable — show no add control.\n// */\n// export interface MenuOrderingInfo {\n// menuId: string;\n// enabled: boolean;\n// availability: {\n// /** ALWAYS_AVAILABLE · WEEKLY_SCHEDULE (see `weekly`) · TIMESTAMP_RANGES (see `ranges`). */\n// type: \"ALWAYS_AVAILABLE\" | \"WEEKLY_SCHEDULE\" | \"TIMESTAMP_RANGES\" | \"UNSPECIFIED\";\n// /** IANA zone the windows are expressed in (\"\" → the visitor's). */\n// timeZone: string;\n// weekly: WeeklyWindow[];\n// ranges: {\n// startIso: string;\n// endIso: string;\n// }[];\n// };\n// }\n// /** A pickup/delivery method the operation offers (display: name, fee, minimum). */\n// export interface FulfillmentMethodInfo {\n// id: string;\n// type: \"PICKUP\" | \"DELIVERY\";\n// name: string;\n// /** Formatted fee (\"\" when free or the currency is unknown). */\n// fee: string;\n// feeAmount: string;\n// /** Formatted minimum order (\"\" when none). */\n// minOrderPrice: string;\n// minOrderPriceAmount: string;\n// }\n// /**\n// * A time the order can be fulfilled at, from the operation's time slots. `code` is what the cart's\n// * delivery method takes (\"PICKUP|ASAP\", or \"PICKUP|<startMs>|<endMs>\" for a scheduled slot).\n// */\n// export interface FulfillmentTime {\n// type: \"PICKUP\" | \"DELIVERY\";\n// code: string;\n// asap: boolean;\n// /** ISO start/end (equal for a point in time); render in OrderingStatus.timeZone. */\n// startIso: string;\n// endIso: string;\n// }\n// /**\n// * A delivery address. country (ISO-3166 alpha-2) and subdivision (\"US-CA\") default to the\n// * restaurant's when omitted — delivery is local.\n// */\n// export interface DeliveryAddress {\n// addressLine: string;\n// city: string;\n// postalCode?: string;\n// country?: string;\n// subdivision?: string;\n// }\n// /** The visitor's fulfillment: pickup or delivery, a time (omitted → ASAP), and the address for delivery. */\n// export interface FulfillmentChoice {\n// type: \"PICKUP\" | \"DELIVERY\";\n// time?: FulfillmentTime;\n// address?: DeliveryAddress;\n// }\n// export interface OrderLine {\n// /** The cart line id — what update/remove take (NOT the menu item id). */\n// lineItemId: string;\n// itemName: string;\n// quantity: number;\n// /** Per-unit price, formatted. */\n// unitPrice: string;\n// /** Line total, formatted. */\n// linePrice: string;\n// /** Resolved https URL (\"\" when none). */\n// imageUrl: string;\n// /** Human-readable selection labels the platform attached (variant, modifiers, special request). */\n// descriptionLines: string[];\n// /** Not IN_STOCK → the line can't be checked out as-is. */\n// status: string;\n// }\n// export interface OrderCart {\n// lines: OrderLine[];\n// /** Sum of line quantities. */\n// itemCount: number;\n// /** Formatted subtotal (after discounts) — from the cart estimate, not hand-summed. */\n// subtotal: string;\n// currency: string;\n// /** The fulfillment code set on the cart (\"PICKUP|ASAP\", …); \"\" when none is set yet. */\n// fulfillmentCode: string;\n// }\n// /** A custom question the owner added to the reservation form. */\n// export interface ReservationCustomField {\n// id: string;\n// name: string;\n// required: boolean;\n// }\n// /** A policy link or text the form must show ({ url } or { text }). */\n// export interface ReservationPolicy {\n// url: string;\n// text: string;\n// }\n// /** The owner's reservation form configuration — render the form from it, validate against it. */\n// export interface ReservationFormConfig {\n// /** firstName and phone are ALWAYS required; these two are per configuration. */\n// lastNameRequired: boolean;\n// emailRequired: boolean;\n// /** An email-marketing consent checkbox; null when the owner disabled it. */\n// marketingCheckbox: {\n// checkedByDefault: boolean;\n// } | null;\n// customFields: ReservationCustomField[];\n// /** Terms and conditions to show when enabled; null otherwise. */\n// terms: ReservationPolicy | null;\n// /** Privacy policy to show when enabled; null otherwise. */\n// privacy: ReservationPolicy | null;\n// /** The owner's post-submit message (\"\" when none). */\n// submitMessage: string;\n// }\n// /** A reservation location as the booking form needs it. */\n// export interface ReservationLocationInfo {\n// id: string;\n// name: string;\n// /** Formatted address (\"\" when none). */\n// address: string;\n// /** IANA zone — slot search and labels are computed in it, never in the visitor's. */\n// timeZone: string;\n// default: boolean;\n// /** Bound the party-size input to [partySizeMin, partySizeMax]. */\n// partySizeMin: number;\n// partySizeMax: number;\n// /** \"AUTOMATIC\" confirms instantly; \"MANUAL\" always asks the restaurant; \"MANUAL_FOR_LARGE_PARTIES\" from the threshold up. */\n// approvalMode: \"AUTOMATIC\" | \"MANUAL\" | \"MANUAL_FOR_LARGE_PARTIES\";\n// /** Parties of this size and up need approval (MANUAL_FOR_LARGE_PARTIES); null otherwise. */\n// manualApprovalPartySizeThreshold: number | null;\n// /** False (premium-gated toggle off) → slots/booking won't work; show an honest notice. */\n// onlineReservationsEnabled: boolean;\n// form: ReservationFormConfig;\n// }\n// /** One reservation time slot. */\n// export interface ReservationSlot {\n// /** ISO datetime — pass to holdSlot as-is. */\n// startIso: string;\n// /** Display label in the location's timezone, e.g. \"7:00 PM\". */\n// label: string;\n// /** \"YYYY-MM-DD\" in the location's timezone — for grouping/labeling. */\n// dayKey: string;\n// durationMinutes: number;\n// /** This slot needs staff approval → the reservation is requested (no hold), ending REQUESTED. */\n// manualApproval: boolean;\n// }\n// /** A 10-minute hold on a slot; completeReservation needs BOTH ids. */\n// export interface ReservationHold {\n// reservationId: string;\n// revision: string;\n// startIso: string;\n// partySize: number;\n// /** When the hold lapses (created + 10 minutes) — drive a countdown from it. */\n// expiresAtIso: string;\n// }\n// /** The visitor's details. firstName + phone are ALWAYS required; the rest per ReservationFormConfig. */\n// export interface ReservationReservee {\n// firstName: string;\n// phone: string;\n// lastName?: string;\n// email?: string;\n// /** The marketing checkbox's value; only when the form shows one. */\n// marketingConsent?: boolean;\n// /** custom field id → the visitor's answer. */\n// customFields?: Record<string, string>;\n// }\n// /** Every status the Reservations API can return. */\n// export type ReservationStatus = \"HELD\" | \"RESERVED\" | \"REQUESTED\" | \"PAYMENT_INFORMATION_PENDING\" | \"CANCELED\" | \"DECLINED\" | \"FINISHED\" | \"NO_SHOW\" | \"SEATED\" | \"UNKNOWN\";\n// /** The outcome of completing or requesting a reservation. */\n// export interface ReservationConfirmation {\n// reservationId: string;\n// /** The API's status, unmapped. */\n// status: ReservationStatus;\n// /** confirmed = RESERVED; pending = REQUESTED or PAYMENT_INFORMATION_PENDING; anything else = other. */\n// outcome: \"confirmed\" | \"pending\" | \"other\";\n// }\n// Restaurants 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 money value is a formatted string in the site's currency and\n// locale (read once from the eCommerce settings' BUSINESS_INFO, the way Wix's own menus code\n// does) — \"\" when the currency could not be read; the raw decimal rides beside it in the\n// `*Amount` fields for arithmetic. Order-cart prices (eCom) are formatted from the cart's currency.\nexport {};\n",
25 "js/wix/time-core.js": "// Declarations of time-core.ts: what this file exports, their arguments and what they return.\n// export interface ZonedParts {\n// year: number;\n// month: number;\n// day: number;\n// hour: number;\n// minute: number;\n// second: number;\n// weekday: number;\n// }\n// export declare function zonedParts(date: Date, timeZone: string): ZonedParts;\n// export declare function zonedDayKey(date: Date, timeZone: string): string;\n// export declare function zonedMinutes(date: Date, timeZone: string): number;\n// export declare function zonedIso(dayKey: string, hhmm: string, timeZone: string): string;\n// export declare function zonedTimeLabel(date: Date, timeZone: string, locale?: string): string;\n// export declare function zonedDateTimeLabel(date: Date, timeZone: string, locale?: string): string;\n// Wall-clock arithmetic in a named IANA zone with nothing but Intl — no date-fns-tz. The one\n// place that knows how to turn \"2026-10-03 19:00 in America/New_York\" into an instant and back;\n// ordering-core (menu availability windows in the setting's zone) and reservations-core (slot\n// search and labels in the LOCATION's zone, never the visitor's) both import it. No imports of its\n// own, so a strip to JS emits none; deploy copies every *-core.ts next to the transports.\nconst WEEKDAYS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nconst pad = (n) => String(n).padStart(2, \"0\");\n/** The wall-clock parts of an instant in `timeZone` (\"\" or an unknown zone → the runtime's zone). */\nexport function zonedParts(date, timeZone) {\n try {\n const parts = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: timeZone || undefined,\n hourCycle: \"h23\",\n weekday: \"short\",\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n }).formatToParts(date);\n const get = (type) => parts.find((p) => p.type === type)?.value ?? \"\";\n const hour = Number(get(\"hour\")) % 24; // some engines print \"24\" for midnight\n return {\n year: Number(get(\"year\")),\n month: Number(get(\"month\")),\n day: Number(get(\"day\")),\n hour,\n minute: Number(get(\"minute\")),\n second: Number(get(\"second\")),\n weekday: Math.max(0, WEEKDAYS.indexOf(get(\"weekday\"))),\n };\n }\n catch {\n return {\n year: date.getFullYear(),\n month: date.getMonth() + 1,\n day: date.getDate(),\n hour: date.getHours(),\n minute: date.getMinutes(),\n second: date.getSeconds(),\n weekday: date.getDay(),\n };\n }\n}\n/** \"YYYY-MM-DD\" of an instant in `timeZone`. */\nexport function zonedDayKey(date, timeZone) {\n const p = zonedParts(date, timeZone);\n return `${p.year}-${pad(p.month)}-${pad(p.day)}`;\n}\n/** Minutes since midnight of an instant in `timeZone`. */\nexport function zonedMinutes(date, timeZone) {\n const p = zonedParts(date, timeZone);\n return p.hour * 60 + p.minute;\n}\n/**\n * The instant at which the wall clock in `timeZone` reads `dayKey` (\"YYYY-MM-DD\") `hhmm` (\"HH:mm\"),\n * as an ISO string. Two offset iterations cover DST edges. Invalid input → \"\".\n */\nexport function zonedIso(dayKey, hhmm, timeZone) {\n const [y, m, d] = dayKey.split(\"-\").map(Number);\n const [h, mi] = hhmm.split(\":\").map(Number);\n if (![y, m, d, h, mi].every(Number.isFinite))\n return \"\";\n const target = Date.UTC(y, m - 1, d, h, mi, 0);\n let utc = target;\n for (let i = 0; i < 2; i++) {\n const p = zonedParts(new Date(utc), timeZone);\n const wall = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);\n utc = target - (wall - utc);\n }\n return new Date(utc).toISOString();\n}\n/** A short time label of an instant in `timeZone` in the visitor's language (\"7:00 PM\"); \"\" on failure. */\nexport function zonedTimeLabel(date, timeZone, locale) {\n try {\n return date.toLocaleTimeString(locale || undefined, { hour: \"numeric\", minute: \"2-digit\", timeZone: timeZone || undefined });\n }\n catch {\n try {\n return date.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" });\n }\n catch {\n return \"\";\n }\n }\n}\n/** A short date + time label of an instant in `timeZone` (\"Fri, Oct 3, 7:00 PM\"); \"\" on failure. */\nexport function zonedDateTimeLabel(date, timeZone, locale) {\n try {\n return date.toLocaleString(locale || undefined, {\n weekday: \"short\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n timeZone: timeZone || undefined,\n });\n }\n catch {\n return \"\";\n }\n}\n"