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.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +14 -12
- 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 -27
- 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 +126 -20
- package/scaffold/templates/astro/src/lib/chrome.ts +59 -0
- package/scaffold/templates/astro/src/lib/client.ts +4 -2
- 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 +41 -26
- package/scaffold/templates/astro/src/pages/checkout.astro +34 -90
- 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 +46 -29
- package/scaffold/templates/astro/src/pages/products/index.astro +40 -26
- package/scaffold/templates/astro/src/pages/search.astro +13 -8
- package/scaffold/templates/astro/src/scripts/init-client.ts +14 -0
- package/scaffold/templates/astro/src/scripts/pdp.ts +47 -53
- package/scaffold/templates/astro/src/scripts/plp.ts +21 -98
- package/scaffold/templates/astro/src/scripts/search.ts +19 -120
- 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 -167
- package/scaffold/templates/astro/src/styles/pages/plp.css +3 -42
- package/scaffold/templates/astro/src/styles/pages/search.css +13 -100
- 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 +64 -28
- package/scaffold/templates/nextjs/components/client/elements.tsx +15 -0
- package/scaffold/templates/nextjs/components/server/Header.tsx +33 -97
- 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 +162 -46
- package/scaffold/templates/nextjs/lib/chrome.ts +40 -0
- package/scaffold/templates/nextjs/lib/client.ts +3 -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 -29
- 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 -179
- package/scaffold/templates/nextjs/styles/pages/plp.module.css +8 -141
- package/scaffold/templates/nextjs/styles/pages/search.module.css +8 -128
- 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 -192
- 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/SortDropdown.astro +0 -26
- package/scaffold/templates/astro/src/components/StockBadge.astro +0 -32
- package/scaffold/templates/astro/src/scripts/auth-forms.ts +0 -82
- package/scaffold/templates/astro/src/scripts/cart-page.ts +0 -143
- package/scaffold/templates/astro/src/scripts/checkout.ts +0 -272
- 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 -243
- 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 -58
- package/scaffold/templates/nextjs/app/checkout/page.tsx +0 -10
- 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 -120
- package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +0 -523
- 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 -148
- 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 -141
- package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +0 -35
- 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/components/server/StockBadge.tsx +0 -19
- 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,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
|
-
})
|