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 }