create-nymbal-app 0.3.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 (167) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +14 -12
  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 -27
  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 +126 -20
  13. package/scaffold/templates/astro/src/lib/chrome.ts +59 -0
  14. package/scaffold/templates/astro/src/lib/client.ts +4 -2
  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 +41 -26
  24. package/scaffold/templates/astro/src/pages/checkout.astro +34 -90
  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 +46 -29
  32. package/scaffold/templates/astro/src/pages/products/index.astro +40 -26
  33. package/scaffold/templates/astro/src/pages/search.astro +13 -8
  34. package/scaffold/templates/astro/src/scripts/init-client.ts +14 -0
  35. package/scaffold/templates/astro/src/scripts/pdp.ts +47 -53
  36. package/scaffold/templates/astro/src/scripts/plp.ts +21 -98
  37. package/scaffold/templates/astro/src/scripts/search.ts +19 -120
  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 -167
  44. package/scaffold/templates/astro/src/styles/pages/plp.css +3 -42
  45. package/scaffold/templates/astro/src/styles/pages/search.css +13 -100
  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 +64 -28
  82. package/scaffold/templates/nextjs/components/client/elements.tsx +15 -0
  83. package/scaffold/templates/nextjs/components/server/Header.tsx +33 -97
  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 +162 -46
  88. package/scaffold/templates/nextjs/lib/chrome.ts +40 -0
  89. package/scaffold/templates/nextjs/lib/client.ts +3 -0
  90. package/scaffold/templates/nextjs/lib/fragments.ts +16 -0
  91. package/scaffold/templates/nextjs/lib/header.ts +39 -0
  92. package/scaffold/templates/nextjs/lib/render.ts +14 -0
  93. package/scaffold/templates/nextjs/next.config.mjs +32 -0
  94. package/scaffold/templates/nextjs/package.json +28 -29
  95. package/scaffold/templates/nextjs/styles/pages/account.module.css +0 -70
  96. package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +6 -85
  97. package/scaffold/templates/nextjs/styles/pages/home.module.css +2 -103
  98. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +3 -179
  99. package/scaffold/templates/nextjs/styles/pages/plp.module.css +8 -141
  100. package/scaffold/templates/nextjs/styles/pages/search.module.css +8 -128
  101. package/scaffold/templates/nextjs/tsconfig.dist.json +5 -0
  102. package/scaffold/templates/astro/src/components/AddressForm.astro +0 -107
  103. package/scaffold/templates/astro/src/components/Breadcrumb.astro +0 -20
  104. package/scaffold/templates/astro/src/components/CategoryNav.astro +0 -27
  105. package/scaffold/templates/astro/src/components/CheckoutSteps.astro +0 -29
  106. package/scaffold/templates/astro/src/components/Footer.astro +0 -108
  107. package/scaffold/templates/astro/src/components/Header.astro +0 -192
  108. package/scaffold/templates/astro/src/components/ImageGallery.astro +0 -42
  109. package/scaffold/templates/astro/src/components/OrderSummary.astro +0 -53
  110. package/scaffold/templates/astro/src/components/Pagination.astro +0 -13
  111. package/scaffold/templates/astro/src/components/PriceDisplay.astro +0 -25
  112. package/scaffold/templates/astro/src/components/ProductCard.astro +0 -44
  113. package/scaffold/templates/astro/src/components/RelatedProducts.astro +0 -28
  114. package/scaffold/templates/astro/src/components/ReviewForm.astro +0 -74
  115. package/scaffold/templates/astro/src/components/ReviewList.astro +0 -41
  116. package/scaffold/templates/astro/src/components/SortDropdown.astro +0 -26
  117. package/scaffold/templates/astro/src/components/StockBadge.astro +0 -32
  118. package/scaffold/templates/astro/src/scripts/auth-forms.ts +0 -82
  119. package/scaffold/templates/astro/src/scripts/cart-page.ts +0 -143
  120. package/scaffold/templates/astro/src/scripts/checkout.ts +0 -272
  121. package/scaffold/templates/astro/src/scripts/image-gallery.ts +0 -22
  122. package/scaffold/templates/astro/src/scripts/profile-form.ts +0 -57
  123. package/scaffold/templates/astro/src/scripts/review-form.ts +0 -27
  124. package/scaffold/templates/astro/src/styles/pages/cart.css +0 -243
  125. package/scaffold/templates/astro/src/styles/pages/checkout.css +0 -270
  126. package/scaffold/templates/nextjs/app/account/addresses/page.tsx +0 -10
  127. package/scaffold/templates/nextjs/app/account/login/page.tsx +0 -23
  128. package/scaffold/templates/nextjs/app/account/profile/page.tsx +0 -10
  129. package/scaffold/templates/nextjs/app/account/register/page.tsx +0 -10
  130. package/scaffold/templates/nextjs/app/cart/page.tsx +0 -10
  131. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +0 -58
  132. package/scaffold/templates/nextjs/app/checkout/page.tsx +0 -10
  133. package/scaffold/templates/nextjs/app/page.tsx +0 -63
  134. package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +0 -111
  135. package/scaffold/templates/nextjs/app/products/page.tsx +0 -40
  136. package/scaffold/templates/nextjs/app/search/page.tsx +0 -24
  137. package/scaffold/templates/nextjs/components/client/AddressManager.tsx +0 -181
  138. package/scaffold/templates/nextjs/components/client/CartPage.tsx +0 -120
  139. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +0 -523
  140. package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +0 -68
  141. package/scaffold/templates/nextjs/components/client/LoginForm.tsx +0 -89
  142. package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +0 -12
  143. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +0 -148
  144. package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +0 -112
  145. package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +0 -131
  146. package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +0 -116
  147. package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +0 -27
  148. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +0 -141
  149. package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +0 -35
  150. package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +0 -36
  151. package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +0 -29
  152. package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +0 -60
  153. package/scaffold/templates/nextjs/components/server/Footer.module.css +0 -64
  154. package/scaffold/templates/nextjs/components/server/Footer.tsx +0 -79
  155. package/scaffold/templates/nextjs/components/server/Header.module.css +0 -147
  156. package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +0 -79
  157. package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +0 -31
  158. package/scaffold/templates/nextjs/components/server/ProductCard.tsx +0 -59
  159. package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +0 -31
  160. package/scaffold/templates/nextjs/components/server/ReviewList.tsx +0 -62
  161. package/scaffold/templates/nextjs/components/server/StockBadge.tsx +0 -19
  162. package/scaffold/templates/nextjs/styles/pages/cart.module.css +0 -240
  163. package/scaffold/templates/nextjs/styles/pages/checkout.module.css +0 -404
  164. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/layout.tsx +0 -0
  165. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/[orderNumber]/page.tsx +0 -0
  166. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/page.tsx +0 -0
  167. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/page.tsx +0 -0
@@ -1,192 +0,0 @@
1
- ---
2
- ---
3
- <header class="site-header" data-testid="site-header">
4
- <div class="header-container container">
5
- <a href="/" class="header-logo">Nymbal</a>
6
-
7
- <label class="hamburger" for="nav-toggle" aria-label="Toggle navigation">
8
- <span class="hamburger-line"></span>
9
- <span class="hamburger-line"></span>
10
- <span class="hamburger-line"></span>
11
- </label>
12
- <input type="checkbox" id="nav-toggle" class="nav-toggle visually-hidden" aria-label="Toggle navigation" />
13
-
14
- <div class="header-nav-wrapper">
15
- <nav class="header-nav" aria-label="Main navigation">
16
- <a href="/category/apparel" class="nav-link">Apparel</a>
17
- <a href="/category/home" class="nav-link">Home</a>
18
- <a href="/category/electronics" class="nav-link">Electronics</a>
19
- <a href="/category/books" class="nav-link">Books</a>
20
- <a href="/products" class="nav-link">All Products</a>
21
- </nav>
22
-
23
- <div class="header-actions">
24
- <nymbal-search-bar placeholder="Search products..." debounce-ms="300"></nymbal-search-bar>
25
- <a href="/account" class="header-account-link" aria-label="Account">
26
- <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">
27
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
28
- <circle cx="12" cy="7" r="4" />
29
- </svg>
30
- </a>
31
- <nymbal-mini-cart></nymbal-mini-cart>
32
- </div>
33
- </div>
34
- </div>
35
- </header>
36
-
37
- <style>
38
- .site-header {
39
- position: sticky;
40
- top: 0;
41
- z-index: var(--nymbal-z-index-sticky, 100);
42
- background-color: var(--nymbal-color-surface, #fff);
43
- border-bottom: 1px solid var(--nymbal-color-border, #e4e4e7);
44
- }
45
-
46
- .header-container {
47
- display: flex;
48
- align-items: center;
49
- gap: var(--nymbal-spacing-md, 1rem);
50
- padding-block: var(--nymbal-spacing-sm, 0.5rem);
51
- flex-wrap: wrap;
52
- }
53
-
54
- .header-logo {
55
- font-size: var(--nymbal-font-size-lg, 1.125rem);
56
- font-weight: var(--nymbal-font-weight-bold, 700);
57
- color: var(--nymbal-color-primary, #18181b);
58
- text-decoration: none;
59
- letter-spacing: -0.02em;
60
- flex-shrink: 0;
61
- }
62
-
63
- .header-logo:hover {
64
- color: var(--nymbal-color-accent, #2563eb);
65
- }
66
-
67
- /* Hamburger toggle */
68
- .hamburger {
69
- display: flex;
70
- flex-direction: column;
71
- justify-content: center;
72
- gap: 4px;
73
- width: 28px;
74
- height: 28px;
75
- cursor: pointer;
76
- margin-left: auto;
77
- padding: 2px;
78
- }
79
-
80
- .hamburger-line {
81
- display: block;
82
- width: 100%;
83
- height: 2px;
84
- background-color: var(--nymbal-color-primary, #18181b);
85
- border-radius: 1px;
86
- transition: transform var(--nymbal-transition-fast, 150ms) ease,
87
- opacity var(--nymbal-transition-fast, 150ms) ease;
88
- }
89
-
90
- .nav-toggle:checked ~ .header-nav-wrapper {
91
- display: flex;
92
- }
93
-
94
- .nav-toggle:checked ~ .hamburger .hamburger-line:nth-child(1) {
95
- transform: translateY(6px) rotate(45deg);
96
- }
97
-
98
- .nav-toggle:checked ~ .hamburger .hamburger-line:nth-child(2) {
99
- opacity: 0;
100
- }
101
-
102
- .nav-toggle:checked ~ .hamburger .hamburger-line:nth-child(3) {
103
- transform: translateY(-6px) rotate(-45deg);
104
- }
105
-
106
- .header-nav-wrapper {
107
- display: none;
108
- flex-direction: column;
109
- width: 100%;
110
- gap: var(--nymbal-spacing-md, 1rem);
111
- padding-block: var(--nymbal-spacing-sm, 0.5rem);
112
- }
113
-
114
- .header-nav {
115
- display: flex;
116
- flex-direction: column;
117
- gap: var(--nymbal-spacing-xs, 0.25rem);
118
- }
119
-
120
- .nav-link {
121
- display: block;
122
- padding: var(--nymbal-spacing-xs, 0.25rem) var(--nymbal-spacing-sm, 0.5rem);
123
- font-size: var(--nymbal-font-size-sm, 0.875rem);
124
- font-weight: var(--nymbal-font-weight-medium, 500);
125
- color: var(--nymbal-color-text, #18181b);
126
- text-decoration: none;
127
- border-radius: var(--nymbal-radius-sm, 0.25rem);
128
- transition: background-color var(--nymbal-transition-fast, 150ms) ease,
129
- color var(--nymbal-transition-fast, 150ms) ease;
130
- }
131
-
132
- .nav-link:hover {
133
- background-color: var(--nymbal-color-border, #e4e4e7);
134
- color: var(--nymbal-color-accent, #2563eb);
135
- }
136
-
137
- .header-actions {
138
- display: flex;
139
- align-items: center;
140
- gap: var(--nymbal-spacing-sm, 0.5rem);
141
- }
142
-
143
- .header-account-link {
144
- display: flex;
145
- align-items: center;
146
- justify-content: center;
147
- width: 36px;
148
- height: 36px;
149
- color: var(--nymbal-color-text, #18181b);
150
- text-decoration: none;
151
- border-radius: var(--nymbal-radius-full, 9999px);
152
- transition: background-color var(--nymbal-transition-fast, 150ms) ease,
153
- color var(--nymbal-transition-fast, 150ms) ease;
154
- }
155
-
156
- .header-account-link:hover {
157
- background-color: var(--nymbal-color-border, #e4e4e7);
158
- color: var(--nymbal-color-accent, #2563eb);
159
- }
160
-
161
- /* Desktop layout */
162
- @media (min-width: 769px) {
163
- .hamburger {
164
- display: none;
165
- }
166
-
167
- .header-container {
168
- flex-wrap: nowrap;
169
- padding-block: var(--nymbal-spacing-md, 1rem);
170
- }
171
-
172
- .header-nav-wrapper {
173
- display: flex;
174
- flex-direction: row;
175
- align-items: center;
176
- justify-content: space-between;
177
- width: auto;
178
- flex: 1;
179
- padding-block: 0;
180
- gap: var(--nymbal-spacing-lg, 1.5rem);
181
- }
182
-
183
- .header-nav {
184
- flex-direction: row;
185
- gap: var(--nymbal-spacing-xs, 0.25rem);
186
- }
187
-
188
- .nav-link {
189
- padding: var(--nymbal-spacing-xs, 0.25rem) var(--nymbal-spacing-sm, 0.5rem);
190
- }
191
- }
192
- </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,26 +0,0 @@
1
- ---
2
- import type { ProductSortKey } from '@nymbalhq/types'
3
-
4
- interface Props {
5
- sort: ProductSortKey
6
- }
7
-
8
- const { sort } = Astro.props
9
-
10
- const OPTIONS: Array<{ value: ProductSortKey; label: string }> = [
11
- { value: 'newest', label: 'Newest' },
12
- { value: 'price-asc', label: 'Price: Low to High' },
13
- { value: 'price-desc', label: 'Price: High to Low' },
14
- { value: 'name-asc', label: 'Name: A to Z' },
15
- { value: 'name-desc', label: 'Name: Z to A' },
16
- ]
17
- ---
18
-
19
- <div class="plp-sort-bar">
20
- <label for="sort-select">Sort by</label>
21
- <select id="sort-select" data-testid="sort-dropdown">
22
- {OPTIONS.map((option) => (
23
- <option value={option.value} selected={option.value === sort}>{option.label}</option>
24
- ))}
25
- </select>
26
- </div>
@@ -1,32 +0,0 @@
1
- ---
2
- interface Props {
3
- stock: number
4
- }
5
-
6
- const { stock } = Astro.props
7
-
8
- let statusClass: string
9
- let label: string
10
-
11
- if (stock <= 0) {
12
- statusClass = 'out-of-stock'
13
- label = 'Out of Stock'
14
- } else if (stock < 5) {
15
- statusClass = 'low-stock'
16
- label = 'Low Stock'
17
- } else {
18
- statusClass = 'in-stock'
19
- label = 'In Stock'
20
- }
21
- ---
22
-
23
- <span class={`pdp-stock ${statusClass}`} data-testid="stock-badge">
24
- <span class="pdp-stock-dot" aria-hidden="true"></span>
25
- {label}
26
- </span>
27
-
28
- <style>
29
- .low-stock {
30
- color: var(--nymbal-color-warning, #d97706);
31
- }
32
- </style>
@@ -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
- })