feat: refactor

This commit is contained in:
xavidop
2025-11-21 10:10:00 +01:00
parent abd98f8009
commit 01ef82b4d9
12 changed files with 92 additions and 64 deletions
+3 -2
View File
@@ -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(),
+4 -3
View File
@@ -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(),
+3 -2
View File
@@ -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:<mimetype>;base64,<encoded_data>'."
),
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<typeof ScanTCGCardInputSchema>;
@@ -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()
});
@@ -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<Partial<GenerateFromPhotoInput> | 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({
@@ -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();
+2 -2
View File
@@ -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<Partial<CardGenerationParams> | undefined>(undefined);
+2 -2
View File
@@ -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) => {
@@ -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<GeneratedCard | null>(null);
const [error, setError] = useState<string>('');
const [imagePreview, setImagePreview] = useState<string | null>(initialValues?.photoDataUri || null);
const { toast } = useToast();
@@ -401,7 +391,7 @@ export function PhotoCardGeneratorFormOnly({ onCardGenerated, initialValues }: P
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>
{languages.map((lang) => (
{LANGUAGES.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
{lang.label}
</SelectItem>
+8 -26
View File
@@ -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<GeneratedCard | null>(null);
const [error, setError] = useState<string>('');
const { toast } = useToast();
@@ -342,7 +324,7 @@ export function TCGCardGenerator({ onCardGenerated, initialValues }: TCGCardGene
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>
{languages.map((lang) => (
{LANGUAGES.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
{lang.label}
</SelectItem>
@@ -361,7 +343,7 @@ export function TCGCardGenerator({ onCardGenerated, initialValues }: TCGCardGene
<SelectValue placeholder="Select model" />
</SelectTrigger>
<SelectContent>
{models.map((model) => (
{AI_MODELS.map((model) => (
<SelectItem key={model.value} value={model.value}>
{model.label}
</SelectItem>
+1 -1
View File
@@ -1,4 +1,4 @@
import type { TCGGame } from '@/types';
import type { TCGGame } from '@/constants';
export interface TCGGameConfig {
id: TCGGame;
+53
View File
@@ -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;
+5 -5
View File
@@ -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;