@wix/agent-skills 1.20.0 → 1.20.1
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/package.json +1 -1
- package/skills/wix-headless-fast/SKILL.md +200 -34
- package/skills/wix-headless-fast/entry/skill.md +10 -2
- package/skills/wix-headless-fast/install/attach.mjs +253 -0
- package/skills/wix-headless-fast/install/deploy.mjs +109 -9
- package/skills/wix-headless-fast/install/fast-path.mjs +29 -34
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
- package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
- package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
- package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
- package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
- package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
- package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
- package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
- package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
- package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
- package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
- package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
- package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
- package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
- package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
- package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
- package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
- package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
- package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
- package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
- package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
- package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
- package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
- package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
- package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +182 -63
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
- package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
- package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -9
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +6 -35
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Post reads over REST — the twin of app/wix/blog/posts.ts. Same exports, same DTOs; the rules and
|
|
2
|
+
// mappers come from posts-core (the SAME file the SDK transport uses, deployed flat next to this one
|
|
3
|
+
// by deploy.mjs --stack static), so this file is only the transport: one fetch with a literal body
|
|
4
|
+
// per function. Every call here is safe from a browser with a visitor token — only PUBLISHED posts
|
|
5
|
+
// come back to it. Porting: keep the paths, keep the bodies, port the core once.
|
|
6
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/posts-stats/query-posts.md
|
|
7
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/posts-stats/get-post-by-slug.md
|
|
8
|
+
import { wixRequest } from "./client.js";
|
|
9
|
+
import { imgSrc } from "./media.js";
|
|
10
|
+
import { BLOG_APP_ID, DETAIL_FIELDSETS, feedQuery, postMetaLine, slugQuery, toDetail, toSummary, type FetchPostsOptions, type Raw } from "./posts-core.js";
|
|
11
|
+
import type { PostDetail, PostPage } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export { BLOG_APP_ID, postMetaLine, type FetchPostsOptions };
|
|
14
|
+
|
|
15
|
+
const POSTS_QUERY = "/blog/v3/posts/query";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One page of published posts, newest first (pinned posts lead); `nextCursor` continues the same
|
|
19
|
+
* query — send it alone, the filters ride inside it.
|
|
20
|
+
* POST /blog/v3/posts/query { query: { filter?, sort, cursorPaging: { limit } } } — first page
|
|
21
|
+
* POST /blog/v3/posts/query { query: { cursorPaging: { limit, cursor } } } — later pages
|
|
22
|
+
*/
|
|
23
|
+
export async function fetchPosts(o: FetchPostsOptions = {}): Promise<PostPage> {
|
|
24
|
+
const res = await wixRequest<Raw>(POSTS_QUERY, { body: { query: feedQuery(o) } });
|
|
25
|
+
const paging: Raw = res?.pagingMetadata ?? {};
|
|
26
|
+
return {
|
|
27
|
+
posts: ((res?.posts ?? []) as Raw[]).map((p) => toSummary(p, imgSrc)),
|
|
28
|
+
nextCursor: paging.hasNext ? (paging.cursors?.next ?? null) : null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One post by URL slug with the full body (the RICH_CONTENT + CONTENT_TEXT fieldsets — without
|
|
34
|
+
* them richContent and contentText are absent). Null when the slug resolves to nothing — a real
|
|
35
|
+
* 404 for the page, never a fallback to another post.
|
|
36
|
+
* POST /blog/v3/posts/query { fieldsets: ["RICH_CONTENT", "CONTENT_TEXT"], query: { filter: { slug: { $eq } }, cursorPaging: { limit: 1 } } }
|
|
37
|
+
*/
|
|
38
|
+
export async function fetchPostBySlug(slug: string): Promise<PostDetail | null> {
|
|
39
|
+
const res = await wixRequest<Raw>(POSTS_QUERY, { body: { fieldsets: DETAIL_FIELDSETS, query: slugQuery(slug) } });
|
|
40
|
+
const raw: Raw | undefined = res?.posts?.[0];
|
|
41
|
+
return raw ? toDetail(raw, imgSrc) : null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Category + tag reads over REST — the twin of app/wix/blog/taxonomy.ts. Same exports, same DTOs;
|
|
2
|
+
// the mappers come from taxonomy-core (the SAME file the SDK transport uses, deployed flat next to
|
|
3
|
+
// this one). Taxonomy queries page by OFFSET (`paging`), unlike posts (`cursorPaging`); a blog's
|
|
4
|
+
// taxonomy fits in one read of 100. All four calls are visitor-safe.
|
|
5
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/category/query-categories.md
|
|
6
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/category/get-category-by-slug.md
|
|
7
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/tags/query-tags.md
|
|
8
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/tags/get-tag-by-slug.md
|
|
9
|
+
import { wixRequest } from "./client.js";
|
|
10
|
+
import { imgSrc } from "./media.js";
|
|
11
|
+
import { CATEGORY_SORT, TAG_SORT, TAXONOMY_LIMIT, toCategory, toTag } from "./taxonomy-core.js";
|
|
12
|
+
import type { Raw } from "./posts-core.js";
|
|
13
|
+
import type { BlogCategory, BlogTag } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Categories in menu order. Non-fatal: [] on failure (the feed renders without its filter row).
|
|
17
|
+
* POST /blog/v3/categories/query { query: { sort: [{ fieldName: "displayPosition", order: "ASC" }], paging: { limit: 100 } } }
|
|
18
|
+
*/
|
|
19
|
+
export async function fetchBlogCategories(): Promise<BlogCategory[]> {
|
|
20
|
+
try {
|
|
21
|
+
const res = await wixRequest<Raw>("/blog/v3/categories/query", { body: { query: { sort: CATEGORY_SORT, paging: { limit: TAXONOMY_LIMIT } } } });
|
|
22
|
+
return ((res?.categories ?? []) as Raw[]).map((c) => toCategory(c, imgSrc)).filter((c) => c.id);
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tags, most-published-posts first. Non-fatal: [] on failure.
|
|
30
|
+
* POST /blog/v3/tags/query { query: { sort: [{ fieldName: "publishedPostCount", order: "DESC" }], paging: { limit: 100 } } }
|
|
31
|
+
*/
|
|
32
|
+
export async function fetchBlogTags(): Promise<BlogTag[]> {
|
|
33
|
+
try {
|
|
34
|
+
const res = await wixRequest<Raw>("/blog/v3/tags/query", { body: { query: { sort: TAG_SORT, paging: { limit: TAXONOMY_LIMIT } } } });
|
|
35
|
+
return ((res?.tags ?? []) as Raw[]).map(toTag).filter((t) => t.id);
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The two by-slug getters answer in DIFFERENT envelopes ({ category } and { tag }); a missing slug
|
|
42
|
+
// is a 404 — mapped to null here, as the SDK transport does.
|
|
43
|
+
|
|
44
|
+
/** One category by its URL slug. Null when not found. GET /blog/v3/categories/slugs/{slug} */
|
|
45
|
+
export async function fetchCategoryBySlug(slug: string): Promise<BlogCategory | null> {
|
|
46
|
+
try {
|
|
47
|
+
const res = await wixRequest<Raw>(`/blog/v3/categories/slugs/${encodeURIComponent(slug)}`, { method: "GET" });
|
|
48
|
+
return res?.category ? toCategory(res.category, imgSrc) : null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One tag by its URL slug. Null when not found. GET /blog/v3/tags/slugs/{slug} */
|
|
55
|
+
export async function fetchTagBySlug(slug: string): Promise<BlogTag | null> {
|
|
56
|
+
try {
|
|
57
|
+
const res = await wixRequest<Raw>(`/blog/v3/tags/slugs/${encodeURIComponent(slug)}`, { method: "GET" });
|
|
58
|
+
return res?.tag ? toTag(res.tag) : null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -62,6 +62,15 @@ feed without covers looks broken.
|
|
|
62
62
|
**Seeding is additive — never delete or overwrite existing content**; ask first if a cleanup
|
|
63
63
|
seems needed.
|
|
64
64
|
|
|
65
|
+
## Supplied content
|
|
66
|
+
|
|
67
|
+
The general rules are in `references/shared/SUPPLIED-CONTENT.md`. For a blog, each post the user
|
|
68
|
+
hands over is one entry. Its title becomes `title`; its body becomes `content` blocks, one block per
|
|
69
|
+
paragraph, heading or list, whether the body arrived as text, HTML or Markdown; a category becomes
|
|
70
|
+
`category` (and joins the `categories` list); tags become `tags`; their cover image becomes
|
|
71
|
+
`coverImageUrl`. A post with no body is a question for the user. Authors and publish dates are
|
|
72
|
+
not seeded — say so.
|
|
73
|
+
|
|
65
74
|
## Escape hatch — individual functions
|
|
66
75
|
`setupBlog` composes exported steps — `installBlogApp`, `getAuthorMemberId`, `createPosts`,
|
|
67
76
|
`createCategories`, `createTags`, `importImage`, `attachPostCovers`, plus `makeCtx()` —
|
|
@@ -8,18 +8,25 @@ the home page and the brand. You never write booking logic; you never skip desig
|
|
|
8
8
|
|
|
9
9
|
## The file map (deployed into `src/`)
|
|
10
10
|
|
|
11
|
-
**
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
**On Astro and React the shipped files are tested and work as they are** — this table and the
|
|
12
|
+
contracts below are everything you need to use them, so don't spend the run reading their source;
|
|
13
|
+
wire them and build your surfaces. Reading them is the right move when something is off (a runtime
|
|
14
|
+
error, a field this playbook doesn't cover) or when the brief wants a behaviour they don't offer —
|
|
15
|
+
then read the file that owns it and change or extend it. On `lib`, `static`, and a port the
|
|
16
|
+
components don't deploy at all, and each wiring section below opens with the files to read before
|
|
17
|
+
writing their equivalents. Files you edit: `SiteLayout.astro`, `styles/global.css`, and the two
|
|
18
|
+
pages' island imports. Files you **create**: your listing and booking components, plus your home page.
|
|
14
19
|
|
|
15
20
|
| file | what it is |
|
|
16
21
|
|---|---|
|
|
17
|
-
| `wix/config.ts` · `wix/sdk.ts`
|
|
18
|
-
| `wix/
|
|
19
|
-
| `wix/bookings/
|
|
20
|
-
| `wix/bookings/
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
22
|
+
| `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
|
|
23
|
+
| `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes)` — every `<img>` attribute for a DTO image (`src`, `srcSet`, `sizes`, lazy): `<img {...imgAttrs(s.imageUrl, "33vw")} alt={s.name} />`; `imgSrc()` / `imgSrcSet()` / `formatMoney()` underneath |
|
|
24
|
+
| `wix/bookings/types.ts` | the DTOs (`ServiceSummary`, `ServiceDetail`, `BookingCategory`, `Slot`, `BookingFormField`, `BookingResult`) — contracts below |
|
|
25
|
+
| `wix/bookings/services.ts` | `fetchServices`, `fetchServiceBySlug`, `fetchBookingCategories` — the transport; the rules and DTO mappers are in `services-core.ts` beside it (shared with the REST layer) |
|
|
26
|
+
| `wix/bookings/booking.ts` | `fetchSlots`, `fetchBookingForm`, `bookService` — the transport; the request bodies, the slot and form mappers, and the checkout-or-place rule are in `booking-core.ts` beside it (shared with the REST layer) |
|
|
27
|
+
| `wix/bookings/services-store.ts` · `booking-flow-store.ts` | the listing and booking state machines, framework-free (`createServicesStore()`, `createBookingFlowStore(service)` — `getState`/`subscribe` + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
|
|
28
|
+
| `hooks/bookings/useServices.ts` | React binding of `services-store.ts`: listing + category filter — contract below |
|
|
29
|
+
| `hooks/bookings/useBookingFlow.ts` | React binding of `booking-flow-store.ts`: the whole booking state machine — contract below |
|
|
23
30
|
| `components/bookings/ServicesView.tsx` (+ `ServiceCard`) · `ServiceBookingView.tsx` | **REFERENCE implementations** — correct, plain; build your own instead of shipping them |
|
|
24
31
|
| `styles/global.css` | the design system: Tailwind v4 + the `@theme` token block (shared across verticals) |
|
|
25
32
|
|
|
@@ -45,30 +52,79 @@ Astro stack additionally gets:
|
|
|
45
52
|
Plus the **theme** (`@theme` block, one edit) and the **chrome** (`SiteLayout`, one pass).
|
|
46
53
|
Style everything with Tailwind utilities on the tokens.
|
|
47
54
|
|
|
55
|
+
### What a complete booking site shows (recommended defaults)
|
|
56
|
+
|
|
57
|
+
Defaults for a brief that says nothing about them; the prompt wins where it differs. Look at the
|
|
58
|
+
services before designing (appointments vs classes, categories, staff, prices, images) and design
|
|
59
|
+
for this business, not a stereotype of its category. Then, by default:
|
|
60
|
+
|
|
61
|
+
- **Home:** what the business offers and one booking action in the first screen; real services
|
|
62
|
+
under truthful headings; not a repeat of the listing.
|
|
63
|
+
- **Listing:** a real service card — image, name, duration and price, link — in the first screen;
|
|
64
|
+
category pills only when there is more than one category; `services === null` → skeleton tiles,
|
|
65
|
+
`[]` → your honest empty state.
|
|
66
|
+
- **Service page:** name, price, and the first bookable day's times in the first screen **at 390px
|
|
67
|
+
wide too** — the image is a bounded band on a phone, not a full-screen hero; week paging beside
|
|
68
|
+
the times; the staff filter only when `service.staff.length > 1`; the form under the times, the
|
|
69
|
+
CTA under the form with its label from `service.free` / `service.price`; a week with no times says
|
|
70
|
+
so and points to the next one.
|
|
71
|
+
- **Confirmed state:** rendered only from `confirmed`; a visitor returning from the hosted checkout
|
|
72
|
+
is not a success signal.
|
|
73
|
+
- **A slug that resolves to nothing** shows only the not-found state — no heading or empty picker
|
|
74
|
+
rendered around it.
|
|
75
|
+
- **Copy:** nothing the owner didn't supply — no invented reviews, availability pressure, or
|
|
76
|
+
guarantees; no Wix IDs or technical words in visible text.
|
|
77
|
+
|
|
48
78
|
### The contracts your components consume
|
|
49
79
|
|
|
80
|
+
Everything you need to build on the shipped code; read the source only when something is off.
|
|
81
|
+
|
|
50
82
|
```ts
|
|
51
83
|
// ServiceSummary (tiles) — display-ready:
|
|
52
84
|
// { id, slug, name, tagLine, type: "APPOINTMENT"|"CLASS", price /* "€75.00" | "Free" */,
|
|
53
|
-
// free, durationMinutes|null, imageUrl
|
|
85
|
+
// free, durationMinutes|null, imageUrl /* https or "" */, categoryId|null, staff: [{id,name}] }
|
|
54
86
|
// ServiceDetail adds: description, formId, paymentOption, cancellationFeeEnabled.
|
|
87
|
+
// BookingCategory = { id, name }.
|
|
55
88
|
|
|
56
89
|
// useServices({ initialServices?, initialCategories? }) →
|
|
57
90
|
// { services: ServiceSummary[]|null /* null = loading → skeletons */,
|
|
58
|
-
// categories: [
|
|
91
|
+
// categories: BookingCategory[], activeCategoryId, setActiveCategoryId(id|null), error }
|
|
92
|
+
// Category filtering is client-side (a bookings catalog is small and fully fetched).
|
|
59
93
|
|
|
60
94
|
// useBookingFlow(service: ServiceDetail) →
|
|
61
95
|
// { days: [{ dayKey, dayLabel, slots: Slot[] }] | null, // day-grouped; null = loading
|
|
62
96
|
// windowStart, nextWeek(), prevWeek(), // 7-day paging (prev clamps to today)
|
|
63
97
|
// staffId, setStaffId(id|undefined), // show a picker only when service.staff.length > 1
|
|
64
|
-
// selectedSlot, setSelectedSlot(slot|null), // Slot = { startLocal, endLocal, dayKey, label,
|
|
65
|
-
// formFields: [{ target, label, type, options
|
|
98
|
+
// selectedSlot, setSelectedSlot(slot|null), // Slot = { startLocal, endLocal, dayKey, label, scheduleId|null, eventId|null, staff }
|
|
99
|
+
// formFields: [{ target, label, type, options?, required }], // never empty (contact-basics fallback); only required ones gate canBook
|
|
66
100
|
// values, setValue(target, value), // inputs write here, keyed by target
|
|
67
101
|
// canBook, // gate the CTA on this
|
|
68
|
-
// book(): Promise<BookingResult>, // paid → the browser
|
|
102
|
+
// book(): Promise<BookingResult>, // paid → the browser navigates to the Wix checkout;
|
|
69
103
|
// booking, confirmed, error } // free/offline → confirmed is set (REAL success)
|
|
104
|
+
// A window or staff change clears selectedSlot and sets days to null while the new week loads.
|
|
105
|
+
// book() rejects on refusal (slot taken, invalid form) AND records .error — render it beside the CTA.
|
|
70
106
|
```
|
|
71
107
|
|
|
108
|
+
### The reference files for stacks where the components don't deploy
|
|
109
|
+
|
|
110
|
+
On `lib`, `static`, and a port, nothing under `components/` or `hooks/` arrives. The state machines
|
|
111
|
+
behind the hooks do arrive — `wix/bookings/services-store.ts`, `booking-flow-store.ts` — so you
|
|
112
|
+
never rewrite them: create a store per surface, `subscribe`, render from `getState()`, call its
|
|
113
|
+
actions. Their `ServicesState` / `BookingFlowState` interfaces are the render contract; read those.
|
|
114
|
+
What you write is the rendering — tiles, the slot picker, the form, the CTA — and for that read
|
|
115
|
+
these first; they are tested code for exactly that behaviour:
|
|
116
|
+
|
|
117
|
+
1. `components/bookings/ServiceBookingView.tsx` — the booking surface as working code: day
|
|
118
|
+
groups with slot chips, week paging controls, the staff filter only when there is more than one
|
|
119
|
+
staff member, one input per `formFields` entry typed from its `type` (a `<select>` when it has
|
|
120
|
+
`options`), the CTA disabled until `canBook`, labelled free vs priced, `error` inline, and the
|
|
121
|
+
confirmed state rendered only from `confirmed`.
|
|
122
|
+
2. `components/bookings/ServicesView.tsx` — category pills only when `categories.length > 1`,
|
|
123
|
+
skeleton tiles while `services === null`, the honest empty state, and `ServiceCard`: image with
|
|
124
|
+
the type badge, name, tagLine, "duration · price".
|
|
125
|
+
|
|
126
|
+
All under `references/bookings/app/`.
|
|
127
|
+
|
|
72
128
|
### Wiring — Astro (default)
|
|
73
129
|
|
|
74
130
|
1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass — merge into the
|
|
@@ -80,11 +136,92 @@ Style everything with Tailwind utilities on the tokens.
|
|
|
80
136
|
batch multiple Writes per message.
|
|
81
137
|
3. Write `pages/index.astro` (home) — it exists from the scaffold; Read it before overwriting.
|
|
82
138
|
|
|
139
|
+
### Wiring — another JS framework (`--stack lib`: Vue, Svelte, Solid, plain Vite)
|
|
140
|
+
|
|
141
|
+
Read the reference files listed above before writing any surface.
|
|
142
|
+
|
|
143
|
+
`deploy.mjs bookings --stack lib` put the data layer in `src/wix/` and nothing else: `sdk.ts`
|
|
144
|
+
(the visitor client, configured with the public client id), `media.ts`, `money.ts`, and
|
|
145
|
+
`wix/bookings/` — `services.ts`, `booking.ts`, `types.ts`, the `*-core.ts` rules, and the two
|
|
146
|
+
stores `services-store.ts`, `booking-flow-store.ts`. None of it is React. The hooks and components
|
|
147
|
+
don't ship on this stack; the stores replace the hooks, and you write the components in your
|
|
148
|
+
framework to the contracts on this page:
|
|
149
|
+
|
|
150
|
+
- bind the stores with your framework's external-store primitive (Vue: `shallowRef` updated in
|
|
151
|
+
`subscribe`; Svelte: `readable(store.getState(), (set) => store.subscribe(() => set(store.getState())))`;
|
|
152
|
+
Solid: a signal set in `subscribe`). `createServicesStore(options)` per listing (`start()` when
|
|
153
|
+
mounted, `stop()` when unmounted), `createBookingFlowStore(service)` per booking surface (the
|
|
154
|
+
`ServiceDetail` from `fetchServiceBySlug`; `start()` in the browser only — availability is
|
|
155
|
+
timezone-specific). State in, actions out — exactly the hooks' contracts above;
|
|
156
|
+
- your tiles, slot picker, form, and CTA to the defaults in "What a complete booking site shows" —
|
|
157
|
+
the shipped `ServiceBookingView.tsx` and `ServicesView.tsx` are readable as behaviour specs.
|
|
158
|
+
|
|
159
|
+
Routes `/services`, `/services/:slug` (via `fetchServiceBySlug`, null → your 404); dev server on
|
|
160
|
+
4321; a static build goes through `npx @wix/cli@latest release` with `site.outputDirectory`
|
|
161
|
+
pointing at the build folder, an SSR build is hosted by you. Service-page tags from the
|
|
162
|
+
`ServiceDetail` (name, tagLine, description).
|
|
163
|
+
|
|
164
|
+
### Wiring — static site (`--stack static`, no bundler)
|
|
165
|
+
|
|
166
|
+
Read the reference files listed above before writing any surface.
|
|
167
|
+
|
|
168
|
+
`deploy.mjs bookings --stack static --out site` put the REST layer in `site/js/wix/` (browser
|
|
169
|
+
ESM, the `.ts` beside each `.js` for reading). Everything the visitor loads lives under `site/` —
|
|
170
|
+
pages, styles, `js/` — and `wix.config.json`'s `site.outputDirectory` is `"./site"`; the project
|
|
171
|
+
root (config, plan, seed output) is never the upload. Same function names and DTOs as the table
|
|
172
|
+
above, so the contracts on this page hold unchanged: `fetchServices`, `fetchServiceBySlug`,
|
|
173
|
+
`fetchBookingCategories` from `./js/wix/services.js`; `fetchSlots`, `fetchBookingForm`,
|
|
174
|
+
`bookService` from `./js/wix/booking.js`. The state machines ship too: `createServicesStore` from
|
|
175
|
+
`./js/wix/services-store.js` (the listing — `start()` once the page is up, `setActiveCategoryId`)
|
|
176
|
+
and `createBookingFlowStore(service)` from `./js/wix/booking-flow-store.js` (the booking surface —
|
|
177
|
+
`fetchServiceBySlug(slug)` first, then the store: days, week paging, staff filter, `formFields`,
|
|
178
|
+
`setValue`, `canBook`, `book()`). No components ship — you write the rendering in plain JS: one
|
|
179
|
+
render function per surface that reads `getState()`, called from `subscribe`, with the surface's
|
|
180
|
+
controls calling the store's actions. `book()` navigates the full document to the hosted checkout
|
|
181
|
+
for a paid service on its own (`window.location.origin` must be on the OAuth app's allowed domains
|
|
182
|
+
so checkout can return); for a free or pay-in-person service it sets `confirmed`. Pages are
|
|
183
|
+
`services.html` and `service.html?slug=…` (Wix static hosting serves files, not directories — name
|
|
184
|
+
the file and link to it). Set `document.title` and the meta description from the `ServiceDetail`
|
|
185
|
+
once it loads. The visitor token persists in `localStorage` on its own; never mint per page.
|
|
186
|
+
`npx @wix/cli@latest release` uploads `site/`.
|
|
187
|
+
|
|
188
|
+
### Wiring — server-rendered, another language (Flask, Laravel, Rails, …)
|
|
189
|
+
|
|
190
|
+
Read the reference files listed above before writing any surface.
|
|
191
|
+
|
|
192
|
+
Run `deploy.mjs bookings --stack static` in the project folder anyway: `js/wix/` is both the
|
|
193
|
+
browser-side code and the readable spec. Then split by where the call runs. **Reads on the
|
|
194
|
+
server:** port `js/wix/services.ts` and `services-core.ts` to your language — the same three
|
|
195
|
+
functions returning the same DTO shapes as dicts, one anonymous visitor token per process for these
|
|
196
|
+
public reads (mint and refresh per `client.ts`) — and render the listing and the service page in
|
|
197
|
+
your templates to the contracts above, so service names and prices are in the HTML; page tags from
|
|
198
|
+
the `ServiceDetail`. **Booking in the browser:** the booking surface on
|
|
199
|
+
`./js/wix/booking-flow-store.js` with the rendered `ServiceDetail` (a JSON script tag, or
|
|
200
|
+
`fetchServiceBySlug(slug)` from `./js/wix/services.js`), exactly as the static wiring above — the
|
|
201
|
+
browser owns the visitor's token, so the server never handles per-visitor tokens, and the booking,
|
|
202
|
+
its cart, and the checkout redirect are the visitor's. Routes stay `/services`, `/services/<slug>`.
|
|
203
|
+
Add your public https origin to the OAuth app's allowed domains before checkout can return.
|
|
204
|
+
|
|
205
|
+
**Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted.** Same port for
|
|
206
|
+
the reads, run at build time with one anonymous token; the generator must emit a page for every
|
|
207
|
+
slug `fetchServices()` returns. Run `deploy.mjs bookings --stack static --out <build dir>` so
|
|
208
|
+
`js/wix/` is inside the output the pages import from, point `site.outputDirectory` at that folder,
|
|
209
|
+
`wix release`. Pages sit at different depths (`/`, `/services/…`): give the templates one base path
|
|
210
|
+
to `js/wix/` (a template variable, or root-relative `/js/wix/…`), never a relative `./js/wix/` — it
|
|
211
|
+
breaks one level down. The frozen listing is the first paint; availability and booking still run
|
|
212
|
+
client-side on the service page through `createBookingFlowStore()` from
|
|
213
|
+
`./js/wix/booking-flow-store.js`, exactly as on a static site, so the booking contract above
|
|
214
|
+
applies. Close with the live URL, the rebuild + release command, and one line for the owner:
|
|
215
|
+
dashboard edits to services reach the site when that command runs; availability and booking are
|
|
216
|
+
live regardless.
|
|
217
|
+
|
|
83
218
|
### Wiring — React SPA (Vite etc.)
|
|
84
219
|
|
|
85
220
|
Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite
|
|
86
221
|
plugins — deploy added the dep). Routes: `/services` → your listing; `/services/:slug` →
|
|
87
|
-
fetch with `fetchServiceBySlug(slug)` client-side, then your booking surface
|
|
222
|
+
fetch with `fetchServiceBySlug(slug)` client-side, then your booking surface on
|
|
223
|
+
`useBookingFlow(service)`. Deploy wrote the public client id into `wix/config.ts`; nothing else
|
|
224
|
+
to configure.
|
|
88
225
|
|
|
89
226
|
## Hard rules
|
|
90
227
|
|
|
@@ -99,31 +236,40 @@ fetch with `fetchServiceBySlug(slug)` client-side, then your booking surface.
|
|
|
99
236
|
- **The confirmed state must reflect REAL success**: render it only from `confirmed` (set by
|
|
100
237
|
the free/offline branch). A visitor returning from the hosted checkout redirect is NOT a
|
|
101
238
|
success signal — don't fake a confirmation page off the return URL.
|
|
102
|
-
-
|
|
239
|
+
- Where the shipped components deploy (Astro, React): theme via the `@theme` tokens, and your
|
|
240
|
+
markup uses Tailwind utilities on the same tokens — one design system across shipped and written
|
|
241
|
+
code. No parallel theme files, no hardcoded palette values. Where they don't (`lib`, `static`, a
|
|
242
|
+
port): style with whatever your stack does well, on one token set of your own; the rule that
|
|
243
|
+
survives is the token set, not Tailwind.
|
|
103
244
|
- Live data or an honest empty state — never mock services, slots, or availability.
|
|
104
|
-
- Keep the detail page's SEO pieces exactly as shipped.
|
|
245
|
+
- Keep the detail page's SEO pieces exactly as shipped (Astro).
|
|
246
|
+
- **Browsing and booking need no login.** They run on the Wix visitor session the shipped client
|
|
247
|
+
already holds; don't gate services or booking behind sign-in unless the brief asks for accounts.
|
|
248
|
+
- **Call every hook before any conditional return.** Hooks first, branches after.
|
|
105
249
|
|
|
106
250
|
## Point the user to their dashboard
|
|
107
251
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
252
|
+
Hand the owner these links — `{siteId}` is `siteId` in `wix.config.json` (the deploy JSON prints it as
|
|
253
|
+
`dashboardUrl`); a service id fills the placeholder from the seed result.
|
|
254
|
+
|
|
255
|
+
| page | `https://manage.wix.com/dashboard/{siteId}/` + |
|
|
256
|
+
|---|---|
|
|
257
|
+
| Services | `bookings/services` |
|
|
258
|
+
| Edit a service | `bookings/services/form/{serviceId}` |
|
|
259
|
+
| Calendar | `bookings/calendar` |
|
|
260
|
+
| Bookings list | `bookings/bookings/bookings-list` |
|
|
261
|
+
| Staff | `bookings/staff` |
|
|
262
|
+
| Availability | `bookings/availability` |
|
|
263
|
+
| Booking form | `bookings/settings/booking-form-page` |
|
|
264
|
+
| Policies | `bookings/settings/policies` |
|
|
265
|
+
|
|
266
|
+
Completing a booking online — free services included — needs a payment method on the site (Settings →
|
|
267
|
+
Accept payments; "manual payments" is enough for free and pay-in-person); taking real online payments
|
|
268
|
+
also needs a premium plan. Until then `book()` surfaces "the site is not accepting payments" — mention
|
|
269
|
+
it, don't treat it as a code failure.
|
|
111
270
|
|
|
112
271
|
## Seeding
|
|
113
272
|
|
|
114
273
|
Per `seed/SEED.md` — plain-data `plan.json` into `seed-bookings.mjs` from the project root.
|
|
115
274
|
Seed services that exercise the UI (an APPOINTMENT with duration+price, a free one, a CLASS
|
|
116
275
|
with future sessions when it fits the business; an image per service).
|
|
117
|
-
|
|
118
|
-
## Verify (before declaring done)
|
|
119
|
-
|
|
120
|
-
- [ ] `/services` renders live services SSR (view-source shows names) through YOUR components;
|
|
121
|
-
empty catalog shows your honest empty state.
|
|
122
|
-
- [ ] A service page shows day-grouped bookable times; week paging works; the form renders
|
|
123
|
-
(schema or fallback) and the CTA stays disabled until a slot + all fields are set.
|
|
124
|
-
- [ ] A FREE service books end-to-end: `book()` resolves, your confirmed state renders.
|
|
125
|
-
- [ ] A PAID service redirects to the Wix-hosted checkout.
|
|
126
|
-
- [ ] Service page view-source carries the SEO tags (Astro).
|
|
127
|
-
- [ ] Card/listing/booking surfaces/home are YOUR designs on the tokens; data-layer/hook files
|
|
128
|
-
unedited.
|
|
129
|
-
- [ ] Dashboard links handed to the owner.
|
package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx
CHANGED
|
@@ -105,7 +105,7 @@ export default function ServiceBookingView({ service }: { service: ServiceDetail
|
|
|
105
105
|
<div className="mt-6 grid max-w-md gap-3">
|
|
106
106
|
{formFields.map((f) => (
|
|
107
107
|
<label key={f.target} className="block">
|
|
108
|
-
<span className="mb-1 block text-xs font-semibold uppercase tracking-wider text-muted-foreground">{f.label}</span>
|
|
108
|
+
<span className="mb-1 block text-xs font-semibold uppercase tracking-wider text-muted-foreground">{f.label}{f.required ? " *" : ""}</span>
|
|
109
109
|
{f.options?.length ? (
|
|
110
110
|
<select
|
|
111
111
|
value={values[f.target] ?? ""}
|
|
@@ -121,6 +121,7 @@ export default function ServiceBookingView({ service }: { service: ServiceDetail
|
|
|
121
121
|
<input
|
|
122
122
|
type={f.type === "EMAIL" ? "email" : f.type === "PHONE" ? "tel" : f.type === "NUMBER" ? "number" : f.type === "URL" ? "url" : "text"}
|
|
123
123
|
value={values[f.target] ?? ""}
|
|
124
|
+
required={f.required}
|
|
124
125
|
onChange={(e) => setValue(f.target, e.target.value)}
|
|
125
126
|
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-shadow focus:ring-2 focus:ring-primary"
|
|
126
127
|
/>
|
|
@@ -1,138 +1,32 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
windowStart: Date;
|
|
14
|
-
nextWeek: () => void;
|
|
15
|
-
prevWeek: () => void;
|
|
16
|
-
/** Staff filter — render a picker only when service.staff.length > 1. */
|
|
17
|
-
staffId: string | undefined;
|
|
18
|
-
setStaffId: (id: string | undefined) => void;
|
|
19
|
-
selectedSlot: Slot | null;
|
|
20
|
-
setSelectedSlot: (s: Slot | null) => void;
|
|
21
|
-
/** Schema-driven form fields (never empty — contact basics fallback). */
|
|
22
|
-
formFields: BookingFormField[];
|
|
23
|
-
values: Record<string, string>;
|
|
24
|
-
setValue: (target: string, value: string) => void;
|
|
25
|
-
/** True when a slot is selected and every field has a value. */
|
|
26
|
-
canBook: boolean;
|
|
27
|
-
/** Books the selected slot. On "redirect" the browser is already navigating. */
|
|
28
|
-
book: () => Promise<BookingResult>;
|
|
29
|
-
booking: boolean;
|
|
30
|
-
/** Set after a free/offline booking completes. */
|
|
31
|
-
confirmed: BookingResult | null;
|
|
32
|
-
error: string | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const dayLabel = (dayKey: string): string => {
|
|
36
|
-
try {
|
|
37
|
-
return new Date(`${dayKey}T12:00:00`).toLocaleDateString(undefined, {
|
|
38
|
-
weekday: "short",
|
|
39
|
-
month: "short",
|
|
40
|
-
day: "numeric",
|
|
41
|
-
});
|
|
42
|
-
} catch {
|
|
43
|
-
return dayKey;
|
|
44
|
-
}
|
|
45
|
-
};
|
|
1
|
+
// React binding of the booking-flow store (wix/bookings/booking-flow-store.ts) — the whole booking
|
|
2
|
+
// state machine for one service lives there, framework-free: availability window (day-grouped
|
|
3
|
+
// slots, week paging, optional staff filter), the schema-driven form, and book(). All correctness
|
|
4
|
+
// (slot scheduleId vs eventId, ANY_RESOURCE, formSubmission, payment option, checkout-or-place)
|
|
5
|
+
// lives in the data layer; you own how it looks. Mount the surface client-only: availability is
|
|
6
|
+
// timezone/session-specific.
|
|
7
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
8
|
+
import { createBookingFlowStore, type BookingFlowState, type BookingFlowStore } from "../../wix/bookings/booking-flow-store";
|
|
9
|
+
import type { ServiceDetail } from "../../wix/bookings/types";
|
|
10
|
+
|
|
11
|
+
export type UseBookingFlow = BookingFlowState &
|
|
12
|
+
Pick<BookingFlowStore, "nextWeek" | "prevWeek" | "setStaffId" | "setSelectedSlot" | "setValue" | "book">;
|
|
46
13
|
|
|
47
14
|
export function useBookingFlow(service: ServiceDetail): UseBookingFlow {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const [selectedSlot, setSelectedSlot] = useState<Slot | null>(null);
|
|
52
|
-
const [formFields, setFormFields] = useState<BookingFormField[]>([]);
|
|
53
|
-
const [values, setValues] = useState<Record<string, string>>({});
|
|
54
|
-
const [booking, setBooking] = useState(false);
|
|
55
|
-
const [confirmed, setConfirmed] = useState<BookingResult | null>(null);
|
|
56
|
-
const [error, setError] = useState<string | null>(null);
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
let alive = true;
|
|
60
|
-
setSlots(null);
|
|
61
|
-
setSelectedSlot(null);
|
|
62
|
-
fetchSlots(service, { from: windowStart, days: 7, staffId })
|
|
63
|
-
.then((s) => alive && setSlots(s))
|
|
64
|
-
.catch((e) => {
|
|
65
|
-
if (!alive) return;
|
|
66
|
-
setSlots([]);
|
|
67
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
68
|
-
});
|
|
69
|
-
return () => {
|
|
70
|
-
alive = false;
|
|
71
|
-
};
|
|
72
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
-
}, [service.id, windowStart, staffId]);
|
|
74
|
-
|
|
15
|
+
const ref = useRef<{ serviceId: string; store: BookingFlowStore } | null>(null);
|
|
16
|
+
if (!ref.current || ref.current.serviceId !== service.id) ref.current = { serviceId: service.id, store: createBookingFlowStore(service) };
|
|
17
|
+
const store = ref.current.store;
|
|
75
18
|
useEffect(() => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
};
|
|
81
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
-
}, [service.formId]);
|
|
83
|
-
|
|
84
|
-
const days = useMemo(() => {
|
|
85
|
-
if (slots === null) return null;
|
|
86
|
-
const byDay = new Map<string, Slot[]>();
|
|
87
|
-
for (const s of slots) byDay.set(s.dayKey, [...(byDay.get(s.dayKey) ?? []), s]);
|
|
88
|
-
return [...byDay.entries()]
|
|
89
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
90
|
-
.map(([dayKey, daySlots]) => ({ dayKey, dayLabel: dayLabel(dayKey), slots: daySlots }));
|
|
91
|
-
}, [slots]);
|
|
92
|
-
|
|
93
|
-
const canBook =
|
|
94
|
-
!!selectedSlot && formFields.every((f) => (values[f.target] ?? "").trim().length > 0);
|
|
95
|
-
|
|
96
|
-
async function book(): Promise<BookingResult> {
|
|
97
|
-
if (!selectedSlot) throw new Error("Pick a time first.");
|
|
98
|
-
setBooking(true);
|
|
99
|
-
setError(null);
|
|
100
|
-
try {
|
|
101
|
-
const result = await bookService(service, selectedSlot, values, { staffId });
|
|
102
|
-
if (result.kind === "redirect") {
|
|
103
|
-
window.location.href = result.url;
|
|
104
|
-
} else {
|
|
105
|
-
setConfirmed(result);
|
|
106
|
-
}
|
|
107
|
-
return result;
|
|
108
|
-
} catch (e) {
|
|
109
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
110
|
-
throw e;
|
|
111
|
-
} finally {
|
|
112
|
-
setBooking(false);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
19
|
+
store.start();
|
|
20
|
+
return () => store.stop();
|
|
21
|
+
}, [store]);
|
|
22
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
116
23
|
return {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}),
|
|
125
|
-
staffId,
|
|
126
|
-
setStaffId,
|
|
127
|
-
selectedSlot,
|
|
128
|
-
setSelectedSlot,
|
|
129
|
-
formFields,
|
|
130
|
-
values,
|
|
131
|
-
setValue: (target, value) => setValues((v) => ({ ...v, [target]: value })),
|
|
132
|
-
canBook,
|
|
133
|
-
book,
|
|
134
|
-
booking,
|
|
135
|
-
confirmed,
|
|
136
|
-
error,
|
|
24
|
+
...state,
|
|
25
|
+
nextWeek: store.nextWeek,
|
|
26
|
+
prevWeek: store.prevWeek,
|
|
27
|
+
setStaffId: store.setStaffId,
|
|
28
|
+
setSelectedSlot: store.setSelectedSlot,
|
|
29
|
+
setValue: store.setValue,
|
|
30
|
+
book: store.book,
|
|
137
31
|
};
|
|
138
32
|
}
|