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,118 +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
- <nymbal-quantity-selector
52
- value="${item.qty}"
53
- min="1"
54
- max="99"
55
- data-variant-id="${item.variantId}"
56
- ></nymbal-quantity-selector>
57
- </div>
58
- <span class="cart-line-total">${formatPrice(item.priceMinor * item.qty, currency)}</span>
59
- </div>
60
- `,
61
- )
62
- .join('')
63
-
64
- container!.innerHTML = `
65
- <div class="cart-layout">
66
- <div>
67
- <div class="cart-header">
68
- <h1 class="cart-title">Your Cart <span class="cart-item-count">(${items.length} ${items.length === 1 ? 'item' : 'items'})</span></h1>
69
- </div>
70
- <div class="cart-items" data-testid="cart-items">
71
- ${itemsHtml}
72
- </div>
73
- </div>
74
- <div class="cart-summary" data-testid="cart-summary">
75
- <h2 class="cart-summary-title">Order Summary</h2>
76
- <dl>
77
- <div class="cart-summary-row">
78
- <dt>Subtotal</dt>
79
- <dd>${formatPrice(subtotal, currency)}</dd>
80
- </div>
81
- <div class="cart-summary-row">
82
- <dt>Shipping</dt>
83
- <dd>Calculated at checkout</dd>
84
- </div>
85
- </dl>
86
- <div class="cart-summary-total">
87
- <span>Estimated Total</span>
88
- <span>${formatPrice(subtotal, currency)}</span>
89
- </div>
90
- <div class="cart-summary-actions">
91
- <a href="/checkout" class="btn btn-primary" data-testid="checkout-btn">Proceed to Checkout</a>
92
- <a href="/products" class="btn btn-secondary">Continue Shopping</a>
93
- </div>
94
- </div>
95
- </div>
96
- `
97
-
98
- bindCartActions()
99
- }
100
-
101
- function bindCartActions() {
102
- container!.querySelectorAll('.cart-line-remove').forEach((btn) => {
103
- btn.addEventListener('click', () => {
104
- const variantId = (btn as HTMLElement).dataset.variantId
105
- if (variantId) client.cart.removeItem(variantId)
106
- })
107
- })
108
-
109
- container!.querySelectorAll<HTMLElement>('nymbal-quantity-selector').forEach((selector) => {
110
- selector.addEventListener('nymbal:quantity:changed', ((e: CustomEvent<{ value: number }>) => {
111
- const variantId = selector.dataset.variantId
112
- if (variantId) client.cart.updateQuantity(variantId, e.detail.value)
113
- }) as EventListener)
114
- })
115
- }
116
-
117
- client.cart.subscribe(renderCart)
118
- renderCart()
@@ -1,271 +0,0 @@
1
- import type { PaymentConfirmContext, PaymentConfirmation } from '@nymbalhq/sdk'
2
- import type { NymbalPaymentElement } from '@nymbalhq/web-components'
3
- import { client } from '../lib/client.js'
4
- import { formatPrice } from '../lib/format.js'
5
- import { submitOnce } from '../lib/submit-once.js'
6
-
7
- const steps = ['contact', 'shipping', 'payment'] as const
8
- type Step = (typeof steps)[number]
9
-
10
- let currentStep: Step = 'contact'
11
-
12
- const stepsEl = document.getElementById('checkout-steps-container')
13
- const contactSection = document.getElementById('checkout-contact')
14
- const shippingSection = document.getElementById('checkout-shipping')
15
- const paymentSection = document.getElementById('checkout-payment')
16
- const summaryEl = document.getElementById('checkout-summary')
17
- const errorEl = document.getElementById('checkout-error')
18
-
19
- function showStep(step: Step) {
20
- currentStep = step
21
- const sections: Record<Step, HTMLElement | null> = {
22
- contact: contactSection,
23
- shipping: shippingSection,
24
- payment: paymentSection,
25
- }
26
- steps.forEach((s) => {
27
- const el = sections[s]
28
- if (el) el.style.display = s === step ? '' : 'none'
29
- })
30
- updateStepIndicator()
31
- }
32
-
33
- function updateStepIndicator() {
34
- if (!stepsEl) return
35
- const currentIndex = steps.indexOf(currentStep)
36
- steps.forEach((step, index) => {
37
- const stepEl = stepsEl.querySelector(`[data-testid="checkout-step-${step}"]`)
38
- const dividerBefore = stepEl?.previousElementSibling
39
- if (!stepEl) return
40
- stepEl.className = 'checkout-step'
41
- if (index < currentIndex) stepEl.classList.add('completed')
42
- else if (index === currentIndex) stepEl.classList.add('active')
43
- if (dividerBefore?.classList.contains('checkout-step-divider')) {
44
- dividerBefore.className = 'checkout-step-divider'
45
- if (index <= currentIndex) dividerBefore.classList.add('completed')
46
- }
47
- })
48
- }
49
-
50
- function showError(message: string) {
51
- if (errorEl) {
52
- errorEl.textContent = message
53
- errorEl.style.display = 'block'
54
- }
55
- }
56
-
57
- function hideError() {
58
- if (errorEl) {
59
- errorEl.style.display = 'none'
60
- errorEl.textContent = ''
61
- }
62
- }
63
-
64
- function getFormValues(form: HTMLFormElement): Record<string, string> {
65
- const data = new FormData(form)
66
- const result: Record<string, string> = {}
67
- data.forEach((value, key) => {
68
- result[key] = String(value)
69
- })
70
- return result
71
- }
72
-
73
- function renderSummary() {
74
- if (!summaryEl) return
75
- const cartState = client.cart.getState()
76
- const items = cartState.items ?? []
77
- const currency = cartState.currency ?? 'GBP'
78
-
79
- summaryEl.innerHTML = `
80
- <h2 class="checkout-summary-title">Order Summary</h2>
81
- <div class="checkout-summary-items">
82
- ${items
83
- .map(
84
- (item) => `
85
- <div class="checkout-summary-item">
86
- <span class="checkout-summary-item-name">${item.productName}</span>
87
- <span class="checkout-summary-item-qty">x${item.qty}</span>
88
- <span class="checkout-summary-item-price">${formatPrice(item.priceMinor * item.qty, currency)}</span>
89
- </div>
90
- `,
91
- )
92
- .join('')}
93
- </div>
94
- <dl>
95
- <div class="checkout-summary-row">
96
- <dt>Subtotal</dt>
97
- <dd>${formatPrice(cartState.subtotalMinor, currency)}</dd>
98
- </div>
99
- <div class="checkout-summary-row">
100
- <dt>Shipping</dt>
101
- <dd>Calculated next</dd>
102
- </div>
103
- </dl>
104
- <div class="checkout-summary-total">
105
- <span>Total</span>
106
- <span>${formatPrice(cartState.subtotalMinor, currency)}</span>
107
- </div>
108
- `
109
- }
110
-
111
- // Contact form
112
- const contactForm = document.getElementById('contact-form') as HTMLFormElement | null
113
- contactForm?.addEventListener('submit', (e) => {
114
- e.preventDefault()
115
- hideError()
116
- const values = getFormValues(contactForm)
117
- if (!values.email) {
118
- showError('Please enter your email address.')
119
- return
120
- }
121
- client.checkout.setEmail(values.email)
122
- showStep('shipping')
123
- })
124
-
125
- // Shipping form
126
- const shippingForm = document.getElementById('shipping-form') as HTMLFormElement | null
127
- shippingForm?.addEventListener('submit', (e) => {
128
- e.preventDefault()
129
- hideError()
130
- const values = getFormValues(shippingForm)
131
-
132
- if (!values.addressLine1 || !values.city || !values.postalCode || !values.country) {
133
- showError('Please complete your shipping address.')
134
- return
135
- }
136
-
137
- client.checkout.setShippingAddress({
138
- firstName: values.firstName ?? '',
139
- lastName: values.lastName ?? '',
140
- addressLine1: values.addressLine1,
141
- addressLine2: values.addressLine2 || undefined,
142
- city: values.city,
143
- region: values.region ?? '',
144
- postalCode: values.postalCode,
145
- country: values.country,
146
- })
147
- client.checkout.setShippingMethod({
148
- carrier: 'Standard',
149
- service: 'Standard Shipping',
150
- amountMinor: 0,
151
- currency: client.cart.getState().currency ?? 'GBP',
152
- estimatedDays: 5,
153
- })
154
- showStep('payment')
155
- })
156
-
157
- // The payment step is the store's: the element renders the payments adapter's client half through its driver.
158
- const paymentForm = document.getElementById('payment-form') as HTMLFormElement | null
159
- const paymentEl = paymentForm?.querySelector<NymbalPaymentElement>('nymbal-payment-element') ?? null
160
- const placeOrderBtn = paymentForm?.querySelector<HTMLButtonElement>('[data-testid="place-order"]')
161
- const paymentBlocked = () => paymentEl?.status === 'error'
162
-
163
- // While the element cannot take a payment (an unknown driver, a form that failed to load), no order is placed.
164
- let heldByPayment = false
165
- function syncPlaceOrder() {
166
- if (!placeOrderBtn) return
167
- const blocked = paymentBlocked()
168
- if (blocked) placeOrderBtn.disabled = true
169
- else if (heldByPayment) placeOrderBtn.disabled = false
170
- heldByPayment = blocked
171
- }
172
- paymentEl?.addEventListener('nymbal:payment:status', syncPlaceOrder)
173
- syncPlaceOrder()
174
- paymentForm?.addEventListener(
175
- 'submit',
176
- (e) => {
177
- if (!paymentBlocked()) return
178
- e.preventDefault()
179
- e.stopImmediatePropagation()
180
- },
181
- true,
182
- )
183
-
184
- // Place Order renders where Continue was, so the second click of a double-click on Continue lands on it. A click that
185
- // continues a multi-click is not a decision to pay: the first click of one on Place Order has already submitted.
186
- placeOrderBtn?.addEventListener('click', (e) => {
187
- if (e.detail > 1) e.preventDefault()
188
- })
189
-
190
- /** No element (a page without a payment step) has nothing to confirm, as a null client does. */
191
- function confirmPayment(
192
- clientSecret: string,
193
- context: PaymentConfirmContext,
194
- ): Promise<PaymentConfirmation> {
195
- return paymentEl?.confirm(clientSecret, context) ?? Promise.resolve({ ok: true, confirmed: false })
196
- }
197
-
198
- // Payment form
199
- submitOnce(paymentForm, async () => {
200
- hideError()
201
-
202
- if (placeOrderBtn) placeOrderBtn.textContent = 'Processing...'
203
- const stay = (message: string) => {
204
- showError(message)
205
- if (placeOrderBtn) placeOrderBtn.textContent = 'Place Order'
206
- }
207
- const leaveFor = (orderNumber: string) => {
208
- client.checkout.reset()
209
- window.location.href = `/order/${encodeURIComponent(orderNumber)}`
210
- return 'leaving' as const
211
- }
212
-
213
- try {
214
- const sameAsShipping = (document.getElementById('billing-same') as HTMLInputElement)?.checked
215
- if (sameAsShipping) {
216
- const shippingAddr = client.checkout.getState().shippingAddress
217
- if (shippingAddr) client.checkout.setBillingAddress(shippingAddr)
218
- } else {
219
- const billingForm = document.getElementById('billing-form') as HTMLFormElement
220
- const values = getFormValues(billingForm)
221
- client.checkout.setBillingAddress({
222
- firstName: values.firstName ?? '',
223
- lastName: values.lastName ?? '',
224
- addressLine1: values.addressLine1,
225
- addressLine2: values.addressLine2 || undefined,
226
- city: values.city,
227
- region: values.region ?? '',
228
- postalCode: values.postalCode,
229
- country: values.country,
230
- })
231
- }
232
-
233
- const result = await client.checkout.submitPayment()
234
- if (result === null) {
235
- // A resubmit of a cart that already became an order (Back, then Place Order) goes to that order.
236
- const { placedOrderNumber, error } = client.checkout.getState()
237
- if (placedOrderNumber) return leaveFor(placedOrderNumber)
238
- return stay(error ?? 'Payment failed. Please try again.')
239
- }
240
- if (result.payment.status === 'failed') {
241
- return stay(client.checkout.getState().error ?? 'Payment failed. Please try again.')
242
- }
243
- // Confirmed only when the driver says the payment went through; otherwise the order page shows it awaiting payment.
244
- if (result.payment.status === 'requires_action') {
245
- const { email, billingAddress } = client.checkout.getState()
246
- const confirmation = await confirmPayment(result.paymentIntent.clientSecret, {
247
- email: email || undefined,
248
- billing: billingAddress ?? undefined,
249
- })
250
- if (!confirmation.ok) return stay(confirmation.error)
251
- if (confirmation.confirmed) client.checkout.markConfirmed()
252
- }
253
- return leaveFor(result.orderNumber)
254
- } catch (err: any) {
255
- stay(err.message ?? 'Payment failed. Please try again.')
256
- }
257
- })
258
-
259
- // Billing same-as-shipping toggle
260
- const billingSameCheckbox = document.getElementById('billing-same') as HTMLInputElement | null
261
- const billingFields = document.getElementById('billing-fields')
262
- billingSameCheckbox?.addEventListener('change', () => {
263
- if (billingFields) {
264
- billingFields.style.display = billingSameCheckbox.checked ? 'none' : ''
265
- }
266
- })
267
-
268
- // Init
269
- showStep('contact')
270
- renderSummary()
271
- 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
- })