'use client'; import { useState, type ChangeEvent, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { Loader2, Upload, X, Download } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import Image from 'next/image'; import { useToast } from '@/hooks/use-toast'; import { downloadCardFromBase64 } from '@/utils/downloadUtils'; import { useAuth } from '@/hooks/useAuth'; import { useApiKeys } from '@/hooks/useApiKeys'; import { TCG_GAMES, getGameConfig } from '@/config/tcg-games'; import type { TCGGame, PhotoCardGenerationParams, GeneratedCard } from '@/types'; import { LANGUAGES, tcgGameSchema, languageSchema } from '@/constants'; const photoCardGeneratorSchema = z.object({ photoDataUri: z.string().min(1, "Photo is required"), game: tcgGameSchema, characterName: z.string().min(1, 'Character name is required'), characterType: z.string().min(1, 'Character type is required'), styleDescription: z.string().min(10, 'Style description must be at least 10 characters'), language: languageSchema, // All game-specific stats are optional and dynamic - use preprocess to handle empty strings and NaN hp: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(10).max(9999).optional()), attack: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(9999).optional()), defense: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(9999).optional()), power: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(9999).optional()), toughness: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(9999).optional()), loyalty: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(99).optional()), energy: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(999).optional()), cost: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(99).optional()), manaCost: z.string().optional(), attackName1: z.string().optional(), attackDamage1: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(999).optional()), attackName2: z.string().optional(), attackDamage2: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(999).optional()), weakness: z.string().optional(), resistance: z.string().optional(), retreatCost: z.preprocess((val) => (val === '' || val === null || Number.isNaN(val)) ? undefined : val, z.number().min(0).max(5).optional()), }); type PhotoCardGeneratorForm = z.infer; export type GenerateFromPhotoInput = PhotoCardGenerationParams & { photoDataUri: string }; interface PhotoCardGeneratorFormOnlyProps { onCardGenerated?: (imageBase64: string, prompt: string, params: GenerateFromPhotoInput) => void; initialValues?: Partial; } export function PhotoCardGeneratorFormOnly({ onCardGenerated, initialValues }: PhotoCardGeneratorFormOnlyProps) { const { user } = useAuth(); const { hasOpenAIKey } = useApiKeys(); const [isGenerating, setIsGenerating] = useState(false); const [generatedCard, setGeneratedCard] = useState(null); const [error, setError] = useState(''); const [imagePreview, setImagePreview] = useState(initialValues?.photoDataUri || null); const { toast } = useToast(); const fileInputRef = useRef(null); const { register, handleSubmit, watch, setValue, reset, formState: { errors }, } = useForm({ resolver: zodResolver(photoCardGeneratorSchema), defaultValues: { photoDataUri: initialValues?.photoDataUri || '', game: initialValues?.game || 'pokemon', characterName: initialValues?.characterName || '', characterType: initialValues?.characterType || '', styleDescription: initialValues?.styleDescription || '', language: initialValues?.language || 'english', hp: initialValues?.hp, attackName1: initialValues?.attackName1 || '', attackDamage1: initialValues?.attackDamage1, attackName2: initialValues?.attackName2 || '', attackDamage2: initialValues?.attackDamage2, weakness: initialValues?.weakness || '', resistance: initialValues?.resistance || '', retreatCost: initialValues?.retreatCost, }, }); const watchedValues = watch(); const currentGame = watchedValues.game || 'pokemon'; const gameConfig = getGameConfig(currentGame); const handleGameChange = (game: TCGGame) => { setValue('game', game, { shouldValidate: true }); const config = getGameConfig(game); // Reset character type setValue('characterType', '', { shouldValidate: false }); // Set default stats for the game const defaultStats = config.defaultStats; if (defaultStats) { Object.entries(defaultStats).forEach(([key, value]) => { setValue(key as any, value as any); }); } }; const handleDownloadCard = () => { if (!generatedCard) return; try { const filename = watchedValues.characterName ? `${watchedValues.characterName.toLowerCase().replace(/\s+/g, '_')}_${currentGame}_card` : `${currentGame}_card`; downloadCardFromBase64(generatedCard.imageBase64, filename); toast({ title: 'Download Started', description: `Your ${gameConfig.name} card is being downloaded.`, }); } catch (error) { console.error('Download error:', error); toast({ title: 'Download Failed', description: 'Failed to download the card. Please try again.', variant: 'destructive', }); } }; const handleImageChange = (event: ChangeEvent) => { const file = event.target.files?.[0]; if (file) { if (file.size > 5 * 1024 * 1024) { // 5MB limit toast({ title: 'File too large', description: 'Please select an image smaller than 5MB.', variant: 'destructive', }); return; } const reader = new FileReader(); reader.onload = (e) => { const dataUrl = e.target?.result as string; setImagePreview(dataUrl); setValue('photoDataUri', dataUrl); }; reader.readAsDataURL(file); } }; const removeImage = () => { setImagePreview(null); setValue('photoDataUri', ''); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const onSubmit = async (data: PhotoCardGeneratorForm) => { console.log('Form submitted with data:', data); console.log('Has user:', !!user, 'Has OpenAI key:', hasOpenAIKey); if (!user) { setError('You must be logged in to generate cards'); return; } if (!hasOpenAIKey) { setError('OpenAI API key is required to generate cards from photos. Please configure it in Settings.'); return; } if (!data.photoDataUri) { toast({ title: 'Photo Required', description: 'Please upload a reference photo first.', variant: 'destructive', }); return; } setIsGenerating(true); setError(''); setGeneratedCard(null); try { const response = await fetch('/api/generate-card-from-photo', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ...data, userId: user.uid, }), }); const result = await response.json(); if (!response.ok) { setError(result.error || 'Failed to generate card'); return; } if (result.error) { setError(result.error); return; } if (result.imageBase64 && result.prompt) { setGeneratedCard({ imageBase64: result.imageBase64, prompt: result.prompt, }); onCardGenerated?.(result.imageBase64, result.prompt, data); toast({ title: 'Card Generated!', description: `Your ${gameConfig.name} card has been generated based on your photo.`, }); } } catch (err) { console.error('Error generating card:', err); setError('Failed to generate card. Please try again.'); } finally { setIsGenerating(false); } }; return (
{/* Form Section */} Generate Card from Photo Upload a photo and create a custom TCG card inspired by your image using AI
{ console.log('Form validation errors:', errors); // Build a detailed error message const errorFields = Object.entries(errors).map(([field, error]) => { return `${field}: ${error?.message || 'Invalid value'}`; }).join(', '); console.log('Detailed errors:', errorFields); toast({ title: 'Validation Error', description: errorFields || 'Please check all required fields and try again.', variant: 'destructive', }); })} className="space-y-6"> {/* Photo Upload */}

Upload Photo

{imagePreview ? (
Preview
) : (

PNG, JPG up to 5MB

)} {errors.photoDataUri && (

{errors.photoDataUri.message}

)}
{/* Game Selection */}

Game Selection {gameConfig.name}

{/* Basic Info */}

Character Information

{errors.characterName && (

{errors.characterName.message}

)}
{errors.characterType && (

{errors.characterType.message}

)}
{/* Style Description */}

Style & Description