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 }