diff --git a/src/ai/flows/generate-tcg-card-from-photo.ts b/src/ai/flows/generate-tcg-card-from-photo.ts index 7ec5ee5..949c7de 100644 --- a/src/ai/flows/generate-tcg-card-from-photo.ts +++ b/src/ai/flows/generate-tcg-card-from-photo.ts @@ -14,10 +14,11 @@ import OpenAI from 'openai'; import { getUserApiKeys } from '@/lib/firestore'; import { getGameConfig } from '@/config/tcg-games'; import type { TCGGame } from '@/types'; +import { tcgGameSchema, languageSchema } from '@/constants'; const GenerateFromPhotoInputSchema = z.object({ userId: z.string().min(1, "User ID is required"), - game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']), + game: tcgGameSchema, photoDataUri: z .string() .describe( @@ -26,7 +27,7 @@ const GenerateFromPhotoInputSchema = z.object({ 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 is required - describe how to adapt the photo"), - language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']).default('english'), + language: languageSchema.default('english'), // Game-specific stats (all optional) hp: z.number().optional(), diff --git a/src/ai/flows/generate-tcg-card.ts b/src/ai/flows/generate-tcg-card.ts index 807c691..360a5c4 100644 --- a/src/ai/flows/generate-tcg-card.ts +++ b/src/ai/flows/generate-tcg-card.ts @@ -14,18 +14,19 @@ import { GoogleGenAI } from '@google/genai'; import { getUserApiKeys } from '@/lib/firestore'; import type { TCGGame } from '@/types'; import { getGameConfig } from '@/config/tcg-games'; +import { tcgGameSchema, languageSchema, aiModelSchema } from '@/constants'; const GenerateTCGCardInputSchema = z.object({ userId: z.string().min(1, "User ID is required"), - game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']), + game: tcgGameSchema, characterName: z.string().min(1, "Character name is required"), characterType: z.string().min(1, "Character type is required"), isSpecialArt: z.boolean(), isHolo: z.boolean(), backgroundDescription: z.string().min(1, "Background description is required"), characterDescription: z.string().min(1, "Character description is required"), - language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']), - model: z.enum(['imagen-4.0-ultra-generate-001', 'imagen-4.0-generate-001', 'imagen-4.0-fast-generate-001', 'gemini-2.5-flash-image', 'gemini-3-pro-image-preview']).default('imagen-4.0-ultra-generate-001'), + language: languageSchema, + model: aiModelSchema.default('imagen-4.0-ultra-generate-001'), // Pokemon-specific hp: z.number().optional(), diff --git a/src/ai/flows/scan-tcg-card.ts b/src/ai/flows/scan-tcg-card.ts index 7da9608..7f1e179 100644 --- a/src/ai/flows/scan-tcg-card.ts +++ b/src/ai/flows/scan-tcg-card.ts @@ -12,6 +12,7 @@ import {ai, createUserAI} from '@/ai/genkit'; import {z} from 'genkit'; import type { TCGGame } from '@/types'; +import { tcgGameSchema } from '@/constants'; const ScanTCGCardInputSchema = z.object({ photoDataUri: z @@ -20,7 +21,7 @@ const ScanTCGCardInputSchema = z.object({ "A photo of a trading card, as a data URI that must include a MIME type and use Base64 encoding. Expected format: 'data:;base64,'." ), userId: z.string().describe("The user ID for personalized API key usage.").optional(), - game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']).optional().describe("The TCG game to scan for. If not specified, will auto-detect."), + game: tcgGameSchema.optional().describe("The TCG game to scan for. If not specified, will auto-detect."), }); export type ScanTCGCardInput = z.infer; @@ -29,7 +30,7 @@ const ScanTCGCardOutputSchema = z.object({ name: z.string().describe("The name of the card.").optional(), set: z.string().describe("The set the card belongs to.").optional(), rarity: z.string().describe("The rarity of the card.").optional(), - game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']).describe("The TCG game this card is from.").optional(), + game: tcgGameSchema.describe("The TCG game this card is from.").optional(), }).describe("Details about the trading card.").optional(), error: z.string().describe("Error message if the card could not be identified.").optional() }); diff --git a/src/app/dashboard/generate-from-photo/edit/page.tsx b/src/app/dashboard/generate-from-photo/edit/page.tsx index 4b414f4..fdd79a6 100644 --- a/src/app/dashboard/generate-from-photo/edit/page.tsx +++ b/src/app/dashboard/generate-from-photo/edit/page.tsx @@ -14,7 +14,7 @@ import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUt import { parsePhotoGenerationParams } from '@/utils/generationParamsUtils'; import { getGameConfig } from '@/config/tcg-games'; import type { GenerateFromPhotoInput } from '@/components/cards/PhotoCardGeneratorFormOnly'; -import type { TCGGame } from '@/types'; +import type { TCGGame, PhotoCardGenerationParams, GeneratedCard } from '@/types'; import Image from 'next/image'; export default function EditGenerateFromPhotoPage() { @@ -22,7 +22,7 @@ export default function EditGenerateFromPhotoPage() { const { toast } = useToast(); const router = useRouter(); const searchParams = useSearchParams(); - const [generatedCard, setGeneratedCard] = useState<{ imageBase64: string; prompt: string; params?: any } | null>(null); + const [generatedCard, setGeneratedCard] = useState<(GeneratedCard & { params?: GenerateFromPhotoInput }) | null>(null); const [originalCard, setOriginalCard] = useState<{ id: string; name: string; imageUrl: string } | null>(null); const [isSaving, setIsSaving] = useState(false); const [initialValues, setInitialValues] = useState | undefined>(undefined); @@ -95,7 +95,7 @@ export default function EditGenerateFromPhotoPage() { const gameName = getGameConfig(game as TCGGame).name; // Exclude photoDataUri from params when saving to Firestore (too large for document storage) - const { photoDataUri, ...paramsWithoutPhoto } = generatedCard.params; + const { photoDataUri, ...paramsWithoutPhoto } = generatedCard.params || {}; await addCardToCollection(user.uid, { name: `Photo-Generated ${gameName} Card`, @@ -106,7 +106,7 @@ export default function EditGenerateFromPhotoPage() { isGenerated: true, isPhotoGenerated: true, prompt: generatedCard.prompt, - photoGenerationParams: paramsWithoutPhoto, + photoGenerationParams: paramsWithoutPhoto as PhotoCardGenerationParams, }); toast({ diff --git a/src/app/dashboard/generate-from-photo/page.tsx b/src/app/dashboard/generate-from-photo/page.tsx index aad1040..eeb0e64 100644 --- a/src/app/dashboard/generate-from-photo/page.tsx +++ b/src/app/dashboard/generate-from-photo/page.tsx @@ -12,7 +12,7 @@ import { useRouter } from 'next/navigation'; import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUtils'; import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; import { getGameConfig } from '@/config/tcg-games'; -import type { TCGGame } from '@/types'; +import type { TCGGame, GeneratedCard, PhotoCardGenerationParams } from '@/types'; export default function GenerateFromPhotoPage() { const { user } = useAuth(); diff --git a/src/app/dashboard/generate/edit/page.tsx b/src/app/dashboard/generate/edit/page.tsx index 28b6193..c7e5e49 100644 --- a/src/app/dashboard/generate/edit/page.tsx +++ b/src/app/dashboard/generate/edit/page.tsx @@ -11,14 +11,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUtils'; import { parseGenerationParams } from '@/utils/generationParamsUtils'; import { getGameConfig } from '@/config/tcg-games'; -import type { TCGGame, CardGenerationParams } from '@/types'; +import type { TCGGame, CardGenerationParams, GeneratedCard } from '@/types'; import Image from 'next/image'; export default function EditGenerateCardPage() { const { user } = useAuth(); const { toast } = useToast(); const searchParams = useSearchParams(); - const [generatedCard, setGeneratedCard] = useState<{ imageBase64: string; prompt: string; params?: any } | null>(null); + const [generatedCard, setGeneratedCard] = useState<(GeneratedCard & { params?: CardGenerationParams }) | null>(null); const [originalCard, setOriginalCard] = useState<{ id: string; name: string; imageUrl: string } | null>(null); const [isSaving, setIsSaving] = useState(false); const [initialValues, setInitialValues] = useState | undefined>(undefined); diff --git a/src/app/dashboard/generate/page.tsx b/src/app/dashboard/generate/page.tsx index c6bffcf..8e2084f 100644 --- a/src/app/dashboard/generate/page.tsx +++ b/src/app/dashboard/generate/page.tsx @@ -10,12 +10,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUtils'; import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; import { getGameConfig } from '@/config/tcg-games'; -import type { TCGGame } from '@/types'; +import type { TCGGame, GeneratedCard, CardGenerationParams } from '@/types'; export default function GenerateCardPage() { const { user } = useAuth(); const { toast } = useToast(); - const [generatedCard, setGeneratedCard] = useState<{ imageBase64: string; prompt: string; params?: any } | null>(null); + const [generatedCard, setGeneratedCard] = useState<(GeneratedCard & { params?: CardGenerationParams }) | null>(null); const [isSaving, setIsSaving] = useState(false); const handleCardGenerated = (imageBase64: string, prompt: string, params: any) => { diff --git a/src/components/cards/PhotoCardGeneratorFormOnly.tsx b/src/components/cards/PhotoCardGeneratorFormOnly.tsx index 7ac1918..38eace4 100644 --- a/src/components/cards/PhotoCardGeneratorFormOnly.tsx +++ b/src/components/cards/PhotoCardGeneratorFormOnly.tsx @@ -19,26 +19,16 @@ 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 } from '@/types'; - -const languages = [ - { value: 'english', label: 'English' }, - { value: 'japanese', label: 'Japanese' }, - { value: 'chinese', label: 'Chinese' }, - { value: 'korean', label: 'Korean' }, - { value: 'spanish', label: 'Spanish' }, - { value: 'french', label: 'French' }, - { value: 'german', label: 'German' }, - { value: 'italian', label: 'Italian' }, -] as const; +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: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']), + 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: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']), + 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()), @@ -71,7 +61,7 @@ export function PhotoCardGeneratorFormOnly({ onCardGenerated, initialValues }: P const { user } = useAuth(); const { hasOpenAIKey } = useApiKeys(); const [isGenerating, setIsGenerating] = useState(false); - const [generatedCard, setGeneratedCard] = useState<{ imageBase64: string; prompt: string } | null>(null); + const [generatedCard, setGeneratedCard] = useState(null); const [error, setError] = useState(''); const [imagePreview, setImagePreview] = useState(initialValues?.photoDataUri || null); const { toast } = useToast(); @@ -401,7 +391,7 @@ export function PhotoCardGeneratorFormOnly({ onCardGenerated, initialValues }: P - {languages.map((lang) => ( + {LANGUAGES.map((lang) => ( {lang.label} diff --git a/src/components/cards/TCGCardGenerator.tsx b/src/components/cards/TCGCardGenerator.tsx index 1f0d28b..6e7d6c4 100644 --- a/src/components/cards/TCGCardGenerator.tsx +++ b/src/components/cards/TCGCardGenerator.tsx @@ -17,38 +17,20 @@ import { Loader2, Download, Upload } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; import { downloadCardFromBase64 } from '@/utils/downloadUtils'; import { useAuth } from '@/hooks/useAuth'; -import type { TCGGame, CardGenerationParams } from '@/types'; +import type { TCGGame, CardGenerationParams, GeneratedCard } from '@/types'; import { TCG_GAMES, getGameConfig, getDefaultStats } from '@/config/tcg-games'; - -const languages = [ - { value: 'english', label: 'English' }, - { value: 'japanese', label: 'Japanese' }, - { value: 'chinese', label: 'Chinese' }, - { value: 'korean', label: 'Korean' }, - { value: 'spanish', label: 'Spanish' }, - { value: 'french', label: 'French' }, - { value: 'german', label: 'German' }, - { value: 'italian', label: 'Italian' }, -] as const; - -const models = [ - { value: 'imagen-4.0-ultra-generate-001', label: 'Imagen 4.0 Ultra' }, - { value: 'imagen-4.0-generate-001', label: 'Imagen 4.0 Standard' }, - { value: 'imagen-4.0-fast-generate-001', label: 'Imagen 4.0 Fast' }, - { value: 'gemini-2.5-flash-image', label: 'Gemini 2.5 Flash (Nano Banana)' }, - { value: 'gemini-3-pro-image-preview', label: 'Gemini 3 Pro Image Preview' }, -] as const; +import { LANGUAGES, AI_MODELS, tcgGameSchema, languageSchema, aiModelSchema } from '@/constants'; const cardGeneratorSchema = z.object({ - game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']), + game: tcgGameSchema, characterName: z.string().min(1, 'Character name is required'), characterType: z.string().min(1, 'Type is required'), isSpecialArt: z.boolean(), isHolo: z.boolean(), backgroundDescription: z.string().min(10, 'Background description must be at least 10 characters'), characterDescription: z.string().min(10, 'Character description must be at least 10 characters'), - language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']), - model: z.enum(['imagen-4.0-ultra-generate-001', 'imagen-4.0-generate-001', 'imagen-4.0-fast-generate-001', 'gemini-2.5-flash-image', 'gemini-3-pro-image-preview']), + language: languageSchema, + model: aiModelSchema, // All possible game-specific stats (will be filtered based on selected game) hp: z.number().optional(), attackName1: z.string().optional(), @@ -87,7 +69,7 @@ interface TCGCardGeneratorProps { export function TCGCardGenerator({ onCardGenerated, initialValues }: TCGCardGeneratorProps) { const { user } = useAuth(); const [isGenerating, setIsGenerating] = useState(false); - const [generatedCard, setGeneratedCard] = useState<{ imageBase64: string; prompt: string } | null>(null); + const [generatedCard, setGeneratedCard] = useState(null); const [error, setError] = useState(''); const { toast } = useToast(); @@ -342,7 +324,7 @@ export function TCGCardGenerator({ onCardGenerated, initialValues }: TCGCardGene - {languages.map((lang) => ( + {LANGUAGES.map((lang) => ( {lang.label} @@ -361,7 +343,7 @@ export function TCGCardGenerator({ onCardGenerated, initialValues }: TCGCardGene - {models.map((model) => ( + {AI_MODELS.map((model) => ( {model.label} diff --git a/src/config/tcg-games.ts b/src/config/tcg-games.ts index 2174b50..4f7280f 100644 --- a/src/config/tcg-games.ts +++ b/src/config/tcg-games.ts @@ -1,4 +1,4 @@ -import type { TCGGame } from '@/types'; +import type { TCGGame } from '@/constants'; export interface TCGGameConfig { id: TCGGame; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..22d6e61 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,53 @@ +/** + * Shared constants and enums used throughout the application + */ + +import { z } from 'zod'; + +// TCG Game Types +export const TCG_GAME_VALUES = ['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball'] as const; +export type TCGGame = typeof TCG_GAME_VALUES[number]; + +// Zod schema for TCG games +export const tcgGameSchema = z.enum(TCG_GAME_VALUES); + +// Language Types +export const LANGUAGE_VALUES = ['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian'] as const; +export type Language = typeof LANGUAGE_VALUES[number]; + +// Zod schema for languages +export const languageSchema = z.enum(LANGUAGE_VALUES); + +// Language options for dropdowns +export const LANGUAGES = [ + { value: 'english', label: 'English' }, + { value: 'japanese', label: 'Japanese' }, + { value: 'chinese', label: 'Chinese' }, + { value: 'korean', label: 'Korean' }, + { value: 'spanish', label: 'Spanish' }, + { value: 'french', label: 'French' }, + { value: 'german', label: 'German' }, + { value: 'italian', label: 'Italian' }, +] as const; + +// AI Model Types +export const AI_MODEL_VALUES = [ + 'imagen-4.0-ultra-generate-001', + 'imagen-4.0-generate-001', + 'imagen-4.0-fast-generate-001', + 'gemini-2.5-flash-image', + 'gemini-3-pro-image-preview' +] as const; +export type AIModel = typeof AI_MODEL_VALUES[number]; + +// Zod schema for AI models +export const aiModelSchema = z.enum(AI_MODEL_VALUES); + +// AI Model options for dropdowns +export const AI_MODELS = [ + { value: 'imagen-4.0-ultra-generate-001', label: 'Imagen 4.0 Ultra' }, + { value: 'imagen-4.0-generate-001', label: 'Imagen 4.0 Standard' }, + { value: 'imagen-4.0-fast-generate-001', label: 'Imagen 4.0 Fast' }, + { value: 'gemini-2.5-flash-image', label: 'Gemini 2.5 Flash (Nano Banana)' }, + { value: 'gemini-3-pro-image-preview', label: 'Gemini 3 Pro Image Preview (Nano Banana Pro)' }, +] as const; diff --git a/src/types/index.ts b/src/types/index.ts index ab1eef9..e92447e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,7 +1,7 @@ import type { Timestamp } from 'firebase/firestore'; +import type { TCGGame, Language, AIModel } from '@/constants'; -// Supported TCG games -export type TCGGame = 'pokemon' | 'onepiece' | 'lorcana' | 'magic' | 'dragonball'; +export type { TCGGame } from '@/constants'; export interface PokemonCard { id: string; // Firestore document ID @@ -40,8 +40,8 @@ export interface CardGenerationParams { isHolo: boolean; // Holographic effect backgroundDescription: string; characterDescription: string; - language: 'english' | 'japanese' | 'chinese' | 'korean' | 'spanish' | 'french' | 'german' | 'italian'; - model: 'imagen-4.0-ultra-generate-001' | 'imagen-4.0-generate-001' | 'imagen-4.0-fast-generate-001' | 'gemini-2.5-flash-image' | 'gemini-3-pro-image-preview'; + language: Language; + model: AIModel; // Game-specific fields (optional, used based on game type) // Pokemon @@ -109,7 +109,7 @@ export interface PhotoCardGenerationParams { characterName: string; characterType: string; styleDescription: string; - language: 'english' | 'japanese' | 'chinese' | 'korean' | 'spanish' | 'french' | 'german' | 'italian'; + language: Language; // Game-specific optional stats (similar to CardGenerationParams) hp?: number;