create-brainerce-store 1.18.0 → 1.19.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 (65) hide show
  1. package/dist/index.js +31 -9
  2. package/messages/en.json +366 -362
  3. package/messages/he.json +366 -362
  4. package/package.json +45 -45
  5. package/templates/nextjs/base/next.config.ts +31 -31
  6. package/templates/nextjs/base/scripts/fetch-store-info.mjs +81 -81
  7. package/templates/nextjs/base/src/app/.well-known/apple-developer-merchantid-domain-association/route.ts +26 -26
  8. package/templates/nextjs/base/src/app/account/layout.tsx +9 -9
  9. package/templates/nextjs/base/src/app/account/page.tsx +122 -122
  10. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -14
  11. package/templates/nextjs/base/src/app/api/auth/me/route.ts +56 -56
  12. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -59
  13. package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -41
  14. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +77 -77
  15. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +198 -198
  16. package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -92
  17. package/templates/nextjs/base/src/app/cart/layout.tsx +9 -9
  18. package/templates/nextjs/base/src/app/cart/page.tsx +204 -204
  19. package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -9
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +860 -860
  21. package/templates/nextjs/base/src/app/forgot-password/page.tsx +112 -112
  22. package/templates/nextjs/base/src/app/layout.tsx.ejs +75 -0
  23. package/templates/nextjs/base/src/app/login/layout.tsx +9 -9
  24. package/templates/nextjs/base/src/app/login/page.tsx +59 -59
  25. package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -9
  26. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +254 -254
  27. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +67 -67
  28. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +486 -486
  29. package/templates/nextjs/base/src/app/products/layout.tsx +18 -18
  30. package/templates/nextjs/base/src/app/products/page.tsx +431 -431
  31. package/templates/nextjs/base/src/app/register/layout.tsx +9 -9
  32. package/templates/nextjs/base/src/app/register/page.tsx +65 -65
  33. package/templates/nextjs/base/src/app/reset-password/page.tsx +132 -132
  34. package/templates/nextjs/base/src/app/robots.ts +14 -14
  35. package/templates/nextjs/base/src/app/sitemap.ts +25 -25
  36. package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -258
  37. package/templates/nextjs/base/src/components/account/address-book.tsx +432 -432
  38. package/templates/nextjs/base/src/components/account/order-history.tsx +350 -350
  39. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
  40. package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -232
  41. package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +247 -111
  42. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
  43. package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +142 -142
  44. package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +59 -59
  45. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +415 -415
  46. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +243 -83
  47. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +519 -519
  48. package/templates/nextjs/base/src/components/layout/footer.tsx +41 -41
  49. package/templates/nextjs/base/src/components/layout/header.tsx +336 -336
  50. package/templates/nextjs/base/src/components/layout/language-switcher.tsx.ejs +63 -0
  51. package/templates/nextjs/base/src/components/products/discount-badge.tsx +22 -22
  52. package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +202 -202
  53. package/templates/nextjs/base/src/components/products/product-card.tsx +218 -218
  54. package/templates/nextjs/base/src/components/products/recommendation-section.tsx +107 -107
  55. package/templates/nextjs/base/src/components/products/stock-badge.tsx +63 -63
  56. package/templates/nextjs/base/src/components/products/variant-selector.tsx +292 -292
  57. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +72 -72
  58. package/templates/nextjs/base/src/i18n.ts.ejs +21 -0
  59. package/templates/nextjs/base/src/lib/auth.ts +149 -149
  60. package/templates/nextjs/base/src/lib/brainerce.ts.ejs +9 -0
  61. package/templates/nextjs/base/src/lib/translations.ts.ejs +31 -0
  62. package/templates/nextjs/base/src/middleware.ts.ejs +81 -0
  63. package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +41 -0
  64. package/templates/nextjs/base/src/lib/translations.ts +0 -11
  65. package/templates/nextjs/base/src/middleware.ts +0 -25
@@ -1,336 +1,336 @@
1
- 'use client';
2
-
3
- import { useState, useEffect, useRef, useCallback } from 'react';
4
- import Link from 'next/link';
5
- import Image from 'next/image';
6
- import { useRouter } from 'next/navigation';
7
- import type { SearchSuggestions, ProductSuggestion } from 'brainerce';
8
- import { formatPrice } from 'brainerce';
9
- import { getClient } from '@/lib/brainerce';
10
- import { useTranslations } from '@/lib/translations';
11
- import { useStoreInfo, useAuth, useCart } from '@/providers/store-provider';
12
- export function Header() {
13
- const t = useTranslations('nav');
14
- const tc = useTranslations('common');
15
- const { storeInfo } = useStoreInfo();
16
- const { isLoggedIn, logout } = useAuth();
17
- const { itemCount } = useCart();
18
- const router = useRouter();
19
-
20
- const [searchQuery, setSearchQuery] = useState('');
21
- const [suggestions, setSuggestions] = useState<SearchSuggestions | null>(null);
22
- const [showSuggestions, setShowSuggestions] = useState(false);
23
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
24
- const searchRef = useRef<HTMLDivElement>(null);
25
- const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
26
-
27
- const currency = storeInfo?.currency || 'USD';
28
-
29
- // Debounced search suggestions
30
- const fetchSuggestions = useCallback((query: string) => {
31
- if (debounceRef.current) clearTimeout(debounceRef.current);
32
-
33
- if (query.length < 2) {
34
- setSuggestions(null);
35
- setShowSuggestions(false);
36
- return;
37
- }
38
-
39
- debounceRef.current = setTimeout(async () => {
40
- try {
41
- const client = getClient();
42
- const result = await client.getSearchSuggestions(query, 5);
43
- setSuggestions(result);
44
- setShowSuggestions(true);
45
- } catch {
46
- setSuggestions(null);
47
- }
48
- }, 300);
49
- }, []);
50
-
51
- // Close suggestions on click outside
52
- useEffect(() => {
53
- function handleClickOutside(e: MouseEvent) {
54
- if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
55
- setShowSuggestions(false);
56
- }
57
- }
58
- document.addEventListener('mousedown', handleClickOutside);
59
- return () => document.removeEventListener('mousedown', handleClickOutside);
60
- }, []);
61
-
62
- function handleSearchSubmit(e: React.FormEvent) {
63
- e.preventDefault();
64
- if (searchQuery.trim()) {
65
- router.push(`/products?search=${encodeURIComponent(searchQuery.trim())}`);
66
- setShowSuggestions(false);
67
- setSearchQuery('');
68
- }
69
- }
70
-
71
- function handleSuggestionClick(suggestion: ProductSuggestion) {
72
- const href = suggestion.slug ? `/products/${suggestion.slug}` : `/products/${suggestion.id}`;
73
- router.push(href);
74
- setShowSuggestions(false);
75
- setSearchQuery('');
76
- }
77
-
78
- return (
79
- <header className="bg-background border-border sticky top-0 z-50 border-b">
80
- <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
81
- <div className="flex h-16 items-center justify-between gap-4">
82
- {/* Logo / Store Name */}
83
- <Link href="/" className="text-foreground flex-shrink-0 text-xl font-bold">
84
- {storeInfo?.name || process.env.NEXT_PUBLIC_STORE_NAME || tc('store')}
85
- </Link>
86
-
87
- {/* Desktop Navigation */}
88
- <nav className="hidden items-center gap-6 md:flex">
89
- <Link
90
- href="/products"
91
- className="text-muted-foreground hover:text-foreground text-sm transition-colors"
92
- >
93
- {t('products')}
94
- </Link>
95
- {isLoggedIn && (
96
- <Link
97
- href="/account"
98
- className="text-muted-foreground hover:text-foreground text-sm transition-colors"
99
- >
100
- {t('account')}
101
- </Link>
102
- )}
103
- </nav>
104
-
105
- {/* Search */}
106
- <div ref={searchRef} className="relative hidden max-w-md flex-1 sm:block">
107
- <form onSubmit={handleSearchSubmit}>
108
- <input
109
- type="text"
110
- value={searchQuery}
111
- onChange={(e) => {
112
- setSearchQuery(e.target.value);
113
- fetchSuggestions(e.target.value);
114
- }}
115
- onFocus={() => {
116
- if (suggestions && searchQuery.length >= 2) {
117
- setShowSuggestions(true);
118
- }
119
- }}
120
- placeholder={t('searchPlaceholder')}
121
- className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-9 w-full rounded border px-3 pe-9 text-sm focus:outline-none focus:ring-2"
122
- />
123
- <button
124
- type="submit"
125
- className="text-muted-foreground hover:text-foreground absolute end-0 top-0 flex h-9 w-9 items-center justify-center"
126
- aria-label={t('search')}
127
- >
128
- <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
129
- <path
130
- strokeLinecap="round"
131
- strokeLinejoin="round"
132
- strokeWidth={2}
133
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
134
- />
135
- </svg>
136
- </button>
137
- </form>
138
-
139
- {/* Search Suggestions Dropdown */}
140
- {showSuggestions && suggestions && (
141
- <div className="bg-background border-border absolute top-full z-50 mt-1 w-full overflow-hidden rounded-lg border shadow-lg">
142
- {suggestions.products.length > 0 && (
143
- <div>
144
- <div className="text-muted-foreground bg-muted px-3 py-1.5 text-xs font-medium">
145
- {t('products')}
146
- </div>
147
- {suggestions.products.map((product) => (
148
- <button
149
- key={product.id}
150
- type="button"
151
- onClick={() => handleSuggestionClick(product)}
152
- className="hover:bg-muted flex w-full items-center gap-3 px-3 py-2 text-start transition-colors"
153
- >
154
- {product.image ? (
155
- <Image
156
- src={product.image}
157
- alt={product.name}
158
- width={32}
159
- height={32}
160
- className="flex-shrink-0 rounded object-cover"
161
- />
162
- ) : (
163
- <div className="bg-muted h-8 w-8 flex-shrink-0 rounded" />
164
- )}
165
- <div className="min-w-0 flex-1">
166
- <p className="text-foreground truncate text-sm">{product.name}</p>
167
- <p className="text-muted-foreground text-xs">
168
- {
169
- formatPrice(product.salePrice || product.price, {
170
- currency,
171
- }) as string
172
- }
173
- </p>
174
- </div>
175
- </button>
176
- ))}
177
- </div>
178
- )}
179
-
180
- {suggestions.categories.length > 0 && (
181
- <div>
182
- <div className="text-muted-foreground bg-muted px-3 py-1.5 text-xs font-medium">
183
- {t('categories')}
184
- </div>
185
- {suggestions.categories.map((cat) => (
186
- <button
187
- key={cat.id}
188
- type="button"
189
- onClick={() => {
190
- router.push(`/products?category=${cat.id}`);
191
- setShowSuggestions(false);
192
- setSearchQuery('');
193
- }}
194
- className="hover:bg-muted flex w-full items-center justify-between px-3 py-2 text-start transition-colors"
195
- >
196
- <span className="text-foreground text-sm">{cat.name}</span>
197
- <span className="text-muted-foreground text-xs">
198
- {cat.productCount} {tc('products')}
199
- </span>
200
- </button>
201
- ))}
202
- </div>
203
- )}
204
-
205
- {suggestions.products.length === 0 && suggestions.categories.length === 0 && (
206
- <div className="text-muted-foreground px-3 py-4 text-center text-sm">
207
- {tc('noResults')}
208
- </div>
209
- )}
210
- </div>
211
- )}
212
- </div>
213
-
214
- {/* Right side actions */}
215
- <div className="flex items-center gap-3">
216
- {/* Auth */}
217
- {isLoggedIn ? (
218
- <button
219
- onClick={logout}
220
- className="text-muted-foreground hover:text-foreground hidden text-sm transition-colors sm:inline-flex"
221
- >
222
- {t('logout')}
223
- </button>
224
- ) : (
225
- <Link
226
- href="/login"
227
- className="text-muted-foreground hover:text-foreground hidden text-sm transition-colors sm:inline-flex"
228
- >
229
- {t('login')}
230
- </Link>
231
- )}
232
-
233
- {/* Cart */}
234
- <Link
235
- href="/cart"
236
- className="text-foreground hover:text-primary relative p-2 transition-colors"
237
- aria-label={`${itemCount} ${itemCount === 1 ? tc('item') : tc('items')}`}
238
- >
239
- <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
240
- <path
241
- strokeLinecap="round"
242
- strokeLinejoin="round"
243
- strokeWidth={2}
244
- d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
245
- />
246
- </svg>
247
- {itemCount > 0 && (
248
- <span className="bg-primary text-primary-foreground absolute -end-0.5 -top-0.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full text-[10px] font-bold">
249
- {itemCount > 99 ? '99+' : itemCount}
250
- </span>
251
- )}
252
- </Link>
253
-
254
- {/* Mobile menu button */}
255
- <button
256
- type="button"
257
- onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
258
- className="text-foreground p-2 md:hidden"
259
- aria-label={t('menu')}
260
- >
261
- <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
262
- {mobileMenuOpen ? (
263
- <path
264
- strokeLinecap="round"
265
- strokeLinejoin="round"
266
- strokeWidth={2}
267
- d="M6 18L18 6M6 6l12 12"
268
- />
269
- ) : (
270
- <path
271
- strokeLinecap="round"
272
- strokeLinejoin="round"
273
- strokeWidth={2}
274
- d="M4 6h16M4 12h16M4 18h16"
275
- />
276
- )}
277
- </svg>
278
- </button>
279
- </div>
280
- </div>
281
-
282
- {/* Mobile Menu */}
283
- {mobileMenuOpen && (
284
- <div className="border-border space-y-2 border-t py-3 md:hidden">
285
- <Link
286
- href="/products"
287
- onClick={() => setMobileMenuOpen(false)}
288
- className="text-foreground hover:bg-muted block rounded px-2 py-2 text-sm"
289
- >
290
- {t('products')}
291
- </Link>
292
- {isLoggedIn && (
293
- <Link
294
- href="/account"
295
- onClick={() => setMobileMenuOpen(false)}
296
- className="text-foreground hover:bg-muted block rounded px-2 py-2 text-sm"
297
- >
298
- {t('account')}
299
- </Link>
300
- )}
301
- {isLoggedIn ? (
302
- <button
303
- onClick={() => {
304
- logout();
305
- setMobileMenuOpen(false);
306
- }}
307
- className="text-foreground hover:bg-muted block w-full rounded px-2 py-2 text-start text-sm"
308
- >
309
- {t('logout')}
310
- </button>
311
- ) : (
312
- <Link
313
- href="/login"
314
- onClick={() => setMobileMenuOpen(false)}
315
- className="text-foreground hover:bg-muted block rounded px-2 py-2 text-sm"
316
- >
317
- {t('login')}
318
- </Link>
319
- )}
320
-
321
- {/* Mobile search */}
322
- <form onSubmit={handleSearchSubmit} className="px-2 pt-2">
323
- <input
324
- type="text"
325
- value={searchQuery}
326
- onChange={(e) => setSearchQuery(e.target.value)}
327
- placeholder={t('searchPlaceholder')}
328
- className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-9 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
329
- />
330
- </form>
331
- </div>
332
- )}
333
- </div>
334
- </header>
335
- );
336
- }
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import Link from 'next/link';
5
+ import Image from 'next/image';
6
+ import { useRouter } from 'next/navigation';
7
+ import type { SearchSuggestions, ProductSuggestion } from 'brainerce';
8
+ import { formatPrice } from 'brainerce';
9
+ import { getClient } from '@/lib/brainerce';
10
+ import { useTranslations } from '@/lib/translations';
11
+ import { useStoreInfo, useAuth, useCart } from '@/providers/store-provider';
12
+ export function Header() {
13
+ const t = useTranslations('nav');
14
+ const tc = useTranslations('common');
15
+ const { storeInfo } = useStoreInfo();
16
+ const { isLoggedIn, logout } = useAuth();
17
+ const { itemCount } = useCart();
18
+ const router = useRouter();
19
+
20
+ const [searchQuery, setSearchQuery] = useState('');
21
+ const [suggestions, setSuggestions] = useState<SearchSuggestions | null>(null);
22
+ const [showSuggestions, setShowSuggestions] = useState(false);
23
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
24
+ const searchRef = useRef<HTMLDivElement>(null);
25
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
26
+
27
+ const currency = storeInfo?.currency || 'USD';
28
+
29
+ // Debounced search suggestions
30
+ const fetchSuggestions = useCallback((query: string) => {
31
+ if (debounceRef.current) clearTimeout(debounceRef.current);
32
+
33
+ if (query.length < 2) {
34
+ setSuggestions(null);
35
+ setShowSuggestions(false);
36
+ return;
37
+ }
38
+
39
+ debounceRef.current = setTimeout(async () => {
40
+ try {
41
+ const client = getClient();
42
+ const result = await client.getSearchSuggestions(query, 5);
43
+ setSuggestions(result);
44
+ setShowSuggestions(true);
45
+ } catch {
46
+ setSuggestions(null);
47
+ }
48
+ }, 300);
49
+ }, []);
50
+
51
+ // Close suggestions on click outside
52
+ useEffect(() => {
53
+ function handleClickOutside(e: MouseEvent) {
54
+ if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
55
+ setShowSuggestions(false);
56
+ }
57
+ }
58
+ document.addEventListener('mousedown', handleClickOutside);
59
+ return () => document.removeEventListener('mousedown', handleClickOutside);
60
+ }, []);
61
+
62
+ function handleSearchSubmit(e: React.FormEvent) {
63
+ e.preventDefault();
64
+ if (searchQuery.trim()) {
65
+ router.push(`/products?search=${encodeURIComponent(searchQuery.trim())}`);
66
+ setShowSuggestions(false);
67
+ setSearchQuery('');
68
+ }
69
+ }
70
+
71
+ function handleSuggestionClick(suggestion: ProductSuggestion) {
72
+ const href = suggestion.slug ? `/products/${suggestion.slug}` : `/products/${suggestion.id}`;
73
+ router.push(href);
74
+ setShowSuggestions(false);
75
+ setSearchQuery('');
76
+ }
77
+
78
+ return (
79
+ <header className="bg-background border-border sticky top-0 z-50 border-b">
80
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
81
+ <div className="flex h-16 items-center justify-between gap-4">
82
+ {/* Logo / Store Name */}
83
+ <Link href="/" className="text-foreground flex-shrink-0 text-xl font-bold">
84
+ {storeInfo?.name || process.env.NEXT_PUBLIC_STORE_NAME || tc('store')}
85
+ </Link>
86
+
87
+ {/* Desktop Navigation */}
88
+ <nav className="hidden items-center gap-6 md:flex">
89
+ <Link
90
+ href="/products"
91
+ className="text-muted-foreground hover:text-foreground text-sm transition-colors"
92
+ >
93
+ {t('products')}
94
+ </Link>
95
+ {isLoggedIn && (
96
+ <Link
97
+ href="/account"
98
+ className="text-muted-foreground hover:text-foreground text-sm transition-colors"
99
+ >
100
+ {t('account')}
101
+ </Link>
102
+ )}
103
+ </nav>
104
+
105
+ {/* Search */}
106
+ <div ref={searchRef} className="relative hidden max-w-md flex-1 sm:block">
107
+ <form onSubmit={handleSearchSubmit}>
108
+ <input
109
+ type="text"
110
+ value={searchQuery}
111
+ onChange={(e) => {
112
+ setSearchQuery(e.target.value);
113
+ fetchSuggestions(e.target.value);
114
+ }}
115
+ onFocus={() => {
116
+ if (suggestions && searchQuery.length >= 2) {
117
+ setShowSuggestions(true);
118
+ }
119
+ }}
120
+ placeholder={t('searchPlaceholder')}
121
+ className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-9 w-full rounded border px-3 pe-9 text-sm focus:outline-none focus:ring-2"
122
+ />
123
+ <button
124
+ type="submit"
125
+ className="text-muted-foreground hover:text-foreground absolute end-0 top-0 flex h-9 w-9 items-center justify-center"
126
+ aria-label={t('search')}
127
+ >
128
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
129
+ <path
130
+ strokeLinecap="round"
131
+ strokeLinejoin="round"
132
+ strokeWidth={2}
133
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
134
+ />
135
+ </svg>
136
+ </button>
137
+ </form>
138
+
139
+ {/* Search Suggestions Dropdown */}
140
+ {showSuggestions && suggestions && (
141
+ <div className="bg-background border-border absolute top-full z-50 mt-1 w-full overflow-hidden rounded-lg border shadow-lg">
142
+ {suggestions.products.length > 0 && (
143
+ <div>
144
+ <div className="text-muted-foreground bg-muted px-3 py-1.5 text-xs font-medium">
145
+ {t('products')}
146
+ </div>
147
+ {suggestions.products.map((product) => (
148
+ <button
149
+ key={product.id}
150
+ type="button"
151
+ onClick={() => handleSuggestionClick(product)}
152
+ className="hover:bg-muted flex w-full items-center gap-3 px-3 py-2 text-start transition-colors"
153
+ >
154
+ {product.image ? (
155
+ <Image
156
+ src={product.image}
157
+ alt={product.name}
158
+ width={32}
159
+ height={32}
160
+ className="flex-shrink-0 rounded object-cover"
161
+ />
162
+ ) : (
163
+ <div className="bg-muted h-8 w-8 flex-shrink-0 rounded" />
164
+ )}
165
+ <div className="min-w-0 flex-1">
166
+ <p className="text-foreground truncate text-sm">{product.name}</p>
167
+ <p className="text-muted-foreground text-xs">
168
+ {
169
+ formatPrice(product.salePrice || product.price, {
170
+ currency,
171
+ }) as string
172
+ }
173
+ </p>
174
+ </div>
175
+ </button>
176
+ ))}
177
+ </div>
178
+ )}
179
+
180
+ {suggestions.categories.length > 0 && (
181
+ <div>
182
+ <div className="text-muted-foreground bg-muted px-3 py-1.5 text-xs font-medium">
183
+ {t('categories')}
184
+ </div>
185
+ {suggestions.categories.map((cat) => (
186
+ <button
187
+ key={cat.id}
188
+ type="button"
189
+ onClick={() => {
190
+ router.push(`/products?category=${cat.id}`);
191
+ setShowSuggestions(false);
192
+ setSearchQuery('');
193
+ }}
194
+ className="hover:bg-muted flex w-full items-center justify-between px-3 py-2 text-start transition-colors"
195
+ >
196
+ <span className="text-foreground text-sm">{cat.name}</span>
197
+ <span className="text-muted-foreground text-xs">
198
+ {cat.productCount} {tc('products')}
199
+ </span>
200
+ </button>
201
+ ))}
202
+ </div>
203
+ )}
204
+
205
+ {suggestions.products.length === 0 && suggestions.categories.length === 0 && (
206
+ <div className="text-muted-foreground px-3 py-4 text-center text-sm">
207
+ {tc('noResults')}
208
+ </div>
209
+ )}
210
+ </div>
211
+ )}
212
+ </div>
213
+
214
+ {/* Right side actions */}
215
+ <div className="flex items-center gap-3">
216
+ {/* Auth */}
217
+ {isLoggedIn ? (
218
+ <button
219
+ onClick={logout}
220
+ className="text-muted-foreground hover:text-foreground hidden text-sm transition-colors sm:inline-flex"
221
+ >
222
+ {t('logout')}
223
+ </button>
224
+ ) : (
225
+ <Link
226
+ href="/login"
227
+ className="text-muted-foreground hover:text-foreground hidden text-sm transition-colors sm:inline-flex"
228
+ >
229
+ {t('login')}
230
+ </Link>
231
+ )}
232
+
233
+ {/* Cart */}
234
+ <Link
235
+ href="/cart"
236
+ className="text-foreground hover:text-primary relative p-2 transition-colors"
237
+ aria-label={`${itemCount} ${itemCount === 1 ? tc('item') : tc('items')}`}
238
+ >
239
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
240
+ <path
241
+ strokeLinecap="round"
242
+ strokeLinejoin="round"
243
+ strokeWidth={2}
244
+ d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
245
+ />
246
+ </svg>
247
+ {itemCount > 0 && (
248
+ <span className="bg-primary text-primary-foreground absolute -end-0.5 -top-0.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full text-[10px] font-bold">
249
+ {itemCount > 99 ? '99+' : itemCount}
250
+ </span>
251
+ )}
252
+ </Link>
253
+
254
+ {/* Mobile menu button */}
255
+ <button
256
+ type="button"
257
+ onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
258
+ className="text-foreground p-2 md:hidden"
259
+ aria-label={t('menu')}
260
+ >
261
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
262
+ {mobileMenuOpen ? (
263
+ <path
264
+ strokeLinecap="round"
265
+ strokeLinejoin="round"
266
+ strokeWidth={2}
267
+ d="M6 18L18 6M6 6l12 12"
268
+ />
269
+ ) : (
270
+ <path
271
+ strokeLinecap="round"
272
+ strokeLinejoin="round"
273
+ strokeWidth={2}
274
+ d="M4 6h16M4 12h16M4 18h16"
275
+ />
276
+ )}
277
+ </svg>
278
+ </button>
279
+ </div>
280
+ </div>
281
+
282
+ {/* Mobile Menu */}
283
+ {mobileMenuOpen && (
284
+ <div className="border-border space-y-2 border-t py-3 md:hidden">
285
+ <Link
286
+ href="/products"
287
+ onClick={() => setMobileMenuOpen(false)}
288
+ className="text-foreground hover:bg-muted block rounded px-2 py-2 text-sm"
289
+ >
290
+ {t('products')}
291
+ </Link>
292
+ {isLoggedIn && (
293
+ <Link
294
+ href="/account"
295
+ onClick={() => setMobileMenuOpen(false)}
296
+ className="text-foreground hover:bg-muted block rounded px-2 py-2 text-sm"
297
+ >
298
+ {t('account')}
299
+ </Link>
300
+ )}
301
+ {isLoggedIn ? (
302
+ <button
303
+ onClick={() => {
304
+ logout();
305
+ setMobileMenuOpen(false);
306
+ }}
307
+ className="text-foreground hover:bg-muted block w-full rounded px-2 py-2 text-start text-sm"
308
+ >
309
+ {t('logout')}
310
+ </button>
311
+ ) : (
312
+ <Link
313
+ href="/login"
314
+ onClick={() => setMobileMenuOpen(false)}
315
+ className="text-foreground hover:bg-muted block rounded px-2 py-2 text-sm"
316
+ >
317
+ {t('login')}
318
+ </Link>
319
+ )}
320
+
321
+ {/* Mobile search */}
322
+ <form onSubmit={handleSearchSubmit} className="px-2 pt-2">
323
+ <input
324
+ type="text"
325
+ value={searchQuery}
326
+ onChange={(e) => setSearchQuery(e.target.value)}
327
+ placeholder={t('searchPlaceholder')}
328
+ className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-9 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
329
+ />
330
+ </form>
331
+ </div>
332
+ )}
333
+ </div>
334
+ </header>
335
+ );
336
+ }