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,108 +1,44 @@
|
|
|
1
1
|
---
|
|
2
|
-
import '
|
|
2
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
3
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
|
4
|
-
import
|
|
5
|
-
import AddressForm from '../components/AddressForm.astro'
|
|
6
|
-
import { fetchCapabilities } from '../lib/api'
|
|
4
|
+
import { fetchCapabilities, fetchCart } from '../lib/api'
|
|
7
5
|
|
|
8
|
-
const capabilities = await
|
|
6
|
+
const [capabilities, cart] = await Promise.all([
|
|
7
|
+
fetchCapabilities(),
|
|
8
|
+
fetchCart(Astro.request.headers.get('cookie')),
|
|
9
|
+
])
|
|
9
10
|
if (capabilities === null) {
|
|
10
11
|
throw new Error('The store did not answer GET /api/capabilities, so checkout cannot tell how to take a payment.')
|
|
11
12
|
}
|
|
12
|
-
const
|
|
13
|
+
const escapeAttribute = (value: string): string =>
|
|
14
|
+
value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<')
|
|
15
|
+
// The payments adapter's client half, the checkout's light-DOM child in its payment slot; a driver the project
|
|
16
|
+
// registers in src/lib/client.ts renders it. HTML, not markup: Astro would take `slot="payment"` on a component's
|
|
17
|
+
// child as a slot of NymbalElement's own.
|
|
18
|
+
const paymentElement = `<nymbal-payment-element slot="payment" data-testid="payment-element" client="${escapeAttribute(
|
|
19
|
+
JSON.stringify(capabilities.payments.client),
|
|
20
|
+
)}"></nymbal-payment-element>`
|
|
13
21
|
---
|
|
14
22
|
|
|
15
|
-
<BaseLayout title="Checkout | Nymbal" description="Complete your purchase.">
|
|
23
|
+
<BaseLayout title="Checkout | Nymbal" description="Complete your purchase." cart={cart}>
|
|
16
24
|
<div class="container section">
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<CheckoutSteps currentStep="contact" />
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="checkout-error" id="checkout-error" style="display:none;" data-testid="checkout-error" role="alert"></div>
|
|
24
|
-
|
|
25
|
-
<div class="checkout-layout">
|
|
26
|
-
<div class="checkout-form">
|
|
27
|
-
<!-- Contact Section -->
|
|
28
|
-
<section class="checkout-section" id="checkout-contact" data-testid="checkout-contact">
|
|
29
|
-
<h2 class="checkout-section-title">Contact Information</h2>
|
|
30
|
-
<form id="contact-form">
|
|
31
|
-
<div class="form-group">
|
|
32
|
-
<label for="contact-email">Email Address</label>
|
|
33
|
-
<input
|
|
34
|
-
type="email"
|
|
35
|
-
id="contact-email"
|
|
36
|
-
name="email"
|
|
37
|
-
required
|
|
38
|
-
autocomplete="email"
|
|
39
|
-
placeholder="you@example.com"
|
|
40
|
-
data-testid="contact-email"
|
|
41
|
-
/>
|
|
42
|
-
</div>
|
|
43
|
-
<button type="submit" class="btn btn-primary" data-testid="contact-continue">
|
|
44
|
-
Continue to Shipping
|
|
45
|
-
</button>
|
|
46
|
-
</form>
|
|
47
|
-
</section>
|
|
48
|
-
|
|
49
|
-
<!-- Shipping Section -->
|
|
50
|
-
<section class="checkout-section" id="checkout-shipping" style="display:none;" data-testid="checkout-shipping">
|
|
51
|
-
<h2 class="checkout-section-title">Shipping Address</h2>
|
|
52
|
-
<form id="shipping-form">
|
|
53
|
-
<AddressForm id="shipping" />
|
|
54
|
-
<button type="submit" class="btn btn-primary" style="margin-top: var(--nymbal-spacing-md, 1rem);" data-testid="shipping-continue">
|
|
55
|
-
Continue to Payment
|
|
56
|
-
</button>
|
|
57
|
-
</form>
|
|
58
|
-
</section>
|
|
59
|
-
|
|
60
|
-
<!-- Payment Section -->
|
|
61
|
-
<section class="checkout-section" id="checkout-payment" style="display:none;" data-testid="checkout-payment">
|
|
62
|
-
<h2 class="checkout-section-title">Payment</h2>
|
|
63
|
-
<form id="payment-form" novalidate>
|
|
64
|
-
<div class="form-group">
|
|
65
|
-
<label>
|
|
66
|
-
<input type="checkbox" id="billing-same" checked />
|
|
67
|
-
Billing address same as shipping
|
|
68
|
-
</label>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
<div id="billing-fields" style="display:none;">
|
|
72
|
-
<h3 class="checkout-section-title" style="font-size: 1rem; margin-top: var(--nymbal-spacing-md, 1rem);">Billing Address</h3>
|
|
73
|
-
<div id="billing-form">
|
|
74
|
-
<AddressForm id="billing" />
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<div class="payment-section" style="margin-top: var(--nymbal-spacing-md, 1rem);">
|
|
79
|
-
{paymentClient !== null && (
|
|
80
|
-
<p class="checkout-section-title" style="font-size: 1rem; margin-bottom: var(--nymbal-spacing-sm, 0.5rem);">Payment Details</p>
|
|
81
|
-
)}
|
|
82
|
-
<!-- The payments adapter's client half; a driver the project registers in src/lib/client.ts renders it. -->
|
|
83
|
-
<nymbal-payment-element data-testid="payment-element" client={JSON.stringify(paymentClient)}></nymbal-payment-element>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<div class="checkout-submit">
|
|
87
|
-
<button type="submit" class="btn btn-primary" data-testid="place-order">
|
|
88
|
-
Place Order
|
|
89
|
-
</button>
|
|
90
|
-
<p class="checkout-terms">
|
|
91
|
-
By placing your order, you agree to our terms and conditions.
|
|
92
|
-
</p>
|
|
93
|
-
</div>
|
|
94
|
-
</form>
|
|
95
|
-
</section>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<aside class="checkout-summary" id="checkout-summary" data-testid="checkout-summary">
|
|
99
|
-
<h2 class="checkout-summary-title">Order Summary</h2>
|
|
100
|
-
<p style="font-size: var(--nymbal-font-size-sm, 0.875rem); color: var(--nymbal-color-text-muted, #71717a);">Loading cart...</p>
|
|
101
|
-
</aside>
|
|
102
|
-
</div>
|
|
25
|
+
<NymbalElement tag="nymbal-checkout" attributes={cart === null ? {} : { cart: JSON.stringify(cart) }}>
|
|
26
|
+
<Fragment set:html={paymentElement} />
|
|
27
|
+
</NymbalElement>
|
|
103
28
|
</div>
|
|
104
29
|
</BaseLayout>
|
|
105
30
|
|
|
106
31
|
<script>
|
|
107
|
-
import '
|
|
32
|
+
import { NymbalCheckout } from '@nymbalhq/web-components/elements/nymbal-checkout'
|
|
33
|
+
import { NymbalPaymentElement } from '@nymbalhq/web-components/elements/nymbal-payment-element'
|
|
34
|
+
|
|
35
|
+
// The page's own elements; the layout defines the rest.
|
|
36
|
+
NymbalPaymentElement.define()
|
|
37
|
+
NymbalCheckout.define()
|
|
38
|
+
|
|
39
|
+
// The element places the order; the page only goes to it.
|
|
40
|
+
document.addEventListener('nymbal:checkout:placed', (event) => {
|
|
41
|
+
const { orderNumber } = (event as CustomEvent<{ orderNumber: string }>).detail
|
|
42
|
+
window.location.href = `/order/${encodeURIComponent(orderNumber)}`
|
|
43
|
+
})
|
|
108
44
|
</script>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
|
+
import { FOOTER } from '../../lib/chrome'
|
|
3
4
|
Astro.response.headers.set('Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=300')
|
|
5
|
+
// A fragment has no <meta charset>, so the charset is in the header.
|
|
6
|
+
Astro.response.headers.set('Content-Type', 'text/html; charset=utf-8')
|
|
4
7
|
---
|
|
5
|
-
<
|
|
8
|
+
<NymbalElement tag="nymbal-site-footer" attributes={FOOTER} />
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
|
+
import { HEADER, headerSlots } from '../../lib/chrome'
|
|
4
|
+
import { headerFragment } from '../../lib/fragments'
|
|
5
|
+
|
|
6
|
+
const header = await headerFragment()
|
|
7
|
+
if (header instanceof Response) return header
|
|
8
|
+
|
|
3
9
|
Astro.response.headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=300')
|
|
10
|
+
// A fragment has no <meta charset>, so the charset is in the header.
|
|
11
|
+
Astro.response.headers.set('Content-Type', 'text/html; charset=utf-8')
|
|
4
12
|
---
|
|
5
|
-
<
|
|
13
|
+
<NymbalElement tag="nymbal-site-header" attributes={HEADER}>
|
|
14
|
+
<Fragment set:html={headerSlots(header.canSearch)} />
|
|
15
|
+
</NymbalElement>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
|
+
import { navFragment } from '../../lib/fragments'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const nav = await navFragment()
|
|
6
|
+
if (nav instanceof Response) return nav
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Astro.response.headers.set('Cache-Control', 'public, s-maxage=21600, stale-while-revalidate=300')
|
|
9
|
+
// A fragment has no <meta charset>, so the charset is in the header.
|
|
10
|
+
Astro.response.headers.set('Content-Type', 'text/html; charset=utf-8')
|
|
8
11
|
---
|
|
9
|
-
<
|
|
12
|
+
<NymbalElement tag="nymbal-category-nav" attributes={{ categories: JSON.stringify(nav.categories) }} />
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
|
+
import { cardProductOf } from '@nymbalhq/web-components/ssr'
|
|
4
|
+
import { productFragment } from '../../../lib/fragments'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const { slug } = Astro.params
|
|
8
|
-
const product = await fetchProduct(slug!)
|
|
6
|
+
const card = await productFragment(Astro.params.slug!)
|
|
7
|
+
if (card instanceof Response) return card
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Astro.response.headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=60')
|
|
10
|
+
// A fragment has no <meta charset>, so the charset is in the header.
|
|
11
|
+
Astro.response.headers.set('Content-Type', 'text/html; charset=utf-8')
|
|
13
12
|
---
|
|
14
|
-
<
|
|
13
|
+
<NymbalElement tag="nymbal-product-card" attributes={{ product: JSON.stringify(cardProductOf(card.product)) }} />
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../styles/pages/home.css'
|
|
3
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
4
|
+
import { cardImageOf, cardProductOf } from '@nymbalhq/web-components/ssr'
|
|
3
5
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
|
4
6
|
import HeroSection from '../components/HeroSection.astro'
|
|
5
|
-
import CategoryNav from '../components/CategoryNav.astro'
|
|
6
|
-
import ProductCard from '../components/ProductCard.astro'
|
|
7
7
|
import NewsletterSignup from '../components/NewsletterSignup.astro'
|
|
8
8
|
import { fetchProducts, fetchCategories } from '../lib/api'
|
|
9
9
|
import { organizationJsonLd } from '../lib/seo'
|
|
@@ -12,17 +12,21 @@ const [productResult, categories] = await Promise.all([
|
|
|
12
12
|
fetchProducts({ limit: 8 }),
|
|
13
13
|
fetchCategories(),
|
|
14
14
|
])
|
|
15
|
+
const products = productResult.items.map(cardProductOf)
|
|
16
|
+
// The first card's image is the page's likely LCP, and the preload scanner never reaches it inside the grid's shadow
|
|
17
|
+
// root: the head starts it.
|
|
18
|
+
const firstImage = products[0] === undefined ? null : cardImageOf(products[0]).url
|
|
15
19
|
|
|
16
|
-
const products = productResult.items
|
|
17
20
|
const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
|
|
18
21
|
---
|
|
19
22
|
|
|
20
23
|
<BaseLayout title="Nymbal — Modern Storefront" jsonLd={jsonLd}>
|
|
24
|
+
{firstImage && <link slot="head" rel="preload" as="image" href={firstImage} fetchpriority="high" />}
|
|
21
25
|
<HeroSection />
|
|
22
26
|
|
|
23
27
|
<section class="section container">
|
|
24
28
|
<h2 class="section-title">Shop by Category</h2>
|
|
25
|
-
<
|
|
29
|
+
<NymbalElement tag="nymbal-category-nav" attributes={{ categories: JSON.stringify(categories) }} />
|
|
26
30
|
</section>
|
|
27
31
|
|
|
28
32
|
<section class="featured-section section">
|
|
@@ -31,19 +35,18 @@ const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
|
|
|
31
35
|
<h2 class="section-title" style="margin-bottom: 0;">Featured Products</h2>
|
|
32
36
|
<a href="/products" class="featured-link">View all</a>
|
|
33
37
|
</div>
|
|
34
|
-
{products.
|
|
35
|
-
<div class="product-grid">
|
|
36
|
-
{products.map((product) => (
|
|
37
|
-
<ProductCard product={product} />
|
|
38
|
-
))}
|
|
39
|
-
</div>
|
|
40
|
-
) : (
|
|
41
|
-
<p style="text-align: center; color: var(--nymbal-color-text-muted, #71717a);">
|
|
42
|
-
Products coming soon.
|
|
43
|
-
</p>
|
|
44
|
-
)}
|
|
38
|
+
<NymbalElement tag="nymbal-product-grid" attributes={{ products: JSON.stringify(products) }} />
|
|
45
39
|
</div>
|
|
46
40
|
</section>
|
|
47
41
|
|
|
48
42
|
<NewsletterSignup />
|
|
49
43
|
</BaseLayout>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
import '../lib/client.js'
|
|
47
|
+
import { NymbalCategoryNav } from '@nymbalhq/web-components/elements/nymbal-category-nav'
|
|
48
|
+
import { NymbalProductGrid } from '@nymbalhq/web-components/elements/nymbal-product-grid'
|
|
49
|
+
|
|
50
|
+
NymbalCategoryNav.define()
|
|
51
|
+
NymbalProductGrid.define()
|
|
52
|
+
</script>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../../styles/pages/confirmation.css'
|
|
3
3
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
4
|
-
import
|
|
4
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
5
5
|
import { fetchOrder } from '../../lib/api'
|
|
6
|
-
import { formatPrice } from '../../lib/format'
|
|
7
6
|
import { isPaid, orderHeading } from '../../lib/order-status'
|
|
8
7
|
|
|
9
8
|
const { orderNumber } = Astro.params
|
|
@@ -11,11 +10,22 @@ const order = await fetchOrder(orderNumber!)
|
|
|
11
10
|
|
|
12
11
|
if (!order) return Astro.redirect('/404')
|
|
13
12
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const summary = {
|
|
14
|
+
items: JSON.stringify(
|
|
15
|
+
(order.lineItems ?? []).map((item) => ({
|
|
16
|
+
name: `${item.productName}${item.variantName ? ` - ${item.variantName}` : ''}`,
|
|
17
|
+
qty: item.qty,
|
|
18
|
+
unitPriceMinor: item.unitPriceMinor,
|
|
19
|
+
lineTotalMinor: item.lineTotalMinor,
|
|
20
|
+
})),
|
|
21
|
+
),
|
|
22
|
+
'subtotal-minor': String(order.subtotalMinor),
|
|
23
|
+
'discount-minor': String(order.discountTotalMinor ?? 0),
|
|
24
|
+
'shipping-minor': String(order.shippingTotalMinor ?? 0),
|
|
25
|
+
'tax-minor': String(order.taxTotalMinor ?? 0),
|
|
26
|
+
'total-minor': String(order.totalMinor),
|
|
27
|
+
currency: order.currency,
|
|
28
|
+
}
|
|
19
29
|
|
|
20
30
|
const shippingAddress = order.shippingAddress
|
|
21
31
|
const heading = orderHeading(order.status)
|
|
@@ -63,45 +73,10 @@ const paid = isPaid(order.status)
|
|
|
63
73
|
<dd style="text-transform: capitalize;" data-testid="order-status">{order.status}</dd>
|
|
64
74
|
</div>
|
|
65
75
|
</dl>
|
|
66
|
-
|
|
67
|
-
<div class="confirmation-items">
|
|
68
|
-
{lineItems.map((item) => (
|
|
69
|
-
<div class="confirmation-item">
|
|
70
|
-
<span>
|
|
71
|
-
<span class="confirmation-item-name">{item.name}</span>
|
|
72
|
-
<span class="confirmation-item-qty"> x{item.qty}</span>
|
|
73
|
-
</span>
|
|
74
|
-
<span class="confirmation-item-price">
|
|
75
|
-
{formatPrice(item.priceMinor * item.qty, order.currency)}
|
|
76
|
-
</span>
|
|
77
|
-
</div>
|
|
78
|
-
))}
|
|
79
|
-
</div>
|
|
80
|
-
|
|
81
|
-
<div class="confirmation-totals">
|
|
82
|
-
<div class="confirmation-total-row">
|
|
83
|
-
<span>Subtotal</span>
|
|
84
|
-
<span>{formatPrice(order.subtotalMinor, order.currency)}</span>
|
|
85
|
-
</div>
|
|
86
|
-
{order.shippingTotalMinor != null && (
|
|
87
|
-
<div class="confirmation-total-row">
|
|
88
|
-
<span>Shipping</span>
|
|
89
|
-
<span>{order.shippingTotalMinor === 0 ? 'Free' : formatPrice(order.shippingTotalMinor, order.currency)}</span>
|
|
90
|
-
</div>
|
|
91
|
-
)}
|
|
92
|
-
{order.taxTotalMinor != null && (
|
|
93
|
-
<div class="confirmation-total-row">
|
|
94
|
-
<span>Tax</span>
|
|
95
|
-
<span>{formatPrice(order.taxTotalMinor, order.currency)}</span>
|
|
96
|
-
</div>
|
|
97
|
-
)}
|
|
98
|
-
<div class="confirmation-total-row grand-total">
|
|
99
|
-
<span>Total</span>
|
|
100
|
-
<span>{formatPrice(order.totalMinor, order.currency)}</span>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
76
|
</div>
|
|
104
77
|
|
|
78
|
+
<NymbalElement tag="nymbal-order-summary" attributes={{ class: 'confirmation-summary', ...summary }} />
|
|
79
|
+
|
|
105
80
|
{shippingAddress && (
|
|
106
81
|
<div class="confirmation-card">
|
|
107
82
|
<h2 class="confirmation-card-title">Shipping Address</h2>
|
|
@@ -124,4 +99,7 @@ const paid = isPaid(order.status)
|
|
|
124
99
|
|
|
125
100
|
<script>
|
|
126
101
|
import '../../scripts/order-status.js'
|
|
102
|
+
import { NymbalOrderSummary } from '@nymbalhq/web-components/elements/nymbal-order-summary'
|
|
103
|
+
|
|
104
|
+
NymbalOrderSummary.define()
|
|
127
105
|
</script>
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../../styles/pages/pdp.css'
|
|
3
3
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import PriceDisplay from '../../components/PriceDisplay.astro'
|
|
7
|
-
import StockBadge from '../../components/StockBadge.astro'
|
|
8
|
-
import ReviewList from '../../components/ReviewList.astro'
|
|
9
|
-
import ReviewForm from '../../components/ReviewForm.astro'
|
|
10
|
-
import RelatedProducts from '../../components/RelatedProducts.astro'
|
|
4
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
5
|
+
import { cardProductOf } from '@nymbalhq/web-components/ssr'
|
|
11
6
|
import { fetchProduct, fetchReviews, fetchProducts } from '../../lib/api'
|
|
12
7
|
import { productJsonLd, breadcrumbJsonLd } from '../../lib/seo'
|
|
13
8
|
|
|
@@ -20,7 +15,7 @@ const [reviewsResult, relatedResult] = await Promise.all([
|
|
|
20
15
|
fetchReviews(product.id),
|
|
21
16
|
fetchProducts({
|
|
22
17
|
limit: 4,
|
|
23
|
-
category: product.
|
|
18
|
+
category: product.categories?.[0]?.slug,
|
|
24
19
|
}),
|
|
25
20
|
])
|
|
26
21
|
|
|
@@ -50,40 +45,51 @@ const hasVariantChoice = (product.variants?.length ?? 0) > 1
|
|
|
50
45
|
---
|
|
51
46
|
|
|
52
47
|
<BaseLayout title={`${product.name} | Nymbal`} description={product.shortDescription ?? product.description ?? ''} jsonLd={jsonLd}>
|
|
48
|
+
<!-- The gallery's main image is the page's LCP: the browser starts it before it parses the body. -->
|
|
49
|
+
<link slot="head" rel="preload" as="image" href={images[0]!.url} fetchpriority="high" />
|
|
53
50
|
<div class="container section" data-product-slug={product.slug}>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
<NymbalElement
|
|
52
|
+
tag="nymbal-breadcrumb"
|
|
53
|
+
attributes={{
|
|
54
|
+
items: JSON.stringify([
|
|
55
|
+
{ label: 'Home', href: '/' },
|
|
56
|
+
{ label: 'Products', href: '/products' },
|
|
57
|
+
{ label: product.name },
|
|
58
|
+
]),
|
|
59
|
+
}}
|
|
60
60
|
/>
|
|
61
61
|
|
|
62
62
|
<div class="pdp-layout">
|
|
63
|
-
<
|
|
63
|
+
<NymbalElement tag="nymbal-image-gallery" attributes={{ class: 'pdp-gallery', images: JSON.stringify(images) }} />
|
|
64
64
|
|
|
65
65
|
<div class="pdp-info">
|
|
66
66
|
<h1 class="pdp-title" data-testid="product-title">{product.name}</h1>
|
|
67
67
|
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
<NymbalElement
|
|
69
|
+
tag="nymbal-product-price"
|
|
70
|
+
attributes={{
|
|
71
|
+
'product-id': product.id,
|
|
72
|
+
'price-minor': String(defaultVariant?.priceMinor ?? product.priceMinor ?? 0),
|
|
73
|
+
currency: product.currency ?? 'GBP',
|
|
74
|
+
}}
|
|
71
75
|
/>
|
|
72
|
-
|
|
73
|
-
<StockBadge stock={stock} />
|
|
76
|
+
<NymbalElement tag="nymbal-stock-badge" attributes={{ 'product-id': product.id, stock: String(stock) }} />
|
|
74
77
|
|
|
75
78
|
<div class="pdp-actions">
|
|
76
79
|
{hasVariantChoice && (
|
|
77
|
-
<nymbal-variant-selector
|
|
80
|
+
<NymbalElement tag="nymbal-variant-selector" attributes={{ product: JSON.stringify(product) }} />
|
|
78
81
|
)}
|
|
79
82
|
|
|
80
83
|
<div class="pdp-add-row">
|
|
81
|
-
<nymbal-quantity-selector value
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
<NymbalElement tag="nymbal-quantity-selector" attributes={{ value: '1', min: '1', max: '99' }} />
|
|
85
|
+
<NymbalElement
|
|
86
|
+
tag="nymbal-add-to-cart"
|
|
87
|
+
attributes={{
|
|
88
|
+
'variant-id': defaultVariant?.id ?? product.id,
|
|
89
|
+
qty: '1',
|
|
90
|
+
...(stock <= 0 && { disabled: '' }),
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
87
93
|
</div>
|
|
88
94
|
</div>
|
|
89
95
|
|
|
@@ -97,11 +103,19 @@ const hasVariantChoice = (product.variants?.length ?? 0) > 1
|
|
|
97
103
|
|
|
98
104
|
<section class="pdp-reviews">
|
|
99
105
|
<h2 class="pdp-reviews-title">Customer Reviews</h2>
|
|
100
|
-
<
|
|
101
|
-
<
|
|
106
|
+
<NymbalElement tag="nymbal-review-list" attributes={{ reviews: JSON.stringify(reviews) }} />
|
|
107
|
+
<NymbalElement tag="nymbal-review-form" attributes={{ class: 'pdp-review-form', 'product-id': product.id }} />
|
|
102
108
|
</section>
|
|
103
109
|
|
|
104
|
-
|
|
110
|
+
{relatedProducts.length > 0 && (
|
|
111
|
+
<section class="section">
|
|
112
|
+
<h2 class="section-title">You may also like</h2>
|
|
113
|
+
<NymbalElement
|
|
114
|
+
tag="nymbal-product-grid"
|
|
115
|
+
attributes={{ products: JSON.stringify(relatedProducts.map(cardProductOf)), lazy: '' }}
|
|
116
|
+
/>
|
|
117
|
+
</section>
|
|
118
|
+
)}
|
|
105
119
|
</div>
|
|
106
120
|
</BaseLayout>
|
|
107
121
|
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../../styles/pages/plp.css'
|
|
3
|
-
import '../../styles/pages/home.css'
|
|
4
3
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import Pagination from '../../components/Pagination.astro'
|
|
4
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
5
|
+
import { cardImageOf, cardProductOf } from '@nymbalhq/web-components/ssr'
|
|
8
6
|
import { PRODUCT_SORT_LABELS } from '@nymbalhq/types'
|
|
9
7
|
import { fetchProducts, fetchCategories, fetchCapabilities, sortFromUrl } from '../../lib/api'
|
|
10
8
|
|
|
@@ -17,17 +15,20 @@ const [productResult, categories] = await Promise.all([
|
|
|
17
15
|
fetchCategories(),
|
|
18
16
|
])
|
|
19
17
|
|
|
20
|
-
const products = productResult.items
|
|
18
|
+
const products = productResult.items.map(cardProductOf)
|
|
19
|
+
// The first card's image is the page's LCP, and the preload scanner never reaches it inside the grid's shadow root:
|
|
20
|
+
// the head starts it.
|
|
21
|
+
const firstImage = products[0] === undefined ? null : cardImageOf(products[0]).url
|
|
21
22
|
const facets = productResult.facets ?? []
|
|
22
23
|
---
|
|
23
24
|
|
|
24
25
|
<BaseLayout title="All Products | Nymbal" description="Browse our full product collection.">
|
|
26
|
+
{firstImage && <link slot="head" rel="preload" as="image" href={firstImage} fetchpriority="high" />}
|
|
25
27
|
<div class="container section">
|
|
26
|
-
<
|
|
28
|
+
<NymbalElement tag="nymbal-breadcrumb" attributes={{ items: JSON.stringify([{ label: 'Home', href: '/' }, { label: 'Products' }]) }} />
|
|
27
29
|
|
|
28
30
|
<div class="plp-header">
|
|
29
31
|
<h1 class="plp-title">All Products</h1>
|
|
30
|
-
<p class="plp-count" id="product-count">{products.length} products</p>
|
|
31
32
|
</div>
|
|
32
33
|
|
|
33
34
|
<div class="plp-layout">
|
|
@@ -57,20 +58,14 @@ const facets = productResult.facets ?? []
|
|
|
57
58
|
</div>
|
|
58
59
|
)}
|
|
59
60
|
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<h2 class="plp-empty-title">No products found</h2>
|
|
69
|
-
<p class="plp-empty-text">Try adjusting your filters.</p>
|
|
70
|
-
</div>
|
|
71
|
-
)}
|
|
72
|
-
|
|
73
|
-
<Pagination />
|
|
61
|
+
<NymbalElement
|
|
62
|
+
tag="nymbal-product-grid"
|
|
63
|
+
attributes={{
|
|
64
|
+
products: JSON.stringify(products),
|
|
65
|
+
source: 'product-list',
|
|
66
|
+
...(productResult.nextCursor !== null && { 'has-more': '' }),
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
74
69
|
</div>
|
|
75
70
|
</div>
|
|
76
71
|
</div>
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../styles/pages/search.css'
|
|
3
|
-
import '
|
|
4
|
-
import '../styles/pages/plp.css'
|
|
3
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
5
4
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
|
5
|
+
import { fetchSearch } from '../lib/api'
|
|
6
|
+
|
|
7
|
+
const q = Astro.url.searchParams.get('q')?.trim() ?? ''
|
|
8
|
+
const search = q === '' ? null : await fetchSearch(q, Astro.url.searchParams.get('category'))
|
|
9
|
+
// The results the page's search would show, so they are in the HTML; the search store takes over once it answers.
|
|
10
|
+
const results: Record<string, string> =
|
|
11
|
+
search === null ? {} : { results: JSON.stringify(search.items), query: q }
|
|
6
12
|
---
|
|
7
13
|
|
|
8
14
|
<BaseLayout title="Search | Nymbal" description="Search our product catalogue.">
|
|
@@ -22,18 +28,11 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
|
|
22
28
|
</div>
|
|
23
29
|
|
|
24
30
|
<div class="container section">
|
|
25
|
-
<p class="search-query-label" id="search-query-label" style="display:none; margin-bottom: var(--nymbal-spacing-md, 1rem);"></p>
|
|
26
31
|
<div class="search-layout">
|
|
27
32
|
<aside class="search-sidebar">
|
|
28
|
-
<nymbal-product-filter source="search"></nymbal-product-filter>
|
|
33
|
+
<nymbal-product-filter source="search" empty></nymbal-product-filter>
|
|
29
34
|
</aside>
|
|
30
|
-
<
|
|
31
|
-
<div class="search-empty">
|
|
32
|
-
<div class="search-empty-icon" aria-hidden="true">🔍</div>
|
|
33
|
-
<h2 class="search-empty-title">Search our store</h2>
|
|
34
|
-
<p class="search-empty-text">Type a keyword to find products.</p>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
35
|
+
<NymbalElement tag="nymbal-search-results" attributes={{ 'data-testid': 'search-results', ...results }} />
|
|
37
36
|
</div>
|
|
38
37
|
</div>
|
|
39
38
|
</BaseLayout>
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import '../lib/client.js'
|
|
2
|
+
import { NymbalCartDrawer } from '@nymbalhq/web-components/elements/nymbal-cart-drawer'
|
|
3
|
+
import { NymbalMiniCart } from '@nymbalhq/web-components/elements/nymbal-mini-cart'
|
|
4
|
+
import { NymbalSearchBar } from '@nymbalhq/web-components/elements/nymbal-search-bar'
|
|
5
|
+
import { NymbalSiteFooter } from '@nymbalhq/web-components/elements/nymbal-site-footer'
|
|
6
|
+
import { NymbalSiteHeader } from '@nymbalhq/web-components/elements/nymbal-site-header'
|
|
7
|
+
import { NymbalToast } from '@nymbalhq/web-components/elements/nymbal-toast'
|
|
8
|
+
|
|
9
|
+
// The layout's elements; each page script defines the ones its page renders.
|
|
10
|
+
NymbalSiteHeader.define()
|
|
11
|
+
NymbalSiteFooter.define()
|
|
12
|
+
NymbalSearchBar.define()
|
|
13
|
+
NymbalMiniCart.define()
|
|
14
|
+
NymbalCartDrawer.define()
|
|
15
|
+
NymbalToast.define()
|
|
2
16
|
|
|
3
17
|
// Navigate to checkout when the cart drawer checkout button is clicked
|
|
4
18
|
document.addEventListener('nymbal:cart:checkout-clicked', () => {
|