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