@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,155 @@
|
|
|
1
|
+
// The one auth seam for every Wix REST call — the twin of app/wix/sdk.ts, over fetch. No
|
|
2
|
+
// dependencies. deploy.mjs --stack static copies it flat into js/wix/ next to the core files and
|
|
3
|
+
// strips it to JS (comments kept); a port to another language reads it as the specification.
|
|
4
|
+
// Imports carry the .js suffix so the stripped output runs as browser ESM unchanged.
|
|
5
|
+
//
|
|
6
|
+
// WIX_CLIENT_ID is the public OAuth client id (`appId` in wix.config.json). It is NOT a secret — it
|
|
7
|
+
// only mints anonymous VISITOR tokens — so it is fine in page source. deploy.mjs writes ./config.
|
|
8
|
+
//
|
|
9
|
+
// THE TOKEN IS THE CART. A visitor token is the shopper's identity: their cart lives on it. Mint
|
|
10
|
+
// once per shopper, persist it, refresh it — never mint per request (a fresh token is a new visitor
|
|
11
|
+
// with an empty cart). Where it persists depends on where this code runs:
|
|
12
|
+
// browser — localStorage, keyed by client id so two sites on one origin don't share a token
|
|
13
|
+
// (what this file does).
|
|
14
|
+
// server (a Python/PHP/Go port) — the SHOPPER'S SESSION, one token set per shopper. Never one
|
|
15
|
+
// process-wide token for everyone: that is one shared cart for all visitors.
|
|
16
|
+
// This is the only part of a port that isn't line-for-line.
|
|
17
|
+
// docs: https://dev.wix.com/docs/go-headless/coding/authentication/visitors.md
|
|
18
|
+
import { WIX_CLIENT_ID } from "./config.js";
|
|
19
|
+
|
|
20
|
+
export const WIX_API_BASE = "https://www.wixapis.com";
|
|
21
|
+
const TOKEN_URL = `${WIX_API_BASE}/oauth2/token`;
|
|
22
|
+
const STORAGE_KEY = `wix-visitor-${WIX_CLIENT_ID}`;
|
|
23
|
+
|
|
24
|
+
export interface Tokens {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
refreshToken: string;
|
|
27
|
+
/** Epoch ms. */
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
/** Whose tokens: absent or "visitor" for an anonymous visitor, "member" after a login (a vertical's rest/auth.ts). */
|
|
30
|
+
role?: "visitor" | "member";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Swap this for a server-session-backed store in a port; the browser default is localStorage. */
|
|
34
|
+
export interface TokenStore {
|
|
35
|
+
load(): Tokens | null;
|
|
36
|
+
save(tokens: Tokens): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let memory: Tokens | null = null;
|
|
40
|
+
export const browserTokenStore: TokenStore = {
|
|
41
|
+
load() {
|
|
42
|
+
if (memory) return memory;
|
|
43
|
+
if (typeof localStorage === "undefined") return null;
|
|
44
|
+
try {
|
|
45
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
46
|
+
if (raw) memory = JSON.parse(raw) as Tokens;
|
|
47
|
+
} catch {
|
|
48
|
+
/* disabled or corrupt storage — mint a fresh visitor */
|
|
49
|
+
}
|
|
50
|
+
return memory;
|
|
51
|
+
},
|
|
52
|
+
save(tokens) {
|
|
53
|
+
memory = tokens;
|
|
54
|
+
if (typeof localStorage === "undefined") return;
|
|
55
|
+
try {
|
|
56
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(tokens));
|
|
57
|
+
} catch {
|
|
58
|
+
/* storage full or disabled — the session just won't survive a reload */
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
let store: TokenStore = browserTokenStore;
|
|
63
|
+
/** A port calls this once with its session-backed store. */
|
|
64
|
+
export function useTokenStore(s: TokenStore): void {
|
|
65
|
+
store = s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// POST /oauth2/token — grantType "anonymous" mints a visitor; "refresh_token" renews and KEEPS the
|
|
69
|
+
// identity (a member refresh token yields member tokens); "authorization_code" + codeVerifier turns
|
|
70
|
+
// a login's authorization code into MEMBER tokens (a vertical's rest/auth.ts). `role` records which.
|
|
71
|
+
export async function mintTokens(body: Record<string, string>, role: Tokens["role"] = "visitor"): Promise<Tokens> {
|
|
72
|
+
const res = await fetch(TOKEN_URL, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({ clientId: WIX_CLIENT_ID, ...body }),
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok) throw new Error(`Wix auth failed (${res.status}).`);
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt: Date.now() + data.expires_in * 1000, role };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The persisted token set as it is (possibly expired), or null before the first mint. */
|
|
83
|
+
export function loadTokens(): Tokens | null {
|
|
84
|
+
return store.load();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Adopt a token set — a member's after login. Every call from here on runs as that identity. */
|
|
88
|
+
export function setTokens(tokens: Tokens): void {
|
|
89
|
+
store.save(tokens);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Drop the current identity (logout): the next call mints a fresh anonymous visitor. */
|
|
93
|
+
export function clearTokens(): void {
|
|
94
|
+
store.save({ accessToken: "", refreshToken: "", expiresAt: 0 });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A valid access token: cached, refreshed when expired, minted on first use. */
|
|
98
|
+
export async function accessToken(): Promise<string> {
|
|
99
|
+
const t = store.load();
|
|
100
|
+
if (t && t.expiresAt - 60_000 > Date.now()) return t.accessToken;
|
|
101
|
+
let fresh: Tokens | undefined;
|
|
102
|
+
if (t?.refreshToken) {
|
|
103
|
+
try {
|
|
104
|
+
fresh = await mintTokens({ grantType: "refresh_token", refreshToken: t.refreshToken }, t.role ?? "visitor");
|
|
105
|
+
} catch {
|
|
106
|
+
/* fall through to a new visitor */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
fresh ??= await mintTokens({ grantType: "anonymous" });
|
|
110
|
+
store.save(fresh);
|
|
111
|
+
return fresh.accessToken;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class WixApiError extends Error {
|
|
115
|
+
/** The response's `details` as Wix sent it (applicationError, validationError) — for callers that map codes. */
|
|
116
|
+
public details?: Record<string, any>;
|
|
117
|
+
constructor(message: string, public status: number, public code?: string, details?: Record<string, any>) {
|
|
118
|
+
super(message);
|
|
119
|
+
this.details = details;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Every Wix call goes through here. `path` is relative to WIX_API_BASE; `query` becomes the query
|
|
125
|
+
* string (an array repeats the key: fields=A&fields=B). Throws WixApiError on a non-2xx, including
|
|
126
|
+
* 404 — callers that treat 404 as "not found" catch it. The header carries the RAW access token,
|
|
127
|
+
* no "Bearer " prefix.
|
|
128
|
+
*/
|
|
129
|
+
export async function wixRequest<T = any>(
|
|
130
|
+
path: string,
|
|
131
|
+
{ method = "POST", body, query }: { method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; body?: unknown; query?: Record<string, string | readonly string[]> } = {},
|
|
132
|
+
): Promise<T> {
|
|
133
|
+
const url = new URL(path, WIX_API_BASE);
|
|
134
|
+
for (const [k, v] of Object.entries(query ?? {})) {
|
|
135
|
+
for (const item of Array.isArray(v) ? v : [v as string]) url.searchParams.append(k, String(item));
|
|
136
|
+
}
|
|
137
|
+
const res = await fetch(url, {
|
|
138
|
+
method,
|
|
139
|
+
headers: { Authorization: await accessToken(), "Content-Type": "application/json" },
|
|
140
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
let message = "", code: string | undefined, details: Record<string, any> | undefined;
|
|
144
|
+
try {
|
|
145
|
+
const j = await res.json();
|
|
146
|
+
message = j?.message ?? "";
|
|
147
|
+
code = j?.details?.applicationError?.code;
|
|
148
|
+
details = j?.details;
|
|
149
|
+
} catch {
|
|
150
|
+
/* no JSON body */
|
|
151
|
+
}
|
|
152
|
+
throw new WixApiError(message || `${method} ${url.pathname} failed (${res.status}).`, res.status, code, details);
|
|
153
|
+
}
|
|
154
|
+
return (res.status === 204 ? null : await res.json()) as T;
|
|
155
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Wix media → browser-loadable URLs — the twin of app/wix/media.ts, without the SDK.
|
|
2
|
+
//
|
|
3
|
+
// A media value arrives as either an absolute https://static.wixstatic.com/... URL or a raw
|
|
4
|
+
// `wix:image://v1/<file>/<name>#originWidth=W&originHeight=H` identifier a browser cannot load
|
|
5
|
+
// (ERR_UNKNOWN_URL_SCHEME). The raw form is scaled through this ONE URL shape; any other hand-built
|
|
6
|
+
// form 403s: https://static.wixstatic.com/media/<file>/v1/fill/w_<W>,h_<H>,al_c,q_90/<file>
|
|
7
|
+
const STATIC = "https://static.wixstatic.com/media";
|
|
8
|
+
const FILL = /\/v1\/fill\/w_\d+,h_\d+/;
|
|
9
|
+
|
|
10
|
+
export type MediaLike = string | null | undefined | { image?: string | null; url?: string | null };
|
|
11
|
+
|
|
12
|
+
function rawOf(value: unknown): string {
|
|
13
|
+
if (!value) return "";
|
|
14
|
+
if (typeof value === "string") return value;
|
|
15
|
+
const o = value as { image?: string | null; url?: string | null };
|
|
16
|
+
return o.image ?? o.url ?? "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Resolve any Wix media value to a URL sized w×h ("" when absent). */
|
|
20
|
+
export function imgSrc(value: unknown, width = 600, height = 600): string {
|
|
21
|
+
const v = rawOf(value);
|
|
22
|
+
if (!v) return "";
|
|
23
|
+
if (v.startsWith("wix:image://")) {
|
|
24
|
+
const file = v.slice("wix:image://v1/".length).split("/")[0].split("#")[0];
|
|
25
|
+
return `${STATIC}/${file}/v1/fill/w_${width},h_${height},al_c,q_90/${file}`;
|
|
26
|
+
}
|
|
27
|
+
// An absolute Wix media URL (bare, or already scaled to some other size): re-issue it through the
|
|
28
|
+
// scaler at the requested size, so every image path lands on one shape.
|
|
29
|
+
const m = v.match(/^https:\/\/static\.wixstatic\.com\/media\/([^/?#]+)/);
|
|
30
|
+
if (m) return `${STATIC}/${m[1]}/v1/fill/w_${width},h_${height},al_c,q_90/${m[1]}`;
|
|
31
|
+
return v;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** srcset candidates at several widths; `ratio` is height/width. "" only when there is no image. */
|
|
35
|
+
export function imgSrcSet(value: unknown, widths: number[] = [320, 480, 640, 960], ratio = 1): string {
|
|
36
|
+
const v = rawOf(value);
|
|
37
|
+
if (!v) return "";
|
|
38
|
+
if (v.startsWith("wix:image://")) return widths.map((w) => `${imgSrc(value, w, Math.round(w * ratio))} ${w}w`).join(", ");
|
|
39
|
+
if (v.includes("static.wixstatic.com/") && FILL.test(v)) {
|
|
40
|
+
return widths.map((w) => `${v.replace(FILL, `/v1/fill/w_${w},h_${Math.round(w * ratio)}`)} ${w}w`).join(", ");
|
|
41
|
+
}
|
|
42
|
+
return v;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Everything an <img> needs for one Wix image, so a tile can never ship `srcset` without `src`.
|
|
47
|
+
* Attribute names as HTML spells them — spread into a template or apply with setAttribute:
|
|
48
|
+
* `<img ${attrs(imgAttrs(p.imageUrl, "(min-width: 1024px) 25vw, 50vw"))} alt="…">`
|
|
49
|
+
* `sizes` is the width the image renders at; `ratio` is height/width. {} when there is no image.
|
|
50
|
+
*/
|
|
51
|
+
export function imgAttrs(value: unknown, sizes: string, ratio = 1): Record<string, string> {
|
|
52
|
+
const src = imgSrc(value, 640, Math.round(640 * ratio));
|
|
53
|
+
if (!src) return {};
|
|
54
|
+
return { src, srcset: imgSrcSet(value, undefined, ratio), sizes, loading: "lazy", decoding: "async" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The media identity before scaling — de-duplicate galleries on this, never on a resolved URL. */
|
|
58
|
+
export function mediaKey(value: unknown): string {
|
|
59
|
+
const v = rawOf(value);
|
|
60
|
+
if (!v) return "";
|
|
61
|
+
// The file id, whichever form the value takes — a raw id and a resolved URL of one photo share it.
|
|
62
|
+
if (v.startsWith("wix:image://")) return v.slice("wix:image://v1/".length).split("/")[0].split("#")[0];
|
|
63
|
+
const m = v.match(/^https:\/\/static\.wixstatic\.com\/media\/([^/?#]+)/);
|
|
64
|
+
return m ? m[1] : v;
|
|
65
|
+
}
|
|
@@ -8,25 +8,31 @@ be exact. You never write commerce code; you never skip designing the store.
|
|
|
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` and `styles/global.css`.
|
|
14
18
|
Files you **create** (skeletons below): the shop, category, and PDP pages with their island
|
|
15
19
|
components, plus your home page.
|
|
16
20
|
|
|
17
21
|
| file | what it is |
|
|
18
22
|
|---|---|
|
|
19
23
|
| `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
|
|
20
|
-
| `wix/media.ts` · `wix/money.ts` | `imgSrc()` / `imgSrcSet()` / `formatMoney()`
|
|
24
|
+
| `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes)` — every `<img>` attribute for a DTO image (`src`, `srcSet`, `sizes`, lazy): `<img {...imgAttrs(p.imageUrl, "25vw")} alt={p.name} />`; `imgSrc()` / `imgSrcSet()` / `formatMoney()` underneath, already used by everything shipped |
|
|
21
25
|
| `wix/storefront/types.ts` | the DTOs (`ProductSummary`, `ProductDetail`, `Cart`, `Category`, `Facet`) — contracts inlined below |
|
|
22
|
-
| `wix/storefront/catalog.ts` | `searchCatalog` (sort/filter/facets/search + cursor paging + result count, all server-side), `fetchFacets`, `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `fetchCategoryBySlug`, `resolveVariant` |
|
|
26
|
+
| `wix/storefront/catalog.ts` | `searchCatalog` (sort/filter/facets/search + cursor paging + result count, all server-side), `fetchFacets`, `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `fetchCategoryBySlug`, `resolveVariant` — the transport; the rules and DTO mappers are in `catalog-core.ts` / `cart-core.ts` beside it (shared with the REST layer) |
|
|
23
27
|
| `wix/storefront/cart.ts` · `cart-store.ts` | Cart V2 + shared cart state (module store — spans Astro islands) |
|
|
28
|
+
| `wix/storefront/shop-store.ts` · `product-detail-store.ts` | the listing and product-detail state machines, framework-free (`createShopStore()`, `createProductDetailStore()` — `getState`/`subscribe` + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
|
|
24
29
|
| `hooks/storefront/useCart.ts` | cart state + actions — contract below |
|
|
25
|
-
| `hooks/storefront/useShop.ts` |
|
|
26
|
-
| `hooks/storefront/useProductDetail.ts` | option selection → variant resolution → add-to-cart — contract below |
|
|
30
|
+
| `hooks/storefront/useShop.ts` | React binding of `shop-store.ts`: category scope, sort, filters, option facets, result count, paging — contract below |
|
|
31
|
+
| `hooks/storefront/useProductDetail.ts` | React binding of `product-detail-store.ts`: option selection → variant resolution → add-to-cart — contract below |
|
|
27
32
|
| `components/storefront/CartButton.tsx` · `CartDrawer.tsx` | header badge + slide-over cart — **wire as-is** (drawer once per page) |
|
|
28
33
|
| `components/storefront/FilterPanel.tsx` | the gallery's filter LAYOUT — toolbar (result count, sort), active chips, then a 16rem sidebar of collapsible groups (price as a two-handle slider bounded by the catalog's real prices, availability, one group per option facet with swatches/pills) beside YOUR results; a bottom sheet under `md` — **wire as-is** in your `ShopView`, your grid as its children: `<FilterPanel shop={shop}>…grid…</FilterPanel>` |
|
|
29
34
|
| `components/storefront/QuickAdd.tsx` | the tile's purchase control — one click for a product with no options, a picker anchored to the tile (bottom sheet on small screens) for one with options, the product page for free-text customization — **wire as-is** as the last row of every tile's text block (`<QuickAdd product={p} />`) |
|
|
35
|
+
| `components/storefront/OptionPicker.tsx` | the purchase controls for one product — option groups (swatches/pills, sold-out choices disabled), choice and text modifiers, an optional quantity stepper, the buy button gated by `useProductDetail` with its plain reason, "Pre-order" when pre-orderable — **wire as-is** in your PDP (`<OptionPicker detail={d} showQuantity />`); QuickAdd's picker is this same component |
|
|
30
36
|
| `components/storefront/ShopView.tsx` · `ProductDetailView.tsx` | **don't ship — YOU create them** (skeletons below): the client islands your shop, category, and PDP pages mount |
|
|
31
37
|
| `styles/global.css` | **the design system**: Tailwind v4 + the `@theme` token block (colors, radii, fonts — same token family as the official Wix templates). Everything, shipped and yours, styles from these tokens |
|
|
32
38
|
|
|
@@ -74,7 +80,9 @@ of its category. Then, by default:
|
|
|
74
80
|
- **Home:** what the store sells and one shopping action in the first screen; real products under
|
|
75
81
|
truthful headings ("Best Sellers" needs data behind it); not a repeat of the shop page. A
|
|
76
82
|
category tile or link on the home page goes to that category's page (`/category/<slug>`) —
|
|
77
|
-
never to an anchor on the shop page that every tile shares
|
|
83
|
+
never to an anchor on the shop page that every tile shares — and its image is a product from
|
|
84
|
+
THAT category (fetched with the category filter) or the category's own image, never a
|
|
85
|
+
positional guess into the all-products list.
|
|
78
86
|
- **Shop:** a real product card — image, name, price, link — in the first screen; the shipped
|
|
79
87
|
`FilterPanel` around the grid (toolbar, then a filter sidebar beside the results on desktop and
|
|
80
88
|
a sheet on phones) — a store with any filterable catalog ships it, not "when it fits"; the
|
|
@@ -88,7 +96,12 @@ of its category. Then, by default:
|
|
|
88
96
|
- **Cart:** the shipped drawer — it opens after every add, and checkout is a button in it.
|
|
89
97
|
- **Overlays you build** (quick-add, mobile nav, filters): mount at the document root (a fixed
|
|
90
98
|
panel inside the `backdrop-blur` header gets clipped), lock background scroll, close on Escape,
|
|
91
|
-
return focus on close — as the shipped `CartDrawer` does.
|
|
99
|
+
return focus on close — as the shipped `CartDrawer` does. An anchored picker is positioned
|
|
100
|
+
inside its tile, not `fixed` with computed offsets. If you add dismiss-on-outside-click, decide
|
|
101
|
+
inside/outside before anything re-renders (a capture-phase listener), or a click on a swatch
|
|
102
|
+
that re-renders the panel reads as "outside" and closes it.
|
|
103
|
+
- **A page whose slug resolves to nothing** (category, product) shows only the not-found state —
|
|
104
|
+
no heading, toolbar, or empty grid rendered around it.
|
|
92
105
|
- **Copy:** nothing the merchant didn't supply — no invented reviews, scarcity, or delivery
|
|
93
106
|
promises; no Wix IDs or technical words in visible text.
|
|
94
107
|
|
|
@@ -160,6 +173,10 @@ only when the catalog has them — never fabricated; `wix-docs` has their contra
|
|
|
160
173
|
|
|
161
174
|
### The pages and islands you create — skeletons
|
|
162
175
|
|
|
176
|
+
The class names in these skeletons are the Astro/React spelling of layout rules that hold on
|
|
177
|
+
every stack, and each rule is also named in words beside its classes. On a stack where the shipped
|
|
178
|
+
components don't deploy (`lib`, `static`, a port), keep the rule and write it in your own CSS.
|
|
179
|
+
|
|
163
180
|
Nothing renders until you write these — the store IS your work. Each page is a thin SSR shell
|
|
164
181
|
(fetch → DTO props → island); each island is a thin view over a hook. The pages' frontmatter
|
|
165
182
|
is **machinery, not design** — reproduce it as the skeletons show, exactly. Hooks first,
|
|
@@ -308,6 +325,7 @@ if (!product) {
|
|
|
308
325
|
import { useShop } from "../../hooks/storefront/useShop";
|
|
309
326
|
import FilterPanel from "./FilterPanel";
|
|
310
327
|
import QuickAdd from "./QuickAdd";
|
|
328
|
+
import { imgAttrs } from "../../wix/media";
|
|
311
329
|
import type { Category, ProductSummary } from "../../wix/storefront/types";
|
|
312
330
|
|
|
313
331
|
export default function ShopView(props: {
|
|
@@ -329,23 +347,37 @@ export default function ShopView(props: {
|
|
|
329
347
|
// • error → a short inline message (retry() re-runs the query)
|
|
330
348
|
// • products === null (or loading) → skeleton tiles; [] → your honest empty state, and a
|
|
331
349
|
// distinct "no products match these filters" with clearFilters() when hasActiveFilters
|
|
332
|
-
// • else YOUR grid of YOUR tiles (ProductSummary contract above): image
|
|
333
|
-
//
|
|
350
|
+
// • else YOUR grid of YOUR tiles (ProductSummary contract above): image via
|
|
351
|
+
// <img {...imgAttrs(p.imageUrl, "(min-width: 768px) 25vw, 50vw")} alt={p.name} /> — src,
|
|
352
|
+
// srcSet, sizes and lazy loading in one spread, so a tile never ships srcSet without src;
|
|
353
|
+
// an empty imageUrl gives {} — render your placeholder then; hoverImageUrl
|
|
354
|
+
// on hover — name, price — a range when
|
|
334
355
|
// price !== maxPrice, else price + labelled compareAtPrice — EVERY ribbon from ribbons,
|
|
335
356
|
// swatches as small color dots when present (else optionsSummary as text); tile links to
|
|
336
357
|
// `/products/${p.slug}`; and <QuickAdd product={p} /> as the LAST ROW of the tile's text
|
|
337
358
|
// block, under name and price, full width — the shipped buy control (direct add / option
|
|
338
359
|
// picker / product page, decided from the product). It is a direct child of the tile root,
|
|
339
360
|
// which carries `relative`: the picker anchors to that root and takes the tile's width.
|
|
340
|
-
//
|
|
341
|
-
//
|
|
361
|
+
// THE TILE ROOT IS A <div className="relative flex flex-col">, NOT THE LINK: the <a> wraps
|
|
362
|
+
// the image and the name/price, and <QuickAdd> sits beside it as the flex column's last child
|
|
363
|
+
// (it pins itself to the bottom with mt-auto). A button inside an <a> is invalid HTML and its
|
|
364
|
+
// click navigates to the product page instead of adding.
|
|
365
|
+
// • ROW RHYTHM: the grid stretches every tile in a row to the tallest; with the tile a flex
|
|
366
|
+
// column and the control pinned to the bottom, the buy buttons share one baseline across the
|
|
367
|
+
// row even when one tile has swatches, a struck price, or a two-line name and its neighbours
|
|
368
|
+
// don't. Put the swatches between price and control; never let them push only that tile's
|
|
369
|
+
// button down. Never overlay the control on the image or float it
|
|
370
|
+
// between image and text, and never wrap it in a smaller positioned box (the picker would
|
|
371
|
+
// inherit that box's width).
|
|
372
|
+
// • the name WRAPS (`min-w-0`, `break-words`) — no `truncate` / `line-clamp-1`: a shopper reads
|
|
373
|
+
// "Red Velvet Cupcake 4-Pack", not "Red Velvet Cupc…"; price on its own line under it.
|
|
342
374
|
// • badges come ONLY from p.ribbons. Do NOT render a "Sale" badge because compareAtPrice
|
|
343
375
|
// is set — the struck price already says it, and a product the merchant ribboned "Sale"
|
|
344
376
|
// would show the badge twice.
|
|
345
|
-
// • name and price on SEPARATE lines
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
377
|
+
// • name and price on SEPARATE lines — never one flex row where a long name and a price
|
|
378
|
+
// range fight for width and the price gets clipped at 390px. Keep the page intro short
|
|
379
|
+
// enough that a full tile (image, name, price) is in the first screen, also on a short
|
|
380
|
+
// desktop window.
|
|
349
381
|
// • hasMore → your "load more" control calling loadMore() (disabled while loadingMore)
|
|
350
382
|
}
|
|
351
383
|
```
|
|
@@ -354,6 +386,7 @@ export default function ShopView(props: {
|
|
|
354
386
|
// src/components/storefront/ProductDetailView.tsx — YOU build the whole PDP surface;
|
|
355
387
|
// your [slug].astro mounts it with the server-fetched product.
|
|
356
388
|
import { useProductDetail } from "../../hooks/storefront/useProductDetail";
|
|
389
|
+
import OptionPicker from "./OptionPicker";
|
|
357
390
|
import type { ProductDetail } from "../../wix/storefront/types";
|
|
358
391
|
|
|
359
392
|
export default function ProductDetailView(props: {
|
|
@@ -374,13 +407,10 @@ export default function ProductDetailView(props: {
|
|
|
374
407
|
// • name, EVERY ribbon (d.product.ribbons), live d.price (the range until every option
|
|
375
408
|
// is picked) with d.compareAtPrice as a labelled "was" when present — never invent one;
|
|
376
409
|
// descriptionHtml rendered as HTML, then d.product.infoSections as sections/accordions
|
|
377
|
-
// •
|
|
378
|
-
// (
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
// ONLY — never resolve variants yourself — calling d.add(); label it "Pre-order" when
|
|
382
|
-
// d.isPreorder; while disabled, render d.blockedReason beside it as neutral guidance
|
|
383
|
-
// (not error styling); d.adding disables, d.error renders inline
|
|
410
|
+
// • <OptionPicker detail={d} showQuantity /> right under the price — the shipped option
|
|
411
|
+
// groups (swatches for a color option, sold-out choices disabled), modifiers, quantity,
|
|
412
|
+
// and the buy button gated by the hook with its plain reason, "Pre-order" when it applies,
|
|
413
|
+
// the add error inline. Never resolve variants or gate the button yourself.
|
|
384
414
|
// • in the first screen at mobile AND desktop: the image, name, price, the first choice,
|
|
385
415
|
// and the button with its reason — a shopper decides without scrolling. On a phone that
|
|
386
416
|
// means the primary image is a bounded band, not a full-height hero: e.g. the gallery
|
|
@@ -390,6 +420,31 @@ export default function ProductDetailView(props: {
|
|
|
390
420
|
}
|
|
391
421
|
```
|
|
392
422
|
|
|
423
|
+
### The reference files for stacks where the components don't deploy
|
|
424
|
+
|
|
425
|
+
On `lib`, `static`, and a port, nothing under `components/` or `hooks/` arrives. The state
|
|
426
|
+
machines behind the hooks do arrive — `wix/storefront/shop-store.ts`, `product-detail-store.ts`,
|
|
427
|
+
`cart-store.ts` — so you never rewrite them: create a store per surface, `subscribe`, render from
|
|
428
|
+
`getState()`, call its actions. Their `*State` interfaces are the render contract; read those.
|
|
429
|
+
What you write is the rendering — grid, product page, picker, filter panel, drawer — and for that
|
|
430
|
+
read these first; they are tested code for exactly that behaviour, and rewriting them from the
|
|
431
|
+
prose above is where the bugs come from:
|
|
432
|
+
|
|
433
|
+
1. `components/storefront/QuickAdd.tsx` — the three purchase paths decided from the summary DTO;
|
|
434
|
+
the panel is positioned inside the tile (the tile is `relative`), a bottom sheet on small
|
|
435
|
+
screens, never `fixed` with computed offsets; it closes on Escape, the close button, a
|
|
436
|
+
successful add, or the scrim — there is NO outside-click handler (one that runs after a
|
|
437
|
+
re-render sees the clicked swatch detached and closes on every pick).
|
|
438
|
+
2. `components/storefront/OptionPicker.tsx` — the purchase controls as working code: swatches vs
|
|
439
|
+
pills, sold-out choices disabled, quantity, the gated button with its reason and the
|
|
440
|
+
"Pre-order" label; the PDP and the tile picker share it.
|
|
441
|
+
3. `components/storefront/CartDrawer.tsx` — the overlay contract as working code: root-level,
|
|
442
|
+
scrim, scroll lock, Escape, focus in and back.
|
|
443
|
+
4. `components/storefront/FilterPanel.tsx` — inline commits at once, the sheet stages until Apply;
|
|
444
|
+
the price pair commits only when valid.
|
|
445
|
+
|
|
446
|
+
All under `references/storefront/app/`.
|
|
447
|
+
|
|
393
448
|
### Wiring — Astro (default)
|
|
394
449
|
|
|
395
450
|
1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass).
|
|
@@ -398,6 +453,88 @@ export default function ProductDetailView(props: {
|
|
|
398
453
|
SSR props; browser-state widgets (cart) are `client:only="react"`.
|
|
399
454
|
3. Write `pages/index.astro` (home) on `SiteLayout`.
|
|
400
455
|
|
|
456
|
+
### Wiring — another JS framework (`--stack lib`: Vue, Svelte, Solid, plain Vite)
|
|
457
|
+
|
|
458
|
+
Read the reference files listed above before writing any surface.
|
|
459
|
+
|
|
460
|
+
`deploy.mjs storefront --stack lib` put the data layer in `src/wix/` and nothing else: `sdk.ts`
|
|
461
|
+
(the visitor client, configured with the public client id), `media.ts`, `money.ts`, and
|
|
462
|
+
`wix/storefront/` — `catalog.ts`, `cart.ts`, `types.ts`, the `*-core.ts` rules, and the three
|
|
463
|
+
stores `shop-store.ts`, `product-detail-store.ts`, `cart-store.ts`. None of it is React. The
|
|
464
|
+
hooks and components don't ship on this stack; the stores replace the hooks, and you write the
|
|
465
|
+
components in your framework to the contracts on this page:
|
|
466
|
+
|
|
467
|
+
- bind the stores with your framework's external-store primitive (Vue: `shallowRef` updated in
|
|
468
|
+
`subscribe`; Svelte: `readable(store.getState(), (set) => store.subscribe(() => set(store.getState())))`;
|
|
469
|
+
Solid: a signal set in `subscribe`). `createShopStore(options)` per listing (`start()` when
|
|
470
|
+
mounted, `stop()` when unmounted), `createProductDetailStore({ initial | slug })` per product
|
|
471
|
+
surface, the cart store as-is (module-level). State in, actions out — exactly the hooks'
|
|
472
|
+
contracts above;
|
|
473
|
+
- your filter panel, quick add, and cart drawer to the contracts in "What a complete storefront
|
|
474
|
+
shows" — the shipped `FilterPanel.tsx`, `QuickAdd.tsx`, `CartDrawer.tsx` are readable as
|
|
475
|
+
behaviour specs (the overlay contract, the three purchase paths, the sidebar/sheet split).
|
|
476
|
+
|
|
477
|
+
Routes `/shop`, `/category/:slug` (via `fetchCategoryBySlug`, null → your 404), `/products/:slug`;
|
|
478
|
+
dev server on 4321; a static build goes through `npx @wix/cli@latest release` with
|
|
479
|
+
`site.outputDirectory` pointing at the build folder, an SSR build is hosted by you. Item-page tags
|
|
480
|
+
from the entity's `seoData`.
|
|
481
|
+
|
|
482
|
+
### Wiring — static site (`--stack static`, no bundler)
|
|
483
|
+
|
|
484
|
+
Read the reference files listed above before writing any surface.
|
|
485
|
+
|
|
486
|
+
`deploy.mjs storefront --stack static --out site` put the REST layer in `site/js/wix/` (browser
|
|
487
|
+
ESM, the `.ts` beside each `.js` for reading). Everything the visitor loads lives under `site/` —
|
|
488
|
+
pages, styles, `js/` — and `wix.config.json`'s `site.outputDirectory` is `"./site"`; the project
|
|
489
|
+
root (config, plan, seed output) is never the upload. Same function names and DTOs as the table
|
|
490
|
+
above, so the contracts on this page hold unchanged: `searchCatalog`, `fetchFacetData`, `fetchProductBySlug`,
|
|
491
|
+
`fetchCategories`, `fetchCategoryBySlug`, `resolveVariant` from `./js/wix/catalog.js`;
|
|
492
|
+
`fetchCart`, `addToCart`, `updateQuantity`, `removeLine`, `checkoutUrl` from `./js/wix/cart.js`.
|
|
493
|
+
The state machines ship too: `createShopStore` from `./js/wix/shop-store.js` (the listing —
|
|
494
|
+
selection, facets, URL sync, cursor paging; `start()` once the page is up), `createProductDetailStore`
|
|
495
|
+
from `./js/wix/product-detail-store.js` (the PDP and every quick-add picker — selections start
|
|
496
|
+
empty, `resolveVariant`, `canAdd`/`blockedReason`, `add()`), and `./js/wix/cart-store.js` (the
|
|
497
|
+
cart, `subscribeCart`/`getCartState`, `addLine`, `updateLineQuantity`, `removeCartLine`,
|
|
498
|
+
`goToCheckout`, `setCartOpen`). No components ship — you write the rendering in plain JS: one
|
|
499
|
+
render function per surface that reads `getState()`, called from `subscribe`, with the
|
|
500
|
+
surface's controls calling the store's actions. The drawer opens after every add on its own;
|
|
501
|
+
overlays follow the CartDrawer contract (root-level, scroll lock, Escape, focus back). Pages are
|
|
502
|
+
`shop.html`, `category.html?slug=…`,
|
|
503
|
+
`product.html?slug=…`. Set `document.title` and the meta description from the entity's `seoData`
|
|
504
|
+
once it loads, on the product AND category pages. The visitor token persists in `localStorage` on
|
|
505
|
+
its own; never mint per page. `npx @wix/cli@latest release` uploads `site/`.
|
|
506
|
+
|
|
507
|
+
### Wiring — server-rendered, another language (Flask, Laravel, Rails, …)
|
|
508
|
+
|
|
509
|
+
Read the reference files listed above before writing any surface.
|
|
510
|
+
|
|
511
|
+
Run `deploy.mjs storefront --stack static` in the project folder anyway: `js/wix/` is both the
|
|
512
|
+
browser-side code and the readable spec. Then split by where the call runs. **Reads on the
|
|
513
|
+
server:** port `js/wix/catalog.ts` and `catalog-core.ts` to your language — the same six functions
|
|
514
|
+
returning the same DTO shapes as dicts, one anonymous visitor token per process for these public
|
|
515
|
+
reads (mint and refresh per `client.ts`) — and render shop, category, and PDP in your templates to
|
|
516
|
+
the contracts above, so product names and prices are in the HTML; item-page tags from the entity's
|
|
517
|
+
`seoData`. **Buying in the browser:** the cart drawer on `./js/wix/cart-store.js`, the PDP's
|
|
518
|
+
option picker and the tile's quick add on `./js/wix/product-detail-store.js` (pass the product's
|
|
519
|
+
slug, or the rendered `ProductDetail` as `initial` in a JSON script tag), exactly as the static
|
|
520
|
+
wiring above — the browser owns the shopper's visitor token, so the server never handles
|
|
521
|
+
per-shopper tokens. Routes stay `/shop`, `/category/<slug>`, `/products/<slug>`. Add your public https origin
|
|
522
|
+
to the OAuth app's allowed domains before checkout can return.
|
|
523
|
+
|
|
524
|
+
**Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted.** Same port for
|
|
525
|
+
the reads, run at build time with one anonymous token; the generator must emit every product and
|
|
526
|
+
category page (a URL generator over `fetchCategories()` plus a full `searchCatalog` walk by
|
|
527
|
+
cursor — never only the first page). Run `deploy.mjs storefront --stack static --out <build dir>`
|
|
528
|
+
so `js/wix/` is inside the output the pages import from, point `site.outputDirectory` at that
|
|
529
|
+
folder, `wix release`. Pages sit at different depths (`/`, `/category/…`, `/products/…`): give
|
|
530
|
+
the templates one base path to `js/wix/` (a template variable, or root-relative `/js/wix/…`),
|
|
531
|
+
never a relative `./js/wix/` — it breaks one level down. The frozen grid is the first paint; the
|
|
532
|
+
shop's sort, filters, facets, search, and load-more still run client-side on it through
|
|
533
|
+
`createShopStore()` from `./js/wix/shop-store.js`, exactly as on a static site, so the gallery
|
|
534
|
+
contract above applies. Close
|
|
535
|
+
with the live URL, the rebuild + release command, and one line for the owner: dashboard edits to
|
|
536
|
+
the catalog reach the site when that command runs; cart and checkout are live regardless.
|
|
537
|
+
|
|
401
538
|
### Wiring — React SPA (Vite etc.)
|
|
402
539
|
|
|
403
540
|
Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite
|
|
@@ -415,16 +552,19 @@ client id into `wix/config.ts`; nothing else to configure.
|
|
|
415
552
|
internals or re-derive a request shape. Extend by calling the exports or adding a new
|
|
416
553
|
function in `wix/storefront/` for what they don't cover (API contracts: the `wix-docs` skill).
|
|
417
554
|
- **Selection→cart goes through `useProductDetail`** — never add a product with options by
|
|
418
|
-
picking `variants[0]`, and never gate `canAdd` yourself.
|
|
419
|
-
`QuickAdd` — a tile never adds a product with options itself, and never hides the buy path
|
|
555
|
+
picking `variants[0]`, and never gate `canAdd` yourself. On the PDP that is the shipped
|
|
556
|
+
`OptionPicker`; in the gallery the shipped `QuickAdd` — a tile never adds a product with options itself, and never hides the buy path
|
|
420
557
|
behind "go to the product page" for a product that has no options.
|
|
421
558
|
- **Filters are the shipped `FilterPanel`** — mounted in the gallery whenever the store has a
|
|
422
559
|
catalog to filter; not rebuilt with fewer controls, not dropped because the brief didn't ask.
|
|
423
560
|
- **Categories are pages** — `/category/[slug]` with its SEO block, linked from the chrome; a
|
|
424
561
|
category that exists only as a state toggle on `/shop` has no URL to share or index.
|
|
425
562
|
- Don't wrap shipped calls in your own API routes — they run client-side by design.
|
|
426
|
-
-
|
|
427
|
-
|
|
563
|
+
- Where the shipped components deploy (Astro, React): theme via the `@theme` tokens, and your
|
|
564
|
+
markup uses Tailwind utilities on the same tokens — one design system across shipped and written
|
|
565
|
+
code. No parallel theme files, no hardcoded palette values in components. Where they don't
|
|
566
|
+
(`lib`, `static`, a port): style with whatever your stack does well, on one token set of your
|
|
567
|
+
own; the rule that survives is the token set, not Tailwind.
|
|
428
568
|
- Checkout only through the shipped cart (`checkout()`) — never a hand-built checkout URL.
|
|
429
569
|
- Live data or an honest empty state — never mock products, prices, reviews, or counts.
|
|
430
570
|
- **Prices and ribbons come from the DTOs as-is** — no computed percent-off, no "Sale" badge
|
|
@@ -446,44 +586,23 @@ client id into `wix/config.ts`; nothing else to configure.
|
|
|
446
586
|
|
|
447
587
|
## Point the user to their dashboard
|
|
448
588
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
589
|
+
Hand the owner these links — `{siteId}` is `siteId` in `wix.config.json` (the deploy JSON also prints
|
|
590
|
+
`dashboardUrl`, `productsUrl`, `categoriesUrl`); an entity id fills the placeholder from the seed result.
|
|
591
|
+
|
|
592
|
+
| page | `https://manage.wix.com/dashboard/{siteId}/` + |
|
|
593
|
+
|---|---|
|
|
594
|
+
| Products | `wix-stores/products` |
|
|
595
|
+
| Edit a product | `wix-stores/products/product/{productId}` |
|
|
596
|
+
| Categories | `wix-stores/categories/list` |
|
|
597
|
+
| Inventory | `wix-stores/inventory` |
|
|
598
|
+
| Orders | `ecom-platform/orders-list` |
|
|
599
|
+
| Store settings | `wix-stores/settings` |
|
|
600
|
+
|
|
601
|
+
Real payments additionally need a premium plan + a connected payment method (Settings → Accept
|
|
602
|
+
payments) — mention it, don't treat it as a code failure.
|
|
453
603
|
|
|
454
604
|
## Seeding
|
|
455
605
|
|
|
456
606
|
Per `seed/SEED.md` — a plain-data `plan.json` into `seed-store.mjs`, run from the project
|
|
457
607
|
root. Independent of the frontend work; seed a catalog that exercises the UI (≥1 product with
|
|
458
608
|
a color option, ≥1 on sale, an image per product) unless the brief says otherwise.
|
|
459
|
-
|
|
460
|
-
## Verify (before declaring done)
|
|
461
|
-
|
|
462
|
-
- [ ] `/shop` renders live products SSR (view-source shows product names) through **your**
|
|
463
|
-
grid/card; the `FilterPanel` shows the result count, and on desktop a sidebar beside the
|
|
464
|
-
grid with the price slider and this catalog's facets (a Color option → swatches); category links lead to `/category/<slug>`, which renders scoped and
|
|
465
|
-
carries the SEO tags in view-source; an unknown slug is a 404; empty catalog shows your
|
|
466
|
-
honest empty state.
|
|
467
|
-
- [ ] Shop first screen: at 1280×720 and at 390px wide, at least one full tile — image, name,
|
|
468
|
-
price — is visible without scrolling; the tile with the longest product name and a price
|
|
469
|
-
range shows the whole price at 390px, nothing clipped.
|
|
470
|
-
- [ ] Tiles: the buy control sits under name and price (not over the image); a product with no
|
|
471
|
-
options adds to the cart in one click; a product with options opens the picker across the
|
|
472
|
-
full tile width (a bottom sheet at 390px wide) and adds the chosen variant; the drawer
|
|
473
|
-
opens after either.
|
|
474
|
-
- [ ] Your PDP at 390px wide: image, name, price, the first option, and the buy button are all
|
|
475
|
-
visible before scrolling. Color options render as swatches, the button shows
|
|
476
|
-
`blockedReason` ("Choose Size") until every option is picked, the price is the range until then and the variant's
|
|
477
|
-
price after, a sale shows the labelled "was", a sold-out combination reads "Out of stock",
|
|
478
|
-
a pre-orderable one reads "Pre-order".
|
|
479
|
-
- [ ] Cards: every ribbon renders; a multi-price product shows a range with no struck price; a
|
|
480
|
-
product with a color option shows its swatches.
|
|
481
|
-
- [ ] PDP gallery: one thumbnail per distinct photo — never two selectors leading to the same image.
|
|
482
|
-
- [ ] Cart: a subscription line shows its plan terms under the product name.
|
|
483
|
-
- [ ] Cart: add / quantity ± / remove work; badge count is live; subtotal shows; cart survives
|
|
484
|
-
a reload (same visitor token).
|
|
485
|
-
- [ ] Checkout button redirects to Wix-hosted checkout.
|
|
486
|
-
- [ ] PDP view-source carries the SEO tags (Astro).
|
|
487
|
-
- [ ] Shop/PDP/home are YOUR designs on the tokens; the data-layer/hook/cart files are
|
|
488
|
-
unedited.
|
|
489
|
-
- [ ] Dashboard links handed to the owner.
|