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