create-nymbal-app 0.0.0 → 0.3.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 (162) hide show
  1. package/bin/create-nymbal-app.mjs +9 -0
  2. package/dist/index.d.ts +37 -0
  3. package/dist/index.js +419 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +41 -7
  6. package/scaffold/project/astro/src/pages/index.astro +57 -0
  7. package/scaffold/templates/astro/README.md +26 -0
  8. package/scaffold/templates/astro/_dot.env.example +6 -0
  9. package/scaffold/templates/astro/_dot.gitignore +16 -0
  10. package/scaffold/templates/astro/_dot.node-version +1 -0
  11. package/scaffold/templates/astro/_dot.npmrc +1 -0
  12. package/scaffold/templates/astro/astro.config.mjs +14 -0
  13. package/scaffold/templates/astro/package.json +59 -0
  14. package/scaffold/templates/astro/src/components/AddressForm.astro +107 -0
  15. package/scaffold/templates/astro/src/components/Breadcrumb.astro +20 -0
  16. package/scaffold/templates/astro/src/components/CategoryNav.astro +27 -0
  17. package/scaffold/templates/astro/src/components/CheckoutSteps.astro +29 -0
  18. package/scaffold/templates/astro/src/components/Footer.astro +108 -0
  19. package/scaffold/templates/astro/src/components/Header.astro +192 -0
  20. package/scaffold/templates/astro/src/components/HeroSection.astro +17 -0
  21. package/scaffold/templates/astro/src/components/ImageGallery.astro +42 -0
  22. package/scaffold/templates/astro/src/components/NewsletterSignup.astro +39 -0
  23. package/scaffold/templates/astro/src/components/OrderSummary.astro +53 -0
  24. package/scaffold/templates/astro/src/components/Pagination.astro +13 -0
  25. package/scaffold/templates/astro/src/components/PriceDisplay.astro +25 -0
  26. package/scaffold/templates/astro/src/components/ProductCard.astro +44 -0
  27. package/scaffold/templates/astro/src/components/RelatedProducts.astro +28 -0
  28. package/scaffold/templates/astro/src/components/ReviewForm.astro +74 -0
  29. package/scaffold/templates/astro/src/components/ReviewList.astro +41 -0
  30. package/scaffold/templates/astro/src/components/SkipNav.astro +3 -0
  31. package/scaffold/templates/astro/src/components/SortDropdown.astro +26 -0
  32. package/scaffold/templates/astro/src/components/StockBadge.astro +32 -0
  33. package/scaffold/templates/astro/src/env.d.ts +2 -0
  34. package/scaffold/templates/astro/src/layouts/AccountLayout.astro +170 -0
  35. package/scaffold/templates/astro/src/layouts/BaseLayout.astro +55 -0
  36. package/scaffold/templates/astro/src/lib/api.ts +89 -0
  37. package/scaffold/templates/astro/src/lib/client.ts +13 -0
  38. package/scaffold/templates/astro/src/lib/format.ts +17 -0
  39. package/scaffold/templates/astro/src/lib/order-status.ts +14 -0
  40. package/scaffold/templates/astro/src/lib/seo.ts +62 -0
  41. package/scaffold/templates/astro/src/lib/submit-once.ts +44 -0
  42. package/scaffold/templates/astro/src/pages/404.astro +18 -0
  43. package/scaffold/templates/astro/src/pages/500.astro +17 -0
  44. package/scaffold/templates/astro/src/pages/account/addresses.astro +110 -0
  45. package/scaffold/templates/astro/src/pages/account/index.astro +67 -0
  46. package/scaffold/templates/astro/src/pages/account/login.astro +55 -0
  47. package/scaffold/templates/astro/src/pages/account/orders/[orderNumber].astro +108 -0
  48. package/scaffold/templates/astro/src/pages/account/orders.astro +81 -0
  49. package/scaffold/templates/astro/src/pages/account/profile.astro +69 -0
  50. package/scaffold/templates/astro/src/pages/account/register.astro +94 -0
  51. package/scaffold/templates/astro/src/pages/cart.astro +16 -0
  52. package/scaffold/templates/astro/src/pages/category/[slug].astro +73 -0
  53. package/scaffold/templates/astro/src/pages/checkout.astro +100 -0
  54. package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -0
  55. package/scaffold/templates/astro/src/pages/fragments/header.astro +5 -0
  56. package/scaffold/templates/astro/src/pages/fragments/nav.astro +9 -0
  57. package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +14 -0
  58. package/scaffold/templates/astro/src/pages/index.astro +49 -0
  59. package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +127 -0
  60. package/scaffold/templates/astro/src/pages/product/[slug].astro +107 -0
  61. package/scaffold/templates/astro/src/pages/products/index.astro +62 -0
  62. package/scaffold/templates/astro/src/pages/search.astro +37 -0
  63. package/scaffold/templates/astro/src/scripts/account-guard.ts +22 -0
  64. package/scaffold/templates/astro/src/scripts/auth-forms.ts +82 -0
  65. package/scaffold/templates/astro/src/scripts/cart-page.ts +143 -0
  66. package/scaffold/templates/astro/src/scripts/checkout.ts +272 -0
  67. package/scaffold/templates/astro/src/scripts/image-gallery.ts +22 -0
  68. package/scaffold/templates/astro/src/scripts/init-client.ts +16 -0
  69. package/scaffold/templates/astro/src/scripts/order-status.ts +27 -0
  70. package/scaffold/templates/astro/src/scripts/pdp.ts +67 -0
  71. package/scaffold/templates/astro/src/scripts/plp.ts +109 -0
  72. package/scaffold/templates/astro/src/scripts/profile-form.ts +57 -0
  73. package/scaffold/templates/astro/src/scripts/review-form.ts +27 -0
  74. package/scaffold/templates/astro/src/scripts/search.ts +145 -0
  75. package/scaffold/templates/astro/src/styles/global.css +362 -0
  76. package/scaffold/templates/astro/src/styles/pages/account.css +331 -0
  77. package/scaffold/templates/astro/src/styles/pages/cart.css +243 -0
  78. package/scaffold/templates/astro/src/styles/pages/checkout.css +270 -0
  79. package/scaffold/templates/astro/src/styles/pages/confirmation.css +142 -0
  80. package/scaffold/templates/astro/src/styles/pages/error.css +69 -0
  81. package/scaffold/templates/astro/src/styles/pages/home.css +269 -0
  82. package/scaffold/templates/astro/src/styles/pages/pdp.css +272 -0
  83. package/scaffold/templates/astro/src/styles/pages/plp.css +213 -0
  84. package/scaffold/templates/astro/src/styles/pages/search.css +152 -0
  85. package/scaffold/templates/astro/tsconfig.json +3 -0
  86. package/scaffold/templates/nextjs/README.md +26 -0
  87. package/scaffold/templates/nextjs/_dot.env.example +6 -0
  88. package/scaffold/templates/nextjs/_dot.gitignore +17 -0
  89. package/scaffold/templates/nextjs/_dot.node-version +1 -0
  90. package/scaffold/templates/nextjs/_dot.npmrc +1 -0
  91. package/scaffold/templates/nextjs/app/account/addresses/page.tsx +10 -0
  92. package/scaffold/templates/nextjs/app/account/layout.tsx +57 -0
  93. package/scaffold/templates/nextjs/app/account/login/page.tsx +23 -0
  94. package/scaffold/templates/nextjs/app/account/orders/[orderNumber]/page.tsx +192 -0
  95. package/scaffold/templates/nextjs/app/account/orders/page.tsx +123 -0
  96. package/scaffold/templates/nextjs/app/account/page.tsx +59 -0
  97. package/scaffold/templates/nextjs/app/account/profile/page.tsx +10 -0
  98. package/scaffold/templates/nextjs/app/account/register/page.tsx +10 -0
  99. package/scaffold/templates/nextjs/app/cart/page.tsx +10 -0
  100. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +58 -0
  101. package/scaffold/templates/nextjs/app/checkout/page.tsx +10 -0
  102. package/scaffold/templates/nextjs/app/error.tsx +49 -0
  103. package/scaffold/templates/nextjs/app/globals.css +473 -0
  104. package/scaffold/templates/nextjs/app/layout.tsx +29 -0
  105. package/scaffold/templates/nextjs/app/not-found.tsx +38 -0
  106. package/scaffold/templates/nextjs/app/order/[orderNumber]/page.tsx +127 -0
  107. package/scaffold/templates/nextjs/app/page.tsx +63 -0
  108. package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +111 -0
  109. package/scaffold/templates/nextjs/app/products/page.tsx +40 -0
  110. package/scaffold/templates/nextjs/app/search/page.tsx +24 -0
  111. package/scaffold/templates/nextjs/components/client/AddressManager.tsx +181 -0
  112. package/scaffold/templates/nextjs/components/client/AuthGuard.tsx +55 -0
  113. package/scaffold/templates/nextjs/components/client/CartDrawerToggle.tsx +39 -0
  114. package/scaffold/templates/nextjs/components/client/CartPage.tsx +120 -0
  115. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +523 -0
  116. package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +68 -0
  117. package/scaffold/templates/nextjs/components/client/LoginForm.tsx +89 -0
  118. package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +12 -0
  119. package/scaffold/templates/nextjs/components/client/NewsletterSignup.tsx +74 -0
  120. package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +24 -0
  121. package/scaffold/templates/nextjs/components/client/OrderStatusRefresh.tsx +48 -0
  122. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +148 -0
  123. package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +61 -0
  124. package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +112 -0
  125. package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +131 -0
  126. package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +116 -0
  127. package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +27 -0
  128. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +141 -0
  129. package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +35 -0
  130. package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +36 -0
  131. package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +29 -0
  132. package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +60 -0
  133. package/scaffold/templates/nextjs/components/server/Footer.module.css +64 -0
  134. package/scaffold/templates/nextjs/components/server/Footer.tsx +79 -0
  135. package/scaffold/templates/nextjs/components/server/Header.module.css +147 -0
  136. package/scaffold/templates/nextjs/components/server/Header.tsx +102 -0
  137. package/scaffold/templates/nextjs/components/server/HeroSection.tsx +18 -0
  138. package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +79 -0
  139. package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +31 -0
  140. package/scaffold/templates/nextjs/components/server/ProductCard.tsx +59 -0
  141. package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +31 -0
  142. package/scaffold/templates/nextjs/components/server/ReviewList.tsx +62 -0
  143. package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -0
  144. package/scaffold/templates/nextjs/components/server/StockBadge.tsx +19 -0
  145. package/scaffold/templates/nextjs/lib/api.ts +89 -0
  146. package/scaffold/templates/nextjs/lib/client.ts +23 -0
  147. package/scaffold/templates/nextjs/lib/format.ts +7 -0
  148. package/scaffold/templates/nextjs/lib/order-status.ts +14 -0
  149. package/scaffold/templates/nextjs/lib/seo.ts +73 -0
  150. package/scaffold/templates/nextjs/next.config.mjs +9 -0
  151. package/scaffold/templates/nextjs/package.json +64 -0
  152. package/scaffold/templates/nextjs/styles/pages/account.module.css +377 -0
  153. package/scaffold/templates/nextjs/styles/pages/cart.module.css +240 -0
  154. package/scaffold/templates/nextjs/styles/pages/checkout.module.css +404 -0
  155. package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +237 -0
  156. package/scaffold/templates/nextjs/styles/pages/error.module.css +132 -0
  157. package/scaffold/templates/nextjs/styles/pages/home.module.css +285 -0
  158. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +319 -0
  159. package/scaffold/templates/nextjs/styles/pages/plp.module.css +280 -0
  160. package/scaffold/templates/nextjs/styles/pages/search.module.css +192 -0
  161. package/scaffold/templates/nextjs/tsconfig.json +41 -0
  162. package/README.md +0 -4
@@ -0,0 +1,94 @@
1
+ ---
2
+ import '../../styles/pages/account.css'
3
+ import BaseLayout from '../../layouts/BaseLayout.astro'
4
+ ---
5
+
6
+ <BaseLayout title="Create Account | Nymbal" description="Create a new Nymbal account.">
7
+ <div class="auth-page">
8
+ <div class="auth-card">
9
+ <h1 class="auth-card-title">Create Account</h1>
10
+ <p class="auth-card-subtitle">Join us and start shopping.</p>
11
+
12
+ <div class="checkout-error" id="register-error" style="display:none; margin-bottom: var(--nymbal-spacing-md, 1rem);" role="alert"></div>
13
+
14
+ <form id="register-form" data-testid="register-form">
15
+ <div class="account-form-row">
16
+ <div class="form-group">
17
+ <label for="register-firstName">First Name</label>
18
+ <input
19
+ type="text"
20
+ id="register-firstName"
21
+ name="firstName"
22
+ autocomplete="given-name"
23
+ placeholder="First name"
24
+ data-testid="register-firstName"
25
+ />
26
+ </div>
27
+
28
+ <div class="form-group">
29
+ <label for="register-lastName">Last Name</label>
30
+ <input
31
+ type="text"
32
+ id="register-lastName"
33
+ name="lastName"
34
+ autocomplete="family-name"
35
+ placeholder="Last name"
36
+ data-testid="register-lastName"
37
+ />
38
+ </div>
39
+ </div>
40
+
41
+ <div class="form-group">
42
+ <label for="register-email">Email</label>
43
+ <input
44
+ type="email"
45
+ id="register-email"
46
+ name="email"
47
+ required
48
+ autocomplete="email"
49
+ placeholder="you@example.com"
50
+ data-testid="register-email"
51
+ />
52
+ </div>
53
+
54
+ <div class="form-group">
55
+ <label for="register-password">Password</label>
56
+ <input
57
+ type="password"
58
+ id="register-password"
59
+ name="password"
60
+ required
61
+ autocomplete="new-password"
62
+ placeholder="Choose a password"
63
+ minlength="8"
64
+ data-testid="register-password"
65
+ />
66
+ </div>
67
+
68
+ <div class="form-group">
69
+ <label for="register-phone">Phone (optional)</label>
70
+ <input
71
+ type="tel"
72
+ id="register-phone"
73
+ name="phone"
74
+ autocomplete="tel"
75
+ placeholder="+44 7700 900000"
76
+ data-testid="register-phone"
77
+ />
78
+ </div>
79
+
80
+ <button type="submit" class="btn btn-primary" data-testid="register-submit">
81
+ Create Account
82
+ </button>
83
+ </form>
84
+
85
+ <p class="auth-footer">
86
+ Already have an account? <a href="/account/login">Sign in</a>
87
+ </p>
88
+ </div>
89
+ </div>
90
+ </BaseLayout>
91
+
92
+ <script>
93
+ import '../../scripts/auth-forms.js'
94
+ </script>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import '../styles/pages/cart.css'
3
+ import BaseLayout from '../layouts/BaseLayout.astro'
4
+ ---
5
+
6
+ <BaseLayout title="Your Cart | Nymbal" description="Review the items in your cart.">
7
+ <div class="container section" id="cart-content" data-testid="cart-page">
8
+ <div class="cart-empty">
9
+ <h1 class="cart-empty-title">Loading your cart...</h1>
10
+ </div>
11
+ </div>
12
+ </BaseLayout>
13
+
14
+ <script>
15
+ import '../scripts/cart-page.js'
16
+ </script>
@@ -0,0 +1,73 @@
1
+ ---
2
+ import '../../styles/pages/plp.css'
3
+ import '../../styles/pages/home.css'
4
+ import BaseLayout from '../../layouts/BaseLayout.astro'
5
+ import Breadcrumb from '../../components/Breadcrumb.astro'
6
+ import ProductCard from '../../components/ProductCard.astro'
7
+ import SortDropdown from '../../components/SortDropdown.astro'
8
+ import Pagination from '../../components/Pagination.astro'
9
+ import { fetchProducts, fetchCategories, sortFromUrl } from '../../lib/api'
10
+
11
+ const { slug } = Astro.params
12
+ const sort = sortFromUrl(Astro.url)
13
+
14
+ const [productResult, categories] = await Promise.all([
15
+ fetchProducts({ limit: 24, category: slug, sort }),
16
+ fetchCategories(),
17
+ ])
18
+
19
+ const products = productResult.items
20
+ const category = categories.find((c) => c.slug === slug)
21
+ const categoryName = category?.name ?? slug ?? 'Category'
22
+ const facets = productResult.facets ?? []
23
+ ---
24
+
25
+ <BaseLayout
26
+ title={`${categoryName} | Nymbal`}
27
+ description={category?.description ?? `Browse ${categoryName} products.`}
28
+ >
29
+ <div class="container section">
30
+ <Breadcrumb items={[{ label: 'Home', href: '/' }, { label: categoryName }]} />
31
+
32
+ <div class="plp-header">
33
+ <h1 class="plp-title">{categoryName}</h1>
34
+ {category?.description && (
35
+ <p style="font-size: var(--nymbal-font-size-sm, 0.875rem); color: var(--nymbal-color-text-muted, #71717a); margin-bottom: var(--nymbal-spacing-sm, 0.5rem);">
36
+ {category.description}
37
+ </p>
38
+ )}
39
+ <p class="plp-count" id="product-count">{products.length} products</p>
40
+ </div>
41
+
42
+ <div class="plp-layout">
43
+ <aside class="plp-sidebar">
44
+ <nymbal-product-filter
45
+ facets={JSON.stringify(facets)}
46
+ ></nymbal-product-filter>
47
+ </aside>
48
+
49
+ <div class="plp-grid-area">
50
+ <SortDropdown sort={sort} />
51
+
52
+ <div class="product-grid" id="product-grid" data-category={slug}>
53
+ {products.map((product) => (
54
+ <ProductCard product={product} />
55
+ ))}
56
+ </div>
57
+
58
+ {products.length === 0 && (
59
+ <div class="plp-empty">
60
+ <h2 class="plp-empty-title">No products found</h2>
61
+ <p class="plp-empty-text">Check back soon for new arrivals.</p>
62
+ </div>
63
+ )}
64
+
65
+ <Pagination />
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </BaseLayout>
70
+
71
+ <script>
72
+ import '../../scripts/plp.js'
73
+ </script>
@@ -0,0 +1,100 @@
1
+ ---
2
+ import '../styles/pages/checkout.css'
3
+ import BaseLayout from '../layouts/BaseLayout.astro'
4
+ import CheckoutSteps from '../components/CheckoutSteps.astro'
5
+ import AddressForm from '../components/AddressForm.astro'
6
+ ---
7
+
8
+ <BaseLayout title="Checkout | Nymbal" description="Complete your purchase.">
9
+ <div class="container section">
10
+ <h1 style="font-size: 1.75rem; font-weight: var(--nymbal-font-weight-bold, 700); margin-bottom: var(--nymbal-spacing-md, 1rem);">Checkout</h1>
11
+
12
+ <div id="checkout-steps-container">
13
+ <CheckoutSteps currentStep="contact" />
14
+ </div>
15
+
16
+ <div class="checkout-error" id="checkout-error" style="display:none;" data-testid="checkout-error" role="alert"></div>
17
+
18
+ <div class="checkout-layout">
19
+ <div class="checkout-form">
20
+ <!-- Contact Section -->
21
+ <section class="checkout-section" id="checkout-contact" data-testid="checkout-contact">
22
+ <h2 class="checkout-section-title">Contact Information</h2>
23
+ <form id="contact-form">
24
+ <div class="form-group">
25
+ <label for="contact-email">Email Address</label>
26
+ <input
27
+ type="email"
28
+ id="contact-email"
29
+ name="email"
30
+ required
31
+ autocomplete="email"
32
+ placeholder="you@example.com"
33
+ data-testid="contact-email"
34
+ />
35
+ </div>
36
+ <button type="submit" class="btn btn-primary" data-testid="contact-continue">
37
+ Continue to Shipping
38
+ </button>
39
+ </form>
40
+ </section>
41
+
42
+ <!-- Shipping Section -->
43
+ <section class="checkout-section" id="checkout-shipping" style="display:none;" data-testid="checkout-shipping">
44
+ <h2 class="checkout-section-title">Shipping Address</h2>
45
+ <form id="shipping-form">
46
+ <AddressForm id="shipping" />
47
+ <button type="submit" class="btn btn-primary" style="margin-top: var(--nymbal-spacing-md, 1rem);" data-testid="shipping-continue">
48
+ Continue to Payment
49
+ </button>
50
+ </form>
51
+ </section>
52
+
53
+ <!-- Payment Section -->
54
+ <section class="checkout-section" id="checkout-payment" style="display:none;" data-testid="checkout-payment">
55
+ <h2 class="checkout-section-title">Payment</h2>
56
+ <form id="payment-form" novalidate>
57
+ <div class="form-group">
58
+ <label>
59
+ <input type="checkbox" id="billing-same" checked />
60
+ Billing address same as shipping
61
+ </label>
62
+ </div>
63
+
64
+ <div id="billing-fields" style="display:none;">
65
+ <h3 class="checkout-section-title" style="font-size: 1rem; margin-top: var(--nymbal-spacing-md, 1rem);">Billing Address</h3>
66
+ <div id="billing-form">
67
+ <AddressForm id="billing" />
68
+ </div>
69
+ </div>
70
+
71
+ <div class="payment-section" style="margin-top: var(--nymbal-spacing-md, 1rem);">
72
+ <label style="margin-bottom: var(--nymbal-spacing-sm, 0.5rem);">Card Details</label>
73
+ <div id="stripe-card-element" style="padding: var(--nymbal-spacing-sm, 0.5rem) var(--nymbal-spacing-md, 1rem); border: 1px solid var(--nymbal-color-border, #e4e4e7); border-radius: var(--nymbal-radius-md, 0.5rem); background: var(--nymbal-color-surface, #fff); min-height: 40px;" data-testid="stripe-card-element">
74
+ <!-- Stripe CardElement mounts here -->
75
+ </div>
76
+ </div>
77
+
78
+ <div class="checkout-submit">
79
+ <button type="submit" class="btn btn-primary" data-testid="place-order">
80
+ Place Order
81
+ </button>
82
+ <p class="checkout-terms">
83
+ By placing your order, you agree to our terms and conditions.
84
+ </p>
85
+ </div>
86
+ </form>
87
+ </section>
88
+ </div>
89
+
90
+ <aside class="checkout-summary" id="checkout-summary" data-testid="checkout-summary">
91
+ <h2 class="checkout-summary-title">Order Summary</h2>
92
+ <p style="font-size: var(--nymbal-font-size-sm, 0.875rem); color: var(--nymbal-color-text-muted, #71717a);">Loading cart...</p>
93
+ </aside>
94
+ </div>
95
+ </div>
96
+ </BaseLayout>
97
+
98
+ <script>
99
+ import '../scripts/checkout.js'
100
+ </script>
@@ -0,0 +1,5 @@
1
+ ---
2
+ import Footer from '../../components/Footer.astro'
3
+ Astro.response.headers.set('Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=300')
4
+ ---
5
+ <Footer />
@@ -0,0 +1,5 @@
1
+ ---
2
+ import Header from '../../components/Header.astro'
3
+ Astro.response.headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=300')
4
+ ---
5
+ <Header />
@@ -0,0 +1,9 @@
1
+ ---
2
+ import CategoryNav from '../../components/CategoryNav.astro'
3
+ import { fetchCategories } from '../../lib/api'
4
+
5
+ Astro.response.headers.set('Cache-Control', 'public, s-maxage=21600, stale-while-revalidate=300')
6
+
7
+ const categories = await fetchCategories()
8
+ ---
9
+ <CategoryNav categories={categories} />
@@ -0,0 +1,14 @@
1
+ ---
2
+ import ProductCard from '../../../components/ProductCard.astro'
3
+ import { fetchProduct } from '../../../lib/api'
4
+
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!)
9
+
10
+ if (!product) {
11
+ return new Response('Not found', { status: 404 })
12
+ }
13
+ ---
14
+ <ProductCard product={product} />
@@ -0,0 +1,49 @@
1
+ ---
2
+ import '../styles/pages/home.css'
3
+ import BaseLayout from '../layouts/BaseLayout.astro'
4
+ import HeroSection from '../components/HeroSection.astro'
5
+ import CategoryNav from '../components/CategoryNav.astro'
6
+ import ProductCard from '../components/ProductCard.astro'
7
+ import NewsletterSignup from '../components/NewsletterSignup.astro'
8
+ import { fetchProducts, fetchCategories } from '../lib/api'
9
+ import { organizationJsonLd } from '../lib/seo'
10
+
11
+ const [productResult, categories] = await Promise.all([
12
+ fetchProducts({ limit: 8 }),
13
+ fetchCategories(),
14
+ ])
15
+
16
+ const products = productResult.items
17
+ const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
18
+ ---
19
+
20
+ <BaseLayout title="Nymbal — Modern Storefront" jsonLd={jsonLd}>
21
+ <HeroSection />
22
+
23
+ <section class="section container">
24
+ <h2 class="section-title">Shop by Category</h2>
25
+ <CategoryNav categories={categories} />
26
+ </section>
27
+
28
+ <section class="featured-section section">
29
+ <div class="container">
30
+ <div class="featured-header">
31
+ <h2 class="section-title" style="margin-bottom: 0;">Featured Products</h2>
32
+ <a href="/products" class="featured-link">View all</a>
33
+ </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
+ )}
45
+ </div>
46
+ </section>
47
+
48
+ <NewsletterSignup />
49
+ </BaseLayout>
@@ -0,0 +1,127 @@
1
+ ---
2
+ import '../../styles/pages/confirmation.css'
3
+ import BaseLayout from '../../layouts/BaseLayout.astro'
4
+ import OrderSummary from '../../components/OrderSummary.astro'
5
+ import { fetchOrder } from '../../lib/api'
6
+ import { formatPrice } from '../../lib/format'
7
+ import { isPaid, orderHeading } from '../../lib/order-status'
8
+
9
+ const { orderNumber } = Astro.params
10
+ const order = await fetchOrder(orderNumber!)
11
+
12
+ if (!order) return Astro.redirect('/404')
13
+
14
+ const lineItems = (order.lineItems ?? []).map((item) => ({
15
+ name: `${item.productName}${item.variantName ? ` - ${item.variantName}` : ''}`,
16
+ qty: item.qty,
17
+ priceMinor: item.unitPriceMinor,
18
+ }))
19
+
20
+ const shippingAddress = order.shippingAddress
21
+ const heading = orderHeading(order.status)
22
+ const paid = isPaid(order.status)
23
+ ---
24
+
25
+ <BaseLayout title={`${heading} | Nymbal`} description="Your order with Nymbal.">
26
+ <div class="container section">
27
+ <div class="confirmation-page" data-order-number={order.orderNumber} data-order-status={order.status}>
28
+ {paid && (
29
+ <div class="confirmation-icon">
30
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
31
+ <polyline points="20 6 9 17 4 12"></polyline>
32
+ </svg>
33
+ </div>
34
+ )}
35
+
36
+ <h1 class="confirmation-title" data-testid="order-heading">{heading}</h1>
37
+ {paid ? (
38
+ <p class="confirmation-subtitle">
39
+ Thank you. Your order <strong data-testid="order-number">{order.orderNumber}</strong> is paid.
40
+ We'll send a confirmation to <strong>{order.email}</strong>.
41
+ </p>
42
+ ) : order.status === 'pending' ? (
43
+ <p class="confirmation-subtitle">
44
+ Your order <strong data-testid="order-number">{order.orderNumber}</strong> is waiting for its payment.
45
+ We'll email <strong>{order.email}</strong> once it arrives.
46
+ </p>
47
+ ) : (
48
+ <p class="confirmation-subtitle">
49
+ Your order <strong data-testid="order-number">{order.orderNumber}</strong> is {order.status.replace(/_/g, ' ')}.
50
+ </p>
51
+ )}
52
+
53
+ <div class="confirmation-card">
54
+ <h2 class="confirmation-card-title">Order Details</h2>
55
+
56
+ <dl>
57
+ <div class="confirmation-detail-row">
58
+ <dt>Order Number</dt>
59
+ <dd>{order.orderNumber}</dd>
60
+ </div>
61
+ <div class="confirmation-detail-row">
62
+ <dt>Status</dt>
63
+ <dd style="text-transform: capitalize;" data-testid="order-status">{order.status}</dd>
64
+ </div>
65
+ </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
+ </div>
104
+
105
+ {shippingAddress && (
106
+ <div class="confirmation-card">
107
+ <h2 class="confirmation-card-title">Shipping Address</h2>
108
+ <p style="font-size: var(--nymbal-font-size-sm, 0.875rem); color: var(--nymbal-color-text-muted, #71717a); line-height: 1.6;">
109
+ {shippingAddress.addressLine1}<br />
110
+ {shippingAddress.addressLine2 && <>{shippingAddress.addressLine2}<br /></>}
111
+ {shippingAddress.city}, {shippingAddress.region} {shippingAddress.postalCode}<br />
112
+ {shippingAddress.country}
113
+ </p>
114
+ </div>
115
+ )}
116
+
117
+ <div class="confirmation-actions">
118
+ <a href="/account/register" class="btn btn-primary">Create an Account</a>
119
+ <a href="/products" class="btn btn-secondary">Continue Shopping</a>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </BaseLayout>
124
+
125
+ <script>
126
+ import '../../scripts/order-status.js'
127
+ </script>
@@ -0,0 +1,107 @@
1
+ ---
2
+ import '../../styles/pages/pdp.css'
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'
11
+ import { fetchProduct, fetchReviews, fetchProducts } from '../../lib/api'
12
+ import { productJsonLd, breadcrumbJsonLd } from '../../lib/seo'
13
+
14
+ const { slug } = Astro.params
15
+
16
+ const product = await fetchProduct(slug!)
17
+ if (!product) return Astro.redirect('/404')
18
+
19
+ const [reviewsResult, relatedResult] = await Promise.all([
20
+ fetchReviews(product.id),
21
+ fetchProducts({
22
+ limit: 4,
23
+ category: product.categoryIds?.[0] ?? undefined,
24
+ }),
25
+ ])
26
+
27
+ const reviews = reviewsResult.items ?? []
28
+ const relatedProducts = (relatedResult.items ?? []).filter((p) => p.id !== product.id).slice(0, 4)
29
+
30
+ const images = Array.isArray(product.media) && product.media.length > 0
31
+ ? product.media.map((m: any) => ({
32
+ url: typeof m === 'string' ? m : m.url,
33
+ alt: typeof m === 'string' ? product.name : (m.alt ?? product.name),
34
+ }))
35
+ : [{ url: `https://picsum.photos/seed/${product.slug}/600/600`, alt: product.name }]
36
+
37
+ const defaultVariant = product.variants?.[0]
38
+ const stock = defaultVariant?.stock ?? (product.inStock ? 10 : 0)
39
+
40
+ const jsonLd = productJsonLd(product, Astro.url.origin)
41
+ const breadcrumb = breadcrumbJsonLd([
42
+ { name: 'Home', url: Astro.url.origin },
43
+ { name: 'Products', url: `${Astro.url.origin}/products` },
44
+ { name: product.name, url: `${Astro.url.origin}/product/${product.slug}` },
45
+ ])
46
+
47
+ // The selector derives its option groups from the product's variants.
48
+ const hasVariantChoice = (product.variants?.length ?? 0) > 1
49
+ ---
50
+
51
+ <BaseLayout title={`${product.name} | Nymbal`} description={product.shortDescription ?? product.description ?? ''} jsonLd={jsonLd}>
52
+ <div class="container section" data-product-slug={product.slug}>
53
+ <Breadcrumb
54
+ items={[
55
+ { label: 'Home', href: '/' },
56
+ { label: 'Products', href: '/products' },
57
+ { label: product.name },
58
+ ]}
59
+ />
60
+
61
+ <div class="pdp-layout">
62
+ <ImageGallery images={images} />
63
+
64
+ <div class="pdp-info">
65
+ <h1 class="pdp-title" data-testid="product-title">{product.name}</h1>
66
+
67
+ <PriceDisplay
68
+ priceMinor={defaultVariant?.priceMinor ?? product.priceMinor}
69
+ currency={product.currency ?? 'GBP'}
70
+ />
71
+
72
+ <StockBadge stock={stock} />
73
+
74
+ <div class="pdp-actions">
75
+ {hasVariantChoice && (
76
+ <nymbal-variant-selector></nymbal-variant-selector>
77
+ )}
78
+
79
+ <div class="pdp-add-row">
80
+ <nymbal-quantity-selector value="1" min="1" max="99"></nymbal-quantity-selector>
81
+ <nymbal-add-to-cart
82
+ variant-id={defaultVariant?.id ?? product.id}
83
+ ></nymbal-add-to-cart>
84
+ </div>
85
+ </div>
86
+
87
+ {product.description && (
88
+ <div class="pdp-description" data-testid="product-description">
89
+ <Fragment set:html={product.description} />
90
+ </div>
91
+ )}
92
+ </div>
93
+ </div>
94
+
95
+ <section class="pdp-reviews">
96
+ <h2 class="pdp-reviews-title">Customer Reviews</h2>
97
+ <ReviewList reviews={reviews} />
98
+ <ReviewForm productId={product.id} />
99
+ </section>
100
+
101
+ <RelatedProducts products={relatedProducts} />
102
+ </div>
103
+ </BaseLayout>
104
+
105
+ <script>
106
+ import '../../scripts/pdp.js'
107
+ </script>
@@ -0,0 +1,62 @@
1
+ ---
2
+ import '../../styles/pages/plp.css'
3
+ import '../../styles/pages/home.css'
4
+ import BaseLayout from '../../layouts/BaseLayout.astro'
5
+ import Breadcrumb from '../../components/Breadcrumb.astro'
6
+ import ProductCard from '../../components/ProductCard.astro'
7
+ import SortDropdown from '../../components/SortDropdown.astro'
8
+ import Pagination from '../../components/Pagination.astro'
9
+ import { fetchProducts, fetchCategories, sortFromUrl } from '../../lib/api'
10
+
11
+ const sort = sortFromUrl(Astro.url)
12
+
13
+ const [productResult, categories] = await Promise.all([
14
+ fetchProducts({ limit: 24, sort }),
15
+ fetchCategories(),
16
+ ])
17
+
18
+ const products = productResult.items
19
+ const facets = productResult.facets ?? []
20
+ ---
21
+
22
+ <BaseLayout title="All Products | Nymbal" description="Browse our full product collection.">
23
+ <div class="container section">
24
+ <Breadcrumb items={[{ label: 'Home', href: '/' }, { label: 'Products' }]} />
25
+
26
+ <div class="plp-header">
27
+ <h1 class="plp-title">All Products</h1>
28
+ <p class="plp-count" id="product-count">{products.length} products</p>
29
+ </div>
30
+
31
+ <div class="plp-layout">
32
+ <aside class="plp-sidebar">
33
+ <nymbal-product-filter
34
+ facets={JSON.stringify(facets)}
35
+ ></nymbal-product-filter>
36
+ </aside>
37
+
38
+ <div class="plp-grid-area">
39
+ <SortDropdown sort={sort} />
40
+
41
+ <div class="product-grid" id="product-grid">
42
+ {products.map((product) => (
43
+ <ProductCard product={product} />
44
+ ))}
45
+ </div>
46
+
47
+ {products.length === 0 && (
48
+ <div class="plp-empty">
49
+ <h2 class="plp-empty-title">No products found</h2>
50
+ <p class="plp-empty-text">Try adjusting your filters.</p>
51
+ </div>
52
+ )}
53
+
54
+ <Pagination />
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </BaseLayout>
59
+
60
+ <script>
61
+ import '../../scripts/plp.js'
62
+ </script>