mmy / components /editor-sections /VisualsSection.tsx
Mohammad Shahid
first commit
3a7a84c
"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>
);
};