1 import React, { useState } from "react";
2 import { useQuery } from "@tanstack/react-query";
3 import { useLocation } from "wouter";
4 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5 import { Button } from "@/components/ui/button";
6 import { Input } from "@/components/ui/input";
7 import { Badge } from "@/components/ui/badge";
8 import { Search, Laptop, Monitor, Star, Wrench, Filter, ExternalLink, Eye, Download } from "lucide-react";
9 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
10 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
11 import { Separator } from "@/components/ui/separator";
12 import Header from "@/components/layout/header";
13 import Footer from "@/components/layout/footer";
14 import type { DeviceBrand, DeviceModel, RepairGuide } from "@shared/schema";
15
16 export default function DeviceFinder() {
17 const [, setLocation] = useLocation();
18 const [selectedDeviceType, setSelectedDeviceType] = useState<"laptop" | "desktop">("laptop");
19 const [selectedBrand, setSelectedBrand] = useState<string>("");
20 const [selectedModel, setSelectedModel] = useState<string>("");
21 const [searchQuery, setSearchQuery] = useState("");
22 const [guideFilter, setGuideFilter] = useState<string>("all");
23 const [categoryFilter, setCategoryFilter] = useState<string>("all");
24
25 // Fetch brands for selected device type
26 const { data: brands = [] } = useQuery<DeviceBrand[]>({
27 queryKey: ["/api/device-brands", selectedDeviceType],
28 queryFn: () => fetch(`/api/device-brands?deviceType=${selectedDeviceType}`).then(res => res.json()),
29 });
30
31 // Fetch models for selected brand
32 const { data: models = [] } = useQuery<DeviceModel[]>({
33 queryKey: ["/api/device-models", selectedBrand],
34 queryFn: () => selectedBrand ? fetch(`/api/brands/${selectedBrand}/models`).then(res => res.json()) : Promise.resolve([]),
35 enabled: !!selectedBrand,
36 });
37
38 // Fetch compatible guides for selected model
39 const { data: compatibleGuides = [] } = useQuery<RepairGuide[]>({
40 queryKey: ["/api/models", selectedModel, "guides"],
41 queryFn: () => selectedModel ? fetch(`/api/models/${selectedModel}/guides`).then(res => res.json()) : Promise.resolve([]),
42 enabled: !!selectedModel,
43 });
44
45 // Search models when typing
46 const { data: searchResults = [] } = useQuery<DeviceModel[]>({
47 queryKey: ["/api/device-models/search", searchQuery, selectedDeviceType],
48 queryFn: () => searchQuery ? fetch(`/api/device-models?search=${encodeURIComponent(searchQuery)}&deviceType=${selectedDeviceType}`).then(res => res.json()) : Promise.resolve([]),
49 enabled: searchQuery.length > 2,
50 });
51
52 const selectedModelData = models.find(m => m.id === selectedModel);
53
54 const getSupportLevelColor = (level: string) => {
55 switch (level) {
56 case "official": return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300";
57 case "community": return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
58 case "basic": return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300";
59 default: return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300";
60 }
61 };
62
63 // Filter compatible guides based on selected filters
64 const filteredGuides = compatibleGuides.filter(guide => {
65 const difficultyMatch = guideFilter === "all" || guide.difficulty === guideFilter;
66 const categoryMatch = categoryFilter === "all" || guide.category === categoryFilter;
67 return difficultyMatch && categoryMatch;
68 });
69
70 const handleGuideClick = (guideId: string) => {
71 setLocation(`/guide/${guideId}`);
72 };
73
74 return (
75 <div className="min-h-screen bg-gray-50 dark:bg-gray-900 amoled:bg-black">
76 <Header />
77
78 <div className="container mx-auto py-8 px-4">
79 <div className="max-w-6xl mx-auto">
80 <div className="text-center mb-8">
81 <h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white amoled:text-white">
82 Device Finder
83 </h1>
84 <p className="text-xl text-gray-600 dark:text-gray-300 amoled:text-gray-300">
85 Find specific repair guides for your laptop or desktop computer model
86 </p>
87 <div className="mt-6 flex flex-wrap items-center justify-center gap-4 text-sm text-gray-500 dark:text-gray-400 amoled:text-gray-400">
88 <div className="flex items-center gap-2">
89 <Laptop className="h-4 w-4 text-primary-600" />
90 <span>25+ Device Brands</span>
91 </div>
92 <Separator orientation="vertical" className="h-4" />
93 <div className="flex items-center gap-2">
94 <Monitor className="h-4 w-4 text-primary-600" />
95 <span>150+ Device Models</span>
96 </div>
97 <Separator orientation="vertical" className="h-4" />
98 <div className="flex items-center gap-2">
99 <Wrench className="h-4 w-4 text-primary-600" />
100 <span>Compatibility Verified</span>
101 </div>
102 </div>
103 </div>
104
105 <Tabs value={selectedDeviceType} onValueChange={(value) => {
106 setSelectedDeviceType(value as "laptop" | "desktop");
107 setSelectedBrand("");
108 setSelectedModel("");
109 setSearchQuery("");
110 }} className="mb-8">
111 <TabsList className="grid w-full grid-cols-2">
112 <TabsTrigger value="laptop" className="flex items-center gap-2">
113 <Laptop className="h-4 w-4" />
114 Laptops
115 </TabsTrigger>
116 <TabsTrigger value="desktop" className="flex items-center gap-2">
117 <Monitor className="h-4 w-4" />
118 Desktops
119 </TabsTrigger>
120 </TabsList>
121
122 <TabsContent value={selectedDeviceType} className="space-y-6">
123 {/* Search Section */}
124 <Card>
125 <CardHeader>
126 <CardTitle className="flex items-center gap-2">
127 <Search className="h-5 w-5" />
128 Quick Search
129 </CardTitle>
130 <CardDescription>
131 Search for your specific device model
132 </CardDescription>
133 </CardHeader>
134 <CardContent>
135 <div className="relative">
136 <Input
137 placeholder={`Search ${selectedDeviceType} models...`}
138 value={searchQuery}
139 onChange={(e) => setSearchQuery(e.target.value)}
140 className="pr-10"
141 />
142 <Search className="absolute right-3 top-3 h-4 w-4 text-muted-foreground" />
143 </div>
144
145 {searchResults.length > 0 && (
146 <div className="mt-4 grid gap-3">
147 {searchResults.slice(0, 5).map((model) => (
148 <Card key={model.id} className="cursor-pointer hover:bg-accent" onClick={() => {
149 setSelectedModel(model.id);
150 setSelectedBrand(model.brandId);
151 setSearchQuery("");
152 }}>
153 <CardContent className="p-4">
154 <div className="flex justify-between items-center">
155 <div>
156 <h4 className="font-semibold">{model.name}</h4>
157 <p className="text-sm text-muted-foreground">{model.series} • {model.year}</p>
158 </div>
159 <Badge variant="secondary">{model.guideCount} guides</Badge>
160 </div>
161 </CardContent>
162 </Card>
163 ))}
164 </div>
165 )}
166 </CardContent>
167 </Card>
168
169 {/* Brand Selection */}
170 <Card>
171 <CardHeader>
172 <CardTitle>Browse by Brand</CardTitle>
173 <CardDescription>
174 Select your device brand to see compatible models
175 </CardDescription>
176 </CardHeader>
177 <CardContent>
178 <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
179 {brands.map((brand) => (
180 <Card
181 key={brand.id}
182 className={`cursor-pointer transition-colors ${
183 selectedBrand === brand.id ? "ring-2 ring-primary" : "hover:bg-accent"
184 }`}
185 onClick={() => {
186 setSelectedBrand(brand.id);
187 setSelectedModel("");
188 }}
189 >
190 <CardContent className="p-4">
191 <div className="flex justify-between items-center mb-2">
192 <h3 className="font-semibold">{brand.name}</h3>
193 <Badge className={getSupportLevelColor(brand.supportLevel)}>
194 {brand.supportLevel}
195 </Badge>
196 </div>
197 <div className="flex items-center gap-2 text-sm text-muted-foreground">
198 <Star className="h-4 w-4" />
199 {brand.popularity} popularity
200 </div>
201 </CardContent>
202 </Card>
203 ))}
204 </div>
205 </CardContent>
206 </Card>
207
208 {/* Model Selection */}
209 {selectedBrand && models.length > 0 && (
210 <Card>
211 <CardHeader>
212 <CardTitle>Select Your Model</CardTitle>
213 <CardDescription>
214 Choose your specific device model
215 </CardDescription>
216 </CardHeader>
217 <CardContent>
218 <div className="grid gap-3 md:grid-cols-2">
219 {models.map((model) => (
220 <Card
221 key={model.id}
222 className={`cursor-pointer transition-colors ${
223 selectedModel === model.id ? "ring-2 ring-primary" : "hover:bg-accent"
224 }`}
225 onClick={() => setSelectedModel(model.id)}
226 >
227 <CardContent className="p-4">
228 <div className="flex justify-between items-center mb-2">
229 <h4 className="font-semibold">{model.name}</h4>
230 <Badge variant="outline">{model.year}</Badge>
231 </div>
232 <p className="text-sm text-muted-foreground mb-2">{model.series}</p>
233 <div className="flex items-center gap-2 text-sm">
234 <Wrench className="h-4 w-4" />
235 {model.guideCount} repair guides
236 </div>
237 </CardContent>
238 </Card>
239 ))}
240 </div>
241 </CardContent>
242 </Card>
243 )}
244
245 {/* Model Details and Compatible Guides */}
246 {selectedModelData && (
247 <div className="grid gap-6 lg:grid-cols-3">
248 {/* Model Details */}
249 <Card className="lg:col-span-1">
250 <CardHeader>
251 <div className="flex items-center justify-between">
252 <div>
253 <CardTitle className="text-lg">{selectedModelData.name}</CardTitle>
254 <CardDescription>
255 {selectedModelData.series} • {selectedModelData.year}
256 </CardDescription>
257 </div>
258 <Badge variant="outline" className="bg-primary-50 dark:bg-primary-900 text-primary-700 dark:text-primary-300">
259 {selectedModelData.guideCount} guides
260 </Badge>
261 </div>
262 </CardHeader>
263 <CardContent className="space-y-4">
264 {selectedModelData.specifications && (
265 <div className="space-y-3">
266 <h4 className="font-semibold text-sm text-gray-900 dark:text-white amoled:text-white flex items-center gap-2">
267 <i className="material-icons text-sm">computer</i>
268 Specifications
269 </h4>
270 <div className="text-sm space-y-2 bg-gray-50 dark:bg-gray-800 amoled:bg-gray-900 p-3 rounded-lg">
271 {Object.entries(selectedModelData.specifications as Record<string, any>).map(([key, value]) => (
272 <div key={key} className="flex justify-between items-center">
273 <span className="capitalize text-gray-600 dark:text-gray-400 amoled:text-gray-400 font-medium">
274 {key}:
275 </span>
276 <span className="text-gray-900 dark:text-white amoled:text-white">{String(value)}</span>
277 </div>
278 ))}
279 </div>
280 </div>
281 )}
282
283 {selectedModelData.commonIssues && (selectedModelData.commonIssues as any[]).length > 0 && (
284 <div className="space-y-3">
285 <h4 className="font-semibold text-sm text-gray-900 dark:text-white amoled:text-white flex items-center gap-2">
286 <i className="material-icons text-sm text-red-600">warning</i>
287 Common Issues
288 </h4>
289 <div className="flex flex-wrap gap-2">
290 {(selectedModelData.commonIssues as any[]).map((issue, index) => (
291 <Badge key={index} variant="destructive" className="text-xs">
292 {String(issue)}
293 </Badge>
294 ))}
295 </div>
296 </div>
297 )}
298 </CardContent>
299 </Card>
300
301 {/* Compatible Guides */}
302 <Card className="lg:col-span-2">
303 <CardHeader>
304 <div className="flex items-center justify-between">
305 <div>
306 <CardTitle className="flex items-center gap-2">
307 <Wrench className="h-5 w-5 text-primary-600" />
308 Compatible Repair Guides
309 </CardTitle>
310 <CardDescription>
311 {filteredGuides.length} of {compatibleGuides.length} guides match your filters
312 </CardDescription>
313 </div>
314
315 {/* Guide Filters */}
316 <div className="flex items-center gap-2">
317 <Select value={categoryFilter} onValueChange={setCategoryFilter}>
318 <SelectTrigger className="w-36">
319 <Filter className="h-4 w-4 mr-1" />
320 <SelectValue placeholder="Category" />
321 </SelectTrigger>
322 <SelectContent>
323 <SelectItem value="all">All Categories</SelectItem>
324 <SelectItem value="hardware">Hardware</SelectItem>
325 <SelectItem value="software">Software</SelectItem>
326 <SelectItem value="cleaning">Cleaning</SelectItem>
327 <SelectItem value="upgrades">Upgrades</SelectItem>
328 </SelectContent>
329 </Select>
330
331 <Select value={guideFilter} onValueChange={setGuideFilter}>
332 <SelectTrigger className="w-32">
333 <SelectValue placeholder="Difficulty" />
334 </SelectTrigger>
335 <SelectContent>
336 <SelectItem value="all">All Levels</SelectItem>
337 <SelectItem value="easy">Easy</SelectItem>
338 <SelectItem value="medium">Medium</SelectItem>
339 <SelectItem value="hard">Hard</SelectItem>
340 </SelectContent>
341 </Select>
342 </div>
343 </div>
344 </CardHeader>
345 <CardContent>
346 <div className="space-y-3 max-h-96 overflow-y-auto">
347 {filteredGuides.map((guide) => (
348 <Card
349 key={guide.id}
350 className="hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer border-l-4 border-l-transparent hover:border-l-primary-500 transition-all duration-200"
351 onClick={() => handleGuideClick(guide.id)}
352 >
353 <CardContent className="p-4">
354 <div className="flex justify-between items-start mb-2">
355 <h5 className="font-semibold text-gray-900 dark:text-white amoled:text-white text-sm">
356 {guide.title}
357 </h5>
358 <div className="flex items-center gap-2">
359 <Badge
360 variant={
361 guide.difficulty === "easy" ? "default" :
362 guide.difficulty === "medium" ? "secondary" : "destructive"
363 }
364 className="text-xs"
365 >
366 {guide.difficulty}
367 </Badge>
368 <Badge variant="outline" className="text-xs">
369 {guide.category}
370 </Badge>
371 </div>
372 </div>
373 <p className="text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300 mb-3 line-clamp-2">
374 {guide.description}
375 </p>
376 <div className="flex items-center justify-between">
377 <div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400 amoled:text-gray-400">
378 <span className="flex items-center gap-1">
379 <i className="material-icons text-xs">schedule</i>
380 {guide.estimatedTime}
381 </span>
382 <span className="flex items-center gap-1">
383 <Eye className="h-3 w-3" />
384 {guide.viewCount}
385 </span>
386 <span className="flex items-center gap-1">
387 <Download className="h-3 w-3" />
388 {guide.downloadCount}
389 </span>
390 </div>
391 <Button
392 size="sm"
393 variant="ghost"
394 className="h-7 px-2 text-primary-600 hover:text-primary-700 hover:bg-primary-50 dark:hover:bg-primary-900"
395 onClick={(e) => {
396 e.stopPropagation();
397 handleGuideClick(guide.id);
398 }}
399 >
400 <ExternalLink className="h-3 w-3 mr-1" />
401 View
402 </Button>
403 </div>
404 </CardContent>
405 </Card>
406 ))}
407
408 {filteredGuides.length === 0 && compatibleGuides.length > 0 && (
409 <div className="text-center py-8 text-gray-500 dark:text-gray-400 amoled:text-gray-400">
410 <Filter className="h-12 w-12 mx-auto mb-4 opacity-50" />
411 <p className="font-medium">No guides match your current filters</p>
412 <p className="text-sm">Try adjusting the difficulty or category filters above</p>
413 <Button
414 variant="outline"
415 size="sm"
416 className="mt-3"
417 onClick={() => {
418 setGuideFilter("all");
419 setCategoryFilter("all");
420 }}
421 >
422 Clear Filters
423 </Button>
424 </div>
425 )}
426
427 {compatibleGuides.length === 0 && (
428 <div className="text-center py-8 text-gray-500 dark:text-gray-400 amoled:text-gray-400">
429 <Wrench className="h-12 w-12 mx-auto mb-4 opacity-50" />
430 <p className="font-medium">No guides available yet for this model</p>
431 <p className="text-sm">Check back later or browse our general repair guides</p>
432 <Button
433 variant="outline"
434 size="sm"
435 className="mt-3"
436 onClick={() => setLocation("/")}
437 >
438 Browse All Guides
439 </Button>
440 </div>
441 )}
442 </div>
443 </CardContent>
444 </Card>
445 </div>
446 )}
447 </TabsContent>
448 </Tabs>
449
450 {/* Popular Devices Section */}
451 <div className="mt-12 text-center">
452 <h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white amoled:text-white">
453 Popular Device Categories
454 </h2>
455 <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-4">
456 <Card className="hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer transition-colors">
457 <CardContent className="p-6 text-center">
458 <div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center mx-auto mb-3">
459 <i className="material-icons text-blue-600 dark:text-blue-300">laptop_mac</i>
460 </div>
461 <h3 className="font-semibold text-gray-900 dark:text-white amoled:text-white mb-1">MacBook Series</h3>
462 <p className="text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300">15+ models supported</p>
463 </CardContent>
464 </Card>
465
466 <Card className="hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer transition-colors">
467 <CardContent className="p-6 text-center">
468 <div className="w-12 h-12 bg-red-100 dark:bg-red-900 rounded-lg flex items-center justify-center mx-auto mb-3">
469 <i className="material-icons text-red-600 dark:text-red-300">laptop_windows</i>
470 </div>
471 <h3 className="font-semibold text-gray-900 dark:text-white amoled:text-white mb-1">ThinkPad Series</h3>
472 <p className="text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300">20+ models supported</p>
473 </CardContent>
474 </Card>
475
476 <Card className="hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer transition-colors">
477 <CardContent className="p-6 text-center">
478 <div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center mx-auto mb-3">
479 <i className="material-icons text-green-600 dark:text-green-300">desktop_windows</i>
480 </div>
481 <h3 className="font-semibold text-gray-900 dark:text-white amoled:text-white mb-1">Gaming PCs</h3>
482 <p className="text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300">Custom builds supported</p>
483 </CardContent>
484 </Card>
485
486 <Card className="hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer transition-colors">
487 <CardContent className="p-6 text-center">
488 <div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center mx-auto mb-3">
489 <i className="material-icons text-purple-600 dark:text-purple-300">business</i>
490 </div>
491 <h3 className="font-semibold text-gray-900 dark:text-white amoled:text-white mb-1">Business Laptops</h3>
492 <p className="text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300">Enterprise solutions</p>
493 </CardContent>
494 </Card>
495 </div>
496 </div>
497 </div>
498 </div>
499
500 <Footer />
501 </div>
502 );
503 }