@feeef.dev/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -0
- package/README.md +94 -0
- package/dist/bin.js +2752 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2730 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
- package/scaffolds/blank/.cursor/rules/feeef-theme.mdc +34 -0
- package/scaffolds/blank/.cursor/rules/filterator.mdc +13 -0
- package/scaffolds/blank/.cursor/rules/order-form.mdc +17 -0
- package/scaffolds/blank/.cursor/rules/theme-source.mdc +32 -0
- package/scaffolds/blank/.cursor/skills/feeef-filterator/SKILL.md +15 -0
- package/scaffolds/blank/.cursor/skills/feeef-theme/SKILL.md +56 -0
- package/scaffolds/blank/.cursor/skills/feeef-theme/reference.md +73 -0
- package/scaffolds/blank/.vscode/extensions.json +3 -0
- package/scaffolds/blank/.vscode/settings.json +13 -0
- package/scaffolds/blank/AGENTS.md +104 -0
- package/scaffolds/blank/README.md +21 -0
- package/scaffolds/blank/docs/00-INDEX.md +33 -0
- package/scaffolds/blank/docs/03-CUSTOM-COMPONENTS.md +116 -0
- package/scaffolds/blank/docs/04-WORKFLOW.md +129 -0
- package/scaffolds/blank/docs/05-ANTI-PATTERNS.md +80 -0
- package/scaffolds/blank/docs/07-SHARED-COMPONENTS.md +144 -0
- package/scaffolds/blank/docs/08-ORDER-FORM.md +376 -0
- package/scaffolds/blank/docs/09-THEME-PORTING.md +211 -0
- package/scaffolds/blank/docs/10-SCHEMA-LIBRARY.md +127 -0
- package/scaffolds/blank/docs/11-PAGES-CHECKOUT-THANKS-PRODUCT.md +81 -0
- package/scaffolds/blank/docs/12-DARK-LIGHT-THEME.md +164 -0
- package/scaffolds/blank/docs/13-TEMPLATE-I18N.md +320 -0
- package/scaffolds/blank/docs/14-FILTERATOR.md +433 -0
- package/scaffolds/blank/docs/16-AUTHORING-TS-IMPORTS.md +73 -0
- package/scaffolds/blank/feeef.template.json +7 -0
- package/scaffolds/blank/locales/README.md +4 -0
- package/scaffolds/blank/locales/ar.json +10 -0
- package/scaffolds/blank/locales/en.json +10 -0
- package/scaffolds/blank/package-lock.json +1975 -0
- package/scaffolds/blank/package.json +23 -0
- package/scaffolds/blank/pages/home/components/hero.tsx +34 -0
- package/scaffolds/blank/pages/home/page.json +3 -0
- package/scaffolds/blank/props.json +6 -0
- package/scaffolds/blank/schema.ts +49 -0
- package/scaffolds/blank/tsconfig.json +24 -0
- package/scaffolds/blank/types/feeef-live-scope.d.ts +162 -0
- package/scaffolds/blank/types/feeef-template-schema.d.ts +84 -0
- package/vendor/template-kit/README.md +64 -0
- package/vendor/template-kit/bin/dev.mjs +61 -0
- package/vendor/template-kit/bin/feeef-template.mjs +212 -0
- package/vendor/template-kit/data/lithium-schema.json +4733 -0
- package/vendor/template-kit/lib/build.mjs +377 -0
- package/vendor/template-kit/lib/compile-component.mjs +188 -0
- package/vendor/template-kit/lib/component-meta.mjs +481 -0
- package/vendor/template-kit/lib/library.mjs +168 -0
- package/vendor/template-kit/lib/locales.mjs +68 -0
- package/vendor/template-kit/lib/paths.mjs +84 -0
- package/vendor/template-kit/lib/shared.mjs +268 -0
- package/vendor/template-kit/lib/theme-schema.mjs +300 -0
- package/vendor/template-kit/lib/unpack.mjs +324 -0
- package/vendor/template-kit/lib/validate.mjs +207 -0
- package/vendor/template-kit/schemas/component.schema.json +55 -0
- package/vendor/template-kit/schemas/template.schema.json +18 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# Filterator & Product / Category Lists
|
|
2
|
+
|
|
3
|
+
> Theme package copy — paths assume this theme folder is the project root.
|
|
4
|
+
|
|
5
|
+
Canonical contract for **structured queries** and **list APIs** so AI can build creative PLPs, related products, search, carousels, and admin-grade filters.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Mental model
|
|
10
|
+
|
|
11
|
+
Two ways to query products:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
A) Flat query params → ProductFilter methods (q, category_id, price_min, in_stock, …)
|
|
15
|
+
B) Filterator JSON → nested AND/OR tree + ordering + paging (?filterator=<json>)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Categories use **flat params only** (no filterator today).
|
|
19
|
+
|
|
20
|
+
In **custom / Dawn JSX** (react-live):
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
const ff = useFeeef();
|
|
24
|
+
const storeCtx = useStore();
|
|
25
|
+
const store = storeCtx?.store;
|
|
26
|
+
if (!ff || !store) return null;
|
|
27
|
+
|
|
28
|
+
const res = await ff.products.list({
|
|
29
|
+
params: {
|
|
30
|
+
store_id: store.id,
|
|
31
|
+
limit: 24,
|
|
32
|
+
page: 1,
|
|
33
|
+
// flat filters and/or:
|
|
34
|
+
filterator: JSON.stringify({ filtering: { … }, ordering: […], paging: {…} }),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const products = res?.data || res || [];
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Always pass **`store_id`**. Prefer flat params for simple UIs; use **filterator** for nested OR/AND, `inList`, null checks, multi-field sorts.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Filterator query shape
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
type ProductFilteratorQuery = {
|
|
48
|
+
filtering?: {
|
|
49
|
+
condition: 'and' | 'or'
|
|
50
|
+
filters?: FilteratorFilter[]
|
|
51
|
+
groups?: FilteratorFilteringGroup[] // nested trees
|
|
52
|
+
}
|
|
53
|
+
ordering?: Array<{ field: string; dir: 'asc' | 'desc' }>
|
|
54
|
+
paging?: { limit?: number; offset?: number; cursor?: string }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type FilteratorFilter = {
|
|
58
|
+
field: string // camelCase → snake_case DB column
|
|
59
|
+
operation: QueryOperation
|
|
60
|
+
value?: unknown // single-value ops
|
|
61
|
+
values?: unknown[] // inList / notIn
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Send as query param:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
GET /products?store_id=…&filterator=<URL-encoded JSON string>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Via SDK:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
ff.products.list({
|
|
75
|
+
params: {
|
|
76
|
+
store_id: store.id,
|
|
77
|
+
filterator: JSON.stringify(query),
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Operators (prefer long names in AI output)
|
|
83
|
+
|
|
84
|
+
| Operation | Aliases | SQL | Notes |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| `equals` | `eq` | `=` | Prefer for exact match |
|
|
87
|
+
| `notEquals` | `neq` | `!=` | |
|
|
88
|
+
| `contains` | — | `LIKE %v%` | Text |
|
|
89
|
+
| `startsWith` | — | `LIKE v%` | |
|
|
90
|
+
| `endsWith` | — | `LIKE %v` | |
|
|
91
|
+
| `greaterThan` | `gt` | `>` | Numbers / dates |
|
|
92
|
+
| `greaterOrEqual` / `greaterThanOrEqual` | `gte` | `>=` | |
|
|
93
|
+
| `lessThan` | `lt` | `<` | |
|
|
94
|
+
| `lessOrEqual` / `lessThanOrEqual` | `lte` | `<=` | |
|
|
95
|
+
| `inList` | `in` | `IN (…)`, use **`values`** | |
|
|
96
|
+
| `notIn` | — | `NOT IN (…)`, use **`values`** | |
|
|
97
|
+
| `isNull` | — | `IS NULL` | No value needed |
|
|
98
|
+
| `isNotNull` | — | `IS NOT NULL` | |
|
|
99
|
+
|
|
100
|
+
**Products filterator** (`ProductFilter.filterator`) accepts short + long aliases.
|
|
101
|
+
**Generic `filterator_apply.ts`** prefers **long** names — when in doubt use `equals`, `greaterThan`, `inList`.
|
|
102
|
+
|
|
103
|
+
### Field mapping
|
|
104
|
+
|
|
105
|
+
- `categoryId` → `category_id`, `createdAt` → `created_at`, etc. (`string.snakeCase`)
|
|
106
|
+
- Exception: `type` / `productType` → column **`type`** (not `product_type`)
|
|
107
|
+
|
|
108
|
+
Useful product fields: `id`, `slug`, `name`, `sku`, `price`, `cost`, `discount`, `stock`, `sold`, `views`, `likes`, `status`, `type`, `categoryId`, `storeId`, `createdAt`, `updatedAt`, `photoUrl`, …
|
|
109
|
+
|
|
110
|
+
### Nested AND / OR
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"filtering": {
|
|
115
|
+
"condition": "or",
|
|
116
|
+
"filters": [
|
|
117
|
+
{ "field": "name", "operation": "contains", "value": "shirt" }
|
|
118
|
+
],
|
|
119
|
+
"groups": [
|
|
120
|
+
{
|
|
121
|
+
"condition": "and",
|
|
122
|
+
"filters": [
|
|
123
|
+
{ "field": "price", "operation": "greaterThan", "value": 1000 },
|
|
124
|
+
{ "field": "sold", "operation": "greaterThan", "value": 10 }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"ordering": [{ "field": "sold", "dir": "desc" }],
|
|
130
|
+
"paging": { "limit": 24, "offset": 0 }
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Meaning: *(name contains shirt) **OR** (price > 1000 **AND** sold > 10)*, sorted by sold desc.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 3. Products — flat query params
|
|
139
|
+
|
|
140
|
+
`GET /products` — implemented in `ProductFilter`. camelCase / snake_case aliases exist for most.
|
|
141
|
+
|
|
142
|
+
| Param | Also | Meaning |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `store_id` | `storeId`, `store_ids` | **Required** for storefront |
|
|
145
|
+
| `q` | `search` | Search name, slug, id, description, sku |
|
|
146
|
+
| `status` / `statuses` | | `published` / `draft` / `archived` / `deleted`; `!published` excludes |
|
|
147
|
+
| `category_id` | `categoryId` | Category id (preferred) |
|
|
148
|
+
| `category` | | id **or** embedded slug (compat) |
|
|
149
|
+
| `category_slug` | `categorySlug` | Embedded slug only |
|
|
150
|
+
| `price_min` / `price_max` | `priceMin` / `priceMax` | Price range |
|
|
151
|
+
| `stock_min` / `stock_max` | | Stock range |
|
|
152
|
+
| `in_stock` | `inStock` | `true` → stock > 0; `!true` → out |
|
|
153
|
+
| `has_media` / `has_photo` | | Media / cover present |
|
|
154
|
+
| `variant` | `variantPresent` | Has variants |
|
|
155
|
+
| `sku` / `barcode` | | Partial match |
|
|
156
|
+
| `sold_min` | | Min sold (or `true` → sold > 0) |
|
|
157
|
+
| `created_after` / `before` | | ISO dates |
|
|
158
|
+
| `updated_after` / `before` | | |
|
|
159
|
+
| `ids` | `products` | Whitelist ids |
|
|
160
|
+
| `published` | | Exclude archived |
|
|
161
|
+
| `random` | | `ORDER BY RANDOM()` (+ media) |
|
|
162
|
+
| `order_by` | `orderBy` | `field:dir` e.g. `price:desc` |
|
|
163
|
+
| `page` / `limit` | | Pagination (limit often default 20) |
|
|
164
|
+
| `filterator` | | JSON string — §2 |
|
|
165
|
+
|
|
166
|
+
### SDK helpers (`feeef.js`)
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
ff.products.list({
|
|
170
|
+
storeId, categoryId, status, q,
|
|
171
|
+
minPrice, maxPrice, inStock, // maps to min_price / max_price — prefer params.price_min for backend certainty
|
|
172
|
+
sortBy: 'price' | 'createdAt' | 'sold' | 'views',
|
|
173
|
+
sortOrder: 'asc' | 'desc',
|
|
174
|
+
page, limit,
|
|
175
|
+
params: { /* escape hatch — price_min, filterator, … */ },
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
ff.products.random(12)
|
|
179
|
+
ff.products.find({ id: slug, by: 'slug' })
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Gotcha:** SDK `minPrice` → `min_price`, but Lucid filter methods are `price_min` / `priceMax`. In custom JSX prefer:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
params: { store_id, price_min: 1000, price_max: 5000, in_stock: true }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Response
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
{ data: ProductEntity[], meta?: { total, currentPage, perPage, … } }
|
|
192
|
+
// or ProductEntity[] depending on client path — always normalize:
|
|
193
|
+
const list = Array.isArray(res) ? res : (res?.data ?? [])
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### ProductEntity (UI-critical)
|
|
197
|
+
|
|
198
|
+
| Field | Type | Notes |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `id` / `slug` | string | Links: `/products/${slug \|\| id}` |
|
|
201
|
+
| `name` / `title` | string\|null | |
|
|
202
|
+
| `photoUrl` | string\|null | Cover |
|
|
203
|
+
| `media` | **`string[]`** | URL strings — **not** `{url}` objects |
|
|
204
|
+
| `price` / `discount` / `stock` / `sold` | number | |
|
|
205
|
+
| `categoryId` / `category` | | |
|
|
206
|
+
| `offers` / `variant` / `addons` | | Upsell / COD |
|
|
207
|
+
| `status` | | Guests usually only see published |
|
|
208
|
+
| `body` | | Markdown only |
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 4. Categories — list request
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
GET /categories?store_id=…&parent_id=…&page&limit
|
|
216
|
+
GET /categories/tree?store_id=…
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| Param | Meaning |
|
|
220
|
+
|---|---|
|
|
221
|
+
| `store_id` | **Required** |
|
|
222
|
+
| `parent_id` | Parent id; `null` / `'null'` → roots |
|
|
223
|
+
| `q` | Search (SDK) |
|
|
224
|
+
| `page` / `limit` | Pagination (default limit often 50) |
|
|
225
|
+
|
|
226
|
+
**No `filterator` on categories.**
|
|
227
|
+
|
|
228
|
+
SDK:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
ff.categories.list({ storeId: store.id, parentId: null, limit: 50 })
|
|
232
|
+
ff.categories.listRootCategories(store.id)
|
|
233
|
+
ff.categories.listChildren(store.id, parentId)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Entity: `id`, `name`, `photoUrl`, `parentId`, `metadata`, children/parent when tree/show.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 5. Creative recipes (AI playbook)
|
|
241
|
+
|
|
242
|
+
### A. Products page (PLP) with URL filters
|
|
243
|
+
|
|
244
|
+
```jsx
|
|
245
|
+
const searchParams = useSearchParams();
|
|
246
|
+
const categoryId = searchParams?.get('category_id') || '';
|
|
247
|
+
const q = searchParams?.get('q') || '';
|
|
248
|
+
const page = Number(searchParams?.get('page') || 1);
|
|
249
|
+
|
|
250
|
+
ff.products.list({
|
|
251
|
+
params: {
|
|
252
|
+
store_id: store.id,
|
|
253
|
+
limit: 24,
|
|
254
|
+
page,
|
|
255
|
+
category_id: categoryId || undefined,
|
|
256
|
+
q: q || undefined,
|
|
257
|
+
search: q || undefined, // dual-send for compat
|
|
258
|
+
in_stock: true, // server-side availability
|
|
259
|
+
price_min: priceFrom || undefined,
|
|
260
|
+
price_max: priceTo || undefined,
|
|
261
|
+
order_by: 'sold:desc',
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Prefer **server** `in_stock` / `price_min` / `price_max` over filtering the array in memory (Dawn grid historically did client-side — avoid for large catalogs).
|
|
267
|
+
|
|
268
|
+
### B. Related products (same category)
|
|
269
|
+
|
|
270
|
+
There is **no** `/related` endpoint. Pattern:
|
|
271
|
+
|
|
272
|
+
```jsx
|
|
273
|
+
const { product } = useCurrentProduct();
|
|
274
|
+
const cat = product?.categoryId;
|
|
275
|
+
const res = await ff.products.list({
|
|
276
|
+
params: {
|
|
277
|
+
store_id: store.id,
|
|
278
|
+
category_id: cat,
|
|
279
|
+
limit: 8,
|
|
280
|
+
in_stock: true,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
const related = (res?.data || []).filter((p) => p.id !== product.id);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Fallbacks: `ff.products.random(8)` or bestsellers `order_by: 'sold:desc'`.
|
|
287
|
+
|
|
288
|
+
### C. “Bestsellers under 5000”
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
filterator: JSON.stringify({
|
|
292
|
+
filtering: {
|
|
293
|
+
condition: 'and',
|
|
294
|
+
filters: [
|
|
295
|
+
{ field: 'price', operation: 'lessOrEqual', value: 5000 },
|
|
296
|
+
{ field: 'stock', operation: 'greaterThan', value: 0 },
|
|
297
|
+
{ field: 'status', operation: 'equals', value: 'published' },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
ordering: [{ field: 'sold', dir: 'desc' }],
|
|
301
|
+
paging: { limit: 12 },
|
|
302
|
+
})
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### D. Multi-category spotlight (`inList`)
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
{
|
|
309
|
+
field: 'categoryId',
|
|
310
|
+
operation: 'inList',
|
|
311
|
+
values: [idA, idB, idC],
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### E. New arrivals with media
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
params: {
|
|
319
|
+
store_id: store.id,
|
|
320
|
+
has_media: true,
|
|
321
|
+
order_by: 'created_at:desc',
|
|
322
|
+
limit: 12,
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### F. Category nav + product grid
|
|
327
|
+
|
|
328
|
+
1. `ff.categories.listRootCategories(store.id)` → chips
|
|
329
|
+
2. On select → set `?category_id=` via `router.push` / `useSearchParams`
|
|
330
|
+
3. Reload products with `category_id`
|
|
331
|
+
|
|
332
|
+
### G. Search as you type
|
|
333
|
+
|
|
334
|
+
Debounce `q` (≥300ms), keep `limit` modest (12–24), show loading skeleton. Send both `q` and `search`.
|
|
335
|
+
|
|
336
|
+
### H. Infinite scroll
|
|
337
|
+
|
|
338
|
+
Use `page` + `meta.lastPage` / `meta.total`, or TanStack `useInfiniteProductsQuery` in native TSX. In react-live, track `page` in `React.useState` and append `data`.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 6. Custom JSX rules
|
|
343
|
+
|
|
344
|
+
| Do | Don’t |
|
|
345
|
+
|---|---|
|
|
346
|
+
| Null-check `ff` / `store` | Assume hooks always return data |
|
|
347
|
+
| `store_id: store.id` on every list | Omit store scope |
|
|
348
|
+
| `product.media` as `string[]` | `product.media[0].url` |
|
|
349
|
+
| `RouterNav` / `Link` to `/p/${slug}` | Hardcode `/products/${slug}` for PDP (alias only; prefer `/p/`) |
|
|
350
|
+
| Server filters (`in_stock`, `price_min`, filterator) | Fetch 200 and filter in JS for PLP |
|
|
351
|
+
| `React.useEffect` for fetches + cancel/ignore flag | Fire list on every render without deps |
|
|
352
|
+
|
|
353
|
+
Fetch skeleton:
|
|
354
|
+
|
|
355
|
+
```jsx
|
|
356
|
+
function App() {
|
|
357
|
+
const ff = useFeeef();
|
|
358
|
+
const storeCtx = useStore();
|
|
359
|
+
const store = storeCtx?.store;
|
|
360
|
+
const [items, setItems] = React.useState([]);
|
|
361
|
+
const [loading, setLoading] = React.useState(true);
|
|
362
|
+
|
|
363
|
+
React.useEffect(() => {
|
|
364
|
+
let alive = true;
|
|
365
|
+
if (!ff || !store?.id) { setLoading(false); return; }
|
|
366
|
+
setLoading(true);
|
|
367
|
+
ff.products.list({
|
|
368
|
+
params: { store_id: store.id, limit: 12, in_stock: true, order_by: 'sold:desc' },
|
|
369
|
+
}).then((res) => {
|
|
370
|
+
if (!alive) return;
|
|
371
|
+
setItems(Array.isArray(res) ? res : (res?.data ?? []));
|
|
372
|
+
setLoading(false);
|
|
373
|
+
}).catch(() => { if (alive) setLoading(false); });
|
|
374
|
+
return () => { alive = false; };
|
|
375
|
+
}, [ff, store?.id]);
|
|
376
|
+
|
|
377
|
+
if (loading) return <div>…</div>;
|
|
378
|
+
return (
|
|
379
|
+
<div>
|
|
380
|
+
{items.map((p) => (
|
|
381
|
+
<RouterNav key={p.id} href={'/products/' + (p.slug || p.id)}>
|
|
382
|
+
<img src={p.photoUrl || (p.media && p.media[0]) || ''} alt={p.name || ''} />
|
|
383
|
+
<span>{p.name}</span>
|
|
384
|
+
</RouterNav>
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## 7. Guest visibility & security
|
|
394
|
+
|
|
395
|
+
- Unauthenticated storefront typically **hides archived** products.
|
|
396
|
+
- Never expose owner-only draft tooling in public custom JSX.
|
|
397
|
+
- Filterator is a query language, not auth — still scoped by `store_id` and backend policies.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## 8. Anti-patterns
|
|
402
|
+
|
|
403
|
+
| Anti-pattern | Fix |
|
|
404
|
+
|---|---|
|
|
405
|
+
| Client-only availability/price on full catalog | `in_stock`, `price_min`/`price_max`, or filterator |
|
|
406
|
+
| `min_price` from SDK without verifying | Use `params.price_min` |
|
|
407
|
+
| Related = copy Shopify recommendations API | Same-category list / random / sold |
|
|
408
|
+
| `product.media[i].url` | `product.media[i]` string |
|
|
409
|
+
| Missing `store_id` | Always set |
|
|
410
|
+
| Huge `limit` (500+) for decorative carousels | Cap 8–24; use random/bestsellers |
|
|
411
|
+
| Short ops only on shared apply helper | Prefer long op names |
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## 9. Quick reference — when to use what
|
|
416
|
+
|
|
417
|
+
| Goal | Tool |
|
|
418
|
+
|---|---|
|
|
419
|
+
| Simple category + search PLP | Flat `category_id` + `q` + `page`/`limit` |
|
|
420
|
+
| In stock / price band | `in_stock`, `price_min`, `price_max` |
|
|
421
|
+
| Nested logic / multi-id / null checks | **filterator** |
|
|
422
|
+
| Related on PDP | Same `category_id`, exclude current id |
|
|
423
|
+
| Surprise grid | `random=true` or `ff.products.random(n)` |
|
|
424
|
+
| Category menu | `ff.categories.listRootCategories` / tree |
|
|
425
|
+
| Featured / offers | List then `p.offers?.length` **or** filterator on known fields |
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Related
|
|
430
|
+
|
|
431
|
+
- Scope API: `docs/03-CUSTOM-COMPONENTS.md`
|
|
432
|
+
- Product PDP pages: [11-PAGES-CHECKOUT-THANKS-PRODUCT.md](./11-PAGES-CHECKOUT-THANKS-PRODUCT.md)
|
|
433
|
+
- Theme porting PLP notes: [09-THEME-PORTING.md](./09-THEME-PORTING.md)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Authoring with TypeScript & imports
|
|
2
|
+
|
|
3
|
+
> Theme package copy — paths assume this theme folder is the project root.
|
|
4
|
+
|
|
5
|
+
Theme components can use **TypeScript** and **ESM imports**. At `feeef template build`
|
|
6
|
+
(or `npm run build`), the kit compiles them into a react-live `code` string that still
|
|
7
|
+
exposes top-level `function App()` — the same contract as plain JSX.
|
|
8
|
+
|
|
9
|
+
## What you write
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# Preferred (page-level → sections.main)
|
|
13
|
+
pages/home/components/hero.tsx
|
|
14
|
+
|
|
15
|
+
# Folder when you need colocated helpers or slots/children
|
|
16
|
+
pages/home/components/hero/
|
|
17
|
+
component.tsx
|
|
18
|
+
labels.ts
|
|
19
|
+
|
|
20
|
+
# Legacy multi-section (still supported)
|
|
21
|
+
pages/home/sections/main/components/hero.tsx
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import clsx from "clsx";
|
|
26
|
+
import { bannerLabel } from "./labels";
|
|
27
|
+
|
|
28
|
+
function App(): JSX.Element {
|
|
29
|
+
const text: string = bannerLabel("Feeef");
|
|
30
|
+
return <div className={clsx("hero")}>{text}</div>;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What the storefront receives
|
|
35
|
+
|
|
36
|
+
Build emits bundled / transpiled JS with:
|
|
37
|
+
|
|
38
|
+
- No top-level `import` / `export` (react-live cannot resolve modules)
|
|
39
|
+
- Top-level `function App()` (wrapper around the bundled module when imports exist)
|
|
40
|
+
- `react` / `react-dom` **external** — CustomLive injects `React` in scope
|
|
41
|
+
|
|
42
|
+
Do **not** `import React from "react"` for JSX; use the live scope (or rely on the
|
|
43
|
+
classic `React.createElement` transform from the bundler).
|
|
44
|
+
|
|
45
|
+
## npm dependencies
|
|
46
|
+
|
|
47
|
+
1. Theme folder is a Node package (`package.json` from `feeef template init`).
|
|
48
|
+
2. Install deps at the **theme root**:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd my-theme
|
|
52
|
+
npm install clsx # example
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
3. Import normally in the component file. The compiler walks up to `feeef.template.json`
|
|
56
|
+
and resolves from that theme’s `node_modules`.
|
|
57
|
+
|
|
58
|
+
`react` stays provided by the storefront — add it only if you need types
|
|
59
|
+
(`npm i -D @types/react`), not as a runtime bundle target.
|
|
60
|
+
|
|
61
|
+
## Plain JSX (legacy)
|
|
62
|
+
|
|
63
|
+
`component.jsx` with **no** imports is copied into `code` unchanged (fast path).
|
|
64
|
+
|
|
65
|
+
## Init
|
|
66
|
+
|
|
67
|
+
This package came from `feeef template init`. Re-read `AGENTS.md` after upgrading the CLI.
|
|
68
|
+
|
|
69
|
+
## Do not
|
|
70
|
+
|
|
71
|
+
- Expect browser-side bundling of theme `node_modules` — only build-time inlining
|
|
72
|
+
- Bundle `react` into component `code`
|
|
73
|
+
- Rely on Node builtins or server-only packages inside components
|