@base44/app-plugin-commerce 0.7.1 → 0.8.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/README.md +4 -10
- package/base44/entities/commerce.ProductReview.jsonc +2 -2
- package/base44/functions/commerce/storefront-catalog/entry.ts +7 -3
- package/package.json +1 -1
- package/scripts/install.js +2 -4
- package/skills/commerce/SKILL.md +9 -3
- package/skills/commerce/docs/api-storefront.md +3 -3
- package/skills/commerce/install/01-install.md +8 -24
- package/skills/commerce/install/02-storefront.md +22 -9
- package/skills/commerce/references/admin-localization.md +95 -0
- package/skills/commerce/references/catalog-rendering.md +1 -1
- package/skills/commerce/references/reviews.md +7 -6
- package/src/commerce/admin/README.md +36 -14
- package/src/commerce/admin/bot/StoreAdminBot.jsx +12 -11
- package/src/commerce/admin/components/AddressForm.jsx +13 -12
- package/src/commerce/admin/components/ConfirmDialog.jsx +5 -4
- package/src/commerce/admin/components/CountrySelect.jsx +12 -8
- package/src/commerce/admin/components/DataTable.jsx +3 -2
- package/src/commerce/admin/components/DateRangePicker.jsx +11 -10
- package/src/commerce/admin/components/EmptyState.jsx +2 -1
- package/src/commerce/admin/components/MediaUploader.jsx +3 -2
- package/src/commerce/admin/components/MetaDataEditor.jsx +4 -3
- package/src/commerce/admin/components/SearchSelect.jsx +4 -3
- package/src/commerce/admin/context/SettingsContext.jsx +12 -25
- package/src/commerce/admin/hooks/useMoney.js +3 -2
- package/src/commerce/admin/i18n/index.js +54 -0
- package/src/commerce/admin/i18n/locales/de.js +896 -0
- package/src/commerce/admin/i18n/locales/en.js +900 -0
- package/src/commerce/admin/i18n/locales/es.js +896 -0
- package/src/commerce/admin/i18n/locales/fr.js +896 -0
- package/src/commerce/admin/i18n/locales/ja.js +896 -0
- package/src/commerce/admin/i18n/locales/pt.js +896 -0
- package/src/commerce/admin/index.jsx +6 -30
- package/src/commerce/admin/layout/AccessDenied.jsx +8 -9
- package/src/commerce/admin/layout/AuthGuard.jsx +2 -1
- package/src/commerce/admin/layout/Sidebar.jsx +29 -18
- package/src/commerce/admin/layout/Topbar.jsx +4 -3
- package/src/commerce/admin/lib/api.js +3 -2
- package/src/commerce/admin/lib/constants.js +44 -43
- package/src/commerce/admin/lib/format.js +4 -3
- package/src/commerce/admin/lib/paths.js +2 -3
- package/src/commerce/admin/pages/Dashboard.jsx +25 -24
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +55 -57
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +22 -21
- package/src/commerce/admin/pages/customers/CustomerEditor.jsx +31 -30
- package/src/commerce/admin/pages/customers/CustomersList.jsx +19 -18
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +75 -71
- package/src/commerce/admin/pages/orders/OrdersList.jsx +16 -15
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +14 -13
- package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +11 -10
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +22 -22
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +18 -17
- package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +18 -17
- package/src/commerce/admin/pages/products/Categories.jsx +14 -13
- package/src/commerce/admin/pages/products/ProductEditor.jsx +16 -15
- package/src/commerce/admin/pages/products/ProductsList.jsx +41 -36
- package/src/commerce/admin/pages/products/Reviews.jsx +54 -46
- package/src/commerce/admin/pages/products/components/AttributesSection.jsx +45 -39
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +8 -7
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +10 -9
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +18 -17
- package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +11 -10
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +9 -10
- package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +3 -9
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +76 -78
- package/src/commerce/admin/pages/reports/Reports.jsx +33 -32
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +36 -39
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +13 -14
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +12 -13
- package/src/commerce/admin/pages/settings/LocationEditor.jsx +48 -47
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +32 -33
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +13 -12
- package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +31 -31
- package/src/commerce/admin/pages/settings/useGroupForm.jsx +4 -3
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +34 -31
- package/src/commerce/admin/pages/status/Webhooks.jsx +11 -10
- package/src/commerce/admin/routes.jsx +31 -43
- package/src/commerce/storefront/pickers.jsx +19 -5
- package/src/commerce/storefront/useCheckout.jsx +28 -3
- package/src/commerce/utils/storefront.js +49 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ It provides a full-featured **commerce data model and behavior** (variant-driven
|
|
|
10
10
|
- **16 backend functions** — 9 admin (`commerce/admin-products`, `commerce/admin-orders`, `commerce/admin-refunds`, `commerce/admin-coupons`, `commerce/admin-customers`, `commerce/admin-reviews`, `commerce/admin-webhooks`, `commerce/admin-reports`, `commerce/admin-tools`), 4 storefront (`commerce/storefront-catalog`, `commerce/storefront-cart`, `commerce/storefront-checkout`, `commerce/storefront-account`), 2 payment (`commerce/payments`, `commerce/payment-webhook`), and an idempotent `commerce/seed-store` — one call seeds the business defaults **and the whole catalog** (products with attributes in, variants/categories/taxonomy created internally).
|
|
11
11
|
- **Payments: manual methods work out of the box; online cards are opt-in** — the seed enables the manual `offline` method (bank transfer, cash on delivery, pickup: on-hold + instructions, no code) and leaves the `card` gateway **disabled**. The order side of card payments *is* premade — checkout routing, payment links for unpaid orders, two idempotent confirmation paths (customer return + webhook) and refund records — so a store that opts in wires a provider by implementing **four functions in one file**, `base44/shared/commerce/card-payment.ts`. **For Stripe there is nothing to write**: `base44/shared/commerce/card-payment.stripe.ts` is a complete implementation used as-is — copy it over the stub and enable the gateway. Any other provider (PayPal, Adyen, a local PSP) follows the same shape. Enable the gateway only with a provider behind it, or checkout answers `503 no_card_payment_provider`. The rule and timing: [`skills/commerce/install/03-data.md`](./skills/commerce/install/03-data.md); provider mechanics: [`skills/commerce/references/online-payments.md`](./skills/commerce/references/online-payments.md). The admin can add more manual methods in Settings → Payments.
|
|
12
12
|
- **Shared commerce engine** (`base44/shared/commerce/`) — totals, tax, shipping, coupons, stock, order lifecycle, webhook dispatch (HMAC-signed), emails, card-payment plumbing, plus static country/currency/continent data.
|
|
13
|
-
- **Admin UI** (`src/commerce/admin/`) — a React/Tailwind/shadcn admin with a familiar store back-office information architecture: dashboard, orders, products, coupons, customers, reports, and full settings including webhooks. Admin-role gated.
|
|
13
|
+
- **Admin UI** (`src/commerce/admin/`) — a React/Tailwind/shadcn admin with a familiar store back-office information architecture: dashboard, orders, products, coupons, customers, reports, and full settings including webhooks. Admin-role gated. Localized — ships in English (default), German, Spanish, French, Japanese and Portuguese, switched by one line of code, with a documented recipe for adding any other language (no i18n dependency; see [`src/commerce/admin/README.md`](./src/commerce/admin/README.md) and the skill's `references/admin-localization.md`).
|
|
14
14
|
- **Storefront helpers** (`src/commerce/utils/`) — framework-free, dependency-free modules for the shopfront you build: `storefront.js` is the API client (`createStorefront(base44)` — cart-token lifecycle, cached store-info, catalog/cart/checkout/reviews/return-page calls); `variants.js` maps an attribute selection (Size, Color) onto a `ProductVariation` and back, plus per-option availability and price ranges; `price.js` encodes the from-price and price-range rules; `totals.js` projects a cart *or* an order into one summary shape; `address-spec.js` is the checkout address form as data; `images.js` and `ribbons.js` normalize the two catalog fields that are arrays of objects (`{src, name, alt}` images, `{id, name}` ribbons) rather than strings; `types.js` writes the catalog shapes down as JSDoc typedefs (`StorefrontProduct` and the rest), so what a field holds is answerable from the frontend; `shipping-promos.js` reads the store's real free-shipping configuration so "Free shipping over €150" states a configured rule rather than an invented number.
|
|
15
15
|
- **Storefront React layer** (`src/commerce/storefront/`) — **headless: the logic is premade, the UI never is.** Nothing in the layer renders markup or carries CSS; every element, class and word of copy in the storefront you build is yours, so a brief like "make it feel like <site>" applies to the whole store, checkout included. **It ships no customer-facing copy either**: where a state needs words you get the *state* — `buy.state`, a picker's `hint.code`, the checkout's `blockers`, a review's `status` — and write the sentence. What ships is every piece of logic that is the same in all stores: `StorefrontProvider` (+ `useStorefront`/`useStoreInfo`/`useFormatMoney`/`useCountries`), `useProductList`/`useCategories`/`useRibbons`, `useProduct`/`useAddToCart`, `useCart`/`useCartLine` (+ `useCartUI`/`CartUIProvider` for a drawer), `useCheckout`/`CheckoutProvider`/`useCheckoutContext`, `useOrderReturn`/`orderReceivedUrl` — plus three render-prop components that stay just as headless (`ShippingMethodPicker`/`PaymentMethodPicker` for the two checkout choices that are store data, `CartLine` for per-row cart bindings), one deliberately rendered-but-unstyled component (`AddressFields` — the checkout address form, whose state/province and `autoComplete` mechanics are where hand-rolled forms break; it ships no CSS and styles via `data-part` selectors or class props), and the framework-free view-model helpers re-exported so one import line covers a page (`variantAxes`, `productPrice`, `productImages`, `productRibbons`, `productSpecs`, `attributesLabel`, `cartTotalsLines`/`orderTotalsLines`, `addressFieldSpec`). Each hook's doc comment states the render rules that keep a store correct (an unbuyable variant option renders disabled, not hidden; a receipt page must render `paymentInstructions`; …) and names its return type from `storefront/types.js`, so a page reads a field's shape off the hook instead of off a backend function. Needs React and nothing else.
|
|
16
16
|
- **StoreAdmin agent + bot** — an AI copilot (`base44/agents/commerce/StoreAdmin.jsonc`, registered as `commerce/StoreAdmin`) with the `commerce/*` functions attached directly as tools (calls run as the chatting user → `requireAdmin()` still applies), variant-aware order editing, plus a chat panel in the admin sidebar with GFM markdown-table rendering.
|
|
@@ -85,18 +85,12 @@ From your existing Base44 app:
|
|
|
85
85
|
npx npq install <only the missing names> # npq audits the package before npm installs it
|
|
86
86
|
```
|
|
87
87
|
See [`src/commerce/admin/README.md`](./src/commerce/admin/README.md) for the exact shadcn component list.
|
|
88
|
-
5. **Mount the admin**
|
|
88
|
+
5. **Mount the admin router** in your app:
|
|
89
89
|
```jsx
|
|
90
|
-
import AdminApp
|
|
90
|
+
import AdminApp from "@/commerce/admin";
|
|
91
91
|
// inside your <Routes>:
|
|
92
|
-
<Route path="/store-admin" element={<AdminApp />}
|
|
93
|
-
<Route index element={<Dashboard />} />
|
|
94
|
-
<Route path="orders" element={<OrdersList />} />
|
|
95
|
-
{/* …products, customers, coupons, reports… */}
|
|
96
|
-
<Route path="*" element={<AdminRoutes />} /> {/* editors, settings, webhooks */}
|
|
97
|
-
</Route>
|
|
92
|
+
<Route path="/store-admin/*" element={<AdminApp />} />
|
|
98
93
|
```
|
|
99
|
-
Name the section they group under in `base44/ui.jsonc` (app-owned — edit in place): `{ "version": 1, "sections": [{ "path": "/store-admin/*", "name": "Store Management" }] }`
|
|
100
94
|
6. **Grant yourself the `admin` role** (Base44 dashboard → users, or `users.inviteUser(email, "admin")`). The admin UI refuses non-admins.
|
|
101
95
|
7. **Seed the store.** Either open `/store-admin` and click **Initialize store defaults** on the first-run setup screen, or call `commerce/seed-store` directly — it creates the settings groups, the payment gateway rows (`offline` enabled, `card` off — enable it only with a provider wired) and — unless you pass your own `locations` — a fallback Shipping & Tax Location, plus the catalog: pass `products` (whole products with attributes — variants, categories, ribbons and taxonomy are created internally) or `with_sample_data: true` for the generic demo. Either way pass `store_name` (the app's name) — it is required on a first seed and becomes both the email subject prefix and the sender name. Once the `general` settings group exists the store counts as ready and the first-run screen stops appearing. Worked example: [`skills/commerce/install/03-data.md`](./skills/commerce/install/03-data.md); the full payload contract: [`skills/commerce/docs/api-admin.md`](./skills/commerce/docs/api-admin.md). Shipping zones are part of the same call — `locations` takes `continents: ["EU"]` and `rest_of_world: true`, so "€20 in Europe, €100 worldwide" is six lines.
|
|
102
96
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"review": {
|
|
26
26
|
"type": "string",
|
|
27
|
-
"description": "Review
|
|
27
|
+
"description": "Review text. May be empty for a stars-only review (submit-review requires text or a rating, not both)."
|
|
28
28
|
},
|
|
29
29
|
"rating": {
|
|
30
30
|
"type": "integer",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"description": "Derived: reviewer purchased the product. Set by commerce/storefront-catalog submit-review."
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
|
-
"required": ["product_id"
|
|
41
|
+
"required": ["product_id"],
|
|
42
42
|
"rls": {
|
|
43
43
|
"read": { "user_condition": { "role": "admin" } },
|
|
44
44
|
"create": { "user_condition": { "role": "admin" } },
|
|
@@ -396,12 +396,16 @@ async function submitReview(sr: any, p: any, user: any): Promise<any> {
|
|
|
396
396
|
const review = String(p.review ?? "").trim();
|
|
397
397
|
const rating = p.rating == null ? null : Math.floor(Number(p.rating));
|
|
398
398
|
|
|
399
|
-
if (!review) {
|
|
400
|
-
throw new HttpError(400, "review is required.", "review_incomplete");
|
|
401
|
-
}
|
|
402
399
|
if (rating != null && (rating < 0 || rating > 5)) {
|
|
403
400
|
throw new HttpError(400, "Rating must be between 0 and 5.", "invalid_rating");
|
|
404
401
|
}
|
|
402
|
+
// Text or stars — either alone is a valid review. Requiring text rejected
|
|
403
|
+
// every stars-only submission from a form that treats the write-up as
|
|
404
|
+
// optional, with a 400 the customer can't act on. Rating 0 is "unrated"
|
|
405
|
+
// (the aggregates ignore it), so it can't stand in for the missing text.
|
|
406
|
+
if (!review && !(rating != null && rating >= 1)) {
|
|
407
|
+
throw new HttpError(400, "A review needs text or a star rating.", "review_incomplete");
|
|
408
|
+
}
|
|
405
409
|
|
|
406
410
|
const verified = await hasPurchased(sr, reviewerEmail, product.id);
|
|
407
411
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base44",
|
package/scripts/install.js
CHANGED
|
@@ -217,10 +217,8 @@
|
|
|
217
217
|
"\n" +
|
|
218
218
|
" 1. No deps to add: sonner, recharts and react-markdown ship with the default\n" +
|
|
219
219
|
" Base44 template — check package.json and npm i only what is truly missing\n" +
|
|
220
|
-
' 2. Mount the admin
|
|
221
|
-
"
|
|
222
|
-
' discovers pages from) and <Route path="*" element={<AdminRoutes />} /> for the\n' +
|
|
223
|
-
" rest, plus the mandatory /order-received route (useOrderReturn + your markup)\n" +
|
|
220
|
+
' 2. Mount the admin router: <Route path="/store-admin/*" element={<AdminApp />} />\n' +
|
|
221
|
+
" plus the mandatory /order-received route (useOrderReturn + your markup)\n" +
|
|
224
222
|
" 3. Seed the store — one commerce/seed-store call (store_name required) takes the\n" +
|
|
225
223
|
" catalog, currency, shipping locations and payment methods\n" +
|
|
226
224
|
" 4. CLI installs only: npx base44 agents push (the hosted runtime syncs agents on write)"
|
package/skills/commerce/SKILL.md
CHANGED
|
@@ -29,6 +29,11 @@ install, then the storefront UI, then the catalog. Every install runs all three,
|
|
|
29
29
|
so fetching them one at a time defers nothing you will not read anyway and costs
|
|
30
30
|
a model round trip per file. Read nothing *else* up front.
|
|
31
31
|
|
|
32
|
+
**Admin language.** English: nothing to do. de/es/fr/ja/pt: in
|
|
33
|
+
`src/commerce/admin/i18n/index.js` repoint `import active from "./locales/de.js"`
|
|
34
|
+
and stop — never edit admin screens. Other:
|
|
35
|
+
[`references/admin-localization.md`](./references/admin-localization.md).
|
|
36
|
+
|
|
32
37
|
## Four things to hold from the start
|
|
33
38
|
|
|
34
39
|
- **Entity names are dotted; SDK access is bracket syntax only** —
|
|
@@ -117,18 +122,19 @@ batch (above).
|
|
|
117
122
|
|
|
118
123
|
| Topic | Open when | Size |
|
|
119
124
|
|---|---|---|
|
|
120
|
-
| [`install/01-install.md`](./install/01-install.md) | installing — routes you to 02 and 03 |
|
|
121
|
-
| [`install/02-storefront.md`](./install/02-storefront.md) | building storefront pages |
|
|
125
|
+
| [`install/01-install.md`](./install/01-install.md) | installing — routes you to 02 and 03 | 6K |
|
|
126
|
+
| [`install/02-storefront.md`](./install/02-storefront.md) | building storefront pages | 37K |
|
|
122
127
|
| [`install/03-data.md`](./install/03-data.md) | seeding catalog, shipping rates/zones, payments; re-callable per slice | 11K |
|
|
123
128
|
| [`docs/entities.md`](./docs/entities.md) | any direct entity read/write ("which entity holds X") | 11K |
|
|
124
129
|
| [`references/catalog-rendering.md`](./references/catalog-rendering.md) | field shapes each catalog call returns, variant edge cases | 16K |
|
|
125
130
|
| [`references/shipping-and-tax.md`](./references/shipping-and-tax.md) | zones beyond 03's recipe, taxes, day-2 edits | 8K |
|
|
126
131
|
| [`references/online-payments.md`](./references/online-payments.md) | enabling card payments, or wiring the provider — at install or any time later | 8K |
|
|
127
132
|
| [`references/storefront-verification.md`](./references/storefront-verification.md) | driving the storefront from a browser script | 3K |
|
|
128
|
-
| [`references/reviews.md`](./references/reviews.md) | review policies (login-gated, verified buyers), moderation |
|
|
133
|
+
| [`references/reviews.md`](./references/reviews.md) | review policies (open by default; login-gated, verified buyers), moderation | 6K |
|
|
129
134
|
| [`references/store-settings.md`](./references/store-settings.md) | changing store behavior through settings keys | 5K |
|
|
130
135
|
| [`references/emails.md`](./references/emails.md) | order-email recipients, subjects, the log | 5K |
|
|
131
136
|
| [`references/admin-product-form.md`](./references/admin-product-form.md) | editing the shipped product editor | 6K |
|
|
137
|
+
| [`references/admin-localization.md`](./references/admin-localization.md) | a language **outside** en/de/es/fr/ja/pt (the six ship; switching is one import) | 5K |
|
|
132
138
|
| [`references/store-admin-agent.md`](./references/store-admin-agent.md) | changing the StoreAdmin copilot | 3K |
|
|
133
139
|
| [`references/guest-access-security.md`](./references/guest-access-security.md) | **adding your own function or entity**; RLS/identity questions | 5K |
|
|
134
140
|
| [`references/operations.md`](./references/operations.md) | scheduled maintenance, scaling limits, outbound webhooks | 7K |
|
|
@@ -114,14 +114,14 @@ This is the **only** way a storefront can enumerate ribbons (the entity is admin
|
|
|
114
114
|
No payload. Returns `{ "attributes": [ { ...attribute, "terms": [ ...values ] } ] }` — each attribute (`id, name, code, order`) with its values (`id, attribute_id, name, order, count`), both sorted by `order`; for filter UIs. Filter with `list-products` `attribute_id` (id or attribute **name**) + `attribute_term` (the value name); `code` is the stable key for a URL.
|
|
115
115
|
|
|
116
116
|
### `submit-review`
|
|
117
|
-
**Payload:** `{ product_id, email?, reviewer?, review
|
|
117
|
+
**Payload:** `{ product_id, email?, reviewer?, review?, rating? }` — at least one of `review` (text) or `rating` (1–5); `email` is required for guests (`400 email_required`); a signed-in caller's session email always wins.
|
|
118
118
|
|
|
119
119
|
> A React storefront reaches this call as `submitReview` (and the paginated list as `getProductReviews`) on the client from `@/commerce/storefront`'s `useStorefront()`, with the list itself already riding along on the product; policies and moderation are [`../references/reviews.md`](../references/reviews.md). Read on for the raw contract.
|
|
120
120
|
|
|
121
|
-
**Public by default: anyone can review with an email address — no login.** A guest passes `email`; for a signed-in caller the session email always wins (the payload cannot impersonate). `reviewer` is the display name only, defaulting to the account's `full_name` then the email's local part. `rating` is
|
|
121
|
+
**Public by default: anyone can review with an email address — no login.** A guest passes `email`; for a signed-in caller the session email always wins (the payload cannot impersonate). `reviewer` is the display name only, defaulting to the account's `full_name` then the email's local part. **Text or stars — either alone is valid**: `rating` is 0–5 (0 = unrated, so a stars-only submission needs 1–5); `review_incomplete` fires only when both are missing. `verified` is derived from the email's order history. Status is `hold` unless `products.auto_approve_reviews` — the one server-side switch, so a hardcoded "awaiting approval" message is wrong when it is on. Stricter policies (login-gated, verified buyers only, rating required) are the storefront's own gate around this call — the server accepts any valid email: [`../references/reviews.md`](../references/reviews.md).
|
|
122
122
|
|
|
123
123
|
**Response:** `{ "review_id", "status": "hold"|"approved", "verified": true }`
|
|
124
|
-
**Errors:** `404 not_found`, `400 email_required|review_incomplete
|
|
124
|
+
**Errors:** `404 not_found`, `400 email_required|review_incomplete` (neither text nor stars)`|invalid_rating`.
|
|
125
125
|
|
|
126
126
|
---
|
|
127
127
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
stage: install/01
|
|
3
3
|
read_when: "The commerce kit's files were just copied into the app, or you are installing it now."
|
|
4
|
-
skip_when: "
|
|
4
|
+
skip_when: "The admin already mounts at /store-admin/* behind the shipped AuthGuard and / routes somewhere real."
|
|
5
5
|
forget_when: "The checklist at the bottom of this file passes (admin mounts, / routes somewhere real, /order-received exists)."
|
|
6
6
|
carry_forward:
|
|
7
|
-
- "The admin's six picker-visible routes are literal <Route> JSX in src/App.jsx (the platform discovers pages by reading that file — an array or a .map() discovers nothing); the rest run off its <Route path=\"*\" element={<AdminRoutes />} />."
|
|
8
7
|
- "Admin enforcement is three layers — AuthGuard (UI), admin-only entity RLS, requireAdmin() in every admin function. Never weaken any of them."
|
|
9
8
|
- "/order-received must exist as a route: every payment link returns there, and confirming is what marks an order paid."
|
|
10
9
|
- "The storefront header shows a visible \"Store manager\" link to /store-admin when the signed-in user's role is admin, and nothing for everyone else."
|
|
@@ -42,33 +41,18 @@ Image generation is the slowest step and nothing depends on it until seed time;
|
|
|
42
41
|
|
|
43
42
|
The only dependency edges are *image URLs → seed payload* and *seed done → real products on the pages*.
|
|
44
43
|
|
|
45
|
-
## Mount the admin
|
|
46
|
-
|
|
47
|
-
The admin mounts as a **layout route in the app's own `src/App.jsx`**: the six screens the store owner opens from the builder are declared there as literal `<Route>` JSX, and everything deeper goes to `<AdminRoutes />` on a splat. The platform discovers an app's pages by reading that file — a screen declared anywhere else is unreachable from the page picker.
|
|
44
|
+
## Mount the admin router
|
|
48
45
|
|
|
49
46
|
```jsx
|
|
50
|
-
import AdminApp
|
|
51
|
-
import
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{/* Literal JSX — the platform reads this file, it never runs it. Do not refactor into a map. */}
|
|
55
|
-
<Route path="/store-admin" element={<AdminApp />}>
|
|
56
|
-
<Route index element={<Dashboard />} />
|
|
57
|
-
<Route path="orders" element={<OrdersList />} />
|
|
58
|
-
<Route path="products" element={<ProductsList />} />
|
|
59
|
-
<Route path="customers" element={<CustomersList />} />
|
|
60
|
-
<Route path="coupons" element={<CouponsList />} />
|
|
61
|
-
<Route path="reports" element={<Reports />} />
|
|
62
|
-
<Route path="*" element={<AdminRoutes />} /> {/* editors, settings, webhooks */}
|
|
63
|
-
</Route>
|
|
47
|
+
import AdminApp from "@/commerce/admin";
|
|
48
|
+
import { Navigate } from "react-router-dom";
|
|
49
|
+
|
|
50
|
+
<Route path="/store-admin/*" element={<AdminApp />} />
|
|
64
51
|
<Route path="/" element={<Navigate to="/store-admin" replace />} /> {/* until a storefront exists */}
|
|
65
52
|
<Route path="/order-received" element={<OrderReceived />} /> {/* mandatory — see below */}
|
|
66
53
|
```
|
|
67
54
|
|
|
68
|
-
- **
|
|
69
|
-
- **Don't add `settings` to the list** — it is a tabbed layout around a nested route, so it only renders correctly from the splat.
|
|
70
|
-
- **Elsewhere than `/store-admin`**: change the layout route's path and pass the prefix — `<AdminApp basePath="/backoffice" />`; the children are unchanged.
|
|
71
|
-
- **Name the group** in `base44/ui.jsonc` — app-owned, so edit it in place, keep any other keys, never recreate a deleted one: `{ "version": 1, "sections": [{ "path": "/store-admin/*", "name": "Store Management" }] }`
|
|
55
|
+
- **The `/*` splat is required** — without it every nested admin link 404s. Mounting elsewhere: `<AdminApp basePath="/backoffice" />` (prefix without the splat).
|
|
72
56
|
- **Give `/` something** — a blank app has no `/` route, and "page not found" at the app's own URL reads like a broken install.
|
|
73
57
|
- **Link the admin from the storefront header** — otherwise the merchant has no way in but typing the URL. Resolve the signed-in user once (`base44.auth.me()`, rejection/no session = not an admin, never blocking the page) and render a plainly visible "Store manager" link to `/store-admin` in the header when `role === "admin"` — and nothing at all for everyone else.
|
|
74
58
|
- **`/order-received` is mandatory**, even offline-only: every payment link returns there, and confirming is what marks an order paid — without it a paying customer hits a 404 and the order stays unpaid. The page is one hook, `useOrderReturn()` ([`./02-storefront.md`](./02-storefront.md)). A different path must be set in Settings → General (`general.order_received_path`).
|
|
@@ -85,7 +69,7 @@ Storefront functions are public on purpose (per-action verification, above). To
|
|
|
85
69
|
|
|
86
70
|
## Done — forget this file
|
|
87
71
|
|
|
88
|
-
- [ ]
|
|
72
|
+
- [ ] `/store-admin/*` mounted with the splat, behind the shipped `AuthGuard`; the three enforcement layers untouched.
|
|
89
73
|
- [ ] `/` routes somewhere real; `/order-received` is a route.
|
|
90
74
|
- [ ] The storefront header shows a visible `/store-admin` link to signed-in admins, and to nobody else.
|
|
91
75
|
- [ ] Anonymous function invocation is allowed in the app's settings.
|
|
@@ -48,7 +48,7 @@ import AdminApp from "@/commerce/admin";
|
|
|
48
48
|
<Route path="/product/:slug" element={<ProductPage />} />
|
|
49
49
|
{/* /bag, /checkout, and /order-received — which is mandatory */}
|
|
50
50
|
</Route>
|
|
51
|
-
<Route path="/store-admin" element={<AdminApp />}
|
|
51
|
+
<Route path="/store-admin/*" element={<AdminApp />} /> {/* own chrome, outside the provider */}
|
|
52
52
|
</Routes>
|
|
53
53
|
</BrowserRouter>
|
|
54
54
|
|
|
@@ -116,7 +116,7 @@ import { useProductList, useCategories, useStoreInfo, useFormatMoney, productPri
|
|
|
116
116
|
|
|
117
117
|
⚑ **Render paging whenever `hasNext` is true** — `{list.hasNext && <button type="button" onClick={list.next} disabled={list.busy}>…</button>}` (append mode: `list.loadMore`); a page that renders nothing for paging ships a catalog silently capped at `per_page`. Drive filters from `useCategories()`/`useRibbons()` data via `setParams`, never from hardcoded names — a renamed ribbon must not strand a dead button.
|
|
118
118
|
|
|
119
|
-
A card can render `name`, `productImages(row)[0]`, `productPrice(row, { formatMoney }).label` (already "From €19.99" when the product sells variants — there is no product `type` flag, and `product.price` alone is a rolled-up from-price), `on_sale`, `short_description`, `stock_status`, `average_rating`/`rating_count`, `productRibbons(row)`, `productSpecs(row)`. ⚑ **Images and ribbons are objects, either may be empty** — render your placeholder, never a broken `<img>` or a raw object. Field matrix: [`../references/catalog-rendering.md`](../references/catalog-rendering.md). That list is an inventory, not a card design and not an order to render in. An even grid of identical cards, each carrying the same name/price/stars trio, is where a generated store lands by default and almost never where this catalog belongs: give the grid a rhythm (a hero piece spanning two columns, an editorial break between rows, a denser tile for a large catalog), and lead each card with the one or two fields *these* products are judged on — carat weight, focal length,
|
|
119
|
+
A card can render `name`, `productImages(row)[0]`, `productPrice(row, { formatMoney }).label` (already "From €19.99" when the product sells variants — there is no product `type` flag, and `product.price` alone is a rolled-up from-price), `on_sale`, `short_description`, `stock_status`, `average_rating`/`rating_count`, `productRibbons(row)`, `productSpecs(row)`. ⚑ **Images and ribbons are objects, either may be empty** — render your placeholder, never a broken `<img>` or a raw object. Field matrix: [`../references/catalog-rendering.md`](../references/catalog-rendering.md). That list is an inventory, not a card design and not an order to render in. An even grid of identical cards, each carrying the same name/price/stars trio, is where a generated store lands by default and almost never where this catalog belongs: give the grid a rhythm (a hero piece spanning two columns, an editorial break between rows, a denser tile for a large catalog), and lead each card with the one or two fields *these* products are judged on — carat weight, focal length, ABV — read off `productSpecs(row)`.
|
|
120
120
|
|
|
121
121
|
⚑ **Ribbons belong in both views** — grid and product page. They are the merchant's own merchandising ("Limited", "Last pieces"), and each links to its filtered listing (`/collection?ribbon_id=<id>`). `productRibbons(row)` hands you `{id, name}` **objects** — render `r.name`, key the link on `r.id`; the entry itself in JSX is React's "Objects are not valid as a React child". Never render a bare "Ribbons:" label with nothing after it. ⚑ **A ribbon link inside a card that is itself a link nests `<a>` in `<a>`** — invalid, React warns. In the grid use plain labels, or link the image and title rather than the whole card; keep ribbon links on the product page.
|
|
122
122
|
|
|
@@ -156,9 +156,13 @@ Build your layout from — all optional, **not one component style**:
|
|
|
156
156
|
- **Variant selector** — `variantAxes(view, p.pick)`, one entry per axis:
|
|
157
157
|
|
|
158
158
|
```jsx
|
|
159
|
+
const SWATCH = { Ivory: "#F2EDE4", "Obsidian Black": "#101014" }; // this catalog's colour names → CSS
|
|
159
160
|
{variantAxes(view, p.pick).map((axis) => (
|
|
160
161
|
<fieldset key={axis.key}>{/* label from axis.name / axis.selectedOption */}
|
|
161
|
-
{axis.options.map((o) => (
|
|
162
|
+
{axis.options.map((o) => /colou?r/i.test(axis.name) ? (
|
|
163
|
+
<button key={o.value} disabled={o.disabled} aria-pressed={o.selected} onClick={o.pick}
|
|
164
|
+
className="swatch" style={{ background: SWATCH[o.value] }} title={o.value} aria-label={o.value} />
|
|
165
|
+
) : (
|
|
162
166
|
<button key={o.value} disabled={o.disabled} aria-pressed={o.selected} onClick={o.pick}>
|
|
163
167
|
{o.value}{/* o.outOfStock → mark visibly */}
|
|
164
168
|
</button>
|
|
@@ -167,7 +171,7 @@ Build your layout from — all optional, **not one component style**:
|
|
|
167
171
|
))}
|
|
168
172
|
```
|
|
169
173
|
|
|
170
|
-
⚑ **One control per axis, never a list of variations**, and ⚑ **an unbuyable option renders `disabled`, never hidden** (`outOfStock` stays visible, just marked). `view.missingAxes` names what's unpicked. **
|
|
174
|
+
⚑ **One control per axis, never a list of variations**, and ⚑ **an unbuyable option renders `disabled`, never hidden** (`outOfStock` stays visible, just marked). `view.missingAxes` names what's unpicked. ⚑ **Pick the control per attribute, as the branch above does**: a colour axis as colour circles (the name stays reachable — `title`, `aria-label`, and the axis label showing `selectedOption`), size chips beside a size guide, a select for a long list; a specialty control where it genuinely fits, not on every axis — but every axis as the same bare chip row is the flattest page this kit produces. Any control keeps the contract: disabled, out-of-stock marked, selection visible.
|
|
171
175
|
- **Buy box** — one button, and **you supply its four words**:
|
|
172
176
|
|
|
173
177
|
```jsx
|
|
@@ -190,7 +194,16 @@ Build your layout from — all optional, **not one component style**:
|
|
|
190
194
|
|
|
191
195
|
⚑ **Never `.map()` the whole list into one grey label/value table** — that is the single most reliable tell of a generated product page. Design the two or three rows that carry *this* catalog's meaning as what they are (a weight set in the display face, a composition as bars, a provenance beside its place); let the rest fall through to the plain row, and don't feel obliged to keep them in one block — a spec can sit under the gallery, beside the price, or inside the description. Branch on `s.key` too where one particular modifier deserves its own treatment regardless of type. ⚑ **Look a spec up with `findSpec(rows, "care")`** (ignores case, spaces, `_`, `-`): meta keys are free text (`care`, `Care`, `Care Instructions`), so `rows.find(s => s.label === "Care")` silently never matches and renders the fallback forever. `[]` means no section at all.
|
|
192
196
|
- **Breadcrumbs** — from `categories` (`/collection?category_id=${c.id}`); skip on a flat catalog. Ribbons (`productRibbons(product)`) are labels, not breadcrumbs.
|
|
193
|
-
- **Reviews, only if the store wants them** — no review UI is a complete outcome (then no star ratings on cards either: an average of nothing is `0`).
|
|
197
|
+
- **Reviews, only if the store wants them** — no review UI is a complete outcome (then no star ratings on cards either: an average of nothing is `0`). ⚑ **Both shapes are exact** — entity-style names (`content`, `reviewer_name`) throw on submit and render blank in the list:
|
|
198
|
+
|
|
199
|
+
```jsx
|
|
200
|
+
const client = useStorefront(); // submitReview is a CLIENT METHOD, not a barrel export
|
|
201
|
+
p.reviews // { items, page, per_page, has_next }
|
|
202
|
+
p.reviews.items[0] // { id, reviewer, review, rating, verified, created_date }
|
|
203
|
+
await client.submitReview({ product_id, review, rating, reviewer, email }) // NOT content/reviewer_name/reviewer_email
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Text or stars — either alone submits. ⚑ **The form renders for every visitor by default** (guests supply an email; hide it when signed in) — login-gate it only when the store asks. ⚑ Derive the confirmation from the response's `status` (`"approved"` vs `"hold"`) — a hardcoded "awaiting approval" lies to every auto-approving store — and refresh the list after, or the review doesn't appear. Codes, policies, moderation: [`../references/reviews.md`](../references/reviews.md).
|
|
194
207
|
- **Title** — give each page type its own `<title>` and description; a store whose every page shares one static title is invisible to search. Nothing here emits structured data either — if the store wants rich results, emit your own `Product`/`Offer` JSON-LD from `product` and `view.display` (price, currency, availability).
|
|
195
208
|
|
|
196
209
|
## Cart / bag
|
|
@@ -249,9 +262,9 @@ import { CheckoutProvider, useCheckoutContext, AddressFields, ShippingMethodPick
|
|
|
249
262
|
|
|
250
263
|
`useCheckout` reprices shipping/tax from the address automatically (debounced, never on a half-typed address), derives the shipping and payment choices, gates the button, and `placeOrder()` handles **both** navigations — online gateway → provider redirect, everything else → `/order-received` — as **full page loads** (`<CheckoutProvider options={{ orderReceivedPath: null }}>` for a router transition instead). `CheckoutProvider` shares it across the page's regions.
|
|
251
264
|
|
|
252
|
-
⚑ Rules: render each picker's `hint` and every branch; a single shipping or payment option still *shows* what it is — never a picker of one, never "nothing selected". Render `addressError` on the address fields. ⚑ Payment methods, currency and countries come from `useStoreInfo()`/`useCountries()` only — `cart.payment_gateways` is always `undefined`, and a default store offers `offline` only, so never hardcode a card option.
|
|
265
|
+
⚑ Rules: render each picker's `hint` and every branch; a single shipping or payment option still *shows* what it is — never a picker of one, never "nothing selected". ⚑ Picks are instant: both pickers reflect a click immediately (shipping optimistically), and `mustChoose` stays true after a choice — the radios keep rendering, still changeable; never disable options while `syncing`/`choosing` (the hint covers it). Render `addressError` on the address fields. ⚑ Payment methods, currency and countries come from `useStoreInfo()`/`useCountries()` only — `cart.payment_gateways` is always `undefined`, and a default store offers `offline` only, so never hardcode a card option.
|
|
253
266
|
|
|
254
|
-
⚑ **A disabled place-order button must say why** — the silent disabled button is the most common checkout dead end. `blockers` is an array of codes; write one line per code, in the store's voice, anchored near the field that fixes it: `empty_cart`
|
|
267
|
+
⚑ **A disabled place-order button must say why** — the silent disabled button is the most common checkout dead end. `blockers` is an array of codes; write one line per code, in the store's voice, anchored near the field that fixes it: `empty_cart` · `billing_incomplete` (required address fields — `missingBillingFields` names them) · `shipping_address_incomplete` (the separate delivery address) · `shipping_address_required` (no address to price yet) · `shipping_method_required` (choose a delivery option) · `shipping_not_available` (can't deliver there) · `payment_method_required` · `cart_loading` / `shipping_recalculating` (transient — a quiet "one moment", not an error).
|
|
255
268
|
|
|
256
269
|
The pickers' `hint.code` works the same way (`missing_address`, `none_available`, `syncing` for shipping; `none_available` for payment): write those words once, and prefer `hint.serverMessage` when it is set — the backend's explanation is more specific than anything you can write.
|
|
257
270
|
|
|
@@ -282,7 +295,7 @@ function CheckoutForm() {
|
|
|
282
295
|
{m.title} {m.costLabel}
|
|
283
296
|
</label>
|
|
284
297
|
))}
|
|
285
|
-
{!mustChoose && chosen && <p>{chosen.title} {chosen.costLabel}</p>}
|
|
298
|
+
{!mustChoose && chosen && <p>{chosen.title} {chosen.costLabel}</p>}{/* the lone option */}
|
|
286
299
|
</fieldset>
|
|
287
300
|
)}
|
|
288
301
|
</ShippingMethodPicker>
|
|
@@ -304,7 +317,7 @@ function CheckoutForm() {
|
|
|
304
317
|
|
|
305
318
|
**`<AddressFields>` is the one shipped component — use it, never hand-roll the address form.** It owns what hand-rolled forms get wrong: the state/province field appears with the right options once a country is picked (shipping rates and taxes match on country *plus* state, so a form without it mis-prices US/CA/AU orders with no error anywhere), every field keeps its `autoComplete` token (what makes browser autofill work), required marks arm on first blur, and the server's "we don't ship there" lands on the country field. `which="shipping"` renders null until `shipToDifferent` is on — the deliver-elsewhere checkbox itself is yours, wired to `c.shipToDifferent` / `c.setShipToDifferent`.
|
|
306
319
|
|
|
307
|
-
It ships **no CSS** bar a `max-width:100%` cap on the selects (an unstyled checkout must not scroll sideways): every element carries `data-part` (`address-fields`, `field`, `label`, `control`, `required`, `error`) plus `data-key` (the field) and `data-span` (1 or 2 — the field's natural width in a two-column grid), so style it in your `index.css` via `[data-part]` selectors or pass `className`/`classes={{ field, label, control, error }}`. ⚑ **`data-part` sits on the element, not a wrapper** — `select[data-part="control"]`, never `[data-part="control"] input`: the descendant form matches nothing and ships the form unstyled. Props: `includeCompany` (
|
|
320
|
+
It ships **no CSS** bar a `max-width:100%` cap on the selects (an unstyled checkout must not scroll sideways): every element carries `data-part` (`address-fields`, `field`, `label`, `control`, `required`, `error`) plus `data-key` (the field) and `data-span` (1 or 2 — the field's natural width in a two-column grid), so style it in your `index.css` via `[data-part]` selectors or pass `className`/`classes={{ field, label, control, error }}`. ⚑ **`data-part` sits on the element, not a wrapper** — `select[data-part="control"]`, never `[data-part="control"] input`: the descendant form matches nothing and ships the form unstyled. Props: `includeCompany` (false), `includePhone` (true), `omit={["…"]}`, `labels={{ postcode: "ZIP code" }}`, `selectPlaceholder`, and two escape hatches — `inputRender` swaps the control only (spread the handed `dom` props onto your input), `fieldRender` replaces the whole labeled block. `c.missingBillingFields` stays the live list of what is missing, for your own per-field marks.
|
|
308
321
|
|
|
309
322
|
⚑ **The `stage === "submitted"` guard goes above the empty-cart branch** — placing an order clears the cart before the browser navigates, and without the guard the page flashes an empty bag over a just-placed order.
|
|
310
323
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
stage: reference
|
|
3
|
+
read_when: "The admin must run in a language OUTSIDE the six that ship (en/de/es/fr/ja/pt), or you are adding new admin UI text that has to localize."
|
|
4
|
+
skip_when: "The admin stays in English (nothing to do) — or it runs in German, Spanish, French, Japanese or Portuguese, which is one import line in src/commerce/admin/i18n/index.js and needs nothing from this file. Also skip for storefront copy (the storefront ships none — write it directly in the store's language) and for transactional emails (backend templates — references/emails.md)."
|
|
5
|
+
forget_when: "The language is switched or the locale file is written, and the admin renders in it."
|
|
6
|
+
carry_forward:
|
|
7
|
+
- "Admin language = one import line in src/commerce/admin/i18n/index.js; no runtime picker, no npm package."
|
|
8
|
+
- "New locale file = copy of locales/en.js with every value translated and {placeholders} kept verbatim; missing keys fall back to English."
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Admin localization
|
|
12
|
+
|
|
13
|
+
> **You probably don't need this file.** English is the default — nothing to
|
|
14
|
+
> do. German, Spanish, French, Japanese and Portuguese ship complete: repoint
|
|
15
|
+
> one import in `src/commerce/admin/i18n/index.js` and you are done. This file
|
|
16
|
+
> is for a **seventh** language, or for adding new admin text. In no case do
|
|
17
|
+
> you read or rewrite the admin's screens — the language never lives there.
|
|
18
|
+
|
|
19
|
+
The Store Management UI (`src/commerce/admin/`) renders every user-visible
|
|
20
|
+
string through `t()` from `src/commerce/admin/i18n/index.js`. The wording
|
|
21
|
+
lives in flat message catalogs at `src/commerce/admin/i18n/locales/<lang>.js`.
|
|
22
|
+
Shipped complete: `en` (default + fallback), `de`, `es`, `fr`, `ja`, `pt`.
|
|
23
|
+
|
|
24
|
+
It is all plain JavaScript inside the plugin — **no i18n npm package, nothing
|
|
25
|
+
to install**. Dates (`lib/format.js`), money (`hooks/useMoney.js`) and
|
|
26
|
+
country/currency display names (`Intl.DisplayNames`) format from the same
|
|
27
|
+
locale automatically; `lib/geo-data.js` keeps English state/province names
|
|
28
|
+
(proper nouns, mirrored from backend data) — do not translate that file.
|
|
29
|
+
|
|
30
|
+
## Scope — what this does and does not cover
|
|
31
|
+
|
|
32
|
+
- **Covers:** every admin screen — navigation, tables, editors, settings,
|
|
33
|
+
dialogs, toasts, the first-run setup screen, enum labels
|
|
34
|
+
(`lib/constants.js` reads its labels from the catalog).
|
|
35
|
+
- **Does not cover:** the storefront (headless — it ships no copy; write your
|
|
36
|
+
storefront's copy directly in the store's language) and transactional
|
|
37
|
+
emails (backend templates — `references/emails.md`). The StoreAdmin bot's
|
|
38
|
+
UI chrome is localized; the bot's *answers* come from the agent and follow
|
|
39
|
+
the language the admin types in.
|
|
40
|
+
|
|
41
|
+
## Switch the admin to a shipped language
|
|
42
|
+
|
|
43
|
+
Edit **one line** in `src/commerce/admin/i18n/index.js` — repoint the
|
|
44
|
+
`active` import:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import en from "./locales/en.js";
|
|
48
|
+
import active from "./locales/de.js"; // was "./locales/en.js" — admin now renders in German
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Leave the `en` import alone: it is the fallback for any key a locale misses.
|
|
52
|
+
There is deliberately no language picker in the UI — a store's back office
|
|
53
|
+
runs in one language, chosen in code. If the plugin is ever re-copied by an
|
|
54
|
+
update, re-apply this one-line edit.
|
|
55
|
+
|
|
56
|
+
## Add a language that is not shipped
|
|
57
|
+
|
|
58
|
+
1. Copy `src/commerce/admin/i18n/locales/en.js` to
|
|
59
|
+
`src/commerce/admin/i18n/locales/<code>.js` (e.g. `it.js`, `nl.js`,
|
|
60
|
+
`pt-BR.js`).
|
|
61
|
+
2. Set its `$locale` to the language's BCP-47 tag (e.g. `"it"`, `"pt-BR"`) —
|
|
62
|
+
this drives `Intl` date/number/name formatting, not just messages.
|
|
63
|
+
3. Translate **every value**; never change a key. Keep `{placeholders}`
|
|
64
|
+
verbatim — they are interpolated by name (`{count}`, `{email}`, `{page}`).
|
|
65
|
+
Keep the tone consistent with the shipped files (formal register: German
|
|
66
|
+
"Sie", Japanese です/ます).
|
|
67
|
+
4. Point the `active` import in `i18n/index.js` at the new file (step above).
|
|
68
|
+
|
|
69
|
+
A partial file is safe: any key the file misses renders in English rather
|
|
70
|
+
than breaking, so translate-verify-iterate works. Your new locale file is not
|
|
71
|
+
part of the published package, so plugin updates won't overwrite it — only
|
|
72
|
+
the one-line import needs re-applying.
|
|
73
|
+
|
|
74
|
+
## Adding new admin UI text
|
|
75
|
+
|
|
76
|
+
Never hard-code a user-visible string in an admin component. Render it as
|
|
77
|
+
`t("area.slug")` (import `{ t }` from the admin's `i18n/`, relative path) and
|
|
78
|
+
add the key to `locales/en.js`, namespaced by admin area
|
|
79
|
+
(`orders.`, `products.`, `settings.`, … — shared vocabulary lives under
|
|
80
|
+
`common.`). Then add the same key to every shipped locale file; if you skip
|
|
81
|
+
one, that language shows the English text for that key. Interpolation:
|
|
82
|
+
`t("orders.n_deleted", { count })` with catalog value `"{count} order(s)
|
|
83
|
+
deleted"`.
|
|
84
|
+
|
|
85
|
+
Keep the locale files structurally identical — same keys in every file. A
|
|
86
|
+
quick parity check:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
node --input-type=module -e '
|
|
90
|
+
import { readdirSync } from "node:fs";
|
|
91
|
+
const dir = new URL(`file://${process.cwd()}/src/commerce/admin/i18n/locales/`);
|
|
92
|
+
const keys = async (f) => Object.keys((await import(new URL(f, dir))).default).sort().join("\n");
|
|
93
|
+
const base = await keys("en.js");
|
|
94
|
+
for (const f of readdirSync(dir)) console.log(f, (await keys(f)) === base ? "ok" : "KEY MISMATCH");'
|
|
95
|
+
```
|
|
@@ -93,7 +93,7 @@ Both lists are inventories of what the data supports — **not a layout and not
|
|
|
93
93
|
|
|
94
94
|
**Card:** image, name, `price.label`, a sale badge from `on_sale`, stars from `average_rating`/`rating_count`, out-of-stock from `stock_status`, one or two ribbons — plus anything else on the row (`weight`, `dimensions`, `productSpecs`). Link the whole card through; the layout is yours.
|
|
95
95
|
|
|
96
|
-
**Product page:** gallery, name, price, variant selectors, stock, `short_description` then `description`, SKU, categories breadcrumb, ribbons, reviews, upsells/cross-sells. All but the markup is handed to you: `productImages(product)` + your own active index, `variantAxes(view, pick)`, `useAddToCart`, `productSpecs(product)`, `p.reviews` (+ `
|
|
96
|
+
**Product page:** gallery, name, price, variant selectors, stock, `short_description` then `description`, SKU, categories breadcrumb, ribbons, reviews, upsells/cross-sells. All but the markup is handed to you: `productImages(product)` + your own active index, `variantAxes(view, pick)`, `useAddToCart`, `productSpecs(product)`, `p.reviews` (+ `useStorefront().submitReview`), and `p.upsells`/`p.crossSells` — added with `useCart().addItem`, matching "already in cart" by product id.
|
|
97
97
|
|
|
98
98
|
**Attributes and modifiers are individually designable.** `productSpecs(product)` rows are `{key, label, titleLabel, value}` plus an inferred `type` — `"numeric"` / `"duration"` (with `number` and `unit` split out), `"location"`, `"list"` (with `items`), `"text"` — so the rendering branch is already made for you: a weight can be a figure, a composition bars, a provenance a place. ⚑ **Never `.map()` the whole list into one grey label/value table**; design the two or three rows this catalog is judged on and let the rest fall through to a plain row. ⚑ **Look a row up with `findSpec(rows, "care")`**, which ignores case, spaces, `_` and `-`: meta keys are free text typed per product (`care`, `Care`, `Care Instructions`), so an equality test on `label` — or on one spelling of `key` — silently never fires and the feature renders its fallback forever; `titleLabel` is the display-cased form, for a heading. What a row *looks* like is a per-catalog design decision — not one uniform chip row per axis and one grey table ([`../install/02-storefront.md`](../install/02-storefront.md)); §5's rules govern selector *behaviour*, never its form.
|
|
99
99
|
|
|
@@ -5,6 +5,7 @@ skip_when: "The product page already renders `p.reviews` and submits through `su
|
|
|
5
5
|
forget_when: "A review submits, appears (or is held) as the store's auto-approve setting dictates, and the aggregate rating renders."
|
|
6
6
|
carry_forward:
|
|
7
7
|
- "Reviews are part of the happy path: the list arrives with the product, submitting is one client call. The confirmation copy must come from the submit response's `status`, never hardcoded."
|
|
8
|
+
- "The review form renders for every visitor by default — guests included, with an email field. Login-gating is an explicit store request, implemented as one conditional around the form."
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
# Reviews
|
|
@@ -16,7 +17,7 @@ Reviews are **part of the happy path**, not an extra: the backend always shipped
|
|
|
16
17
|
Both live on the storefront client in `@/commerce/utils` — in React, `useStorefront()` is that client:
|
|
17
18
|
|
|
18
19
|
- **`getProductReviews(slugOrRef, { page, per_page })`** → `{ items, page, per_page, has_next, average_rating, rating_count }`. The same reviews `get-product` returns — page or refresh the list without re-fetching the page; `useProduct(slug, { reviewsPerPage })` sizes the first one.
|
|
19
|
-
- **`submitReview({ product_id, review
|
|
20
|
+
- **`useStorefront().submitReview({ product_id, review?, rating?, reviewer?, email? })`** → `{ review_id, status, verified }`. ⚑ **A client method, never a barrel export** — importing the name itself from `@/commerce/storefront` throws `does not provide an export named …` and the whole app renders blank. **Text or stars — at least one**: `review` is the body text, `rating` is **1–5 stars** (0 counts as unrated); stars-only and text-only are both valid, only both missing rejects. `reviewer` is the display name. ⚑ **One set of names, both directions** — the payload above, rows back as `{ id, reviewer, review, rating, verified, created_date }`. Not the entity's columns: a near-miss (`content`, `reviewer_name`, `reviewer_email`, …) throws client-side rather than being aliased, since the same wrong names would render the list blank too. It **rejects** with `email_required` | `review_incomplete` (neither text nor stars) | `invalid_rating` | `not_found` — catch it, read `storefrontErrorCode(e)`, and land each code on its own field, so a failed submit says what to fix instead of resolving into nothing. After an approved submission, refresh the list yourself so the review actually appears.
|
|
20
21
|
|
|
21
22
|
## What ships
|
|
22
23
|
|
|
@@ -25,17 +26,17 @@ Both live on the storefront client in `@/commerce/utils` — in React, `useStore
|
|
|
25
26
|
- `storefront-account` `my-reviews` lists a signed-in customer's own — what a "My reviews" account tab renders from;
|
|
26
27
|
- moderation is in the admin (Products → Reviews); `commerce/admin-reviews` recalculates the product's rating on every status change.
|
|
27
28
|
|
|
28
|
-
## The policy is the store's
|
|
29
|
+
## The policy is the store's — and open is the default
|
|
29
30
|
|
|
30
|
-
Which visitors may submit is a gate **you** render, in your own words. The
|
|
31
|
+
Which visitors may submit is a gate **you** render, in your own words — and **the default is no gate at all**. Build the form for every visitor, signed in or not (that is the server's own rule); a review form a guest cannot use is a policy the store has to ask for, never something to add on your own initiative. The two stricter patterns are each one small change away when the store does ask:
|
|
31
32
|
|
|
32
33
|
| Policy | Who may submit | How you implement it |
|
|
33
34
|
|---|---|---|
|
|
34
|
-
| **Open
|
|
35
|
-
| **Login-gated** | a signed-in visitor only |
|
|
35
|
+
| **Open — the default** | anyone with a valid email | render the form for everyone; the email field is required for guests, hidden when signed in |
|
|
36
|
+
| **Login-gated** | a signed-in visitor only | one conditional around the same form: render it when your app has a user, otherwise your "sign in to review" line |
|
|
36
37
|
| **Verified buyers** | someone whose own orders include a `processing`/`completed` order for this product | check `storefront-account` `my-orders` for the product, gate on the result |
|
|
37
38
|
|
|
38
|
-
Policies are **UI-side by design**: the server accepts any valid email, so a stricter rule is exactly this gate — and a policy that must hold against handcrafted API calls too belongs in a backend function of your own wrapping `submit-review`. Either way: hide the email field for a signed-in visitor (the session's email wins server-side)
|
|
39
|
+
Policies are **UI-side by design**: the server accepts any valid email, so a stricter rule is exactly this gate — and a policy that must hold against handcrafted API calls too belongs in a backend function of your own wrapping `submit-review`. Either way: hide the email field for a signed-in visitor (the session's email wins server-side). The server needs only one of text/stars — if your form makes either (or both) mandatory, validate before you call, so the customer meets your words rather than a raw 400. An honest middle ground for most stores: accept everything and render the `verified` flag as a "Verified purchase" badge.
|
|
39
40
|
|
|
40
41
|
## Auto-approval and moderation
|
|
41
42
|
|
|
@@ -25,20 +25,12 @@ Tailwind + shadcn/ui + React Router) to get a full store back office.
|
|
|
25
25
|
npx npq install <only the missing names> # npq audits the package before npm installs it
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
4. Mount
|
|
29
|
-
Base44 discovers an app's pages from, so the screens that should be listed as
|
|
30
|
-
pages are declared there literally, and the rest go to `<AdminRoutes />` on a
|
|
31
|
-
splat. The full step is the skill's `install/01-install.md`.
|
|
28
|
+
4. Mount the app in your router:
|
|
32
29
|
|
|
33
30
|
```jsx
|
|
34
|
-
import AdminApp
|
|
35
|
-
|
|
36
|
-
<Route path="/store-admin" element={<AdminApp />}
|
|
37
|
-
<Route index element={<Dashboard />} />
|
|
38
|
-
<Route path="orders" element={<OrdersList />} />
|
|
39
|
-
{/* …products, customers, coupons, reports… */}
|
|
40
|
-
<Route path="*" element={<AdminRoutes />} />
|
|
41
|
-
</Route>
|
|
31
|
+
import AdminApp from "@/commerce/admin";
|
|
32
|
+
|
|
33
|
+
<Route path="/store-admin/*" element={<AdminApp />} />
|
|
42
34
|
// mounted elsewhere? → <AdminApp basePath="/backoffice" />
|
|
43
35
|
```
|
|
44
36
|
|
|
@@ -83,17 +75,47 @@ npx shadcn@latest add <component>
|
|
|
83
75
|
## Layout of this folder
|
|
84
76
|
|
|
85
77
|
```
|
|
86
|
-
index.jsx AdminApp: providers → auth guard → layout →
|
|
87
|
-
routes.jsx Route table + <AdminRoutes/>
|
|
78
|
+
index.jsx AdminApp: providers → auth guard → layout → routes
|
|
79
|
+
routes.jsx Route table + <AdminRoutes/>
|
|
88
80
|
layout/ AdminLayout, Sidebar, Topbar, AuthGuard (admin-role gate), AccessDenied
|
|
89
81
|
bot/ StoreAdminBot (chat panel over the commerce/StoreAdmin agent), Markdown (GFM renderer)
|
|
90
82
|
context/ SettingsContext (store settings + first-run seeding), BasePathContext
|
|
91
83
|
hooks/ useAsync, usePagedList, useRealtime (live updates), useMoney, useDebounce
|
|
84
|
+
i18n/ t() + locale; locales/ = en (default), de, es, fr, ja, pt
|
|
92
85
|
lib/ api (function calls), constants, format, geo-data, order/product utils
|
|
93
86
|
components/ DataTable, SearchSelect, MoneyInput, DateRangePicker, AddressForm, …
|
|
94
87
|
pages/ All admin pages (orders, products, coupons, customers, reports, settings, webhooks)
|
|
95
88
|
```
|
|
96
89
|
|
|
90
|
+
## Localization
|
|
91
|
+
|
|
92
|
+
Every user-visible string renders through `t()` from `i18n/`; the wording
|
|
93
|
+
lives in `i18n/locales/<lang>.js`. English (`en`) is the default and the
|
|
94
|
+
fallback; `de`, `es`, `fr`, `ja` and `pt` ship complete. Dates, money and
|
|
95
|
+
country/currency names follow the same locale via the browser's `Intl` —
|
|
96
|
+
no i18n package is involved anywhere.
|
|
97
|
+
|
|
98
|
+
**Switch language** — edit one line in `i18n/index.js`:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import active from "./locales/en.js"; // ← point at "./locales/de.js" for German
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
There is deliberately no runtime picker: a store's back office runs in one
|
|
105
|
+
language. (Re-apply this one-line edit if a plugin update re-copies this
|
|
106
|
+
folder.)
|
|
107
|
+
|
|
108
|
+
**Add a language** — copy `i18n/locales/en.js` to `i18n/locales/<code>.js`,
|
|
109
|
+
set its `$locale` to the BCP-47 tag, translate every value keeping
|
|
110
|
+
`{placeholders}` verbatim, then point the `active` import at the new file.
|
|
111
|
+
Missing keys fall back to English, so a partial file renders mixed rather
|
|
112
|
+
than broken.
|
|
113
|
+
|
|
114
|
+
**Adding admin UI text?** Render it as `t("area.slug")`, add the key to
|
|
115
|
+
`i18n/locales/en.js`, and translate it in each shipped locale (or leave it —
|
|
116
|
+
it falls back to English). Full recipe for agents: the commerce skill's
|
|
117
|
+
`references/admin-localization.md`.
|
|
118
|
+
|
|
97
119
|
## Live-updating views
|
|
98
120
|
|
|
99
121
|
The dashboard, the orders list and the reports tabs refresh themselves — a new
|