create-nymbal-app 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +7 -4
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -4
  5. package/scaffold/project/astro/src/pages/index.astro +13 -15
  6. package/scaffold/templates/astro/_dot.node-version +1 -1
  7. package/scaffold/templates/astro/astro.config.mjs +23 -0
  8. package/scaffold/templates/astro/package.json +26 -26
  9. package/scaffold/templates/astro/src/components/SkipNav.astro +3 -1
  10. package/scaffold/templates/astro/src/elements/index.ts +11 -0
  11. package/scaffold/templates/astro/src/layouts/BaseLayout.astro +15 -4
  12. package/scaffold/templates/astro/src/lib/api.ts +113 -21
  13. package/scaffold/templates/astro/src/lib/chrome.ts +59 -0
  14. package/scaffold/templates/astro/src/lib/client.ts +4 -4
  15. package/scaffold/templates/astro/src/lib/fragments.ts +48 -0
  16. package/scaffold/templates/astro/src/lib/server-elements.ts +5 -0
  17. package/scaffold/templates/astro/src/middleware.ts +37 -0
  18. package/scaffold/templates/astro/src/pages/account/addresses.astro +5 -97
  19. package/scaffold/templates/astro/src/pages/account/login.astro +7 -34
  20. package/scaffold/templates/astro/src/pages/account/profile.astro +6 -56
  21. package/scaffold/templates/astro/src/pages/account/register.astro +7 -73
  22. package/scaffold/templates/astro/src/pages/cart.astro +14 -7
  23. package/scaffold/templates/astro/src/pages/category/[slug].astro +17 -21
  24. package/scaffold/templates/astro/src/pages/checkout.astro +30 -94
  25. package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -2
  26. package/scaffold/templates/astro/src/pages/fragments/header.astro +12 -2
  27. package/scaffold/templates/astro/src/pages/fragments/nav.astro +8 -5
  28. package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +9 -10
  29. package/scaffold/templates/astro/src/pages/index.astro +18 -15
  30. package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +22 -44
  31. package/scaffold/templates/astro/src/pages/product/[slug].astro +44 -30
  32. package/scaffold/templates/astro/src/pages/products/index.astro +16 -21
  33. package/scaffold/templates/astro/src/pages/search.astro +10 -11
  34. package/scaffold/templates/astro/src/scripts/init-client.ts +14 -0
  35. package/scaffold/templates/astro/src/scripts/pdp.ts +24 -21
  36. package/scaffold/templates/astro/src/scripts/plp.ts +15 -93
  37. package/scaffold/templates/astro/src/scripts/search.ts +7 -132
  38. package/scaffold/templates/astro/src/scripts/signed-in-redirect.ts +4 -0
  39. package/scaffold/templates/astro/src/styles/global.css +49 -45
  40. package/scaffold/templates/astro/src/styles/pages/account.css +0 -90
  41. package/scaffold/templates/astro/src/styles/pages/confirmation.css +6 -0
  42. package/scaffold/templates/astro/src/styles/pages/home.css +9 -100
  43. package/scaffold/templates/astro/src/styles/pages/pdp.css +5 -171
  44. package/scaffold/templates/astro/src/styles/pages/plp.css +3 -42
  45. package/scaffold/templates/astro/src/styles/pages/search.css +4 -110
  46. package/scaffold/templates/astro/src/styles/tokens.css +56 -0
  47. package/scaffold/templates/nextjs/_dot.node-version +1 -1
  48. package/scaffold/templates/nextjs/app/(catalog)/category/[slug]/page.tsx +74 -0
  49. package/scaffold/templates/nextjs/app/(catalog)/home/[key]/page.tsx +80 -0
  50. package/scaffold/templates/nextjs/app/(catalog)/layout.tsx +13 -0
  51. package/scaffold/templates/nextjs/app/(catalog)/not-found.tsx +7 -0
  52. package/scaffold/templates/nextjs/app/(catalog)/product/[slug]/page.tsx +182 -0
  53. package/scaffold/templates/nextjs/app/(catalog)/products/page.tsx +62 -0
  54. package/scaffold/templates/nextjs/app/(catalog)/search/SearchView.tsx +90 -0
  55. package/scaffold/templates/nextjs/app/(catalog)/search/page.tsx +46 -0
  56. package/scaffold/templates/nextjs/app/(shopper)/account/addresses/AddressesView.tsx +5 -0
  57. package/scaffold/templates/nextjs/app/(shopper)/account/addresses/page.tsx +17 -0
  58. package/scaffold/templates/nextjs/app/(shopper)/account/login/LoginView.tsx +28 -0
  59. package/scaffold/templates/nextjs/app/(shopper)/account/login/page.tsx +23 -0
  60. package/scaffold/templates/nextjs/app/(shopper)/account/profile/ProfileView.tsx +5 -0
  61. package/scaffold/templates/nextjs/app/(shopper)/account/profile/page.tsx +20 -0
  62. package/scaffold/templates/nextjs/app/(shopper)/account/register/RegisterView.tsx +26 -0
  63. package/scaffold/templates/nextjs/app/(shopper)/account/register/page.tsx +11 -0
  64. package/scaffold/templates/nextjs/app/(shopper)/cart/CartView.tsx +5 -0
  65. package/scaffold/templates/nextjs/app/(shopper)/cart/page.tsx +22 -0
  66. package/scaffold/templates/nextjs/app/(shopper)/checkout/CheckoutView.tsx +27 -0
  67. package/scaffold/templates/nextjs/app/(shopper)/checkout/page.tsx +39 -0
  68. package/scaffold/templates/nextjs/app/(shopper)/layout.tsx +12 -0
  69. package/scaffold/templates/nextjs/app/{order → (shopper)/order}/[orderNumber]/page.tsx +24 -5
  70. package/scaffold/templates/nextjs/app/error.tsx +3 -7
  71. package/scaffold/templates/nextjs/app/fragments/footer/route.ts +13 -0
  72. package/scaffold/templates/nextjs/app/fragments/header/[key]/route.ts +18 -0
  73. package/scaffold/templates/nextjs/app/fragments/nav/[key]/route.ts +23 -0
  74. package/scaffold/templates/nextjs/app/fragments/product/[slug]/route.ts +22 -0
  75. package/scaffold/templates/nextjs/app/globals.css +5 -23
  76. package/scaffold/templates/nextjs/app/layout.tsx +8 -5
  77. package/scaffold/templates/nextjs/app/not-found.tsx +9 -33
  78. package/scaffold/templates/nextjs/app/tokens.css +56 -0
  79. package/scaffold/templates/nextjs/components/client/ListingControls.tsx +87 -0
  80. package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +48 -6
  81. package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +30 -15
  82. package/scaffold/templates/nextjs/components/client/elements.tsx +15 -0
  83. package/scaffold/templates/nextjs/components/server/Header.tsx +33 -102
  84. package/scaffold/templates/nextjs/components/server/NotFoundView.tsx +39 -0
  85. package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -3
  86. package/scaffold/templates/nextjs/elements/index.ts +12 -0
  87. package/scaffold/templates/nextjs/lib/api.ts +151 -55
  88. package/scaffold/templates/nextjs/lib/chrome.ts +40 -0
  89. package/scaffold/templates/nextjs/lib/fragments.ts +16 -0
  90. package/scaffold/templates/nextjs/lib/header.ts +39 -0
  91. package/scaffold/templates/nextjs/lib/render.ts +14 -0
  92. package/scaffold/templates/nextjs/next.config.mjs +32 -0
  93. package/scaffold/templates/nextjs/package.json +28 -27
  94. package/scaffold/templates/nextjs/styles/pages/account.module.css +0 -70
  95. package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +6 -85
  96. package/scaffold/templates/nextjs/styles/pages/home.module.css +2 -103
  97. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +3 -183
  98. package/scaffold/templates/nextjs/styles/pages/plp.module.css +9 -143
  99. package/scaffold/templates/nextjs/styles/pages/search.module.css +1 -145
  100. package/scaffold/templates/nextjs/tsconfig.dist.json +5 -0
  101. package/scaffold/templates/astro/src/components/AddressForm.astro +0 -107
  102. package/scaffold/templates/astro/src/components/Breadcrumb.astro +0 -20
  103. package/scaffold/templates/astro/src/components/CategoryNav.astro +0 -27
  104. package/scaffold/templates/astro/src/components/CheckoutSteps.astro +0 -29
  105. package/scaffold/templates/astro/src/components/Footer.astro +0 -108
  106. package/scaffold/templates/astro/src/components/Header.astro +0 -198
  107. package/scaffold/templates/astro/src/components/ImageGallery.astro +0 -42
  108. package/scaffold/templates/astro/src/components/OrderSummary.astro +0 -53
  109. package/scaffold/templates/astro/src/components/Pagination.astro +0 -13
  110. package/scaffold/templates/astro/src/components/PriceDisplay.astro +0 -25
  111. package/scaffold/templates/astro/src/components/ProductCard.astro +0 -44
  112. package/scaffold/templates/astro/src/components/RelatedProducts.astro +0 -28
  113. package/scaffold/templates/astro/src/components/ReviewForm.astro +0 -74
  114. package/scaffold/templates/astro/src/components/ReviewList.astro +0 -41
  115. package/scaffold/templates/astro/src/components/StockBadge.astro +0 -15
  116. package/scaffold/templates/astro/src/scripts/auth-forms.ts +0 -82
  117. package/scaffold/templates/astro/src/scripts/cart-page.ts +0 -118
  118. package/scaffold/templates/astro/src/scripts/checkout.ts +0 -271
  119. package/scaffold/templates/astro/src/scripts/image-gallery.ts +0 -22
  120. package/scaffold/templates/astro/src/scripts/profile-form.ts +0 -57
  121. package/scaffold/templates/astro/src/scripts/review-form.ts +0 -27
  122. package/scaffold/templates/astro/src/styles/pages/cart.css +0 -215
  123. package/scaffold/templates/astro/src/styles/pages/checkout.css +0 -270
  124. package/scaffold/templates/nextjs/app/account/addresses/page.tsx +0 -10
  125. package/scaffold/templates/nextjs/app/account/login/page.tsx +0 -23
  126. package/scaffold/templates/nextjs/app/account/profile/page.tsx +0 -10
  127. package/scaffold/templates/nextjs/app/account/register/page.tsx +0 -10
  128. package/scaffold/templates/nextjs/app/cart/page.tsx +0 -10
  129. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +0 -61
  130. package/scaffold/templates/nextjs/app/checkout/page.tsx +0 -17
  131. package/scaffold/templates/nextjs/app/page.tsx +0 -63
  132. package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +0 -111
  133. package/scaffold/templates/nextjs/app/products/page.tsx +0 -40
  134. package/scaffold/templates/nextjs/app/search/page.tsx +0 -24
  135. package/scaffold/templates/nextjs/components/client/AddressManager.tsx +0 -181
  136. package/scaffold/templates/nextjs/components/client/CartPage.tsx +0 -121
  137. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +0 -482
  138. package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +0 -68
  139. package/scaffold/templates/nextjs/components/client/LoginForm.tsx +0 -89
  140. package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +0 -12
  141. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +0 -137
  142. package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +0 -112
  143. package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +0 -131
  144. package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +0 -116
  145. package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +0 -27
  146. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +0 -181
  147. package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +0 -36
  148. package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +0 -29
  149. package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +0 -60
  150. package/scaffold/templates/nextjs/components/server/Footer.module.css +0 -64
  151. package/scaffold/templates/nextjs/components/server/Footer.tsx +0 -79
  152. package/scaffold/templates/nextjs/components/server/Header.module.css +0 -147
  153. package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +0 -79
  154. package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +0 -31
  155. package/scaffold/templates/nextjs/components/server/ProductCard.tsx +0 -59
  156. package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +0 -31
  157. package/scaffold/templates/nextjs/components/server/ReviewList.tsx +0 -62
  158. package/scaffold/templates/nextjs/styles/pages/cart.module.css +0 -240
  159. package/scaffold/templates/nextjs/styles/pages/checkout.module.css +0 -404
  160. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/layout.tsx +0 -0
  161. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/[orderNumber]/page.tsx +0 -0
  162. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/page.tsx +0 -0
  163. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/page.tsx +0 -0
@@ -1,108 +1,44 @@
1
1
  ---
2
- import '../styles/pages/checkout.css'
2
+ import NymbalElement from '@nymbalhq/web-components/astro'
3
3
  import BaseLayout from '../layouts/BaseLayout.astro'
4
- import CheckoutSteps from '../components/CheckoutSteps.astro'
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 fetchCapabilities()
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 paymentClient = capabilities.payments.client
13
+ const escapeAttribute = (value: string): string =>
14
+ value.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;')
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
- <h1 style="font-size: 1.75rem; font-weight: var(--nymbal-font-weight-bold, 700); margin-bottom: var(--nymbal-spacing-md, 1rem);">Checkout</h1>
18
-
19
- <div id="checkout-steps-container">
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 '../scripts/checkout.js'
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 Footer from '../../components/Footer.astro'
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
- <Footer />
8
+ <NymbalElement tag="nymbal-site-footer" attributes={FOOTER} />
@@ -1,5 +1,15 @@
1
1
  ---
2
- import Header from '../../components/Header.astro'
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
- <Header />
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 CategoryNav from '../../components/CategoryNav.astro'
3
- import { fetchCategories } from '../../lib/api'
2
+ import NymbalElement from '@nymbalhq/web-components/astro'
3
+ import { navFragment } from '../../lib/fragments'
4
4
 
5
- Astro.response.headers.set('Cache-Control', 'public, s-maxage=21600, stale-while-revalidate=300')
5
+ const nav = await navFragment()
6
+ if (nav instanceof Response) return nav
6
7
 
7
- const categories = await fetchCategories()
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
- <CategoryNav categories={categories} />
12
+ <NymbalElement tag="nymbal-category-nav" attributes={{ categories: JSON.stringify(nav.categories) }} />
@@ -1,14 +1,13 @@
1
1
  ---
2
- import ProductCard from '../../../components/ProductCard.astro'
3
- import { fetchProduct } from '../../../lib/api'
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
- Astro.response.headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=60')
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
- if (!product) {
11
- return new Response('Not found', { status: 404 })
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
- <ProductCard product={product} />
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
- <CategoryNav categories={categories} />
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.length > 0 ? (
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 OrderSummary from '../../components/OrderSummary.astro'
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 lineItems = (order.lineItems ?? []).map((item) => ({
15
- name: `${item.productName}${item.variantName ? ` - ${item.variantName}` : ''}`,
16
- qty: item.qty,
17
- priceMinor: item.unitPriceMinor,
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 Breadcrumb from '../../components/Breadcrumb.astro'
5
- import ImageGallery from '../../components/ImageGallery.astro'
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.categoryIds?.[0] ?? undefined,
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
- <Breadcrumb
55
- items={[
56
- { label: 'Home', href: '/' },
57
- { label: 'Products', href: '/products' },
58
- { label: product.name },
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
- <ImageGallery images={images} />
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
- <PriceDisplay
69
- priceMinor={defaultVariant?.priceMinor ?? product.priceMinor}
70
- currency={product.currency ?? 'GBP'}
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></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="1" min="1" max="99"></nymbal-quantity-selector>
82
- <nymbal-add-to-cart
83
- variant-id={defaultVariant?.id ?? product.id}
84
- qty="1"
85
- disabled={stock <= 0}
86
- ></nymbal-add-to-cart>
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
- <ReviewList reviews={reviews} />
101
- <ReviewForm productId={product.id} />
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
- <RelatedProducts products={relatedProducts} />
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 Breadcrumb from '../../components/Breadcrumb.astro'
6
- import ProductCard from '../../components/ProductCard.astro'
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
- <Breadcrumb items={[{ label: 'Home', href: '/' }, { label: 'Products' }]} />
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
- <div class="product-grid" id="product-grid">
61
- {products.map((product) => (
62
- <ProductCard product={product} />
63
- ))}
64
- </div>
65
-
66
- {products.length === 0 && (
67
- <div class="plp-empty">
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 '../styles/pages/home.css'
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
- <div id="search-results" data-testid="search-results">
31
- <div class="search-empty">
32
- <div class="search-empty-icon" aria-hidden="true">&#128269;</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', () => {