|
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
|
} |