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.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/scaffold/project/astro/src/pages/index.astro +13 -15
- package/scaffold/templates/astro/_dot.node-version +1 -1
- package/scaffold/templates/astro/astro.config.mjs +23 -0
- package/scaffold/templates/astro/package.json +26 -26
- package/scaffold/templates/astro/src/components/SkipNav.astro +3 -1
- package/scaffold/templates/astro/src/elements/index.ts +11 -0
- package/scaffold/templates/astro/src/layouts/BaseLayout.astro +15 -4
- package/scaffold/templates/astro/src/lib/api.ts +113 -21
- package/scaffold/templates/astro/src/lib/chrome.ts +59 -0
- package/scaffold/templates/astro/src/lib/client.ts +4 -4
- package/scaffold/templates/astro/src/lib/fragments.ts +48 -0
- package/scaffold/templates/astro/src/lib/server-elements.ts +5 -0
- package/scaffold/templates/astro/src/middleware.ts +37 -0
- package/scaffold/templates/astro/src/pages/account/addresses.astro +5 -97
- package/scaffold/templates/astro/src/pages/account/login.astro +7 -34
- package/scaffold/templates/astro/src/pages/account/profile.astro +6 -56
- package/scaffold/templates/astro/src/pages/account/register.astro +7 -73
- package/scaffold/templates/astro/src/pages/cart.astro +14 -7
- package/scaffold/templates/astro/src/pages/category/[slug].astro +17 -21
- package/scaffold/templates/astro/src/pages/checkout.astro +30 -94
- package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -2
- package/scaffold/templates/astro/src/pages/fragments/header.astro +12 -2
- package/scaffold/templates/astro/src/pages/fragments/nav.astro +8 -5
- package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +9 -10
- package/scaffold/templates/astro/src/pages/index.astro +18 -15
- package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +22 -44
- package/scaffold/templates/astro/src/pages/product/[slug].astro +44 -30
- package/scaffold/templates/astro/src/pages/products/index.astro +16 -21
- package/scaffold/templates/astro/src/pages/search.astro +10 -11
- package/scaffold/templates/astro/src/scripts/init-client.ts +14 -0
- package/scaffold/templates/astro/src/scripts/pdp.ts +24 -21
- package/scaffold/templates/astro/src/scripts/plp.ts +15 -93
- package/scaffold/templates/astro/src/scripts/search.ts +7 -132
- package/scaffold/templates/astro/src/scripts/signed-in-redirect.ts +4 -0
- package/scaffold/templates/astro/src/styles/global.css +49 -45
- package/scaffold/templates/astro/src/styles/pages/account.css +0 -90
- package/scaffold/templates/astro/src/styles/pages/confirmation.css +6 -0
- package/scaffold/templates/astro/src/styles/pages/home.css +9 -100
- package/scaffold/templates/astro/src/styles/pages/pdp.css +5 -171
- package/scaffold/templates/astro/src/styles/pages/plp.css +3 -42
- package/scaffold/templates/astro/src/styles/pages/search.css +4 -110
- package/scaffold/templates/astro/src/styles/tokens.css +56 -0
- package/scaffold/templates/nextjs/_dot.node-version +1 -1
- package/scaffold/templates/nextjs/app/(catalog)/category/[slug]/page.tsx +74 -0
- package/scaffold/templates/nextjs/app/(catalog)/home/[key]/page.tsx +80 -0
- package/scaffold/templates/nextjs/app/(catalog)/layout.tsx +13 -0
- package/scaffold/templates/nextjs/app/(catalog)/not-found.tsx +7 -0
- package/scaffold/templates/nextjs/app/(catalog)/product/[slug]/page.tsx +182 -0
- package/scaffold/templates/nextjs/app/(catalog)/products/page.tsx +62 -0
- package/scaffold/templates/nextjs/app/(catalog)/search/SearchView.tsx +90 -0
- package/scaffold/templates/nextjs/app/(catalog)/search/page.tsx +46 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/addresses/AddressesView.tsx +5 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/addresses/page.tsx +17 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/login/LoginView.tsx +28 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/login/page.tsx +23 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/profile/ProfileView.tsx +5 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/profile/page.tsx +20 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/register/RegisterView.tsx +26 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/register/page.tsx +11 -0
- package/scaffold/templates/nextjs/app/(shopper)/cart/CartView.tsx +5 -0
- package/scaffold/templates/nextjs/app/(shopper)/cart/page.tsx +22 -0
- package/scaffold/templates/nextjs/app/(shopper)/checkout/CheckoutView.tsx +27 -0
- package/scaffold/templates/nextjs/app/(shopper)/checkout/page.tsx +39 -0
- package/scaffold/templates/nextjs/app/(shopper)/layout.tsx +12 -0
- package/scaffold/templates/nextjs/app/{order → (shopper)/order}/[orderNumber]/page.tsx +24 -5
- package/scaffold/templates/nextjs/app/error.tsx +3 -7
- package/scaffold/templates/nextjs/app/fragments/footer/route.ts +13 -0
- package/scaffold/templates/nextjs/app/fragments/header/[key]/route.ts +18 -0
- package/scaffold/templates/nextjs/app/fragments/nav/[key]/route.ts +23 -0
- package/scaffold/templates/nextjs/app/fragments/product/[slug]/route.ts +22 -0
- package/scaffold/templates/nextjs/app/globals.css +5 -23
- package/scaffold/templates/nextjs/app/layout.tsx +8 -5
- package/scaffold/templates/nextjs/app/not-found.tsx +9 -33
- package/scaffold/templates/nextjs/app/tokens.css +56 -0
- package/scaffold/templates/nextjs/components/client/ListingControls.tsx +87 -0
- package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +48 -6
- package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +30 -15
- package/scaffold/templates/nextjs/components/client/elements.tsx +15 -0
- package/scaffold/templates/nextjs/components/server/Header.tsx +33 -102
- package/scaffold/templates/nextjs/components/server/NotFoundView.tsx +39 -0
- package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -3
- package/scaffold/templates/nextjs/elements/index.ts +12 -0
- package/scaffold/templates/nextjs/lib/api.ts +151 -55
- package/scaffold/templates/nextjs/lib/chrome.ts +40 -0
- package/scaffold/templates/nextjs/lib/fragments.ts +16 -0
- package/scaffold/templates/nextjs/lib/header.ts +39 -0
- package/scaffold/templates/nextjs/lib/render.ts +14 -0
- package/scaffold/templates/nextjs/next.config.mjs +32 -0
- package/scaffold/templates/nextjs/package.json +28 -27
- package/scaffold/templates/nextjs/styles/pages/account.module.css +0 -70
- package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +6 -85
- package/scaffold/templates/nextjs/styles/pages/home.module.css +2 -103
- package/scaffold/templates/nextjs/styles/pages/pdp.module.css +3 -183
- package/scaffold/templates/nextjs/styles/pages/plp.module.css +9 -143
- package/scaffold/templates/nextjs/styles/pages/search.module.css +1 -145
- package/scaffold/templates/nextjs/tsconfig.dist.json +5 -0
- package/scaffold/templates/astro/src/components/AddressForm.astro +0 -107
- package/scaffold/templates/astro/src/components/Breadcrumb.astro +0 -20
- package/scaffold/templates/astro/src/components/CategoryNav.astro +0 -27
- package/scaffold/templates/astro/src/components/CheckoutSteps.astro +0 -29
- package/scaffold/templates/astro/src/components/Footer.astro +0 -108
- package/scaffold/templates/astro/src/components/Header.astro +0 -198
- package/scaffold/templates/astro/src/components/ImageGallery.astro +0 -42
- package/scaffold/templates/astro/src/components/OrderSummary.astro +0 -53
- package/scaffold/templates/astro/src/components/Pagination.astro +0 -13
- package/scaffold/templates/astro/src/components/PriceDisplay.astro +0 -25
- package/scaffold/templates/astro/src/components/ProductCard.astro +0 -44
- package/scaffold/templates/astro/src/components/RelatedProducts.astro +0 -28
- package/scaffold/templates/astro/src/components/ReviewForm.astro +0 -74
- package/scaffold/templates/astro/src/components/ReviewList.astro +0 -41
- package/scaffold/templates/astro/src/components/StockBadge.astro +0 -15
- package/scaffold/templates/astro/src/scripts/auth-forms.ts +0 -82
- package/scaffold/templates/astro/src/scripts/cart-page.ts +0 -118
- package/scaffold/templates/astro/src/scripts/checkout.ts +0 -271
- package/scaffold/templates/astro/src/scripts/image-gallery.ts +0 -22
- package/scaffold/templates/astro/src/scripts/profile-form.ts +0 -57
- package/scaffold/templates/astro/src/scripts/review-form.ts +0 -27
- package/scaffold/templates/astro/src/styles/pages/cart.css +0 -215
- package/scaffold/templates/astro/src/styles/pages/checkout.css +0 -270
- package/scaffold/templates/nextjs/app/account/addresses/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/account/login/page.tsx +0 -23
- package/scaffold/templates/nextjs/app/account/profile/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/account/register/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/cart/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +0 -61
- package/scaffold/templates/nextjs/app/checkout/page.tsx +0 -17
- package/scaffold/templates/nextjs/app/page.tsx +0 -63
- package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +0 -111
- package/scaffold/templates/nextjs/app/products/page.tsx +0 -40
- package/scaffold/templates/nextjs/app/search/page.tsx +0 -24
- package/scaffold/templates/nextjs/components/client/AddressManager.tsx +0 -181
- package/scaffold/templates/nextjs/components/client/CartPage.tsx +0 -121
- package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +0 -482
- package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +0 -68
- package/scaffold/templates/nextjs/components/client/LoginForm.tsx +0 -89
- package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +0 -12
- package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +0 -137
- package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +0 -112
- package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +0 -131
- package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +0 -116
- package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +0 -27
- package/scaffold/templates/nextjs/components/client/SearchResults.tsx +0 -181
- package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +0 -36
- package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +0 -29
- package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +0 -60
- package/scaffold/templates/nextjs/components/server/Footer.module.css +0 -64
- package/scaffold/templates/nextjs/components/server/Footer.tsx +0 -79
- package/scaffold/templates/nextjs/components/server/Header.module.css +0 -147
- package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +0 -79
- package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +0 -31
- package/scaffold/templates/nextjs/components/server/ProductCard.tsx +0 -59
- package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +0 -31
- package/scaffold/templates/nextjs/components/server/ReviewList.tsx +0 -62
- package/scaffold/templates/nextjs/styles/pages/cart.module.css +0 -240
- package/scaffold/templates/nextjs/styles/pages/checkout.module.css +0 -404
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/layout.tsx +0 -0
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/[orderNumber]/page.tsx +0 -0
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/page.tsx +0 -0
- /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
|
-
})
|