1 import { useState, useEffect } from "react";
2 import { useQuery } from "@tanstack/react-query";
3 import Header from "@/components/layout/header";
4 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5 import { Button } from "@/components/ui/button";
6 import { Badge } from "@/components/ui/badge";
7 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
8 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
9 import { Input } from "@/components/ui/input";
10 import { Progress } from "@/components/ui/progress";
11 import { useToast } from "@/hooks/use-toast";
12 import {
13 Download,
14 FileText,
15 Video,
16 HardDrive,
17 Search,
18 Filter,
19 Package,
20 ExternalLink,
21 Archive,
22 Trash2,
23 CheckCircle,
24 AlertCircle,
25 Smartphone
26 } from "lucide-react";
27 import { downloadGuide, downloadGuidePackage, getEstimatedSize, type DownloadOptions } from "@/lib/download-utils";
28 import type { RepairGuide, TroubleshootingFlow, DeviceComponent } from "@shared/schema";
29
30 interface DownloadableItem {
31 id: string;
32 title: string;
33 type: 'guide' | 'troubleshooting' | 'component' | 'package';
34 size: string;
35 format: string;
36 description?: string;
37 category?: string;
38 difficulty?: string;
39 estimatedTime?: string;
40 deviceType?: string;
41 data?: any;
42 }
43
44 export default function Downloads() {
45 const [searchQuery, setSearchQuery] = useState("");
46 const [selectedCategory, setSelectedCategory] = useState("all");
47 const [selectedDifficulty, setSelectedDifficulty] = useState("all");
48 const [selectedDeviceType, setSelectedDeviceType] = useState("all");
49 const [selectedFormat, setSelectedFormat] = useState("html");
50 const [downloadProgress, setDownloadProgress] = useState<{ [key: string]: number }>({});
51 const [downloadedItems, setDownloadedItems] = useState<string[]>([]);
52 const { toast } = useToast();
53
54 const { data: repairGuides = [], isLoading: guidesLoading } = useQuery<RepairGuide[]>({
55 queryKey: ["/api/repair-guides"],
56 });
57
58 const { data: troubleshootingFlows = [], isLoading: flowsLoading } = useQuery<TroubleshootingFlow[]>({
59 queryKey: ["/api/troubleshooting-flows"],
60 });
61
62 const { data: deviceComponents = [], isLoading: componentsLoading } = useQuery<DeviceComponent[]>({
63 queryKey: ["/api/device-components"],
64 });
65
66 // Convert data to downloadable items
67 const downloadableItems: DownloadableItem[] = [
68 ...repairGuides.map(guide => ({
69 id: guide.id,
70 title: guide.title,
71 type: 'guide' as const,
72 size: getEstimatedSize(guide, { format: selectedFormat as any }),
73 format: selectedFormat.toUpperCase(),
74 description: guide.description,
75 category: guide.category,
76 difficulty: guide.difficulty,
77 estimatedTime: guide.estimatedTime,
78 deviceType: guide.deviceType,
79 data: guide
80 })),
81 ...troubleshootingFlows.map(flow => ({
82 id: flow.id,
83 title: flow.title,
84 type: 'troubleshooting' as const,
85 size: '5-15 KB',
86 format: 'JSON',
87 description: flow.description,
88 category: flow.type,
89 data: flow
90 })),
91 ...deviceComponents.map(component => ({
92 id: component.id,
93 title: component.name,
94 type: 'component' as const,
95 size: '2-8 KB',
96 format: 'JSON',
97 description: component.description,
98 category: component.category,
99 deviceType: component.deviceType,
100 data: component
101 }))
102 ];
103
104 // Filter downloadable items
105 const filteredItems = downloadableItems.filter(item => {
106 const matchesSearch = item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
107 item.description?.toLowerCase().includes(searchQuery.toLowerCase());
108 const matchesCategory = selectedCategory === "all" || item.category === selectedCategory;
109 const matchesDifficulty = selectedDifficulty === "all" || item.difficulty === selectedDifficulty;
110 const matchesDeviceType = selectedDeviceType === "all" || item.deviceType === selectedDeviceType;
111
112 return matchesSearch && matchesCategory && matchesDifficulty && matchesDeviceType;
113 });
114
115 // Handle individual download
116 const handleDownload = async (item: DownloadableItem) => {
117 try {
118 setDownloadProgress(prev => ({ ...prev, [item.id]: 0 }));
119
120 // Simulate progress
121 const progressInterval = setInterval(() => {
122 setDownloadProgress(prev => {
123 const current = prev[item.id] || 0;
124 if (current >= 100) {
125 clearInterval(progressInterval);
126 return prev;
127 }
128 return { ...prev, [item.id]: current + 10 };
129 });
130 }, 100);
131
132 let success = false;
133
134 if (item.type === 'guide') {
135 const options: DownloadOptions = {
136 format: selectedFormat as any,
137 includeImages: true
138 };
139 success = await downloadGuide(item.data, options);
140 } else {
141 // For troubleshooting and components, download as JSON
142 const content = JSON.stringify(item.data, null, 2);
143 const blob = new Blob([content], { type: 'application/json' });
144 const url = URL.createObjectURL(blob);
145
146 const link = document.createElement('a');
147 link.href = url;
148 link.download = `${item.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`;
149 document.body.appendChild(link);
150 link.click();
151 document.body.removeChild(link);
152 URL.revokeObjectURL(url);
153 success = true;
154 }
155
156 clearInterval(progressInterval);
157 setDownloadProgress(prev => ({ ...prev, [item.id]: 100 }));
158
159 if (success) {
160 setDownloadedItems(prev => [...prev, item.id]);
161 toast({
162 title: "Download Complete",
163 description: `${item.title} has been downloaded successfully.`,
164 });
165 } else {
166 throw new Error("Download failed");
167 }
168
169 // Clear progress after 2 seconds
170 setTimeout(() => {
171 setDownloadProgress(prev => {
172 const { [item.id]: _, ...rest } = prev;
173 return rest;
174 });
175 }, 2000);
176
177 } catch (error) {
178 console.error('Download error:', error);
179 setDownloadProgress(prev => {
180 const { [item.id]: _, ...rest } = prev;
181 return rest;
182 });
183 toast({
184 title: "Download Failed",
185 description: `Failed to download ${item.title}. Please try again.`,
186 variant: "destructive",
187 });
188 }
189 };
190
191 // Handle package download
192 const handlePackageDownload = async (category?: string, deviceType?: string) => {
193 try {
194 let guidesToDownload = repairGuides;
195
196 if (category && category !== "all") {
197 guidesToDownload = guidesToDownload.filter(guide => guide.category === category);
198 }
199
200 if (deviceType && deviceType !== "all") {
201 guidesToDownload = guidesToDownload.filter(guide => guide.deviceType === deviceType);
202 }
203
204 if (guidesToDownload.length === 0) {
205 toast({
206 title: "No Guides Found",
207 description: "No guides match the selected criteria.",
208 variant: "destructive",
209 });
210 return;
211 }
212
213 const packageName = category && category !== "all"
214 ? `${category}_guides_${deviceType || 'all'}`
215 : `all_repair_guides`;
216
217 const success = await downloadGuidePackage(guidesToDownload, packageName);
218
219 if (success) {
220 toast({
221 title: "Package Download Complete",
222 description: `Downloaded ${guidesToDownload.length} guides successfully.`,
223 });
224 }
225 } catch (error) {
226 console.error('Package download error:', error);
227 toast({
228 title: "Package Download Failed",
229 description: "Failed to download guide package. Please try again.",
230 variant: "destructive",
231 });
232 }
233 };
234
235 // Handle complete export
236 const handleCompleteExport = async () => {
237 try {
238 const bundle = {
239 exportInfo: {
240 title: 'JCR Guide Pro Complete Export',
241 version: '1.0',
242 exportedAt: new Date().toISOString(),
243 totalItems: repairGuides.length + troubleshootingFlows.length + deviceComponents.length
244 },
245 repairGuides: repairGuides,
246 troubleshootingFlows: troubleshootingFlows,
247 deviceComponents: deviceComponents
248 };
249
250 const content = JSON.stringify(bundle, null, 2);
251 const blob = new Blob([content], { type: 'application/json' });
252 const url = URL.createObjectURL(blob);
253
254 const link = document.createElement('a');
255 link.href = url;
256 link.download = `jcr_guide_pro_complete_export_${new Date().toISOString().split('T')[0]}.json`;
257 document.body.appendChild(link);
258 link.click();
259 document.body.removeChild(link);
260 URL.revokeObjectURL(url);
261
262 toast({
263 title: "Complete Export Successful",
264 description: `Exported ${bundle.exportInfo.totalItems} items to device storage.`,
265 });
266 } catch (error) {
267 console.error('Complete export error:', error);
268 toast({
269 title: "Export Failed",
270 description: "Failed to create complete export. Please try again.",
271 variant: "destructive",
272 });
273 }
274 };
275
276 const getTypeIcon = (type: string) => {
277 switch (type) {
278 case 'guide': return <FileText className="h-4 w-4" />;
279 case 'troubleshooting': return <AlertCircle className="h-4 w-4" />;
280 case 'component': return <HardDrive className="h-4 w-4" />;
281 default: return <FileText className="h-4 w-4" />;
282 }
283 };
284
285 const getCategoryColor = (category?: string) => {
286 switch (category) {
287 case 'hardware': return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
288 case 'software': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
289 case 'cleaning': return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300';
290 case 'upgrades': return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300';
291 case 'power': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
292 case 'performance': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
293 case 'display': return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300';
294 default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300';
295 }
296 };
297
298 const getDifficultyColor = (difficulty?: string) => {
299 switch (difficulty) {
300 case 'easy': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
301 case 'medium': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
302 case 'hard': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
303 default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300';
304 }
305 };
306
307 return (
308 <div className="min-h-screen bg-background">
309 <Header />
310
311 <main className="container mx-auto px-4 py-8">
312 <div className="max-w-6xl mx-auto">
313 {/* Header */}
314 <div className="mb-8">
315 <h1 className="text-3xl font-bold mb-4">Download Center</h1>
316 <p className="text-muted-foreground mb-6">
317 Download repair guides, troubleshooting flows, and component information for offline access
318 </p>
319
320 {/* Quick Export Options */}
321 <div className="flex flex-wrap gap-4 mb-6">
322 <Button onClick={handleCompleteExport} size="lg" className="bg-gradient-to-r from-blue-600 to-blue-700">
323 <Package className="mr-2 h-4 w-4" />
324 Export Everything
325 </Button>
326 <Button
327 onClick={() => handlePackageDownload("hardware")}
328 variant="outline"
329 >
330 <Download className="mr-2 h-4 w-4" />
331 Hardware Guides
332 </Button>
333 <Button
334 onClick={() => handlePackageDownload("software")}
335 variant="outline"
336 >
337 <Download className="mr-2 h-4 w-4" />
338 Software Guides
339 </Button>
340 <Button
341 onClick={() => handlePackageDownload("cleaning")}
342 variant="outline"
343 >
344 <Download className="mr-2 h-4 w-4" />
345 Cleaning Guides
346 </Button>
347 </div>
348 </div>
349
350 {/* Filters */}
351 <Card className="mb-6">
352 <CardHeader>
353 <CardTitle className="flex items-center gap-2">
354 <Filter className="h-5 w-5" />
355 Filter Downloads
356 </CardTitle>
357 </CardHeader>
358 <CardContent>
359 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
360 <div>
361 <label className="text-sm font-medium mb-2 block">Search</label>
362 <div className="relative">
363 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
364 <Input
365 placeholder="Search guides..."
366 value={searchQuery}
367 onChange={(e) => setSearchQuery(e.target.value)}
368 className="pl-10"
369 />
370 </div>
371 </div>
372
373 <div>
374 <label className="text-sm font-medium mb-2 block">Category</label>
375 <Select value={selectedCategory} onValueChange={setSelectedCategory}>
376 <SelectTrigger>
377 <SelectValue placeholder="All categories" />
378 </SelectTrigger>
379 <SelectContent>
380 <SelectItem value="all">All Categories</SelectItem>
381 <SelectItem value="hardware">Hardware</SelectItem>
382 <SelectItem value="software">Software</SelectItem>
383 <SelectItem value="cleaning">Cleaning</SelectItem>
384 <SelectItem value="upgrades">Upgrades</SelectItem>
385 <SelectItem value="power">Power Issues</SelectItem>
386 <SelectItem value="performance">Performance</SelectItem>
387 <SelectItem value="display">Display</SelectItem>
388 </SelectContent>
389 </Select>
390 </div>
391
392 <div>
393 <label className="text-sm font-medium mb-2 block">Difficulty</label>
394 <Select value={selectedDifficulty} onValueChange={setSelectedDifficulty}>
395 <SelectTrigger>
396 <SelectValue placeholder="All difficulties" />
397 </SelectTrigger>
398 <SelectContent>
399 <SelectItem value="all">All Levels</SelectItem>
400 <SelectItem value="easy">Easy</SelectItem>
401 <SelectItem value="medium">Medium</SelectItem>
402 <SelectItem value="hard">Hard</SelectItem>
403 </SelectContent>
404 </Select>
405 </div>
406
407 <div>
408 <label className="text-sm font-medium mb-2 block">Device Type</label>
409 <Select value={selectedDeviceType} onValueChange={setSelectedDeviceType}>
410 <SelectTrigger>
411 <SelectValue placeholder="All devices" />
412 </SelectTrigger>
413 <SelectContent>
414 <SelectItem value="all">All Devices</SelectItem>
415 <SelectItem value="laptop">Laptop</SelectItem>
416 <SelectItem value="desktop">Desktop</SelectItem>
417 </SelectContent>
418 </Select>
419 </div>
420
421 <div>
422 <label className="text-sm font-medium mb-2 block">Download Format</label>
423 <Select value={selectedFormat} onValueChange={setSelectedFormat}>
424 <SelectTrigger>
425 <SelectValue />
426 </SelectTrigger>
427 <SelectContent>
428 <SelectItem value="html">HTML (Recommended)</SelectItem>
429 <SelectItem value="json">JSON Data</SelectItem>
430 <SelectItem value="pdf">PDF (Printable)</SelectItem>
431 </SelectContent>
432 </Select>
433 </div>
434 </div>
435 </CardContent>
436 </Card>
437
438 {/* Download Items */}
439 {guidesLoading || flowsLoading || componentsLoading ? (
440 <div className="text-center py-8">Loading downloadable content...</div>
441 ) : (
442 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
443 {filteredItems.map((item) => (
444 <Card key={item.id} className="hover:shadow-lg transition-shadow">
445 <CardHeader className="pb-3">
446 <div className="flex items-start justify-between mb-2">
447 <div className="flex items-center gap-2">
448 {getTypeIcon(item.type)}
449 <span className="text-sm font-medium capitalize">{item.type}</span>
450 </div>
451 {downloadedItems.includes(item.id) && (
452 <CheckCircle className="h-4 w-4 text-green-600" />
453 )}
454 </div>
455 <CardTitle className="text-lg leading-tight">{item.title}</CardTitle>
456 {item.description && (
457 <CardDescription className="text-sm line-clamp-2">
458 {item.description}
459 </CardDescription>
460 )}
461 </CardHeader>
462 <CardContent>
463 <div className="flex flex-wrap gap-2 mb-4">
464 {item.category && (
465 <Badge className={getCategoryColor(item.category)}>
466 {item.category}
467 </Badge>
468 )}
469 {item.difficulty && (
470 <Badge className={getDifficultyColor(item.difficulty)}>
471 {item.difficulty}
472 </Badge>
473 )}
474 {item.deviceType && (
475 <Badge variant="outline" className="flex items-center gap-1">
476 <Smartphone className="h-3 w-3" />
477 {item.deviceType}
478 </Badge>
479 )}
480 </div>
481
482 <div className="flex items-center justify-between mb-4">
483 <span className="text-sm text-muted-foreground">
484 Size: {item.size}
485 </span>
486 <span className="text-sm font-medium">
487 {item.format}
488 </span>
489 </div>
490
491 {downloadProgress[item.id] !== undefined ? (
492 <div className="space-y-2">
493 <Progress value={downloadProgress[item.id]} className="h-2" />
494 <p className="text-sm text-center text-muted-foreground">
495 Downloading... {downloadProgress[item.id]}%
496 </p>
497 </div>
498 ) : (
499 <Button
500 onClick={() => handleDownload(item)}
501 className="w-full"
502 disabled={downloadedItems.includes(item.id)}
503 >
504 {downloadedItems.includes(item.id) ? (
505 <>
506 <CheckCircle className="mr-2 h-4 w-4" />
507 Downloaded
508 </>
509 ) : (
510 <>
511 <Download className="mr-2 h-4 w-4" />
512 Download
513 </>
514 )}
515 </Button>
516 )}
517 </CardContent>
518 </Card>
519 ))}
520 </div>
521 )}
522
523 {filteredItems.length === 0 && !guidesLoading && !flowsLoading && !componentsLoading && (
524 <div className="text-center py-12">
525 <p className="text-muted-foreground">No items found matching your search criteria.</p>
526 </div>
527 )}
528 </div>
529 </main>
530 </div>
531 );
532 }