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,192 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import Link from 'next/link'
5
+ import { useParams } from 'next/navigation'
6
+ import { useNymbalClient } from '@nymbalhq/react'
7
+ import type { Order } from '@nymbalhq/types'
8
+ import { formatPrice, formatDate } from '@/lib/format'
9
+ import styles from '@/styles/pages/account.module.css'
10
+ import confirmStyles from '@/styles/pages/confirmation.module.css'
11
+
12
+ export default function OrderDetailPage() {
13
+ const params = useParams<{ orderNumber: string }>()
14
+ const orderNumber = decodeURIComponent(params.orderNumber ?? '')
15
+ const client = useNymbalClient()
16
+ const [order, setOrder] = useState<Order | null>(null)
17
+ const [loading, setLoading] = useState(true)
18
+
19
+ useEffect(() => {
20
+ if (!orderNumber) return
21
+ let active = true
22
+ client.customer.orders
23
+ .get(orderNumber)
24
+ .then((found) => {
25
+ if (active) setOrder(found)
26
+ })
27
+ .catch(() => {
28
+ if (active) setOrder(null)
29
+ })
30
+ .finally(() => {
31
+ if (active) setLoading(false)
32
+ })
33
+ return () => {
34
+ active = false
35
+ }
36
+ }, [client, orderNumber])
37
+
38
+ if (loading) {
39
+ return (
40
+ <div>
41
+ <h2 className={styles.pageTitle}>Order Details</h2>
42
+ <div className="loading-center">
43
+ <div className="spinner spinner-lg" role="status">
44
+ <span className="visually-hidden">Loading order...</span>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ )
49
+ }
50
+
51
+ if (!order) {
52
+ return (
53
+ <div>
54
+ <h2 className={styles.pageTitle}>Order Not Found</h2>
55
+ <p className="text-muted">This order could not be found.</p>
56
+ <Link
57
+ href="/account/orders"
58
+ className="btn btn-outline"
59
+ style={{ marginTop: 'var(--nymbal-spacing-md)', display: 'inline-flex' }}
60
+ >
61
+ Back to Orders
62
+ </Link>
63
+ </div>
64
+ )
65
+ }
66
+
67
+ return (
68
+ <div data-testid="order-detail">
69
+ <div
70
+ style={{
71
+ display: 'flex',
72
+ justifyContent: 'space-between',
73
+ alignItems: 'center',
74
+ flexWrap: 'wrap',
75
+ gap: 'var(--nymbal-spacing-sm)',
76
+ marginBottom: 'var(--nymbal-spacing-lg)',
77
+ }}
78
+ >
79
+ <h2 className={styles.pageTitle} style={{ marginBottom: 0 }}>
80
+ Order #{order.orderNumber}
81
+ </h2>
82
+ <span
83
+ className={`${styles.orderStatus} ${styles.statusPending}`}
84
+ style={{ textTransform: 'capitalize' }}
85
+ >
86
+ {order.status}
87
+ </span>
88
+ </div>
89
+
90
+ {order.createdAt && (
91
+ <p className="text-sm text-muted mb-md">Placed on {formatDate(order.createdAt)}</p>
92
+ )}
93
+
94
+ <div className={styles.formCard} style={{ marginBottom: 'var(--nymbal-spacing-lg)' }}>
95
+ <h3 className={styles.formCardTitle}>Items</h3>
96
+ <div className={confirmStyles.lineItems}>
97
+ {order.lineItems.map((item) => (
98
+ <div key={item.variantId} className={confirmStyles.lineItem}>
99
+ {item.imageUrl && (
100
+ <div className={confirmStyles.lineItemImage}>
101
+ <img src={item.imageUrl} alt={item.productName} />
102
+ </div>
103
+ )}
104
+ <div className={confirmStyles.lineItemInfo}>
105
+ <p className={confirmStyles.lineItemName}>{item.productName}</p>
106
+ {item.variantName && (
107
+ <p className={confirmStyles.lineItemVariant}>{item.variantName}</p>
108
+ )}
109
+ <p className={confirmStyles.lineItemQty}>Qty: {item.qty}</p>
110
+ </div>
111
+ <span className={confirmStyles.lineItemPrice}>
112
+ {formatPrice(item.lineTotalMinor, order.currency)}
113
+ </span>
114
+ </div>
115
+ ))}
116
+ </div>
117
+
118
+ <div className={confirmStyles.totals}>
119
+ <div className={confirmStyles.totalRow}>
120
+ <span className={confirmStyles.totalLabel}>Subtotal</span>
121
+ <span>{formatPrice(order.subtotalMinor, order.currency)}</span>
122
+ </div>
123
+ <div className={confirmStyles.totalRow}>
124
+ <span className={confirmStyles.totalLabel}>Shipping</span>
125
+ <span>
126
+ {order.shippingTotalMinor === 0
127
+ ? 'Free'
128
+ : formatPrice(order.shippingTotalMinor, order.currency)}
129
+ </span>
130
+ </div>
131
+ <div className={confirmStyles.totalRow}>
132
+ <span className={confirmStyles.totalLabel}>Tax</span>
133
+ <span>{formatPrice(order.taxTotalMinor, order.currency)}</span>
134
+ </div>
135
+ <div className={`${confirmStyles.totalRow} ${confirmStyles.totalGrand}`}>
136
+ <span>Total</span>
137
+ <span>{formatPrice(order.totalMinor, order.currency)}</span>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <div className={styles.formCard}>
143
+ <h3 className={styles.formCardTitle}>Delivery Details</h3>
144
+ <div
145
+ className={confirmStyles.addressGrid}
146
+ style={{ marginTop: 'var(--nymbal-spacing-md)' }}
147
+ >
148
+ {order.shippingAddress && (
149
+ <div className={confirmStyles.addressCard}>
150
+ <h4 className={confirmStyles.addressTitle}>Shipping Address</h4>
151
+ <p className={confirmStyles.addressText}>
152
+ {order.shippingAddress.firstName} {order.shippingAddress.lastName}
153
+ <br />
154
+ {order.shippingAddress.addressLine1}
155
+ {order.shippingAddress.addressLine2 && <>, {order.shippingAddress.addressLine2}</>}
156
+ <br />
157
+ {order.shippingAddress.city}, {order.shippingAddress.region}{' '}
158
+ {order.shippingAddress.postalCode}
159
+ <br />
160
+ {order.shippingAddress.country}
161
+ </p>
162
+ </div>
163
+ )}
164
+ {order.billingAddress && (
165
+ <div className={confirmStyles.addressCard}>
166
+ <h4 className={confirmStyles.addressTitle}>Billing Address</h4>
167
+ <p className={confirmStyles.addressText}>
168
+ {order.billingAddress.firstName} {order.billingAddress.lastName}
169
+ <br />
170
+ {order.billingAddress.addressLine1}
171
+ {order.billingAddress.addressLine2 && <>, {order.billingAddress.addressLine2}</>}
172
+ <br />
173
+ {order.billingAddress.city}, {order.billingAddress.region}{' '}
174
+ {order.billingAddress.postalCode}
175
+ <br />
176
+ {order.billingAddress.country}
177
+ </p>
178
+ </div>
179
+ )}
180
+ </div>
181
+ </div>
182
+
183
+ <Link
184
+ href="/account/orders"
185
+ className="btn btn-outline"
186
+ style={{ marginTop: 'var(--nymbal-spacing-lg)', display: 'inline-flex' }}
187
+ >
188
+ Back to Orders
189
+ </Link>
190
+ </div>
191
+ )
192
+ }
@@ -0,0 +1,123 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import Link from 'next/link'
5
+ import { useNymbalClient } from '@nymbalhq/react'
6
+ import type { Order } from '@nymbalhq/types'
7
+ import { formatPrice, formatDate } from '@/lib/format'
8
+ import styles from '@/styles/pages/account.module.css'
9
+
10
+ function getStatusClass(status: string): string {
11
+ const map: Record<string, string> = {
12
+ pending: styles.statusPending,
13
+ confirmed: styles.statusConfirmed,
14
+ processing: styles.statusProcessing,
15
+ shipped: styles.statusShipped,
16
+ delivered: styles.statusDelivered,
17
+ cancelled: styles.statusCancelled,
18
+ refunded: styles.statusRefunded,
19
+ }
20
+ return map[status.toLowerCase()] ?? styles.statusPending
21
+ }
22
+
23
+ export default function OrdersPage() {
24
+ const client = useNymbalClient()
25
+ const [orders, setOrders] = useState<Order[]>([])
26
+ const [loading, setLoading] = useState(true)
27
+ const [failed, setFailed] = useState(false)
28
+
29
+ useEffect(() => {
30
+ let active = true
31
+ client.customer.orders
32
+ .list()
33
+ .then((page) => {
34
+ if (active) setOrders(page.items)
35
+ })
36
+ .catch(() => {
37
+ if (active) setFailed(true)
38
+ })
39
+ .finally(() => {
40
+ if (active) setLoading(false)
41
+ })
42
+ return () => {
43
+ active = false
44
+ }
45
+ }, [client])
46
+
47
+ if (loading) {
48
+ return (
49
+ <div>
50
+ <h2 className={styles.pageTitle}>Order History</h2>
51
+ <div className="loading-center">
52
+ <div className="spinner spinner-lg" role="status">
53
+ <span className="visually-hidden">Loading orders...</span>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ if (failed) {
61
+ return (
62
+ <div>
63
+ <h2 className={styles.pageTitle}>Order History</h2>
64
+ <p className="text-muted">Unable to load orders. Please try again later.</p>
65
+ </div>
66
+ )
67
+ }
68
+
69
+ if (orders.length === 0) {
70
+ return (
71
+ <div>
72
+ <h2 className={styles.pageTitle}>Order History</h2>
73
+ <div className={styles.emptyOrders}>
74
+ <h3 className={styles.emptyOrdersTitle}>No orders yet</h3>
75
+ <p className={styles.emptyOrdersText}>When you place an order, it will appear here.</p>
76
+ <Link
77
+ href="/products"
78
+ className="btn btn-primary"
79
+ style={{ marginTop: 'var(--nymbal-spacing-md)', display: 'inline-flex' }}
80
+ >
81
+ Start Shopping
82
+ </Link>
83
+ </div>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ return (
89
+ <div>
90
+ <h2 className={styles.pageTitle}>Order History</h2>
91
+ <div className={styles.orderList} data-testid="orders-list">
92
+ {orders.map((order) => (
93
+ <div key={order.id} className={styles.orderCard}>
94
+ <div className={styles.orderHeader}>
95
+ <Link
96
+ href={`/account/orders/${encodeURIComponent(order.orderNumber)}`}
97
+ className={styles.orderNumber}
98
+ data-testid={`order-${order.orderNumber}`}
99
+ >
100
+ #{order.orderNumber}
101
+ </Link>
102
+ <span className={`${styles.orderStatus} ${getStatusClass(order.status)}`}>
103
+ {order.status}
104
+ </span>
105
+ </div>
106
+ <div className={styles.orderMeta}>
107
+ {order.createdAt && (
108
+ <span>
109
+ <span className={styles.orderMetaLabel}>Date: </span>
110
+ {formatDate(order.createdAt)}
111
+ </span>
112
+ )}
113
+ <span>
114
+ <span className={styles.orderMetaLabel}>Total: </span>
115
+ {formatPrice(order.totalMinor, order.currency)}
116
+ </span>
117
+ </div>
118
+ </div>
119
+ ))}
120
+ </div>
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,59 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { useAuth } from '@nymbalhq/react'
5
+ import styles from '@/styles/pages/account.module.css'
6
+
7
+ export default function AccountDashboard() {
8
+ const { customer } = useAuth()
9
+
10
+ const greeting = customer?.firstName ? `Welcome back, ${customer.firstName}` : 'Welcome back'
11
+
12
+ return (
13
+ <div data-testid="account-dashboard">
14
+ <h2 className={styles.pageTitle}>{greeting}</h2>
15
+ {customer?.email && <p className="text-sm text-muted mb-md">{customer.email}</p>}
16
+
17
+ <div
18
+ style={{
19
+ display: 'grid',
20
+ gridTemplateColumns: 'repeat(auto-fill, minmax(14rem, 1fr))',
21
+ gap: 'var(--nymbal-spacing-md)',
22
+ }}
23
+ >
24
+ <Link
25
+ href="/account/orders"
26
+ className={styles.formCard}
27
+ style={{ textDecoration: 'none', color: 'inherit' }}
28
+ >
29
+ <h3 className={styles.formCardTitle}>Orders</h3>
30
+ <p className="text-sm text-muted">View your order history and track shipments.</p>
31
+ </Link>
32
+ <Link
33
+ href="/account/addresses"
34
+ className={styles.formCard}
35
+ style={{ textDecoration: 'none', color: 'inherit' }}
36
+ >
37
+ <h3 className={styles.formCardTitle}>Addresses</h3>
38
+ <p className="text-sm text-muted">Manage your shipping and billing addresses.</p>
39
+ </Link>
40
+ <Link
41
+ href="/account/profile"
42
+ className={styles.formCard}
43
+ style={{ textDecoration: 'none', color: 'inherit' }}
44
+ >
45
+ <h3 className={styles.formCardTitle}>Profile</h3>
46
+ <p className="text-sm text-muted">Update your personal information.</p>
47
+ </Link>
48
+ <Link
49
+ href="/products"
50
+ className={styles.formCard}
51
+ style={{ textDecoration: 'none', color: 'inherit' }}
52
+ >
53
+ <h3 className={styles.formCardTitle}>Shop</h3>
54
+ <p className="text-sm text-muted">Browse our latest products and collections.</p>
55
+ </Link>
56
+ </div>
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,10 @@
1
+ import type { Metadata } from 'next'
2
+ import { ProfileForm } from '@/components/client/ProfileForm'
3
+
4
+ export const metadata: Metadata = {
5
+ title: 'Profile',
6
+ }
7
+
8
+ export default function ProfilePage() {
9
+ return <ProfileForm />
10
+ }
@@ -0,0 +1,10 @@
1
+ import type { Metadata } from 'next'
2
+ import { RegisterForm } from '@/components/client/RegisterForm'
3
+
4
+ export const metadata: Metadata = {
5
+ title: 'Create Account',
6
+ }
7
+
8
+ export default function RegisterPage() {
9
+ return <RegisterForm />
10
+ }
@@ -0,0 +1,10 @@
1
+ import type { Metadata } from 'next'
2
+ import { CartPage } from '@/components/client/CartPage'
3
+
4
+ export const metadata: Metadata = {
5
+ title: 'Shopping Cart',
6
+ }
7
+
8
+ export default function CartRoute() {
9
+ return <CartPage />
10
+ }
@@ -0,0 +1,58 @@
1
+ import type { Metadata } from 'next'
2
+ import { notFound } from 'next/navigation'
3
+ import { fetchProducts, fetchCategories, sortFromSearchParams } from '@/lib/api'
4
+ import { Breadcrumb } from '@/components/server/Breadcrumb'
5
+ import { ProductGrid } from '@/components/client/ProductGrid'
6
+ import styles from '@/styles/pages/plp.module.css'
7
+
8
+ interface CategoryPageProps {
9
+ params: Promise<{ slug: string }>
10
+ searchParams: Promise<{ sort?: string | string[] }>
11
+ }
12
+
13
+ export async function generateMetadata({
14
+ params,
15
+ }: CategoryPageProps): Promise<Metadata> {
16
+ const { slug } = await params
17
+ const categories = await fetchCategories()
18
+ const category = categories.find((c) => c.slug === slug)
19
+
20
+ return {
21
+ title: category?.name ?? slug,
22
+ description: category?.description ?? `Browse products in ${slug}.`,
23
+ }
24
+ }
25
+
26
+ export default async function CategoryPage({ params, searchParams }: CategoryPageProps) {
27
+ const { slug } = await params
28
+ const sort = sortFromSearchParams(await searchParams)
29
+ const [{ items, nextCursor }, categories] = await Promise.all([
30
+ fetchProducts({ limit: 24, category: slug, sort }),
31
+ fetchCategories(),
32
+ ])
33
+
34
+ const category = categories.find((c) => c.slug === slug)
35
+ if (!category) notFound()
36
+
37
+ return (
38
+ <div className={`container ${styles.page}`}>
39
+ <Breadcrumb
40
+ items={[
41
+ { label: 'Home', href: '/' },
42
+ { label: category.name },
43
+ ]}
44
+ />
45
+ <h1 className={styles.pageTitle}>{category.name}</h1>
46
+ {category.description && (
47
+ <p className="text-muted mb-lg">{category.description}</p>
48
+ )}
49
+ <ProductGrid
50
+ initialProducts={items}
51
+ initialCursor={nextCursor}
52
+ initialSort={sort}
53
+ categorySlug={slug}
54
+ categories={categories}
55
+ />
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,10 @@
1
+ import type { Metadata } from 'next'
2
+ import { CheckoutForm } from '@/components/client/CheckoutForm'
3
+
4
+ export const metadata: Metadata = {
5
+ title: 'Checkout',
6
+ }
7
+
8
+ export default function CheckoutRoute() {
9
+ return <CheckoutForm />
10
+ }
@@ -0,0 +1,49 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import styles from '@/styles/pages/error.module.css'
5
+
6
+ interface ErrorProps {
7
+ error: Error & { digest?: string }
8
+ reset: () => void
9
+ }
10
+
11
+ export default function ErrorPage({ error: _error, reset }: ErrorProps) {
12
+ return (
13
+ <div className={styles.page}>
14
+ <div className={styles.wrapper}>
15
+ <div className={styles.icon}>
16
+ <svg
17
+ viewBox="0 0 24 24"
18
+ fill="none"
19
+ stroke="currentColor"
20
+ strokeWidth="2"
21
+ strokeLinecap="round"
22
+ strokeLinejoin="round"
23
+ aria-hidden="true"
24
+ >
25
+ <circle cx="12" cy="12" r="10" />
26
+ <line x1="12" y1="8" x2="12" y2="12" />
27
+ <line x1="12" y1="16" x2="12.01" y2="16" />
28
+ </svg>
29
+ </div>
30
+ <h1 className={styles.title}>Something went wrong</h1>
31
+ <p className={styles.message}>
32
+ An unexpected error occurred. Please try again or return to the home page.
33
+ </p>
34
+ <div className={styles.actions}>
35
+ <Link href="/" className={styles.homeLink}>
36
+ Go Home
37
+ </Link>
38
+ <button
39
+ type="button"
40
+ className={styles.retryButton}
41
+ onClick={reset}
42
+ >
43
+ Try Again
44
+ </button>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ )
49
+ }