1 import React, { useState, useEffect } from "react";
2 import { useQuery } from "@tanstack/react-query";
3 import { useLocation } from "wouter";
4 import { Button } from "@/components/ui/button";
5 import { Input } from "@/components/ui/input";
6 import {
7 Search,
8 Laptop,
9 Monitor,
10 Wrench,
11 ArrowRight,
12 Cpu,
13 HardDrive,
14 MemoryStick,
15 Sparkles,
16 CheckCircle2,
17 AlertTriangle,
18 X,
19 Layers,
20 ChevronRight
21 } from "lucide-react";
22 import GuideDetailModal from "@/components/repair/guide-detail-modal";
23 import type { DeviceBrand, DeviceModel, RepairGuide } from "@shared/schema";
24 import { cn } from "@/lib/utils";
25
26 export default function DeviceFinder() {
27 const [, setLocation] = useLocation();
28 const [selectedDeviceType, setSelectedDeviceType] = useState<"laptop" | "desktop">("laptop");
29 const [selectedBrand, setSelectedBrand] = useState<string>("");
30 const [selectedModelId, setSelectedModelId] = useState<string>("");
31 const [searchQuery, setSearchQuery] = useState("");
32 const [selectedGuideId, setSelectedGuideId] = useState<string | null>(null);
33
34 // Check URL params for preselected model
35 useEffect(() => {
36 const params = new URLSearchParams(window.location.search);
37 const modelParam = params.get("model");
38 if (modelParam) {
39 setSelectedModelId(modelParam);
40 }
41 }, []);
42
43 // Fetch brands for selected device type
44 const { data: brands = [], isLoading: brandsLoading } = useQuery<DeviceBrand[]>({
45 queryKey: ["/api/device-brands", selectedDeviceType],
46 queryFn: () => fetch(`/api/device-brands?deviceType=${selectedDeviceType}`).then(res => res.json()),
47 });
48
49 // Fetch models for selected brand or all models
50 const { data: allModels = [], isLoading: modelsLoading } = useQuery<DeviceModel[]>({
51 queryKey: ["/api/device-models", selectedBrand, selectedDeviceType],
52 queryFn: () => {
53 const url = selectedBrand
54 ? `/api/brands/${selectedBrand}/models`
55 : `/api/device-models?deviceType=${selectedDeviceType}`;
56 return fetch(url).then(res => res.json());
57 },
58 });
59
60 // Search models when typing
61 const { data: searchResults = [] } = useQuery<DeviceModel[]>({
62 queryKey: ["/api/device-models/search", searchQuery, selectedDeviceType],
63 queryFn: () => fetch(`/api/device-models?search=${encodeURIComponent(searchQuery)}&deviceType=${selectedDeviceType}`).then(res => res.json()),
64 enabled: searchQuery.trim().length > 1,
65 });
66
67 // Fetch compatible guides for selected model
68 const { data: compatibleGuides = [], isLoading: guidesLoading } = useQuery<RepairGuide[]>({
69 queryKey: ["/api/models", selectedModelId, "guides"],
70 queryFn: () => selectedModelId ? fetch(`/api/models/${selectedModelId}/guides`).then(res => res.json()) : Promise.resolve([]),
71 enabled: !!selectedModelId,
72 });
73
74 const displayModels = searchQuery.trim().length > 1 ? searchResults : allModels;
75 const activeModel = allModels.find(m => m.id === selectedModelId) || searchResults.find(m => m.id === selectedModelId);
76
77 return (
78 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-10">
79
80 {/* Header Banner */}
81 <div className="text-center max-w-3xl mx-auto space-y-4">
82 <div className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full bg-primary/10 text-primary border border-primary/20 text-xs font-bold uppercase tracking-wider">
83 <Sparkles className="h-3.5 w-3.5" /> Comprehensive Model Catalog
84 </div>
85 <h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight">
86 Find Your <span className="bg-gradient-to-r from-primary to-indigo-500 bg-clip-text text-transparent">Exact System</span>
87 </h1>
88 <p className="text-sm sm:text-base text-muted-foreground leading-relaxed">
89 Select your brand and model to view tailored disassembly guides, component compatibility, and factory common issues.
90 </p>
91
92 {/* Device Switcher Tabs */}
93 <div className="inline-flex p-1.5 rounded-2xl bg-muted/60 border border-border/50 shadow-sm mx-auto">
94 <button
95 onClick={() => {
96 setSelectedDeviceType("laptop");
97 setSelectedBrand("");
98 setSelectedModelId("");
99 }}
100 className={cn(
101 "flex items-center gap-2 px-6 py-2 rounded-xl text-xs sm:text-sm font-bold transition-all",
102 selectedDeviceType === "laptop"
103 ? "bg-background text-foreground shadow-sm"
104 : "text-muted-foreground hover:text-foreground"
105 )}
106 >
107 <Laptop className="h-4 w-4" /> Laptop Models
108 </button>
109 <button
110 onClick={() => {
111 setSelectedDeviceType("desktop");
112 setSelectedBrand("");
113 setSelectedModelId("");
114 }}
115 className={cn(
116 "flex items-center gap-2 px-6 py-2 rounded-xl text-xs sm:text-sm font-bold transition-all",
117 selectedDeviceType === "desktop"
118 ? "bg-background text-foreground shadow-sm"
119 : "text-muted-foreground hover:text-foreground"
120 )}
121 >
122 <Monitor className="h-4 w-4" /> Desktop & Form Factors
123 </button>
124 </div>
125 </div>
126
127 {/* Search Input Bar */}
128 <div className="max-w-2xl mx-auto relative group">
129 <Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground group-focus-within:text-primary transition-colors" />
130 <Input
131 type="text"
132 value={searchQuery}
133 onChange={(e) => setSearchQuery(e.target.value)}
134 placeholder={`Search ${selectedDeviceType}s (e.g. MacBook Pro, ThinkPad X1, Dell XPS, Alienware, ROG, OptiPlex)...`}
135 className="w-full pl-12 pr-10 h-12 rounded-2xl bg-card/80 border border-border/60 shadow-sm text-sm focus-visible:ring-2 focus-visible:ring-primary/20"
136 />
137 {searchQuery && (
138 <button
139 onClick={() => setSearchQuery("")}
140 className="absolute right-3.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
141 >
142 <X className="h-4 w-4" />
143 </button>
144 )}
145 </div>
146
147 {/* Brands Grid / Filter */}
148 {!searchQuery && (
149 <div className="space-y-3">
150 <div className="flex items-center justify-between">
151 <h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-2">
152 <Layers className="h-4 w-4" /> Filter by Brand
153 </h3>
154 {selectedBrand && (
155 <Button
156 variant="ghost"
157 size="sm"
158 onClick={() => setSelectedBrand("")}
159 className="h-7 text-xs text-muted-foreground hover:text-foreground gap-1"
160 >
161 <X className="h-3 w-3" /> Show All Brands
162 </Button>
163 )}
164 </div>
165
166 <div className="flex flex-wrap gap-2">
167 <button
168 onClick={() => setSelectedBrand("")}
169 className={cn(
170 "px-4 py-2 rounded-xl text-xs font-bold transition-all border",
171 selectedBrand === ""
172 ? "bg-primary text-primary-foreground border-primary shadow-sm"
173 : "bg-card/50 border-border/50 text-muted-foreground hover:text-foreground hover:bg-card"
174 )}
175 >
176 All Brands ({brands.length})
177 </button>
178 {brands.map((brand) => (
179 <button
180 key={brand.id}
181 onClick={() => {
182 setSelectedBrand(selectedBrand === brand.id ? "" : brand.id);
183 setSelectedModelId("");
184 }}
185 className={cn(
186 "px-4 py-2 rounded-xl text-xs font-bold transition-all border flex items-center gap-2",
187 selectedBrand === brand.id
188 ? "bg-primary text-primary-foreground border-primary shadow-sm"
189 : "bg-card/50 border-border/50 text-muted-foreground hover:text-foreground hover:bg-card"
190 )}
191 >
192 <span>{brand.name}</span>
193 {allModels.filter(m => m.brandId === brand.id).length > 0 && (
194 <span className={cn(
195 "text-[10px] px-1.5 py-0.2 rounded-full",
196 selectedBrand === brand.id ? "bg-white/20 text-white" : "bg-muted text-muted-foreground"
197 )}>
198 {allModels.filter(m => m.brandId === brand.id).length}
199 </span>
200 )}
201 </button>
202 ))}
203 </div>
204 </div>
205 )}
206
207 {/* Main Grid: Models List & Active Model Inspection Panel */}
208 <div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
209
210 {/* Left: Device Models Grid */}
211 <div className={cn("space-y-4", activeModel ? "lg:col-span-7" : "lg:col-span-12")}>
212 <div className="flex items-center justify-between">
213 <h2 className="text-lg font-bold">
214 Available Models ({displayModels.length})
215 </h2>
216 <span className="text-xs text-muted-foreground">Select a system to inspect teardowns</span>
217 </div>
218
219 <div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
220 {displayModels.map((model) => {
221 const isSelected = selectedModelId === model.id;
222 const specs = model.specifications as Record<string, string> || {};
223
224 return (
225 <div
226 key={model.id}
227 onClick={() => setSelectedModelId(model.id)}
228 className={cn(
229 "p-4 rounded-2xl border transition-all duration-200 cursor-pointer flex flex-col justify-between text-left",
230 isSelected
231 ? "bg-primary/10 border-primary shadow-md ring-1 ring-primary/30"
232 : "bg-card/50 hover:bg-card/90 border-border/50 hover:border-primary/40 hover:shadow-sm"
233 )}
234 >
235 <div className="space-y-1.5">
236 <div className="flex items-start justify-between gap-2">
237 <h4 className="text-sm font-bold leading-tight group-hover:text-primary transition-colors">
238 {model.name}
239 </h4>
240 {model.year && (
241 <span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-muted text-muted-foreground shrink-0">
242 {model.year}
243 </span>
244 )}
245 </div>
246 {model.series && (
247 <div className="text-xs text-primary/80 font-medium">{model.series} Series</div>
248 )}
249 {specs.cpu && (
250 <p className="text-[11px] text-muted-foreground line-clamp-1 flex items-center gap-1">
251 <Cpu className="h-3 w-3 shrink-0" /> {specs.cpu}
252 </p>
253 )}
254 </div>
255
256 <div className="mt-3 pt-2.5 border-t border-border/30 flex items-center justify-between text-xs">
257 <span className="text-[10px] font-semibold text-muted-foreground">
258 {model.guideCount || 12} Verified Guides
259 </span>
260 <span className="text-xs font-bold text-primary flex items-center gap-0.5">
261 Inspect <ChevronRight className="h-3 w-3" />
262 </span>
263 </div>
264 </div>
265 );
266 })}
267 </div>
268
269 {displayModels.length === 0 && (
270 <div className="text-center py-12 bg-muted/20 rounded-3xl border border-dashed border-border/60 space-y-2">
271 <h4 className="font-bold text-base">No models found</h4>
272 <p className="text-xs text-muted-foreground">Try broadening your search query or choosing another brand.</p>
273 </div>
274 )}
275 </div>
276
277 {/* Right: Detailed Model Inspection & Compatible Guides */}
278 {activeModel && (
279 <div className="lg:col-span-5 space-y-6">
280 <div className="p-6 rounded-3xl bg-card/80 backdrop-blur-xl border border-primary/30 shadow-lg space-y-6 sticky top-24">
281
282 {/* Header */}
283 <div className="space-y-2 border-b border-border/40 pb-4">
284 <div className="flex items-center justify-between">
285 <span className="px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider rounded-full bg-primary/10 text-primary border border-primary/20">
286 Active System
287 </span>
288 <button
289 onClick={() => setSelectedModelId("")}
290 className="text-muted-foreground hover:text-foreground text-xs"
291 >
292 <X className="h-4 w-4" />
293 </button>
294 </div>
295 <h3 className="text-xl font-extrabold">{activeModel.name}</h3>
296 <p className="text-xs text-muted-foreground">
297 {activeModel.series} Series • Release {activeModel.year || "2024"}
298 </p>
299 </div>
300
301 {/* Hardware Specifications */}
302 {activeModel.specifications && Object.keys(activeModel.specifications).length > 0 && (
303 <div className="space-y-2.5">
304 <h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
305 <Cpu className="h-3.5 w-3.5 text-primary" /> Key Specifications
306 </h4>
307 <div className="grid grid-cols-1 gap-2 text-xs">
308 {Object.entries(activeModel.specifications as Record<string, string>).map(([key, val]) => (
309 <div key={key} className="p-2.5 rounded-xl bg-muted/40 border border-border/40 flex justify-between items-center">
310 <span className="font-semibold capitalize text-muted-foreground">{key}:</span>
311 <span className="font-medium text-foreground text-right">{val}</span>
312 </div>
313 ))}
314 </div>
315 </div>
316 )}
317
318 {/* Common Issues & Diagnostics */}
319 {Array.isArray(activeModel.commonIssues) && activeModel.commonIssues.length > 0 && (
320 <div className="space-y-2.5">
321 <h4 className="text-xs font-bold uppercase tracking-wider text-amber-500 flex items-center gap-1.5">
322 <AlertTriangle className="h-3.5 w-3.5" /> Known Issues & Service Points
323 </h4>
324 <ul className="space-y-1.5 text-xs text-muted-foreground">
325 {(activeModel.commonIssues as string[]).map((issue, i) => (
326 <li key={i} className="flex items-start gap-2 bg-amber-500/5 p-2 rounded-xl border border-amber-500/20">
327 <CheckCircle2 className="h-3.5 w-3.5 text-amber-500 shrink-0 mt-0.5" />
328 <span>{issue}</span>
329 </li>
330 ))}
331 </ul>
332 </div>
333 )}
334
335 {/* Compatible Repair Guides */}
336 <div className="space-y-3 pt-2 border-t border-border/40">
337 <h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
338 <Wrench className="h-3.5 w-3.5 text-indigo-500" /> Compatible Procedures ({compatibleGuides.length})
339 </h4>
340
341 <div className="space-y-2 max-h-60 overflow-y-auto pr-1">
342 {compatibleGuides.map((guide) => (
343 <button
344 key={guide.id}
345 onClick={() => setSelectedGuideId(guide.id)}
346 className="w-full p-2.5 rounded-xl bg-muted/30 hover:bg-primary/10 border border-border/40 hover:border-primary/40 transition-all flex items-center justify-between text-left group"
347 >
348 <div className="space-y-0.5">
349 <span className="text-xs font-bold block group-hover:text-primary transition-colors">
350 {guide.title}
351 </span>
352 <span className="text-[10px] text-muted-foreground capitalize">
353 {guide.category} • {guide.difficulty} • {guide.estimatedTime}
354 </span>
355 </div>
356 <ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all shrink-0" />
357 </button>
358 ))}
359 </div>
360 </div>
361
362 </div>
363 </div>
364 )}
365 </div>
366
367 {/* Guide Detail Modal */}
368 {selectedGuideId && (
369 <GuideDetailModal
370 guideId={selectedGuideId}
371 onClose={() => setSelectedGuideId(null)}
372 />
373 )}
374 </div>
375 );
376 }