1 import React, { useState } from "react";
2 import { useMutation, useQueryClient } from "@tanstack/react-query";
3 import { useForm } from "react-hook-form";
4 import { zodResolver } from "@hookform/resolvers/zod";
5 import { z } from "zod";
6 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
7 import { Button } from "@/components/ui/button";
8 import { Input } from "@/components/ui/input";
9 import { Textarea } from "@/components/ui/textarea";
10 import { Label } from "@/components/ui/label";
11 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
12 import { Badge } from "@/components/ui/badge";
13 import { useToast } from "@/hooks/use-toast";
14 import { apiRequest } from "@/lib/queryClient";
15 import { Plus, X, Save, Eye } from "lucide-react";
16 import { useLocation } from "wouter";
17
18 const createGuideSchema = z.object({
19 title: z.string().min(10, "Title must be at least 10 characters"),
20 description: z.string().min(20, "Description must be at least 20 characters"),
21 deviceType: z.enum(["laptop", "desktop"]),
22 category: z.enum(["hardware", "software", "cleaning", "upgrades"]),
23 difficulty: z.enum(["easy", "medium", "hard"]),
24 estimatedTime: z.string().min(5, "Please provide estimated time"),
25 imageUrl: z.string().url("Please provide a valid image URL"),
26 });
27
28 const stepSchema = z.object({
29 stepNumber: z.number(),
30 title: z.string().min(5, "Step title required"),
31 description: z.string().min(10, "Step description required"),
32 notes: z.array(z.string()).optional(),
33 warnings: z.array(z.string()).optional(),
34 });
35
36 type CreateGuideForm = z.infer<typeof createGuideSchema>;
37 type RepairStep = z.infer<typeof stepSchema>;
38
39 export default function CreateGuide() {
40 const [, setLocation] = useLocation();
41 const { toast } = useToast();
42 const queryClient = useQueryClient();
43
44 const [steps, setSteps] = useState<RepairStep[]>([]);
45 const [toolsRequired, setToolsRequired] = useState<string[]>([]);
46 const [safetyWarnings, setSafetyWarnings] = useState<string[]>([]);
47 const [alternativeSolutions, setAlternativeSolutions] = useState("");
48 const [currentTool, setCurrentTool] = useState("");
49 const [currentSafety, setCurrentSafety] = useState("");
50 const [previewMode, setPreviewMode] = useState(false);
51
52 const form = useForm<CreateGuideForm>({
53 resolver: zodResolver(createGuideSchema),
54 defaultValues: {
55 deviceType: "laptop",
56 category: "hardware",
57 difficulty: "easy",
58 },
59 });
60
61 const createGuideMutation = useMutation({
62 mutationFn: async (data: any) => {
63 return apiRequest("/api/user-guides", "POST", data);
64 },
65 onSuccess: () => {
66 toast({
67 title: "Success",
68 description: "Your repair guide has been created successfully!",
69 });
70 queryClient.invalidateQueries({ queryKey: ["/api/repair-guides"] });
71 setLocation("/");
72 },
73 onError: () => {
74 toast({
75 title: "Error",
76 description: "Failed to create repair guide. Please try again.",
77 variant: "destructive",
78 });
79 },
80 });
81
82 const addStep = () => {
83 const newStep: RepairStep = {
84 stepNumber: steps.length + 1,
85 title: "",
86 description: "",
87 notes: [],
88 warnings: [],
89 };
90 setSteps([...steps, newStep]);
91 };
92
93 const updateStep = (index: number, field: keyof RepairStep, value: any) => {
94 const updatedSteps = [...steps];
95 updatedSteps[index] = { ...updatedSteps[index], [field]: value };
96 setSteps(updatedSteps);
97 };
98
99 const removeStep = (index: number) => {
100 const updatedSteps = steps.filter((_, i) => i !== index);
101 // Renumber remaining steps
102 const renumberedSteps = updatedSteps.map((step, i) => ({
103 ...step,
104 stepNumber: i + 1,
105 }));
106 setSteps(renumberedSteps);
107 };
108
109 const addTool = () => {
110 if (currentTool.trim() && !toolsRequired.includes(currentTool.trim())) {
111 setToolsRequired([...toolsRequired, currentTool.trim()]);
112 setCurrentTool("");
113 }
114 };
115
116 const addSafetyWarning = () => {
117 if (currentSafety.trim() && !safetyWarnings.includes(currentSafety.trim())) {
118 setSafetyWarnings([...safetyWarnings, currentSafety.trim()]);
119 setCurrentSafety("");
120 }
121 };
122
123 const onSubmit = (data: CreateGuideForm) => {
124 if (steps.length === 0) {
125 toast({
126 title: "Error",
127 description: "Please add at least one step to your guide.",
128 variant: "destructive",
129 });
130 return;
131 }
132
133 if (toolsRequired.length === 0) {
134 toast({
135 title: "Error",
136 description: "Please add at least one required tool.",
137 variant: "destructive",
138 });
139 return;
140 }
141
142 const guideData = {
143 ...data,
144 steps,
145 toolsRequired,
146 safetyWarnings,
147 alternativeSolutions: alternativeSolutions || "No alternative solutions provided",
148 };
149
150 createGuideMutation.mutate(guideData);
151 };
152
153 const getDifficultyColor = (difficulty: string) => {
154 switch (difficulty) {
155 case "easy": return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300";
156 case "medium": return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
157 case "hard": return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300";
158 default: return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300";
159 }
160 };
161
162 if (previewMode) {
163 const formData = form.getValues();
164 return (
165 <div className="container mx-auto py-8 px-4">
166 <div className="max-w-4xl mx-auto">
167 <div className="flex justify-between items-center mb-6">
168 <h1 className="text-3xl font-bold">Guide Preview</h1>
169 <Button onClick={() => setPreviewMode(false)} variant="outline">
170 <X className="h-4 w-4 mr-2" />
171 Back to Edit
172 </Button>
173 </div>
174
175 <Card>
176 <CardHeader>
177 <div className="flex justify-between items-start">
178 <div>
179 <CardTitle className="text-2xl">{formData.title}</CardTitle>
180 <CardDescription className="mt-2">{formData.description}</CardDescription>
181 </div>
182 <div className="flex gap-2">
183 <Badge className={getDifficultyColor(formData.difficulty)}>
184 {formData.difficulty}
185 </Badge>
186 <Badge variant="secondary">{formData.category}</Badge>
187 </div>
188 </div>
189 <div className="flex items-center gap-4 text-sm text-muted-foreground">
190 <span>📱 {formData.deviceType}</span>
191 <span>⏱️ {formData.estimatedTime}</span>
192 </div>
193 </CardHeader>
194 <CardContent className="space-y-6">
195 {formData.imageUrl && (
196 <img src={formData.imageUrl} alt="Guide preview" className="w-full h-64 object-cover rounded-lg" />
197 )}
198
199 <div>
200 <h3 className="font-semibold mb-2">Tools Required</h3>
201 <div className="flex flex-wrap gap-2">
202 {toolsRequired.map((tool, index) => (
203 <Badge key={index} variant="outline">{tool}</Badge>
204 ))}
205 </div>
206 </div>
207
208 {safetyWarnings.length > 0 && (
209 <div>
210 <h3 className="font-semibold mb-2 text-red-600">Safety Warnings</h3>
211 <ul className="list-disc list-inside space-y-1">
212 {safetyWarnings.map((warning, index) => (
213 <li key={index} className="text-sm text-red-600">{warning}</li>
214 ))}
215 </ul>
216 </div>
217 )}
218
219 <div>
220 <h3 className="font-semibold mb-4">Repair Steps</h3>
221 <div className="space-y-4">
222 {steps.map((step, index) => (
223 <Card key={index}>
224 <CardContent className="p-4">
225 <h4 className="font-semibold mb-2">Step {step.stepNumber}: {step.title}</h4>
226 <p className="text-sm mb-2">{step.description}</p>
227 {step.notes && step.notes.length > 0 && (
228 <div className="text-xs text-blue-600">
229 <strong>Notes:</strong> {step.notes.join(", ")}
230 </div>
231 )}
232 {step.warnings && step.warnings.length > 0 && (
233 <div className="text-xs text-red-600">
234 <strong>Warnings:</strong> {step.warnings.join(", ")}
235 </div>
236 )}
237 </CardContent>
238 </Card>
239 ))}
240 </div>
241 </div>
242
243 {alternativeSolutions && (
244 <div>
245 <h3 className="font-semibold mb-2">Alternative Solutions</h3>
246 <p className="text-sm">{alternativeSolutions}</p>
247 </div>
248 )}
249 </CardContent>
250 </Card>
251 </div>
252 </div>
253 );
254 }
255
256 return (
257 <div className="container mx-auto py-8 px-4">
258 <div className="max-w-4xl mx-auto">
259 <div className="flex justify-between items-center mb-6">
260 <div>
261 <h1 className="text-3xl font-bold">Create Repair Guide</h1>
262 <p className="text-muted-foreground">Share your repair knowledge with the community</p>
263 </div>
264 <div className="flex gap-2">
265 <Button onClick={() => setPreviewMode(true)} variant="outline">
266 <Eye className="h-4 w-4 mr-2" />
267 Preview
268 </Button>
269 </div>
270 </div>
271
272 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
273 <Card>
274 <CardHeader>
275 <CardTitle>Basic Information</CardTitle>
276 </CardHeader>
277 <CardContent className="space-y-4">
278 <div>
279 <Label htmlFor="title">Guide Title</Label>
280 <Input
281 id="title"
282 {...form.register("title")}
283 placeholder="e.g., How to Replace a Laptop Screen"
284 />
285 {form.formState.errors.title && (
286 <p className="text-sm text-red-600">{form.formState.errors.title.message}</p>
287 )}
288 </div>
289
290 <div>
291 <Label htmlFor="description">Description</Label>
292 <Textarea
293 id="description"
294 {...form.register("description")}
295 placeholder="Detailed description of what this guide covers..."
296 rows={3}
297 />
298 {form.formState.errors.description && (
299 <p className="text-sm text-red-600">{form.formState.errors.description.message}</p>
300 )}
301 </div>
302
303 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
304 <div>
305 <Label>Device Type</Label>
306 <Select onValueChange={(value) => form.setValue("deviceType", value as "laptop" | "desktop")}>
307 <SelectTrigger>
308 <SelectValue placeholder="Select device" />
309 </SelectTrigger>
310 <SelectContent>
311 <SelectItem value="laptop">Laptop</SelectItem>
312 <SelectItem value="desktop">Desktop</SelectItem>
313 </SelectContent>
314 </Select>
315 </div>
316
317 <div>
318 <Label>Category</Label>
319 <Select onValueChange={(value) => form.setValue("category", value as any)}>
320 <SelectTrigger>
321 <SelectValue placeholder="Select category" />
322 </SelectTrigger>
323 <SelectContent>
324 <SelectItem value="hardware">Hardware</SelectItem>
325 <SelectItem value="software">Software</SelectItem>
326 <SelectItem value="cleaning">Cleaning</SelectItem>
327 <SelectItem value="upgrades">Upgrades</SelectItem>
328 </SelectContent>
329 </Select>
330 </div>
331
332 <div>
333 <Label>Difficulty</Label>
334 <Select onValueChange={(value) => form.setValue("difficulty", value as any)}>
335 <SelectTrigger>
336 <SelectValue placeholder="Select difficulty" />
337 </SelectTrigger>
338 <SelectContent>
339 <SelectItem value="easy">Easy</SelectItem>
340 <SelectItem value="medium">Medium</SelectItem>
341 <SelectItem value="hard">Hard</SelectItem>
342 </SelectContent>
343 </Select>
344 </div>
345
346 <div>
347 <Label htmlFor="estimatedTime">Time Required</Label>
348 <Input
349 id="estimatedTime"
350 {...form.register("estimatedTime")}
351 placeholder="e.g., 30-45 min"
352 />
353 </div>
354 </div>
355
356 <div>
357 <Label htmlFor="imageUrl">Guide Image URL</Label>
358 <Input
359 id="imageUrl"
360 {...form.register("imageUrl")}
361 placeholder="https://example.com/image.jpg"
362 />
363 {form.formState.errors.imageUrl && (
364 <p className="text-sm text-red-600">{form.formState.errors.imageUrl.message}</p>
365 )}
366 </div>
367 </CardContent>
368 </Card>
369
370 <Card>
371 <CardHeader>
372 <CardTitle>Tools & Safety</CardTitle>
373 </CardHeader>
374 <CardContent className="space-y-4">
375 <div>
376 <Label>Required Tools</Label>
377 <div className="flex gap-2 mb-2">
378 <Input
379 value={currentTool}
380 onChange={(e) => setCurrentTool(e.target.value)}
381 placeholder="Add a required tool..."
382 onKeyPress={(e) => e.key === "Enter" && (e.preventDefault(), addTool())}
383 />
384 <Button type="button" onClick={addTool}>
385 <Plus className="h-4 w-4" />
386 </Button>
387 </div>
388 <div className="flex flex-wrap gap-2">
389 {toolsRequired.map((tool, index) => (
390 <Badge key={index} variant="outline" className="cursor-pointer" onClick={() => setToolsRequired(toolsRequired.filter((_, i) => i !== index))}>
391 {tool} <X className="h-3 w-3 ml-1" />
392 </Badge>
393 ))}
394 </div>
395 </div>
396
397 <div>
398 <Label>Safety Warnings</Label>
399 <div className="flex gap-2 mb-2">
400 <Input
401 value={currentSafety}
402 onChange={(e) => setCurrentSafety(e.target.value)}
403 placeholder="Add a safety warning..."
404 onKeyPress={(e) => e.key === "Enter" && (e.preventDefault(), addSafetyWarning())}
405 />
406 <Button type="button" onClick={addSafetyWarning}>
407 <Plus className="h-4 w-4" />
408 </Button>
409 </div>
410 <div className="flex flex-wrap gap-2">
411 {safetyWarnings.map((warning, index) => (
412 <Badge key={index} variant="destructive" className="cursor-pointer" onClick={() => setSafetyWarnings(safetyWarnings.filter((_, i) => i !== index))}>
413 {warning} <X className="h-3 w-3 ml-1" />
414 </Badge>
415 ))}
416 </div>
417 </div>
418 </CardContent>
419 </Card>
420
421 <Card>
422 <CardHeader>
423 <div className="flex justify-between items-center">
424 <CardTitle>Repair Steps</CardTitle>
425 <Button type="button" onClick={addStep}>
426 <Plus className="h-4 w-4 mr-2" />
427 Add Step
428 </Button>
429 </div>
430 </CardHeader>
431 <CardContent>
432 <div className="space-y-4">
433 {steps.map((step, index) => (
434 <Card key={index}>
435 <CardContent className="p-4">
436 <div className="flex justify-between items-center mb-2">
437 <h4 className="font-semibold">Step {step.stepNumber}</h4>
438 <Button type="button" variant="destructive" size="sm" onClick={() => removeStep(index)}>
439 <X className="h-4 w-4" />
440 </Button>
441 </div>
442 <div className="space-y-2">
443 <Input
444 placeholder="Step title..."
445 value={step.title}
446 onChange={(e) => updateStep(index, "title", e.target.value)}
447 />
448 <Textarea
449 placeholder="Step description..."
450 value={step.description}
451 onChange={(e) => updateStep(index, "description", e.target.value)}
452 rows={2}
453 />
454 </div>
455 </CardContent>
456 </Card>
457 ))}
458 {steps.length === 0 && (
459 <div className="text-center py-8 text-muted-foreground">
460 <p>No steps added yet. Click "Add Step" to get started.</p>
461 </div>
462 )}
463 </div>
464 </CardContent>
465 </Card>
466
467 <Card>
468 <CardHeader>
469 <CardTitle>Additional Information</CardTitle>
470 </CardHeader>
471 <CardContent>
472 <div>
473 <Label htmlFor="alternatives">Alternative Solutions (Optional)</Label>
474 <Textarea
475 id="alternatives"
476 value={alternativeSolutions}
477 onChange={(e) => setAlternativeSolutions(e.target.value)}
478 placeholder="Any alternative approaches or solutions..."
479 rows={3}
480 />
481 </div>
482 </CardContent>
483 </Card>
484
485 <div className="flex justify-end gap-4">
486 <Button type="button" variant="outline" onClick={() => setLocation("/")}>
487 Cancel
488 </Button>
489 <Button type="submit" disabled={createGuideMutation.isPending}>
490 <Save className="h-4 w-4 mr-2" />
491 {createGuideMutation.isPending ? "Creating..." : "Create Guide"}
492 </Button>
493 </div>
494 </form>
495 </div>
496 </div>
497 );
498 }