|
1
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; |
|
2
|
import { Button } from "@/components/ui/button"; |
|
3
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; |
|
4
|
import { apiRequest } from "@/lib/queryClient"; |
|
5
|
import type { RepairGuide } from "@shared/schema"; |
|
6
|
|
|
7
|
interface GuideDetailModalProps { |
|
8
|
guideId: string; |
|
9
|
onClose: () => void; |
|
10
|
isPage?: boolean; |
|
11
|
} |
|
12
|
|
|
13
|
export default function GuideDetailModal({ guideId, onClose, isPage = false }: GuideDetailModalProps) { |
|
14
|
const queryClient = useQueryClient(); |
|
15
|
|
|
16
|
const { data: guide, isLoading, error } = useQuery<RepairGuide>({ |
|
17
|
queryKey: ["/api/repair-guides", guideId], |
|
18
|
enabled: !!guideId, |
|
19
|
}); |
|
20
|
|
|
21
|
const bookmarkMutation = useMutation({ |
|
22
|
mutationFn: () => apiRequest("PATCH", `/api/repair-guides/${guideId}/bookmark`), |
|
23
|
onSuccess: () => { |
|
24
|
queryClient.invalidateQueries({ queryKey: ["/api/repair-guides"] }); |
|
25
|
}, |
|
26
|
}); |
|
27
|
|
|
28
|
if (isLoading) { |
|
29
|
return ( |
|
30
|
<Dialog open={true} onOpenChange={() => onClose()}> |
|
31
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto"> |
|
32
|
<div className="animate-pulse space-y-4"> |
|
33
|
<div className="h-8 bg-gray-200 rounded w-3/4"></div> |
|
34
|
<div className="h-4 bg-gray-200 rounded w-full"></div> |
|
35
|
<div className="h-4 bg-gray-200 rounded w-5/6"></div> |
|
36
|
<div className="space-y-3"> |
|
37
|
{[...Array(3)].map((_, i) => ( |
|
38
|
<div key={i} className="h-24 bg-gray-200 rounded"></div> |
|
39
|
))} |
|
40
|
</div> |
|
41
|
</div> |
|
42
|
</DialogContent> |
|
43
|
</Dialog> |
|
44
|
); |
|
45
|
} |
|
46
|
|
|
47
|
if (error || !guide) { |
|
48
|
return ( |
|
49
|
<Dialog open={true} onOpenChange={() => onClose()}> |
|
50
|
<DialogContent className="max-w-md"> |
|
51
|
<div className="text-center py-6"> |
|
52
|
<div className="text-gray-400 mb-4"> |
|
53
|
<i className="material-icons text-6xl">error_outline</i> |
|
54
|
</div> |
|
55
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Guide Not Found</h3> |
|
56
|
<p className="text-gray-600 mb-4">The repair guide you're looking for doesn't exist.</p> |
|
57
|
<Button onClick={onClose}>Close</Button> |
|
58
|
</div> |
|
59
|
</DialogContent> |
|
60
|
</Dialog> |
|
61
|
); |
|
62
|
} |
|
63
|
|
|
64
|
const getDifficultyColor = (difficulty: string) => { |
|
65
|
switch (difficulty) { |
|
66
|
case "easy": |
|
67
|
return "bg-success-50 text-success-600"; |
|
68
|
case "medium": |
|
69
|
return "bg-warning-50 text-warning-700"; |
|
70
|
case "hard": |
|
71
|
return "bg-danger-50 text-danger-700"; |
|
72
|
default: |
|
73
|
return "bg-gray-50 text-gray-600"; |
|
74
|
} |
|
75
|
}; |
|
76
|
|
|
77
|
const modalContent = ( |
|
78
|
<> |
|
79
|
<DialogHeader className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 rounded-t-xl"> |
|
80
|
<div className="flex items-center justify-between"> |
|
81
|
<DialogTitle className="text-xl font-medium text-gray-900">{guide.title}</DialogTitle> |
|
82
|
<div className="flex items-center space-x-2"> |
|
83
|
<Button |
|
84
|
variant="ghost" |
|
85
|
size="icon" |
|
86
|
onClick={() => bookmarkMutation.mutate()} |
|
87
|
disabled={bookmarkMutation.isPending} |
|
88
|
> |
|
89
|
<i className={`material-icons ${guide.isBookmarked ? "text-primary-700" : "text-gray-400"}`}> |
|
90
|
{guide.isBookmarked ? "bookmark" : "bookmark_border"} |
|
91
|
</i> |
|
92
|
</Button> |
|
93
|
{!isPage && ( |
|
94
|
<Button variant="ghost" size="icon" onClick={onClose}> |
|
95
|
<i className="material-icons">close</i> |
|
96
|
</Button> |
|
97
|
)} |
|
98
|
</div> |
|
99
|
</div> |
|
100
|
<div className="flex items-center space-x-4 mt-2"> |
|
101
|
<span className={`inline-block px-2 py-1 text-xs font-medium rounded-full ${getDifficultyColor(guide.difficulty)}`}> |
|
102
|
{guide.difficulty.toUpperCase()} |
|
103
|
</span> |
|
104
|
<span className="inline-block px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full"> |
|
105
|
{guide.estimatedTime} |
|
106
|
</span> |
|
107
|
<span className="text-sm text-gray-500">Updated recently</span> |
|
108
|
</div> |
|
109
|
</DialogHeader> |
|
110
|
|
|
111
|
<div className="p-6"> |
|
112
|
{/* Safety Warning */} |
|
113
|
{guide.safetyWarnings.length > 0 && ( |
|
114
|
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6"> |
|
115
|
<div className="flex items-start space-x-3"> |
|
116
|
<i className="material-icons text-yellow-600 text-lg mt-0.5">warning</i> |
|
117
|
<div> |
|
118
|
<h3 className="font-medium text-yellow-800 mb-1">Safety Precautions</h3> |
|
119
|
<ul className="text-sm text-yellow-700 space-y-1"> |
|
120
|
{guide.safetyWarnings.map((warning, index) => ( |
|
121
|
<li key={index}>• {warning}</li> |
|
122
|
))} |
|
123
|
</ul> |
|
124
|
</div> |
|
125
|
</div> |
|
126
|
</div> |
|
127
|
)} |
|
128
|
|
|
129
|
{/* Required Tools */} |
|
130
|
<div className="mb-6"> |
|
131
|
<h3 className="font-medium text-gray-900 mb-3">Required Tools</h3> |
|
132
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3"> |
|
133
|
{guide.toolsRequired.map((tool, index) => ( |
|
134
|
<div key={index} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg"> |
|
135
|
<i className="material-icons text-gray-600">build</i> |
|
136
|
<span className="text-sm">{tool}</span> |
|
137
|
</div> |
|
138
|
))} |
|
139
|
</div> |
|
140
|
</div> |
|
141
|
|
|
142
|
{/* Step-by-step Instructions */} |
|
143
|
<div className="space-y-6"> |
|
144
|
<h3 className="font-medium text-gray-900">Installation Steps</h3> |
|
145
|
|
|
146
|
{guide.steps.map((step) => ( |
|
147
|
<div key={step.stepNumber} className="flex space-x-4"> |
|
148
|
<div className="flex-shrink-0 w-8 h-8 bg-primary-700 text-white rounded-full flex items-center justify-center text-sm font-medium"> |
|
149
|
{step.stepNumber} |
|
150
|
</div> |
|
151
|
<div className="flex-1"> |
|
152
|
<h4 className="font-medium text-gray-900 mb-2">{step.title}</h4> |
|
153
|
<p className="text-sm text-gray-600 mb-3">{step.description}</p> |
|
154
|
|
|
155
|
{step.imageUrl && ( |
|
156
|
<img |
|
157
|
src={step.imageUrl} |
|
158
|
alt={step.title} |
|
159
|
className="rounded-lg shadow-sm w-full max-w-md mb-3" |
|
160
|
/> |
|
161
|
)} |
|
162
|
|
|
163
|
{step.notes && step.notes.length > 0 && ( |
|
164
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-3"> |
|
165
|
<p className="text-sm text-blue-800"> |
|
166
|
<strong>Note:</strong> {step.notes.join(", ")} |
|
167
|
</p> |
|
168
|
</div> |
|
169
|
)} |
|
170
|
|
|
171
|
{step.warnings && step.warnings.length > 0 && ( |
|
172
|
<div className="bg-red-50 border border-red-200 rounded-lg p-3"> |
|
173
|
<p className="text-sm text-red-800"> |
|
174
|
<strong>Warning:</strong> {step.warnings.join(", ")} |
|
175
|
</p> |
|
176
|
</div> |
|
177
|
)} |
|
178
|
</div> |
|
179
|
</div> |
|
180
|
))} |
|
181
|
|
|
182
|
{/* Alternative Solutions */} |
|
183
|
{guide.alternativeSolutions && ( |
|
184
|
<div className="bg-gray-50 rounded-lg p-4"> |
|
185
|
<h4 className="font-medium text-gray-900 mb-2">Alternative Solutions</h4> |
|
186
|
<p className="text-sm text-gray-600">{guide.alternativeSolutions}</p> |
|
187
|
</div> |
|
188
|
)} |
|
189
|
</div> |
|
190
|
</div> |
|
191
|
</> |
|
192
|
); |
|
193
|
|
|
194
|
if (isPage) { |
|
195
|
return ( |
|
196
|
<div className="max-w-4xl mx-auto px-4 py-8"> |
|
197
|
<div className="bg-white rounded-xl shadow-material-lg"> |
|
198
|
{modalContent} |
|
199
|
</div> |
|
200
|
</div> |
|
201
|
); |
|
202
|
} |
|
203
|
|
|
204
|
return ( |
|
205
|
<Dialog open={true} onOpenChange={() => onClose()}> |
|
206
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto p-0"> |
|
207
|
{modalContent} |
|
208
|
</DialogContent> |
|
209
|
</Dialog> |
|
210
|
); |
|
211
|
} |