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,198 +0,0 @@
1
- ---
2
- import { fetchCapabilities } from '../lib/api'
3
-
4
- const capabilities = await fetchCapabilities()
5
- const canSearch = capabilities?.search.capabilities.includes('search') ?? false
6
- ---
7
- <header class="site-header" data-testid="site-header">
8
- <div class="header-container container">
9
- <a href="/" class="header-logo">Nymbal</a>
10
-
11
- <label class="hamburger" for="nav-toggle" aria-label="Toggle navigation">
12
- <span class="hamburger-line"></span>
13
- <span class="hamburger-line"></span>
14
- <span class="hamburger-line"></span>
15
- </label>
16
- <input type="checkbox" id="nav-toggle" class="nav-toggle visually-hidden" aria-label="Toggle navigation" />
17
-
18
- <div class="header-nav-wrapper">
19
- <nav class="header-nav" aria-label="Main navigation">
20
- <a href="/category/apparel" class="nav-link">Apparel</a>
21
- <a href="/category/home" class="nav-link">Home</a>
22
- <a href="/category/electronics" class="nav-link">Electronics</a>
23
- <a href="/category/books" class="nav-link">Books</a>
24
- <a href="/products" class="nav-link">All Products</a>
25
- </nav>
26
-
27
- <div class="header-actions">
28
- {canSearch && (
29
- <nymbal-search-bar placeholder="Search products..." debounce-ms="300"></nymbal-search-bar>
30
- )}
31
- <a href="/account" class="header-account-link" aria-label="Account">
32
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
33
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
34
- <circle cx="12" cy="7" r="4" />
35
- </svg>
36
- </a>
37
- <nymbal-mini-cart></nymbal-mini-cart>
38
- </div>
39
- </div>
40
- </div>
41
- </header>
42
-
43
- <style>
44
- .site-header {
45
- position: sticky;
46
- top: 0;
47
- z-index: var(--nymbal-z-index-sticky, 100);
48
- background-color: var(--nymbal-color-surface, #fff);
49
- border-bottom: 1px solid var(--nymbal-color-border, #e4e4e7);
50
- }
51
-
52
- .header-container {
53
- display: flex;
54
- align-items: center;
55
- gap: var(--nymbal-spacing-md, 1rem);
56
- padding-block: var(--nymbal-spacing-sm, 0.5rem);
57
- flex-wrap: wrap;
58
- }
59
-
60
- .header-logo {
61
- font-size: var(--nymbal-font-size-lg, 1.125rem);
62
- font-weight: var(--nymbal-font-weight-bold, 700);
63
- color: var(--nymbal-color-primary, #18181b);
64
- text-decoration: none;
65
- letter-spacing: -0.02em;
66
- flex-shrink: 0;
67
- }
68
-
69
- .header-logo:hover {
70
- color: var(--nymbal-color-accent, #2563eb);
71
- }
72
-
73
- /* Hamburger toggle */
74
- .hamburger {
75
- display: flex;
76
- flex-direction: column;
77
- justify-content: center;
78
- gap: 4px;
79
- width: 28px;
80
- height: 28px;
81
- cursor: pointer;
82
- margin-left: auto;
83
- padding: 2px;
84
- }
85
-
86
- .hamburger-line {
87
- display: block;
88
- width: 100%;
89
- height: 2px;
90
- background-color: var(--nymbal-color-primary, #18181b);
91
- border-radius: 1px;
92
- transition: transform var(--nymbal-transition-fast, 150ms) ease,
93
- opacity var(--nymbal-transition-fast, 150ms) ease;
94
- }
95
-
96
- .nav-toggle:checked ~ .header-nav-wrapper {
97
- display: flex;
98
- }
99
-
100
- .nav-toggle:checked ~ .hamburger .hamburger-line:nth-child(1) {
101
- transform: translateY(6px) rotate(45deg);
102
- }
103
-
104
- .nav-toggle:checked ~ .hamburger .hamburger-line:nth-child(2) {
105
- opacity: 0;
106
- }
107
-
108
- .nav-toggle:checked ~ .hamburger .hamburger-line:nth-child(3) {
109
- transform: translateY(-6px) rotate(-45deg);
110
- }
111
-
112
- .header-nav-wrapper {
113
- display: none;
114
- flex-direction: column;
115
- width: 100%;
116
- gap: var(--nymbal-spacing-md, 1rem);
117
- padding-block: var(--nymbal-spacing-sm, 0.5rem);
118
- }
119
-
120
- .header-nav {
121
- display: flex;
122
- flex-direction: column;
123
- gap: var(--nymbal-spacing-xs, 0.25rem);
124
- }
125
-
126
- .nav-link {
127
- display: block;
128
- padding: var(--nymbal-spacing-xs, 0.25rem) var(--nymbal-spacing-sm, 0.5rem);
129
- font-size: var(--nymbal-font-size-sm, 0.875rem);
130
- font-weight: var(--nymbal-font-weight-medium, 500);
131
- color: var(--nymbal-color-text, #18181b);
132
- text-decoration: none;
133
- border-radius: var(--nymbal-radius-sm, 0.25rem);
134
- transition: background-color var(--nymbal-transition-fast, 150ms) ease,
135
- color var(--nymbal-transition-fast, 150ms) ease;
136
- }
137
-
138
- .nav-link:hover {
139
- background-color: var(--nymbal-color-border, #e4e4e7);
140
- color: var(--nymbal-color-accent, #2563eb);
141
- }
142
-
143
- .header-actions {
144
- display: flex;
145
- align-items: center;
146
- gap: var(--nymbal-spacing-sm, 0.5rem);
147
- }
148
-
149
- .header-account-link {
150
- display: flex;
151
- align-items: center;
152
- justify-content: center;
153
- width: 36px;
154
- height: 36px;
155
- color: var(--nymbal-color-text, #18181b);
156
- text-decoration: none;
157
- border-radius: var(--nymbal-radius-full, 9999px);
158
- transition: background-color var(--nymbal-transition-fast, 150ms) ease,
159
- color var(--nymbal-transition-fast, 150ms) ease;
160
- }
161
-
162
- .header-account-link:hover {
163
- background-color: var(--nymbal-color-border, #e4e4e7);
164
- color: var(--nymbal-color-accent, #2563eb);
165
- }
166
-
167
- /* Desktop layout */
168
- @media (min-width: 769px) {
169
- .hamburger {
170
- display: none;
171
- }
172
-
173
- .header-container {
174
- flex-wrap: nowrap;
175
- padding-block: var(--nymbal-spacing-md, 1rem);
176
- }
177
-
178
- .header-nav-wrapper {
179
- display: flex;
180
- flex-direction: row;
181
- align-items: center;
182
- justify-content: space-between;
183
- width: auto;
184
- flex: 1;
185
- padding-block: 0;
186
- gap: var(--nymbal-spacing-lg, 1.5rem);
187
- }
188
-
189
- .header-nav {
190
- flex-direction: row;
191
- gap: var(--nymbal-spacing-xs, 0.25rem);
192
- }
193
-
194
- .nav-link {
195
- padding: var(--nymbal-spacing-xs, 0.25rem) var(--nymbal-spacing-sm, 0.5rem);
196
- }
197
- }
198
- </style>
@@ -1,42 +0,0 @@
1
- ---
2
- import '../styles/pages/pdp.css'
3
-
4
- interface Props {
5
- images: Array<{ url: string; alt: string }>
6
- }
7
-
8
- const { images } = Astro.props
9
- const mainImage = images[0] ?? { url: 'https://picsum.photos/seed/placeholder/600/600', alt: 'Product image' }
10
- ---
11
-
12
- <div class="pdp-gallery" data-testid="product-gallery">
13
- <img
14
- src={mainImage.url}
15
- alt={mainImage.alt}
16
- class="pdp-main-image"
17
- id="gallery-main-image"
18
- width="600"
19
- height="600"
20
- />
21
- {images.length > 1 && (
22
- <div class="pdp-thumbnails" id="gallery-thumbnails">
23
- {images.map((img, i) => (
24
- <img
25
- src={img.url}
26
- alt={img.alt}
27
- class={`pdp-thumbnail ${i === 0 ? 'active' : ''}`}
28
- data-index={i}
29
- data-url={img.url}
30
- data-alt={img.alt}
31
- width="80"
32
- height="80"
33
- loading="lazy"
34
- />
35
- ))}
36
- </div>
37
- )}
38
- </div>
39
-
40
- <script>
41
- import '../scripts/image-gallery.js'
42
- </script>
@@ -1,53 +0,0 @@
1
- ---
2
- import { formatPrice } from '../lib/format'
3
-
4
- interface Props {
5
- items: Array<{ name: string; qty: number; priceMinor: number }>
6
- subtotalMinor: number
7
- currency: string
8
- shippingMinor?: number
9
- taxMinor?: number
10
- totalMinor?: number
11
- }
12
-
13
- const { items, subtotalMinor, currency, shippingMinor, taxMinor, totalMinor } = Astro.props
14
- const computedTotal = totalMinor ?? subtotalMinor + (shippingMinor ?? 0) + (taxMinor ?? 0)
15
- ---
16
-
17
- <div class="order-summary" data-testid="order-summary">
18
- <div class="checkout-summary-items">
19
- {items.map((item) => (
20
- <div class="checkout-summary-item">
21
- <span class="checkout-summary-item-name">{item.name}</span>
22
- <span class="checkout-summary-item-qty">x{item.qty}</span>
23
- <span class="checkout-summary-item-price">
24
- {formatPrice(item.priceMinor * item.qty, currency)}
25
- </span>
26
- </div>
27
- ))}
28
- </div>
29
-
30
- <dl>
31
- <div class="checkout-summary-row">
32
- <dt>Subtotal</dt>
33
- <dd>{formatPrice(subtotalMinor, currency)}</dd>
34
- </div>
35
- {shippingMinor != null && (
36
- <div class="checkout-summary-row">
37
- <dt>Shipping</dt>
38
- <dd>{shippingMinor === 0 ? 'Free' : formatPrice(shippingMinor, currency)}</dd>
39
- </div>
40
- )}
41
- {taxMinor != null && (
42
- <div class="checkout-summary-row">
43
- <dt>Tax</dt>
44
- <dd>{formatPrice(taxMinor, currency)}</dd>
45
- </div>
46
- )}
47
- </dl>
48
-
49
- <div class="checkout-summary-total">
50
- <span>Total</span>
51
- <span>{formatPrice(computedTotal, currency)}</span>
52
- </div>
53
- </div>
@@ -1,13 +0,0 @@
1
- ---
2
- ---
3
-
4
- <div class="plp-pagination">
5
- <button
6
- type="button"
7
- class="btn btn-secondary"
8
- id="load-more-btn"
9
- data-testid="load-more"
10
- >
11
- Load More
12
- </button>
13
- </div>
@@ -1,25 +0,0 @@
1
- ---
2
- import { formatPrice } from '../lib/format'
3
-
4
- interface Props {
5
- priceMinor: number
6
- currency: string
7
- compareAtMinor?: number
8
- }
9
-
10
- const { priceMinor, currency, compareAtMinor } = Astro.props
11
- const price = formatPrice(priceMinor, currency)
12
- const comparePrice = compareAtMinor ? formatPrice(compareAtMinor, currency) : null
13
- const isOnSale = compareAtMinor != null && compareAtMinor > priceMinor
14
- ---
15
-
16
- <div class="pdp-price" data-testid="product-price">
17
- {isOnSale && comparePrice && (
18
- <span
19
- style="text-decoration: line-through; color: var(--nymbal-color-text-muted, #71717a); font-weight: var(--nymbal-font-weight-normal, 400); margin-right: var(--nymbal-spacing-sm, 0.5rem);"
20
- >
21
- {comparePrice}
22
- </span>
23
- )}
24
- <span>{price}</span>
25
- </div>
@@ -1,44 +0,0 @@
1
- ---
2
- import { formatPrice } from '../lib/format'
3
-
4
- interface Props {
5
- product: {
6
- slug: string
7
- name: string
8
- priceMinor: number | null
9
- priceRange?: { minMinor: number } | null
10
- currency: string | null
11
- media: any
12
- imageUrl?: string
13
- }
14
- }
15
-
16
- const { product } = Astro.props
17
- const imageUrl =
18
- product.imageUrl ??
19
- (Array.isArray(product.media) && product.media[0]?.url) ??
20
- `https://picsum.photos/seed/${product.slug}/600/600`
21
- // The "from" price, which the price sorts order by.
22
- const fromMinor = product.priceRange?.minMinor ?? product.priceMinor
23
- const price =
24
- fromMinor != null ? formatPrice(fromMinor, product.currency ?? 'GBP') : ''
25
- ---
26
-
27
- <a
28
- href={`/product/${product.slug}`}
29
- class="product-card"
30
- data-testid={`product-card-${product.slug}`}
31
- >
32
- <img
33
- src={imageUrl}
34
- alt={product.name}
35
- class="product-card-image"
36
- loading="lazy"
37
- width="600"
38
- height="600"
39
- />
40
- <div class="product-card-body">
41
- <span class="product-card-name">{product.name}</span>
42
- {price && <span class="product-card-price">{price}</span>}
43
- </div>
44
- </a>
@@ -1,28 +0,0 @@
1
- ---
2
- import ProductCard from './ProductCard.astro'
3
-
4
- interface Props {
5
- products: Array<{
6
- slug: string
7
- name: string
8
- priceMinor: number | null
9
- currency: string | null
10
- media: any
11
- imageUrl?: string
12
- }>
13
- }
14
-
15
- const { products } = Astro.props
16
- const displayProducts = products.slice(0, 4)
17
- ---
18
-
19
- {displayProducts.length > 0 && (
20
- <section class="section" data-testid="related-products">
21
- <h2 class="section-title">You may also like</h2>
22
- <div class="product-grid">
23
- {displayProducts.map((product) => (
24
- <ProductCard product={product} />
25
- ))}
26
- </div>
27
- </section>
28
- )}
@@ -1,74 +0,0 @@
1
- ---
2
- interface Props {
3
- productId: string
4
- }
5
-
6
- const { productId } = Astro.props
7
- ---
8
-
9
- <form
10
- class="review-form"
11
- id="review-form"
12
- data-product-id={productId}
13
- data-testid="review-form"
14
- >
15
- <h3 style="font-size: 1.125rem; font-weight: var(--nymbal-font-weight-bold, 700); margin-bottom: var(--nymbal-spacing-md, 1rem);">
16
- Write a Review
17
- </h3>
18
-
19
- <div class="form-group">
20
- <label for="review-rating">Rating</label>
21
- <select id="review-rating" name="rating" required data-testid="review-rating">
22
- <option value="">Select a rating</option>
23
- <option value="5">5 - Excellent</option>
24
- <option value="4">4 - Very Good</option>
25
- <option value="3">3 - Good</option>
26
- <option value="2">2 - Fair</option>
27
- <option value="1">1 - Poor</option>
28
- </select>
29
- </div>
30
-
31
- <div class="form-group">
32
- <label for="review-title">Title</label>
33
- <input
34
- type="text"
35
- id="review-title"
36
- name="title"
37
- placeholder="Summarise your experience"
38
- required
39
- data-testid="review-title"
40
- />
41
- </div>
42
-
43
- <div class="form-group">
44
- <label for="review-body">Review</label>
45
- <textarea
46
- id="review-body"
47
- name="body"
48
- rows="4"
49
- placeholder="Tell us what you think..."
50
- required
51
- data-testid="review-body"
52
- style="resize: vertical;"
53
- ></textarea>
54
- </div>
55
-
56
- <button type="submit" class="btn btn-primary" data-testid="review-submit">
57
- Submit Review
58
- </button>
59
-
60
- <p class="review-form-message" id="review-form-message" style="display:none; margin-top: var(--nymbal-spacing-md, 1rem); font-size: var(--nymbal-font-size-sm, 0.875rem);"></p>
61
- </form>
62
-
63
- <script>
64
- import '../scripts/review-form.js'
65
- </script>
66
-
67
- <style>
68
- .review-form {
69
- margin-top: var(--nymbal-spacing-xl, 2rem);
70
- padding-top: var(--nymbal-spacing-xl, 2rem);
71
- border-top: 1px solid var(--nymbal-color-border, #e4e4e7);
72
- max-width: 32rem;
73
- }
74
- </style>
@@ -1,41 +0,0 @@
1
- ---
2
- import '../styles/pages/pdp.css'
3
- import { formatDate } from '../lib/format'
4
-
5
- interface Props {
6
- reviews: Array<{
7
- id: string
8
- rating: number
9
- title: string
10
- body: string
11
- submittedAt: string
12
- }>
13
- }
14
-
15
- const { reviews } = Astro.props
16
-
17
- function renderStars(rating: number): string {
18
- const filled = '★'
19
- const empty = '☆'
20
- return filled.repeat(rating) + empty.repeat(5 - rating)
21
- }
22
- ---
23
-
24
- {reviews.length > 0 ? (
25
- <div class="review-list" data-testid="review-list">
26
- {reviews.map((review) => (
27
- <div class="review-item" data-testid={`review-${review.id}`}>
28
- <div class="review-header">
29
- <span class="review-rating" aria-label={`${review.rating} out of 5 stars`}>
30
- {renderStars(review.rating)}
31
- </span>
32
- <span class="review-date">{formatDate(review.submittedAt)}</span>
33
- </div>
34
- <h4 class="review-title">{review.title}</h4>
35
- <p class="review-body">{review.body}</p>
36
- </div>
37
- ))}
38
- </div>
39
- ) : (
40
- <p class="reviews-empty">No reviews yet. Be the first to review this product.</p>
41
- )}
@@ -1,15 +0,0 @@
1
- ---
2
- import { STOCK_LABELS, stockLevel } from '@nymbalhq/types'
3
-
4
- interface Props {
5
- stock: number
6
- }
7
-
8
- const { stock } = Astro.props
9
- const level = stockLevel(stock)
10
- ---
11
-
12
- <span class="pdp-stock" data-testid="stock-badge" data-level={level}>
13
- <span class="pdp-stock-dot" aria-hidden="true"></span>
14
- <span data-stock-label>{STOCK_LABELS[level]}</span>
15
- </span>
@@ -1,82 +0,0 @@
1
- import { client } from '../lib/client.js'
2
- import { submitOnce } from '../lib/submit-once.js'
3
-
4
- function getRedirectUrl(): string {
5
- const params = new URLSearchParams(window.location.search)
6
- return params.get('redirect') ?? '/account'
7
- }
8
-
9
- function showFormError(formId: string, message: string) {
10
- const errorEl = document.getElementById(`${formId}-error`)
11
- if (errorEl) {
12
- errorEl.textContent = message
13
- errorEl.style.display = 'block'
14
- }
15
- }
16
-
17
- function hideFormError(formId: string) {
18
- const errorEl = document.getElementById(`${formId}-error`)
19
- if (errorEl) {
20
- errorEl.style.display = 'none'
21
- errorEl.textContent = ''
22
- }
23
- }
24
-
25
- // Login form handler
26
- const loginForm = document.getElementById('login-form') as HTMLFormElement | null
27
- submitOnce(loginForm, async () => {
28
- hideFormError('login')
29
-
30
- const email = (document.getElementById('login-email') as HTMLInputElement)?.value
31
- const password = (document.getElementById('login-password') as HTMLInputElement)?.value
32
-
33
- if (!email || !password) {
34
- showFormError('login', 'Please enter your email and password.')
35
- return
36
- }
37
-
38
- const submitBtn = loginForm?.querySelector(
39
- '[data-testid="login-submit"]',
40
- ) as HTMLButtonElement | null
41
- if (submitBtn) submitBtn.textContent = 'Signing in...'
42
-
43
- try {
44
- await client.auth.login(email, password)
45
- window.location.href = getRedirectUrl()
46
- return 'leaving'
47
- } catch (err: any) {
48
- showFormError('login', err.message ?? 'Invalid email or password.')
49
- if (submitBtn) submitBtn.textContent = 'Sign In'
50
- }
51
- })
52
-
53
- // Register form handler
54
- const registerForm = document.getElementById('register-form') as HTMLFormElement | null
55
- submitOnce(registerForm, async () => {
56
- hideFormError('register')
57
-
58
- const email = (document.getElementById('register-email') as HTMLInputElement)?.value
59
- const password = (document.getElementById('register-password') as HTMLInputElement)?.value
60
- const firstName = (document.getElementById('register-firstName') as HTMLInputElement)?.value
61
- const lastName = (document.getElementById('register-lastName') as HTMLInputElement)?.value
62
- const phone = (document.getElementById('register-phone') as HTMLInputElement)?.value
63
-
64
- if (!email || !password) {
65
- showFormError('register', 'Please enter your email and password.')
66
- return
67
- }
68
-
69
- const submitBtn = registerForm?.querySelector(
70
- '[data-testid="register-submit"]',
71
- ) as HTMLButtonElement | null
72
- if (submitBtn) submitBtn.textContent = 'Creating account...'
73
-
74
- try {
75
- await client.auth.register({ email, password, firstName, lastName, phone })
76
- window.location.href = getRedirectUrl()
77
- return 'leaving'
78
- } catch (err: any) {
79
- showFormError('register', err.message ?? 'Registration failed. Please try again.')
80
- if (submitBtn) submitBtn.textContent = 'Create Account'
81
- }
82
- })