|
1
|
import { useState } from "react"; |
|
2
|
import { useMutation, useQueryClient } from "@tanstack/react-query"; |
|
3
|
import { Button } from "@/components/ui/button"; |
|
4
|
import { DownloadModal } from "@/components/ui/download-modal"; |
|
5
|
import { apiRequest } from "@/lib/queryClient"; |
|
6
|
import { Download, Bookmark, Eye, Clock, Wrench, ArrowRight, Laptop, Monitor, Sparkles, Cpu, Code, Eraser, MoveUp } from "lucide-react"; |
|
7
|
import type { RepairGuide } from "@shared/schema"; |
|
8
|
import { cn } from "@/lib/utils"; |
|
9
|
|
|
10
|
interface GuideCardProps { |
|
11
|
guide: RepairGuide; |
|
12
|
onViewGuide: () => void; |
|
13
|
} |
|
14
|
|
|
15
|
export default function GuideCard({ guide, onViewGuide }: GuideCardProps) { |
|
16
|
const queryClient = useQueryClient(); |
|
17
|
const [showDownloadModal, setShowDownloadModal] = useState(false); |
|
18
|
|
|
19
|
const bookmarkMutation = useMutation({ |
|
20
|
mutationFn: () => apiRequest("PATCH", `/api/repair-guides/${guide.id}/bookmark`), |
|
21
|
onSuccess: () => { |
|
22
|
queryClient.invalidateQueries({ queryKey: ["api", "repair-guides"] }); |
|
23
|
}, |
|
24
|
}); |
|
25
|
|
|
26
|
const getDifficultyStyles = (difficulty: string) => { |
|
27
|
switch (difficulty) { |
|
28
|
case "easy": |
|
29
|
return "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30"; |
|
30
|
case "medium": |
|
31
|
return "bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/30"; |
|
32
|
case "hard": |
|
33
|
return "bg-rose-500/15 text-rose-600 dark:text-rose-400 border-rose-500/30"; |
|
34
|
default: |
|
35
|
return "bg-slate-500/15 text-slate-600 dark:text-slate-400 border-slate-500/30"; |
|
36
|
} |
|
37
|
}; |
|
38
|
|
|
39
|
const getCategoryInfo = (category: string) => { |
|
40
|
switch (category) { |
|
41
|
case "hardware": |
|
42
|
return { label: "Hardware", icon: Cpu, color: "text-indigo-500 bg-indigo-500/10 border-indigo-500/20" }; |
|
43
|
case "software": |
|
44
|
return { label: "Software", icon: Code, color: "text-emerald-500 bg-emerald-500/10 border-emerald-500/20" }; |
|
45
|
case "cleaning": |
|
46
|
return { label: "Cleaning", icon: Eraser, color: "text-amber-500 bg-amber-500/10 border-amber-500/20" }; |
|
47
|
case "upgrades": |
|
48
|
return { label: "Upgrades", icon: MoveUp, color: "text-purple-500 bg-purple-500/10 border-purple-500/20" }; |
|
49
|
default: |
|
50
|
return { label: category, icon: Sparkles, color: "text-primary bg-primary/10 border-primary/20" }; |
|
51
|
} |
|
52
|
}; |
|
53
|
|
|
54
|
const formatCount = (count: number) => { |
|
55
|
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`; |
|
56
|
return count.toString(); |
|
57
|
}; |
|
58
|
|
|
59
|
const categoryInfo = getCategoryInfo(guide.category); |
|
60
|
const CategoryIcon = categoryInfo.icon; |
|
61
|
|
|
62
|
return ( |
|
63
|
<div className="group relative bg-card/60 hover:bg-card/90 backdrop-blur-md rounded-3xl border border-border/50 hover:border-primary/40 hover:shadow-xl transition-all duration-300 flex flex-col h-full overflow-hidden"> |
|
64
|
|
|
65
|
{/* Image with Gradient Overlay */} |
|
66
|
<div className="relative aspect-[16/10] overflow-hidden bg-muted/40 cursor-pointer" onClick={onViewGuide}> |
|
67
|
<img |
|
68
|
src={guide.imageUrl} |
|
69
|
alt={guide.title} |
|
70
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" |
|
71
|
loading="lazy" |
|
72
|
/> |
|
73
|
<div className="absolute inset-0 bg-gradient-to-t from-background/90 via-transparent to-black/30" /> |
|
74
|
|
|
75
|
{/* Floating Badges */} |
|
76
|
<div className="absolute top-3 left-3 flex flex-wrap gap-1.5 items-center"> |
|
77
|
<span className={cn( |
|
78
|
"px-2.5 py-0.5 text-[11px] font-bold uppercase tracking-wider rounded-full border backdrop-blur-md", |
|
79
|
getDifficultyStyles(guide.difficulty) |
|
80
|
)}> |
|
81
|
{guide.difficulty} |
|
82
|
</span> |
|
83
|
<span className={cn( |
|
84
|
"px-2.5 py-0.5 text-[11px] font-semibold rounded-full border backdrop-blur-md flex items-center gap-1", |
|
85
|
categoryInfo.color |
|
86
|
)}> |
|
87
|
<CategoryIcon className="h-3 w-3" /> |
|
88
|
{categoryInfo.label} |
|
89
|
</span> |
|
90
|
</div> |
|
91
|
|
|
92
|
<div className="absolute top-3 right-3 flex items-center gap-1.5"> |
|
93
|
<span className="px-2 py-0.5 text-[11px] font-medium rounded-full bg-black/40 text-white backdrop-blur-md border border-white/10 flex items-center gap-1"> |
|
94
|
{guide.deviceType === "laptop" ? <Laptop className="h-3 w-3" /> : <Monitor className="h-3 w-3" />} |
|
95
|
{guide.deviceType === "laptop" ? "Laptop" : "Desktop"} |
|
96
|
</span> |
|
97
|
<button |
|
98
|
onClick={(e) => { |
|
99
|
e.stopPropagation(); |
|
100
|
bookmarkMutation.mutate(); |
|
101
|
}} |
|
102
|
disabled={bookmarkMutation.isPending} |
|
103
|
className="h-8 w-8 rounded-full bg-black/40 backdrop-blur-md border border-white/20 flex items-center justify-center text-white hover:bg-primary transition-colors" |
|
104
|
title={guide.isBookmarked ? "Remove Bookmark" : "Save Guide"} |
|
105
|
> |
|
106
|
<Bookmark className={cn("h-3.5 w-3.5", guide.isBookmarked && "fill-current text-primary")} /> |
|
107
|
</button> |
|
108
|
</div> |
|
109
|
</div> |
|
110
|
|
|
111
|
<div className="p-5 flex flex-col flex-1"> |
|
112
|
<div className="flex items-center gap-2 text-xs font-semibold text-muted-foreground mb-2.5"> |
|
113
|
<Clock className="h-3.5 w-3.5 text-primary" /> {guide.estimatedTime} |
|
114
|
<span className="opacity-40">•</span> |
|
115
|
<Wrench className="h-3.5 w-3.5 text-primary" /> {guide.toolsRequired ? guide.toolsRequired.length : 0} Tools |
|
116
|
</div> |
|
117
|
|
|
118
|
<h3 |
|
119
|
onClick={onViewGuide} |
|
120
|
className="text-base sm:text-lg font-bold mb-2 group-hover:text-primary transition-colors line-clamp-1 cursor-pointer" |
|
121
|
> |
|
122
|
{guide.title} |
|
123
|
</h3> |
|
124
|
<p className="text-xs sm:text-sm text-muted-foreground mb-4 line-clamp-2 leading-relaxed"> |
|
125
|
{guide.description} |
|
126
|
</p> |
|
127
|
|
|
128
|
<div className="mt-auto pt-3 border-t border-border/40 flex items-center justify-between"> |
|
129
|
<div className="flex items-center gap-3 text-xs font-medium text-muted-foreground"> |
|
130
|
<div className="flex items-center gap-1"> |
|
131
|
<Eye className="h-3.5 w-3.5" /> {formatCount(guide.viewCount || 0)} |
|
132
|
</div> |
|
133
|
<div className="flex items-center gap-1"> |
|
134
|
<Download className="h-3.5 w-3.5" /> {formatCount(guide.downloadCount || 0)} |
|
135
|
</div> |
|
136
|
</div> |
|
137
|
|
|
138
|
<div className="flex items-center gap-1.5"> |
|
139
|
<Button |
|
140
|
variant="ghost" |
|
141
|
size="icon" |
|
142
|
className="h-8 w-8 rounded-full text-muted-foreground hover:text-primary hover:bg-primary/10" |
|
143
|
onClick={() => setShowDownloadModal(true)} |
|
144
|
title="Download for offline access" |
|
145
|
> |
|
146
|
<Download className="h-4 w-4" /> |
|
147
|
</Button> |
|
148
|
<Button |
|
149
|
size="sm" |
|
150
|
onClick={onViewGuide} |
|
151
|
className="rounded-full h-8 px-3.5 gap-1.5 text-xs font-semibold" |
|
152
|
> |
|
153
|
Open <ArrowRight className="h-3.5 w-3.5" /> |
|
154
|
</Button> |
|
155
|
</div> |
|
156
|
</div> |
|
157
|
</div> |
|
158
|
|
|
159
|
<DownloadModal |
|
160
|
open={showDownloadModal} |
|
161
|
onOpenChange={setShowDownloadModal} |
|
162
|
guide={guide} |
|
163
|
/> |
|
164
|
</div> |
|
165
|
); |
|
166
|
} |