create-nymbal-app 0.3.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 (167) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +14 -12
  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 -27
  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 +126 -20
  13. package/scaffold/templates/astro/src/lib/chrome.ts +59 -0
  14. package/scaffold/templates/astro/src/lib/client.ts +4 -2
  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 +41 -26
  24. package/scaffold/templates/astro/src/pages/checkout.astro +34 -90
  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 +46 -29
  32. package/scaffold/templates/astro/src/pages/products/index.astro +40 -26
  33. package/scaffold/templates/astro/src/pages/search.astro +13 -8
  34. package/scaffold/templates/astro/src/scripts/init-client.ts +14 -0
  35. package/scaffold/templates/astro/src/scripts/pdp.ts +47 -53
  36. package/scaffold/templates/astro/src/scripts/plp.ts +21 -98
  37. package/scaffold/templates/astro/src/scripts/search.ts +19 -120
  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 -167
  44. package/scaffold/templates/astro/src/styles/pages/plp.css +3 -42
  45. package/scaffold/templates/astro/src/styles/pages/search.css +13 -100
  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 +64 -28
  82. package/scaffold/templates/nextjs/components/client/elements.tsx +15 -0
  83. package/scaffold/templates/nextjs/components/server/Header.tsx +33 -97
  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 +162 -46
  88. package/scaffold/templates/nextjs/lib/chrome.ts +40 -0
  89. package/scaffold/templates/nextjs/lib/client.ts +3 -0
  90. package/scaffold/templates/nextjs/lib/fragments.ts +16 -0
  91. package/scaffold/templates/nextjs/lib/header.ts +39 -0
  92. package/scaffold/templates/nextjs/lib/render.ts +14 -0
  93. package/scaffold/templates/nextjs/next.config.mjs +32 -0
  94. package/scaffold/templates/nextjs/package.json +28 -29
  95. package/scaffold/templates/nextjs/styles/pages/account.module.css +0 -70
  96. package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +6 -85
  97. package/scaffold/templates/nextjs/styles/pages/home.module.css +2 -103
  98. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +3 -179
  99. package/scaffold/templates/nextjs/styles/pages/plp.module.css +8 -141
  100. package/scaffold/templates/nextjs/styles/pages/search.module.css +8 -128
  101. package/scaffold/templates/nextjs/tsconfig.dist.json +5 -0
  102. package/scaffold/templates/astro/src/components/AddressForm.astro +0 -107
  103. package/scaffold/templates/astro/src/components/Breadcrumb.astro +0 -20
  104. package/scaffold/templates/astro/src/components/CategoryNav.astro +0 -27
  105. package/scaffold/templates/astro/src/components/CheckoutSteps.astro +0 -29
  106. package/scaffold/templates/astro/src/components/Footer.astro +0 -108
  107. package/scaffold/templates/astro/src/components/Header.astro +0 -192
  108. package/scaffold/templates/astro/src/components/ImageGallery.astro +0 -42
  109. package/scaffold/templates/astro/src/components/OrderSummary.astro +0 -53
  110. package/scaffold/templates/astro/src/components/Pagination.astro +0 -13
  111. package/scaffold/templates/astro/src/components/PriceDisplay.astro +0 -25
  112. package/scaffold/templates/astro/src/components/ProductCard.astro +0 -44
  113. package/scaffold/templates/astro/src/components/RelatedProducts.astro +0 -28
  114. package/scaffold/templates/astro/src/components/ReviewForm.astro +0 -74
  115. package/scaffold/templates/astro/src/components/ReviewList.astro +0 -41
  116. package/scaffold/templates/astro/src/components/SortDropdown.astro +0 -26
  117. package/scaffold/templates/astro/src/components/StockBadge.astro +0 -32
  118. package/scaffold/templates/astro/src/scripts/auth-forms.ts +0 -82
  119. package/scaffold/templates/astro/src/scripts/cart-page.ts +0 -143
  120. package/scaffold/templates/astro/src/scripts/checkout.ts +0 -272
  121. package/scaffold/templates/astro/src/scripts/image-gallery.ts +0 -22
  122. package/scaffold/templates/astro/src/scripts/profile-form.ts +0 -57
  123. package/scaffold/templates/astro/src/scripts/review-form.ts +0 -27
  124. package/scaffold/templates/astro/src/styles/pages/cart.css +0 -243
  125. package/scaffold/templates/astro/src/styles/pages/checkout.css +0 -270
  126. package/scaffold/templates/nextjs/app/account/addresses/page.tsx +0 -10
  127. package/scaffold/templates/nextjs/app/account/login/page.tsx +0 -23
  128. package/scaffold/templates/nextjs/app/account/profile/page.tsx +0 -10
  129. package/scaffold/templates/nextjs/app/account/register/page.tsx +0 -10
  130. package/scaffold/templates/nextjs/app/cart/page.tsx +0 -10
  131. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +0 -58
  132. package/scaffold/templates/nextjs/app/checkout/page.tsx +0 -10
  133. package/scaffold/templates/nextjs/app/page.tsx +0 -63
  134. package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +0 -111
  135. package/scaffold/templates/nextjs/app/products/page.tsx +0 -40
  136. package/scaffold/templates/nextjs/app/search/page.tsx +0 -24
  137. package/scaffold/templates/nextjs/components/client/AddressManager.tsx +0 -181
  138. package/scaffold/templates/nextjs/components/client/CartPage.tsx +0 -120
  139. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +0 -523
  140. package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +0 -68
  141. package/scaffold/templates/nextjs/components/client/LoginForm.tsx +0 -89
  142. package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +0 -12
  143. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +0 -148
  144. package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +0 -112
  145. package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +0 -131
  146. package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +0 -116
  147. package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +0 -27
  148. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +0 -141
  149. package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +0 -35
  150. package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +0 -36
  151. package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +0 -29
  152. package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +0 -60
  153. package/scaffold/templates/nextjs/components/server/Footer.module.css +0 -64
  154. package/scaffold/templates/nextjs/components/server/Footer.tsx +0 -79
  155. package/scaffold/templates/nextjs/components/server/Header.module.css +0 -147
  156. package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +0 -79
  157. package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +0 -31
  158. package/scaffold/templates/nextjs/components/server/ProductCard.tsx +0 -59
  159. package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +0 -31
  160. package/scaffold/templates/nextjs/components/server/ReviewList.tsx +0 -62
  161. package/scaffold/templates/nextjs/components/server/StockBadge.tsx +0 -19
  162. package/scaffold/templates/nextjs/styles/pages/cart.module.css +0 -240
  163. package/scaffold/templates/nextjs/styles/pages/checkout.module.css +0 -404
  164. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/layout.tsx +0 -0
  165. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/[orderNumber]/page.tsx +0 -0
  166. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/page.tsx +0 -0
  167. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/page.tsx +0 -0
@@ -1,143 +0,0 @@
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()
@@ -1,272 +0,0 @@
1
- import { client } from '../lib/client.js'
2
- import { formatPrice } from '../lib/format.js'
3
- import { submitOnce } from '../lib/submit-once.js'
4
-
5
- const steps = ['contact', 'shipping', 'payment'] as const
6
- type Step = (typeof steps)[number]
7
-
8
- let currentStep: Step = 'contact'
9
-
10
- const stepsEl = document.getElementById('checkout-steps-container')
11
- const contactSection = document.getElementById('checkout-contact')
12
- const shippingSection = document.getElementById('checkout-shipping')
13
- const paymentSection = document.getElementById('checkout-payment')
14
- const summaryEl = document.getElementById('checkout-summary')
15
- const errorEl = document.getElementById('checkout-error')
16
-
17
- function showStep(step: Step) {
18
- currentStep = step
19
- const sections: Record<Step, HTMLElement | null> = {
20
- contact: contactSection,
21
- shipping: shippingSection,
22
- payment: paymentSection,
23
- }
24
- steps.forEach((s) => {
25
- const el = sections[s]
26
- if (el) el.style.display = s === step ? '' : 'none'
27
- })
28
- updateStepIndicator()
29
- }
30
-
31
- function updateStepIndicator() {
32
- if (!stepsEl) return
33
- const currentIndex = steps.indexOf(currentStep)
34
- steps.forEach((step, index) => {
35
- const stepEl = stepsEl.querySelector(`[data-testid="checkout-step-${step}"]`)
36
- const dividerBefore = stepEl?.previousElementSibling
37
- if (!stepEl) return
38
- stepEl.className = 'checkout-step'
39
- if (index < currentIndex) stepEl.classList.add('completed')
40
- else if (index === currentIndex) stepEl.classList.add('active')
41
- if (dividerBefore?.classList.contains('checkout-step-divider')) {
42
- dividerBefore.className = 'checkout-step-divider'
43
- if (index <= currentIndex) dividerBefore.classList.add('completed')
44
- }
45
- })
46
- }
47
-
48
- function showError(message: string) {
49
- if (errorEl) {
50
- errorEl.textContent = message
51
- errorEl.style.display = 'block'
52
- }
53
- }
54
-
55
- function hideError() {
56
- if (errorEl) {
57
- errorEl.style.display = 'none'
58
- errorEl.textContent = ''
59
- }
60
- }
61
-
62
- function getFormValues(form: HTMLFormElement): Record<string, string> {
63
- const data = new FormData(form)
64
- const result: Record<string, string> = {}
65
- data.forEach((value, key) => {
66
- result[key] = String(value)
67
- })
68
- return result
69
- }
70
-
71
- function renderSummary() {
72
- if (!summaryEl) return
73
- const cartState = client.cart.getState()
74
- const items = cartState.items ?? []
75
- const currency = cartState.currency ?? 'GBP'
76
-
77
- summaryEl.innerHTML = `
78
- <h2 class="checkout-summary-title">Order Summary</h2>
79
- <div class="checkout-summary-items">
80
- ${items
81
- .map(
82
- (item) => `
83
- <div class="checkout-summary-item">
84
- <span class="checkout-summary-item-name">${item.productName}</span>
85
- <span class="checkout-summary-item-qty">x${item.qty}</span>
86
- <span class="checkout-summary-item-price">${formatPrice(item.priceMinor * item.qty, currency)}</span>
87
- </div>
88
- `,
89
- )
90
- .join('')}
91
- </div>
92
- <dl>
93
- <div class="checkout-summary-row">
94
- <dt>Subtotal</dt>
95
- <dd>${formatPrice(cartState.subtotalMinor, currency)}</dd>
96
- </div>
97
- <div class="checkout-summary-row">
98
- <dt>Shipping</dt>
99
- <dd>Calculated next</dd>
100
- </div>
101
- </dl>
102
- <div class="checkout-summary-total">
103
- <span>Total</span>
104
- <span>${formatPrice(cartState.subtotalMinor, currency)}</span>
105
- </div>
106
- `
107
- }
108
-
109
- // Contact form
110
- const contactForm = document.getElementById('contact-form') as HTMLFormElement | null
111
- contactForm?.addEventListener('submit', (e) => {
112
- e.preventDefault()
113
- hideError()
114
- const values = getFormValues(contactForm)
115
- if (!values.email) {
116
- showError('Please enter your email address.')
117
- return
118
- }
119
- client.checkout.setEmail(values.email)
120
- showStep('shipping')
121
- })
122
-
123
- // Shipping form
124
- const shippingForm = document.getElementById('shipping-form') as HTMLFormElement | null
125
- shippingForm?.addEventListener('submit', (e) => {
126
- e.preventDefault()
127
- hideError()
128
- const values = getFormValues(shippingForm)
129
-
130
- if (!values.addressLine1 || !values.city || !values.postalCode || !values.country) {
131
- showError('Please complete your shipping address.')
132
- return
133
- }
134
-
135
- client.checkout.setShippingAddress({
136
- firstName: values.firstName ?? '',
137
- lastName: values.lastName ?? '',
138
- addressLine1: values.addressLine1,
139
- addressLine2: values.addressLine2 || undefined,
140
- city: values.city,
141
- region: values.region ?? '',
142
- postalCode: values.postalCode,
143
- country: values.country,
144
- })
145
- client.checkout.setShippingMethod({
146
- carrier: 'Standard',
147
- service: 'Standard Shipping',
148
- amountMinor: 0,
149
- currency: client.cart.getState().currency ?? 'GBP',
150
- estimatedDays: 5,
151
- })
152
- showStep('payment')
153
- })
154
-
155
- // Stripe setup. The env says only whether a Stripe client exists; the payment flow follows the server's answer.
156
- let stripe: any = null
157
- let cardElement: any = null
158
-
159
- const stripeKey = import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY
160
- const isStubPayments =
161
- import.meta.env.PUBLIC_NYMBAL_PAYMENTS_PROVIDER === 'native-stub' || !stripeKey
162
-
163
- async function mountStripe() {
164
- if (isStubPayments) {
165
- const cardContainer = document.getElementById('stripe-card-element')
166
- if (cardContainer) {
167
- cardContainer.innerHTML =
168
- '<p style="color: var(--nymbal-color-text-muted, #71717a); font-size: 0.875rem; margin: 0;">Test mode — payment skipped</p>'
169
- }
170
- return
171
- }
172
- try {
173
- const { loadStripe } = await import('@stripe/stripe-js')
174
- stripe = await loadStripe(stripeKey)
175
- if (!stripe) return
176
-
177
- const elements = stripe.elements()
178
- cardElement = elements.create('card', {
179
- style: {
180
- base: {
181
- fontSize: '14px',
182
- color: '#18181b',
183
- '::placeholder': { color: '#71717a' },
184
- },
185
- },
186
- })
187
-
188
- const cardContainer = document.getElementById('stripe-card-element')
189
- if (cardContainer) cardElement.mount(cardContainer)
190
- } catch {
191
- // Stripe unavailable
192
- }
193
- }
194
-
195
- mountStripe()
196
-
197
- // Payment form
198
- const paymentForm = document.getElementById('payment-form') as HTMLFormElement | null
199
- submitOnce(paymentForm, async () => {
200
- hideError()
201
-
202
- const submitBtn = paymentForm?.querySelector(
203
- '[data-testid="place-order"]',
204
- ) as HTMLButtonElement | null
205
- if (submitBtn) submitBtn.textContent = 'Processing...'
206
- const stay = (message: string) => {
207
- showError(message)
208
- if (submitBtn) submitBtn.textContent = 'Place Order'
209
- }
210
- const leaveFor = (orderNumber: string) => {
211
- client.checkout.reset()
212
- window.location.href = `/order/${encodeURIComponent(orderNumber)}`
213
- return 'leaving' as const
214
- }
215
-
216
- try {
217
- const sameAsShipping = (document.getElementById('billing-same') as HTMLInputElement)?.checked
218
- if (sameAsShipping) {
219
- const shippingAddr = client.checkout.getState().shippingAddress
220
- if (shippingAddr) client.checkout.setBillingAddress(shippingAddr)
221
- } else {
222
- const billingForm = document.getElementById('billing-form') as HTMLFormElement
223
- const values = getFormValues(billingForm)
224
- client.checkout.setBillingAddress({
225
- firstName: values.firstName ?? '',
226
- lastName: values.lastName ?? '',
227
- addressLine1: values.addressLine1,
228
- addressLine2: values.addressLine2 || undefined,
229
- city: values.city,
230
- region: values.region ?? '',
231
- postalCode: values.postalCode,
232
- country: values.country,
233
- })
234
- }
235
-
236
- const result = await client.checkout.submitPayment()
237
- if (result === null) {
238
- // A resubmit of a cart that already became an order (Back, then Place Order) goes to that order.
239
- const { placedOrderNumber, error } = client.checkout.getState()
240
- if (placedOrderNumber) return leaveFor(placedOrderNumber)
241
- return stay(error ?? 'Payment failed. Please try again.')
242
- }
243
- if (result.payment.status === 'failed') {
244
- return stay(client.checkout.getState().error ?? 'Payment failed. Please try again.')
245
- }
246
- // Without a Stripe client (a +stub-pending order) the order page shows it awaiting payment.
247
- if (result.payment.status === 'requires_action' && stripe && cardElement) {
248
- const { error } = await stripe.confirmCardPayment(result.paymentIntent.clientSecret, {
249
- payment_method: { card: cardElement },
250
- })
251
- if (error) return stay(error.message ?? 'Payment failed.')
252
- client.checkout.markConfirmed()
253
- }
254
- return leaveFor(result.orderNumber)
255
- } catch (err: any) {
256
- stay(err.message ?? 'Payment failed. Please try again.')
257
- }
258
- })
259
-
260
- // Billing same-as-shipping toggle
261
- const billingSameCheckbox = document.getElementById('billing-same') as HTMLInputElement | null
262
- const billingFields = document.getElementById('billing-fields')
263
- billingSameCheckbox?.addEventListener('change', () => {
264
- if (billingFields) {
265
- billingFields.style.display = billingSameCheckbox.checked ? 'none' : ''
266
- }
267
- })
268
-
269
- // Init
270
- showStep('contact')
271
- renderSummary()
272
- client.cart.subscribe(renderSummary)
@@ -1,22 +0,0 @@
1
- const thumbnails = document.getElementById('gallery-thumbnails')
2
- const mainImage = document.getElementById('gallery-main-image') as HTMLImageElement | null
3
-
4
- if (thumbnails && mainImage) {
5
- thumbnails.addEventListener('click', (e) => {
6
- const target = e.target as HTMLElement
7
- if (!target.classList.contains('pdp-thumbnail')) return
8
-
9
- const url = target.dataset.url
10
- const alt = target.dataset.alt
11
-
12
- if (url) {
13
- mainImage.src = url
14
- mainImage.alt = alt ?? ''
15
- }
16
-
17
- thumbnails
18
- .querySelectorAll('.pdp-thumbnail')
19
- .forEach((t) => t.classList.remove('active'))
20
- target.classList.add('active')
21
- })
22
- }
@@ -1,57 +0,0 @@
1
- import { client } from '../lib/client.js'
2
- import { submitOnce } from '../lib/submit-once.js'
3
-
4
- const form = document.getElementById('profile-form') as HTMLFormElement | null
5
- const messageEl = document.getElementById('profile-message')
6
-
7
- function populateForm() {
8
- const profile = client.auth.getState().customer
9
- if (!profile) return
10
-
11
- const firstNameEl = document.getElementById('profile-firstName') as HTMLInputElement
12
- const lastNameEl = document.getElementById('profile-lastName') as HTMLInputElement
13
- const emailEl = document.getElementById('profile-email') as HTMLInputElement
14
- const phoneEl = document.getElementById('profile-phone') as HTMLInputElement
15
-
16
- if (firstNameEl) firstNameEl.value = profile.firstName ?? ''
17
- if (lastNameEl) lastNameEl.value = profile.lastName ?? ''
18
- if (emailEl) emailEl.value = profile.email ?? ''
19
- if (phoneEl) phoneEl.value = profile.phone ?? ''
20
- }
21
-
22
- // Load profile data
23
- async function init() {
24
- try {
25
- await client.auth.loadProfile()
26
- } catch {
27
- // ignore
28
- }
29
- populateForm()
30
- }
31
-
32
- client.auth.subscribe(populateForm)
33
- init()
34
-
35
- submitOnce(form, async () => {
36
- if (!form || !messageEl) return
37
-
38
- const firstName = (document.getElementById('profile-firstName') as HTMLInputElement)?.value
39
- const lastName = (document.getElementById('profile-lastName') as HTMLInputElement)?.value
40
- const phone = (document.getElementById('profile-phone') as HTMLInputElement)?.value
41
-
42
- const submitBtn = form.querySelector('[data-testid="profile-save"]') as HTMLButtonElement | null
43
- if (submitBtn) submitBtn.textContent = 'Saving...'
44
-
45
- try {
46
- await client.auth.updateProfile({ firstName, lastName, phone })
47
- messageEl.textContent = 'Profile updated successfully.'
48
- messageEl.style.color = 'var(--nymbal-color-success, #16a34a)'
49
- messageEl.style.display = 'block'
50
- } catch (err) {
51
- messageEl.textContent = err instanceof Error ? err.message : 'Failed to update profile.'
52
- messageEl.style.color = 'var(--nymbal-color-error, #dc2626)'
53
- messageEl.style.display = 'block'
54
- } finally {
55
- if (submitBtn) submitBtn.textContent = 'Save Changes'
56
- }
57
- })
@@ -1,27 +0,0 @@
1
- import { client } from '../lib/client.js'
2
- import { submitOnce } from '../lib/submit-once.js'
3
-
4
- const form = document.getElementById('review-form') as HTMLFormElement | null
5
- const messageEl = document.getElementById('review-form-message')
6
-
7
- submitOnce(form, async () => {
8
- if (!form || !messageEl) return
9
-
10
- const productId = form.dataset.productId ?? ''
11
- const formData = new FormData(form)
12
- const rating = Number(formData.get('rating'))
13
- const title = String(formData.get('title'))
14
- const body = String(formData.get('body'))
15
-
16
- try {
17
- await client.adapter.reviews.submit({ productId, rating, title, body })
18
- messageEl.textContent = 'Thank you for your review!'
19
- messageEl.style.display = 'block'
20
- messageEl.style.color = 'var(--nymbal-color-success, #16a34a)'
21
- form.reset()
22
- } catch {
23
- messageEl.textContent = 'Something went wrong. Please try again.'
24
- messageEl.style.display = 'block'
25
- messageEl.style.color = 'var(--nymbal-color-error, #dc2626)'
26
- }
27
- })