|
1
|
import { useState, useEffect, useRef } from "react"; |
|
2
|
import { useLocation } from "wouter"; |
|
3
|
import { useQuery } from "@tanstack/react-query"; |
|
4
|
import { Input } from "@/components/ui/input"; |
|
5
|
import { Button } from "@/components/ui/button"; |
|
6
|
import { MobileMenu } from "@/components/layout/mobile-menu"; |
|
7
|
import ThemeCustomizerModal from "@/components/ui/theme-customizer-modal"; |
|
8
|
import { |
|
9
|
DropdownMenu, |
|
10
|
DropdownMenuContent, |
|
11
|
DropdownMenuItem, |
|
12
|
DropdownMenuSeparator, |
|
13
|
DropdownMenuTrigger, |
|
14
|
DropdownMenuLabel, |
|
15
|
} from "@/components/ui/dropdown-menu"; |
|
16
|
import { useAuth } from "@/hooks/useAuth"; |
|
17
|
import { useTheme, ACCENT_COLORS } from "@/contexts/theme-context"; |
|
18
|
import { |
|
19
|
Search, |
|
20
|
Wrench, |
|
21
|
Sparkles, |
|
22
|
Monitor, |
|
23
|
Download, |
|
24
|
CloudOff, |
|
25
|
Laptop, |
|
26
|
Cpu, |
|
27
|
Code, |
|
28
|
Eraser, |
|
29
|
MoveUp, |
|
30
|
BookOpen, |
|
31
|
Sun, |
|
32
|
Moon, |
|
33
|
X, |
|
34
|
Palette, |
|
35
|
Binary, |
|
36
|
PlusCircle, |
|
37
|
Bookmark, |
|
38
|
ShieldCheck, |
|
39
|
User, |
|
40
|
LogOut, |
|
41
|
LogIn, |
|
42
|
Sliders, |
|
43
|
ChevronDown, |
|
44
|
Layers, |
|
45
|
Flame, |
|
46
|
Radio |
|
47
|
} from "lucide-react"; |
|
48
|
import type { RepairGuide, DeviceModel } from "@shared/schema"; |
|
49
|
import { cn } from "@/lib/utils"; |
|
50
|
|
|
51
|
export default function Header() { |
|
52
|
const [location, setLocation] = useLocation(); |
|
53
|
const [searchQuery, setSearchQuery] = useState(""); |
|
54
|
const [showResults, setShowResults] = useState(false); |
|
55
|
const [isFocused, setIsFocused] = useState(false); |
|
56
|
const [themeCustomizerOpen, setThemeCustomizerOpen] = useState(false); |
|
57
|
const [selectedFilter, setSelectedFilter] = useState<"all" | "guides" | "models" | "software">("all"); |
|
58
|
|
|
59
|
const { user, isAuthenticated, logoutMutation } = useAuth() as any; |
|
60
|
const { theme, setTheme, accent } = useTheme(); |
|
61
|
const searchContainerRef = useRef<HTMLDivElement>(null); |
|
62
|
const searchInputRef = useRef<HTMLInputElement>(null); |
|
63
|
|
|
64
|
// Search guides |
|
65
|
const { data: searchGuides = [] } = useQuery<RepairGuide[]>({ |
|
66
|
queryKey: ["/api/repair-guides/search", searchQuery], |
|
67
|
queryFn: () => fetch(`/api/repair-guides/search?q=${encodeURIComponent(searchQuery)}`).then(res => res.ok ? res.json() : []), |
|
68
|
enabled: searchQuery.trim().length > 1, |
|
69
|
}); |
|
70
|
|
|
71
|
// Search models |
|
72
|
const { data: searchModels = [] } = useQuery<DeviceModel[]>({ |
|
73
|
queryKey: ["/api/device-models/search", searchQuery], |
|
74
|
queryFn: () => fetch(`/api/device-models?search=${encodeURIComponent(searchQuery)}`).then(res => res.ok ? res.json() : []), |
|
75
|
enabled: searchQuery.trim().length > 1, |
|
76
|
}); |
|
77
|
|
|
78
|
// Global keyboard shortcut (Cmd+K or / to search) |
|
79
|
useEffect(() => { |
|
80
|
const handleKeyDown = (e: KeyboardEvent) => { |
|
81
|
if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || (e.key === "/" && document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA")) { |
|
82
|
e.preventDefault(); |
|
83
|
searchInputRef.current?.focus(); |
|
84
|
setShowResults(true); |
|
85
|
} |
|
86
|
if (e.key === "Escape") { |
|
87
|
setShowResults(false); |
|
88
|
searchInputRef.current?.blur(); |
|
89
|
} |
|
90
|
}; |
|
91
|
window.addEventListener("keydown", handleKeyDown); |
|
92
|
return () => window.removeEventListener("keydown", handleKeyDown); |
|
93
|
}, []); |
|
94
|
|
|
95
|
// Handle click outside to close results |
|
96
|
useEffect(() => { |
|
97
|
const handleClickOutside = (event: MouseEvent) => { |
|
98
|
if (searchContainerRef.current && !searchContainerRef.current.contains(event.target as Node)) { |
|
99
|
setShowResults(false); |
|
100
|
setIsFocused(false); |
|
101
|
} |
|
102
|
}; |
|
103
|
document.addEventListener("mousedown", handleClickOutside); |
|
104
|
return () => document.removeEventListener("mousedown", handleClickOutside); |
|
105
|
}, []); |
|
106
|
|
|
107
|
const handleSearchSubmit = (e: React.FormEvent) => { |
|
108
|
e.preventDefault(); |
|
109
|
if (searchQuery.trim()) { |
|
110
|
setShowResults(false); |
|
111
|
setLocation(`/?search=${encodeURIComponent(searchQuery.trim())}`); |
|
112
|
} |
|
113
|
}; |
|
114
|
|
|
115
|
const getCategoryIcon = (category: string) => { |
|
116
|
switch (category) { |
|
117
|
case "hardware": return Cpu; |
|
118
|
case "software": return Code; |
|
119
|
case "cleaning": return Eraser; |
|
120
|
case "upgrades": return MoveUp; |
|
121
|
default: return Wrench; |
|
122
|
} |
|
123
|
}; |
|
124
|
|
|
125
|
const currentAccentData = ACCENT_COLORS[accent] || ACCENT_COLORS.indigo; |
|
126
|
|
|
127
|
// Filtered lists based on category pill |
|
128
|
const filteredModels = selectedFilter === "all" || selectedFilter === "models" ? searchModels : []; |
|
129
|
const filteredGuides = selectedFilter === "all" || selectedFilter === "guides" ? searchGuides : []; |
|
130
|
|
|
131
|
const navLinks = [ |
|
132
|
{ label: "Guides", path: "/", icon: BookOpen, matchExact: true }, |
|
133
|
{ label: "Device Finder", path: "/device-finder", icon: Laptop }, |
|
134
|
{ |
|
135
|
label: "AI Diagnostics", |
|
136
|
path: "/troubleshooting", |
|
137
|
icon: Sparkles, |
|
138
|
highlight: true |
|
139
|
}, |
|
140
|
{ label: "Software Hub", path: "/software-tools", icon: Binary }, |
|
141
|
{ label: "Downloads", path: "/downloads", icon: Download }, |
|
142
|
]; |
|
143
|
|
|
144
|
return ( |
|
145
|
<header className="sticky top-0 z-50 w-full border-b bg-background/90 backdrop-blur-xl border-border/60 transition-colors"> |
|
146
|
{/* Top micro-bar for workbench status */} |
|
147
|
<div className="hidden lg:flex items-center justify-between px-6 py-1 text-[11px] font-medium border-b border-border/30 bg-muted/20 text-muted-foreground"> |
|
148
|
<div className="flex items-center gap-3"> |
|
149
|
<div className="flex items-center gap-1.5 font-mono"> |
|
150
|
<span className="relative flex h-2 w-2"> |
|
151
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span> |
|
152
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span> |
|
153
|
</span> |
|
154
|
<span className="font-semibold text-foreground/80">JCR WORKBENCH PRO</span> |
|
155
|
<span className="text-muted-foreground/60">• v2.5 Online</span> |
|
156
|
</div> |
|
157
|
<span className="text-border">|</span> |
|
158
|
<span className="flex items-center gap-1"> |
|
159
|
<Radio className="h-3 w-3 text-primary animate-pulse" /> 624 Comprehensive Repair Guides & Manuals Active |
|
160
|
</span> |
|
161
|
</div> |
|
162
|
|
|
163
|
<div className="flex items-center gap-4"> |
|
164
|
<button |
|
165
|
onClick={() => setLocation("/offline-settings")} |
|
166
|
className="hover:text-foreground transition-colors flex items-center gap-1" |
|
167
|
> |
|
168
|
<CloudOff className="h-3 w-3" /> Offline Engine Ready |
|
169
|
</button> |
|
170
|
<span className="text-border">|</span> |
|
171
|
<button |
|
172
|
onClick={() => setThemeCustomizerOpen(true)} |
|
173
|
className="hover:text-foreground transition-colors flex items-center gap-1" |
|
174
|
> |
|
175
|
<span |
|
176
|
className="inline-block w-2.5 h-2.5 rounded-full border border-border shadow-xs" |
|
177
|
style={{ backgroundColor: currentAccentData.bgHex }} |
|
178
|
/> |
|
179
|
<span>Accent: {currentAccentData.label.split(" ")[0]}</span> |
|
180
|
</button> |
|
181
|
</div> |
|
182
|
</div> |
|
183
|
|
|
184
|
{/* Main Header Container */} |
|
185
|
<div className="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8"> |
|
186
|
<div className="flex items-center justify-between h-16 gap-3 lg:gap-6"> |
|
187
|
|
|
188
|
{/* Left Brand Area */} |
|
189
|
<div className="flex items-center gap-2 shrink-0"> |
|
190
|
<MobileMenu /> |
|
191
|
|
|
192
|
<button |
|
193
|
id="header-brand-logo" |
|
194
|
onClick={() => setLocation("/")} |
|
195
|
className="flex items-center gap-3 group text-left transition-transform active:scale-95" |
|
196
|
> |
|
197
|
<div className="relative flex items-center justify-center h-10 w-10 rounded-xl bg-gradient-to-br from-primary via-primary/90 to-primary/70 text-primary-foreground shadow-md shadow-primary/20 group-hover:shadow-lg group-hover:shadow-primary/30 transition-all duration-300"> |
|
198
|
<Wrench className="h-5 w-5 transition-transform duration-300 group-hover:rotate-12" /> |
|
199
|
<div className="absolute -top-1 -right-1 flex h-3 w-3"> |
|
200
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-50"></span> |
|
201
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-primary border-2 border-background"></span> |
|
202
|
</div> |
|
203
|
</div> |
|
204
|
|
|
205
|
<div className="flex flex-col"> |
|
206
|
<div className="flex items-center gap-1.5"> |
|
207
|
<span className="text-lg font-black tracking-tight text-foreground font-sans"> |
|
208
|
JCR<span className="text-primary font-bold">GUIDE</span> |
|
209
|
</span> |
|
210
|
<span className="text-[10px] font-extrabold uppercase px-1.5 py-0.5 rounded bg-primary/10 text-primary border border-primary/20 tracking-wider"> |
|
211
|
PRO |
|
212
|
</span> |
|
213
|
</div> |
|
214
|
<span className="text-[10px] text-muted-foreground font-medium hidden sm:block"> |
|
215
|
Hardware & Diagnostics Master |
|
216
|
</span> |
|
217
|
</div> |
|
218
|
</button> |
|
219
|
</div> |
|
220
|
|
|
221
|
{/* Center: Spotlight Search Launcher */} |
|
222
|
<div ref={searchContainerRef} className="flex-1 max-w-xl relative"> |
|
223
|
<form onSubmit={handleSearchSubmit} className="relative"> |
|
224
|
<div className="relative flex items-center"> |
|
225
|
<Search className={cn( |
|
226
|
"absolute left-3.5 h-4 w-4 transition-colors duration-200 pointer-events-none", |
|
227
|
isFocused ? "text-primary" : "text-muted-foreground" |
|
228
|
)} /> |
|
229
|
|
|
230
|
<Input |
|
231
|
ref={searchInputRef} |
|
232
|
type="text" |
|
233
|
value={searchQuery} |
|
234
|
onChange={(e) => { |
|
235
|
setSearchQuery(e.target.value); |
|
236
|
setShowResults(true); |
|
237
|
}} |
|
238
|
onFocus={() => { |
|
239
|
setIsFocused(true); |
|
240
|
setShowResults(true); |
|
241
|
}} |
|
242
|
className="w-full pl-10 pr-20 h-10 bg-muted/40 hover:bg-muted/70 focus:bg-background border-border/60 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/20 rounded-xl transition-all text-xs sm:text-sm placeholder:text-muted-foreground/70" |
|
243
|
placeholder="Search guides, laptops, parts, error codes..." |
|
244
|
/> |
|
245
|
|
|
246
|
<div className="absolute right-2.5 flex items-center gap-1"> |
|
247
|
{searchQuery ? ( |
|
248
|
<button |
|
249
|
type="button" |
|
250
|
onClick={() => { |
|
251
|
setSearchQuery(""); |
|
252
|
searchInputRef.current?.focus(); |
|
253
|
}} |
|
254
|
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" |
|
255
|
title="Clear search" |
|
256
|
> |
|
257
|
<X className="h-3.5 w-3.5" /> |
|
258
|
</button> |
|
259
|
) : ( |
|
260
|
<kbd className="hidden sm:inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-mono text-muted-foreground bg-background border border-border/80 rounded-md shadow-2xs"> |
|
261
|
⌘K |
|
262
|
</kbd> |
|
263
|
)} |
|
264
|
</div> |
|
265
|
</div> |
|
266
|
</form> |
|
267
|
|
|
268
|
{/* Rich Autocomplete & Spotlight Overlay */} |
|
269
|
{showResults && (isFocused || searchQuery.trim().length > 0) && ( |
|
270
|
<div className="absolute top-full left-0 right-0 mt-2 bg-popover/95 backdrop-blur-2xl border border-border/80 rounded-2xl shadow-2xl overflow-hidden z-50 animate-in fade-in-50 zoom-in-95 duration-150"> |
|
271
|
|
|
272
|
{/* Search Header Filters */} |
|
273
|
<div className="p-2.5 bg-muted/30 border-b border-border/40 flex items-center justify-between gap-1 overflow-x-auto text-xs"> |
|
274
|
<span className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider px-2">Filter:</span> |
|
275
|
<div className="flex items-center gap-1"> |
|
276
|
{(["all", "guides", "models", "software"] as const).map((filter) => ( |
|
277
|
<button |
|
278
|
key={filter} |
|
279
|
type="button" |
|
280
|
onClick={() => setSelectedFilter(filter)} |
|
281
|
className={cn( |
|
282
|
"px-2.5 py-1 rounded-lg text-xs font-medium capitalize transition-colors", |
|
283
|
selectedFilter === filter |
|
284
|
? "bg-primary text-primary-foreground shadow-xs" |
|
285
|
: "text-muted-foreground hover:text-foreground hover:bg-muted" |
|
286
|
)} |
|
287
|
> |
|
288
|
{filter} |
|
289
|
</button> |
|
290
|
))} |
|
291
|
</div> |
|
292
|
</div> |
|
293
|
|
|
294
|
<div className="max-h-[65vh] overflow-y-auto p-2 space-y-3"> |
|
295
|
|
|
296
|
{/* Empty state / Quick Jump when query is empty */} |
|
297
|
{searchQuery.trim().length <= 1 && ( |
|
298
|
<div className="p-3 space-y-3"> |
|
299
|
<div className="text-[11px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5"> |
|
300
|
<Flame className="h-3.5 w-3.5 text-amber-500" /> Popular Quick Searches |
|
301
|
</div> |
|
302
|
<div className="grid grid-cols-2 gap-2"> |
|
303
|
{[ |
|
304
|
{ label: "MacBook Pro Screen Replacement", path: "/?search=MacBook%20Pro" }, |
|
305
|
{ label: "Dell XPS 15 Battery Swelling", path: "/?search=Dell%20XPS" }, |
|
306
|
{ label: "Thermal Paste Re-application", path: "/?search=Thermal" }, |
|
307
|
{ label: "Blue Screen (BSOD) Flowchart", path: "/troubleshooting/software" }, |
|
308
|
{ label: "Lenovo ThinkPad Keyboard", path: "/?search=ThinkPad" }, |
|
309
|
{ label: "BIOS Firmware Flashing Tool", path: "/software-tools" }, |
|
310
|
].map((item, idx) => ( |
|
311
|
<button |
|
312
|
key={idx} |
|
313
|
onClick={() => { |
|
314
|
setLocation(item.path); |
|
315
|
setShowResults(false); |
|
316
|
}} |
|
317
|
className="p-2 text-left rounded-xl bg-muted/40 hover:bg-primary/10 hover:text-primary transition-all text-xs font-medium truncate" |
|
318
|
> |
|
319
|
→ {item.label} |
|
320
|
</button> |
|
321
|
))} |
|
322
|
</div> |
|
323
|
</div> |
|
324
|
)} |
|
325
|
|
|
326
|
{/* Device Models Results */} |
|
327
|
{filteredModels.length > 0 && ( |
|
328
|
<div> |
|
329
|
<div className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center justify-between"> |
|
330
|
<span className="flex items-center gap-1.5"> |
|
331
|
<Laptop className="h-3 w-3 text-indigo-500" /> Compatible Models |
|
332
|
</span> |
|
333
|
<span>{filteredModels.length} found</span> |
|
334
|
</div> |
|
335
|
<div className="space-y-1 mt-1"> |
|
336
|
{filteredModels.slice(0, 3).map((model) => ( |
|
337
|
<button |
|
338
|
key={model.id} |
|
339
|
onClick={() => { |
|
340
|
setLocation(`/device-finder?model=${model.id}`); |
|
341
|
setShowResults(false); |
|
342
|
}} |
|
343
|
className="w-full p-2.5 flex items-center justify-between hover:bg-primary/10 rounded-xl transition-colors text-left group" |
|
344
|
> |
|
345
|
<div className="flex items-center gap-2.5"> |
|
346
|
<div className="h-7 w-7 rounded-lg bg-indigo-500/10 text-indigo-500 flex items-center justify-center shrink-0"> |
|
347
|
<Monitor className="h-3.5 w-3.5" /> |
|
348
|
</div> |
|
349
|
<div> |
|
350
|
<span className="text-xs font-semibold block text-foreground group-hover:text-primary transition-colors">{model.name}</span> |
|
351
|
<span className="text-[10px] text-muted-foreground">{model.series} {model.year ? `• ${model.year}` : ""}</span> |
|
352
|
</div> |
|
353
|
</div> |
|
354
|
<span className="text-[10px] font-semibold px-2 py-0.5 rounded-md bg-muted text-muted-foreground group-hover:bg-primary group-hover:text-primary-foreground transition-colors"> |
|
355
|
View Specs |
|
356
|
</span> |
|
357
|
</button> |
|
358
|
))} |
|
359
|
</div> |
|
360
|
</div> |
|
361
|
)} |
|
362
|
|
|
363
|
{/* Repair Guides Results */} |
|
364
|
{filteredGuides.length > 0 && ( |
|
365
|
<div> |
|
366
|
<div className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center justify-between"> |
|
367
|
<span className="flex items-center gap-1.5"> |
|
368
|
<Wrench className="h-3 w-3 text-primary" /> Repair Procedures |
|
369
|
</span> |
|
370
|
<span>{filteredGuides.length} found</span> |
|
371
|
</div> |
|
372
|
<div className="space-y-1 mt-1"> |
|
373
|
{filteredGuides.slice(0, 4).map((guide) => { |
|
374
|
const Icon = getCategoryIcon(guide.category); |
|
375
|
return ( |
|
376
|
<button |
|
377
|
key={guide.id} |
|
378
|
onClick={() => { |
|
379
|
setLocation(`/guide/${guide.id}`); |
|
380
|
setShowResults(false); |
|
381
|
}} |
|
382
|
className="w-full p-2.5 flex items-center gap-3 hover:bg-primary/10 rounded-xl transition-colors text-left group" |
|
383
|
> |
|
384
|
<div className="h-8 w-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0 group-hover:bg-primary group-hover:text-primary-foreground transition-colors"> |
|
385
|
<Icon className="h-4 w-4" /> |
|
386
|
</div> |
|
387
|
<div className="flex-1 min-w-0"> |
|
388
|
<span className="text-xs font-semibold block truncate text-foreground group-hover:text-primary transition-colors"> |
|
389
|
{guide.title} |
|
390
|
</span> |
|
391
|
<div className="flex items-center gap-2 text-[10px] text-muted-foreground"> |
|
392
|
<span className="capitalize font-medium text-foreground/70">{guide.category}</span> |
|
393
|
<span>•</span> |
|
394
|
<span>{guide.deviceType}</span> |
|
395
|
<span>•</span> |
|
396
|
<span className={cn( |
|
397
|
"capitalize px-1.5 py-0.2 rounded font-semibold", |
|
398
|
guide.difficulty === "easy" ? "text-emerald-500" : |
|
399
|
guide.difficulty === "medium" ? "text-amber-500" : "text-rose-500" |
|
400
|
)}> |
|
401
|
{guide.difficulty} |
|
402
|
</span> |
|
403
|
</div> |
|
404
|
</div> |
|
405
|
</button> |
|
406
|
); |
|
407
|
})} |
|
408
|
</div> |
|
409
|
</div> |
|
410
|
)} |
|
411
|
|
|
412
|
{searchQuery.trim().length > 1 && filteredGuides.length === 0 && filteredModels.length === 0 && ( |
|
413
|
<div className="py-8 text-center text-muted-foreground"> |
|
414
|
<Search className="h-8 w-8 mx-auto mb-2 opacity-30" /> |
|
415
|
<p className="text-xs font-medium">No direct matches for "{searchQuery}"</p> |
|
416
|
<p className="text-[11px] opacity-70 mt-1">Try searching for generic terms like "battery", "display", or "RAM"</p> |
|
417
|
</div> |
|
418
|
)} |
|
419
|
</div> |
|
420
|
|
|
421
|
{searchQuery.trim().length > 0 && ( |
|
422
|
<div className="p-2.5 bg-muted/40 border-t border-border/40 text-center flex items-center justify-between px-4"> |
|
423
|
<span className="text-[11px] text-muted-foreground">Press <strong>Enter</strong> to search</span> |
|
424
|
<button |
|
425
|
onClick={handleSearchSubmit} |
|
426
|
className="text-xs text-primary font-semibold hover:underline flex items-center gap-1" |
|
427
|
> |
|
428
|
View all results → |
|
429
|
</button> |
|
430
|
</div> |
|
431
|
)} |
|
432
|
</div> |
|
433
|
)} |
|
434
|
</div> |
|
435
|
|
|
436
|
{/* Right Navigation & Tools Deck */} |
|
437
|
<div className="flex items-center gap-1.5 sm:gap-2 shrink-0"> |
|
438
|
|
|
439
|
{/* Desktop Navigation Links */} |
|
440
|
<nav className="hidden lg:flex items-center gap-1 bg-muted/30 p-1 rounded-xl border border-border/40"> |
|
441
|
{navLinks.map((link) => { |
|
442
|
const isActive = link.matchExact |
|
443
|
? location === link.path |
|
444
|
: location.startsWith(link.path); |
|
445
|
const Icon = link.icon; |
|
446
|
|
|
447
|
return ( |
|
448
|
<Button |
|
449
|
key={link.path} |
|
450
|
variant="ghost" |
|
451
|
size="sm" |
|
452
|
onClick={() => setLocation(link.path)} |
|
453
|
className={cn( |
|
454
|
"rounded-lg h-8 px-3 text-xs font-semibold gap-1.5 transition-all", |
|
455
|
isActive |
|
456
|
? "bg-background text-foreground shadow-xs font-bold border border-border/50" |
|
457
|
: "text-muted-foreground hover:text-foreground hover:bg-muted/60", |
|
458
|
link.highlight && !isActive && "text-primary hover:text-primary" |
|
459
|
)} |
|
460
|
> |
|
461
|
<Icon className={cn("h-3.5 w-3.5", link.highlight && "text-primary")} /> |
|
462
|
<span>{link.label}</span> |
|
463
|
{link.highlight && ( |
|
464
|
<span className="inline-flex items-center px-1 rounded text-[9px] font-extrabold bg-primary/20 text-primary"> |
|
465
|
AI |
|
466
|
</span> |
|
467
|
)} |
|
468
|
</Button> |
|
469
|
); |
|
470
|
})} |
|
471
|
</nav> |
|
472
|
|
|
473
|
{/* Quick Actions Dropdown (Technician Toolbox) */} |
|
474
|
<DropdownMenu> |
|
475
|
<DropdownMenuTrigger asChild> |
|
476
|
<Button |
|
477
|
variant="outline" |
|
478
|
size="sm" |
|
479
|
className="rounded-xl h-9 px-2.5 text-xs font-semibold gap-1.5 border-border/70 hover:border-primary/50 text-foreground" |
|
480
|
title="Technician Toolbox" |
|
481
|
> |
|
482
|
<Sliders className="h-3.5 w-3.5 text-primary" /> |
|
483
|
<span className="hidden md:inline">Toolbox</span> |
|
484
|
<ChevronDown className="h-3 w-3 opacity-60" /> |
|
485
|
</Button> |
|
486
|
</DropdownMenuTrigger> |
|
487
|
<DropdownMenuContent align="end" className="w-60 rounded-2xl p-2 shadow-2xl border-border/70 bg-popover/95 backdrop-blur-xl"> |
|
488
|
<DropdownMenuLabel className="px-3 py-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground"> |
|
489
|
Technician Quick Actions |
|
490
|
</DropdownMenuLabel> |
|
491
|
|
|
492
|
<DropdownMenuItem onClick={() => setLocation("/create-guide")} className="rounded-xl gap-2.5 p-2 text-xs cursor-pointer"> |
|
493
|
<PlusCircle className="h-4 w-4 text-emerald-500" /> |
|
494
|
<div> |
|
495
|
<span className="font-semibold block">Create New Guide</span> |
|
496
|
<span className="text-[10px] text-muted-foreground">Document teardown / fix</span> |
|
497
|
</div> |
|
498
|
</DropdownMenuItem> |
|
499
|
|
|
500
|
<DropdownMenuItem onClick={() => setLocation("/favorites")} className="rounded-xl gap-2.5 p-2 text-xs cursor-pointer"> |
|
501
|
<Bookmark className="h-4 w-4 text-amber-500" /> |
|
502
|
<div> |
|
503
|
<span className="font-semibold block">Saved & Bookmarks</span> |
|
504
|
<span className="text-[10px] text-muted-foreground">Quick-access field guides</span> |
|
505
|
</div> |
|
506
|
</DropdownMenuItem> |
|
507
|
|
|
508
|
<DropdownMenuItem onClick={() => setLocation("/downloads")} className="rounded-xl gap-2.5 p-2 text-xs cursor-pointer"> |
|
509
|
<Download className="h-4 w-4 text-primary" /> |
|
510
|
<div> |
|
511
|
<span className="font-semibold block">Offline Manuals</span> |
|
512
|
<span className="text-[10px] text-muted-foreground">PDFs, ROMs & Drivers</span> |
|
513
|
</div> |
|
514
|
</DropdownMenuItem> |
|
515
|
|
|
516
|
<DropdownMenuSeparator className="my-1 opacity-50" /> |
|
517
|
|
|
518
|
<DropdownMenuLabel className="px-3 py-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground"> |
|
519
|
Management & Utilities |
|
520
|
</DropdownMenuLabel> |
|
521
|
|
|
522
|
<DropdownMenuItem onClick={() => setLocation("/admin")} className="rounded-xl gap-2.5 p-2 text-xs cursor-pointer"> |
|
523
|
<ShieldCheck className="h-4 w-4 text-indigo-500" /> |
|
524
|
<div> |
|
525
|
<span className="font-semibold block">Admin Dashboard</span> |
|
526
|
<span className="text-[10px] text-muted-foreground">Manage guides & inventory</span> |
|
527
|
</div> |
|
528
|
</DropdownMenuItem> |
|
529
|
|
|
530
|
<DropdownMenuItem onClick={() => setLocation("/offline-settings")} className="rounded-xl gap-2.5 p-2 text-xs cursor-pointer"> |
|
531
|
<CloudOff className="h-4 w-4 text-rose-500" /> |
|
532
|
<div> |
|
533
|
<span className="font-semibold block">Offline Sync Engine</span> |
|
534
|
<span className="text-[10px] text-muted-foreground">Cache data for field work</span> |
|
535
|
</div> |
|
536
|
</DropdownMenuItem> |
|
537
|
|
|
538
|
<DropdownMenuSeparator className="my-1 opacity-50" /> |
|
539
|
|
|
540
|
<DropdownMenuItem onClick={() => setThemeCustomizerOpen(true)} className="rounded-xl gap-2.5 p-2 text-xs cursor-pointer font-semibold text-primary"> |
|
541
|
<Palette className="h-4 w-4 text-primary" /> |
|
542
|
<span>Workbench Color Studio</span> |
|
543
|
</DropdownMenuItem> |
|
544
|
</DropdownMenuContent> |
|
545
|
</DropdownMenu> |
|
546
|
|
|
547
|
{/* Theme Customizer Icon Button */} |
|
548
|
<Button |
|
549
|
variant="ghost" |
|
550
|
size="icon" |
|
551
|
className="rounded-xl h-9 w-9 text-muted-foreground hover:text-foreground hover:bg-muted" |
|
552
|
onClick={() => setThemeCustomizerOpen(true)} |
|
553
|
title="Customize Theme & Accents" |
|
554
|
> |
|
555
|
<Palette className="h-4 w-4 text-primary" /> |
|
556
|
</Button> |
|
557
|
|
|
558
|
{/* Theme Mode Quick Toggle (Light / Dark / AMOLED) */} |
|
559
|
<Button |
|
560
|
variant="ghost" |
|
561
|
size="icon" |
|
562
|
className="rounded-xl h-9 w-9 text-muted-foreground hover:text-foreground hover:bg-muted" |
|
563
|
onClick={() => { |
|
564
|
if (theme === "dark") setTheme("light"); |
|
565
|
else if (theme === "light") setTheme("amoled"); |
|
566
|
else setTheme("dark"); |
|
567
|
}} |
|
568
|
title={`Current Theme: ${theme.toUpperCase()} (Click to toggle)`} |
|
569
|
> |
|
570
|
{theme === "dark" ? ( |
|
571
|
<Moon className="h-4 w-4 text-indigo-400" /> |
|
572
|
) : theme === "amoled" ? ( |
|
573
|
<Layers className="h-4 w-4 text-foreground" /> |
|
574
|
) : ( |
|
575
|
<Sun className="h-4 w-4 text-amber-500" /> |
|
576
|
)} |
|
577
|
</Button> |
|
578
|
|
|
579
|
{/* User Profile / Auth Button */} |
|
580
|
{isAuthenticated ? ( |
|
581
|
<DropdownMenu> |
|
582
|
<DropdownMenuTrigger asChild> |
|
583
|
<Button |
|
584
|
variant="ghost" |
|
585
|
size="sm" |
|
586
|
className="rounded-xl h-9 pl-1.5 pr-2.5 gap-2 text-xs font-semibold hover:bg-muted border border-border/50" |
|
587
|
> |
|
588
|
<div className="h-6 w-6 rounded-lg bg-primary/20 text-primary flex items-center justify-center font-bold text-[11px]"> |
|
589
|
{user?.firstName ? user.firstName[0].toUpperCase() : "U"} |
|
590
|
</div> |
|
591
|
<span className="hidden sm:inline font-medium max-w-[80px] truncate"> |
|
592
|
{user?.firstName || "Account"} |
|
593
|
</span> |
|
594
|
</Button> |
|
595
|
</DropdownMenuTrigger> |
|
596
|
<DropdownMenuContent align="end" className="w-52 rounded-2xl p-2 shadow-2xl border-border/70"> |
|
597
|
<div className="px-3 py-2 border-b border-border/40"> |
|
598
|
<p className="text-xs font-bold truncate">{user?.firstName ? `${user.firstName} ${user.lastName || ""}` : "Technician"}</p> |
|
599
|
<p className="text-[10px] text-muted-foreground truncate">{user?.email || "Signed in"}</p> |
|
600
|
</div> |
|
601
|
<DropdownMenuItem onClick={() => setLocation("/favorites")} className="rounded-xl gap-2 p-2 text-xs cursor-pointer mt-1"> |
|
602
|
<Bookmark className="h-3.5 w-3.5 text-muted-foreground" /> Saved Guides |
|
603
|
</DropdownMenuItem> |
|
604
|
<DropdownMenuItem onClick={() => setLocation("/admin")} className="rounded-xl gap-2 p-2 text-xs cursor-pointer"> |
|
605
|
<ShieldCheck className="h-3.5 w-3.5 text-muted-foreground" /> Dashboard |
|
606
|
</DropdownMenuItem> |
|
607
|
<DropdownMenuSeparator className="my-1" /> |
|
608
|
<DropdownMenuItem |
|
609
|
onClick={() => logoutMutation.mutate()} |
|
610
|
className="rounded-xl gap-2 p-2 text-xs cursor-pointer text-destructive focus:text-destructive" |
|
611
|
> |
|
612
|
<LogOut className="h-3.5 w-3.5" /> Sign Out |
|
613
|
</DropdownMenuItem> |
|
614
|
</DropdownMenuContent> |
|
615
|
</DropdownMenu> |
|
616
|
) : ( |
|
617
|
<Button |
|
618
|
variant="default" |
|
619
|
size="sm" |
|
620
|
onClick={() => setLocation("/auth")} |
|
621
|
className="rounded-xl h-9 px-3 text-xs font-bold gap-1.5 shadow-xs" |
|
622
|
> |
|
623
|
<LogIn className="h-3.5 w-3.5" /> |
|
624
|
<span className="hidden sm:inline">Sign In</span> |
|
625
|
</Button> |
|
626
|
)} |
|
627
|
|
|
628
|
</div> |
|
629
|
</div> |
|
630
|
</div> |
|
631
|
|
|
632
|
{/* Theme Customizer Modal */} |
|
633
|
<ThemeCustomizerModal |
|
634
|
open={themeCustomizerOpen} |
|
635
|
onOpenChange={setThemeCustomizerOpen} |
|
636
|
/> |
|
637
|
</header> |
|
638
|
); |
|
639
|
} |