@wix/agent-skills 1.13.0 → 1.15.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 +4 -1
- package/package.json +1 -1
- package/skills/README.md +23 -0
- package/skills/wix-app/SKILL.md +10 -2
- package/skills/wix-app/references/AUTO_PATTERNS_DASHBOARD.md +326 -0
- package/skills/wix-app/references/CODE_QUALITY.md +1 -0
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +124 -115
- package/skills/wix-app/references/DASHBOARD_PAGE.md +3 -0
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +23 -9
- package/skills/wix-app/references/SITE_PLUGIN.md +1 -1
- package/skills/wix-app/references/auto-patterns-dashboard/action-cell.md +115 -0
- package/skills/wix-app/references/auto-patterns-dashboard/app-config-structure.md +264 -0
- package/skills/wix-app/references/auto-patterns-dashboard/app-context.md +37 -0
- package/skills/wix-app/references/auto-patterns-dashboard/bulk-actions.md +86 -0
- package/skills/wix-app/references/auto-patterns-dashboard/collection-page-actions.md +119 -0
- package/skills/wix-app/references/auto-patterns-dashboard/collection-page.md +103 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-actions-override.md +76 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-columns-override.md +82 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-components-override.md +89 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-header-override.md +124 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-sections-override.md +91 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-slots-override.md +71 -0
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page-actions.md +70 -0
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page-view-actions.md +73 -0
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page.md +98 -0
- package/skills/wix-app/references/auto-patterns-dashboard/example-patterns.json +278 -0
- package/skills/wix-app/references/auto-patterns-dashboard/pages-configuration.md +94 -0
- package/skills/wix-app/references/auto-patterns-dashboard/resolved-action.md +43 -0
- package/skills/wix-app/references/auto-patterns-dashboard/sdk-utilities.md +66 -0
- package/skills/wix-app/references/auto-patterns-dashboard/views.md +76 -0
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +1 -14
- package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +54 -29
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +2 -2
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +17 -105
- package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +127 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +2 -1
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +15 -15
- package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +25 -53
- package/skills/wix-app/scripts/generate-auto-patterns.js +510 -0
- package/skills/wix-auth/SKILL.md +12 -0
- package/skills/wix-auth/references/device-flow.md +140 -0
- package/skills/wix-docs/SKILL.md +188 -0
- package/skills/wix-docs/references/API_SPEC_SEARCH.md +213 -0
- package/skills/wix-docs/references/EXTRACTING.md +120 -0
- package/skills/wix-headless/SKILL.md +84 -130
- package/skills/wix-headless/entry/bootstrap.mjs +93 -0
- package/skills/wix-headless/entry/skill.md +78 -0
- package/skills/wix-headless/references/AI_FEATURES.md +178 -0
- package/skills/wix-headless/references/CAPABILITIES.md +84 -0
- package/skills/wix-headless/references/DISCOVERY.md +34 -56
- package/skills/wix-headless/references/DOC_DISCOVERY.md +37 -0
- package/skills/wix-headless/references/FEEDBACK.md +118 -0
- package/skills/wix-headless/references/IMAGE_GENERATION.md +69 -0
- package/skills/wix-headless/references/SDK_HANDOFF.md +97 -0
- package/skills/wix-headless/references/SEED.md +44 -188
- package/skills/wix-headless/references/SETUP.md +32 -103
- package/skills/wix-headless/references/astro.md +98 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-blog.md +196 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +236 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-bookings.md +279 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-cms.md +163 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-events.md +203 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +222 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +124 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-custom-login.md +160 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-non-astro.md +118 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-portfolio.md +132 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-pricing-plans.md +124 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-orders.md +188 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-reservations.md +173 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurants.md +161 -0
- package/skills/wix-headless/references/inline-recipes/setup-blog.md +175 -0
- package/skills/wix-headless/references/inline-recipes/setup-bookings.md +228 -0
- package/skills/wix-headless/references/inline-recipes/setup-cms.md +200 -0
- package/skills/wix-headless/references/inline-recipes/setup-events.md +196 -0
- package/skills/wix-headless/references/inline-recipes/setup-forms.md +174 -0
- package/skills/wix-headless/references/inline-recipes/setup-online-store.md +176 -0
- package/skills/wix-headless/references/inline-recipes/setup-portfolio.md +153 -0
- package/skills/wix-headless/references/inline-recipes/setup-pricing-plans.md +229 -0
- package/skills/wix-headless/references/inline-recipes/setup-restaurant-orders.md +161 -0
- package/skills/wix-headless/references/inline-recipes/setup-restaurant-reservations.md +117 -0
- package/skills/wix-headless/references/inline-recipes/setup-restaurants.md +152 -0
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +57 -0
- package/skills/wix-headless/references/managed/CONNECT.md +74 -0
- package/skills/wix-headless/references/managed/CREATE.md +130 -0
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +57 -0
- package/skills/wix-headless/references/non-astro.md +59 -0
- package/skills/wix-headless/references/self-managed/AUTHENTICATION.md +22 -0
- package/skills/wix-headless/references/self-managed/DEPLOYMENT.md +7 -0
- package/skills/wix-headless/references/stripe/AUTHENTICATION.md +55 -0
- package/skills/wix-headless/references/stripe/DEPLOYMENT.md +63 -0
- package/skills/wix-manage/SKILL.md +24 -1
- package/skills/wix-manage/references/analytics/query-site-analytics.md +241 -0
- package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +1 -1
- package/skills/wix-manage/references/bookings/diagnose-availability-issues.md +265 -0
- package/skills/wix-manage/references/marketing/create-and-publish-social-post.md +436 -0
- package/skills/wix-manage/references/marketing/generate-and-publish-marketing-plan.md +209 -0
- package/skills/wix-manage/references/rich-content/author-ricos-rich-content.md +249 -0
- package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +51 -0
- package/skills/wix-vibe-headless/SKILL.md +136 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +128 -0
- package/skills/wix-vibe-headless/references/blog/wix-blog.js +217 -0
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +153 -0
- package/skills/wix-vibe-headless/references/bookings/wix-bookings-checkout.js +147 -0
- package/skills/wix-vibe-headless/references/bookings/wix-bookings-services.js +215 -0
- package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +154 -0
- package/skills/wix-vibe-headless/references/cms/wix-cms.js +186 -0
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +142 -0
- package/skills/wix-vibe-headless/references/events/wix-events-browse.js +135 -0
- package/skills/wix-vibe-headless/references/events/wix-events-registration.js +187 -0
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +211 -0
- package/skills/wix-vibe-headless/references/members/wix-members-auth.js +387 -0
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +111 -0
- package/skills/wix-vibe-headless/references/portfolio/wix-portfolio.js +194 -0
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +127 -0
- package/skills/wix-vibe-headless/references/pricing-plans/wix-pricing-plans.js +157 -0
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +138 -0
- package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-menu.js +224 -0
- package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-ordering.js +156 -0
- package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-reservations.js +107 -0
- package/skills/wix-vibe-headless/references/shared/wix-client.js +186 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +155 -0
- package/skills/wix-vibe-headless/references/storefront/wix-store-cart.js +147 -0
- package/skills/wix-vibe-headless/references/storefront/wix-store-catalog.js +142 -0
- package/skills/wix-headless/references/BUILD-astro.md +0 -244
- package/skills/wix-headless/references/BUILD-own-build.md +0 -167
- package/skills/wix-headless/references/BUILD.md +0 -105
- package/skills/wix-headless/references/DESIGN_SYSTEM.md +0 -165
- package/skills/wix-headless/references/DISCOVERY-connect.md +0 -55
- package/skills/wix-headless/references/DISCOVERY-create.md +0 -238
- package/skills/wix-headless/references/PLAN-connect.md +0 -19
- package/skills/wix-headless/references/PLAN-create.md +0 -17
- package/skills/wix-headless/references/PLAN.md +0 -92
- package/skills/wix-headless/references/astro/blog/BLOG_PAGES.md +0 -300
- package/skills/wix-headless/references/astro/blog/BLOG_SETUP.md +0 -218
- package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +0 -45
- package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +0 -77
- package/skills/wix-headless/references/astro/cms/CMS_FOUNDATIONS.md +0 -214
- package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +0 -231
- package/skills/wix-headless/references/astro/cms/PORTFOLIO.md +0 -453
- package/skills/wix-headless/references/astro/cms/RESOURCE_LIBRARY.md +0 -354
- package/skills/wix-headless/references/astro/cms/TEAM_DIRECTORY.md +0 -299
- package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +0 -351
- package/skills/wix-headless/references/astro/ecom/CART_PAGES.md +0 -119
- package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +0 -103
- package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +0 -132
- package/skills/wix-headless/references/astro/forms/CONTACT_FORM.md +0 -463
- package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +0 -119
- package/skills/wix-headless/references/astro/gift-cards/PAGES.md +0 -129
- package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +0 -178
- package/skills/wix-headless/references/astro/stores/CATEGORY_PAGES.md +0 -85
- package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +0 -129
- package/skills/wix-headless/references/astro/stores/HOME_AND_NAV.md +0 -229
- package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +0 -215
- package/skills/wix-headless/references/astro/stores/SHARED_WIRING.md +0 -165
- package/skills/wix-headless/references/astro/templates/Footer.astro +0 -27
- package/skills/wix-headless/references/astro/templates/Layout.astro +0 -132
- package/skills/wix-headless/references/astro/templates/Navigation.astro +0 -38
- package/skills/wix-headless/references/astro/templates/astro.config.mjs +0 -41
- package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +0 -374
- package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +0 -136
- package/skills/wix-headless/references/astro/templates/bookings/SeoTags.astro +0 -25
- package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +0 -71
- package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +0 -54
- package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +0 -49
- package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +0 -245
- package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +0 -424
- package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +0 -176
- package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +0 -181
- package/skills/wix-headless/references/astro/templates/ecom/CartBadge.tsx +0 -84
- package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +0 -543
- package/skills/wix-headless/references/astro/templates/ecom/cart.astro +0 -13
- package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +0 -302
- package/skills/wix-headless/references/astro/templates/ecom/discounts.ts +0 -71
- package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +0 -24
- package/skills/wix-headless/references/astro/templates/gift-cards/GiftCardPurchase.tsx +0 -172
- package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +0 -42
- package/skills/wix-headless/references/astro/templates/gift-cards/_nav-snippet.astro +0 -11
- package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +0 -220
- package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +0 -51
- package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.ts +0 -75
- package/skills/wix-headless/references/astro/templates/global.css +0 -288
- package/skills/wix-headless/references/astro/templates/index.astro +0 -56
- package/skills/wix-headless/references/astro/templates/stores/AddToCartButton.tsx +0 -77
- package/skills/wix-headless/references/astro/templates/stores/BackInStockForm.tsx +0 -176
- package/skills/wix-headless/references/astro/templates/stores/CategoryRail.astro +0 -69
- package/skills/wix-headless/references/astro/templates/stores/ProductCard.astro +0 -79
- package/skills/wix-headless/references/astro/templates/stores/ProductPurchase.tsx +0 -397
- package/skills/wix-headless/references/astro/templates/stores/SeoTags.astro +0 -21
- package/skills/wix-headless/references/astro/templates/stores/back-in-stock.ts +0 -67
- package/skills/wix-headless/references/astro/templates/stores/categories.ts +0 -155
- package/skills/wix-headless/references/astro/templates/stores/category/[slug].astro +0 -130
- package/skills/wix-headless/references/astro/templates/stores/components-stores.css +0 -400
- package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +0 -128
- package/skills/wix-headless/references/astro/templates/stores/products/index.astro +0 -101
- package/skills/wix-headless/references/blog/BLOG_CONTENT.md +0 -161
- package/skills/wix-headless/references/blog/INSTRUCTIONS.md +0 -32
- package/skills/wix-headless/references/bookings/FLOW.md +0 -246
- package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +0 -95
- package/skills/wix-headless/references/bookings/SERVICES_DATA.md +0 -355
- package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +0 -176
- package/skills/wix-headless/references/cms/INSTRUCTIONS.md +0 -71
- package/skills/wix-headless/references/commands/known-apps.json +0 -6
- package/skills/wix-headless/references/custom/CONNECTION_PLAN.md +0 -142
- package/skills/wix-headless/references/custom/INSTRUCTIONS.md +0 -88
- package/skills/wix-headless/references/custom/blog/WIRING.md +0 -52
- package/skills/wix-headless/references/custom/bookings/WIRING.md +0 -197
- package/skills/wix-headless/references/custom/cms/WIRING.md +0 -97
- package/skills/wix-headless/references/custom/ecom/WIRING.md +0 -67
- package/skills/wix-headless/references/custom/forms/WIRING.md +0 -101
- package/skills/wix-headless/references/custom/stores/WIRING.md +0 -80
- package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +0 -59
- package/skills/wix-headless/references/forms/INSTRUCTIONS.md +0 -43
- package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +0 -51
- package/skills/wix-headless/references/images/INSTRUCTIONS.md +0 -357
- package/skills/wix-headless/references/seed-recipes.md +0 -49
- package/skills/wix-headless/references/shared/AGENTS_MD.md +0 -47
- package/skills/wix-headless/references/shared/AUTHENTICATION.md +0 -108
- package/skills/wix-headless/references/shared/BUILD_NOISE.md +0 -8
- package/skills/wix-headless/references/shared/DESIGN_MD.md +0 -84
- package/skills/wix-headless/references/shared/DOCS_SEARCH.md +0 -41
- package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +0 -186
- package/skills/wix-headless/references/shared/IMPLEMENTER.md +0 -206
- package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +0 -97
- package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +0 -143
- package/skills/wix-headless/references/shared/STYLING.md +0 -141
- package/skills/wix-headless/references/stores/INSTRUCTIONS.md +0 -126
- package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +0 -252
- package/skills/wix-headless/references/verticals/_schema.json +0 -74
- package/skills/wix-headless/references/verticals/_schema.md +0 -95
- package/skills/wix-headless/references/verticals/blog.md +0 -31
- package/skills/wix-headless/references/verticals/bookings.md +0 -41
- package/skills/wix-headless/references/verticals/cms.md +0 -30
- package/skills/wix-headless/references/verticals/ecom.md +0 -25
- package/skills/wix-headless/references/verticals/forms.md +0 -26
- package/skills/wix-headless/references/verticals/gift-cards.md +0 -24
- package/skills/wix-headless/references/verticals/stores.md +0 -33
- package/skills/wix-headless/scripts/check-manifest.mjs +0 -335
- package/skills/wix-headless/scripts/compose.mjs +0 -525
- package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -157
- package/skills/wix-headless/scripts/flatten.sh +0 -61
- package/skills/wix-headless/scripts/generate-images.mjs +0 -242
- package/skills/wix-headless/scripts/patch-decorative-slots.mjs +0 -148
- package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +0 -190
- package/skills/wix-headless/scripts/scaffold.sh +0 -139
- package/skills/wix-headless/scripts/seed-utilities.sh +0 -93
- package/skills/wix-headless/scripts/write-decorative-json.mjs +0 -62
- package/skills/wix-headless/shared-utilities/analytics.ts +0 -40
- package/skills/wix-headless/shared-utilities/ricos.ts +0 -179
- package/skills/wix-headless/shared-utilities/wix-image.ts +0 -72
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// ── Composer skeleton: src/components/Footer.astro ────────────────────────────
|
|
3
|
-
// Substitutions (see compose.mjs):
|
|
4
|
-
// {{brand.name}} — brand name for the wordmark + copyright.
|
|
5
|
-
// {{shell.footerTagline}} — short brand-voice tagline from data.shell.
|
|
6
|
-
// {{nav.links}} — same inlined Navigation links array (mirror/subset);
|
|
7
|
-
// one <a href={href}>{label}</a> per entry, labels VERBATIM. No cart,
|
|
8
|
-
// login, or account links.
|
|
9
|
-
// Fixed bulk (keep literal): transition:persist, the year expression.
|
|
10
|
-
const year = new Date().getFullYear();
|
|
11
|
-
---
|
|
12
|
-
<footer class="site-footer" transition:persist="site-footer">
|
|
13
|
-
<div class="max-w-6xl mx-auto px-6 py-16 flex flex-col md:flex-row md:items-start md:justify-between gap-8">
|
|
14
|
-
<div class="flex flex-col gap-3 max-w-prose">
|
|
15
|
-
<span class="font-display text-lg text-ink">{{brand.name}}</span>
|
|
16
|
-
<p class="text-sm">{{shell.footerTagline}}</p>
|
|
17
|
-
</div>
|
|
18
|
-
<nav>
|
|
19
|
-
<ul class="flex flex-col gap-3 text-sm">
|
|
20
|
-
{{nav.links}}
|
|
21
|
-
</ul>
|
|
22
|
-
</nav>
|
|
23
|
-
</div>
|
|
24
|
-
<div class="max-w-6xl mx-auto px-6 pb-8 text-xs">
|
|
25
|
-
<p>© {year} {{brand.name}}. All rights reserved.</p>
|
|
26
|
-
</div>
|
|
27
|
-
</footer>
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// ── Composer skeleton: src/layouts/Layout.astro ──────────────────────────────
|
|
3
|
-
// Fully replaces the scaffold's bare stub. The <script> below and the View
|
|
4
|
-
// Transitions wiring are LITERAL fixed bulk — do not re-author. Composer's only
|
|
5
|
-
// substitutions (see compose.mjs):
|
|
6
|
-
// {{components-css-imports}} — one `import '../styles/components-<pack>.css';`
|
|
7
|
-
// per pack in "Packs with components" (omit entirely when none).
|
|
8
|
-
// {{fonts.googleHref}} — the Google Fonts <link> href built from designTokens
|
|
9
|
-
// .fonts (display + body families). Omit the <link> if fonts are system.
|
|
10
|
-
// {{brand.name}} — default <title>.
|
|
11
|
-
import '../styles/global.css';
|
|
12
|
-
{{components-css-imports}}
|
|
13
|
-
import { ClientRouter } from 'astro:transitions';
|
|
14
|
-
import Navigation from '../components/Navigation.astro';
|
|
15
|
-
import Footer from '../components/Footer.astro';
|
|
16
|
-
|
|
17
|
-
interface Props {
|
|
18
|
-
title?: string;
|
|
19
|
-
description?: string;
|
|
20
|
-
hasSeoTags?: boolean;
|
|
21
|
-
}
|
|
22
|
-
const { title, description, hasSeoTags = false } = Astro.props;
|
|
23
|
-
---
|
|
24
|
-
<!doctype html>
|
|
25
|
-
<html lang="en">
|
|
26
|
-
<head>
|
|
27
|
-
<meta charset="utf-8" />
|
|
28
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
29
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E" />
|
|
30
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
31
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
32
|
-
<link rel="stylesheet" href="{{fonts.googleHref}}" />
|
|
33
|
-
{!hasSeoTags && (
|
|
34
|
-
<>
|
|
35
|
-
<title>{title ? `${title} · {{brand.name}}` : "{{brand.name}}"}</title>
|
|
36
|
-
{description && <meta name="description" content={description} />}
|
|
37
|
-
</>
|
|
38
|
-
)}
|
|
39
|
-
<slot name="head" />
|
|
40
|
-
<ClientRouter />
|
|
41
|
-
</head>
|
|
42
|
-
<body>
|
|
43
|
-
<div class="nav-progress" data-nav-progress aria-hidden="true"></div>
|
|
44
|
-
<Navigation />
|
|
45
|
-
<main><slot /></main>
|
|
46
|
-
<Footer />
|
|
47
|
-
<script>
|
|
48
|
-
// Persisted rail aria-current sync + top loading bar + rail-anchored
|
|
49
|
-
// scroll across listing-to-listing navigation. Stores pack relies on
|
|
50
|
-
// all three; do not split into separate scripts.
|
|
51
|
-
function syncCategoryRail() {
|
|
52
|
-
const path = window.location.pathname;
|
|
53
|
-
const activeSlug = path.startsWith("/category/")
|
|
54
|
-
? path.slice("/category/".length).replace(/\/$/, "")
|
|
55
|
-
: path === "/products" || path === "/products/"
|
|
56
|
-
? ""
|
|
57
|
-
: null;
|
|
58
|
-
if (activeSlug === null) return;
|
|
59
|
-
const pills = document.querySelectorAll<HTMLAnchorElement>(
|
|
60
|
-
"[data-category-rail] .category-pill",
|
|
61
|
-
);
|
|
62
|
-
for (const pill of pills) {
|
|
63
|
-
const slug = pill.dataset.categorySlug ?? "";
|
|
64
|
-
if (slug === activeSlug) pill.setAttribute("aria-current", "page");
|
|
65
|
-
else pill.removeAttribute("aria-current");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// Eager click feedback — flip aria-current before the network call.
|
|
69
|
-
document.addEventListener("click", (event) => {
|
|
70
|
-
const target = event.target as HTMLElement | null;
|
|
71
|
-
const pill = target?.closest<HTMLAnchorElement>(
|
|
72
|
-
"[data-category-rail] .category-pill",
|
|
73
|
-
);
|
|
74
|
-
if (!pill) return;
|
|
75
|
-
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
76
|
-
const pills = document.querySelectorAll<HTMLAnchorElement>(
|
|
77
|
-
"[data-category-rail] .category-pill",
|
|
78
|
-
);
|
|
79
|
-
for (const p of pills) p.removeAttribute("aria-current");
|
|
80
|
-
pill.setAttribute("aria-current", "page");
|
|
81
|
-
}, true);
|
|
82
|
-
const progress = document.querySelector<HTMLElement>("[data-nav-progress]");
|
|
83
|
-
function startProgress() {
|
|
84
|
-
if (!progress) return;
|
|
85
|
-
progress.dataset.state = "loading";
|
|
86
|
-
document.body.dataset.navigating = "true";
|
|
87
|
-
}
|
|
88
|
-
function endProgress() {
|
|
89
|
-
if (!progress) return;
|
|
90
|
-
progress.dataset.state = "done";
|
|
91
|
-
delete document.body.dataset.navigating;
|
|
92
|
-
window.setTimeout(() => {
|
|
93
|
-
if (progress.dataset.state === "done") delete progress.dataset.state;
|
|
94
|
-
}, 320);
|
|
95
|
-
}
|
|
96
|
-
// Anchor the persisted rail across listing-to-listing navigation. The
|
|
97
|
-
// page-header above the rail differs between /products and /category/[slug]
|
|
98
|
-
// (breadcrumbs, lede, optional image), so naive scroll preservation reads
|
|
99
|
-
// as a jump. Capture rail.getBoundingClientRect().top before the swap and
|
|
100
|
-
// re-equalize after.
|
|
101
|
-
const isListing = (p: string) =>
|
|
102
|
-
p === "/products" || p === "/products/" || p.startsWith("/category/");
|
|
103
|
-
let railAnchorTop: number | null = null;
|
|
104
|
-
document.addEventListener("astro:before-preparation", (event: any) => {
|
|
105
|
-
const from = window.location.pathname;
|
|
106
|
-
const to = event?.to?.pathname ?? from;
|
|
107
|
-
const rail = document.querySelector<HTMLElement>("[data-category-rail]");
|
|
108
|
-
railAnchorTop =
|
|
109
|
-
rail && isListing(from) && isListing(to)
|
|
110
|
-
? rail.getBoundingClientRect().top
|
|
111
|
-
: null;
|
|
112
|
-
startProgress();
|
|
113
|
-
});
|
|
114
|
-
document.addEventListener("astro:after-swap", () => {
|
|
115
|
-
syncCategoryRail();
|
|
116
|
-
if (railAnchorTop !== null) {
|
|
117
|
-
const rail = document.querySelector<HTMLElement>("[data-category-rail]");
|
|
118
|
-
if (rail) {
|
|
119
|
-
const delta = rail.getBoundingClientRect().top - railAnchorTop;
|
|
120
|
-
if (Math.abs(delta) > 0.5) {
|
|
121
|
-
window.scrollBy({ top: delta, left: 0, behavior: "instant" as ScrollBehavior });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
railAnchorTop = null;
|
|
126
|
-
endProgress();
|
|
127
|
-
});
|
|
128
|
-
document.addEventListener("astro:page-load", endProgress);
|
|
129
|
-
syncCategoryRail();
|
|
130
|
-
</script>
|
|
131
|
-
</body>
|
|
132
|
-
</html>
|
|
@@ -1,38 +0,0 @@
|
|
|
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-6 flex items-center justify-between gap-6 py-4">
|
|
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-4 md:gap-6 items-start md:items-center
|
|
29
|
-
bg-paper md:bg-transparent border-b md:border-0 border-rule px-6 md:px-0 py-4 md:py-0"
|
|
30
|
-
>
|
|
31
|
-
<ul class="flex flex-col md:flex-row gap-4 md:gap-6 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>
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { availabilityTimeSlots, eventTimeSlots } from "@wix/bookings";
|
|
3
|
-
import type { SelectedSlot } from "./bookingDriver";
|
|
4
|
-
import { STAFF_MEMBER_RESOURCE_TYPE_ID } from "./bookingDriver";
|
|
5
|
-
|
|
6
|
-
// AvailabilityCalendar.tsx — client:only="react" island. A week calendar: a
|
|
7
|
-
// 7-day strip with week navigation, then the picked day's time slots. This is
|
|
8
|
-
// the usable shape — a flat list of every slot with time-only labels leaves the
|
|
9
|
-
// visitor unable to tell which day a slot is on.
|
|
10
|
-
//
|
|
11
|
-
// SDK calls run ambiently (the @wix/astro visitor client), like the ecom
|
|
12
|
-
// CartView island — no createClient/OAuthStrategy here. On an own/own-build SPA
|
|
13
|
-
// acquire a visitor client (OAuthStrategy) and call the same functions on it.
|
|
14
|
-
//
|
|
15
|
-
// Branches on serviceType:
|
|
16
|
-
// APPOINTMENT → availabilityTimeSlots.listAvailabilityTimeSlots() (serviceId is
|
|
17
|
-
// a single GUID string; slots carry scheduleId).
|
|
18
|
-
// CLASS → eventTimeSlots.listEventTimeSlots() (serviceIds is an array;
|
|
19
|
-
// slots carry eventInfo.eventId and NO scheduleId).
|
|
20
|
-
//
|
|
21
|
-
// LOCATION SCOPING (multi-location): listAvailabilityTimeSlots returns one slot
|
|
22
|
-
// PER LOCATION per time — "if locations is not specified, returns time slots for
|
|
23
|
-
// all locations where the service is available". A service offered at >1 location
|
|
24
|
-
// therefore yields DUPLICATE same-time rows unless the call is scoped to a single
|
|
25
|
-
// location. So availability is ALWAYS scoped to one business location: the picker
|
|
26
|
-
// below defaults to the carried/first location and the call always passes a
|
|
27
|
-
// single-element `locations` filter. (One location per availability call — the
|
|
28
|
-
// same single-location guarantee the location-first SoT gets from its selector.)
|
|
29
|
-
//
|
|
30
|
-
// STAFF: a single slot carries multiple staff in availableResources, so staff is
|
|
31
|
-
// a per-slot dropdown — it never multiplies rows. Only location multiplies rows.
|
|
32
|
-
// Re-export SelectedSlot from bookingDriver so the form/driver share one shape.
|
|
33
|
-
export type { SelectedSlot } from "./bookingDriver";
|
|
34
|
-
|
|
35
|
-
// A staff member as carried on the service (service.staffMemberDetails.staffMembers[]),
|
|
36
|
-
// fetched via conditionalFields:["STAFF_MEMBER_DETAILS"]. NOTE: the id field is
|
|
37
|
-
// `staffMemberId` which — despite the name — IS the resource id (it matches the
|
|
38
|
-
// service's staffMemberIds and the Staff Members API resourceId). That id is what
|
|
39
|
-
// filters availability and books the resource.
|
|
40
|
-
export interface StaffMemberOption {
|
|
41
|
-
staffMemberId?: string;
|
|
42
|
-
name?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// A business location the service is offered at (derived from service.locations[]).
|
|
46
|
-
export interface LocationOption {
|
|
47
|
-
_id: string;
|
|
48
|
-
name?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface Props {
|
|
52
|
-
serviceId: string;
|
|
53
|
-
serviceName: string;
|
|
54
|
-
serviceType: "APPOINTMENT" | "CLASS";
|
|
55
|
-
staffMembers?: StaffMemberOption[];
|
|
56
|
-
locations?: LocationOption[]; // business locations the service offers
|
|
57
|
-
locationId?: string; // the location chosen on the catalog (the default scope), if any
|
|
58
|
-
onSlotSelected: (slot: SelectedSlot) => void;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface FetchOpts {
|
|
62
|
-
resourceId?: string; // a staff resource id to filter by (omit for "any staff")
|
|
63
|
-
locationId?: string; // a business location id to scope to (omit only when the service has none)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const DAY_MS = 86_400_000;
|
|
67
|
-
const HORIZON_WEEKS = 13; // how far "check next availability" probes forward
|
|
68
|
-
const ANY_STAFF = ""; // the "any staff member" sentinel for the select value
|
|
69
|
-
|
|
70
|
-
const pad = (n: number) => String(n).padStart(2, "0");
|
|
71
|
-
// Local date string YYYY-MM-DDThh:mm:ss (no Z) — availability expects local time.
|
|
72
|
-
const localDateString = (d: Date) =>
|
|
73
|
-
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
74
|
-
const tz = () => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
75
|
-
const dateKey = (iso: string) => iso.slice(0, 10); // group slots by calendar day
|
|
76
|
-
const timeLabel = (iso: string) =>
|
|
77
|
-
new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
78
|
-
|
|
79
|
-
const startOfDay = (d: Date) => { const n = new Date(d); n.setHours(0, 0, 0, 0); return n; };
|
|
80
|
-
// Monday-start week containing `d`.
|
|
81
|
-
const mondayOf = (d: Date) => {
|
|
82
|
-
const n = startOfDay(d);
|
|
83
|
-
const day = (n.getDay() + 6) % 7; // 0 = Monday
|
|
84
|
-
n.setDate(n.getDate() - day);
|
|
85
|
-
return n;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// Fetch one week of availability and group the slots by calendar day.
|
|
89
|
-
const fetchWeek = async (
|
|
90
|
-
serviceId: string,
|
|
91
|
-
serviceType: "APPOINTMENT" | "CLASS",
|
|
92
|
-
weekStart: Date,
|
|
93
|
-
opts: FetchOpts = {},
|
|
94
|
-
): Promise<Record<string, SelectedSlot[]>> => {
|
|
95
|
-
const from = startOfDay(weekStart);
|
|
96
|
-
const to = new Date(from.getTime() + 7 * DAY_MS);
|
|
97
|
-
const timeZone = tz();
|
|
98
|
-
let raw: any[] = [];
|
|
99
|
-
|
|
100
|
-
if (serviceType === "CLASS") {
|
|
101
|
-
// CLASS filters via eventFilter: staff by resources.id ($hasSome), location by
|
|
102
|
-
// location.id (a bare array, NOT a $hasSome operator).
|
|
103
|
-
const eventFilter: Record<string, any> = {};
|
|
104
|
-
if (opts.resourceId) eventFilter["resources.id"] = { $hasSome: [opts.resourceId] };
|
|
105
|
-
if (opts.locationId) eventFilter["location.id"] = [opts.locationId];
|
|
106
|
-
const result = await eventTimeSlots.listEventTimeSlots({
|
|
107
|
-
serviceIds: [serviceId], // CLASS API takes an array
|
|
108
|
-
fromLocalDate: localDateString(from),
|
|
109
|
-
toLocalDate: localDateString(to),
|
|
110
|
-
timeZone,
|
|
111
|
-
includeNonBookable: false,
|
|
112
|
-
cursorPaging: { limit: 100 },
|
|
113
|
-
...(Object.keys(eventFilter).length ? { eventFilter } : {}),
|
|
114
|
-
});
|
|
115
|
-
raw = (result.timeSlots ?? []).map((s: any) => ({
|
|
116
|
-
serviceType: "CLASS" as const,
|
|
117
|
-
serviceId,
|
|
118
|
-
localStartDate: s.localStartDate,
|
|
119
|
-
localEndDate: s.localEndDate,
|
|
120
|
-
timezone: result.timeZone ?? timeZone,
|
|
121
|
-
eventId: s.eventInfo?.eventId, // session id — no scheduleId on event slots
|
|
122
|
-
}));
|
|
123
|
-
} else {
|
|
124
|
-
// APPOINTMENT filters staff via resourceTypes (resourceIds), location via locations[].
|
|
125
|
-
// Scoping to a single location collapses the per-location duplicate rows.
|
|
126
|
-
const result = await availabilityTimeSlots.listAvailabilityTimeSlots({
|
|
127
|
-
serviceId, // single GUID string — NOT an array
|
|
128
|
-
fromLocalDate: localDateString(from),
|
|
129
|
-
toLocalDate: localDateString(to),
|
|
130
|
-
timeZone,
|
|
131
|
-
bookable: true,
|
|
132
|
-
cursorPaging: { limit: 100 },
|
|
133
|
-
...(opts.resourceId
|
|
134
|
-
? {
|
|
135
|
-
resourceTypes: [
|
|
136
|
-
{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds: [opts.resourceId] },
|
|
137
|
-
],
|
|
138
|
-
includeResourceTypeIds: [STAFF_MEMBER_RESOURCE_TYPE_ID],
|
|
139
|
-
}
|
|
140
|
-
: {}),
|
|
141
|
-
...(opts.locationId
|
|
142
|
-
? { locations: [{ _id: opts.locationId, locationType: "BUSINESS" as const }] }
|
|
143
|
-
: {}),
|
|
144
|
-
});
|
|
145
|
-
raw = (result.timeSlots ?? []).map((s: any) => ({
|
|
146
|
-
serviceType: "APPOINTMENT" as const,
|
|
147
|
-
serviceId,
|
|
148
|
-
localStartDate: s.localStartDate,
|
|
149
|
-
localEndDate: s.localEndDate,
|
|
150
|
-
timezone: result.timeZone ?? timeZone,
|
|
151
|
-
scheduleId: s.scheduleId,
|
|
152
|
-
locationId: s.location?._id, // the id field is _id, not .id
|
|
153
|
-
locationType: s.location?.locationType,
|
|
154
|
-
}));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const byDay: Record<string, SelectedSlot[]> = {};
|
|
158
|
-
for (const slot of raw) {
|
|
159
|
-
if (!slot.localStartDate) continue;
|
|
160
|
-
(byDay[dateKey(slot.localStartDate)] ||= []).push(slot);
|
|
161
|
-
}
|
|
162
|
-
return byDay;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
export default function AvailabilityCalendar({
|
|
166
|
-
serviceId,
|
|
167
|
-
serviceName,
|
|
168
|
-
serviceType,
|
|
169
|
-
staffMembers,
|
|
170
|
-
locations,
|
|
171
|
-
locationId,
|
|
172
|
-
onSlotSelected,
|
|
173
|
-
}: Props) {
|
|
174
|
-
const thisMonday = useMemo(() => mondayOf(new Date()), []);
|
|
175
|
-
const [weekStart, setWeekStart] = useState<Date>(thisMonday);
|
|
176
|
-
const [byDay, setByDay] = useState<Record<string, SelectedSlot[]>>({});
|
|
177
|
-
const [selectedDay, setSelectedDay] = useState<string | null>(null);
|
|
178
|
-
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
|
179
|
-
const [status, setStatus] = useState<"loading" | "ready" | "error" | "searching">("loading");
|
|
180
|
-
// The chosen staff resource id ("" = any staff member → ANY_RESOURCE fallback).
|
|
181
|
-
const [selectedResource, setSelectedResource] = useState<string>(ANY_STAFF);
|
|
182
|
-
|
|
183
|
-
// Staff options keyed by resource id; picker shown only when >1 staff.
|
|
184
|
-
const staffOptions = useMemo(
|
|
185
|
-
() =>
|
|
186
|
-
(staffMembers ?? [])
|
|
187
|
-
.map((s) => ({ id: s.staffMemberId ?? "", name: s.name ?? "Staff member" }))
|
|
188
|
-
.filter((s) => s.id),
|
|
189
|
-
[staffMembers],
|
|
190
|
-
);
|
|
191
|
-
const showStaffPicker = staffOptions.length > 1;
|
|
192
|
-
|
|
193
|
-
// Business-location options. Availability is always scoped to ONE of these (the
|
|
194
|
-
// carried location if it's one of them, else the first), so multi-location
|
|
195
|
-
// services don't return duplicate per-location rows. Picker shown only when >1.
|
|
196
|
-
const locationOptions = useMemo(
|
|
197
|
-
() => (locations ?? []).filter((l) => l._id),
|
|
198
|
-
[locations],
|
|
199
|
-
);
|
|
200
|
-
const [selectedLocation, setSelectedLocation] = useState<string>(() => {
|
|
201
|
-
if (locationId && locationOptions.some((l) => l._id === locationId)) return locationId;
|
|
202
|
-
return locationOptions[0]?._id ?? "";
|
|
203
|
-
});
|
|
204
|
-
const showLocationPicker = locationOptions.length > 1;
|
|
205
|
-
|
|
206
|
-
const days = useMemo(
|
|
207
|
-
() => Array.from({ length: 7 }, (_, i) => new Date(weekStart.getTime() + i * DAY_MS)),
|
|
208
|
-
[weekStart],
|
|
209
|
-
);
|
|
210
|
-
const atFirstWeek = weekStart.getTime() <= thisMonday.getTime();
|
|
211
|
-
|
|
212
|
-
const fetchOpts = useCallback(
|
|
213
|
-
(): FetchOpts => ({
|
|
214
|
-
resourceId: selectedResource || undefined,
|
|
215
|
-
locationId: selectedLocation || undefined,
|
|
216
|
-
}),
|
|
217
|
-
[selectedResource, selectedLocation],
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
const load = useCallback(async (start: Date) => {
|
|
221
|
-
setStatus("loading"); setSelectedKey(null);
|
|
222
|
-
try {
|
|
223
|
-
const grouped = await fetchWeek(serviceId, serviceType, start, fetchOpts());
|
|
224
|
-
setByDay(grouped);
|
|
225
|
-
const firstDayWithSlots = Object.keys(grouped).sort()[0] ?? null;
|
|
226
|
-
setSelectedDay(firstDayWithSlots);
|
|
227
|
-
setStatus("ready");
|
|
228
|
-
} catch (err) {
|
|
229
|
-
console.error("[availability] list failed:", err);
|
|
230
|
-
setStatus("error");
|
|
231
|
-
}
|
|
232
|
-
}, [serviceId, serviceType, fetchOpts]);
|
|
233
|
-
|
|
234
|
-
// Reload whenever the week OR the staff/location filter changes.
|
|
235
|
-
useEffect(() => { void load(weekStart); }, [weekStart, load]);
|
|
236
|
-
|
|
237
|
-
const shiftWeek = (deltaWeeks: number) =>
|
|
238
|
-
setWeekStart((prev) => startOfDay(new Date(prev.getTime() + deltaWeeks * 7 * DAY_MS)));
|
|
239
|
-
|
|
240
|
-
// Probe forward week-by-week for the next week that has any slots.
|
|
241
|
-
const checkNextAvailability = useCallback(async () => {
|
|
242
|
-
setStatus("searching");
|
|
243
|
-
let cursor = new Date(weekStart.getTime() + 7 * DAY_MS);
|
|
244
|
-
for (let i = 0; i < HORIZON_WEEKS; i++) {
|
|
245
|
-
try {
|
|
246
|
-
const grouped = await fetchWeek(serviceId, serviceType, cursor, fetchOpts());
|
|
247
|
-
if (Object.keys(grouped).length > 0) {
|
|
248
|
-
setWeekStart(cursor); // triggers load() via the effect
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
} catch { /* keep probing */ }
|
|
252
|
-
cursor = new Date(cursor.getTime() + 7 * DAY_MS);
|
|
253
|
-
}
|
|
254
|
-
setStatus("ready"); // nothing found within the horizon
|
|
255
|
-
}, [serviceId, serviceType, weekStart, fetchOpts]);
|
|
256
|
-
|
|
257
|
-
const handleSelect = (s: SelectedSlot) => {
|
|
258
|
-
const id = s.serviceType === "CLASS" ? s.eventId : s.scheduleId;
|
|
259
|
-
if (!s.localStartDate || !id) return;
|
|
260
|
-
setSelectedKey(`${s.localStartDate}|${id}`);
|
|
261
|
-
// When a specific staff was chosen, record it on the slot so the booking books
|
|
262
|
-
// that resource; otherwise the driver emits the ANY_RESOURCE fallback.
|
|
263
|
-
const chosen = selectedResource
|
|
264
|
-
? staffOptions.find((o) => o.id === selectedResource)
|
|
265
|
-
: null;
|
|
266
|
-
onSlotSelected(chosen ? { ...s, resource: { _id: chosen.id, name: chosen.name } } : s);
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const weekLabel = `${days[0].toLocaleDateString([], { month: "short", day: "numeric" })} – ${days[6].toLocaleDateString([], { month: "short", day: "numeric" })}`;
|
|
270
|
-
const daySlots = selectedDay ? byDay[selectedDay] ?? [] : [];
|
|
271
|
-
const weekIsEmpty = status === "ready" && Object.keys(byDay).length === 0;
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
<div className="availability-calendar">
|
|
275
|
-
{(showLocationPicker || showStaffPicker) && (
|
|
276
|
-
<div className="availability-filters">
|
|
277
|
-
{showLocationPicker && (
|
|
278
|
-
<div className="availability-staff">
|
|
279
|
-
<label className="availability-staff-label" htmlFor="location-select">Location</label>
|
|
280
|
-
<select
|
|
281
|
-
id="location-select"
|
|
282
|
-
className="availability-staff-select"
|
|
283
|
-
value={selectedLocation}
|
|
284
|
-
onChange={(e) => { setSelectedLocation(e.target.value); setSelectedKey(null); }}
|
|
285
|
-
>
|
|
286
|
-
{locationOptions.map((o) => (
|
|
287
|
-
<option key={o._id} value={o._id}>{o.name ?? "Location"}</option>
|
|
288
|
-
))}
|
|
289
|
-
</select>
|
|
290
|
-
</div>
|
|
291
|
-
)}
|
|
292
|
-
{showStaffPicker && (
|
|
293
|
-
<div className="availability-staff">
|
|
294
|
-
<label className="availability-staff-label" htmlFor="staff-select">Staff member</label>
|
|
295
|
-
<select
|
|
296
|
-
id="staff-select"
|
|
297
|
-
className="availability-staff-select"
|
|
298
|
-
value={selectedResource}
|
|
299
|
-
onChange={(e) => { setSelectedResource(e.target.value); setSelectedKey(null); }}
|
|
300
|
-
>
|
|
301
|
-
<option value={ANY_STAFF}>Any staff member</option>
|
|
302
|
-
{staffOptions.map((o) => (
|
|
303
|
-
<option key={o.id} value={o.id}>{o.name}</option>
|
|
304
|
-
))}
|
|
305
|
-
</select>
|
|
306
|
-
</div>
|
|
307
|
-
)}
|
|
308
|
-
</div>
|
|
309
|
-
)}
|
|
310
|
-
|
|
311
|
-
<div className="availability-week-nav">
|
|
312
|
-
<button type="button" className="availability-nav-btn" onClick={() => shiftWeek(-1)} disabled={atFirstWeek} aria-label="Previous week">← Prev week</button>
|
|
313
|
-
<span className="availability-week-label">{weekLabel}</span>
|
|
314
|
-
<button type="button" className="availability-nav-btn" onClick={() => shiftWeek(1)} aria-label="Next week">Next week →</button>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
<div className="availability-day-strip" role="group" aria-label="Pick a day">
|
|
318
|
-
{days.map((d) => {
|
|
319
|
-
const key = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
320
|
-
const has = (byDay[key]?.length ?? 0) > 0;
|
|
321
|
-
const isSelected = key === selectedDay;
|
|
322
|
-
return (
|
|
323
|
-
<button
|
|
324
|
-
key={key}
|
|
325
|
-
type="button"
|
|
326
|
-
className={["availability-day", isSelected ? "availability-day--selected" : "", has ? "availability-day--has-slots" : "availability-day--empty"].filter(Boolean).join(" ")}
|
|
327
|
-
disabled={!has}
|
|
328
|
-
aria-pressed={isSelected}
|
|
329
|
-
onClick={() => { setSelectedDay(key); setSelectedKey(null); }}
|
|
330
|
-
>
|
|
331
|
-
<span className="availability-day-name">{d.toLocaleDateString([], { weekday: "short" })}</span>
|
|
332
|
-
<span className="availability-day-num">{d.getDate()}</span>
|
|
333
|
-
</button>
|
|
334
|
-
);
|
|
335
|
-
})}
|
|
336
|
-
</div>
|
|
337
|
-
|
|
338
|
-
{status === "loading" && <p className="availability-loading">Checking availability…</p>}
|
|
339
|
-
{status === "searching" && <p className="availability-loading">Searching for the next available time…</p>}
|
|
340
|
-
{status === "error" && (
|
|
341
|
-
<>
|
|
342
|
-
<p className="availability-error">Could not load availability — please try again.</p>
|
|
343
|
-
<button type="button" className="availability-nav-btn" onClick={() => void load(weekStart)}>Retry</button>
|
|
344
|
-
</>
|
|
345
|
-
)}
|
|
346
|
-
{weekIsEmpty && (
|
|
347
|
-
<div className="availability-empty">
|
|
348
|
-
<p>No availability this week.</p>
|
|
349
|
-
<button type="button" className="availability-nav-btn" onClick={() => void checkNextAvailability()}>Check next availability</button>
|
|
350
|
-
</div>
|
|
351
|
-
)}
|
|
352
|
-
{status === "ready" && daySlots.length > 0 && (
|
|
353
|
-
<div className="availability-slots" role="group" aria-label={`Available times for ${serviceName}`}>
|
|
354
|
-
{daySlots.map((s) => {
|
|
355
|
-
const id = s.serviceType === "CLASS" ? s.eventId : s.scheduleId;
|
|
356
|
-
const key = `${s.localStartDate}|${id}`;
|
|
357
|
-
const isSelected = key === selectedKey;
|
|
358
|
-
return (
|
|
359
|
-
<button
|
|
360
|
-
key={key}
|
|
361
|
-
type="button"
|
|
362
|
-
className={["time-slot", isSelected ? "time-slot--selected" : "time-slot--available"].join(" ")}
|
|
363
|
-
aria-pressed={isSelected}
|
|
364
|
-
onClick={() => handleSelect(s)}
|
|
365
|
-
>
|
|
366
|
-
<span className="time-slot-time">{timeLabel(s.localStartDate)}</span>
|
|
367
|
-
</button>
|
|
368
|
-
);
|
|
369
|
-
})}
|
|
370
|
-
</div>
|
|
371
|
-
)}
|
|
372
|
-
</div>
|
|
373
|
-
);
|
|
374
|
-
}
|