Spaces:
Paused
Paused
| "use client"; | |
| import { useState } from 'react'; | |
| import { UseFormReturn } from 'react-hook-form'; | |
| import { z } from 'zod'; | |
| import { toast } from 'sonner'; | |
| import Image from 'next/image'; | |
| import { Sparkles, Upload } from 'lucide-react'; | |
| import { heroFormSchema } from '@/lib/validators'; | |
| import { regenerateImageAction } from '@/app/actions/db.actions'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Input } from '@/components/ui/input'; | |
| import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; | |
| // This helper can live in a utils file. It handles file uploads to a standard API endpoint. | |
| async function uploadFile(file: File): Promise<string> { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| // This remains a standard fetch call as it's handling multipart/form-data | |
| const response = await fetch('/api/upload', { method: 'POST', body: formData }); | |
| if (!response.ok) throw new Error('Upload failed'); | |
| const { url } = await response.json(); | |
| return url; | |
| } | |
| type FormSchemaType = z.infer<typeof heroFormSchema>; | |
| interface VisualsSectionProps { | |
| form: UseFormReturn<FormSchemaType>; | |
| } | |
| export const VisualsSection = ({ form }: VisualsSectionProps) => { | |
| const [isGeneratingAvatar, setIsGeneratingAvatar] = useState(false); | |
| const [isGeneratingIcon, setIsGeneratingIcon] = useState(false); | |
| const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>, fieldName: 'avatarUrl' | 'iconUrl') => { | |
| const file = e.target.files?.[0]; | |
| if (!file) return; | |
| const toastId = toast.loading(`Uploading image...`); | |
| try { | |
| const url = await uploadFile(file); | |
| form.setValue(fieldName, url, { shouldValidate: true, shouldDirty: true }); | |
| toast.success("Upload complete!", { id: toastId }); | |
| } catch { | |
| toast.error("Upload failed.", { id: toastId }); | |
| } | |
| }; | |
| const handleGenerateImage = async (imageType: 'avatar' | 'icon') => { | |
| const setLoading = imageType === 'avatar' ? setIsGeneratingAvatar : setIsGeneratingIcon; | |
| const promptField = imageType === 'avatar' ? 'avatarPrompt' : 'iconPrompt'; | |
| const urlField = imageType === 'avatar' ? 'avatarUrl' : 'iconUrl'; | |
| setLoading(true); | |
| const toastId = toast.loading(`Generating new ${imageType}...`); | |
| const prompt = form.getValues(promptField); | |
| const heroName = form.getValues('heroName'); | |
| const result = await regenerateImageAction(prompt, heroName, imageType); | |
| if (result.success && result.data) { | |
| form.setValue(urlField, result.data, { shouldValidate: true, shouldDirty: true }); | |
| toast.success(`New ${imageType} generated!`, { id: toastId }); | |
| } else { | |
| toast.error(result.message || `Could not generate ${imageType}.`, { id: toastId }); | |
| } | |
| setLoading(false); | |
| }; | |
| return ( | |
| <div className="p-4 border rounded-lg"> | |
| <h3 className="text-lg font-semibold mb-4">Visuals</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {/* Avatar Section */} | |
| <div className="space-y-2"> | |
| <FormField | |
| control={form.control} | |
| name="avatarPrompt" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Avatar Prompt</FormLabel> | |
| <FormControl> | |
| <Input placeholder="AI prompt for the hero's portrait" {...field} /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <div className="w-full aspect-square relative rounded-md overflow-hidden border bg-muted"> | |
| {form.watch('avatarUrl') && ( | |
| <Image src={form.watch('avatarUrl')} alt="Hero Avatar" layout="fill" objectFit="cover" /> | |
| )} | |
| </div> | |
| <div className="flex gap-2"> | |
| <Button asChild variant="outline" className="flex-1 cursor-pointer"> | |
| <label><Upload className="h-4 w-4 mr-2" /> Upload <Input type="file" className="hidden" accept="image/*" onChange={(e) => handleFileChange(e, 'avatarUrl')} /></label> | |
| </Button> | |
| <Button type="button" className="flex-1" onClick={() => handleGenerateImage('avatar')} disabled={isGeneratingAvatar}> | |
| <Sparkles className="h-4 w-4 mr-2" /> | |
| {isGeneratingAvatar ? 'Generating...' : 'Generate AI'} | |
| </Button> | |
| </div> | |
| </div> | |
| {/* Icon Section */} | |
| <div className="space-y-2"> | |
| <FormField | |
| control={form.control} | |
| name="iconPrompt" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Icon Prompt</FormLabel> | |
| <FormControl> | |
| <Input placeholder="AI prompt for the superpower icon" {...field} /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <div className="w-full aspect-square relative rounded-md overflow-hidden border bg-muted p-4"> | |
| {form.watch('iconUrl') && ( | |
| <Image src={form.watch('iconUrl')} alt="Superpower Icon" layout="fill" objectFit="contain" /> | |
| )} | |
| </div> | |
| <div className="flex gap-2"> | |
| <Button asChild variant="outline" className="flex-1 cursor-pointer"> | |
| <label><Upload className="h-4 w-4 mr-2" /> Upload <Input type="file" className="hidden" accept="image/*" onChange={(e) => handleFileChange(e, 'iconUrl')} /></label> | |
| </Button> | |
| <Button type="button" className="flex-1" onClick={() => handleGenerateImage('icon')} disabled={isGeneratingIcon}> | |
| <Sparkles className="h-4 w-4 mr-2" /> | |
| {isGeneratingIcon ? 'Generating...' : 'Generate AI'} | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; |