Spaces:
Paused
Paused
File size: 6,699 Bytes
3a7a84c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | "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>
);
}; |