1 import { useState, useMemo, useEffect } from "react";
2 import { useQuery } from "@tanstack/react-query";
3 import {
4 Cpu,
5 Code,
6 Eraser,
7 MoveUp,
8 LayoutGrid,
9 Wrench,
10 FileText,
11 Download,
12 ChevronRight,
13 ChevronLeft,
14 ChevronsLeft,
15 ChevronsRight,
16 Sparkles,
17 SlidersHorizontal,
18 X,
19 ArrowRight,
20 Shield,
21 Monitor,
22 HardDrive,
23 Flame,
24 Zap,
25 RotateCcw,
26 CheckCircle2,
27 Check
28 } from "lucide-react";
29 import { Button } from "@/components/ui/button";
30 import { Badge } from "@/components/ui/badge";
31 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
32 import GuideCard from "@/components/repair/guide-card";
33 import ManualDetailModal, { type TechnicalManual } from "@/components/repair/manual-detail-modal";
34 import TechnicianDownloadHub, { ALL_DOWNLOADS } from "@/components/repair/technician-download-hub";
35 import type { RepairGuide } from "@shared/schema";
36 import { cn } from "@/lib/utils";
37
38 export interface CategoryMeta {
39 id: "hardware" | "software" | "cleaning" | "upgrades";
40 name: string;
41 shortName: string;
42 badgeTitle: string;
43 description: string;
44 longDescription: string;
45 icon: any;
46 color: string;
47 textColor: string;
48 bgColor: string;
49 borderColor: string;
50 accentBg: string;
51 manuals: TechnicalManual[];
52 }
53
54 export interface SubTopic {
55 id: string;
56 name: string;
57 keywords: string[];
58 }
59
60 export const CATEGORY_SUBTOPICS: Record<string, SubTopic[]> = {
61 hardware: [
62 { id: "all", name: "All Hardware", keywords: [] },
63 { id: "screens", name: "Screens & Hinges", keywords: ["screen", "hinge", "oled", "ips", "display", "bezel", "panel"] },
64 { id: "motherboard", name: "Logic Boards & VRM", keywords: ["motherboard", "logic board", "vrm", "power delivery", "mosfet", "rail", "schematic"] },
65 { id: "battery", name: "Battery & Power", keywords: ["battery", "capacity cell", "charging", "discharge", "power harness", "cell"] },
66 { id: "ports", name: "DC-In & USB-C Ports", keywords: ["dc-in", "usb-c", "receptacle", "soldering", "charging port", "type-c", "port"] },
67 { id: "keyboard", name: "Keyboards & Chassis", keywords: ["keyboard", "palmrest", "top-case", "trackpad", "chassis", "housing"] },
68 ],
69 software: [
70 { id: "all", name: "All Software", keywords: [] },
71 { id: "windows", name: "Windows 11 & OS Recovery", keywords: ["windows", "installation", "driver", "injection", "os recovery", "clean install"] },
72 { id: "bios", name: "UEFI / BIOS Crisis Flash", keywords: ["uefi", "bios", "crisis", "blind usb", "flashing", "firmware", "microcode"] },
73 { id: "bsod", name: "BSOD & Kernel Dumps", keywords: ["bsod", "blue screen", "minidump", "crash", "windbg", "kernel", "whea"] },
74 { id: "linux", name: "Linux & EFI Bootloaders", keywords: ["linux", "dual-boot", "bootloader", "grub", "efi", "bcd"] },
75 ],
76 cleaning: [
77 { id: "all", name: "All Cleaning", keywords: [] },
78 { id: "thermal", name: "Thermal Paste & PTM7950", keywords: ["thermal", "repaste", "ptm7950", "phase-change", "liquid metal", "compound", "mx-6"] },
79 { id: "ultrasonic", name: "Ultrasonic Bath & Liquid Damage", keywords: ["ultrasonic", "liquid damage", "de-oxidation", "corrosion", "ipa", "submersion"] },
80 { id: "fans", name: "Fans & Heatsink De-dusting", keywords: ["fan", "bearing", "heatsink", "dust", "de-dusting", "vapor chamber", "airflow"] },
81 ],
82 upgrades: [
83 { id: "all", name: "All Upgrades", keywords: [] },
84 { id: "nvme", name: "Gen4/Gen5 NVMe M.2 SSDs", keywords: ["nvme", "ssd", "m.2", "gen4", "gen5", "cloning", "storage", "partition"] },
85 { id: "ram", name: "DDR5 SODIMM RAM", keywords: ["ddr5", "ram", "sodimm", "dual-channel", "memory", "timing", "cudimm"] },
86 { id: "egpu", name: "eGPU & OCuLink Graphics", keywords: ["egpu", "thunderbolt", "oculink", "gpu", "graphics", "bandwidth"] },
87 { id: "cooling", name: "Liquid Cooling Loops", keywords: ["liquid cooling", "custom loop", "aio", "pump", "radiator", "overhaul"] },
88 ],
89 };
90
91 export const CATEGORY_DEFINITIONS: CategoryMeta[] = [
92 {
93 id: "hardware",
94 name: "Hardware Repairs & Teardowns",
95 shortName: "Hardware",
96 badgeTitle: "Component Level",
97 description: "Motherboards, OLED/IPS displays, hinges, batteries, ports, and logic boards.",
98 longDescription: "Physical component replacements, soldering bench standards, micro-connector latching, and structural hinge rebuilds.",
99 icon: Cpu,
100 color: "text-indigo-500",
101 textColor: "text-indigo-600 dark:text-indigo-400",
102 bgColor: "bg-indigo-500/10",
103 borderColor: "border-indigo-500/30",
104 accentBg: "from-indigo-500/10 via-indigo-500/5 to-transparent",
105 manuals: [
106 {
107 id: "man-hw-01",
108 category: "hardware",
109 title: "Universal Laptop Teardown & Screw Torque Specification Manual",
110 code: "DOC-HW-TEARDOWN-2026",
111 fileSize: "3.4 MB PDF",
112 pages: 42,
113 author: "JCR Bench Engineering Group",
114 lastUpdated: "2026 Revision 4.2",
115 summary: "Standard chassis screw torque limits, eDP ribbon connector zero-insertion-force (ZIF) latch protocols, and magnesium frame unibody separation techniques.",
116 keyTopics: [
117 "Thread engagement limits for M2/M2.5 chassis standoffs",
118 "eDP 30-pin and 40-pin ribbon cable pinout safety",
119 "Trackpad grounding tape and haptic sensor calibration",
120 "Antenna routing channels and RF isolation shields"
121 ],
122 safetyDirectives: [
123 "Always disconnect battery power harness before lifting motherboard",
124 "Maintain grounded ESD wrist strap under 1 megohm resistance",
125 "Never reuse stripped Torx screws on hinge anchors"
126 ],
127 recommendedTools: ["Torx T4/T5/T6", "Phillips #00", "Plastic Spudger Kit", "Torque Screwdriver (0.2-0.5 Nm)"]
128 },
129 {
130 id: "man-hw-02",
131 category: "hardware",
132 title: "Motherboard Power Delivery & DC-In VRM Diagnostic Schematic",
133 code: "SCHEM-HW-PWR-VRM",
134 fileSize: "4.8 MB PDF",
135 pages: 28,
136 author: "JCR Micro-soldering Team",
137 lastUpdated: "2026 Revision 2.0",
138 summary: "Detailed 19V / 20V USB-C Power Delivery handshake verification, 3.3V/5V always-on buck converter testing, and MOSFET short detection.",
139 keyTopics: [
140 "USB-C Power Delivery CC1/CC2 communication triggers",
141 "High-side and low-side switching MOSFET gate measurements",
142 "Thermal camera hot-spot analysis for shorted SMD capacitors",
143 "RTC battery discharge circuits and CMOS reset sequences"
144 ],
145 safetyDirectives: [
146 "Use current-limited DC bench power supply when injecting 1V into shorted rails",
147 "Do not bypass thermal fuses or PPTC resettable fuses"
148 ],
149 recommendedTools: ["Digital Multimeter (True RMS)", "DC Bench Power Supply", "Thermal Camera", "Hot Air Rework Station"]
150 }
151 ]
152 },
153 {
154 id: "software",
155 name: "Software, OS & Firmware Recovery",
156 shortName: "Software",
157 badgeTitle: "System & Kernel",
158 description: "BSOD stop-codes, UEFI/BIOS flashing, driver cleanups, and bootloaders.",
159 longDescription: "Kernel minidump analysis, blind USB UEFI/BIOS crisis recovery, clean installation architectures, and hardware driver conflicts.",
160 icon: Code,
161 color: "text-emerald-500",
162 textColor: "text-emerald-600 dark:text-emerald-400",
163 bgColor: "bg-emerald-500/10",
164 borderColor: "border-emerald-500/30",
165 accentBg: "from-emerald-500/10 via-emerald-500/5 to-transparent",
166 manuals: [
167 {
168 id: "man-sw-01",
169 category: "software",
170 title: "Windows Kernel Crash (BSOD) & Minidump Diagnostic Manual",
171 code: "DOC-SW-BSOD-DUMP",
172 fileSize: "2.2 MB PDF",
173 pages: 36,
174 author: "JCR System Architecture",
175 lastUpdated: "2026 Technical Release",
176 summary: "WinDbg stack trace interpretation, WHEA_UNCORRECTABLE_ERROR triage, kernel-mode driver symbol resolution, and page fault diagnosis.",
177 keyTopics: [
178 "WinDbg preview command line cheat-sheet (!analyze -v)",
179 "Distinguishing hardware bit flips vs corrupted kernel drivers",
180 "Resolving ntoskrnl.exe and watchdog.sys timeout errors",
181 "SFC and DISM offline image repair commands"
182 ],
183 safetyDirectives: [
184 "Back up EFI boot partitions before modifying boot configuration BCD",
185 "Always test stable memory in Safe Mode before registry alterations"
186 ],
187 recommendedTools: ["WinDbg Preview", "PassMark MemTest86", "Display Driver Uninstaller", "Rufus Bootable USB"]
188 },
189 {
190 id: "man-sw-02",
191 category: "software",
192 title: "UEFI / BIOS Crisis Flash & SPI Programmer Recovery Protocol",
193 code: "DOC-SW-BIOS-CRISIS",
194 fileSize: "3.1 MB PDF",
195 pages: 30,
196 author: "JCR Firmware Lab",
197 lastUpdated: "2026 Revision 3.1",
198 summary: "Corrupted SPI flash rom recovery, blind USB BIOS flashback key combinations, and CH341A 3.3V/1.8V chip clip flashing.",
199 keyTopics: [
200 "OEM blind crisis key combinations (Fn+R, Win+B, Fn+Esc)",
201 "CH341A clamp hookup to 8-pin SOIC SPI chips without desoldering",
202 "Injecting clean Intel ME/CSME regions into corrupted ROM dumps",
203 "Bypassing corrupt TPM / Secure Boot provisioning locks"
204 ],
205 safetyDirectives: [
206 "Verify 1.8V adapter voltage when flashing modern Winbond 1.8V chips",
207 "Never flash firmware over unstable battery power"
208 ],
209 recommendedTools: ["CH341A Mini Programmer", "SOIC8 Test Clip", "NeoProgrammer Utility", "Hex Editor (HxD)"]
210 }
211 ]
212 },
213 {
214 id: "cleaning",
215 name: "Cleaning, De-dusting & Thermal Service",
216 shortName: "Cleaning",
217 badgeTitle: "Thermals & Fluid",
218 description: "Thermal paste renewal, liquid metal barriers, ultrasonic PCB baths, and fan lubrication.",
219 longDescription: "Complete thermal subsystem overhauls, phase-change interface replacements (PTM7950), and ultrasonic de-oxidation.",
220 icon: Eraser,
221 color: "text-amber-500",
222 textColor: "text-amber-600 dark:text-amber-400",
223 bgColor: "bg-amber-500/10",
224 borderColor: "border-amber-500/30",
225 accentBg: "from-amber-500/10 via-amber-500/5 to-transparent",
226 manuals: [
227 {
228 id: "man-cl-01",
229 category: "cleaning",
230 title: "Phase-Change Thermal Interface & Liquid Metal Application Guide",
231 code: "DOC-CLN-THERMAL-2026",
232 fileSize: "2.8 MB PDF",
233 pages: 24,
234 author: "Thermal Dynamics Bench",
235 lastUpdated: "2026 Revision 5.0",
236 summary: "Standard operating procedures for Honeywell PTM7950 phase-change pads, liquid metal barrier foam gaskets, and thermal putty placement on VRM/VRAM.",
237 keyTopics: [
238 "PTM7950 phase-transition curing cycle (45°C burn-in)",
239 "Conformal silicone coating barriers to prevent gallium migration",
240 "Thermal putty (TG-PP10 / Upsiren U6 Pro) compression tolerances",
241 "Measuring delta-T across CPU individual core sensors"
242 ],
243 safetyDirectives: [
244 "Never apply liquid metal on bare aluminum heatsinks (creates brittle alloy)",
245 "Wear nitrile gloves when handling anhydrous 99.9% isopropyl alcohol"
246 ],
247 recommendedTools: ["99.9% Anhydrous IPA", "MG Chemicals Conformal Coating", "PTM7950 Sheets", "Thermal Putty"]
248 },
249 {
250 id: "man-cl-02",
251 category: "cleaning",
252 title: "Ultrasonic PCB Bath & Liquid Spill Corrosion Treatment",
253 code: "DOC-CLN-ULTRASONIC",
254 fileSize: "3.6 MB PDF",
255 pages: 32,
256 author: "JCR Decontamination Dept",
257 lastUpdated: "2026 Technical Spec",
258 summary: "Step-by-step restoration of liquid-damaged logic boards using heated ultrasonic baths, deionized water rinses, and desiccant oven curing.",
259 keyTopics: [
260 "Sweeping frequencies (40 kHz vs 80 kHz) to protect crystal oscillators",
261 "Branson EC cleaning solution mixing ratios",
262 "De-oxidation of corroded test points and 0201 SMD resistors",
263 "Post-wash bakeout cycle (65°C for 4 hours in convection chamber)"
264 ],
265 safetyDirectives: [
266 "Remove all microphones, CMOS batteries, and camera modules before ultrasonic submersion",
267 "Allow complete 100% moisture bakeout before applying bench power"
268 ],
269 recommendedTools: ["Ultrasonic Cleaner (6L Heated)", "Deionized Water", "Bakeout Oven / Hot Plate", "Insulated Tweezers"]
270 }
271 ]
272 },
273 {
274 id: "upgrades",
275 name: "Hardware Upgrades & Modifications",
276 shortName: "Upgrades",
277 badgeTitle: "Performance & Modding",
278 description: "Gen4/Gen5 NVMe SSDs, DDR5 SODIMM RAM, Wi-Fi 7 cards, eGPUs, and cooling mods.",
279 longDescription: "Storage cloning, high-speed memory timing synchronization, external graphics interfaces, and custom watercooling manifolds.",
280 icon: MoveUp,
281 color: "text-blue-500",
282 textColor: "text-blue-600 dark:text-blue-400",
283 bgColor: "bg-blue-500/10",
284 borderColor: "border-blue-500/30",
285 accentBg: "from-blue-500/10 via-blue-500/5 to-transparent",
286 manuals: [
287 {
288 id: "man-upg-01",
289 category: "upgrades",
290 title: "PCIe Gen4/Gen5 M.2 SSD Partition Cloning & Thermal Sizing Guide",
291 code: "DOC-UPG-NVME-CLONE",
292 fileSize: "2.4 MB PDF",
293 pages: 26,
294 author: "Storage Engineering Group",
295 lastUpdated: "2026 Technical Spec",
296 summary: "Bare-metal drive cloning with Clonezilla and Macrium, GPT EFI partition alignment, 4Kn vs 512e sector translation, and graphene heat spreaders.",
297 keyTopics: [
298 "Creating bootable Rescue Media and direct drive-to-drive cloning",
299 "Expanding unallocated partition space after cloning to larger SSD",
300 "Heatsink thermal pad placement on M.2 controller vs NAND flash dies",
301 "Enabling PCIe Gen4 speeds in UEFI without thermal throttling"
302 ],
303 safetyDirectives: [
304 "Ensure source and target drive IDs are strictly verified before cloning",
305 "Do not over-tighten M.2 mounting screw to prevent PCB substrate bending"
306 ],
307 recommendedTools: ["Dual-Bay NVMe Enclosure", "Clonezilla Live USB", "Graphene Thermal Heatsink", "M2x3mm Screws"]
308 },
309 {
310 id: "man-upg-02",
311 category: "upgrades",
312 title: "SODIMM & CUDIMM DDR5 Memory Speed & Timing Synchronization",
313 code: "DOC-UPG-RAM-TIMINGS",
314 fileSize: "1.8 MB PDF",
315 pages: 20,
316 author: "Memory Architecture Lab",
317 lastUpdated: "2026 Revision 2.4",
318 summary: "Matching memory ranks (1Rx8 vs 2Rx8), SPD JEDEC profile negotiation, CAS latencies, and dual-channel bandwidth validation.",
319 keyTopics: [
320 "Why mixing 1Rx16 with 1Rx8 memory causes stuttering and lower FPS",
321 "Enabling XMP/EXPO profiles on compatible gaming and creator motherboards",
322 "Troubleshooting memory training boot loops on initial DDR5 installs",
323 "AIDA64 and PassMark memory latency benchmarking protocols"
324 ],
325 safetyDirectives: [
326 "Insert SODIMM sticks at a 30-degree angle and press down until dual clips click",
327 "Ensure power cord and battery are completely disconnected during RAM insertion"
328 ],
329 recommendedTools: ["HWiNFO64", "CPU-Z", "PassMark MemTest86", "ESD Anti-Static Wrist Strap"]
330 }
331 ]
332 }
333 ];
334
335 interface CategorizedGuidesHubProps {
336 selectedDeviceType: "all" | "laptop" | "desktop";
337 selectedCategory: string;
338 onCategoryChange: (categoryId: string) => void;
339 searchQuery: string;
340 onSearchChange: (query: string) => void;
341 selectedDifficulty: string;
342 onDifficultyChange: (diff: string) => void;
343 sortOption: string;
344 onSortChange: (sort: string) => void;
345 onViewGuide: (guideId: string) => void;
346 }
347
348 // Brand matcher helper across title, description, and alternative solutions
349 const isGuideMatchingBrand = (guide: RepairGuide, brandName: string): boolean => {
350 if (!brandName || brandName === "all") return true;
351 const b = brandName.toLowerCase();
352 const title = (guide.title || "").toLowerCase();
353 const desc = (guide.description || "").toLowerCase();
354 const alt = (guide.alternativeSolutions || "").toLowerCase();
355
356 switch (b) {
357 case "apple":
358 return title.includes("apple") || title.includes("macbook") || title.includes("mac ") || title.includes("imac") || title.includes("mac mini") || title.includes("mac studio") || desc.includes("apple") || alt.includes("apple");
359 case "dell":
360 return title.includes("dell") || title.includes("xps") || title.includes("alienware") || title.includes("optiplex") || title.includes("latitude") || title.includes("precision") || desc.includes("dell") || alt.includes("dell");
361 case "lenovo":
362 return title.includes("lenovo") || title.includes("thinkpad") || title.includes("legion") || title.includes("thinkcentre") || title.includes("thinkstation") || title.includes("yoga") || desc.includes("lenovo") || alt.includes("lenovo");
363 case "hp":
364 return title.includes("hp") || title.includes("spectre") || title.includes("omen") || title.includes("elitebook") || title.includes("prodesk") || title.includes("elitedesk") || title.includes("z4") || desc.includes("hp") || alt.includes("hp");
365 case "asus":
366 return title.includes("asus") || title.includes("rog") || title.includes("zenbook") || title.includes("strix") || title.includes("zephyrus") || title.includes("proart") || desc.includes("asus") || alt.includes("asus");
367 case "acer":
368 return title.includes("acer") || title.includes("predator") || title.includes("helios") || title.includes("swift") || title.includes("aspire") || desc.includes("acer") || alt.includes("acer");
369 case "msi":
370 return title.includes("msi") || title.includes("stealth") || title.includes("raider") || title.includes("trident") || title.includes("creator") || desc.includes("msi") || alt.includes("msi");
371 case "razer":
372 return title.includes("razer") || title.includes("blade") || desc.includes("razer") || alt.includes("razer");
373 case "microsoft":
374 return title.includes("microsoft") || title.includes("surface") || desc.includes("microsoft") || alt.includes("microsoft");
375 case "framework":
376 return title.includes("framework") || desc.includes("framework") || alt.includes("framework");
377 case "samsung":
378 return title.includes("samsung") || title.includes("galaxy book") || desc.includes("samsung") || alt.includes("samsung");
379 case "lg":
380 return title.includes("lg") || title.includes("gram") || desc.includes("lg") || alt.includes("lg");
381 case "custom built":
382 case "custom":
383 return title.includes("custom") || title.includes("atx") || title.includes("sff") || title.includes("itx") || title.includes("watercooled") || desc.includes("custom") || alt.includes("custom");
384 default:
385 return title.includes(b) || desc.includes(b) || alt.includes(b);
386 }
387 };
388
389 export default function CategorizedGuidesHub({
390 selectedDeviceType,
391 selectedCategory,
392 onCategoryChange,
393 searchQuery,
394 onSearchChange,
395 selectedDifficulty,
396 onDifficultyChange,
397 sortOption,
398 onSortChange,
399 onViewGuide,
400 }: CategorizedGuidesHubProps) {
401 const [selectedManual, setSelectedManual] = useState<TechnicalManual | null>(null);
402 const [viewFormat, setViewFormat] = useState<"all" | "guides" | "manuals">("all");
403 const [selectedBrandFilter, setSelectedBrandFilter] = useState<string>("all");
404 const [selectedSubTopics, setSelectedSubTopics] = useState<Record<string, string>>({
405 hardware: "all",
406 software: "all",
407 cleaning: "all",
408 upgrades: "all",
409 });
410 const [categoryPages, setCategoryPages] = useState<Record<string, number>>({
411 hardware: 1,
412 software: 1,
413 cleaning: 1,
414 upgrades: 1,
415 });
416 const [pageSize, setPageSize] = useState<number>(24);
417
418 const { data: allGuides = [], isLoading } = useQuery<RepairGuide[]>({
419 queryKey: ["/api/repair-guides"],
420 queryFn: () => fetch("/api/repair-guides").then((res) => res.json()),
421 staleTime: 10 * 60 * 1000, // 10 minutes cache
422 gcTime: 30 * 60 * 1000, // 30 minutes in memory
423 });
424
425 // Reset pagination when filter criteria change
426 useEffect(() => {
427 setCategoryPages({
428 hardware: 1,
429 software: 1,
430 cleaning: 1,
431 upgrades: 1,
432 });
433 }, [selectedDeviceType, selectedDifficulty, selectedBrandFilter, searchQuery, sortOption]);
434
435 // Extract all distinct brand names for quick filtering
436 const availableBrands = [
437 "all",
438 "Apple",
439 "Dell",
440 "Lenovo",
441 "HP",
442 "ASUS",
443 "Acer",
444 "MSI",
445 "Razer",
446 "Microsoft",
447 "Framework",
448 "Samsung",
449 "LG",
450 "Custom Built"
451 ];
452
453 // Memoized Brand Guide Counts in a single efficient O(N * Brands) pass
454 const brandGuideCounts = useMemo(() => {
455 const counts: Record<string, number> = {};
456 for (const b of availableBrands) {
457 counts[b] = 0;
458 }
459
460 const devFiltered = allGuides.filter(g =>
461 !selectedDeviceType || selectedDeviceType === "all" || g.deviceType === selectedDeviceType
462 );
463
464 counts["all"] = devFiltered.length;
465
466 for (const g of devFiltered) {
467 for (const b of availableBrands) {
468 if (b !== "all" && isGuideMatchingBrand(g, b)) {
469 counts[b]++;
470 }
471 }
472 }
473
474 return counts;
475 }, [allGuides, selectedDeviceType]);
476
477 const getBrandGuideCount = (brandName: string) => {
478 return brandGuideCounts[brandName] || 0;
479 };
480
481 // Memoized category total counts
482 const categoryTotalCounts = useMemo(() => {
483 const counts: Record<string, number> = {
484 hardware: 0,
485 software: 0,
486 cleaning: 0,
487 upgrades: 0,
488 };
489 for (const g of allGuides) {
490 if (!selectedDeviceType || selectedDeviceType === "all" || g.deviceType === selectedDeviceType) {
491 const cat = g.category?.toLowerCase();
492 if (counts[cat] !== undefined) {
493 counts[cat]++;
494 }
495 }
496 }
497 return counts;
498 }, [allGuides, selectedDeviceType]);
499
500 const getCategoryTotalCount = (categoryId: string) => {
501 return categoryTotalCounts[categoryId.toLowerCase()] || 0;
502 };
503
504 // Memoized filtered and sorted guides per category
505 const categoryGuidesMap = useMemo(() => {
506 const map: Record<string, RepairGuide[]> = {
507 hardware: [],
508 software: [],
509 cleaning: [],
510 upgrades: [],
511 };
512
513 const categories = ["hardware", "software", "cleaning", "upgrades"];
514 const q = searchQuery.trim().toLowerCase();
515
516 for (const catId of categories) {
517 const activeSubTopic = selectedSubTopics[catId] || "all";
518 const subTopicsList = CATEGORY_SUBTOPICS[catId] || [];
519 const currentSubTopicDef = subTopicsList.find(st => st.id === activeSubTopic);
520
521 const filtered = allGuides.filter((guide) => {
522 if (guide.category.toLowerCase() !== catId) return false;
523 if (selectedDeviceType && selectedDeviceType !== "all" && guide.deviceType !== selectedDeviceType) return false;
524 if (selectedDifficulty !== "all" && guide.difficulty.toLowerCase() !== selectedDifficulty.toLowerCase()) return false;
525 if (!isGuideMatchingBrand(guide, selectedBrandFilter)) return false;
526
527 if (activeSubTopic !== "all" && currentSubTopicDef && currentSubTopicDef.keywords.length > 0) {
528 const titleLower = guide.title.toLowerCase();
529 const descLower = guide.description.toLowerCase();
530 const matchesSub = currentSubTopicDef.keywords.some(k =>
531 titleLower.includes(k.toLowerCase()) || descLower.includes(k.toLowerCase())
532 );
533 if (!matchesSub) return false;
534 }
535
536 if (q) {
537 const matchTitle = guide.title.toLowerCase().includes(q);
538 const matchDesc = guide.description.toLowerCase().includes(q);
539 const matchTools = guide.toolsRequired && guide.toolsRequired.some(t => t.toLowerCase().includes(q));
540 const matchSteps = guide.steps && guide.steps.some(s => s.title.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
541 if (!matchTitle && !matchDesc && !matchTools && !matchSteps) return false;
542 }
543
544 return true;
545 });
546
547 filtered.sort((a, b) => {
548 switch (sortOption) {
549 case "difficulty": {
550 const order = { easy: 1, medium: 2, hard: 3 };
551 return (order[a.difficulty as keyof typeof order] || 2) - (order[b.difficulty as keyof typeof order] || 2);
552 }
553 case "time":
554 return a.estimatedTime.localeCompare(b.estimatedTime);
555 case "popular":
556 default:
557 return (b.viewCount || 0) - (a.viewCount || 0);
558 }
559 });
560
561 map[catId] = filtered;
562 }
563
564 return map;
565 }, [allGuides, selectedDeviceType, selectedDifficulty, selectedBrandFilter, selectedSubTopics, searchQuery, sortOption]);
566
567 const getGuidesForCategory = (categoryId: string) => {
568 return categoryGuidesMap[categoryId.toLowerCase()] || [];
569 };
570
571 const displayedCategories = selectedCategory && selectedCategory !== "downloads"
572 ? CATEGORY_DEFINITIONS.filter((c) => c.id === selectedCategory)
573 : selectedCategory === "downloads"
574 ? []
575 : CATEGORY_DEFINITIONS;
576
577 const isFocusedHub = Boolean(selectedCategory && selectedCategory !== "downloads");
578
579 return (
580 <div className="space-y-8">
581
582 {/* Category Navigation Dock */}
583 <div className="space-y-3">
584 <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
585 <div className="flex items-center gap-2.5 flex-wrap">
586 <h2 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">
587 Core Repair Hubs
588 </h2>
589 <Badge variant="secondary" className="text-[11px] font-semibold bg-primary/10 text-primary border border-primary/20">
590 {allGuides.length || 624} Verified Guides • 8 Schematics • {ALL_DOWNLOADS.length} Tools
591 </Badge>
592 </div>
593
594 <div className="flex items-center gap-2 text-xs text-muted-foreground">
595 <span>
596 {selectedCategory
597 ? `Viewing ${selectedCategory.toUpperCase()} Hub`
598 : "Organized Across 4 Core Technical Disciplines"}
599 </span>
600 </div>
601 </div>
602
603 {/* Streamlined Category Tab Dock */}
604 <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-2 sm:gap-2.5">
605 {/* All Sections Tab */}
606 <button
607 onClick={() => onCategoryChange("")}
608 className={cn(
609 "flex items-center gap-2.5 p-3 rounded-2xl border text-left transition-all duration-200 cursor-pointer shadow-2xs",
610 !selectedCategory
611 ? "bg-primary text-primary-foreground border-primary shadow-sm ring-2 ring-primary/20"
612 : "bg-card/70 hover:bg-card/95 border-border/50 hover:border-primary/40 text-foreground"
613 )}
614 >
615 <div className={cn(
616 "h-8 w-8 rounded-xl flex items-center justify-center shrink-0",
617 !selectedCategory ? "bg-white/20 text-white" : "bg-primary/10 text-primary"
618 )}>
619 <LayoutGrid className="h-4 w-4" />
620 </div>
621 <div className="min-w-0">
622 <span className="text-xs font-bold block truncate">All Hubs</span>
623 <span className={cn("text-[10px] block truncate", !selectedCategory ? "text-white/80" : "text-muted-foreground")}>
624 {allGuides.length || 624} Guides
625 </span>
626 </div>
627 </button>
628
629 {/* 4 Main Categories Tabs */}
630 {CATEGORY_DEFINITIONS.map((cat) => {
631 const isSelected = selectedCategory === cat.id;
632 const Icon = cat.icon;
633 const count = getCategoryTotalCount(cat.id);
634
635 return (
636 <button
637 key={cat.id}
638 onClick={() => onCategoryChange(isSelected ? "" : cat.id)}
639 className={cn(
640 "flex items-center gap-2.5 p-3 rounded-2xl border text-left transition-all duration-200 cursor-pointer shadow-2xs",
641 isSelected
642 ? "bg-primary text-primary-foreground border-primary shadow-sm ring-2 ring-primary/20"
643 : "bg-card/70 hover:bg-card/95 border-border/50 hover:border-primary/40 text-foreground"
644 )}
645 >
646 <div className={cn(
647 "h-8 w-8 rounded-xl flex items-center justify-center shrink-0",
648 isSelected ? "bg-white/20 text-white" : `${cat.bgColor} ${cat.color}`
649 )}>
650 <Icon className="h-4 w-4" />
651 </div>
652 <div className="min-w-0">
653 <span className="text-xs font-bold block truncate">{cat.shortName} Hub</span>
654 <span className={cn("text-[10px] block truncate", isSelected ? "text-white/80" : "text-muted-foreground")}>
655 {count} Guides
656 </span>
657 </div>
658 </button>
659 );
660 })}
661
662 {/* 5th Section: Essential Technician Software & Download Hub */}
663 <button
664 onClick={() => onCategoryChange(selectedCategory === "downloads" ? "" : "downloads")}
665 className={cn(
666 "flex items-center gap-2.5 p-3 rounded-2xl border text-left transition-all duration-200 cursor-pointer shadow-2xs",
667 selectedCategory === "downloads"
668 ? "bg-emerald-600 text-white border-emerald-600 shadow-sm ring-2 ring-emerald-500/30"
669 : "bg-card/70 hover:bg-card/95 border-emerald-500/30 hover:border-emerald-500/60 text-foreground"
670 )}
671 >
672 <div className={cn(
673 "h-8 w-8 rounded-xl flex items-center justify-center shrink-0",
674 selectedCategory === "downloads"
675 ? "bg-white/20 text-white"
676 : "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
677 )}>
678 <Download className="h-4 w-4" />
679 </div>
680 <div className="min-w-0">
681 <span className="text-xs font-bold block truncate">Software Hub</span>
682 <span className={cn("text-[10px] block truncate", selectedCategory === "downloads" ? "text-white/80" : "text-emerald-600 dark:text-emerald-400 font-semibold")}>
683 {ALL_DOWNLOADS.length} Utilities
684 </span>
685 </div>
686 </button>
687 </div>
688 </div>
689
690 {/* Brand & Hardware OEM Filter Section */}
691 {selectedCategory !== "downloads" && (
692 <div className="rounded-2xl bg-card/70 backdrop-blur-md border border-border/50 p-3.5 sm:p-4 space-y-3 shadow-2xs">
693 <div className="flex flex-wrap items-center justify-between gap-2 border-b border-border/40 pb-2.5">
694 <div className="flex items-center gap-2">
695 <div className="h-7 w-7 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0">
696 <SlidersHorizontal className="h-3.5 w-3.5" />
697 </div>
698 <div>
699 <h3 className="text-xs sm:text-sm font-bold text-foreground flex items-center gap-1.5">
700 Brand & OEM Platform Filters
701 </h3>
702 <p className="text-[11px] text-muted-foreground hidden sm:block">
703 Isolate verified teardowns, schematics, and BIOS/component guides by system manufacturer
704 </p>
705 </div>
706 </div>
707
708 <div className="flex items-center gap-2">
709 {selectedBrandFilter !== "all" ? (
710 <button
711 type="button"
712 onClick={() => {
713 setSelectedBrandFilter("all");
714 setCategoryPages({
715 hardware: 1,
716 software: 1,
717 cleaning: 1,
718 upgrades: 1,
719 });
720 }}
721 className="px-2.5 py-1 rounded-xl text-xs font-bold text-destructive hover:bg-destructive/10 border border-destructive/30 flex items-center gap-1.5 cursor-pointer transition-colors bg-destructive/5 shadow-2xs"
722 >
723 <X className="h-3.5 w-3.5" />
724 <span>Clear Brand ({selectedBrandFilter})</span>
725 </button>
726 ) : (
727 <span className="text-[11px] font-medium text-muted-foreground bg-muted/50 px-2.5 py-0.5 rounded-full border border-border/30">
728 Showing All {allGuides.length || 624} Hardware Systems
729 </span>
730 )}
731 </div>
732 </div>
733
734 {/* Brand Buttons - Naturally Wrapped Without Slider/Scroll Bar */}
735 <div className="flex flex-wrap items-center gap-1.5 sm:gap-2 pt-0.5">
736 {availableBrands.map((b) => {
737 const isSelected = selectedBrandFilter === b;
738 const brandCount = getBrandGuideCount(b);
739
740 return (
741 <button
742 key={b}
743 type="button"
744 onClick={() => {
745 const nextBrand = isSelected ? "all" : b;
746 setSelectedBrandFilter(nextBrand);
747 setCategoryPages({
748 hardware: 1,
749 software: 1,
750 cleaning: 1,
751 upgrades: 1,
752 });
753 }}
754 className={cn(
755 "px-3 py-1.5 rounded-xl text-xs font-semibold transition-all duration-150 cursor-pointer border flex items-center gap-2 select-none",
756 isSelected
757 ? "bg-primary text-primary-foreground border-primary shadow-xs ring-2 ring-primary/25 font-bold scale-[1.02]"
758 : "bg-background/80 hover:bg-background border-border/60 hover:border-primary/40 text-muted-foreground hover:text-foreground"
759 )}
760 >
761 {isSelected && <Check className="h-3.5 w-3.5 shrink-0 text-white" />}
762 <span>{b === "all" ? "All Brands" : b}</span>
763 <span
764 className={cn(
765 "text-[10px] px-1.5 py-0.5 rounded-full font-bold transition-colors",
766 isSelected ? "bg-white/20 text-white" : "bg-muted text-muted-foreground"
767 )}
768 >
769 {brandCount}
770 </span>
771 </button>
772 );
773 })}
774 </div>
775 </div>
776 )}
777
778 {/* Unified Filter Toolbar */}
779 {selectedCategory !== "downloads" && (
780 <div className="rounded-2xl bg-card/60 backdrop-blur-md border border-border/50 p-3 flex flex-wrap items-center justify-between gap-3 shadow-2xs">
781 {/* Left: View Format & Difficulty */}
782 <div className="flex flex-wrap items-center gap-2.5">
783 {/* View Format Selector */}
784 <div className="flex items-center p-0.5 rounded-xl bg-muted/60 border border-border/40 text-xs">
785 <button
786 onClick={() => setViewFormat("all")}
787 className={cn(
788 "px-3 py-1 rounded-lg font-semibold transition-all cursor-pointer",
789 viewFormat === "all" ? "bg-background text-foreground shadow-xs" : "text-muted-foreground hover:text-foreground"
790 )}
791 >
792 All Resources
793 </button>
794 <button
795 onClick={() => setViewFormat("guides")}
796 className={cn(
797 "px-3 py-1 rounded-lg font-semibold transition-all cursor-pointer",
798 viewFormat === "guides" ? "bg-background text-foreground shadow-xs" : "text-muted-foreground hover:text-foreground"
799 )}
800 >
801 Guides Only
802 </button>
803 <button
804 onClick={() => setViewFormat("manuals")}
805 className={cn(
806 "px-3 py-1 rounded-lg font-semibold transition-all cursor-pointer",
807 viewFormat === "manuals" ? "bg-background text-foreground shadow-xs" : "text-muted-foreground hover:text-foreground"
808 )}
809 >
810 Manuals Only
811 </button>
812 </div>
813
814 {/* Difficulty Pills */}
815 <div className="hidden sm:flex items-center gap-1 pl-2 border-l border-border/40">
816 <span className="text-xs font-semibold text-muted-foreground mr-1">Difficulty:</span>
817 {["all", "easy", "medium", "hard"].map((diff) => (
818 <button
819 key={diff}
820 onClick={() => onDifficultyChange(diff)}
821 className={cn(
822 "px-2.5 py-0.5 rounded-lg text-xs font-semibold capitalize transition-all cursor-pointer",
823 selectedDifficulty === diff
824 ? "bg-primary/10 text-primary border border-primary/20 font-bold"
825 : "text-muted-foreground hover:text-foreground"
826 )}
827 >
828 {diff}
829 </button>
830 ))}
831 </div>
832
833 {/* Per-Page Selector when in Focused Hub */}
834 {isFocusedHub && (
835 <div className="hidden md:flex items-center gap-1.5 pl-2 border-l border-border/40 text-xs text-muted-foreground">
836 <span>Per page:</span>
837 {[12, 24, 48].map((size) => (
838 <button
839 key={size}
840 onClick={() => setPageSize(size)}
841 className={cn(
842 "px-2 py-0.5 rounded-md font-semibold cursor-pointer transition-colors",
843 pageSize === size ? "bg-primary/15 text-primary font-bold" : "hover:text-foreground"
844 )}
845 >
846 {size}
847 </button>
848 ))}
849 </div>
850 )}
851 </div>
852
853 {/* Right: Sort & Reset */}
854 <div className="flex items-center gap-2 ml-auto">
855 <span className="text-xs font-semibold text-muted-foreground hidden sm:inline">Sort:</span>
856 <Select value={sortOption} onValueChange={onSortChange}>
857 <SelectTrigger className="w-36 h-8 rounded-xl border-border/60 bg-background text-xs font-semibold">
858 <SelectValue />
859 </SelectTrigger>
860 <SelectContent className="rounded-xl border-border/50">
861 <SelectItem value="popular">Most Popular</SelectItem>
862 <SelectItem value="difficulty">Difficulty Level</SelectItem>
863 <SelectItem value="time">Fastest Time</SelectItem>
864 </SelectContent>
865 </Select>
866
867 {(selectedCategory || selectedDifficulty !== "all" || selectedBrandFilter !== "all" || searchQuery || viewFormat !== "all") && (
868 <Button
869 variant="ghost"
870 size="sm"
871 onClick={() => {
872 onCategoryChange("");
873 onDifficultyChange("all");
874 setSelectedBrandFilter("all");
875 onSearchChange("");
876 setViewFormat("all");
877 setSelectedSubTopics({
878 hardware: "all",
879 software: "all",
880 cleaning: "all",
881 upgrades: "all",
882 });
883 }}
884 className="h-8 rounded-xl text-xs text-muted-foreground hover:text-foreground gap-1 px-2.5 cursor-pointer"
885 >
886 <RotateCcw className="h-3.5 w-3.5" /> Reset Filters
887 </Button>
888 )}
889 </div>
890 </div>
891 )}
892
893 {/* Main Core Repair Hubs Section */}
894 <div className="space-y-8">
895 {displayedCategories.map((cat) => {
896 const categoryGuides = getGuidesForCategory(cat.id);
897 const totalCategoryGuides = getCategoryTotalCount(cat.id);
898 const Icon = cat.icon;
899 const subTopics = CATEGORY_SUBTOPICS[cat.id] || [];
900 const activeSubTopic = selectedSubTopics[cat.id] || "all";
901
902 // Calculate Safe Pagination
903 const effectivePageSize = isFocusedHub ? pageSize : 12;
904 const totalPages = Math.max(1, Math.ceil(categoryGuides.length / effectivePageSize));
905 const currentPage = Math.min(Math.max(1, categoryPages[cat.id] || 1), totalPages);
906 const startIndex = (currentPage - 1) * effectivePageSize;
907 const displayedGuides = categoryGuides.slice(startIndex, startIndex + effectivePageSize);
908
909 return (
910 <section
911 key={cat.id}
912 id={`hub-${cat.id}`}
913 className={cn(
914 "rounded-3xl border bg-card/60 backdrop-blur-xl p-5 sm:p-7 space-y-6 shadow-xs relative overflow-hidden transition-all",
915 cat.borderColor
916 )}
917 >
918 {/* Subtle background category gradient */}
919 <div
920 className={cn(
921 "absolute top-0 right-0 w-80 h-80 bg-gradient-to-bl rounded-full blur-3xl opacity-20 pointer-events-none -z-0",
922 cat.accentBg
923 )}
924 />
925
926 {/* Category Header */}
927 <div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-3 border-b border-border/40 pb-4">
928 <div className="flex items-start gap-3.5">
929 <div className={cn("h-12 w-12 rounded-2xl flex items-center justify-center shrink-0 shadow-sm", cat.bgColor, cat.color)}>
930 <Icon className="h-6 w-6" />
931 </div>
932 <div>
933 <div className="flex items-center gap-2 mb-1 flex-wrap">
934 <span className={cn("inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider border", cat.bgColor, cat.textColor, cat.borderColor)}>
935 {cat.badgeTitle}
936 </span>
937 <span className="text-[11px] font-bold text-foreground">
938 {totalCategoryGuides} Dedicated Guides in Hub
939 </span>
940 <span className="text-[11px] text-muted-foreground">• {cat.manuals.length} Official Schematics</span>
941 </div>
942 <h3 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">
943 {cat.name}
944 </h3>
945 <p className="text-xs sm:text-sm text-muted-foreground max-w-3xl leading-relaxed mt-0.5">
946 {isFocusedHub ? cat.longDescription : cat.description}
947 </p>
948 </div>
949 </div>
950
951 {/* Hub Navigation Button */}
952 <div className="flex items-center gap-2 self-start md:self-auto shrink-0">
953 {selectedCategory !== cat.id ? (
954 <Button
955 variant="outline"
956 size="sm"
957 onClick={() => {
958 onCategoryChange(cat.id);
959 window.scrollTo({ top: 0, behavior: "smooth" });
960 }}
961 className="rounded-xl text-xs font-bold gap-1.5 h-9 border-border/60 hover:border-primary/40 hover:bg-primary/5 cursor-pointer shadow-2xs"
962 >
963 <span>Enter {cat.shortName} Hub ({totalCategoryGuides})</span>
964 <ArrowRight className="h-3.5 w-3.5 text-primary" />
965 </Button>
966 ) : (
967 <Button
968 variant="ghost"
969 size="sm"
970 onClick={() => onCategoryChange("")}
971 className="rounded-xl text-xs font-bold gap-1.5 h-9 text-muted-foreground hover:text-foreground cursor-pointer"
972 >
973 <LayoutGrid className="h-3.5 w-3.5" />
974 <span>View All 4 Hubs</span>
975 </Button>
976 )}
977 </div>
978 </div>
979
980 {/* Sub-Topic Component Filter Chips */}
981 <div className="relative z-10 space-y-2">
982 <div className="flex items-center justify-between text-xs">
983 <span className="font-bold text-muted-foreground uppercase tracking-wider text-[10px] flex items-center gap-1">
984 <SlidersHorizontal className="h-3 w-3 text-primary" />
985 {cat.shortName} Sub-Topics & Diagnostic Focus:
986 </span>
987 <span className="text-[11px] text-muted-foreground">
988 {categoryGuides.length} guides match active filters
989 </span>
990 </div>
991
992 <div className="flex flex-wrap items-center gap-1.5 pt-0.5">
993 {subTopics.map((st) => (
994 <button
995 key={st.id}
996 onClick={() => {
997 setSelectedSubTopics(prev => ({ ...prev, [cat.id]: st.id }));
998 setCategoryPages(prev => ({ ...prev, [cat.id]: 1 }));
999 }}
1000 className={cn(
1001 "px-3 py-1.5 rounded-xl text-xs font-semibold shrink-0 transition-all cursor-pointer border flex items-center gap-1.5",
1002 activeSubTopic === st.id
1003 ? "bg-primary text-primary-foreground border-primary shadow-xs font-bold"
1004 : "bg-background/80 hover:bg-background border-border/50 text-muted-foreground hover:text-foreground"
1005 )}
1006 >
1007 <span>{st.name}</span>
1008 </button>
1009 ))}
1010 </div>
1011 </div>
1012
1013 {/* Official Technical Manuals Shelf */}
1014 {(viewFormat === "all" || viewFormat === "manuals") && (
1015 <div className="relative z-10 space-y-2.5 pt-1">
1016 <div className="flex items-center justify-between">
1017 <h4 className="text-xs font-bold uppercase tracking-wider text-foreground flex items-center gap-1.5">
1018 <FileText className={cn("h-3.5 w-3.5", cat.color)} />
1019 Official {cat.shortName} Technical Manuals & Schematics ({cat.manuals.length})
1020 </h4>
1021 <span className="text-[10px] text-muted-foreground font-medium">
1022 Engineering Specifications & Torque Limits
1023 </span>
1024 </div>
1025
1026 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1027 {cat.manuals.map((manual) => (
1028 <div
1029 key={manual.id}
1030 onClick={() => setSelectedManual(manual)}
1031 className="rounded-2xl border border-border/60 bg-background/80 hover:bg-background hover:border-primary/40 p-3.5 transition-all flex flex-col justify-between gap-2.5 cursor-pointer group shadow-2xs"
1032 >
1033 <div className="space-y-1">
1034 <div className="flex items-center justify-between gap-2">
1035 <span className="text-[10px] font-mono font-bold px-2 py-0.5 rounded-md bg-muted text-muted-foreground">
1036 {manual.code}
1037 </span>
1038 <span className="text-[10px] font-semibold text-muted-foreground">
1039 {manual.pages} Pages • {manual.fileSize}
1040 </span>
1041 </div>
1042 <h5 className="text-xs sm:text-sm font-bold text-foreground group-hover:text-primary transition-colors leading-snug line-clamp-1">
1043 {manual.title}
1044 </h5>
1045 <p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
1046 {manual.summary}
1047 </p>
1048 </div>
1049
1050 <div className="flex items-center justify-between pt-2 border-t border-border/40 text-[11px]">
1051 <span className="text-muted-foreground font-medium flex items-center gap-1 text-[10px]">
1052 <Shield className="h-3 w-3 text-emerald-500" />
1053 <span>{manual.author}</span>
1054 </span>
1055 <span className="font-bold text-primary flex items-center gap-1 group-hover:underline text-xs">
1056 <span>Open Manual</span>
1057 <ChevronRight className="h-3 w-3 transition-transform group-hover:translate-x-0.5" />
1058 </span>
1059 </div>
1060 </div>
1061 ))}
1062 </div>
1063 </div>
1064 )}
1065
1066 {/* Repair Guides Grid with Seamless Integration */}
1067 {(viewFormat === "all" || viewFormat === "guides") && (
1068 <div className="relative z-10 space-y-4 pt-1">
1069 <div className="flex items-center justify-between flex-wrap gap-2">
1070 <h4 className="text-xs font-bold uppercase tracking-wider text-foreground flex items-center gap-1.5">
1071 <Wrench className={cn("h-3.5 w-3.5", cat.color)} />
1072 {cat.shortName} Step-by-Step Technical Guides ({categoryGuides.length})
1073 </h4>
1074 <div className="flex items-center gap-2">
1075 <span className="text-[11px] text-muted-foreground font-medium">
1076 Showing {startIndex + 1}–{Math.min(startIndex + effectivePageSize, categoryGuides.length)} of {categoryGuides.length}
1077 </span>
1078 </div>
1079 </div>
1080
1081 {isLoading ? (
1082 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1083 {[...Array(6)].map((_, i) => (
1084 <div key={i} className="h-64 rounded-3xl bg-muted/40 animate-pulse border border-border/30" />
1085 ))}
1086 </div>
1087 ) : categoryGuides.length > 0 ? (
1088 <>
1089 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1090 {displayedGuides.map((guide) => (
1091 <GuideCard
1092 key={guide.id}
1093 guide={guide}
1094 onViewGuide={() => onViewGuide(guide.id)}
1095 />
1096 ))}
1097 </div>
1098
1099 {/* Pagination Controls */}
1100 {totalPages > 1 && (
1101 <div className="pt-4 flex flex-col sm:flex-row items-center justify-between gap-3 border-t border-border/40">
1102 <div className="text-xs text-muted-foreground font-medium">
1103 Page <span className="font-bold text-foreground">{currentPage}</span> of <span className="font-bold text-foreground">{totalPages}</span> ({categoryGuides.length} total guides)
1104 </div>
1105
1106 <div className="flex items-center gap-1.5">
1107 <Button
1108 variant="outline"
1109 size="sm"
1110 disabled={currentPage <= 1}
1111 onClick={() => {
1112 setCategoryPages(prev => ({ ...prev, [cat.id]: 1 }));
1113 const el = document.getElementById(`hub-${cat.id}`);
1114 if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
1115 }}
1116 className="h-8 w-8 p-0 rounded-xl"
1117 >
1118 <ChevronsLeft className="h-4 w-4" />
1119 </Button>
1120
1121 <Button
1122 variant="outline"
1123 size="sm"
1124 disabled={currentPage <= 1}
1125 onClick={() => {
1126 setCategoryPages(prev => ({ ...prev, [cat.id]: Math.max(1, currentPage - 1) }));
1127 const el = document.getElementById(`hub-${cat.id}`);
1128 if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
1129 }}
1130 className="h-8 px-2.5 rounded-xl text-xs gap-1"
1131 >
1132 <ChevronLeft className="h-3.5 w-3.5" />
1133 <span className="hidden sm:inline">Prev</span>
1134 </Button>
1135
1136 {/* Numbered Page Buttons */}
1137 <div className="flex items-center gap-1">
1138 {Array.from({ length: totalPages }, (_, i) => i + 1)
1139 .filter(p => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 1)
1140 .map((pageNum, idx, arr) => {
1141 const showEllipsis = idx > 0 && pageNum - arr[idx - 1] > 1;
1142 return (
1143 <div key={pageNum} className="flex items-center">
1144 {showEllipsis && <span className="px-1 text-xs text-muted-foreground">...</span>}
1145 <Button
1146 variant={currentPage === pageNum ? "default" : "outline"}
1147 size="sm"
1148 onClick={() => {
1149 setCategoryPages(prev => ({ ...prev, [cat.id]: pageNum }));
1150 const el = document.getElementById(`hub-${cat.id}`);
1151 if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
1152 }}
1153 className={cn(
1154 "h-8 w-8 p-0 rounded-xl text-xs font-bold",
1155 currentPage === pageNum ? "shadow-xs" : "border-border/50"
1156 )}
1157 >
1158 {pageNum}
1159 </Button>
1160 </div>
1161 );
1162 })}
1163 </div>
1164
1165 <Button
1166 variant="outline"
1167 size="sm"
1168 disabled={currentPage >= totalPages}
1169 onClick={() => {
1170 setCategoryPages(prev => ({ ...prev, [cat.id]: Math.min(totalPages, currentPage + 1) }));
1171 const el = document.getElementById(`hub-${cat.id}`);
1172 if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
1173 }}
1174 className="h-8 px-2.5 rounded-xl text-xs gap-1"
1175 >
1176 <span className="hidden sm:inline">Next</span>
1177 <ChevronRight className="h-3.5 w-3.5" />
1178 </Button>
1179
1180 <Button
1181 variant="outline"
1182 size="sm"
1183 disabled={currentPage >= totalPages}
1184 onClick={() => {
1185 setCategoryPages(prev => ({ ...prev, [cat.id]: totalPages }));
1186 const el = document.getElementById(`hub-${cat.id}`);
1187 if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
1188 }}
1189 className="h-8 w-8 p-0 rounded-xl"
1190 >
1191 <ChevronsRight className="h-4 w-4" />
1192 </Button>
1193 </div>
1194 </div>
1195 )}
1196 </>
1197 ) : (
1198 <div className="rounded-2xl border border-dashed border-border/60 bg-muted/20 p-6 text-center space-y-3">
1199 <p className="text-xs font-semibold text-foreground">
1200 No {cat.shortName} repair guides match your current filter criteria.
1201 </p>
1202 <div className="flex flex-wrap items-center justify-center gap-2">
1203 {selectedBrandFilter !== "all" && (
1204 <Button
1205 variant="outline"
1206 size="sm"
1207 onClick={() => {
1208 setSelectedBrandFilter("all");
1209 setCategoryPages(prev => ({ ...prev, [cat.id]: 1 }));
1210 }}
1211 className="h-7 text-xs rounded-xl border-border/60"
1212 >
1213 Reset Brand Filter ({selectedBrandFilter})
1214 </Button>
1215 )}
1216 {activeSubTopic !== "all" && (
1217 <Button
1218 variant="outline"
1219 size="sm"
1220 onClick={() => {
1221 setSelectedSubTopics(prev => ({ ...prev, [cat.id]: "all" }));
1222 setCategoryPages(prev => ({ ...prev, [cat.id]: 1 }));
1223 }}
1224 className="h-7 text-xs rounded-xl border-border/60"
1225 >
1226 Reset Sub-Topic
1227 </Button>
1228 )}
1229 {searchQuery && (
1230 <Button
1231 variant="outline"
1232 size="sm"
1233 onClick={() => {
1234 onSearchChange("");
1235 setCategoryPages(prev => ({ ...prev, [cat.id]: 1 }));
1236 }}
1237 className="h-7 text-xs rounded-xl border-border/60"
1238 >
1239 Clear Search Query
1240 </Button>
1241 )}
1242 </div>
1243 </div>
1244 )}
1245 </div>
1246 )}
1247 </section>
1248 );
1249 })}
1250
1251 {/* Section 5: Essential Technician Software & Download Hub */}
1252 {(!selectedCategory || selectedCategory === "downloads") && (
1253 <TechnicianDownloadHub />
1254 )}
1255 </div>
1256
1257 {/* Technical Manual Detail Modal */}
1258 {selectedManual && (
1259 <ManualDetailModal
1260 manual={selectedManual}
1261 onClose={() => setSelectedManual(null)}
1262 />
1263 )}
1264 </div>
1265 );
1266 }