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.
- package/dist/index.js +7 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scaffold/templates/astro/package.json +25 -26
- package/scaffold/templates/astro/src/components/Header.astro +7 -1
- package/scaffold/templates/astro/src/components/StockBadge.astro +5 -22
- package/scaffold/templates/astro/src/lib/api.ts +18 -4
- package/scaffold/templates/astro/src/lib/client.ts +2 -0
- package/scaffold/templates/astro/src/pages/category/[slug].astro +26 -7
- package/scaffold/templates/astro/src/pages/checkout.astro +12 -4
- package/scaffold/templates/astro/src/pages/product/[slug].astro +4 -1
- package/scaffold/templates/astro/src/pages/products/index.astro +26 -7
- package/scaffold/templates/astro/src/pages/search.astro +11 -5
- package/scaffold/templates/astro/src/scripts/cart-page.ts +10 -35
- package/scaffold/templates/astro/src/scripts/checkout.ts +48 -49
- package/scaffold/templates/astro/src/scripts/pdp.ts +35 -44
- package/scaffold/templates/astro/src/scripts/plp.ts +6 -5
- package/scaffold/templates/astro/src/scripts/search.ts +32 -8
- package/scaffold/templates/astro/src/styles/pages/cart.css +0 -28
- package/scaffold/templates/astro/src/styles/pages/pdp.css +6 -2
- package/scaffold/templates/astro/src/styles/pages/search.css +19 -0
- package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +7 -4
- package/scaffold/templates/nextjs/app/checkout/page.tsx +9 -2
- package/scaffold/templates/nextjs/app/products/page.tsx +7 -7
- package/scaffold/templates/nextjs/components/client/CartPage.tsx +1 -0
- package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +50 -91
- package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +39 -50
- package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +41 -20
- package/scaffold/templates/nextjs/components/client/SearchResults.tsx +87 -47
- package/scaffold/templates/nextjs/components/server/Header.tsx +9 -4
- package/scaffold/templates/nextjs/lib/api.ts +24 -4
- package/scaffold/templates/nextjs/lib/client.ts +3 -0
- package/scaffold/templates/nextjs/package.json +26 -28
- package/scaffold/templates/nextjs/styles/pages/pdp.module.css +6 -2
- package/scaffold/templates/nextjs/styles/pages/plp.module.css +3 -2
- package/scaffold/templates/nextjs/styles/pages/search.module.css +24 -0
- package/scaffold/templates/astro/src/components/SortDropdown.astro +0 -26
- package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +0 -35
- 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
|
-
/**
|
|
31
|
-
export function
|
|
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
|
-
|
|
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.
|
|
21
|
-
"@nymbalhq/config": "1.
|
|
22
|
-
"@nymbalhq/platform": "1.
|
|
23
|
-
"@nymbalhq/stack": "1.
|
|
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.
|
|
28
|
-
"@nymbalhq/react": "1.
|
|
29
|
-
"@nymbalhq/web-components": "1.
|
|
30
|
-
"@nymbalhq/types": "1.
|
|
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.
|
|
43
|
-
"@nymbalhq/config": "1.
|
|
44
|
-
"@nymbalhq/http": "1.
|
|
45
|
-
"@nymbalhq/platform": "1.
|
|
46
|
-
"@nymbalhq/react": "1.
|
|
47
|
-
"@nymbalhq/sdk": "1.
|
|
48
|
-
"@nymbalhq/stack": "1.
|
|
49
|
-
"@nymbalhq/types": "1.
|
|
50
|
-
"@nymbalhq/web-components": "1.
|
|
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.
|
|
55
|
-
"@nymbalhq/config": "1.
|
|
56
|
-
"@nymbalhq/http": "1.
|
|
57
|
-
"@nymbalhq/platform": "1.
|
|
58
|
-
"@nymbalhq/react": "1.
|
|
59
|
-
"@nymbalhq/sdk": "1.
|
|
60
|
-
"@nymbalhq/stack": "1.
|
|
61
|
-
"@nymbalhq/types": "1.
|
|
62
|
-
"@nymbalhq/web-components": "1.
|
|
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
|
-
.
|
|
151
|
+
.stockBadge[data-level='in'] {
|
|
152
152
|
background-color: #dcfce7;
|
|
153
153
|
color: var(--nymbal-color-success);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
.
|
|
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
|
-
.
|
|
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
|
-
}
|