|
1
|
import { useState } from "react"; |
|
2
|
import { useLocation } from "wouter"; |
|
3
|
import { Button } from "@/components/ui/button"; |
|
4
|
import { |
|
5
|
Sheet, |
|
6
|
SheetContent, |
|
7
|
SheetHeader, |
|
8
|
SheetTitle, |
|
9
|
SheetTrigger, |
|
10
|
} from "@/components/ui/sheet"; |
|
11
|
import { Separator } from "@/components/ui/separator"; |
|
12
|
import { useAuth } from "@/hooks/useAuth"; |
|
13
|
import type { User } from "@shared/schema"; |
|
14
|
import { |
|
15
|
Menu, |
|
16
|
Wrench, |
|
17
|
BookOpen, |
|
18
|
Bookmark, |
|
19
|
Sparkles, |
|
20
|
Laptop, |
|
21
|
Binary, |
|
22
|
PlusCircle, |
|
23
|
Download, |
|
24
|
CloudOff, |
|
25
|
LogIn, |
|
26
|
LogOut, |
|
27
|
ShieldCheck, |
|
28
|
Palette |
|
29
|
} from "lucide-react"; |
|
30
|
import { cn } from "@/lib/utils"; |
|
31
|
import ThemeCustomizerModal from "@/components/ui/theme-customizer-modal"; |
|
32
|
|
|
33
|
export function MobileMenu() { |
|
34
|
const [open, setOpen] = useState(false); |
|
35
|
const [themeModalOpen, setThemeModalOpen] = useState(false); |
|
36
|
const [location, setLocation] = useLocation(); |
|
37
|
const { user, isAuthenticated, logoutMutation } = useAuth() as any; |
|
38
|
|
|
39
|
const handleNavigation = (path: string) => { |
|
40
|
setLocation(path); |
|
41
|
setOpen(false); |
|
42
|
}; |
|
43
|
|
|
44
|
const navItems = [ |
|
45
|
{ label: "Repair Guides", path: "/", icon: BookOpen }, |
|
46
|
{ label: "Device Finder", path: "/device-finder", icon: Laptop }, |
|
47
|
{ label: "AI Diagnostic Assistant", path: "/troubleshooting", icon: Sparkles, badge: "AI" }, |
|
48
|
{ label: "Software Hub", path: "/software-tools", icon: Binary }, |
|
49
|
{ label: "Saved Guides", path: "/favorites", icon: Bookmark, authRequired: true }, |
|
50
|
]; |
|
51
|
|
|
52
|
const toolItems = [ |
|
53
|
{ label: "Create Repair Guide", path: "/create-guide", icon: PlusCircle }, |
|
54
|
{ label: "Offline Manuals & Downloads", path: "/downloads", icon: Download }, |
|
55
|
{ label: "Offline Sync Engine", path: "/offline-settings", icon: CloudOff }, |
|
56
|
{ label: "Admin Workbench", path: "/admin", icon: ShieldCheck }, |
|
57
|
]; |
|
58
|
|
|
59
|
return ( |
|
60
|
<> |
|
61
|
<Sheet open={open} onOpenChange={setOpen}> |
|
62
|
<SheetTrigger asChild> |
|
63
|
<Button variant="ghost" size="icon" className="lg:hidden rounded-xl h-9 w-9 text-muted-foreground hover:text-foreground"> |
|
64
|
<Menu className="h-5 w-5" /> |
|
65
|
<span className="sr-only">Toggle navigation menu</span> |
|
66
|
</Button> |
|
67
|
</SheetTrigger> |
|
68
|
<SheetContent side="left" className="w-80 p-0 flex flex-col bg-background/95 backdrop-blur-2xl border-r border-border/60"> |
|
69
|
|
|
70
|
{/* Header Brand */} |
|
71
|
<div className="p-5 border-b border-border/40 bg-muted/20"> |
|
72
|
<SheetHeader> |
|
73
|
<SheetTitle className="flex items-center gap-3 text-left"> |
|
74
|
<div className="flex items-center justify-center h-9 w-9 rounded-xl bg-primary text-primary-foreground shadow-sm"> |
|
75
|
<Wrench className="h-4 w-4" /> |
|
76
|
</div> |
|
77
|
<div> |
|
78
|
<div className="flex items-center gap-1.5"> |
|
79
|
<span className="text-base font-extrabold tracking-tight">JCR GUIDE</span> |
|
80
|
<span className="text-[9px] font-extrabold uppercase px-1.5 py-0.5 rounded bg-primary/10 text-primary border border-primary/20"> |
|
81
|
PRO |
|
82
|
</span> |
|
83
|
</div> |
|
84
|
<p className="text-[10px] text-muted-foreground font-normal">Technician Field Guide</p> |
|
85
|
</div> |
|
86
|
</SheetTitle> |
|
87
|
</SheetHeader> |
|
88
|
</div> |
|
89
|
|
|
90
|
{/* Navigation Links Scrollable */} |
|
91
|
<div className="flex-1 overflow-y-auto p-4 space-y-6"> |
|
92
|
|
|
93
|
{/* Primary Nav */} |
|
94
|
<div className="space-y-1.5"> |
|
95
|
<span className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider px-3"> |
|
96
|
Navigation |
|
97
|
</span> |
|
98
|
<div className="space-y-1"> |
|
99
|
{navItems.map((item) => { |
|
100
|
if (item.authRequired && !isAuthenticated) return null; |
|
101
|
const isActive = location === item.path; |
|
102
|
const Icon = item.icon; |
|
103
|
|
|
104
|
return ( |
|
105
|
<Button |
|
106
|
key={item.path} |
|
107
|
variant="ghost" |
|
108
|
className={cn( |
|
109
|
"w-full justify-between rounded-xl h-10 px-3 text-xs font-semibold transition-colors", |
|
110
|
isActive ? "bg-primary/10 text-primary font-bold" : "text-foreground/80 hover:bg-muted" |
|
111
|
)} |
|
112
|
onClick={() => handleNavigation(item.path)} |
|
113
|
> |
|
114
|
<div className="flex items-center gap-3"> |
|
115
|
<Icon className={cn("h-4 w-4", isActive ? "text-primary" : "text-muted-foreground")} /> |
|
116
|
<span>{item.label}</span> |
|
117
|
</div> |
|
118
|
{item.badge && ( |
|
119
|
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-primary/20 text-primary"> |
|
120
|
{item.badge} |
|
121
|
</span> |
|
122
|
)} |
|
123
|
</Button> |
|
124
|
); |
|
125
|
})} |
|
126
|
</div> |
|
127
|
</div> |
|
128
|
|
|
129
|
<Separator className="opacity-50" /> |
|
130
|
|
|
131
|
{/* Tools & Content */} |
|
132
|
<div className="space-y-1.5"> |
|
133
|
<span className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider px-3"> |
|
134
|
Technician Utilities |
|
135
|
</span> |
|
136
|
<div className="space-y-1"> |
|
137
|
{toolItems.map((item) => { |
|
138
|
const isActive = location === item.path; |
|
139
|
const Icon = item.icon; |
|
140
|
|
|
141
|
return ( |
|
142
|
<Button |
|
143
|
key={item.path} |
|
144
|
variant="ghost" |
|
145
|
className={cn( |
|
146
|
"w-full justify-start rounded-xl h-10 px-3 text-xs font-semibold transition-colors gap-3", |
|
147
|
isActive ? "bg-primary/10 text-primary font-bold" : "text-foreground/80 hover:bg-muted" |
|
148
|
)} |
|
149
|
onClick={() => handleNavigation(item.path)} |
|
150
|
> |
|
151
|
<Icon className={cn("h-4 w-4", isActive ? "text-primary" : "text-muted-foreground")} /> |
|
152
|
<span>{item.label}</span> |
|
153
|
</Button> |
|
154
|
); |
|
155
|
})} |
|
156
|
|
|
157
|
<Button |
|
158
|
variant="ghost" |
|
159
|
className="w-full justify-start rounded-xl h-10 px-3 text-xs font-semibold transition-colors gap-3 text-primary hover:bg-primary/10" |
|
160
|
onClick={() => { |
|
161
|
setOpen(false); |
|
162
|
setThemeModalOpen(true); |
|
163
|
}} |
|
164
|
> |
|
165
|
<Palette className="h-4 w-4 text-primary" /> |
|
166
|
<span>Customize Theme Colors</span> |
|
167
|
</Button> |
|
168
|
</div> |
|
169
|
</div> |
|
170
|
|
|
171
|
</div> |
|
172
|
|
|
173
|
{/* Footer Auth area */} |
|
174
|
<div className="p-4 border-t border-border/40 bg-muted/20"> |
|
175
|
{!isAuthenticated ? ( |
|
176
|
<Button |
|
177
|
onClick={() => handleNavigation("/auth")} |
|
178
|
className="w-full rounded-xl h-10 text-xs font-bold gap-2 shadow-xs" |
|
179
|
> |
|
180
|
<LogIn className="h-4 w-4" /> |
|
181
|
Sign In to Workbench |
|
182
|
</Button> |
|
183
|
) : ( |
|
184
|
<div className="space-y-3"> |
|
185
|
<div className="flex items-center gap-3 p-2 rounded-xl bg-background border border-border/50"> |
|
186
|
<div className="h-8 w-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center font-bold text-xs"> |
|
187
|
{user?.firstName ? user.firstName[0].toUpperCase() : "U"} |
|
188
|
</div> |
|
189
|
<div className="flex-1 min-w-0"> |
|
190
|
<p className="text-xs font-bold truncate"> |
|
191
|
{user?.firstName ? `${user.firstName} ${user.lastName || ""}`.trim() : "Technician"} |
|
192
|
</p> |
|
193
|
<p className="text-[10px] text-muted-foreground truncate">{user?.email}</p> |
|
194
|
</div> |
|
195
|
</div> |
|
196
|
|
|
197
|
<Button |
|
198
|
variant="outline" |
|
199
|
size="sm" |
|
200
|
onClick={() => { |
|
201
|
logoutMutation.mutate(); |
|
202
|
setOpen(false); |
|
203
|
}} |
|
204
|
className="w-full rounded-xl h-9 text-xs font-semibold gap-2 border-border/60 text-destructive hover:bg-destructive/10 hover:text-destructive" |
|
205
|
> |
|
206
|
<LogOut className="h-3.5 w-3.5" /> |
|
207
|
Sign Out |
|
208
|
</Button> |
|
209
|
</div> |
|
210
|
)} |
|
211
|
</div> |
|
212
|
|
|
213
|
</SheetContent> |
|
214
|
</Sheet> |
|
215
|
|
|
216
|
<ThemeCustomizerModal |
|
217
|
open={themeModalOpen} |
|
218
|
onOpenChange={setThemeModalOpen} |
|
219
|
/> |
|
220
|
</> |
|
221
|
); |
|
222
|
} |