mirror of
https://github.com/xavidop/cardex.git
synced 2026-08-30 09:48:51 +00:00
feat: refactor
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import type { TCGGame } from '@/types';
|
||||
import type { TCGGame } from '@/constants';
|
||||
|
||||
export interface TCGGameConfig {
|
||||
id: TCGGame;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user