create-nymbal-app 0.0.0 → 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.
Files changed (159) hide show
  1. package/bin/create-nymbal-app.mjs +9 -0
  2. package/dist/index.d.ts +37 -0
  3. package/dist/index.js +419 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +41 -7
  6. package/scaffold/project/astro/src/pages/index.astro +57 -0
  7. package/scaffold/templates/astro/README.md +26 -0
  8. package/scaffold/templates/astro/_dot.env.example +6 -0
  9. package/scaffold/templates/astro/_dot.gitignore +16 -0
  10. package/scaffold/templates/astro/_dot.node-version +1 -0
  11. package/scaffold/templates/astro/_dot.npmrc +1 -0
  12. package/scaffold/templates/astro/astro.config.mjs +14 -0
  13. package/scaffold/templates/astro/package.json +59 -0
  14. package/scaffold/templates/astro/src/components/AddressForm.astro +107 -0
  15. package/scaffold/templates/astro/src/components/Breadcrumb.astro +20 -0
  16. package/scaffold/templates/astro/src/components/CategoryNav.astro +27 -0
  17. package/scaffold/templates/astro/src/components/CheckoutSteps.astro +29 -0
  18. package/scaffold/templates/astro/src/components/Footer.astro +108 -0
  19. package/scaffold/templates/astro/src/components/Header.astro +192 -0
  20. package/scaffold/templates/astro/src/components/HeroSection.astro +17 -0
  21. package/scaffold/templates/astro/src/components/ImageGallery.astro +42 -0
  22. package/scaffold/templates/astro/src/components/NewsletterSignup.astro +39 -0
  23. package/scaffold/templates/astro/src/components/OrderSummary.astro +53 -0
  24. package/scaffold/templates/astro/src/components/Pagination.astro +13 -0
  25. package/scaffold/templates/astro/src/components/PriceDisplay.astro +25 -0
  26. package/scaffold/templates/astro/src/components/ProductCard.astro +44 -0
  27. package/scaffold/templates/astro/src/components/RelatedProducts.astro +28 -0
  28. package/scaffold/templates/astro/src/components/ReviewForm.astro +100 -0
  29. package/scaffold/templates/astro/src/components/ReviewList.astro +41 -0
  30. package/scaffold/templates/astro/src/components/SkipNav.astro +3 -0
  31. package/scaffold/templates/astro/src/components/SortDropdown.astro +26 -0
  32. package/scaffold/templates/astro/src/components/StockBadge.astro +32 -0
  33. package/scaffold/templates/astro/src/env.d.ts +2 -0
  34. package/scaffold/templates/astro/src/layouts/AccountLayout.astro +168 -0
  35. package/scaffold/templates/astro/src/layouts/BaseLayout.astro +55 -0
  36. package/scaffold/templates/astro/src/lib/api.ts +89 -0
  37. package/scaffold/templates/astro/src/lib/client.ts +13 -0
  38. package/scaffold/templates/astro/src/lib/format.ts +17 -0
  39. package/scaffold/templates/astro/src/lib/order-status.ts +14 -0
  40. package/scaffold/templates/astro/src/lib/seo.ts +62 -0
  41. package/scaffold/templates/astro/src/pages/404.astro +18 -0
  42. package/scaffold/templates/astro/src/pages/500.astro +17 -0
  43. package/scaffold/templates/astro/src/pages/account/addresses.astro +110 -0
  44. package/scaffold/templates/astro/src/pages/account/index.astro +67 -0
  45. package/scaffold/templates/astro/src/pages/account/login.astro +55 -0
  46. package/scaffold/templates/astro/src/pages/account/orders/[orderNumber].astro +108 -0
  47. package/scaffold/templates/astro/src/pages/account/orders.astro +81 -0
  48. package/scaffold/templates/astro/src/pages/account/profile.astro +131 -0
  49. package/scaffold/templates/astro/src/pages/account/register.astro +94 -0
  50. package/scaffold/templates/astro/src/pages/cart.astro +16 -0
  51. package/scaffold/templates/astro/src/pages/category/[slug].astro +73 -0
  52. package/scaffold/templates/astro/src/pages/checkout.astro +100 -0
  53. package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -0
  54. package/scaffold/templates/astro/src/pages/fragments/header.astro +5 -0
  55. package/scaffold/templates/astro/src/pages/fragments/nav.astro +9 -0
  56. package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +14 -0
  57. package/scaffold/templates/astro/src/pages/index.astro +49 -0
  58. package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +127 -0
  59. package/scaffold/templates/astro/src/pages/product/[slug].astro +107 -0
  60. package/scaffold/templates/astro/src/pages/products/index.astro +62 -0
  61. package/scaffold/templates/astro/src/pages/search.astro +37 -0
  62. package/scaffold/templates/astro/src/scripts/account-guard.ts +22 -0
  63. package/scaffold/templates/astro/src/scripts/auth-forms.ts +89 -0
  64. package/scaffold/templates/astro/src/scripts/cart-page.ts +143 -0
  65. package/scaffold/templates/astro/src/scripts/checkout.ts +277 -0
  66. package/scaffold/templates/astro/src/scripts/image-gallery.ts +22 -0
  67. package/scaffold/templates/astro/src/scripts/init-client.ts +16 -0
  68. package/scaffold/templates/astro/src/scripts/order-status.ts +27 -0
  69. package/scaffold/templates/astro/src/scripts/pdp.ts +67 -0
  70. package/scaffold/templates/astro/src/scripts/plp.ts +109 -0
  71. package/scaffold/templates/astro/src/scripts/search.ts +145 -0
  72. package/scaffold/templates/astro/src/styles/global.css +362 -0
  73. package/scaffold/templates/astro/src/styles/pages/account.css +331 -0
  74. package/scaffold/templates/astro/src/styles/pages/cart.css +243 -0
  75. package/scaffold/templates/astro/src/styles/pages/checkout.css +270 -0
  76. package/scaffold/templates/astro/src/styles/pages/confirmation.css +142 -0
  77. package/scaffold/templates/astro/src/styles/pages/error.css +69 -0
  78. package/scaffold/templates/astro/src/styles/pages/home.css +269 -0
  79. package/scaffold/templates/astro/src/styles/pages/pdp.css +272 -0
  80. package/scaffold/templates/astro/src/styles/pages/plp.css +213 -0
  81. package/scaffold/templates/astro/src/styles/pages/search.css +152 -0
  82. package/scaffold/templates/astro/tsconfig.json +3 -0
  83. package/scaffold/templates/nextjs/README.md +26 -0
  84. package/scaffold/templates/nextjs/_dot.env.example +6 -0
  85. package/scaffold/templates/nextjs/_dot.gitignore +17 -0
  86. package/scaffold/templates/nextjs/_dot.node-version +1 -0
  87. package/scaffold/templates/nextjs/_dot.npmrc +1 -0
  88. package/scaffold/templates/nextjs/app/account/addresses/page.tsx +10 -0
  89. package/scaffold/templates/nextjs/app/account/layout.tsx +57 -0
  90. package/scaffold/templates/nextjs/app/account/login/page.tsx +23 -0
  91. package/scaffold/templates/nextjs/app/account/orders/[orderNumber]/page.tsx +192 -0
  92. package/scaffold/templates/nextjs/app/account/orders/page.tsx +123 -0
  93. package/scaffold/templates/nextjs/app/account/page.tsx +59 -0
  94. package/scaffold/templates/nextjs/app/account/profile/page.tsx +10 -0
  95. package/scaffold/templates/nextjs/app/account/register/page.tsx +10 -0
  96. package/scaffold/templates/nextjs/app/cart/page.tsx +10 -0
  97. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +58 -0
  98. package/scaffold/templates/nextjs/app/checkout/page.tsx +10 -0
  99. package/scaffold/templates/nextjs/app/error.tsx +49 -0
  100. package/scaffold/templates/nextjs/app/globals.css +473 -0
  101. package/scaffold/templates/nextjs/app/layout.tsx +29 -0
  102. package/scaffold/templates/nextjs/app/not-found.tsx +38 -0
  103. package/scaffold/templates/nextjs/app/order/[orderNumber]/page.tsx +127 -0
  104. package/scaffold/templates/nextjs/app/page.tsx +63 -0
  105. package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +111 -0
  106. package/scaffold/templates/nextjs/app/products/page.tsx +40 -0
  107. package/scaffold/templates/nextjs/app/search/page.tsx +24 -0
  108. package/scaffold/templates/nextjs/components/client/AddressManager.tsx +181 -0
  109. package/scaffold/templates/nextjs/components/client/AuthGuard.tsx +55 -0
  110. package/scaffold/templates/nextjs/components/client/CartDrawerToggle.tsx +39 -0
  111. package/scaffold/templates/nextjs/components/client/CartPage.tsx +120 -0
  112. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +523 -0
  113. package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +68 -0
  114. package/scaffold/templates/nextjs/components/client/LoginForm.tsx +89 -0
  115. package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +12 -0
  116. package/scaffold/templates/nextjs/components/client/NewsletterSignup.tsx +74 -0
  117. package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +24 -0
  118. package/scaffold/templates/nextjs/components/client/OrderStatusRefresh.tsx +48 -0
  119. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +148 -0
  120. package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +61 -0
  121. package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +112 -0
  122. package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +131 -0
  123. package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +116 -0
  124. package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +27 -0
  125. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +141 -0
  126. package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +35 -0
  127. package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +36 -0
  128. package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +29 -0
  129. package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +60 -0
  130. package/scaffold/templates/nextjs/components/server/Footer.module.css +64 -0
  131. package/scaffold/templates/nextjs/components/server/Footer.tsx +79 -0
  132. package/scaffold/templates/nextjs/components/server/Header.module.css +147 -0
  133. package/scaffold/templates/nextjs/components/server/Header.tsx +102 -0
  134. package/scaffold/templates/nextjs/components/server/HeroSection.tsx +18 -0
  135. package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +79 -0
  136. package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +31 -0
  137. package/scaffold/templates/nextjs/components/server/ProductCard.tsx +59 -0
  138. package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +31 -0
  139. package/scaffold/templates/nextjs/components/server/ReviewList.tsx +62 -0
  140. package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -0
  141. package/scaffold/templates/nextjs/components/server/StockBadge.tsx +19 -0
  142. package/scaffold/templates/nextjs/lib/api.ts +89 -0
  143. package/scaffold/templates/nextjs/lib/client.ts +23 -0
  144. package/scaffold/templates/nextjs/lib/format.ts +7 -0
  145. package/scaffold/templates/nextjs/lib/order-status.ts +14 -0
  146. package/scaffold/templates/nextjs/lib/seo.ts +73 -0
  147. package/scaffold/templates/nextjs/next.config.mjs +9 -0
  148. package/scaffold/templates/nextjs/package.json +64 -0
  149. package/scaffold/templates/nextjs/styles/pages/account.module.css +377 -0
  150. package/scaffold/templates/nextjs/styles/pages/cart.module.css +240 -0
  151. package/scaffold/templates/nextjs/styles/pages/checkout.module.css +404 -0
  152. package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +237 -0
  153. package/scaffold/templates/nextjs/styles/pages/error.module.css +132 -0
  154. package/scaffold/templates/nextjs/styles/pages/home.module.css +285 -0
  155. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +319 -0
  156. package/scaffold/templates/nextjs/styles/pages/plp.module.css +280 -0
  157. package/scaffold/templates/nextjs/styles/pages/search.module.css +192 -0
  158. package/scaffold/templates/nextjs/tsconfig.json +41 -0
  159. package/README.md +0 -4
@@ -0,0 +1,107 @@
1
+ ---
2
+ import '../../styles/pages/pdp.css'
3
+ import BaseLayout from '../../layouts/BaseLayout.astro'
4
+ import Breadcrumb from '../../components/Breadcrumb.astro'
5
+ import ImageGallery from '../../components/ImageGallery.astro'
6
+ import PriceDisplay from '../../components/PriceDisplay.astro'
7
+ import StockBadge from '../../components/StockBadge.astro'
8
+ import ReviewList from '../../components/ReviewList.astro'
9
+ import ReviewForm from '../../components/ReviewForm.astro'
10
+ import RelatedProducts from '../../components/RelatedProducts.astro'
11
+ import { fetchProduct, fetchReviews, fetchProducts } from '../../lib/api'
12
+ import { productJsonLd, breadcrumbJsonLd } from '../../lib/seo'
13
+
14
+ const { slug } = Astro.params
15
+
16
+ const product = await fetchProduct(slug!)
17
+ if (!product) return Astro.redirect('/404')
18
+
19
+ const [reviewsResult, relatedResult] = await Promise.all([
20
+ fetchReviews(product.id),
21
+ fetchProducts({
22
+ limit: 4,
23
+ category: product.categoryIds?.[0] ?? undefined,
24
+ }),
25
+ ])
26
+
27
+ const reviews = reviewsResult.items ?? []
28
+ const relatedProducts = (relatedResult.items ?? []).filter((p) => p.id !== product.id).slice(0, 4)
29
+
30
+ const images = Array.isArray(product.media) && product.media.length > 0
31
+ ? product.media.map((m: any) => ({
32
+ url: typeof m === 'string' ? m : m.url,
33
+ alt: typeof m === 'string' ? product.name : (m.alt ?? product.name),
34
+ }))
35
+ : [{ url: `https://picsum.photos/seed/${product.slug}/600/600`, alt: product.name }]
36
+
37
+ const defaultVariant = product.variants?.[0]
38
+ const stock = defaultVariant?.stock ?? (product.inStock ? 10 : 0)
39
+
40
+ const jsonLd = productJsonLd(product, Astro.url.origin)
41
+ const breadcrumb = breadcrumbJsonLd([
42
+ { name: 'Home', url: Astro.url.origin },
43
+ { name: 'Products', url: `${Astro.url.origin}/products` },
44
+ { name: product.name, url: `${Astro.url.origin}/product/${product.slug}` },
45
+ ])
46
+
47
+ // The selector derives its option groups from the product's variants.
48
+ const hasVariantChoice = (product.variants?.length ?? 0) > 1
49
+ ---
50
+
51
+ <BaseLayout title={`${product.name} | Nymbal`} description={product.shortDescription ?? product.description ?? ''} jsonLd={jsonLd}>
52
+ <div class="container section" data-product-slug={product.slug}>
53
+ <Breadcrumb
54
+ items={[
55
+ { label: 'Home', href: '/' },
56
+ { label: 'Products', href: '/products' },
57
+ { label: product.name },
58
+ ]}
59
+ />
60
+
61
+ <div class="pdp-layout">
62
+ <ImageGallery images={images} />
63
+
64
+ <div class="pdp-info">
65
+ <h1 class="pdp-title" data-testid="product-title">{product.name}</h1>
66
+
67
+ <PriceDisplay
68
+ priceMinor={defaultVariant?.priceMinor ?? product.priceMinor}
69
+ currency={product.currency ?? 'GBP'}
70
+ />
71
+
72
+ <StockBadge stock={stock} />
73
+
74
+ <div class="pdp-actions">
75
+ {hasVariantChoice && (
76
+ <nymbal-variant-selector></nymbal-variant-selector>
77
+ )}
78
+
79
+ <div class="pdp-add-row">
80
+ <nymbal-quantity-selector value="1" min="1" max="99"></nymbal-quantity-selector>
81
+ <nymbal-add-to-cart
82
+ variant-id={defaultVariant?.id ?? product.id}
83
+ ></nymbal-add-to-cart>
84
+ </div>
85
+ </div>
86
+
87
+ {product.description && (
88
+ <div class="pdp-description" data-testid="product-description">
89
+ <Fragment set:html={product.description} />
90
+ </div>
91
+ )}
92
+ </div>
93
+ </div>
94
+
95
+ <section class="pdp-reviews">
96
+ <h2 class="pdp-reviews-title">Customer Reviews</h2>
97
+ <ReviewList reviews={reviews} />
98
+ <ReviewForm productId={product.id} />
99
+ </section>
100
+
101
+ <RelatedProducts products={relatedProducts} />
102
+ </div>
103
+ </BaseLayout>
104
+
105
+ <script>
106
+ import '../../scripts/pdp.js'
107
+ </script>
@@ -0,0 +1,62 @@
1
+ ---
2
+ import '../../styles/pages/plp.css'
3
+ import '../../styles/pages/home.css'
4
+ import BaseLayout from '../../layouts/BaseLayout.astro'
5
+ import Breadcrumb from '../../components/Breadcrumb.astro'
6
+ import ProductCard from '../../components/ProductCard.astro'
7
+ import SortDropdown from '../../components/SortDropdown.astro'
8
+ import Pagination from '../../components/Pagination.astro'
9
+ import { fetchProducts, fetchCategories, sortFromUrl } from '../../lib/api'
10
+
11
+ const sort = sortFromUrl(Astro.url)
12
+
13
+ const [productResult, categories] = await Promise.all([
14
+ fetchProducts({ limit: 24, sort }),
15
+ fetchCategories(),
16
+ ])
17
+
18
+ const products = productResult.items
19
+ const facets = productResult.facets ?? []
20
+ ---
21
+
22
+ <BaseLayout title="All Products | Nymbal" description="Browse our full product collection.">
23
+ <div class="container section">
24
+ <Breadcrumb items={[{ label: 'Home', href: '/' }, { label: 'Products' }]} />
25
+
26
+ <div class="plp-header">
27
+ <h1 class="plp-title">All Products</h1>
28
+ <p class="plp-count" id="product-count">{products.length} products</p>
29
+ </div>
30
+
31
+ <div class="plp-layout">
32
+ <aside class="plp-sidebar">
33
+ <nymbal-product-filter
34
+ facets={JSON.stringify(facets)}
35
+ ></nymbal-product-filter>
36
+ </aside>
37
+
38
+ <div class="plp-grid-area">
39
+ <SortDropdown sort={sort} />
40
+
41
+ <div class="product-grid" id="product-grid">
42
+ {products.map((product) => (
43
+ <ProductCard product={product} />
44
+ ))}
45
+ </div>
46
+
47
+ {products.length === 0 && (
48
+ <div class="plp-empty">
49
+ <h2 class="plp-empty-title">No products found</h2>
50
+ <p class="plp-empty-text">Try adjusting your filters.</p>
51
+ </div>
52
+ )}
53
+
54
+ <Pagination />
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </BaseLayout>
59
+
60
+ <script>
61
+ import '../../scripts/plp.js'
62
+ </script>
@@ -0,0 +1,37 @@
1
+ ---
2
+ import '../styles/pages/search.css'
3
+ import '../styles/pages/home.css'
4
+ import BaseLayout from '../layouts/BaseLayout.astro'
5
+ ---
6
+
7
+ <BaseLayout title="Search | Nymbal" description="Search our product catalogue.">
8
+ <div class="search-hero">
9
+ <h1 class="search-hero-title">Search</h1>
10
+ <div class="search-bar-wrapper">
11
+ <input
12
+ type="search"
13
+ id="search-input"
14
+ class="search-input-large"
15
+ placeholder="Search for products..."
16
+ aria-label="Search products"
17
+ data-testid="search-page-input"
18
+ autocomplete="off"
19
+ />
20
+ </div>
21
+ </div>
22
+
23
+ <div class="container section">
24
+ <p class="search-query-label" id="search-query-label" style="display:none; margin-bottom: var(--nymbal-spacing-md, 1rem);"></p>
25
+ <div id="search-results" data-testid="search-results">
26
+ <div class="search-empty">
27
+ <div class="search-empty-icon" aria-hidden="true">&#128269;</div>
28
+ <h2 class="search-empty-title">Search our store</h2>
29
+ <p class="search-empty-text">Type a keyword to find products.</p>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </BaseLayout>
34
+
35
+ <script>
36
+ import '../scripts/search.js'
37
+ </script>
@@ -0,0 +1,22 @@
1
+ import { client } from '../lib/client.js'
2
+
3
+ export async function guardAuth(): Promise<boolean> {
4
+ let state = client.auth.getState()
5
+
6
+ if (!state.isAuthenticated) {
7
+ try {
8
+ await client.auth.loadProfile()
9
+ state = client.auth.getState()
10
+ } catch {
11
+ // will redirect below
12
+ }
13
+ }
14
+
15
+ if (!state.isAuthenticated) {
16
+ const redirect = encodeURIComponent(window.location.pathname + window.location.search)
17
+ window.location.href = `/account/login?redirect=${redirect}`
18
+ return false
19
+ }
20
+
21
+ return true
22
+ }
@@ -0,0 +1,89 @@
1
+ import { client } from '../lib/client.js'
2
+
3
+ function getRedirectUrl(): string {
4
+ const params = new URLSearchParams(window.location.search)
5
+ return params.get('redirect') ?? '/account'
6
+ }
7
+
8
+ function showFormError(formId: string, message: string) {
9
+ const errorEl = document.getElementById(`${formId}-error`)
10
+ if (errorEl) {
11
+ errorEl.textContent = message
12
+ errorEl.style.display = 'block'
13
+ }
14
+ }
15
+
16
+ function hideFormError(formId: string) {
17
+ const errorEl = document.getElementById(`${formId}-error`)
18
+ if (errorEl) {
19
+ errorEl.style.display = 'none'
20
+ errorEl.textContent = ''
21
+ }
22
+ }
23
+
24
+ // Login form handler
25
+ const loginForm = document.getElementById('login-form') as HTMLFormElement | null
26
+ loginForm?.addEventListener('submit', async (e) => {
27
+ e.preventDefault()
28
+ hideFormError('login')
29
+
30
+ const email = (document.getElementById('login-email') as HTMLInputElement)?.value
31
+ const password = (document.getElementById('login-password') as HTMLInputElement)?.value
32
+
33
+ if (!email || !password) {
34
+ showFormError('login', 'Please enter your email and password.')
35
+ return
36
+ }
37
+
38
+ const submitBtn = loginForm.querySelector('[data-testid="login-submit"]') as HTMLButtonElement | null
39
+ if (submitBtn) {
40
+ submitBtn.disabled = true
41
+ submitBtn.textContent = 'Signing in...'
42
+ }
43
+
44
+ try {
45
+ await client.auth.login(email, password)
46
+ window.location.href = getRedirectUrl()
47
+ } catch (err: any) {
48
+ showFormError('login', err.message ?? 'Invalid email or password.')
49
+ if (submitBtn) {
50
+ submitBtn.disabled = false
51
+ submitBtn.textContent = 'Sign In'
52
+ }
53
+ }
54
+ })
55
+
56
+ // Register form handler
57
+ const registerForm = document.getElementById('register-form') as HTMLFormElement | null
58
+ registerForm?.addEventListener('submit', async (e) => {
59
+ e.preventDefault()
60
+ hideFormError('register')
61
+
62
+ const email = (document.getElementById('register-email') as HTMLInputElement)?.value
63
+ const password = (document.getElementById('register-password') as HTMLInputElement)?.value
64
+ const firstName = (document.getElementById('register-firstName') as HTMLInputElement)?.value
65
+ const lastName = (document.getElementById('register-lastName') as HTMLInputElement)?.value
66
+ const phone = (document.getElementById('register-phone') as HTMLInputElement)?.value
67
+
68
+ if (!email || !password) {
69
+ showFormError('register', 'Please enter your email and password.')
70
+ return
71
+ }
72
+
73
+ const submitBtn = registerForm.querySelector('[data-testid="register-submit"]') as HTMLButtonElement | null
74
+ if (submitBtn) {
75
+ submitBtn.disabled = true
76
+ submitBtn.textContent = 'Creating account...'
77
+ }
78
+
79
+ try {
80
+ await client.auth.register({ email, password, firstName, lastName, phone })
81
+ window.location.href = getRedirectUrl()
82
+ } catch (err: any) {
83
+ showFormError('register', err.message ?? 'Registration failed. Please try again.')
84
+ if (submitBtn) {
85
+ submitBtn.disabled = false
86
+ submitBtn.textContent = 'Create Account'
87
+ }
88
+ }
89
+ })
@@ -0,0 +1,143 @@
1
+ import { client } from '../lib/client.js'
2
+ import { formatPrice } from '../lib/format.js'
3
+
4
+ const container = document.getElementById('cart-content')
5
+ if (!container) throw new Error('Missing #cart-content')
6
+
7
+ function renderCart() {
8
+ const state = client.cart.getState()
9
+ const items = state.items ?? []
10
+ const currency = state.currency || 'GBP'
11
+
12
+ if (items.length === 0) {
13
+ container!.innerHTML = `
14
+ <div class="cart-empty">
15
+ <h1 class="cart-empty-title">Your cart is empty</h1>
16
+ <p class="cart-empty-text">Looks like you haven't added anything yet.</p>
17
+ <a href="/products" class="btn btn-primary" data-testid="continue-shopping">Continue Shopping</a>
18
+ </div>
19
+ `
20
+ return
21
+ }
22
+
23
+ const subtotal = items.reduce((sum: number, item: any) => sum + item.priceMinor * item.qty, 0)
24
+
25
+ const itemsHtml = items
26
+ .map(
27
+ (item: any) => `
28
+ <div class="cart-line-item" data-testid="cart-item-${item.variantId}">
29
+ <img
30
+ src="${item.imageUrl || `https://picsum.photos/seed/${item.productId}/200/200`}"
31
+ alt="${item.productName}"
32
+ class="cart-line-image"
33
+ width="100"
34
+ height="100"
35
+ loading="lazy"
36
+ />
37
+ <div class="cart-line-info">
38
+ <a href="/product/${item.productId}" class="cart-line-name">${item.productName}</a>
39
+ ${item.variantName ? `<span class="cart-line-variant">${item.variantName}</span>` : ''}
40
+ <span class="cart-line-price-each">${formatPrice(item.priceMinor, currency)} each</span>
41
+ <button
42
+ type="button"
43
+ class="cart-line-remove"
44
+ data-variant-id="${item.variantId}"
45
+ data-testid="cart-remove-${item.variantId}"
46
+ >
47
+ Remove
48
+ </button>
49
+ </div>
50
+ <div class="cart-line-quantity">
51
+ <button
52
+ type="button"
53
+ class="cart-line-quantity-btn"
54
+ data-action="decrement"
55
+ data-variant-id="${item.variantId}"
56
+ aria-label="Decrease quantity"
57
+ >-</button>
58
+ <span class="cart-line-quantity-value">${item.qty}</span>
59
+ <button
60
+ type="button"
61
+ class="cart-line-quantity-btn"
62
+ data-action="increment"
63
+ data-variant-id="${item.variantId}"
64
+ aria-label="Increase quantity"
65
+ >+</button>
66
+ </div>
67
+ <span class="cart-line-total">${formatPrice(item.priceMinor * item.qty, currency)}</span>
68
+ </div>
69
+ `,
70
+ )
71
+ .join('')
72
+
73
+ container!.innerHTML = `
74
+ <div class="cart-layout">
75
+ <div>
76
+ <div class="cart-header">
77
+ <h1 class="cart-title">Your Cart <span class="cart-item-count">(${items.length} ${items.length === 1 ? 'item' : 'items'})</span></h1>
78
+ </div>
79
+ <div class="cart-items" data-testid="cart-items">
80
+ ${itemsHtml}
81
+ </div>
82
+ </div>
83
+ <div class="cart-summary" data-testid="cart-summary">
84
+ <h2 class="cart-summary-title">Order Summary</h2>
85
+ <dl>
86
+ <div class="cart-summary-row">
87
+ <dt>Subtotal</dt>
88
+ <dd>${formatPrice(subtotal, currency)}</dd>
89
+ </div>
90
+ <div class="cart-summary-row">
91
+ <dt>Shipping</dt>
92
+ <dd>Calculated at checkout</dd>
93
+ </div>
94
+ </dl>
95
+ <div class="cart-summary-total">
96
+ <span>Estimated Total</span>
97
+ <span>${formatPrice(subtotal, currency)}</span>
98
+ </div>
99
+ <div class="cart-summary-actions">
100
+ <a href="/checkout" class="btn btn-primary" data-testid="checkout-btn">Proceed to Checkout</a>
101
+ <a href="/products" class="btn btn-secondary">Continue Shopping</a>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ `
106
+
107
+ bindCartActions()
108
+ }
109
+
110
+ function bindCartActions() {
111
+ container!.querySelectorAll('.cart-line-remove').forEach((btn) => {
112
+ btn.addEventListener('click', () => {
113
+ const variantId = (btn as HTMLElement).dataset.variantId
114
+ if (variantId) client.cart.removeItem(variantId)
115
+ })
116
+ })
117
+
118
+ container!.querySelectorAll('.cart-line-quantity-btn').forEach((btn) => {
119
+ btn.addEventListener('click', () => {
120
+ const el = btn as HTMLElement
121
+ const variantId = el.dataset.variantId
122
+ const action = el.dataset.action
123
+ if (!variantId) return
124
+
125
+ const state = client.cart.getState()
126
+ const item = (state.items ?? []).find((i: any) => i.variantId === variantId)
127
+ if (!item) return
128
+
129
+ if (action === 'increment') {
130
+ client.cart.updateQuantity(variantId, item.qty + 1)
131
+ } else if (action === 'decrement') {
132
+ if (item.qty <= 1) {
133
+ client.cart.removeItem(variantId)
134
+ } else {
135
+ client.cart.updateQuantity(variantId, item.qty - 1)
136
+ }
137
+ }
138
+ })
139
+ })
140
+ }
141
+
142
+ client.cart.subscribe(renderCart)
143
+ renderCart()