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,148 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect, useState, useCallback } from 'react'
4
- import type { DenormalisedProduct } from '@nymbalhq/sdk'
5
- import type { Category, Facet, ProductSortKey } from '@nymbalhq/types'
6
- import { useProductList } from '@nymbalhq/react'
7
- import { ProductFilter } from '@nymbalhq/react'
8
- import { ProductCard } from '@/components/server/ProductCard'
9
- import { SortDropdown } from './SortDropdown'
10
- import styles from '@/styles/pages/plp.module.css'
11
-
12
- interface ProductGridProps {
13
- initialProducts: DenormalisedProduct[]
14
- initialCursor: string | null
15
- initialSort: ProductSortKey
16
- categorySlug?: string
17
- categories: Category[]
18
- }
19
-
20
- export function ProductGrid({
21
- initialProducts,
22
- initialCursor,
23
- initialSort,
24
- categorySlug,
25
- categories,
26
- }: ProductGridProps) {
27
- const { products, facets, pagination, loading, sort, load, loadMore, setSort } = useProductList()
28
- const [initialized, setInitialized] = useState(false)
29
- const [showMobileFilters, setShowMobileFilters] = useState(false)
30
- // Preserve the widest facet list seen so filter options never disappear during a
31
- // filtered query (the API always returns full facets, but this guards against regressions).
32
- const [stableFacets, setStableFacets] = useState<Facet[]>(
33
- categories.length > 0
34
- ? [
35
- {
36
- field: 'category',
37
- label: 'Category',
38
- values: categories.map((c) => ({ value: c.slug, label: c.name })),
39
- },
40
- ]
41
- : [],
42
- )
43
-
44
- useEffect(() => {
45
- load({ limit: 24, category: categorySlug, sort: initialSort })
46
- setInitialized(true)
47
- }, [categorySlug, initialSort, load])
48
-
49
- // The URL carries the sort, so a reload or a shared link renders the same order server-side.
50
- const handleSort = useCallback(
51
- (key: ProductSortKey) => {
52
- const url = new URL(window.location.href)
53
- url.searchParams.set('sort', key)
54
- window.history.replaceState(window.history.state, '', url)
55
- void setSort(key)
56
- },
57
- [setSort],
58
- )
59
-
60
- // Keep stableFacets updated whenever the store returns a wider facet list.
61
- useEffect(() => {
62
- if (facets.length > 0) {
63
- const incomingValues = facets.reduce((acc, f) => acc + f.values.length, 0)
64
- const stableValues = stableFacets.reduce((acc, f) => acc + f.values.length, 0)
65
- if (incomingValues >= stableValues) {
66
- setStableFacets(facets)
67
- }
68
- }
69
- }, [facets, stableFacets])
70
-
71
- const displayProducts = initialized && products.length > 0 ? products : initialProducts
72
- const hasMore = initialized ? pagination.hasMore : !!initialCursor
73
-
74
- const handleLoadMore = useCallback(() => {
75
- if (hasMore && !loading) {
76
- loadMore()
77
- }
78
- }, [hasMore, loading, loadMore])
79
-
80
- // Use stableFacets (which always contains the full category list) so all filter
81
- // options remain visible after a category filter is applied.
82
- const displayFacets = stableFacets
83
-
84
- return (
85
- <div className={styles.layout}>
86
- <aside className={styles.sidebar}>
87
- <button
88
- className={styles.mobileFilterToggle}
89
- onClick={() => setShowMobileFilters((v) => !v)}
90
- type="button"
91
- >
92
- {showMobileFilters ? 'Hide Filters' : 'Show Filters'}
93
- </button>
94
- <div
95
- className={styles.filterSection}
96
- style={{ display: showMobileFilters ? 'block' : undefined }}
97
- >
98
- <ProductFilter facets={displayFacets} />
99
- </div>
100
- </aside>
101
-
102
- <div className={styles.content}>
103
- <div className={styles.sortBar}>
104
- <span className={styles.resultCount}>
105
- {displayProducts.length} product{displayProducts.length !== 1 ? 's' : ''}
106
- </span>
107
- <SortDropdown value={initialized ? sort : initialSort} onChange={handleSort} />
108
- </div>
109
-
110
- {displayProducts.length > 0 ? (
111
- <div className="product-grid">
112
- {displayProducts.map((product) => (
113
- <ProductCard
114
- key={product.id}
115
- product={{
116
- slug: product.slug,
117
- name: product.name,
118
- priceMinor: product.priceMinor ?? null,
119
- priceRange: product.priceRange ?? null,
120
- currency: product.currency ?? null,
121
- media: product.media,
122
- }}
123
- />
124
- ))}
125
- </div>
126
- ) : (
127
- <div className={styles.empty}>
128
- <p>No products found.</p>
129
- </div>
130
- )}
131
-
132
- {hasMore && (
133
- <div className={styles.pagination}>
134
- <button
135
- type="button"
136
- className="btn btn-secondary"
137
- onClick={handleLoadMore}
138
- disabled={loading}
139
- data-testid="load-more"
140
- >
141
- {loading ? 'Loading...' : 'Load More'}
142
- </button>
143
- </div>
144
- )}
145
- </div>
146
- </div>
147
- )
148
- }
@@ -1,112 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useEffect, useCallback, type FormEvent } from 'react'
4
- import { useAuth } from '@nymbalhq/react'
5
- import styles from '@/styles/pages/account.module.css'
6
-
7
- export function ProfileForm() {
8
- const { customer, updateProfile } = useAuth()
9
-
10
- const [firstName, setFirstName] = useState('')
11
- const [lastName, setLastName] = useState('')
12
- const [phone, setPhone] = useState('')
13
- const [saving, setSaving] = useState(false)
14
- const [success, setSuccess] = useState(false)
15
- const [error, setError] = useState<string | null>(null)
16
-
17
- useEffect(() => {
18
- if (customer) {
19
- setFirstName(customer.firstName ?? '')
20
- setLastName(customer.lastName ?? '')
21
- setPhone(customer.phone ?? '')
22
- }
23
- }, [customer])
24
-
25
- const handleSubmit = useCallback(
26
- async (e: FormEvent) => {
27
- e.preventDefault()
28
- setSaving(true)
29
- setError(null)
30
- setSuccess(false)
31
-
32
- try {
33
- await updateProfile({ firstName, lastName, phone })
34
- setSuccess(true)
35
- } catch (err) {
36
- setError(err instanceof Error ? err.message : 'Failed to update profile.')
37
- } finally {
38
- setSaving(false)
39
- }
40
- },
41
- [firstName, lastName, phone, updateProfile],
42
- )
43
-
44
- return (
45
- <div>
46
- <h2 className={styles.pageTitle}>Profile</h2>
47
-
48
- {success && (
49
- <div className={`${styles.alert} ${styles.alertSuccess}`}>
50
- Profile updated successfully.
51
- </div>
52
- )}
53
- {error && <div className={`${styles.alert} ${styles.alertError}`}>{error}</div>}
54
-
55
- <div className={styles.formCard}>
56
- <h3 className={styles.formCardTitle}>Personal Information</h3>
57
- <form onSubmit={handleSubmit}>
58
- <div className={styles.formGrid}>
59
- <div>
60
- <label htmlFor="profile-first-name">First Name</label>
61
- <input
62
- id="profile-first-name"
63
- data-testid="profile-firstName"
64
- type="text"
65
- value={firstName}
66
- onChange={(e) => setFirstName(e.target.value)}
67
- autoComplete="given-name"
68
- />
69
- </div>
70
- <div>
71
- <label htmlFor="profile-last-name">Last Name</label>
72
- <input
73
- id="profile-last-name"
74
- data-testid="profile-lastName"
75
- type="text"
76
- value={lastName}
77
- onChange={(e) => setLastName(e.target.value)}
78
- autoComplete="family-name"
79
- />
80
- </div>
81
- <div className={styles.formGridFull}>
82
- <label htmlFor="profile-email">Email</label>
83
- <input
84
- id="profile-email"
85
- type="email"
86
- value={customer?.email ?? ''}
87
- disabled
88
- autoComplete="email"
89
- />
90
- </div>
91
- <div className={styles.formGridFull}>
92
- <label htmlFor="profile-phone">Phone</label>
93
- <input
94
- id="profile-phone"
95
- type="tel"
96
- value={phone}
97
- onChange={(e) => setPhone(e.target.value)}
98
- placeholder="Your phone number"
99
- autoComplete="tel"
100
- />
101
- </div>
102
- </div>
103
- <div className={styles.formActions}>
104
- <button type="submit" className={styles.saveButton} disabled={saving}>
105
- {saving ? 'Saving...' : 'Save Changes'}
106
- </button>
107
- </div>
108
- </form>
109
- </div>
110
- </div>
111
- )
112
- }
@@ -1,131 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useCallback, type FormEvent } from 'react'
4
- import { useRouter } from 'next/navigation'
5
- import Link from 'next/link'
6
- import { useAuth } from '@nymbalhq/react'
7
- import styles from '@/styles/pages/account.module.css'
8
-
9
- export function RegisterForm() {
10
- const router = useRouter()
11
- const { register } = useAuth()
12
-
13
- const [firstName, setFirstName] = useState('')
14
- const [lastName, setLastName] = useState('')
15
- const [email, setEmail] = useState('')
16
- const [password, setPassword] = useState('')
17
- const [confirmPassword, setConfirmPassword] = useState('')
18
- const [error, setError] = useState<string | null>(null)
19
- const [loading, setLoading] = useState(false)
20
-
21
- const handleSubmit = useCallback(
22
- async (e: FormEvent) => {
23
- e.preventDefault()
24
- setError(null)
25
-
26
- if (password !== confirmPassword) {
27
- setError('Passwords do not match.')
28
- return
29
- }
30
-
31
- setLoading(true)
32
-
33
- try {
34
- await register({ firstName, lastName, email, password })
35
- router.push('/account')
36
- } catch (err) {
37
- setError(err instanceof Error ? err.message : 'Registration failed. Please try again.')
38
- setLoading(false)
39
- }
40
- },
41
- [firstName, lastName, email, password, confirmPassword, register, router],
42
- )
43
-
44
- return (
45
- <div className={styles.authPage}>
46
- <div className={styles.authCard} data-testid="register-form">
47
- <h1 className={styles.authTitle}>Create Account</h1>
48
- <p className={styles.authSubtitle}>Join us and start shopping.</p>
49
-
50
- {error && <div className={styles.alertError + ' ' + styles.alert}>{error}</div>}
51
-
52
- <form onSubmit={handleSubmit} className={styles.authForm}>
53
- <div className="form-group">
54
- <label htmlFor="register-first-name">First Name</label>
55
- <input
56
- id="register-first-name"
57
- type="text"
58
- value={firstName}
59
- onChange={(e) => setFirstName(e.target.value)}
60
- required
61
- placeholder="First name"
62
- autoComplete="given-name"
63
- />
64
- </div>
65
- <div className="form-group">
66
- <label htmlFor="register-last-name">Last Name</label>
67
- <input
68
- id="register-last-name"
69
- type="text"
70
- value={lastName}
71
- onChange={(e) => setLastName(e.target.value)}
72
- required
73
- placeholder="Last name"
74
- autoComplete="family-name"
75
- />
76
- </div>
77
- <div className="form-group">
78
- <label htmlFor="register-email">Email</label>
79
- <input
80
- id="register-email"
81
- type="email"
82
- value={email}
83
- onChange={(e) => setEmail(e.target.value)}
84
- required
85
- data-testid="register-email"
86
- placeholder="you@example.com"
87
- autoComplete="email"
88
- />
89
- </div>
90
- <div className="form-group">
91
- <label htmlFor="register-password">Password</label>
92
- <input
93
- id="register-password"
94
- type="password"
95
- value={password}
96
- onChange={(e) => setPassword(e.target.value)}
97
- required
98
- placeholder="Create a password"
99
- autoComplete="new-password"
100
- minLength={8}
101
- />
102
- </div>
103
- <div className="form-group">
104
- <label htmlFor="register-confirm-password">Confirm Password</label>
105
- <input
106
- id="register-confirm-password"
107
- type="password"
108
- value={confirmPassword}
109
- onChange={(e) => setConfirmPassword(e.target.value)}
110
- required
111
- placeholder="Confirm your password"
112
- autoComplete="new-password"
113
- />
114
- </div>
115
- <button
116
- type="submit"
117
- className={styles.authSubmit}
118
- disabled={loading}
119
- >
120
- {loading ? 'Creating account...' : 'Create Account'}
121
- </button>
122
- </form>
123
-
124
- <p className={styles.authFooter}>
125
- Already have an account?{' '}
126
- <Link href="/account/login">Sign in</Link>
127
- </p>
128
- </div>
129
- </div>
130
- )
131
- }
@@ -1,116 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useCallback, type FormEvent } from 'react'
4
- import styles from '@/styles/pages/pdp.module.css'
5
- import { getNymbalClient } from '@/lib/client'
6
-
7
- interface ReviewFormProps {
8
- productId: string
9
- }
10
-
11
- export function ReviewForm({ productId }: ReviewFormProps) {
12
- const [rating, setRating] = useState(5)
13
- const [title, setTitle] = useState('')
14
- const [body, setBody] = useState('')
15
- const [submitting, setSubmitting] = useState(false)
16
- const [success, setSuccess] = useState(false)
17
- const [error, setError] = useState<string | null>(null)
18
-
19
- const handleSubmit = useCallback(
20
- async (e: FormEvent) => {
21
- e.preventDefault()
22
- setSubmitting(true)
23
- setError(null)
24
-
25
- try {
26
- await getNymbalClient().adapter.reviews.submit({ productId, rating, title, body })
27
- setSuccess(true)
28
- setTitle('')
29
- setBody('')
30
- setRating(5)
31
- } catch {
32
- setError('Failed to submit review.')
33
- } finally {
34
- setSubmitting(false)
35
- }
36
- },
37
- [productId, rating, title, body],
38
- )
39
-
40
- if (success) {
41
- return (
42
- <div className={styles.reviewsSection}>
43
- <h3 className={styles.reviewsTitle}>Write a Review</h3>
44
- <p style={{ fontSize: 'var(--nymbal-font-size-sm)', color: 'var(--nymbal-color-success)' }}>
45
- Thank you for your review! It has been submitted successfully.
46
- </p>
47
- </div>
48
- )
49
- }
50
-
51
- return (
52
- <div className={styles.reviewsSection}>
53
- <h3 className={styles.reviewsTitle}>Write a Review</h3>
54
-
55
- {error && (
56
- <p
57
- style={{
58
- fontSize: 'var(--nymbal-font-size-sm)',
59
- color: 'var(--nymbal-color-error)',
60
- marginBottom: 'var(--nymbal-spacing-md)',
61
- }}
62
- >
63
- {error}
64
- </p>
65
- )}
66
-
67
- <form onSubmit={handleSubmit} data-testid="review-form">
68
- <div className="form-group">
69
- <label htmlFor="review-rating">Rating</label>
70
- <div className={styles.reviewStars} style={{ cursor: 'pointer', fontSize: '1.5rem' }}>
71
- {Array.from({ length: 5 }, (_, i) => (
72
- <span
73
- key={i}
74
- onClick={() => setRating(i + 1)}
75
- className={i < rating ? styles.starFilled : styles.starEmpty}
76
- role="button"
77
- tabIndex={0}
78
- aria-label={`Rate ${i + 1} star${i === 0 ? '' : 's'}`}
79
- onKeyDown={(e) => {
80
- if (e.key === 'Enter' || e.key === ' ') setRating(i + 1)
81
- }}
82
- >
83
- ★
84
- </span>
85
- ))}
86
- </div>
87
- </div>
88
- <div className="form-group">
89
- <label htmlFor="review-title">Title</label>
90
- <input
91
- id="review-title"
92
- type="text"
93
- value={title}
94
- onChange={(e) => setTitle(e.target.value)}
95
- required
96
- placeholder="Summarise your experience"
97
- />
98
- </div>
99
- <div className="form-group">
100
- <label htmlFor="review-body">Review</label>
101
- <textarea
102
- id="review-body"
103
- value={body}
104
- onChange={(e) => setBody(e.target.value)}
105
- required
106
- rows={4}
107
- placeholder="Tell us what you thought..."
108
- />
109
- </div>
110
- <button type="submit" className="btn btn-primary" disabled={submitting}>
111
- {submitting ? 'Submitting...' : 'Submit Review'}
112
- </button>
113
- </form>
114
- </div>
115
- )
116
- }
@@ -1,27 +0,0 @@
1
- 'use client'
2
-
3
- import { useCallback } from 'react'
4
- import { useRouter } from 'next/navigation'
5
- import { SearchBar } from '@nymbalhq/react'
6
-
7
- interface SearchProduct {
8
- slug: string
9
- }
10
-
11
- export function SearchBarWrapper() {
12
- const router = useRouter()
13
-
14
- const handleSelect = useCallback(
15
- (event: CustomEvent<{ product: unknown }>) => {
16
- const product = event.detail.product as SearchProduct
17
- if (product?.slug) {
18
- router.push(`/product/${encodeURIComponent(product.slug)}`)
19
- }
20
- },
21
- [router],
22
- )
23
-
24
- return (
25
- <SearchBar placeholder="Search products..." debounceMs={300} onSearchSelect={handleSelect} />
26
- )
27
- }
@@ -1,141 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect } from 'react'
4
- import { useSearchParams } from 'next/navigation'
5
- import Link from 'next/link'
6
- import Image from 'next/image'
7
- import { useSearch } from '@nymbalhq/react'
8
- import { formatPrice } from '@/lib/format'
9
- import styles from '@/styles/pages/search.module.css'
10
-
11
- export function SearchResults() {
12
- const searchParams = useSearchParams()
13
- const query = searchParams.get('q') ?? ''
14
- const { results, loading, search } = useSearch()
15
-
16
- useEffect(() => {
17
- if (query) {
18
- search(query)
19
- }
20
- }, [query, search])
21
-
22
- return (
23
- <div className={`container ${styles.page}`}>
24
- <div className={styles.header}>
25
- <h1 className={styles.pageTitle}>Search Results</h1>
26
- {query && (
27
- <p className={styles.queryText}>
28
- Showing results for <span className={styles.queryTerm}>&ldquo;{query}&rdquo;</span>
29
- </p>
30
- )}
31
- </div>
32
-
33
- {loading ? (
34
- <div className={styles.loading}>
35
- <div className="spinner spinner-lg" role="status">
36
- <span className="visually-hidden">Searching...</span>
37
- </div>
38
- </div>
39
- ) : !query ? (
40
- <div className={styles.empty}>
41
- <div className={styles.emptyIcon}>
42
- <svg
43
- viewBox="0 0 24 24"
44
- fill="none"
45
- stroke="currentColor"
46
- strokeWidth="2"
47
- strokeLinecap="round"
48
- strokeLinejoin="round"
49
- aria-hidden="true"
50
- >
51
- <circle cx="11" cy="11" r="8" />
52
- <line x1="21" y1="21" x2="16.65" y2="16.65" />
53
- </svg>
54
- </div>
55
- <h2 className={styles.emptyTitle}>Search our store</h2>
56
- <p className={styles.emptyText}>
57
- Use the search bar above to find what you are looking for.
58
- </p>
59
- </div>
60
- ) : results && results.length > 0 ? (
61
- <>
62
- <p className={styles.resultCount}>
63
- {results.length} result{results.length !== 1 ? 's' : ''} found
64
- </p>
65
- <div className={styles.grid}>
66
- {results.map((product) => {
67
- const media = Array.isArray(product.media) ? product.media : []
68
- const imageUrl = media.length > 0 && typeof (media[0] as Record<string, unknown>)?.url === 'string'
69
- ? (media[0] as Record<string, string>).url
70
- : null
71
- return (
72
- <Link
73
- key={product.id}
74
- href={`/product/${product.slug}`}
75
- className={styles.productCard}
76
- data-testid={`product-card-${product.slug}`}
77
- >
78
- <div className={styles.productImage}>
79
- {imageUrl && (
80
- <Image
81
- src={imageUrl}
82
- alt={product.name}
83
- width={400}
84
- height={400}
85
- sizes="(max-width: 639px) 100vw, (max-width: 1023px) 50vw, 25vw"
86
- />
87
- )}
88
- </div>
89
- <div className={styles.productInfo}>
90
- <p className={styles.productName}>{product.name}</p>
91
- {product.priceMinor != null && product.currency && (
92
- <p className={styles.productPrice}>
93
- {formatPrice(product.priceMinor, product.currency)}
94
- </p>
95
- )}
96
- </div>
97
- </Link>
98
- )
99
- })}
100
- </div>
101
- </>
102
- ) : (
103
- <div className={styles.empty}>
104
- <div className={styles.emptyIcon}>
105
- <svg
106
- viewBox="0 0 24 24"
107
- fill="none"
108
- stroke="currentColor"
109
- strokeWidth="2"
110
- strokeLinecap="round"
111
- strokeLinejoin="round"
112
- aria-hidden="true"
113
- >
114
- <circle cx="11" cy="11" r="8" />
115
- <line x1="21" y1="21" x2="16.65" y2="16.65" />
116
- </svg>
117
- </div>
118
- <h2 className={styles.emptyTitle}>No results found</h2>
119
- <p className={styles.emptyText}>
120
- We could not find anything matching &ldquo;{query}&rdquo;. Try a different search term.
121
- </p>
122
- </div>
123
- )}
124
-
125
- <div className={styles.suggestions}>
126
- <h3 className={styles.suggestionsTitle}>Popular searches</h3>
127
- <div className={styles.suggestionTags}>
128
- {['Shirts', 'Shoes', 'Jackets', 'Accessories', 'New Arrivals'].map((term) => (
129
- <Link
130
- key={term}
131
- href={`/search?q=${encodeURIComponent(term)}`}
132
- className={styles.suggestionTag}
133
- >
134
- {term}
135
- </Link>
136
- ))}
137
- </div>
138
- </div>
139
- </div>
140
- )
141
- }