create-nymbal-app 0.3.0 → 0.4.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.
Files changed (39) hide show
  1. package/dist/index.js +7 -8
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/scaffold/templates/astro/package.json +25 -26
  5. package/scaffold/templates/astro/src/components/Header.astro +7 -1
  6. package/scaffold/templates/astro/src/components/StockBadge.astro +5 -22
  7. package/scaffold/templates/astro/src/lib/api.ts +18 -4
  8. package/scaffold/templates/astro/src/lib/client.ts +2 -0
  9. package/scaffold/templates/astro/src/pages/category/[slug].astro +26 -7
  10. package/scaffold/templates/astro/src/pages/checkout.astro +12 -4
  11. package/scaffold/templates/astro/src/pages/product/[slug].astro +4 -1
  12. package/scaffold/templates/astro/src/pages/products/index.astro +26 -7
  13. package/scaffold/templates/astro/src/pages/search.astro +11 -5
  14. package/scaffold/templates/astro/src/scripts/cart-page.ts +10 -35
  15. package/scaffold/templates/astro/src/scripts/checkout.ts +48 -49
  16. package/scaffold/templates/astro/src/scripts/pdp.ts +35 -44
  17. package/scaffold/templates/astro/src/scripts/plp.ts +6 -5
  18. package/scaffold/templates/astro/src/scripts/search.ts +32 -8
  19. package/scaffold/templates/astro/src/styles/pages/cart.css +0 -28
  20. package/scaffold/templates/astro/src/styles/pages/pdp.css +6 -2
  21. package/scaffold/templates/astro/src/styles/pages/search.css +19 -0
  22. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +7 -4
  23. package/scaffold/templates/nextjs/app/checkout/page.tsx +9 -2
  24. package/scaffold/templates/nextjs/app/products/page.tsx +7 -7
  25. package/scaffold/templates/nextjs/components/client/CartPage.tsx +1 -0
  26. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +50 -91
  27. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +39 -50
  28. package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +41 -20
  29. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +87 -47
  30. package/scaffold/templates/nextjs/components/server/Header.tsx +9 -4
  31. package/scaffold/templates/nextjs/lib/api.ts +24 -4
  32. package/scaffold/templates/nextjs/lib/client.ts +3 -0
  33. package/scaffold/templates/nextjs/package.json +26 -28
  34. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +6 -2
  35. package/scaffold/templates/nextjs/styles/pages/plp.module.css +3 -2
  36. package/scaffold/templates/nextjs/styles/pages/search.module.css +24 -0
  37. package/scaffold/templates/astro/src/components/SortDropdown.astro +0 -26
  38. package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +0 -35
  39. package/scaffold/templates/nextjs/components/server/StockBadge.tsx +0 -19
@@ -1,6 +1,7 @@
1
+ import { unstable_rethrow } from 'next/navigation'
1
2
  import type { DenormalisedProduct, ProductListResult, ReviewsListResult } from '@nymbalhq/sdk'
2
3
  import { DEFAULT_PRODUCT_SORT, PRODUCT_SORT_KEYS } from '@nymbalhq/types'
3
- import type { Category, Order, ProductSortKey } from '@nymbalhq/types'
4
+ import type { Category, Order, ProductSortKey, StoreCapabilities } from '@nymbalhq/types'
4
5
 
5
6
  const API_URL = process.env.NYMBAL_API_URL ?? 'http://localhost:3001'
6
7
 
@@ -27,10 +28,29 @@ export async function fetchProducts(params?: {
27
28
  }
28
29
  }
29
30
 
30
- /** The sort a listing URL asks for; a missing or unknown `?sort=` is the default. */
31
- export function sortFromSearchParams(params: { sort?: string | string[] }): ProductSortKey {
31
+ /** What the store can do, which decides what the page renders; null when the API did not answer. */
32
+ export async function fetchCapabilities(): Promise<StoreCapabilities | null> {
33
+ try {
34
+ const res = await fetch(`${API_URL}/api/capabilities`, { cache: 'no-store' })
35
+ if (!res.ok) return null
36
+ const envelope = (await res.json()) as { data: StoreCapabilities }
37
+ return envelope.data ?? null
38
+ } catch (err) {
39
+ // Next signals a request-time render by throwing from fetch at build; that must reach Next, not become null.
40
+ unstable_rethrow(err)
41
+ console.error('[api] fetchCapabilities failed', err)
42
+ return null
43
+ }
44
+ }
45
+
46
+ /** The sort a listing URL asks for, among the store's keys; a missing or unknown `?sort=` is the store's default. */
47
+ export function sortFromSearchParams(
48
+ params: { sort?: string | string[] },
49
+ catalog?: StoreCapabilities['catalog'],
50
+ ): ProductSortKey {
32
51
  const sort = Array.isArray(params.sort) ? params.sort[0] : params.sort
33
- return PRODUCT_SORT_KEYS.find((key) => key === sort) ?? DEFAULT_PRODUCT_SORT
52
+ const keys = catalog?.sortKeys ?? PRODUCT_SORT_KEYS
53
+ return keys.find((key) => key === sort) ?? catalog?.defaultSort ?? DEFAULT_PRODUCT_SORT
34
54
  }
35
55
 
36
56
  export async function fetchProduct(slug: string): Promise<DenormalisedProduct | null> {
@@ -1,5 +1,8 @@
1
1
  import { createNymbalClient, type NymbalClient } from '@nymbalhq/sdk'
2
2
 
3
+ // A payment driver of the project's own is registered here, at module load, before any checkout renders:
4
+ // registerPaymentDriver(name, driver) from @nymbalhq/sdk, named by the driver of the payments adapter's client half.
5
+
3
6
  let instance: NymbalClient | null = null
4
7
  let restoring: Promise<void> | null = null
5
8
 
@@ -17,19 +17,17 @@
17
17
  "seed": "nymbal seed"
18
18
  },
19
19
  "dependencies": {
20
- "nymbal": "1.0.0",
21
- "@nymbalhq/config": "1.0.0",
22
- "@nymbalhq/platform": "1.0.0",
23
- "@nymbalhq/stack": "1.0.0",
20
+ "nymbal": "1.1.0",
21
+ "@nymbalhq/config": "1.1.0",
22
+ "@nymbalhq/platform": "1.1.0",
23
+ "@nymbalhq/stack": "1.1.0",
24
24
  "next": "^16.4.2",
25
25
  "react": "^19.3.1",
26
26
  "react-dom": "^19.3.1",
27
- "@nymbalhq/sdk": "1.0.0",
28
- "@nymbalhq/react": "1.0.0",
29
- "@nymbalhq/web-components": "1.0.0",
30
- "@nymbalhq/types": "1.0.0",
31
- "@stripe/stripe-js": "^4.0.0",
32
- "@stripe/react-stripe-js": "^3.0.0"
27
+ "@nymbalhq/sdk": "1.1.0",
28
+ "@nymbalhq/react": "1.1.0",
29
+ "@nymbalhq/web-components": "1.1.0",
30
+ "@nymbalhq/types": "1.1.0"
33
31
  },
34
32
  "devDependencies": {
35
33
  "@types/node": "^22.9.0",
@@ -39,26 +37,26 @@
39
37
  },
40
38
  "pnpm": {
41
39
  "overrides": {
42
- "nymbal": "1.0.0",
43
- "@nymbalhq/config": "1.0.0",
44
- "@nymbalhq/http": "1.0.0",
45
- "@nymbalhq/platform": "1.0.0",
46
- "@nymbalhq/react": "1.0.0",
47
- "@nymbalhq/sdk": "1.0.0",
48
- "@nymbalhq/stack": "1.0.0",
49
- "@nymbalhq/types": "1.0.0",
50
- "@nymbalhq/web-components": "1.0.0"
40
+ "nymbal": "1.1.0",
41
+ "@nymbalhq/config": "1.1.0",
42
+ "@nymbalhq/http": "1.1.0",
43
+ "@nymbalhq/platform": "1.1.0",
44
+ "@nymbalhq/react": "1.1.0",
45
+ "@nymbalhq/sdk": "1.1.0",
46
+ "@nymbalhq/stack": "1.1.0",
47
+ "@nymbalhq/types": "1.1.0",
48
+ "@nymbalhq/web-components": "1.1.0"
51
49
  }
52
50
  },
53
51
  "overrides": {
54
- "nymbal": "1.0.0",
55
- "@nymbalhq/config": "1.0.0",
56
- "@nymbalhq/http": "1.0.0",
57
- "@nymbalhq/platform": "1.0.0",
58
- "@nymbalhq/react": "1.0.0",
59
- "@nymbalhq/sdk": "1.0.0",
60
- "@nymbalhq/stack": "1.0.0",
61
- "@nymbalhq/types": "1.0.0",
62
- "@nymbalhq/web-components": "1.0.0"
52
+ "nymbal": "1.1.0",
53
+ "@nymbalhq/config": "1.1.0",
54
+ "@nymbalhq/http": "1.1.0",
55
+ "@nymbalhq/platform": "1.1.0",
56
+ "@nymbalhq/react": "1.1.0",
57
+ "@nymbalhq/sdk": "1.1.0",
58
+ "@nymbalhq/stack": "1.1.0",
59
+ "@nymbalhq/types": "1.1.0",
60
+ "@nymbalhq/web-components": "1.1.0"
63
61
  }
64
62
  }
@@ -148,12 +148,16 @@
148
148
  margin-bottom: var(--nymbal-spacing-md);
149
149
  }
150
150
 
151
- .inStock {
151
+ .stockBadge[data-level='in'] {
152
152
  background-color: #dcfce7;
153
153
  color: var(--nymbal-color-success);
154
154
  }
155
155
 
156
- .outOfStock {
156
+ .stockBadge[data-level='low'] {
157
+ color: var(--nymbal-color-warning);
158
+ }
159
+
160
+ .stockBadge[data-level='out'] {
157
161
  background-color: #fef2f2;
158
162
  color: var(--nymbal-color-error);
159
163
  }
@@ -118,7 +118,8 @@
118
118
  color: var(--nymbal-color-text-muted);
119
119
  }
120
120
 
121
- .sortSelect {
121
+ /* The sort element renders the select. */
122
+ .sortSelect select {
122
123
  padding: 0.5rem 0.75rem;
123
124
  font-size: var(--nymbal-font-size-sm);
124
125
  border: 1px solid var(--nymbal-color-border);
@@ -129,7 +130,7 @@
129
130
  min-width: 10rem;
130
131
  }
131
132
 
132
- .sortSelect:focus {
133
+ .sortSelect select:focus {
133
134
  outline: none;
134
135
  border-color: var(--nymbal-color-accent);
135
136
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
@@ -143,6 +143,30 @@
143
143
  margin-inline: auto;
144
144
  }
145
145
 
146
+ /* ── Category filter and load more ───────────────────────────────── */
147
+
148
+ .layout {
149
+ display: grid;
150
+ grid-template-columns: 1fr;
151
+ gap: var(--nymbal-spacing-lg);
152
+ }
153
+
154
+ @media (min-width: 769px) {
155
+ .layout {
156
+ grid-template-columns: 240px minmax(0, 1fr);
157
+ }
158
+ }
159
+
160
+ .sidebar {
161
+ align-self: start;
162
+ }
163
+
164
+ .pagination {
165
+ display: flex;
166
+ justify-content: center;
167
+ margin-top: var(--nymbal-spacing-xl);
168
+ }
169
+
146
170
  /* ── Loading ──────────────────────────────────────────────────────── */
147
171
 
148
172
  .loading {
@@ -1,26 +0,0 @@
1
- ---
2
- import type { ProductSortKey } from '@nymbalhq/types'
3
-
4
- interface Props {
5
- sort: ProductSortKey
6
- }
7
-
8
- const { sort } = Astro.props
9
-
10
- const OPTIONS: Array<{ value: ProductSortKey; label: string }> = [
11
- { value: 'newest', label: 'Newest' },
12
- { value: 'price-asc', label: 'Price: Low to High' },
13
- { value: 'price-desc', label: 'Price: High to Low' },
14
- { value: 'name-asc', label: 'Name: A to Z' },
15
- { value: 'name-desc', label: 'Name: Z to A' },
16
- ]
17
- ---
18
-
19
- <div class="plp-sort-bar">
20
- <label for="sort-select">Sort by</label>
21
- <select id="sort-select" data-testid="sort-dropdown">
22
- {OPTIONS.map((option) => (
23
- <option value={option.value} selected={option.value === sort}>{option.label}</option>
24
- ))}
25
- </select>
26
- </div>
@@ -1,35 +0,0 @@
1
- 'use client'
2
-
3
- import type { ProductSortKey } from '@nymbalhq/types'
4
- import styles from '@/styles/pages/plp.module.css'
5
-
6
- const OPTIONS: Array<{ value: ProductSortKey; label: string }> = [
7
- { value: 'newest', label: 'Newest' },
8
- { value: 'price-asc', label: 'Price: Low to High' },
9
- { value: 'price-desc', label: 'Price: High to Low' },
10
- { value: 'name-asc', label: 'Name: A to Z' },
11
- { value: 'name-desc', label: 'Name: Z to A' },
12
- ]
13
-
14
- interface SortDropdownProps {
15
- value: ProductSortKey
16
- onChange: (sort: ProductSortKey) => void
17
- }
18
-
19
- export function SortDropdown({ value, onChange }: SortDropdownProps) {
20
- return (
21
- <select
22
- className={styles.sortSelect}
23
- value={value}
24
- onChange={(e) => onChange(e.target.value as ProductSortKey)}
25
- data-testid="sort-dropdown"
26
- aria-label="Sort products"
27
- >
28
- {OPTIONS.map((option) => (
29
- <option key={option.value} value={option.value}>
30
- {option.label}
31
- </option>
32
- ))}
33
- </select>
34
- )
35
- }
@@ -1,19 +0,0 @@
1
- import styles from '@/styles/pages/pdp.module.css'
2
-
3
- interface StockBadgeProps {
4
- stock: number
5
- }
6
-
7
- export function StockBadge({ stock }: StockBadgeProps) {
8
- const inStock = stock > 0
9
-
10
- return (
11
- <span
12
- className={`${styles.stockBadge} ${inStock ? styles.inStock : styles.outOfStock}`}
13
- data-testid="stock-badge"
14
- >
15
- <span className={styles.stockDot} aria-hidden="true" />
16
- {inStock ? (stock <= 5 ? `Only ${stock} left` : 'In Stock') : 'Out of Stock'}
17
- </span>
18
- )
19
- }