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,473 @@
1
+ @import '@nymbalhq/web-components/tokens.css';
2
+
3
+ /* ── CSS Reset ─────────────────────────────────────────────────────── */
4
+
5
+ *,
6
+ *::before,
7
+ *::after {
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ html {
17
+ -webkit-text-size-adjust: 100%;
18
+ -moz-text-size-adjust: 100%;
19
+ text-size-adjust: 100%;
20
+ scroll-behavior: smooth;
21
+ }
22
+
23
+ body {
24
+ font-family: var(--nymbal-font-family);
25
+ font-size: var(--nymbal-font-size-base);
26
+ font-weight: var(--nymbal-font-weight-normal);
27
+ line-height: 1.6;
28
+ color: var(--nymbal-color-text);
29
+ background-color: var(--nymbal-color-surface);
30
+ -webkit-font-smoothing: antialiased;
31
+ -moz-osx-font-smoothing: grayscale;
32
+ }
33
+
34
+ img,
35
+ picture,
36
+ video,
37
+ canvas,
38
+ svg {
39
+ display: block;
40
+ max-width: 100%;
41
+ height: auto;
42
+ }
43
+
44
+ input,
45
+ button,
46
+ textarea,
47
+ select {
48
+ font: inherit;
49
+ color: inherit;
50
+ }
51
+
52
+ p,
53
+ h1,
54
+ h2,
55
+ h3,
56
+ h4,
57
+ h5,
58
+ h6 {
59
+ overflow-wrap: break-word;
60
+ }
61
+
62
+ h1,
63
+ h2,
64
+ h3,
65
+ h4,
66
+ h5,
67
+ h6 {
68
+ line-height: 1.2;
69
+ font-weight: var(--nymbal-font-weight-bold);
70
+ }
71
+
72
+ h1 {
73
+ font-size: 2rem;
74
+ }
75
+
76
+ h2 {
77
+ font-size: 1.5rem;
78
+ }
79
+
80
+ h3 {
81
+ font-size: 1.25rem;
82
+ }
83
+
84
+ ul,
85
+ ol {
86
+ list-style: none;
87
+ }
88
+
89
+ a {
90
+ color: var(--nymbal-color-accent);
91
+ text-decoration: none;
92
+ transition: color var(--nymbal-transition-fast);
93
+ }
94
+
95
+ a:hover {
96
+ color: var(--nymbal-color-accent-hover);
97
+ text-decoration: underline;
98
+ }
99
+
100
+ a:focus-visible {
101
+ outline: 2px solid var(--nymbal-color-accent);
102
+ outline-offset: 2px;
103
+ border-radius: var(--nymbal-radius-sm);
104
+ }
105
+
106
+ /* ── Layout ────────────────────────────────────────────────────────── */
107
+
108
+ .container {
109
+ width: 100%;
110
+ max-width: 72rem;
111
+ margin-inline: auto;
112
+ padding-inline: var(--nymbal-spacing-md);
113
+ }
114
+
115
+ .section {
116
+ padding-block: var(--nymbal-spacing-xl);
117
+ }
118
+
119
+ .section-title {
120
+ font-size: 1.5rem;
121
+ font-weight: var(--nymbal-font-weight-bold);
122
+ margin-bottom: var(--nymbal-spacing-lg);
123
+ }
124
+
125
+ /* ── Accessibility ─────────────────────────────────────────────────── */
126
+
127
+ .visually-hidden {
128
+ position: absolute;
129
+ width: 1px;
130
+ height: 1px;
131
+ padding: 0;
132
+ margin: -1px;
133
+ overflow: hidden;
134
+ clip: rect(0, 0, 0, 0);
135
+ white-space: nowrap;
136
+ border: 0;
137
+ }
138
+
139
+ .skip-nav {
140
+ position: absolute;
141
+ top: -100%;
142
+ left: var(--nymbal-spacing-md);
143
+ z-index: 9999;
144
+ padding: var(--nymbal-spacing-sm) var(--nymbal-spacing-md);
145
+ background: var(--nymbal-color-primary);
146
+ color: var(--nymbal-color-text-inverse);
147
+ border-radius: var(--nymbal-radius-md);
148
+ font-weight: var(--nymbal-font-weight-medium);
149
+ transition: top var(--nymbal-transition-fast);
150
+ }
151
+
152
+ .skip-nav:focus {
153
+ top: var(--nymbal-spacing-sm);
154
+ text-decoration: none;
155
+ color: var(--nymbal-color-text-inverse);
156
+ }
157
+
158
+ /* ── Buttons ───────────────────────────────────────────────────────── */
159
+
160
+ .btn {
161
+ display: inline-flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ gap: var(--nymbal-spacing-sm);
165
+ padding: 0.625rem 1.25rem;
166
+ font-size: var(--nymbal-font-size-sm);
167
+ font-weight: var(--nymbal-font-weight-medium);
168
+ line-height: 1;
169
+ border: 1px solid transparent;
170
+ border-radius: var(--nymbal-radius-md);
171
+ cursor: pointer;
172
+ transition: background-color var(--nymbal-transition-fast),
173
+ color var(--nymbal-transition-fast),
174
+ border-color var(--nymbal-transition-fast),
175
+ box-shadow var(--nymbal-transition-fast);
176
+ text-decoration: none;
177
+ white-space: nowrap;
178
+ user-select: none;
179
+ }
180
+
181
+ .btn:focus-visible {
182
+ outline: 2px solid var(--nymbal-color-accent);
183
+ outline-offset: 2px;
184
+ }
185
+
186
+ .btn:disabled {
187
+ opacity: 0.5;
188
+ cursor: not-allowed;
189
+ }
190
+
191
+ .btn-primary {
192
+ background-color: var(--nymbal-color-primary);
193
+ color: var(--nymbal-color-text-inverse);
194
+ border-color: var(--nymbal-color-primary);
195
+ }
196
+
197
+ .btn-primary:hover:not(:disabled) {
198
+ background-color: var(--nymbal-color-primary-hover);
199
+ border-color: var(--nymbal-color-primary-hover);
200
+ }
201
+
202
+ .btn-secondary {
203
+ background-color: var(--nymbal-color-accent);
204
+ color: var(--nymbal-color-text-inverse);
205
+ border-color: var(--nymbal-color-accent);
206
+ }
207
+
208
+ .btn-secondary:hover:not(:disabled) {
209
+ background-color: var(--nymbal-color-accent-hover);
210
+ border-color: var(--nymbal-color-accent-hover);
211
+ }
212
+
213
+ .btn-outline {
214
+ background-color: transparent;
215
+ color: var(--nymbal-color-primary);
216
+ border-color: var(--nymbal-color-border);
217
+ }
218
+
219
+ .btn-outline:hover:not(:disabled) {
220
+ background-color: var(--nymbal-color-primary);
221
+ color: var(--nymbal-color-text-inverse);
222
+ border-color: var(--nymbal-color-primary);
223
+ }
224
+
225
+ /* ── Form Inputs ───────────────────────────────────────────────────── */
226
+
227
+ .input,
228
+ input[type='text'],
229
+ input[type='email'],
230
+ input[type='password'],
231
+ input[type='search'],
232
+ input[type='tel'],
233
+ input[type='number'],
234
+ input[type='url'],
235
+ textarea,
236
+ select {
237
+ display: block;
238
+ width: 100%;
239
+ padding: 0.625rem 0.75rem;
240
+ font-size: var(--nymbal-font-size-sm);
241
+ line-height: 1.5;
242
+ color: var(--nymbal-color-text);
243
+ background-color: var(--nymbal-color-surface);
244
+ border: 1px solid var(--nymbal-color-border);
245
+ border-radius: var(--nymbal-radius-md);
246
+ transition: border-color var(--nymbal-transition-fast),
247
+ box-shadow var(--nymbal-transition-fast);
248
+ appearance: none;
249
+ }
250
+
251
+ .input:focus,
252
+ input:focus,
253
+ textarea:focus,
254
+ select:focus {
255
+ outline: none;
256
+ border-color: var(--nymbal-color-accent);
257
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
258
+ }
259
+
260
+ .input::placeholder,
261
+ input::placeholder,
262
+ textarea::placeholder {
263
+ color: var(--nymbal-color-text-muted);
264
+ }
265
+
266
+ label {
267
+ display: block;
268
+ font-size: var(--nymbal-font-size-sm);
269
+ font-weight: var(--nymbal-font-weight-medium);
270
+ margin-bottom: var(--nymbal-spacing-xs);
271
+ color: var(--nymbal-color-text);
272
+ }
273
+
274
+ .form-group {
275
+ margin-bottom: var(--nymbal-spacing-md);
276
+ }
277
+
278
+ .form-error {
279
+ font-size: var(--nymbal-font-size-xs);
280
+ color: var(--nymbal-color-error);
281
+ margin-top: var(--nymbal-spacing-xs);
282
+ }
283
+
284
+ /* ── Product Grid ──────────────────────────────────────────────────── */
285
+
286
+ .product-grid {
287
+ display: grid;
288
+ grid-template-columns: 1fr;
289
+ gap: var(--nymbal-spacing-lg);
290
+ }
291
+
292
+ @media (min-width: 640px) {
293
+ .product-grid {
294
+ grid-template-columns: repeat(2, 1fr);
295
+ }
296
+ }
297
+
298
+ @media (min-width: 1024px) {
299
+ .product-grid {
300
+ grid-template-columns: repeat(4, 1fr);
301
+ }
302
+ }
303
+
304
+ /* ── Link Styles ───────────────────────────────────────────────────── */
305
+
306
+ .link-muted {
307
+ color: var(--nymbal-color-text-muted);
308
+ text-decoration: none;
309
+ }
310
+
311
+ .link-muted:hover {
312
+ color: var(--nymbal-color-text);
313
+ text-decoration: underline;
314
+ }
315
+
316
+ .link-plain {
317
+ color: inherit;
318
+ text-decoration: none;
319
+ }
320
+
321
+ .link-plain:hover {
322
+ text-decoration: underline;
323
+ }
324
+
325
+ /* ── Utilities ─────────────────────────────────────────────────────── */
326
+
327
+ .text-center {
328
+ text-align: center;
329
+ }
330
+
331
+ .text-muted {
332
+ color: var(--nymbal-color-text-muted);
333
+ }
334
+
335
+ .text-sm {
336
+ font-size: var(--nymbal-font-size-sm);
337
+ }
338
+
339
+ .text-xs {
340
+ font-size: var(--nymbal-font-size-xs);
341
+ }
342
+
343
+ .font-medium {
344
+ font-weight: var(--nymbal-font-weight-medium);
345
+ }
346
+
347
+ .font-bold {
348
+ font-weight: var(--nymbal-font-weight-bold);
349
+ }
350
+
351
+ .mt-sm {
352
+ margin-top: var(--nymbal-spacing-sm);
353
+ }
354
+
355
+ .mt-md {
356
+ margin-top: var(--nymbal-spacing-md);
357
+ }
358
+
359
+ .mt-lg {
360
+ margin-top: var(--nymbal-spacing-lg);
361
+ }
362
+
363
+ .mb-md {
364
+ margin-bottom: var(--nymbal-spacing-md);
365
+ }
366
+
367
+ .mb-lg {
368
+ margin-bottom: var(--nymbal-spacing-lg);
369
+ }
370
+
371
+ .flex {
372
+ display: flex;
373
+ }
374
+
375
+ .flex-col {
376
+ flex-direction: column;
377
+ }
378
+
379
+ .items-center {
380
+ align-items: center;
381
+ }
382
+
383
+ .justify-between {
384
+ justify-content: space-between;
385
+ }
386
+
387
+ .gap-sm {
388
+ gap: var(--nymbal-spacing-sm);
389
+ }
390
+
391
+ .gap-md {
392
+ gap: var(--nymbal-spacing-md);
393
+ }
394
+
395
+ .gap-lg {
396
+ gap: var(--nymbal-spacing-lg);
397
+ }
398
+
399
+ /* ── Badge ─────────────────────────────────────────────────────────── */
400
+
401
+ .badge {
402
+ display: inline-flex;
403
+ align-items: center;
404
+ padding: 0.125rem 0.5rem;
405
+ font-size: var(--nymbal-font-size-xs);
406
+ font-weight: var(--nymbal-font-weight-medium);
407
+ border-radius: var(--nymbal-radius-full);
408
+ line-height: 1.5;
409
+ }
410
+
411
+ .badge-success {
412
+ background-color: #dcfce7;
413
+ color: var(--nymbal-color-success);
414
+ }
415
+
416
+ .badge-error {
417
+ background-color: #fef2f2;
418
+ color: var(--nymbal-color-error);
419
+ }
420
+
421
+ .badge-warning {
422
+ background-color: #fef3c7;
423
+ color: var(--nymbal-color-warning);
424
+ }
425
+
426
+ /* ── Divider ───────────────────────────────────────────────────────── */
427
+
428
+ .divider {
429
+ border: 0;
430
+ border-top: 1px solid var(--nymbal-color-border);
431
+ margin-block: var(--nymbal-spacing-md);
432
+ }
433
+
434
+ /* ── Card ──────────────────────────────────────────────────────────── */
435
+
436
+ .card {
437
+ background: var(--nymbal-color-surface);
438
+ border: 1px solid var(--nymbal-color-border);
439
+ border-radius: var(--nymbal-radius-lg);
440
+ padding: var(--nymbal-spacing-lg);
441
+ box-shadow: var(--nymbal-shadow-sm);
442
+ }
443
+
444
+ /* ── Loading Spinner ───────────────────────────────────────────────── */
445
+
446
+ .spinner {
447
+ display: inline-block;
448
+ width: 1.25rem;
449
+ height: 1.25rem;
450
+ border: 2px solid var(--nymbal-color-border);
451
+ border-top-color: var(--nymbal-color-accent);
452
+ border-radius: 50%;
453
+ animation: spin 0.6s linear infinite;
454
+ }
455
+
456
+ .spinner-lg {
457
+ width: 2rem;
458
+ height: 2rem;
459
+ border-width: 3px;
460
+ }
461
+
462
+ @keyframes spin {
463
+ to {
464
+ transform: rotate(360deg);
465
+ }
466
+ }
467
+
468
+ .loading-center {
469
+ display: flex;
470
+ align-items: center;
471
+ justify-content: center;
472
+ min-height: 12rem;
473
+ }
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from 'react'
2
+ import type { Metadata } from 'next'
3
+ import './globals.css'
4
+ import { NymbalSetup } from '@/components/client/NymbalSetup'
5
+ import { Header } from '@/components/server/Header'
6
+ import { Footer } from '@/components/server/Footer'
7
+ import { SkipNav } from '@/components/server/SkipNav'
8
+ import { CartDrawerToggle } from '@/components/client/CartDrawerToggle'
9
+
10
+ export const metadata: Metadata = {
11
+ title: { default: 'Nymbal Store', template: '%s | Nymbal' },
12
+ description: 'Shop the Nymbal store',
13
+ }
14
+
15
+ export default function RootLayout({ children }: { children: ReactNode }) {
16
+ return (
17
+ <html lang="en">
18
+ <body>
19
+ <NymbalSetup>
20
+ <SkipNav />
21
+ <Header />
22
+ <main id="main">{children}</main>
23
+ <Footer />
24
+ <CartDrawerToggle />
25
+ </NymbalSetup>
26
+ </body>
27
+ </html>
28
+ )
29
+ }
@@ -0,0 +1,38 @@
1
+ import Link from 'next/link'
2
+ import styles from '@/styles/pages/error.module.css'
3
+
4
+ export default function NotFound() {
5
+ return (
6
+ <div className={styles.page}>
7
+ <div className={styles.wrapper}>
8
+ <div className={`${styles.icon} ${styles.notFoundIcon}`}>
9
+ <svg
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="2"
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ aria-hidden="true"
17
+ >
18
+ <circle cx="11" cy="11" r="8" />
19
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
20
+ </svg>
21
+ </div>
22
+ <p className={styles.statusCode}>404</p>
23
+ <h1 className={styles.title}>Page Not Found</h1>
24
+ <p className={styles.message}>
25
+ The page you are looking for does not exist or may have been moved.
26
+ </p>
27
+ <div className={styles.actions}>
28
+ <Link href="/" className={styles.homeLink}>
29
+ Go Home
30
+ </Link>
31
+ <Link href="/products" className={styles.retryButton}>
32
+ Browse Products
33
+ </Link>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ )
38
+ }
@@ -0,0 +1,127 @@
1
+ import type { Metadata } from 'next'
2
+ import { notFound } from 'next/navigation'
3
+ import Link from 'next/link'
4
+ import { fetchOrder } from '@/lib/api'
5
+ import { OrderSummary } from '@/components/server/OrderSummary'
6
+ import { OrderStatusRefresh } from '@/components/client/OrderStatusRefresh'
7
+ import { isPaid, orderHeading } from '@/lib/order-status'
8
+ import styles from '@/styles/pages/confirmation.module.css'
9
+
10
+ interface OrderPageProps {
11
+ params: Promise<{ orderNumber: string }>
12
+ }
13
+
14
+ export async function generateMetadata({ params }: OrderPageProps): Promise<Metadata> {
15
+ const { orderNumber } = await params
16
+ return {
17
+ title: `Order ${orderNumber}`,
18
+ }
19
+ }
20
+
21
+ function AddressBlock({
22
+ title,
23
+ address,
24
+ }: {
25
+ title: string
26
+ address: {
27
+ firstName?: string
28
+ lastName?: string
29
+ addressLine1: string
30
+ addressLine2?: string
31
+ city: string
32
+ region: string
33
+ postalCode: string
34
+ country: string
35
+ } | null
36
+ }) {
37
+ if (!address) return null
38
+
39
+ return (
40
+ <div className={styles.addressCard}>
41
+ <h4 className={styles.addressTitle}>{title}</h4>
42
+ <p className={styles.addressText}>
43
+ {address.firstName} {address.lastName}
44
+ <br />
45
+ {address.addressLine1}
46
+ {address.addressLine2 && <>, {address.addressLine2}</>}
47
+ <br />
48
+ {address.city}, {address.region} {address.postalCode}
49
+ <br />
50
+ {address.country}
51
+ </p>
52
+ </div>
53
+ )
54
+ }
55
+
56
+ export default async function OrderPage({ params }: OrderPageProps) {
57
+ const { orderNumber } = await params
58
+ const order = await fetchOrder(orderNumber)
59
+
60
+ if (!order) notFound()
61
+ const paid = isPaid(order.status)
62
+
63
+ return (
64
+ <div className={`container ${styles.page}`}>
65
+ <OrderStatusRefresh orderNumber={order.orderNumber} status={order.status} />
66
+ <div className={styles.wrapper}>
67
+ <div className={styles.successHeader}>
68
+ {paid && (
69
+ <div className={styles.checkCircle}>
70
+ <svg
71
+ viewBox="0 0 24 24"
72
+ fill="none"
73
+ stroke="currentColor"
74
+ strokeWidth="2"
75
+ strokeLinecap="round"
76
+ strokeLinejoin="round"
77
+ aria-hidden="true"
78
+ >
79
+ <polyline points="20 6 9 17 4 12" />
80
+ </svg>
81
+ </div>
82
+ )}
83
+ <h1 className={styles.successTitle} data-testid="order-heading">
84
+ {orderHeading(order.status)}
85
+ </h1>
86
+ <p className={styles.successSubtitle}>
87
+ {paid ? 'Thank you for your order. ' : ''}Your order number is{' '}
88
+ <span className={styles.orderNumber} data-testid="order-number">
89
+ {order.orderNumber}
90
+ </span>
91
+ . Status:{' '}
92
+ <span data-testid="order-status" style={{ textTransform: 'capitalize' }}>
93
+ {order.status}
94
+ </span>
95
+ .{order.status === 'pending' && ' We will confirm it once the payment arrives.'}
96
+ </p>
97
+ </div>
98
+
99
+ <OrderSummary
100
+ items={order.lineItems}
101
+ subtotalMinor={order.subtotalMinor}
102
+ currency={order.currency}
103
+ shippingTotalMinor={order.shippingTotalMinor}
104
+ taxTotalMinor={order.taxTotalMinor}
105
+ totalMinor={order.totalMinor}
106
+ />
107
+
108
+ <div className={styles.orderCard}>
109
+ <h3 className={styles.orderCardTitle}>Delivery Details</h3>
110
+ <div className={styles.addressGrid}>
111
+ <AddressBlock title="Shipping Address" address={order.shippingAddress} />
112
+ <AddressBlock title="Billing Address" address={order.billingAddress} />
113
+ </div>
114
+ </div>
115
+
116
+ <div className={styles.actions}>
117
+ <Link href="/products" className={styles.continueButton}>
118
+ Continue Shopping
119
+ </Link>
120
+ <Link href="/account/orders" className={styles.orderLink}>
121
+ View All Orders
122
+ </Link>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ )
127
+ }