@wix/agent-skills 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -9
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +51 -42
- package/skills/wix-app/references/BACKEND_EVENT.md +10 -65
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +101 -406
- package/skills/wix-app/references/DASHBOARD_MENU_PLUGIN.md +14 -111
- package/skills/wix-app/references/DASHBOARD_MODAL.md +56 -205
- package/skills/wix-app/references/DASHBOARD_PAGE.md +19 -98
- package/skills/wix-app/references/DASHBOARD_PLUGIN.md +6 -108
- package/skills/wix-app/references/DATA_COLLECTION.md +113 -198
- package/skills/wix-app/references/EMBEDDED_SCRIPT.md +41 -268
- package/skills/wix-app/references/EXTENSION_REGISTRATION.md +27 -76
- package/skills/wix-app/references/SERVICE_PLUGIN.md +35 -188
- package/skills/wix-app/references/SITE_PLUGIN.md +8 -92
- package/skills/wix-app/references/STORES_VERSIONING.md +1 -0
- package/skills/wix-app/references/backend-event/COMMON-EVENTS.md +0 -44
- package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +19 -330
- package/skills/wix-app/references/dashboard-menu-plugin/bookings-slots.md +0 -12
- package/skills/wix-app/references/dashboard-menu-plugin/ecommerce-slots.md +7 -0
- package/skills/wix-app/references/dashboard-menu-plugin/restaurants-slots.md +0 -12
- package/skills/wix-app/references/dashboard-plugin/SLOTS.md +3 -15
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +2 -18
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +2 -42
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +1 -11
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +1 -11
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +2 -40
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +2 -19
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +2 -18
- package/skills/wix-app/references/site-plugin/SLOTS.md +17 -68
- package/skills/wix-design-system/SKILL.md +4 -0
- package/skills/wix-headless/SKILL.md +131 -386
- package/skills/wix-headless/references/BUILD-astro.md +246 -0
- package/skills/wix-headless/references/BUILD-own-build.md +167 -0
- package/skills/wix-headless/references/BUILD.md +105 -0
- package/skills/wix-headless/references/DESIGN_SYSTEM.md +175 -0
- package/skills/wix-headless/references/DISCOVERY-connect.md +55 -0
- package/skills/wix-headless/references/DISCOVERY-create.md +238 -0
- package/skills/wix-headless/references/DISCOVERY.md +50 -306
- package/skills/wix-headless/references/PLAN-connect.md +19 -0
- package/skills/wix-headless/references/PLAN-create.md +17 -0
- package/skills/wix-headless/references/PLAN.md +92 -0
- package/skills/wix-headless/references/SEED.md +214 -0
- package/skills/wix-headless/references/SETUP.md +73 -248
- package/skills/wix-headless/references/{blog → astro/blog}/BLOG_PAGES.md +6 -5
- package/skills/wix-headless/references/{blog → astro/blog}/BLOG_SETUP.md +2 -2
- package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +337 -0
- package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +428 -0
- package/skills/wix-headless/references/astro/cms/CMS_FOUNDATIONS.md +214 -0
- package/skills/wix-headless/references/{cms → astro/cms}/FAQ_KNOWLEDGE_BASE.md +14 -12
- package/skills/wix-headless/references/{cms → astro/cms}/PORTFOLIO.md +15 -16
- package/skills/wix-headless/references/{cms → astro/cms}/RESOURCE_LIBRARY.md +13 -15
- package/skills/wix-headless/references/{cms → astro/cms}/TEAM_DIRECTORY.md +11 -11
- package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +354 -0
- package/skills/wix-headless/references/{ecom → astro/ecom}/CART_PAGES.md +8 -9
- package/skills/wix-headless/references/{ecom → astro/ecom}/CART_WIRING.md +6 -6
- package/skills/wix-headless/references/{ecom → astro/ecom}/COMPONENTS_CSS.md +6 -6
- package/skills/wix-headless/references/{forms → astro/forms}/CONTACT_FORM.md +18 -32
- package/skills/wix-headless/references/{gift-cards → astro/gift-cards}/COMPONENTS.md +3 -3
- package/skills/wix-headless/references/{gift-cards → astro/gift-cards}/PAGES.md +2 -2
- package/skills/wix-headless/references/{stores → astro/stores}/BACK_IN_STOCK.md +7 -7
- package/skills/wix-headless/references/{stores → astro/stores}/CATEGORY_PAGES.md +12 -13
- package/skills/wix-headless/references/{stores → astro/stores}/COMPONENTS_CSS.md +7 -7
- package/skills/wix-headless/references/{stores → astro/stores}/HOME_AND_NAV.md +11 -9
- package/skills/wix-headless/references/{stores → astro/stores}/PRODUCT_PAGES.md +11 -9
- package/skills/wix-headless/references/{stores → astro/stores}/SHARED_WIRING.md +8 -7
- package/skills/wix-headless/references/astro/templates/Footer.astro +27 -0
- package/skills/wix-headless/references/astro/templates/Layout.astro +132 -0
- package/skills/wix-headless/references/astro/templates/Navigation.astro +38 -0
- package/skills/wix-headless/references/astro/templates/astro.config.mjs +41 -0
- package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +218 -0
- package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +194 -0
- package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +84 -0
- package/skills/wix-headless/references/astro/templates/bookings/SeoTags.astro +25 -0
- package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +50 -0
- package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +45 -0
- package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +34 -0
- package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +74 -0
- package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +111 -0
- package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +413 -0
- package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +76 -0
- package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +81 -0
- package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +65 -0
- package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +302 -0
- package/skills/wix-headless/references/astro/templates/global.css +286 -0
- package/skills/wix-headless/references/astro/templates/index.astro +56 -0
- package/skills/wix-headless/{templates → references/astro/templates}/stores/back-in-stock.ts +1 -1
- package/skills/wix-headless/{templates → references/astro/templates}/stores/components-stores.css +138 -33
- package/skills/wix-headless/references/blog/BLOG_CONTENT.md +28 -19
- package/skills/wix-headless/references/blog/INSTRUCTIONS.md +6 -6
- package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +106 -0
- package/skills/wix-headless/references/bookings/SERVICES_DATA.md +295 -0
- package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +35 -232
- package/skills/wix-headless/references/cms/INSTRUCTIONS.md +39 -8
- package/skills/wix-headless/references/commands/known-apps.json +2 -1
- package/skills/wix-headless/references/custom/CONNECTION_PLAN.md +142 -0
- package/skills/wix-headless/references/custom/INSTRUCTIONS.md +88 -0
- package/skills/wix-headless/references/custom/blog/WIRING.md +52 -0
- package/skills/wix-headless/references/custom/bookings/WIRING.md +88 -0
- package/skills/wix-headless/references/custom/cms/WIRING.md +97 -0
- package/skills/wix-headless/references/custom/ecom/WIRING.md +67 -0
- package/skills/wix-headless/references/custom/forms/WIRING.md +101 -0
- package/skills/wix-headless/references/custom/stores/WIRING.md +80 -0
- package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +12 -12
- package/skills/wix-headless/references/forms/INSTRUCTIONS.md +6 -6
- package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +13 -13
- package/skills/wix-headless/references/images/INSTRUCTIONS.md +58 -60
- package/skills/wix-headless/references/seed-recipes.md +49 -0
- package/skills/wix-headless/references/shared/AGENTS_MD.md +47 -0
- package/skills/wix-headless/references/shared/AUTHENTICATION.md +108 -0
- package/skills/wix-headless/references/shared/BUILD_NOISE.md +2 -2
- package/skills/wix-headless/references/shared/DESIGN_MD.md +85 -0
- package/skills/wix-headless/references/shared/DOCS_SEARCH.md +41 -0
- package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +40 -39
- package/skills/wix-headless/references/shared/IMPLEMENTER.md +92 -41
- package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +3 -3
- package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +32 -312
- package/skills/wix-headless/references/shared/STYLING.md +48 -21
- package/skills/wix-headless/references/stores/INSTRUCTIONS.md +59 -29
- package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +31 -28
- package/skills/wix-headless/references/verticals/_schema.json +31 -117
- package/skills/wix-headless/references/verticals/_schema.md +60 -150
- package/skills/wix-headless/references/verticals/blog.md +5 -99
- package/skills/wix-headless/references/verticals/bookings.md +41 -0
- package/skills/wix-headless/references/verticals/cms.md +9 -100
- package/skills/wix-headless/references/verticals/ecom.md +6 -77
- package/skills/wix-headless/references/verticals/forms.md +5 -85
- package/skills/wix-headless/references/verticals/gift-cards.md +6 -103
- package/skills/wix-headless/references/verticals/stores.md +10 -144
- package/skills/wix-headless/scripts/check-manifest.mjs +148 -11
- package/skills/wix-headless/scripts/compose.mjs +540 -0
- package/skills/wix-headless/scripts/emit-design-tokens.mjs +136 -53
- package/skills/wix-headless/scripts/flatten.sh +61 -0
- package/skills/wix-headless/scripts/generate-images.mjs +242 -0
- package/skills/wix-headless/scripts/patch-decorative-slots.mjs +67 -43
- package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +3 -3
- package/skills/wix-headless/scripts/scaffold.sh +113 -27
- package/skills/wix-headless/scripts/seed-utilities.sh +50 -12
- package/skills/wix-headless/scripts/write-decorative-json.mjs +62 -0
- package/skills/wix-manage/SKILL.md +4 -8
- package/skills/wix-manage/references/blog/how-to-create-blog-posts.md +2 -0
- package/skills/wix-manage/references/cms/cms-data-items-crud.md +4 -0
- package/skills/wix-manage/references/cms/cms-references-and-relationships.md +2 -0
- package/skills/wix-manage/references/cms/cms-schema-management.md +4 -0
- package/skills/wix-manage/references/domains/domain-search-and-purchase.md +10 -7
- package/skills/wix-manage/references/ecommerce/api-shipping.md +443 -0
- package/skills/wix-manage/references/ecommerce/flow-add-free-shipping.md +5 -6
- package/skills/wix-manage/references/ecommerce/flow-fix-coverage-gaps.md +4 -5
- package/skills/wix-manage/references/ecommerce/flow-optimize-shipping-rates.md +4 -5
- package/skills/wix-manage/references/ecommerce/goal-increase-aov.md +1 -1
- package/skills/wix-manage/references/ecommerce/goal-reduce-cart-abandonment.md +1 -2
- package/skills/wix-manage/references/ecommerce/guardrail-shipping-health.md +2 -2
- package/skills/wix-manage/references/ecommerce/recipe-apply-shipping-recommendations.md +4 -5
- package/skills/wix-manage/references/ecommerce/recommend-ecommerce-strategy.md +7 -7
- package/skills/wix-manage/references/ecommerce/setup-discount-rules.md +209 -72
- package/skills/wix-manage/references/ecommerce/setup-store-pickup-location.md +9 -10
- package/skills/wix-manage/references/ecommerce/skill-graph.md +4 -4
- package/skills/wix-manage/references/forms/create-form.md +50 -0
- package/skills/wix-manage/references/stores/bulk-create-products-with-options.md +2 -0
- package/skills/wix-manage/references/stores/create-product-from-image.md +91 -38
- package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +2 -0
- package/skills/wix-app/references/custom-element-widget/default-thumbnail.png +0 -0
- package/skills/wix-app/references/data-collection/extension-template.ts +0 -107
- package/skills/wix-headless/references/ORCHESTRATION.md +0 -269
- package/skills/wix-headless/references/commands/install-app.md +0 -42
- package/skills/wix-headless/references/commands/mcp-bootstrap.md +0 -28
- package/skills/wix-headless/references/designer/INSTRUCTIONS.md +0 -828
- package/skills/wix-headless/references/shared/MCP_PREFIX.md +0 -105
- package/skills/wix-headless/scripts/copy-utility-templates.mjs +0 -153
- package/skills/wix-headless/scripts/finalize-run-json.mjs +0 -141
- package/skills/wix-headless/scripts/init-site-json.mjs +0 -63
- package/skills/wix-headless/scripts/preview.sh +0 -45
- package/skills/wix-headless/scripts/release.sh +0 -73
- package/skills/wix-headless/templates/ecom/components-ecom.css +0 -215
- package/skills/wix-manage/references/events/list-events.md +0 -203
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/CartBadge.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/CartView.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/cart.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/discounts.ts +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/thank-you.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/GiftCardPurchase.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/_home-teaser-snippet.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/_nav-snippet.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/components-gift-cards.css +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/gift-cards.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/gift-cards.ts +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/AddToCartButton.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/BackInStockForm.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/CategoryRail.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/ProductCard.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/ProductPurchase.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/SeoTags.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/categories.ts +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/category/[slug].astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/products/[slug].astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/products/index.astro +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
// ── Composer skeleton: src/components/Navigation.astro ────────────────────────
|
|
3
|
+
// Substitutions (see compose.mjs):
|
|
4
|
+
// {{shell.navBrandMark}} — brand mark / wordmark text from data.shell.
|
|
5
|
+
// {{nav.links}} — one <a href={href}>{label}</a> per entry in the inlined
|
|
6
|
+
// Navigation links array, labels VERBATIM (no editorial rebrands). Render
|
|
7
|
+
// each inside a <li class="site-nav-item">.
|
|
8
|
+
// Fixed bulk (keep literal): transition:persist, the <!-- nav:links --> marker,
|
|
9
|
+
// the empty <div class="nav-actions"> mount, the mobile hamburger toggle.
|
|
10
|
+
---
|
|
11
|
+
<header class="site-nav" transition:persist="site-nav">
|
|
12
|
+
<nav class="max-w-6xl mx-auto px-lg flex items-center justify-between gap-lg py-md">
|
|
13
|
+
<a href="/" class="font-display text-lg tracking-tight">{{shell.navBrandMark}}</a>
|
|
14
|
+
|
|
15
|
+
<input type="checkbox" id="nav-toggle" class="peer hidden" aria-hidden="true" />
|
|
16
|
+
<label
|
|
17
|
+
for="nav-toggle"
|
|
18
|
+
class="md:hidden inline-flex flex-col gap-[5px] cursor-pointer"
|
|
19
|
+
aria-label="Toggle navigation"
|
|
20
|
+
>
|
|
21
|
+
<span class="block h-[2px] w-6 bg-ink"></span>
|
|
22
|
+
<span class="block h-[2px] w-6 bg-ink"></span>
|
|
23
|
+
<span class="block h-[2px] w-6 bg-ink"></span>
|
|
24
|
+
</label>
|
|
25
|
+
|
|
26
|
+
<div
|
|
27
|
+
class="hidden peer-checked:flex md:flex absolute md:static top-full left-0 right-0 md:right-auto
|
|
28
|
+
flex-col md:flex-row gap-md md:gap-lg items-start md:items-center
|
|
29
|
+
bg-paper md:bg-transparent border-b md:border-0 border-rule px-lg md:px-0 py-md md:py-0"
|
|
30
|
+
>
|
|
31
|
+
<ul class="flex flex-col md:flex-row gap-md md:gap-lg items-start md:items-center">
|
|
32
|
+
{{nav.links}}
|
|
33
|
+
<!-- nav:links -->
|
|
34
|
+
</ul>
|
|
35
|
+
<div class="nav-actions"></div>
|
|
36
|
+
</div>
|
|
37
|
+
</nav>
|
|
38
|
+
</header>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ── Composer skeleton: astro.config.mjs ───────────────────────────────────────
|
|
2
|
+
// UNLIKE the other skeletons, this one is a MERGE TARGET, not a clobber. The
|
|
3
|
+
// scaffold's generated astro.config.mjs varies (integrations args, image.domains,
|
|
4
|
+
// vite.server settings differ run to run). Composer must NOT overwrite it with
|
|
5
|
+
// this file — it applies exactly two mutations to the scaffold's own config and
|
|
6
|
+
// preserves everything else (see compose.mjs):
|
|
7
|
+
//
|
|
8
|
+
// (1) register the Tailwind v4 Vite plugin → vite.plugins: [tailwindcss()]
|
|
9
|
+
// (2) fix the bare `process.env` reference → globalThis guard (TS-safe)
|
|
10
|
+
//
|
|
11
|
+
// The shape below is the canonical post-mutation result for a typical scaffold —
|
|
12
|
+
// use it as the reference for what those two edits should produce.
|
|
13
|
+
// @ts-check
|
|
14
|
+
import { defineConfig } from "astro/config";
|
|
15
|
+
import wix from "@wix/astro";
|
|
16
|
+
import wixPages from "@wix/astro-pages";
|
|
17
|
+
import react from "@astrojs/react";
|
|
18
|
+
import cloudProviderFetchAdapter from "@wix/cloud-provider-fetch-adapter";
|
|
19
|
+
import tailwindcss from "@tailwindcss/vite"; // (1) added by Composer
|
|
20
|
+
|
|
21
|
+
// (2) `process.env` guarded so strict `tsc --noEmit` does not fail with
|
|
22
|
+
// "Cannot find name 'process'" without an @types/node dependency.
|
|
23
|
+
const isBuild =
|
|
24
|
+
(/** @type {any} */ (globalThis)).process?.env?.NODE_ENV === "production";
|
|
25
|
+
|
|
26
|
+
// https://astro.build/config
|
|
27
|
+
export default defineConfig({
|
|
28
|
+
integrations: [wix(), wixPages(), react()],
|
|
29
|
+
security: { checkOrigin: false },
|
|
30
|
+
...(isBuild && { adapter: cloudProviderFetchAdapter({}) }),
|
|
31
|
+
|
|
32
|
+
image: {
|
|
33
|
+
domains: ["static.wixstatic.com"],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
vite: {
|
|
37
|
+
plugins: [tailwindcss()], // (1) merge into any existing vite config
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
output: "server",
|
|
41
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { createClient, OAuthStrategy } from "@wix/sdk";
|
|
3
|
+
import { availabilityTimeSlots, eventTimeSlots, bookings } from "@wix/bookings";
|
|
4
|
+
// Browser client ID comes from astro:env/client (NOT import.meta.env, which is
|
|
5
|
+
// undefined in the browser bundle → OAuthStrategy posts an empty client_id and
|
|
6
|
+
// oauth2/token 400s).
|
|
7
|
+
import { WIX_CLIENT_ID } from "astro:env/client";
|
|
8
|
+
|
|
9
|
+
// AvailabilityCalendar.tsx — client:only="react" island. Date navigator + slot
|
|
10
|
+
// grid. Branches on serviceType:
|
|
11
|
+
// APPOINTMENT → availabilityTimeSlots.listAvailabilityTimeSlots() (1-on-1;
|
|
12
|
+
// slots carry scheduleId + startDate).
|
|
13
|
+
// CLASS → eventTimeSlots.listEventTimeSlots() (group sessions; slots
|
|
14
|
+
// carry eventInfo.eventId + capacity; NO scheduleId).
|
|
15
|
+
// Adapt styling/copy; keep the SDK wiring + slot normalization.
|
|
16
|
+
|
|
17
|
+
export interface SelectedSlot {
|
|
18
|
+
serviceType: "APPOINTMENT" | "CLASS";
|
|
19
|
+
localStartDate: string;
|
|
20
|
+
localEndDate: string;
|
|
21
|
+
serviceId: string;
|
|
22
|
+
timezone: string;
|
|
23
|
+
// APPOINTMENT only:
|
|
24
|
+
scheduleId?: string;
|
|
25
|
+
locationId?: string;
|
|
26
|
+
locationType?: string;
|
|
27
|
+
// CLASS only:
|
|
28
|
+
eventId?: string;
|
|
29
|
+
totalCapacity?: number;
|
|
30
|
+
remainingCapacity?: number;
|
|
31
|
+
bookableCapacity?: number;
|
|
32
|
+
isFull?: boolean; // CLASS full → waitlist
|
|
33
|
+
// resolved on select (CLASS):
|
|
34
|
+
instructorName?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
serviceId: string;
|
|
39
|
+
serviceName: string;
|
|
40
|
+
serviceType: "APPOINTMENT" | "CLASS";
|
|
41
|
+
onSlotSelected: (slot: SelectedSlot) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const wixClient = createClient({
|
|
45
|
+
modules: { availabilityTimeSlots, eventTimeSlots, bookings },
|
|
46
|
+
auth: OAuthStrategy({ clientId: WIX_CLIENT_ID }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
50
|
+
// Local date string YYYY-MM-DDThh:mm:ss (no Z) — API expects local business time.
|
|
51
|
+
const localDateString = (d: Date) =>
|
|
52
|
+
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
53
|
+
const tz = () => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
54
|
+
|
|
55
|
+
const fetchSlots = async (
|
|
56
|
+
serviceId: string,
|
|
57
|
+
serviceType: "APPOINTMENT" | "CLASS",
|
|
58
|
+
date: Date,
|
|
59
|
+
): Promise<{ slots: SelectedSlot[]; timezone: string }> => {
|
|
60
|
+
const start = new Date(date); start.setHours(0, 0, 0, 0);
|
|
61
|
+
const end = new Date(date); end.setHours(23, 59, 59, 0);
|
|
62
|
+
const timeZone = tz();
|
|
63
|
+
|
|
64
|
+
if (serviceType === "CLASS") {
|
|
65
|
+
const result = await wixClient.eventTimeSlots.listEventTimeSlots({
|
|
66
|
+
serviceIds: [serviceId], // plural for the CLASS API
|
|
67
|
+
fromLocalDate: localDateString(start),
|
|
68
|
+
toLocalDate: localDateString(end),
|
|
69
|
+
timeZone,
|
|
70
|
+
includeNonBookable: true, // include full sessions → waitlist CTA
|
|
71
|
+
cursorPaging: { limit: 50 },
|
|
72
|
+
});
|
|
73
|
+
const z = result.timeZone ?? timeZone;
|
|
74
|
+
const slots: SelectedSlot[] = (result.timeSlots ?? []).map((s: any) => ({
|
|
75
|
+
serviceType: "CLASS",
|
|
76
|
+
serviceId,
|
|
77
|
+
localStartDate: s.localStartDate,
|
|
78
|
+
localEndDate: s.localEndDate,
|
|
79
|
+
eventId: s.eventInfo?.eventId, // session id — NO scheduleId on event slots
|
|
80
|
+
totalCapacity: s.totalCapacity ?? undefined,
|
|
81
|
+
remainingCapacity: s.remainingCapacity ?? undefined,
|
|
82
|
+
bookableCapacity: s.bookableCapacity ?? undefined,
|
|
83
|
+
isFull: (s.bookableCapacity ?? s.remainingCapacity ?? 0) <= 0,
|
|
84
|
+
timezone: z,
|
|
85
|
+
}));
|
|
86
|
+
return { slots, timezone: z };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// APPOINTMENT
|
|
90
|
+
const result = await wixClient.availabilityTimeSlots.listAvailabilityTimeSlots({
|
|
91
|
+
serviceId, // a single GUID STRING — NOT an array (the array form is the CLASS `serviceIds` field)
|
|
92
|
+
fromLocalDate: localDateString(start),
|
|
93
|
+
toLocalDate: localDateString(end),
|
|
94
|
+
timeZone,
|
|
95
|
+
bookable: true,
|
|
96
|
+
cursorPaging: { limit: 50 },
|
|
97
|
+
});
|
|
98
|
+
const z = result.timeZone ?? timeZone;
|
|
99
|
+
const slots: SelectedSlot[] = (result.timeSlots ?? []).map((s: any) => ({
|
|
100
|
+
serviceType: "APPOINTMENT",
|
|
101
|
+
serviceId,
|
|
102
|
+
localStartDate: s.localStartDate,
|
|
103
|
+
localEndDate: s.localEndDate,
|
|
104
|
+
scheduleId: s.scheduleId,
|
|
105
|
+
locationId: s.location?.id,
|
|
106
|
+
locationType: s.location?.locationType,
|
|
107
|
+
isFull: false,
|
|
108
|
+
timezone: z,
|
|
109
|
+
}));
|
|
110
|
+
return { slots, timezone: z };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// CLASS instructor lives on the event's resources, which the LIST call omits —
|
|
114
|
+
// only getEventTimeSlot returns them. Resolve for the picked slot.
|
|
115
|
+
const fetchInstructor = async (eventId: string): Promise<string | undefined> => {
|
|
116
|
+
try {
|
|
117
|
+
const { timeSlot } = await wixClient.eventTimeSlots.getEventTimeSlot(eventId);
|
|
118
|
+
const names = (timeSlot?.availableResources ?? [])
|
|
119
|
+
.flatMap((g: any) => g.resources ?? [])
|
|
120
|
+
.map((r: any) => r.name)
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
return names[0];
|
|
123
|
+
} catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const dayLabel = (d: Date) =>
|
|
129
|
+
d.toLocaleDateString([], { weekday: "long", month: "long", day: "numeric" });
|
|
130
|
+
const timeLabel = (iso: string) =>
|
|
131
|
+
new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
132
|
+
const startOfToday = () => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; };
|
|
133
|
+
|
|
134
|
+
const capacityLabel = (s: SelectedSlot): string | null => {
|
|
135
|
+
if (s.serviceType !== "CLASS") return null;
|
|
136
|
+
const left = s.bookableCapacity ?? s.remainingCapacity ?? 0;
|
|
137
|
+
if (left <= 0) return "Full · join waitlist";
|
|
138
|
+
if (left <= 3) return `${left} spot${left === 1 ? "" : "s"} left`;
|
|
139
|
+
return `${left} spots`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default function AvailabilityCalendar({ serviceId, serviceName, serviceType, onSlotSelected }: Props) {
|
|
143
|
+
const [date, setDate] = useState<Date>(() => startOfToday());
|
|
144
|
+
const [slots, setSlots] = useState<SelectedSlot[]>([]);
|
|
145
|
+
const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
|
|
146
|
+
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
|
147
|
+
const today = useMemo(() => startOfToday(), []);
|
|
148
|
+
const atToday = date.getTime() <= today.getTime();
|
|
149
|
+
|
|
150
|
+
const load = useCallback(async (target: Date) => {
|
|
151
|
+
setStatus("loading"); setSelectedKey(null);
|
|
152
|
+
try {
|
|
153
|
+
const { slots: fetched } = await fetchSlots(serviceId, serviceType, target);
|
|
154
|
+
setSlots(fetched); setStatus("ready");
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error("[availability] list failed:", err);
|
|
157
|
+
setStatus("error");
|
|
158
|
+
}
|
|
159
|
+
}, [serviceId, serviceType]);
|
|
160
|
+
|
|
161
|
+
useEffect(() => { void load(date); }, [date, load]);
|
|
162
|
+
|
|
163
|
+
const shiftDay = (delta: number) =>
|
|
164
|
+
setDate((prev) => { const n = new Date(prev); n.setDate(n.getDate() + delta); n.setHours(0, 0, 0, 0); return n; });
|
|
165
|
+
|
|
166
|
+
const keyOf = (s: SelectedSlot) => `${s.localStartDate}|${s.eventId ?? s.scheduleId ?? ""}`;
|
|
167
|
+
|
|
168
|
+
const handleSelect = async (s: SelectedSlot) => {
|
|
169
|
+
// Guard on the type-appropriate id — CLASS slots have no scheduleId.
|
|
170
|
+
const id = s.serviceType === "CLASS" ? s.eventId : s.scheduleId;
|
|
171
|
+
if (!s.localStartDate || !id) return;
|
|
172
|
+
setSelectedKey(keyOf(s));
|
|
173
|
+
const instructorName = s.serviceType === "CLASS" && s.eventId ? await fetchInstructor(s.eventId) : undefined;
|
|
174
|
+
onSlotSelected({ ...s, instructorName });
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="availability-calendar">
|
|
179
|
+
<div className="availability-date-nav">
|
|
180
|
+
<button type="button" className="availability-nav-btn" onClick={() => shiftDay(-1)} disabled={atToday} aria-label="Previous day">← Prev</button>
|
|
181
|
+
<span className="availability-date-label">{dayLabel(date)}</span>
|
|
182
|
+
<button type="button" className="availability-nav-btn" onClick={() => shiftDay(1)} aria-label="Next day">Next →</button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{status === "loading" && <p className="availability-loading">Checking availability…</p>}
|
|
186
|
+
{status === "error" && (
|
|
187
|
+
<>
|
|
188
|
+
<p className="availability-error">Could not load availability — please try again.</p>
|
|
189
|
+
<button type="button" className="availability-nav-btn" onClick={() => void load(date)}>Retry</button>
|
|
190
|
+
</>
|
|
191
|
+
)}
|
|
192
|
+
{status === "ready" && slots.length === 0 && (
|
|
193
|
+
<p className="availability-empty">No availability on this date — try another day.</p>
|
|
194
|
+
)}
|
|
195
|
+
{status === "ready" && slots.length > 0 && (
|
|
196
|
+
<div className="availability-slots" role="group" aria-label={`Available times for ${serviceName}`}>
|
|
197
|
+
{slots.map((s) => {
|
|
198
|
+
const key = keyOf(s);
|
|
199
|
+
const isSelected = key === selectedKey;
|
|
200
|
+
const cap = capacityLabel(s);
|
|
201
|
+
return (
|
|
202
|
+
<button
|
|
203
|
+
key={key}
|
|
204
|
+
type="button"
|
|
205
|
+
className={["time-slot", isSelected ? "time-slot--selected" : "time-slot--available", s.isFull ? "time-slot--full" : ""].filter(Boolean).join(" ")}
|
|
206
|
+
aria-pressed={isSelected}
|
|
207
|
+
onClick={() => void handleSelect(s)}
|
|
208
|
+
>
|
|
209
|
+
<span className="time-slot-time">{timeLabel(s.localStartDate)}</span>
|
|
210
|
+
{cap && <span className="time-slot-capacity">{cap}</span>}
|
|
211
|
+
</button>
|
|
212
|
+
);
|
|
213
|
+
})}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { createClient, OAuthStrategy } from "@wix/sdk";
|
|
3
|
+
import { bookings } from "@wix/bookings";
|
|
4
|
+
import type { SelectedSlot } from "./AvailabilityCalendar";
|
|
5
|
+
import { WIX_CLIENT_ID } from "astro:env/client"; // browser client ID, NOT import.meta.env
|
|
6
|
+
|
|
7
|
+
// BookingForm.tsx — client:only="react" island. Renders after a slot is picked.
|
|
8
|
+
// Flow: createBooking → CREATED holds NO seat, so POST the booking to
|
|
9
|
+
// the elevated /api/confirm-booking to occupy it (and validate availability).
|
|
10
|
+
// A full CLASS session throws SESSION_CAPACITY_EXCEEDED whose message carries the
|
|
11
|
+
// v1 session id → register on the native waitlist via /api/waitlist.
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
serviceId: string;
|
|
15
|
+
serviceName: string;
|
|
16
|
+
serviceType: "APPOINTMENT" | "CLASS";
|
|
17
|
+
slot: SelectedSlot;
|
|
18
|
+
onSuccess: (bookingId: string, startDate: string) => void;
|
|
19
|
+
onCancel: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const wixClient = createClient({
|
|
23
|
+
modules: { bookings },
|
|
24
|
+
auth: OAuthStrategy({ clientId: WIX_CLIENT_ID }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const slotDisplay = (slot: SelectedSlot) =>
|
|
28
|
+
slot.localStartDate
|
|
29
|
+
? new Date(slot.localStartDate).toLocaleString([], {
|
|
30
|
+
weekday: "long", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit",
|
|
31
|
+
})
|
|
32
|
+
: "";
|
|
33
|
+
|
|
34
|
+
export default function BookingForm({ serviceName, serviceType, slot, onSuccess, onCancel }: Props) {
|
|
35
|
+
const [firstName, setFirstName] = useState("");
|
|
36
|
+
const [lastName, setLastName] = useState("");
|
|
37
|
+
const [email, setEmail] = useState("");
|
|
38
|
+
const [phone, setPhone] = useState("");
|
|
39
|
+
const [participants, setParticipants] = useState(1);
|
|
40
|
+
const [submitting, setSubmitting] = useState(false);
|
|
41
|
+
const [error, setError] = useState<string | null>(null);
|
|
42
|
+
const [waitlisted, setWaitlisted] = useState(false);
|
|
43
|
+
|
|
44
|
+
const isWaitlist = serviceType === "CLASS" && !!slot.isFull;
|
|
45
|
+
// Party size only applies to CLASS (APPOINTMENT is 1-on-1).
|
|
46
|
+
const maxParty = serviceType === "CLASS" ? Math.max(1, slot.bookableCapacity ?? slot.remainingCapacity ?? 1) : 1;
|
|
47
|
+
const partyOptions = Array.from({ length: Math.min(maxParty, 8) }, (_, i) => i + 1);
|
|
48
|
+
|
|
49
|
+
const contactDetails = () => ({
|
|
50
|
+
firstName: firstName.trim(),
|
|
51
|
+
lastName: lastName.trim() || undefined,
|
|
52
|
+
email: email.trim(),
|
|
53
|
+
phone: phone.trim() || undefined,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const buildSlot = () =>
|
|
57
|
+
serviceType === "CLASS"
|
|
58
|
+
// CLASS: serviceId + eventId only — Wix derives the rest from the event.
|
|
59
|
+
? { serviceId: slot.serviceId, eventId: slot.eventId }
|
|
60
|
+
// APPOINTMENT: scheduleId + explicit start/end/timezone/location.
|
|
61
|
+
: {
|
|
62
|
+
serviceId: slot.serviceId,
|
|
63
|
+
scheduleId: slot.scheduleId,
|
|
64
|
+
startDate: slot.localStartDate,
|
|
65
|
+
endDate: slot.localEndDate,
|
|
66
|
+
timezone: slot.timezone,
|
|
67
|
+
...(slot.locationId
|
|
68
|
+
? { location: { _id: slot.locationId, locationType: slot.locationType as any } }
|
|
69
|
+
: { location: { locationType: "OWNER_BUSINESS" as const } }),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
setError(null);
|
|
75
|
+
if (!firstName.trim() || !email.trim()) {
|
|
76
|
+
setError("Please enter your first name and email.");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
setSubmitting(true);
|
|
80
|
+
try {
|
|
81
|
+
const result = await wixClient.bookings.createBooking({
|
|
82
|
+
totalParticipants: participants, // party size
|
|
83
|
+
contactDetails: contactDetails(),
|
|
84
|
+
selectedPaymentOption: "OFFLINE" as const,
|
|
85
|
+
bookedEntity: { slot: buildSlot() },
|
|
86
|
+
} as any);
|
|
87
|
+
|
|
88
|
+
const status = result.booking?.status;
|
|
89
|
+
const bookingId = result.booking?._id ?? "";
|
|
90
|
+
const revision = result.booking?.revision ?? "";
|
|
91
|
+
|
|
92
|
+
// Defensive: if createBooking ever returns a WAITING_LIST booking directly,
|
|
93
|
+
// treat it as joined. In practice a full session THROWS
|
|
94
|
+
// SESSION_CAPACITY_EXCEEDED (handled in catch → /api/waitlist) rather than
|
|
95
|
+
// returning this status — both paths land the user on the waitlist.
|
|
96
|
+
if (status === "WAITING_LIST") { setWaitlisted(true); setSubmitting(false); return; }
|
|
97
|
+
|
|
98
|
+
// CREATED → confirm server-side to hold the seat.
|
|
99
|
+
const confirmRes = await fetch("/api/confirm-booking", {
|
|
100
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify({ bookingId, revision }),
|
|
102
|
+
});
|
|
103
|
+
const confirmJson = await confirmRes.json().catch(() => ({}));
|
|
104
|
+
if (!confirmRes.ok || confirmJson.ok === false) {
|
|
105
|
+
setError("This session just filled up — please pick another time.");
|
|
106
|
+
setSubmitting(false); onCancel(); return;
|
|
107
|
+
}
|
|
108
|
+
onSuccess(bookingId, slot.localStartDate);
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
console.error("[booking] createBooking/confirm failed:", err);
|
|
111
|
+
// Full CLASS session → native waitlist. The error carries the v1 session id.
|
|
112
|
+
const full = `${err?.message ?? ""} ${JSON.stringify(err?.details ?? {})} ${String(err)}`;
|
|
113
|
+
if (/SESSION_CAPACITY_EXCEEDED/i.test(full) || /no available spots/i.test(full)) {
|
|
114
|
+
const sessionId = full.match(/on session ([^\s"}]+)/)?.[1];
|
|
115
|
+
if (sessionId) {
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetch("/api/waitlist", {
|
|
118
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
119
|
+
body: JSON.stringify({ sessionId, totalParticipants: participants, contactDetails: contactDetails() }),
|
|
120
|
+
});
|
|
121
|
+
const j = await res.json().catch(() => ({}));
|
|
122
|
+
if (res.ok && j.ok !== false) { setWaitlisted(true); setSubmitting(false); return; }
|
|
123
|
+
} catch (e) { console.error("[waitlist] failed:", e); }
|
|
124
|
+
}
|
|
125
|
+
setError("This class is full and we couldn't add you to the waitlist — please try again later.");
|
|
126
|
+
setSubmitting(false); return;
|
|
127
|
+
}
|
|
128
|
+
setError("This time slot was just taken. Please select another.");
|
|
129
|
+
setSubmitting(false); onCancel();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (waitlisted) {
|
|
134
|
+
return (
|
|
135
|
+
<div className="booking-form">
|
|
136
|
+
<div className="booking-form-header">
|
|
137
|
+
<h3 className="booking-form-title">You're on the waitlist</h3>
|
|
138
|
+
<p className="booking-form-subtitle">{serviceName} · {slotDisplay(slot)}</p>
|
|
139
|
+
</div>
|
|
140
|
+
<p>We'll email <strong>{email.trim()}</strong> if a spot opens up.</p>
|
|
141
|
+
<button type="button" className="booking-cancel" onClick={onCancel}>← Back to available times</button>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<form className="booking-form" onSubmit={handleSubmit}>
|
|
148
|
+
<div className="booking-form-header">
|
|
149
|
+
<h3 className="booking-form-title">{isWaitlist ? "Join the waitlist" : "Reserve your spot"}</h3>
|
|
150
|
+
<p className="booking-form-subtitle">
|
|
151
|
+
{serviceName} · {slotDisplay(slot)}{slot.instructorName ? ` · with ${slot.instructorName}` : ""}
|
|
152
|
+
</p>
|
|
153
|
+
{isWaitlist && <p className="booking-form-note">This session is full — join the waitlist and we'll notify you if a spot opens.</p>}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{error && <p className="booking-error">{error}</p>}
|
|
157
|
+
|
|
158
|
+
<div className="booking-field-row">
|
|
159
|
+
<div className="booking-field">
|
|
160
|
+
<label className="booking-label booking-label--required" htmlFor="bf-first">First name</label>
|
|
161
|
+
<input id="bf-first" className="booking-input" type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} required autoComplete="given-name" />
|
|
162
|
+
</div>
|
|
163
|
+
<div className="booking-field">
|
|
164
|
+
<label className="booking-label" htmlFor="bf-last">Last name</label>
|
|
165
|
+
<input id="bf-last" className="booking-input" type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} autoComplete="family-name" />
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div className="booking-field">
|
|
170
|
+
<label className="booking-label booking-label--required" htmlFor="bf-email">Email</label>
|
|
171
|
+
<input id="bf-email" className="booking-input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required autoComplete="email" />
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div className="booking-field">
|
|
175
|
+
<label className="booking-label" htmlFor="bf-phone">Phone</label>
|
|
176
|
+
<input id="bf-phone" className="booking-input" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} autoComplete="tel" />
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{!isWaitlist && maxParty > 1 && (
|
|
180
|
+
<div className="booking-field">
|
|
181
|
+
<label className="booking-label" htmlFor="bf-party">Number of spots</label>
|
|
182
|
+
<select id="bf-party" className="booking-input" value={participants} onChange={(e) => setParticipants(Number(e.target.value))}>
|
|
183
|
+
{partyOptions.map((n) => <option key={n} value={n}>{n}</option>)}
|
|
184
|
+
</select>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
<button type="submit" className="booking-submit" disabled={submitting}>
|
|
189
|
+
{submitting ? (isWaitlist ? "Joining…" : "Confirming…") : isWaitlist ? "Join Waitlist" : "Confirm Booking"}
|
|
190
|
+
</button>
|
|
191
|
+
<button type="button" className="booking-cancel" onClick={onCancel} disabled={submitting}>← Back to available times</button>
|
|
192
|
+
</form>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { createClient, OAuthStrategy } from "@wix/sdk";
|
|
3
|
+
import { bookings } from "@wix/bookings";
|
|
4
|
+
import { WIX_CLIENT_ID } from "astro:env/client";
|
|
5
|
+
|
|
6
|
+
// ManageBooking.tsx — client:only="react" island for /manage-booking.
|
|
7
|
+
// The server (manage-booking.astro) already minted an anonymous action token
|
|
8
|
+
// (app-elevated) and passed it in. The cancel call is a NO-AUTH anonymous method,
|
|
9
|
+
// so the visitor client runs it directly with the token. Reschedule-in-place
|
|
10
|
+
// needs a verified class V2Slot shape, so we offer cancel-and-rebook instead.
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
token: string | null;
|
|
14
|
+
revision: string;
|
|
15
|
+
serviceName: string;
|
|
16
|
+
startDate: string | null;
|
|
17
|
+
status: string | null;
|
|
18
|
+
canCancel: boolean;
|
|
19
|
+
showSummary?: boolean; // false when embedded under a page that already shows the service/date
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const wixClient = createClient({
|
|
23
|
+
modules: { bookings },
|
|
24
|
+
auth: OAuthStrategy({ clientId: WIX_CLIENT_ID }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const fmt = (iso: string | null) => {
|
|
28
|
+
if (!iso) return null;
|
|
29
|
+
const d = new Date(iso);
|
|
30
|
+
return Number.isNaN(d.getTime())
|
|
31
|
+
? null
|
|
32
|
+
: d.toLocaleString([], { weekday: "long", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default function ManageBooking({ token, revision, serviceName, startDate, status, canCancel, showSummary }: Props) {
|
|
36
|
+
const [state, setState] = useState<"idle" | "canceling" | "canceled" | "error">(
|
|
37
|
+
status === "CANCELED" ? "canceled" : "idle",
|
|
38
|
+
);
|
|
39
|
+
const when = fmt(startDate);
|
|
40
|
+
|
|
41
|
+
const handleCancel = async () => {
|
|
42
|
+
if (!token) return;
|
|
43
|
+
setState("canceling");
|
|
44
|
+
try {
|
|
45
|
+
await wixClient.bookings.bookingsCancelBookingAnonymously(token, revision);
|
|
46
|
+
setState("canceled");
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error("[manage-booking] cancel failed:", err);
|
|
49
|
+
setState("error");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (state === "canceled") {
|
|
54
|
+
return (
|
|
55
|
+
<div className="manage-booking">
|
|
56
|
+
<p className="manage-booking-status">
|
|
57
|
+
Your booking for <strong>{serviceName}</strong>{when ? <> on <strong>{when}</strong></> : null} is canceled.
|
|
58
|
+
</p>
|
|
59
|
+
<a href="/services" className="booking-submit" style={{ display: "inline-block" }}>Book another service</a>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="manage-booking">
|
|
66
|
+
{showSummary !== false && (
|
|
67
|
+
<p className="manage-booking-summary">
|
|
68
|
+
<strong>{serviceName}</strong>{when ? <> · {when}</> : null}
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
{state === "error" && <p className="booking-error">Something went wrong canceling your booking. Please try again.</p>}
|
|
72
|
+
{canCancel ? (
|
|
73
|
+
<button type="button" className="booking-submit" onClick={handleCancel} disabled={state === "canceling"}>
|
|
74
|
+
{state === "canceling" ? "Canceling…" : "Cancel this booking"}
|
|
75
|
+
</button>
|
|
76
|
+
) : (
|
|
77
|
+
<p className="manage-booking-note">This booking can no longer be canceled online.</p>
|
|
78
|
+
)}
|
|
79
|
+
<p className="manage-booking-note">
|
|
80
|
+
Need a different time? Cancel above, then <a href="/services" className="manage-booking-link">book another time</a>.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
// SeoTags.astro — renders Wix-resolved SEO tags into <head> for service pages.
|
|
3
|
+
// Pass `service.seoData.tags` (Services V2) — title/meta/link/script — so the
|
|
4
|
+
// service detail page ships Wix's sitemap/SEO/social tags. Mirrors the stores
|
|
5
|
+
// SeoTags template. Mount inside the Layout's `head` slot on `/services/[slug]`.
|
|
6
|
+
interface SeoTag {
|
|
7
|
+
type?: string;
|
|
8
|
+
props?: Record<string, any> | null;
|
|
9
|
+
children?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface Props { tags?: SeoTag[]; }
|
|
13
|
+
const { tags = [] } = Astro.props;
|
|
14
|
+
const visibleTags = tags.filter((t) => !t.disabled);
|
|
15
|
+
---
|
|
16
|
+
{
|
|
17
|
+
visibleTags.map((tag) => {
|
|
18
|
+
const props = tag.props ?? {};
|
|
19
|
+
if (tag.type === "title") return <title set:html={tag.children ?? ""} />;
|
|
20
|
+
if (tag.type === "meta") return <meta {...props} />;
|
|
21
|
+
if (tag.type === "link") return <link {...props} />;
|
|
22
|
+
if (tag.type === "script") return <script {...props} is:inline set:html={tag.children ?? ""} />;
|
|
23
|
+
return null;
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import AvailabilityCalendar from "./AvailabilityCalendar";
|
|
3
|
+
import type { SelectedSlot } from "./AvailabilityCalendar";
|
|
4
|
+
import BookingForm from "./BookingForm";
|
|
5
|
+
|
|
6
|
+
// ServiceBookingFlow.tsx — client:only="react" coordinator island. Holds the
|
|
7
|
+
// selected-slot state shared between AvailabilityCalendar and BookingForm,
|
|
8
|
+
// transitions between them, and redirects to the confirmation page on success.
|
|
9
|
+
// serviceType MUST be threaded through so the calendar picks the right time-slots
|
|
10
|
+
// API and the form builds the right createBooking shape.
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
serviceId: string;
|
|
14
|
+
serviceName: string;
|
|
15
|
+
serviceType: "APPOINTMENT" | "CLASS";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ServiceBookingFlow({ serviceId, serviceName, serviceType }: Props) {
|
|
19
|
+
const [selectedSlot, setSelectedSlot] = useState<SelectedSlot | null>(null);
|
|
20
|
+
|
|
21
|
+
const handleSuccess = (bookingId: string, startDate?: string) => {
|
|
22
|
+
const params = new URLSearchParams();
|
|
23
|
+
if (bookingId) params.set("bookingId", bookingId);
|
|
24
|
+
if (startDate) params.set("startDate", startDate);
|
|
25
|
+
if (serviceName) params.set("service", serviceName);
|
|
26
|
+
window.location.href = `/booking-confirmation?${params.toString()}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (!selectedSlot) {
|
|
30
|
+
return (
|
|
31
|
+
<AvailabilityCalendar
|
|
32
|
+
serviceId={serviceId}
|
|
33
|
+
serviceName={serviceName}
|
|
34
|
+
serviceType={serviceType}
|
|
35
|
+
onSlotSelected={setSelectedSlot}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<BookingForm
|
|
42
|
+
serviceId={serviceId}
|
|
43
|
+
serviceName={serviceName}
|
|
44
|
+
serviceType={serviceType}
|
|
45
|
+
slot={selectedSlot}
|
|
46
|
+
onSuccess={handleSuccess}
|
|
47
|
+
onCancel={() => setSelectedSlot(null)}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|