create-nymbal-app 0.0.0 → 0.2.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 (159) 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 +100 -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 +168 -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/pages/404.astro +18 -0
  42. package/scaffold/templates/astro/src/pages/500.astro +17 -0
  43. package/scaffold/templates/astro/src/pages/account/addresses.astro +110 -0
  44. package/scaffold/templates/astro/src/pages/account/index.astro +67 -0
  45. package/scaffold/templates/astro/src/pages/account/login.astro +55 -0
  46. package/scaffold/templates/astro/src/pages/account/orders/[orderNumber].astro +108 -0
  47. package/scaffold/templates/astro/src/pages/account/orders.astro +81 -0
  48. package/scaffold/templates/astro/src/pages/account/profile.astro +131 -0
  49. package/scaffold/templates/astro/src/pages/account/register.astro +94 -0
  50. package/scaffold/templates/astro/src/pages/cart.astro +16 -0
  51. package/scaffold/templates/astro/src/pages/category/[slug].astro +73 -0
  52. package/scaffold/templates/astro/src/pages/checkout.astro +100 -0
  53. package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -0
  54. package/scaffold/templates/astro/src/pages/fragments/header.astro +5 -0
  55. package/scaffold/templates/astro/src/pages/fragments/nav.astro +9 -0
  56. package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +14 -0
  57. package/scaffold/templates/astro/src/pages/index.astro +49 -0
  58. package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +127 -0
  59. package/scaffold/templates/astro/src/pages/product/[slug].astro +107 -0
  60. package/scaffold/templates/astro/src/pages/products/index.astro +62 -0
  61. package/scaffold/templates/astro/src/pages/search.astro +37 -0
  62. package/scaffold/templates/astro/src/scripts/account-guard.ts +22 -0
  63. package/scaffold/templates/astro/src/scripts/auth-forms.ts +89 -0
  64. package/scaffold/templates/astro/src/scripts/cart-page.ts +143 -0
  65. package/scaffold/templates/astro/src/scripts/checkout.ts +277 -0
  66. package/scaffold/templates/astro/src/scripts/image-gallery.ts +22 -0
  67. package/scaffold/templates/astro/src/scripts/init-client.ts +16 -0
  68. package/scaffold/templates/astro/src/scripts/order-status.ts +27 -0
  69. package/scaffold/templates/astro/src/scripts/pdp.ts +67 -0
  70. package/scaffold/templates/astro/src/scripts/plp.ts +109 -0
  71. package/scaffold/templates/astro/src/scripts/search.ts +145 -0
  72. package/scaffold/templates/astro/src/styles/global.css +362 -0
  73. package/scaffold/templates/astro/src/styles/pages/account.css +331 -0
  74. package/scaffold/templates/astro/src/styles/pages/cart.css +243 -0
  75. package/scaffold/templates/astro/src/styles/pages/checkout.css +270 -0
  76. package/scaffold/templates/astro/src/styles/pages/confirmation.css +142 -0
  77. package/scaffold/templates/astro/src/styles/pages/error.css +69 -0
  78. package/scaffold/templates/astro/src/styles/pages/home.css +269 -0
  79. package/scaffold/templates/astro/src/styles/pages/pdp.css +272 -0
  80. package/scaffold/templates/astro/src/styles/pages/plp.css +213 -0
  81. package/scaffold/templates/astro/src/styles/pages/search.css +152 -0
  82. package/scaffold/templates/astro/tsconfig.json +3 -0
  83. package/scaffold/templates/nextjs/README.md +26 -0
  84. package/scaffold/templates/nextjs/_dot.env.example +6 -0
  85. package/scaffold/templates/nextjs/_dot.gitignore +17 -0
  86. package/scaffold/templates/nextjs/_dot.node-version +1 -0
  87. package/scaffold/templates/nextjs/_dot.npmrc +1 -0
  88. package/scaffold/templates/nextjs/app/account/addresses/page.tsx +10 -0
  89. package/scaffold/templates/nextjs/app/account/layout.tsx +57 -0
  90. package/scaffold/templates/nextjs/app/account/login/page.tsx +23 -0
  91. package/scaffold/templates/nextjs/app/account/orders/[orderNumber]/page.tsx +192 -0
  92. package/scaffold/templates/nextjs/app/account/orders/page.tsx +123 -0
  93. package/scaffold/templates/nextjs/app/account/page.tsx +59 -0
  94. package/scaffold/templates/nextjs/app/account/profile/page.tsx +10 -0
  95. package/scaffold/templates/nextjs/app/account/register/page.tsx +10 -0
  96. package/scaffold/templates/nextjs/app/cart/page.tsx +10 -0
  97. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +58 -0
  98. package/scaffold/templates/nextjs/app/checkout/page.tsx +10 -0
  99. package/scaffold/templates/nextjs/app/error.tsx +49 -0
  100. package/scaffold/templates/nextjs/app/globals.css +473 -0
  101. package/scaffold/templates/nextjs/app/layout.tsx +29 -0
  102. package/scaffold/templates/nextjs/app/not-found.tsx +38 -0
  103. package/scaffold/templates/nextjs/app/order/[orderNumber]/page.tsx +127 -0
  104. package/scaffold/templates/nextjs/app/page.tsx +63 -0
  105. package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +111 -0
  106. package/scaffold/templates/nextjs/app/products/page.tsx +40 -0
  107. package/scaffold/templates/nextjs/app/search/page.tsx +24 -0
  108. package/scaffold/templates/nextjs/components/client/AddressManager.tsx +181 -0
  109. package/scaffold/templates/nextjs/components/client/AuthGuard.tsx +55 -0
  110. package/scaffold/templates/nextjs/components/client/CartDrawerToggle.tsx +39 -0
  111. package/scaffold/templates/nextjs/components/client/CartPage.tsx +120 -0
  112. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +523 -0
  113. package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +68 -0
  114. package/scaffold/templates/nextjs/components/client/LoginForm.tsx +89 -0
  115. package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +12 -0
  116. package/scaffold/templates/nextjs/components/client/NewsletterSignup.tsx +74 -0
  117. package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +24 -0
  118. package/scaffold/templates/nextjs/components/client/OrderStatusRefresh.tsx +48 -0
  119. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +148 -0
  120. package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +61 -0
  121. package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +112 -0
  122. package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +131 -0
  123. package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +116 -0
  124. package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +27 -0
  125. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +141 -0
  126. package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +35 -0
  127. package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +36 -0
  128. package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +29 -0
  129. package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +60 -0
  130. package/scaffold/templates/nextjs/components/server/Footer.module.css +64 -0
  131. package/scaffold/templates/nextjs/components/server/Footer.tsx +79 -0
  132. package/scaffold/templates/nextjs/components/server/Header.module.css +147 -0
  133. package/scaffold/templates/nextjs/components/server/Header.tsx +102 -0
  134. package/scaffold/templates/nextjs/components/server/HeroSection.tsx +18 -0
  135. package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +79 -0
  136. package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +31 -0
  137. package/scaffold/templates/nextjs/components/server/ProductCard.tsx +59 -0
  138. package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +31 -0
  139. package/scaffold/templates/nextjs/components/server/ReviewList.tsx +62 -0
  140. package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -0
  141. package/scaffold/templates/nextjs/components/server/StockBadge.tsx +19 -0
  142. package/scaffold/templates/nextjs/lib/api.ts +89 -0
  143. package/scaffold/templates/nextjs/lib/client.ts +23 -0
  144. package/scaffold/templates/nextjs/lib/format.ts +7 -0
  145. package/scaffold/templates/nextjs/lib/order-status.ts +14 -0
  146. package/scaffold/templates/nextjs/lib/seo.ts +73 -0
  147. package/scaffold/templates/nextjs/next.config.mjs +9 -0
  148. package/scaffold/templates/nextjs/package.json +64 -0
  149. package/scaffold/templates/nextjs/styles/pages/account.module.css +377 -0
  150. package/scaffold/templates/nextjs/styles/pages/cart.module.css +240 -0
  151. package/scaffold/templates/nextjs/styles/pages/checkout.module.css +404 -0
  152. package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +237 -0
  153. package/scaffold/templates/nextjs/styles/pages/error.module.css +132 -0
  154. package/scaffold/templates/nextjs/styles/pages/home.module.css +285 -0
  155. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +319 -0
  156. package/scaffold/templates/nextjs/styles/pages/plp.module.css +280 -0
  157. package/scaffold/templates/nextjs/styles/pages/search.module.css +192 -0
  158. package/scaffold/templates/nextjs/tsconfig.json +41 -0
  159. package/README.md +0 -4
@@ -0,0 +1,131 @@
1
+ ---
2
+ import '../../styles/pages/account.css'
3
+ import AccountLayout from '../../layouts/AccountLayout.astro'
4
+ ---
5
+
6
+ <AccountLayout title="Profile">
7
+ <h1 class="account-page-title">Profile</h1>
8
+ <p class="account-page-subtitle">Update your personal information.</p>
9
+
10
+ <form id="profile-form" class="account-form" data-testid="profile-form">
11
+ <div class="account-form-row">
12
+ <div class="form-group">
13
+ <label for="profile-firstName">First Name</label>
14
+ <input
15
+ type="text"
16
+ id="profile-firstName"
17
+ name="firstName"
18
+ autocomplete="given-name"
19
+ data-testid="profile-firstName"
20
+ />
21
+ </div>
22
+
23
+ <div class="form-group">
24
+ <label for="profile-lastName">Last Name</label>
25
+ <input
26
+ type="text"
27
+ id="profile-lastName"
28
+ name="lastName"
29
+ autocomplete="family-name"
30
+ data-testid="profile-lastName"
31
+ />
32
+ </div>
33
+ </div>
34
+
35
+ <div class="form-group">
36
+ <label for="profile-email">Email</label>
37
+ <input
38
+ type="email"
39
+ id="profile-email"
40
+ name="email"
41
+ disabled
42
+ autocomplete="email"
43
+ data-testid="profile-email"
44
+ style="opacity: 0.6; cursor: not-allowed;"
45
+ />
46
+ </div>
47
+
48
+ <div class="form-group">
49
+ <label for="profile-phone">Phone</label>
50
+ <input
51
+ type="tel"
52
+ id="profile-phone"
53
+ name="phone"
54
+ autocomplete="tel"
55
+ data-testid="profile-phone"
56
+ />
57
+ </div>
58
+
59
+ <div class="account-form-actions">
60
+ <button type="submit" class="btn btn-primary" data-testid="profile-save">Save Changes</button>
61
+ </div>
62
+
63
+ <p class="form-message" id="profile-message" style="display:none; margin-top: var(--nymbal-spacing-md, 1rem); font-size: var(--nymbal-font-size-sm, 0.875rem);"></p>
64
+ </form>
65
+ </AccountLayout>
66
+
67
+ <script>
68
+ import { client } from '../../lib/client.js'
69
+
70
+ const form = document.getElementById('profile-form') as HTMLFormElement | null
71
+ const messageEl = document.getElementById('profile-message')
72
+
73
+ function populateForm() {
74
+ const profile = client.auth.getState().customer
75
+ if (!profile) return
76
+
77
+ const firstNameEl = document.getElementById('profile-firstName') as HTMLInputElement
78
+ const lastNameEl = document.getElementById('profile-lastName') as HTMLInputElement
79
+ const emailEl = document.getElementById('profile-email') as HTMLInputElement
80
+ const phoneEl = document.getElementById('profile-phone') as HTMLInputElement
81
+
82
+ if (firstNameEl) firstNameEl.value = profile.firstName ?? ''
83
+ if (lastNameEl) lastNameEl.value = profile.lastName ?? ''
84
+ if (emailEl) emailEl.value = profile.email ?? ''
85
+ if (phoneEl) phoneEl.value = profile.phone ?? ''
86
+ }
87
+
88
+ // Load profile data
89
+ async function init() {
90
+ try {
91
+ await client.auth.loadProfile()
92
+ } catch {
93
+ // ignore
94
+ }
95
+ populateForm()
96
+ }
97
+
98
+ client.auth.subscribe(populateForm)
99
+ init()
100
+
101
+ form?.addEventListener('submit', async (e) => {
102
+ e.preventDefault()
103
+ if (!form || !messageEl) return
104
+
105
+ const firstName = (document.getElementById('profile-firstName') as HTMLInputElement)?.value
106
+ const lastName = (document.getElementById('profile-lastName') as HTMLInputElement)?.value
107
+ const phone = (document.getElementById('profile-phone') as HTMLInputElement)?.value
108
+
109
+ const submitBtn = form.querySelector('[data-testid="profile-save"]') as HTMLButtonElement
110
+ if (submitBtn) {
111
+ submitBtn.disabled = true
112
+ submitBtn.textContent = 'Saving...'
113
+ }
114
+
115
+ try {
116
+ await client.auth.updateProfile({ firstName, lastName, phone })
117
+ messageEl.textContent = 'Profile updated successfully.'
118
+ messageEl.style.color = 'var(--nymbal-color-success, #16a34a)'
119
+ messageEl.style.display = 'block'
120
+ } catch (err) {
121
+ messageEl.textContent = err instanceof Error ? err.message : 'Failed to update profile.'
122
+ messageEl.style.color = 'var(--nymbal-color-error, #dc2626)'
123
+ messageEl.style.display = 'block'
124
+ } finally {
125
+ if (submitBtn) {
126
+ submitBtn.disabled = false
127
+ submitBtn.textContent = 'Save Changes'
128
+ }
129
+ }
130
+ })
131
+ </script>
@@ -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>