create-nymbal-app 0.4.0 → 0.5.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 (163) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +7 -4
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -4
  5. package/scaffold/project/astro/src/pages/index.astro +13 -15
  6. package/scaffold/templates/astro/_dot.node-version +1 -1
  7. package/scaffold/templates/astro/astro.config.mjs +23 -0
  8. package/scaffold/templates/astro/package.json +26 -26
  9. package/scaffold/templates/astro/src/components/SkipNav.astro +3 -1
  10. package/scaffold/templates/astro/src/elements/index.ts +11 -0
  11. package/scaffold/templates/astro/src/layouts/BaseLayout.astro +15 -4
  12. package/scaffold/templates/astro/src/lib/api.ts +113 -21
  13. package/scaffold/templates/astro/src/lib/chrome.ts +59 -0
  14. package/scaffold/templates/astro/src/lib/client.ts +4 -4
  15. package/scaffold/templates/astro/src/lib/fragments.ts +48 -0
  16. package/scaffold/templates/astro/src/lib/server-elements.ts +5 -0
  17. package/scaffold/templates/astro/src/middleware.ts +37 -0
  18. package/scaffold/templates/astro/src/pages/account/addresses.astro +5 -97
  19. package/scaffold/templates/astro/src/pages/account/login.astro +7 -34
  20. package/scaffold/templates/astro/src/pages/account/profile.astro +6 -56
  21. package/scaffold/templates/astro/src/pages/account/register.astro +7 -73
  22. package/scaffold/templates/astro/src/pages/cart.astro +14 -7
  23. package/scaffold/templates/astro/src/pages/category/[slug].astro +17 -21
  24. package/scaffold/templates/astro/src/pages/checkout.astro +30 -94
  25. package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -2
  26. package/scaffold/templates/astro/src/pages/fragments/header.astro +12 -2
  27. package/scaffold/templates/astro/src/pages/fragments/nav.astro +8 -5
  28. package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +9 -10
  29. package/scaffold/templates/astro/src/pages/index.astro +18 -15
  30. package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +22 -44
  31. package/scaffold/templates/astro/src/pages/product/[slug].astro +44 -30
  32. package/scaffold/templates/astro/src/pages/products/index.astro +16 -21
  33. package/scaffold/templates/astro/src/pages/search.astro +10 -11
  34. package/scaffold/templates/astro/src/scripts/init-client.ts +14 -0
  35. package/scaffold/templates/astro/src/scripts/pdp.ts +24 -21
  36. package/scaffold/templates/astro/src/scripts/plp.ts +15 -93
  37. package/scaffold/templates/astro/src/scripts/search.ts +7 -132
  38. package/scaffold/templates/astro/src/scripts/signed-in-redirect.ts +4 -0
  39. package/scaffold/templates/astro/src/styles/global.css +49 -45
  40. package/scaffold/templates/astro/src/styles/pages/account.css +0 -90
  41. package/scaffold/templates/astro/src/styles/pages/confirmation.css +6 -0
  42. package/scaffold/templates/astro/src/styles/pages/home.css +9 -100
  43. package/scaffold/templates/astro/src/styles/pages/pdp.css +5 -171
  44. package/scaffold/templates/astro/src/styles/pages/plp.css +3 -42
  45. package/scaffold/templates/astro/src/styles/pages/search.css +4 -110
  46. package/scaffold/templates/astro/src/styles/tokens.css +56 -0
  47. package/scaffold/templates/nextjs/_dot.node-version +1 -1
  48. package/scaffold/templates/nextjs/app/(catalog)/category/[slug]/page.tsx +74 -0
  49. package/scaffold/templates/nextjs/app/(catalog)/home/[key]/page.tsx +80 -0
  50. package/scaffold/templates/nextjs/app/(catalog)/layout.tsx +13 -0
  51. package/scaffold/templates/nextjs/app/(catalog)/not-found.tsx +7 -0
  52. package/scaffold/templates/nextjs/app/(catalog)/product/[slug]/page.tsx +182 -0
  53. package/scaffold/templates/nextjs/app/(catalog)/products/page.tsx +62 -0
  54. package/scaffold/templates/nextjs/app/(catalog)/search/SearchView.tsx +90 -0
  55. package/scaffold/templates/nextjs/app/(catalog)/search/page.tsx +46 -0
  56. package/scaffold/templates/nextjs/app/(shopper)/account/addresses/AddressesView.tsx +5 -0
  57. package/scaffold/templates/nextjs/app/(shopper)/account/addresses/page.tsx +17 -0
  58. package/scaffold/templates/nextjs/app/(shopper)/account/login/LoginView.tsx +28 -0
  59. package/scaffold/templates/nextjs/app/(shopper)/account/login/page.tsx +23 -0
  60. package/scaffold/templates/nextjs/app/(shopper)/account/profile/ProfileView.tsx +5 -0
  61. package/scaffold/templates/nextjs/app/(shopper)/account/profile/page.tsx +20 -0
  62. package/scaffold/templates/nextjs/app/(shopper)/account/register/RegisterView.tsx +26 -0
  63. package/scaffold/templates/nextjs/app/(shopper)/account/register/page.tsx +11 -0
  64. package/scaffold/templates/nextjs/app/(shopper)/cart/CartView.tsx +5 -0
  65. package/scaffold/templates/nextjs/app/(shopper)/cart/page.tsx +22 -0
  66. package/scaffold/templates/nextjs/app/(shopper)/checkout/CheckoutView.tsx +27 -0
  67. package/scaffold/templates/nextjs/app/(shopper)/checkout/page.tsx +39 -0
  68. package/scaffold/templates/nextjs/app/(shopper)/layout.tsx +12 -0
  69. package/scaffold/templates/nextjs/app/{order → (shopper)/order}/[orderNumber]/page.tsx +24 -5
  70. package/scaffold/templates/nextjs/app/error.tsx +3 -7
  71. package/scaffold/templates/nextjs/app/fragments/footer/route.ts +13 -0
  72. package/scaffold/templates/nextjs/app/fragments/header/[key]/route.ts +18 -0
  73. package/scaffold/templates/nextjs/app/fragments/nav/[key]/route.ts +23 -0
  74. package/scaffold/templates/nextjs/app/fragments/product/[slug]/route.ts +22 -0
  75. package/scaffold/templates/nextjs/app/globals.css +5 -23
  76. package/scaffold/templates/nextjs/app/layout.tsx +8 -5
  77. package/scaffold/templates/nextjs/app/not-found.tsx +9 -33
  78. package/scaffold/templates/nextjs/app/tokens.css +56 -0
  79. package/scaffold/templates/nextjs/components/client/ListingControls.tsx +87 -0
  80. package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +48 -6
  81. package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +30 -15
  82. package/scaffold/templates/nextjs/components/client/elements.tsx +15 -0
  83. package/scaffold/templates/nextjs/components/server/Header.tsx +33 -102
  84. package/scaffold/templates/nextjs/components/server/NotFoundView.tsx +39 -0
  85. package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -3
  86. package/scaffold/templates/nextjs/elements/index.ts +12 -0
  87. package/scaffold/templates/nextjs/lib/api.ts +151 -55
  88. package/scaffold/templates/nextjs/lib/chrome.ts +40 -0
  89. package/scaffold/templates/nextjs/lib/fragments.ts +16 -0
  90. package/scaffold/templates/nextjs/lib/header.ts +39 -0
  91. package/scaffold/templates/nextjs/lib/render.ts +14 -0
  92. package/scaffold/templates/nextjs/next.config.mjs +32 -0
  93. package/scaffold/templates/nextjs/package.json +28 -27
  94. package/scaffold/templates/nextjs/styles/pages/account.module.css +0 -70
  95. package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +6 -85
  96. package/scaffold/templates/nextjs/styles/pages/home.module.css +2 -103
  97. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +3 -183
  98. package/scaffold/templates/nextjs/styles/pages/plp.module.css +9 -143
  99. package/scaffold/templates/nextjs/styles/pages/search.module.css +1 -145
  100. package/scaffold/templates/nextjs/tsconfig.dist.json +5 -0
  101. package/scaffold/templates/astro/src/components/AddressForm.astro +0 -107
  102. package/scaffold/templates/astro/src/components/Breadcrumb.astro +0 -20
  103. package/scaffold/templates/astro/src/components/CategoryNav.astro +0 -27
  104. package/scaffold/templates/astro/src/components/CheckoutSteps.astro +0 -29
  105. package/scaffold/templates/astro/src/components/Footer.astro +0 -108
  106. package/scaffold/templates/astro/src/components/Header.astro +0 -198
  107. package/scaffold/templates/astro/src/components/ImageGallery.astro +0 -42
  108. package/scaffold/templates/astro/src/components/OrderSummary.astro +0 -53
  109. package/scaffold/templates/astro/src/components/Pagination.astro +0 -13
  110. package/scaffold/templates/astro/src/components/PriceDisplay.astro +0 -25
  111. package/scaffold/templates/astro/src/components/ProductCard.astro +0 -44
  112. package/scaffold/templates/astro/src/components/RelatedProducts.astro +0 -28
  113. package/scaffold/templates/astro/src/components/ReviewForm.astro +0 -74
  114. package/scaffold/templates/astro/src/components/ReviewList.astro +0 -41
  115. package/scaffold/templates/astro/src/components/StockBadge.astro +0 -15
  116. package/scaffold/templates/astro/src/scripts/auth-forms.ts +0 -82
  117. package/scaffold/templates/astro/src/scripts/cart-page.ts +0 -118
  118. package/scaffold/templates/astro/src/scripts/checkout.ts +0 -271
  119. package/scaffold/templates/astro/src/scripts/image-gallery.ts +0 -22
  120. package/scaffold/templates/astro/src/scripts/profile-form.ts +0 -57
  121. package/scaffold/templates/astro/src/scripts/review-form.ts +0 -27
  122. package/scaffold/templates/astro/src/styles/pages/cart.css +0 -215
  123. package/scaffold/templates/astro/src/styles/pages/checkout.css +0 -270
  124. package/scaffold/templates/nextjs/app/account/addresses/page.tsx +0 -10
  125. package/scaffold/templates/nextjs/app/account/login/page.tsx +0 -23
  126. package/scaffold/templates/nextjs/app/account/profile/page.tsx +0 -10
  127. package/scaffold/templates/nextjs/app/account/register/page.tsx +0 -10
  128. package/scaffold/templates/nextjs/app/cart/page.tsx +0 -10
  129. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +0 -61
  130. package/scaffold/templates/nextjs/app/checkout/page.tsx +0 -17
  131. package/scaffold/templates/nextjs/app/page.tsx +0 -63
  132. package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +0 -111
  133. package/scaffold/templates/nextjs/app/products/page.tsx +0 -40
  134. package/scaffold/templates/nextjs/app/search/page.tsx +0 -24
  135. package/scaffold/templates/nextjs/components/client/AddressManager.tsx +0 -181
  136. package/scaffold/templates/nextjs/components/client/CartPage.tsx +0 -121
  137. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +0 -482
  138. package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +0 -68
  139. package/scaffold/templates/nextjs/components/client/LoginForm.tsx +0 -89
  140. package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +0 -12
  141. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +0 -137
  142. package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +0 -112
  143. package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +0 -131
  144. package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +0 -116
  145. package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +0 -27
  146. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +0 -181
  147. package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +0 -36
  148. package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +0 -29
  149. package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +0 -60
  150. package/scaffold/templates/nextjs/components/server/Footer.module.css +0 -64
  151. package/scaffold/templates/nextjs/components/server/Footer.tsx +0 -79
  152. package/scaffold/templates/nextjs/components/server/Header.module.css +0 -147
  153. package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +0 -79
  154. package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +0 -31
  155. package/scaffold/templates/nextjs/components/server/ProductCard.tsx +0 -59
  156. package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +0 -31
  157. package/scaffold/templates/nextjs/components/server/ReviewList.tsx +0 -62
  158. package/scaffold/templates/nextjs/styles/pages/cart.module.css +0 -240
  159. package/scaffold/templates/nextjs/styles/pages/checkout.module.css +0 -404
  160. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/layout.tsx +0 -0
  161. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/[orderNumber]/page.tsx +0 -0
  162. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/page.tsx +0 -0
  163. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/page.tsx +0 -0
@@ -1,111 +0,0 @@
1
- import type { Metadata } from 'next'
2
- import { notFound } from 'next/navigation'
3
- import { fetchProduct, fetchProducts, fetchReviews } from '@/lib/api'
4
- import { productJsonLd } from '@/lib/seo'
5
- import { Breadcrumb } from '@/components/server/Breadcrumb'
6
- import { ReviewList } from '@/components/server/ReviewList'
7
- import { RelatedProducts } from '@/components/server/RelatedProducts'
8
- import { ImageGallery } from '@/components/client/ImageGallery'
9
- import { ProductInteractive } from '@/components/client/ProductInteractive'
10
- import { ReviewForm } from '@/components/client/ReviewForm'
11
- import styles from '@/styles/pages/pdp.module.css'
12
-
13
- interface ProductPageProps {
14
- params: Promise<{ slug: string }>
15
- }
16
-
17
- export async function generateMetadata({
18
- params,
19
- }: ProductPageProps): Promise<Metadata> {
20
- const { slug } = await params
21
- const product = await fetchProduct(slug)
22
-
23
- if (!product) {
24
- return { title: 'Product Not Found' }
25
- }
26
-
27
- return {
28
- title: product.name,
29
- description: product.shortDescription ?? product.description ?? `Buy ${product.name}.`,
30
- }
31
- }
32
-
33
- export default async function ProductPage({ params }: ProductPageProps) {
34
- const { slug } = await params
35
- const product = await fetchProduct(slug)
36
-
37
- if (!product) notFound()
38
-
39
- const [{ items: reviews }, { items: relatedProducts }] = await Promise.all([
40
- fetchReviews(product.id),
41
- fetchProducts({ limit: 4 }),
42
- ])
43
-
44
- const related = relatedProducts.filter((p) => p.id !== product.id).slice(0, 4)
45
-
46
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'
47
- const jsonLd = productJsonLd(product, baseUrl)
48
-
49
- const mediaArray = Array.isArray(product.media) ? product.media : []
50
- const images = mediaArray.length > 0
51
- ? mediaArray.map((m: Record<string, string>) => ({
52
- url: m.url,
53
- alt: m.alt ?? product.name,
54
- }))
55
- : [{ url: `https://picsum.photos/seed/${product.slug}/600/600`, alt: product.name }]
56
-
57
- return (
58
- <>
59
- <script
60
- type="application/ld+json"
61
- dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
62
- />
63
-
64
- <div className={`container ${styles.page}`}>
65
- <Breadcrumb
66
- items={[
67
- { label: 'Home', href: '/' },
68
- { label: 'Products', href: '/products' },
69
- { label: product.name },
70
- ]}
71
- />
72
-
73
- <div className={styles.productLayout}>
74
- <ImageGallery images={images} />
75
-
76
- <div className={styles.info}>
77
- <ProductInteractive product={product} />
78
-
79
- {product.description && (
80
- <div className={styles.description}>
81
- <p>{product.description}</p>
82
- </div>
83
- )}
84
-
85
- <div className={styles.detailsSection}>
86
- <details className={styles.detailsItem}>
87
- <summary className={styles.detailsSummary}>Details</summary>
88
- <div className={styles.detailsContent}>
89
- {product.description ?? 'No additional details available.'}
90
- </div>
91
- </details>
92
- <details className={styles.detailsItem}>
93
- <summary className={styles.detailsSummary}>
94
- Shipping & Returns
95
- </summary>
96
- <div className={styles.detailsContent}>
97
- Free standard shipping on orders over $50. Returns accepted within
98
- 30 days of purchase.
99
- </div>
100
- </details>
101
- </div>
102
- </div>
103
- </div>
104
-
105
- <ReviewList reviews={reviews} />
106
- <ReviewForm productId={product.id} />
107
- <RelatedProducts products={related} />
108
- </div>
109
- </>
110
- )
111
- }
@@ -1,40 +0,0 @@
1
- import type { Metadata } from 'next'
2
- import { fetchProducts, fetchCapabilities, sortFromSearchParams } from '@/lib/api'
3
- import { Breadcrumb } from '@/components/server/Breadcrumb'
4
- import { ProductGrid } from '@/components/client/ProductGrid'
5
- import styles from '@/styles/pages/plp.module.css'
6
-
7
- export const metadata: Metadata = {
8
- title: 'All Products',
9
- description: 'Browse our full collection of products.',
10
- }
11
-
12
- interface ProductsPageProps {
13
- searchParams: Promise<{ sort?: string | string[] }>
14
- }
15
-
16
- export default async function ProductsPage({ searchParams }: ProductsPageProps) {
17
- const capabilities = await fetchCapabilities()
18
- const sort = sortFromSearchParams(await searchParams, capabilities?.catalog)
19
- const { items, nextCursor, facets } = await fetchProducts({ limit: 24, sort })
20
-
21
- return (
22
- <div className={`container ${styles.page}`}>
23
- <Breadcrumb
24
- items={[
25
- { label: 'Home', href: '/' },
26
- { label: 'Products' },
27
- ]}
28
- />
29
- <h1 className={styles.pageTitle}>All Products</h1>
30
- <ProductGrid
31
- initialProducts={items}
32
- initialCursor={nextCursor}
33
- initialSort={sort}
34
- initialFacets={facets ?? []}
35
- sortKeys={capabilities?.catalog.sortKeys ?? []}
36
- defaultSort={capabilities?.catalog.defaultSort}
37
- />
38
- </div>
39
- )
40
- }
@@ -1,24 +0,0 @@
1
- import type { Metadata } from 'next'
2
- import { Suspense } from 'react'
3
- import { SearchResults } from '@/components/client/SearchResults'
4
-
5
- export const metadata: Metadata = {
6
- title: 'Search',
7
- description: 'Search our product catalog.',
8
- }
9
-
10
- export default function SearchPage() {
11
- return (
12
- <Suspense
13
- fallback={
14
- <div className="loading-center">
15
- <div className="spinner spinner-lg" role="status">
16
- <span className="visually-hidden">Loading...</span>
17
- </div>
18
- </div>
19
- }
20
- >
21
- <SearchResults />
22
- </Suspense>
23
- )
24
- }
@@ -1,181 +0,0 @@
1
- 'use client'
2
-
3
- import { useCallback, useEffect, useState, type FormEvent } from 'react'
4
- import { useNymbalClient } from '@nymbalhq/react'
5
- import type { Address, CustomerAddress } from '@nymbalhq/types'
6
- import styles from '@/styles/pages/account.module.css'
7
-
8
- // The same fields and test ids as the Astro template's AddressForm (`new-address-<name>`).
9
- const FIELDS: Array<{
10
- name: keyof Address
11
- label: string
12
- autoComplete: string
13
- required: boolean
14
- full?: boolean
15
- placeholder?: string
16
- }> = [
17
- { name: 'firstName', label: 'First Name', autoComplete: 'given-name', required: true },
18
- { name: 'lastName', label: 'Last Name', autoComplete: 'family-name', required: true },
19
- {
20
- name: 'addressLine1',
21
- label: 'Address Line 1',
22
- autoComplete: 'address-line1',
23
- required: true,
24
- full: true,
25
- },
26
- {
27
- name: 'addressLine2',
28
- label: 'Address Line 2 (optional)',
29
- autoComplete: 'address-line2',
30
- required: false,
31
- full: true,
32
- },
33
- { name: 'city', label: 'City', autoComplete: 'address-level2', required: true },
34
- { name: 'region', label: 'State / County', autoComplete: 'address-level1', required: true },
35
- { name: 'postalCode', label: 'Postcode', autoComplete: 'postal-code', required: true },
36
- {
37
- name: 'country',
38
- label: 'Country (2-letter code)',
39
- autoComplete: 'country',
40
- required: true,
41
- placeholder: 'GB',
42
- },
43
- ]
44
-
45
- export function AddressManager() {
46
- const client = useNymbalClient()
47
- const [addresses, setAddresses] = useState<CustomerAddress[] | null>(null)
48
- const [loadError, setLoadError] = useState(false)
49
- const [saving, setSaving] = useState(false)
50
- const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null)
51
-
52
- const load = useCallback(async () => {
53
- try {
54
- setAddresses(await client.customer.addresses.list())
55
- setLoadError(false)
56
- } catch {
57
- setLoadError(true)
58
- }
59
- }, [client])
60
-
61
- useEffect(() => {
62
- void load()
63
- }, [load])
64
-
65
- async function handleSubmit(e: FormEvent<HTMLFormElement>) {
66
- e.preventDefault()
67
- const form = e.currentTarget
68
- const data = new FormData(form)
69
- const field = (name: string) => String(data.get(name) ?? '').trim()
70
- const address: Address = {
71
- firstName: field('firstName'),
72
- lastName: field('lastName'),
73
- addressLine1: field('addressLine1'),
74
- city: field('city'),
75
- region: field('region'),
76
- postalCode: field('postalCode'),
77
- country: field('country'),
78
- ...(field('addressLine2') && { addressLine2: field('addressLine2') }),
79
- }
80
-
81
- setSaving(true)
82
- setMessage(null)
83
- try {
84
- await client.customer.addresses.create(address)
85
- form.reset()
86
- setMessage({ ok: true, text: 'Address saved successfully.' })
87
- await load()
88
- } catch {
89
- setMessage({ ok: false, text: 'Failed to save address. Please try again.' })
90
- } finally {
91
- setSaving(false)
92
- }
93
- }
94
-
95
- return (
96
- <div>
97
- <h2 className={styles.pageTitle}>Addresses</h2>
98
-
99
- <div data-testid="addresses-list">
100
- {addresses === null && !loadError && (
101
- <p className="text-sm text-muted">Loading addresses...</p>
102
- )}
103
- {loadError && <p className="text-sm text-muted">Unable to load addresses.</p>}
104
- {addresses !== null && addresses.length === 0 && (
105
- <div className={styles.emptyOrders}>
106
- <h3 className={styles.emptyOrdersTitle}>No saved addresses</h3>
107
- <p className={styles.emptyOrdersText}>Add an address below to get started.</p>
108
- </div>
109
- )}
110
- {addresses?.map((addr) => (
111
- <div
112
- key={addr.id}
113
- data-testid={`address-${addr.id}`}
114
- className={styles.formCard}
115
- style={{ marginBottom: 'var(--nymbal-spacing-md)' }}
116
- >
117
- <p style={{ fontSize: 'var(--nymbal-font-size-sm)', lineHeight: 1.6 }}>
118
- {addr.firstName} {addr.lastName}
119
- <br />
120
- {addr.addressLine1}
121
- {addr.addressLine2 && <>, {addr.addressLine2}</>}
122
- <br />
123
- {addr.city}, {addr.region} {addr.postalCode}
124
- <br />
125
- {addr.country}
126
- </p>
127
- <div
128
- style={{
129
- display: 'flex',
130
- gap: 'var(--nymbal-spacing-sm)',
131
- marginTop: 'var(--nymbal-spacing-sm)',
132
- }}
133
- >
134
- {addr.isDefaultShipping && <span className="badge">Default Shipping</span>}
135
- {addr.isDefaultBilling && <span className="badge">Default Billing</span>}
136
- </div>
137
- </div>
138
- ))}
139
- </div>
140
-
141
- <div className={styles.formCard} style={{ marginTop: 'var(--nymbal-spacing-xl)' }}>
142
- <h3 className={styles.formCardTitle}>Add New Address</h3>
143
- {message && (
144
- <div
145
- className={`${styles.alert} ${message.ok ? styles.alertSuccess : styles.alertError}`}
146
- >
147
- {message.text}
148
- </div>
149
- )}
150
- <form onSubmit={handleSubmit} data-testid="add-address-form">
151
- <div className={styles.formGrid}>
152
- {FIELDS.map((f) => (
153
- <div key={f.name} className={f.full ? styles.formGridFull : undefined}>
154
- <label htmlFor={`new-address-${f.name}`}>{f.label}</label>
155
- <input
156
- id={`new-address-${f.name}`}
157
- name={f.name}
158
- type="text"
159
- required={f.required}
160
- autoComplete={f.autoComplete}
161
- placeholder={f.placeholder}
162
- data-testid={`new-address-${f.name}`}
163
- />
164
- </div>
165
- ))}
166
- </div>
167
- <div className={styles.formActions}>
168
- <button
169
- type="submit"
170
- className={styles.saveButton}
171
- disabled={saving}
172
- data-testid="save-address"
173
- >
174
- {saving ? 'Saving...' : 'Save Address'}
175
- </button>
176
- </div>
177
- </form>
178
- </div>
179
- </div>
180
- )
181
- }
@@ -1,121 +0,0 @@
1
- 'use client'
2
-
3
- import Link from 'next/link'
4
- import Image from 'next/image'
5
- import { useCart, QuantitySelector } from '@nymbalhq/react'
6
- import { formatPrice } from '@/lib/format'
7
- import styles from '@/styles/pages/cart.module.css'
8
-
9
- export function CartPage() {
10
- const { items, subtotalMinor, currency, removeItem, updateQuantity, loading } = useCart()
11
-
12
- if (loading) {
13
- return (
14
- <div className="loading-center">
15
- <div className="spinner spinner-lg" role="status">
16
- <span className="visually-hidden">Loading cart...</span>
17
- </div>
18
- </div>
19
- )
20
- }
21
-
22
- if (!items || items.length === 0) {
23
- return (
24
- <div className={`container ${styles.page}`}>
25
- <h1 className={styles.pageTitle}>Shopping Cart</h1>
26
- <div className={styles.empty}>
27
- <h2 className={styles.emptyTitle}>Your cart is empty</h2>
28
- <p className={styles.emptyText}>
29
- Looks like you have not added anything to your cart yet.
30
- </p>
31
- <Link href="/products" className="btn btn-primary" data-testid="continue-shopping">
32
- Continue Shopping
33
- </Link>
34
- </div>
35
- </div>
36
- )
37
- }
38
-
39
- const cartCurrency = currency ?? 'GBP'
40
-
41
- return (
42
- <div className={`container ${styles.page}`}>
43
- <h1 className={styles.pageTitle}>Shopping Cart</h1>
44
- <div className={styles.layout}>
45
- <div className={styles.itemsSection}>
46
- <div className={styles.itemsList}>
47
- {items.map((item) => (
48
- <div key={item.variantId} className={styles.item} data-testid={`cart-item-${item.variantId}`}>
49
- <div className={styles.itemImage}>
50
- {item.imageUrl ? (
51
- <Image
52
- src={item.imageUrl}
53
- alt={item.productName}
54
- width={96}
55
- height={96}
56
- />
57
- ) : (
58
- <span className="visually-hidden">No image</span>
59
- )}
60
- </div>
61
- <div className={styles.itemDetails}>
62
- <Link href={`/product/${item.productId}`} className={styles.itemName}>
63
- {item.productName}
64
- </Link>
65
- {item.variantName && (
66
- <span className={styles.itemVariant}>{item.variantName}</span>
67
- )}
68
- <div className={styles.itemActions}>
69
- <QuantitySelector
70
- value={item.qty}
71
- min={1}
72
- max={99}
73
- onQuantityChange={(e) => updateQuantity(item.variantId, e.detail.value)}
74
- />
75
- <button
76
- type="button"
77
- className={styles.removeButton}
78
- onClick={() => removeItem(item.variantId)}
79
- data-testid={`cart-remove-${item.variantId}`}
80
- aria-label={`Remove ${item.productName} from cart`}
81
- >
82
- Remove
83
- </button>
84
- </div>
85
- </div>
86
- <span className={styles.itemPrice}>
87
- {formatPrice(item.priceMinor * item.qty, cartCurrency)}
88
- </span>
89
- </div>
90
- ))}
91
- </div>
92
- </div>
93
-
94
- <div className={styles.summary}>
95
- <h2 className={styles.summaryTitle}>Order Summary</h2>
96
- <div className={styles.summaryRow}>
97
- <span className={styles.summaryLabel}>Subtotal</span>
98
- <span className={styles.summaryValue}>
99
- {formatPrice(subtotalMinor ?? 0, cartCurrency)}
100
- </span>
101
- </div>
102
- <div className={styles.summaryRow}>
103
- <span className={styles.summaryLabel}>Shipping</span>
104
- <span className={styles.summaryValue}>Calculated at checkout</span>
105
- </div>
106
- <hr className={styles.summaryDivider} />
107
- <div className={`${styles.summaryRow} ${styles.summaryTotal}`}>
108
- <span>Estimated Total</span>
109
- <span>{formatPrice(subtotalMinor ?? 0, cartCurrency)}</span>
110
- </div>
111
- <Link href="/checkout" className={styles.checkoutButton}>
112
- Proceed to Checkout
113
- </Link>
114
- <Link href="/products" className={styles.continueShoppingLink}>
115
- Continue Shopping
116
- </Link>
117
- </div>
118
- </div>
119
- </div>
120
- )
121
- }