From ff22a83b6de12839ab0bbbc0882e2d74444f43c3 Mon Sep 17 00:00:00 2001 From: xavidop Date: Fri, 21 Nov 2025 11:13:43 +0100 Subject: [PATCH] feat: refactors --- src/ai/flows/generate-tcg-card-from-photo.ts | 97 ++++--- src/ai/flows/generate-tcg-card.ts | 150 ++++------ src/ai/prompts/game-prompts.ts | 149 ++++++++++ src/app/api/download/route.ts | 34 ++- src/app/api/generate-card-from-photo/route.ts | 49 ++-- src/app/api/generate-card/route.ts | 50 ++-- src/app/api/generate-video/route.ts | 14 +- .../collection/[cardId]/edit/page.tsx | 2 +- src/app/dashboard/collection/page.tsx | 2 +- src/app/layout.tsx | 4 +- src/app/login/page.tsx | 86 +----- src/app/signup/page.tsx | 87 +----- src/components/auth/AuthPageLayout.tsx | 84 ++++++ src/components/auth/AuthPageWrapper.tsx | 36 +++ src/components/cards/CardForm.tsx | 6 +- src/constants/index.ts | 137 +++++++++ src/lib/api-handler.ts | 68 +++++ src/lib/api-response.ts | 99 +++++++ src/lib/firestore.ts | 274 ++++++++++-------- src/lib/logger.ts | 109 +++++++ src/lib/utils.ts | 65 ++++- src/lib/validation.ts | 151 ++++++++++ src/utils/downloadUtils.ts | 174 +++++------ src/utils/generationParamsUtils.ts | 113 +++++--- src/utils/imageUtils.ts | 107 ++++--- src/utils/shareUtils.ts | 137 ++++----- src/utils/storageUtils.ts | 169 ++++++----- 27 files changed, 1657 insertions(+), 796 deletions(-) create mode 100644 src/ai/prompts/game-prompts.ts create mode 100644 src/components/auth/AuthPageLayout.tsx create mode 100644 src/components/auth/AuthPageWrapper.tsx create mode 100644 src/lib/api-handler.ts create mode 100644 src/lib/api-response.ts create mode 100644 src/lib/logger.ts create mode 100644 src/lib/validation.ts diff --git a/src/ai/flows/generate-tcg-card-from-photo.ts b/src/ai/flows/generate-tcg-card-from-photo.ts index 949c7de..24a7d4e 100644 --- a/src/ai/flows/generate-tcg-card-from-photo.ts +++ b/src/ai/flows/generate-tcg-card-from-photo.ts @@ -15,6 +15,13 @@ import { getUserApiKeys } from '@/lib/firestore'; import { getGameConfig } from '@/config/tcg-games'; import type { TCGGame } from '@/types'; import { tcgGameSchema, languageSchema } from '@/constants'; +import { + generatePokemonPrompt, + generateOnePiecePrompt, + generateLorcanaPrompt, + generateMagicPrompt, + generateDragonBallPrompt, +} from '@/ai/prompts/game-prompts'; const GenerateFromPhotoInputSchema = z.object({ userId: z.string().min(1, "User ID is required"), @@ -147,19 +154,59 @@ function generatePhotoBasedCardPrompt(params: GenerateFromPhotoInput): string { switch (game) { case 'pokemon': - gameSpecificPrompt = generatePokemonPhotoPrompt(params); + gameSpecificPrompt = generatePokemonPrompt({ + characterName: params.characterName, + characterType: params.characterType, + hp: params.hp, + attackName1: params.attackName1, + attackDamage1: params.attackDamage1, + attackName2: params.attackName2, + attackDamage2: params.attackDamage2, + weakness: params.weakness, + resistance: params.resistance, + retreatCost: params.retreatCost, + language: params.language, + }); break; case 'onepiece': - gameSpecificPrompt = generateOnePiecePhotoPrompt(params); + gameSpecificPrompt = generateOnePiecePrompt({ + characterName: params.characterName, + characterType: params.characterType, + power: params.power, + cost: params.cost, + counter: params.counter, + }); break; case 'lorcana': - gameSpecificPrompt = generateLorcanaPhotoPrompt(params); + gameSpecificPrompt = generateLorcanaPrompt({ + characterName: params.characterName, + characterType: params.characterType, + inkCost: params.inkCost, + strength: params.strength, + willpower: params.willpower, + lore: params.lore, + inkable: params.inkable, + }); break; case 'magic': - gameSpecificPrompt = generateMagicPhotoPrompt(params); + gameSpecificPrompt = generateMagicPrompt({ + characterName: params.characterName, + characterType: params.characterType, + manaCost: params.manaCost, + cardType: params.cardType, + subType: params.subType, + powerToughness: params.powerToughness, + }); break; case 'dragonball': - gameSpecificPrompt = generateDragonBallPhotoPrompt(params); + gameSpecificPrompt = generateDragonBallPrompt({ + characterName: params.characterName, + characterType: params.characterType, + combatPower: params.combatPower, + comboCost: params.comboCost, + comboEnergy: params.comboEnergy, + era: params.era, + }); break; } @@ -175,43 +222,3 @@ ${languageInstruction} The final output must look like an authentic ${gameName} trading card with all text clearly legible and properly formatted. Make sure the information is displayed clearly and the character illustration feels natural within the adapted photographic style.`; } - -function generatePokemonPhotoPrompt(params: GenerateFromPhotoInput): string { - const { characterName, characterType, hp = 130, attackName1 = "Quick Attack", attackDamage1 = 60, attackName2 = "Special Move", attackDamage2 = 90, weakness = "Fighting", resistance = "Psychic", retreatCost = 2, language = 'english' } = params; - - const languageNote = language !== 'english' ? `All attack names and descriptions should be written in ${language}.` : ''; - - return `A regular classic Pokémon trading card (not full art), featuring the Pokémon "${characterName}". The card has a standard vertical layout with a detailed illustration in the main area. The Pokémon "${characterName}", a ${characterType} type, should be depicted as the main subject. - -The card layout includes: The top left corner displays "${characterName}" in a stylized font, with "HP ${hp}" next to it in red. Below the Pokémon's name, the ${characterType} type symbol is clearly visible. In the lower section, the card has two attacks listed. The first attack is ${attackName1}, deals ${attackDamage1} damage. The second attack is ${attackName2}, deals ${attackDamage2} damage. - -Below the attacks, the Weakness is ${weakness} (x2), Resistance is ${resistance} (-30), and Retreat Cost shows ${retreatCost} energy symbols. The bottom edge of the card features a thin line of text indicating the rarity and copyright information. - -${languageNote} - -The overall style should match official Pokémon TCG card design with proper fonts, layout, and professional quality artwork.`; -} - -function generateOnePiecePhotoPrompt(params: GenerateFromPhotoInput): string { - const { characterName, characterType, power = 5000, cost = 4, counter = 1000 } = params; - - return `Card layout: One Piece Card Game style with "${characterName}" prominently displayed. ${characterType} color indicator. Cost: ${cost}, Power: ${power}, Counter: ${counter}. Include attribute icons and ability text box.`; -} - -function generateLorcanaPhotoPrompt(params: GenerateFromPhotoInput): string { - const { characterName, characterType, inkCost = 3, strength = 2, willpower = 3, lore = 2, inkable = true } = params; - - return `Card layout: Disney Lorcana style with "${characterName}" as title. ${characterType} ink color. Ink cost: ${inkCost}, Strength: ${strength}, Willpower: ${willpower}, Lore: ${lore}. ${inkable ? 'Include inkwell symbol.' : 'No inkwell symbol.'}`; -} - -function generateMagicPhotoPrompt(params: GenerateFromPhotoInput): string { - const { characterName, manaCost = '{2}{G}', cardType = 'Creature', subType = 'Warrior', powerToughness = '3/3' } = params; - - return `Card layout: Magic: The Gathering style. Card name: "${characterName}", Mana cost: ${manaCost} in top right. Type line: "${cardType} — ${subType}". ${cardType.toLowerCase().includes('creature') ? `Power/Toughness: ${powerToughness}` : ''}`; -} - -function generateDragonBallPhotoPrompt(params: GenerateFromPhotoInput): string { - const { characterName, characterType, combatPower = 20000, comboCost = 1, comboEnergy = 5000, era = 'Universe Survival Saga' } = params; - - return `Card layout: Dragon Ball Super Card Game style. Character: "${characterName}", ${characterType} color. Combat Power: ${combatPower}, Combo Cost: ${comboCost}, Combo Energy: ${comboEnergy}, Era: ${era}.`; -} diff --git a/src/ai/flows/generate-tcg-card.ts b/src/ai/flows/generate-tcg-card.ts index 360a5c4..bb7e6de 100644 --- a/src/ai/flows/generate-tcg-card.ts +++ b/src/ai/flows/generate-tcg-card.ts @@ -15,6 +15,13 @@ import { getUserApiKeys } from '@/lib/firestore'; import type { TCGGame } from '@/types'; import { getGameConfig } from '@/config/tcg-games'; import { tcgGameSchema, languageSchema, aiModelSchema } from '@/constants'; +import { + generatePokemonPrompt as sharedPokemonPrompt, + generateOnePiecePrompt as sharedOnePiecePrompt, + generateLorcanaPrompt as sharedLorcanaPrompt, + generateMagicPrompt as sharedMagicPrompt, + generateDragonBallPrompt as sharedDragonBallPrompt, +} from '@/ai/prompts/game-prompts'; const GenerateTCGCardInputSchema = z.object({ userId: z.string().min(1, "User ID is required"), @@ -200,19 +207,60 @@ function generateCardPrompt(params: GenerateTCGCardInput): string { switch (game) { case 'pokemon': - gameSpecificPrompt = generatePokemonPrompt(params); + gameSpecificPrompt = sharedPokemonPrompt({ + characterName: params.characterName, + characterType: params.characterType, + hp: params.hp, + attackName1: params.attackName1, + attackDamage1: params.attackDamage1, + attackName2: params.attackName2, + attackDamage2: params.attackDamage2, + weakness: params.weakness, + resistance: params.resistance, + retreatCost: params.retreatCost, + language: params.language, + }); break; case 'onepiece': - gameSpecificPrompt = generateOnePiecePrompt(params); + gameSpecificPrompt = sharedOnePiecePrompt({ + characterName: params.characterName, + characterType: params.characterType, + power: params.power, + cost: params.cost, + counter: params.counter, + lifePoints: params.lifePoints, + }); break; case 'lorcana': - gameSpecificPrompt = generateLorcanaPrompt(params); + gameSpecificPrompt = sharedLorcanaPrompt({ + characterName: params.characterName, + characterType: params.characterType, + inkCost: params.inkCost, + strength: params.strength, + willpower: params.willpower, + lore: params.lore, + inkable: params.inkable, + }); break; case 'magic': - gameSpecificPrompt = generateMagicPrompt(params); + gameSpecificPrompt = sharedMagicPrompt({ + characterName: params.characterName, + characterType: params.characterType, + manaCost: params.manaCost, + cardType: params.cardType, + subType: params.subType, + powerToughness: params.powerToughness, + }); break; case 'dragonball': - gameSpecificPrompt = generateDragonBallPrompt(params); + gameSpecificPrompt = sharedDragonBallPrompt({ + characterName: params.characterName, + characterType: params.characterType, + combatPower: params.combatPower, + comboCost: params.comboCost, + comboEnergy: params.comboEnergy, + era: params.era, + }); break; } @@ -226,95 +274,3 @@ The overall style should match official ${gameName} card design with proper font Make sure all information is displayed clearly and legibly. The output should just be the card itself.`; } - -function generatePokemonPrompt(params: GenerateTCGCardInput): string { - const { - characterName, - characterType, - hp = 130, - attackName1 = "Quick Attack", - attackDamage1 = 60, - attackName2 = "Special Move", - attackDamage2 = 90, - weakness = "Fighting", - resistance = "Psychic", - retreatCost = 2, - } = params; - - return `The card layout includes: The top left corner displays "${characterName}" in a stylized font, with "HP ${hp}" next to it in red. Below the Pokémon's name, the ${characterType} type symbol is clearly visible. In the lower section, the card has two attacks listed. The first attack is ${attackName1}, deals ${attackDamage1} damage. The second attack is ${attackName2}, deals ${attackDamage2} damage. - -Below the attacks, the Weakness is ${weakness} (x2), Resistance is ${resistance} (-30), and Retreat Cost shows ${retreatCost} energy symbols. The bottom edge of the card features a thin line of text indicating the rarity and copyright information.`; -} - -function generateOnePiecePrompt(params: GenerateTCGCardInput): string { - const { - characterName, - characterType, - power = 5000, - cost = 4, - counter = 1000, - lifePoints, - } = params; - - const leaderInfo = lifePoints ? `As a Leader card, it displays ${lifePoints} life points in the top corner.` : ''; - - return `The card layout follows One Piece Card Game design: The character name "${characterName}" appears prominently at the top. The card shows a ${characterType} color indicator. The cost of ${cost} is displayed in the top left corner. The power value of ${power} is shown prominently. The counter value of ${counter} appears at the bottom. ${leaderInfo} - -The card includes attribute icons and a text box for abilities. The bottom features the One Piece Card Game logo and card number.`; -} - -function generateLorcanaPrompt(params: GenerateTCGCardInput): string { - const { - characterName, - characterType, - inkCost = 3, - strength = 2, - willpower = 3, - lore = 2, - inkable = true, - } = params; - - const inkableIndicator = inkable ? 'The card has an inkwell symbol in the bottom left, indicating it is inkable.' : 'No inkwell symbol appears, as this card is not inkable.'; - - return `The card layout follows Disney Lorcana design: "${characterName}" appears as the character name at the top. The ${characterType} ink color is indicated by the card's color scheme and symbol. The ink cost of ${inkCost} is displayed in a prominent circle in the top left corner. - -The card shows strength of ${strength} (shield icon), willpower of ${willpower} (hexagon icon), and lore value of ${lore} (diamond icon) clearly displayed. ${inkableIndicator} - -The card includes a text box for character abilities and flavor text in the Disney Lorcana style. The bottom features the Lorcana logo, card number, and rarity symbol.`; -} - -function generateMagicPrompt(params: GenerateTCGCardInput): string { - const { - characterName, - characterType, - manaCost = '{2}{G}', - cardType = 'Creature', - subType = 'Elf Warrior', - powerToughness = '3/3', - } = params; - - return `The card layout follows Magic: The Gathering design: "${characterName}" appears as the card name at the top. The mana cost ${manaCost} is displayed in the top right corner using standard Magic mana symbols. The ${characterType} color frame is used. - -The type line reads "${cardType} — ${subType}". The main illustration takes up the center portion of the card. Below the illustration is a text box for abilities and flavor text. - -${cardType.toLowerCase().includes('creature') ? `The bottom right corner displays the power/toughness of ${powerToughness}.` : ''} - -The bottom features the expansion symbol, card number, artist credit, and copyright information in standard Magic: The Gathering layout.`; -} - -function generateDragonBallPrompt(params: GenerateTCGCardInput): string { - const { - characterName, - characterType, - combatPower = 20000, - comboCost = 1, - comboEnergy = 5000, - era = 'Universe Survival Saga', - } = params; - - return `The card layout follows Dragon Ball Super Card Game design: The character name "${characterName}" appears at the top in the distinctive Dragon Ball font. The card uses a ${characterType} color border and energy symbols. - -The combat power of ${combatPower} is prominently displayed in the top right corner. The combo cost shows ${comboCost} and combo energy displays ${comboEnergy}. The era "${era}" is indicated on the card. - -The card includes energy cost symbols, special traits, and a text box for card effects. The character illustration is dynamic and action-packed. The bottom features the Dragon Ball Super Card Game logo, card number, and rarity indicator.`; -} diff --git a/src/ai/prompts/game-prompts.ts b/src/ai/prompts/game-prompts.ts new file mode 100644 index 0000000..a0f81bc --- /dev/null +++ b/src/ai/prompts/game-prompts.ts @@ -0,0 +1,149 @@ +/** + * Shared game-specific prompt generators for TCG card generation. + * Used by both standard card generation and photo-based card generation. + */ + +interface BaseCardParams { + characterName: string; + characterType: string; + language?: string; +} + +interface PokemonParams extends BaseCardParams { + hp?: number; + attackName1?: string; + attackDamage1?: number; + attackName2?: string; + attackDamage2?: number; + weakness?: string; + resistance?: string; + retreatCost?: number; +} + +interface OnePieceParams extends BaseCardParams { + power?: number; + cost?: number; + counter?: number; + lifePoints?: number; +} + +interface LorcanaParams extends BaseCardParams { + inkCost?: number; + strength?: number; + willpower?: number; + lore?: number; + inkable?: boolean; +} + +interface MagicParams extends BaseCardParams { + manaCost?: string; + cardType?: string; + subType?: string; + powerToughness?: string; +} + +interface DragonBallParams extends BaseCardParams { + combatPower?: number; + comboCost?: number; + comboEnergy?: number; + era?: string; +} + +export function generatePokemonPrompt(params: PokemonParams): string { + const { + characterName, + characterType, + hp = 130, + attackName1 = "Quick Attack", + attackDamage1 = 60, + attackName2 = "Special Move", + attackDamage2 = 90, + weakness = "Fighting", + resistance = "Psychic", + retreatCost = 2, + language = 'english', + } = params; + + const languageNote = language !== 'english' ? `All attack names and descriptions should be written in ${language}.` : ''; + + return `The card layout includes: The top left corner displays "${characterName}" in a stylized font, with "HP ${hp}" next to it in red. Below the Pokémon's name, the ${characterType} type symbol is clearly visible. In the lower section, the card has two attacks listed. The first attack is ${attackName1}, deals ${attackDamage1} damage. The second attack is ${attackName2}, deals ${attackDamage2} damage. + +Below the attacks, the Weakness is ${weakness} (x2), Resistance is ${resistance} (-30), and Retreat Cost shows ${retreatCost} energy symbols. The bottom edge of the card features a thin line of text indicating the rarity and copyright information. + +${languageNote} + +The overall style should match official Pokémon TCG card design with proper fonts, layout, and professional quality artwork.`; +} + +export function generateOnePiecePrompt(params: OnePieceParams): string { + const { + characterName, + characterType, + power = 5000, + cost = 4, + counter = 1000, + lifePoints, + } = params; + + const leaderInfo = lifePoints ? `As a Leader card, it displays ${lifePoints} life points in the top corner.` : ''; + + return `The card layout follows One Piece Card Game design: The character name "${characterName}" appears prominently at the top. The card shows a ${characterType} color indicator. The cost of ${cost} is displayed in the top left corner. The power value of ${power} is shown prominently. The counter value of ${counter} appears at the bottom. ${leaderInfo} + +The card includes attribute icons and a text box for abilities. The bottom features the One Piece Card Game logo and card number.`; +} + +export function generateLorcanaPrompt(params: LorcanaParams): string { + const { + characterName, + characterType, + inkCost = 3, + strength = 2, + willpower = 3, + lore = 2, + inkable = true, + } = params; + + const inkableIndicator = inkable ? 'The card has an inkwell symbol in the bottom left, indicating it is inkable.' : 'No inkwell symbol appears, as this card is not inkable.'; + + return `The card layout follows Disney Lorcana design: "${characterName}" appears as the character name at the top. The ${characterType} ink color is indicated by the card's color scheme and symbol. The ink cost of ${inkCost} is displayed in a prominent circle in the top left corner. + +The card shows strength of ${strength} (shield icon), willpower of ${willpower} (hexagon icon), and lore value of ${lore} (diamond icon) clearly displayed. ${inkableIndicator} + +The card includes a text box for character abilities and flavor text in the Disney Lorcana style. The bottom features the Lorcana logo, card number, and rarity symbol.`; +} + +export function generateMagicPrompt(params: MagicParams): string { + const { + characterName, + characterType, + manaCost = '{2}{G}', + cardType = 'Creature', + subType = 'Elf Warrior', + powerToughness = '3/3', + } = params; + + return `The card layout follows Magic: The Gathering design: "${characterName}" appears as the card name at the top. The mana cost ${manaCost} is displayed in the top right corner using standard Magic mana symbols. The ${characterType} color frame is used. + +The type line reads "${cardType} — ${subType}". The main illustration takes up the center portion of the card. Below the illustration is a text box for abilities and flavor text. + +${cardType.toLowerCase().includes('creature') ? `The bottom right corner displays the power/toughness of ${powerToughness}.` : ''} + +The bottom features the expansion symbol, card number, artist credit, and copyright information in standard Magic: The Gathering layout.`; +} + +export function generateDragonBallPrompt(params: DragonBallParams): string { + const { + characterName, + characterType, + combatPower = 20000, + comboCost = 1, + comboEnergy = 5000, + era = 'Universe Survival Saga', + } = params; + + return `The card layout follows Dragon Ball Super Card Game design: The character name "${characterName}" appears at the top in the distinctive Dragon Ball font. The card uses a ${characterType} color border and energy symbols. + +The combat power of ${combatPower} is prominently displayed in the top right corner. The combo cost shows ${comboCost} and combo energy displays ${comboEnergy}. The era "${era}" is indicated on the card. + +The card includes energy cost symbols, special traits, and a text box for card effects. The character illustration is dynamic and action-packed. The bottom features the Dragon Ball Super Card Game logo, card number, and rarity indicator.`; +} diff --git a/src/app/api/download/route.ts b/src/app/api/download/route.ts index ffea9d5..5f55229 100644 --- a/src/app/api/download/route.ts +++ b/src/app/api/download/route.ts @@ -1,5 +1,22 @@ import { NextRequest, NextResponse } from 'next/server'; +import { ERROR_MESSAGES, CACHE_CONTROL } from '@/constants'; +/** + * Validates if a URL is from Firebase Storage + * @param url - URL to validate + * @returns true if URL is from Firebase Storage (production or emulator) + */ +function isValidFirebaseStorageUrl(url: string): boolean { + return url.includes('firebasestorage.googleapis.com') || + url.includes('localhost:9199') || + url.includes('127.0.0.1:9199'); +} + +/** + * GET /api/download + * Proxy endpoint to download files from Firebase Storage to avoid CORS issues + * Only allows Firebase Storage URLs for security + */ export async function GET(request: NextRequest) { try { const url = request.nextUrl.searchParams.get('url'); @@ -11,11 +28,8 @@ export async function GET(request: NextRequest) { ); } - // Validate that the URL is from Firebase Storage (production or emulator) - const isFirebaseStorage = url.includes('firebasestorage.googleapis.com'); - const isFirebaseEmulator = url.includes('localhost:9199') || url.includes('127.0.0.1:9199'); - - if (!isFirebaseStorage && !isFirebaseEmulator) { + // Validate URL is from Firebase Storage + if (!isValidFirebaseStorageUrl(url)) { return NextResponse.json( { error: 'Only Firebase Storage URLs are allowed' }, { status: 403 } @@ -33,7 +47,6 @@ export async function GET(request: NextRequest) { throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); } - // Get the content type from the original response const contentType = response.headers.get('content-type') || 'application/octet-stream'; // Stream the response @@ -41,7 +54,7 @@ export async function GET(request: NextRequest) { headers: { 'Content-Type': contentType, 'Content-Disposition': 'attachment', - 'Cache-Control': 'public, max-age=31536000', + 'Cache-Control': CACHE_CONTROL.ONE_YEAR, 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET', 'Access-Control-Allow-Headers': 'Content-Type', @@ -49,8 +62,13 @@ export async function GET(request: NextRequest) { }); } catch (error) { console.error('Download proxy error:', error); + + const errorMessage = error instanceof Error + ? error.message + : ERROR_MESSAGES.IMAGE_DOWNLOAD_FAILED; + return NextResponse.json( - { error: 'Failed to download file' }, + { error: errorMessage }, { status: 500 } ); } diff --git a/src/app/api/generate-card-from-photo/route.ts b/src/app/api/generate-card-from-photo/route.ts index 434af5c..3d6c0a5 100644 --- a/src/app/api/generate-card-from-photo/route.ts +++ b/src/app/api/generate-card-from-photo/route.ts @@ -1,34 +1,23 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; import { generateTCGCardFromPhoto } from '@/ai/flows/generate-tcg-card-from-photo'; +import { handleApiRequest } from '@/lib/api-handler'; +/** + * Required fields for photo-based card generation + */ +const REQUIRED_FIELDS = [ + 'userId', + 'photoDataUri', + 'characterName', + 'characterType', + 'styleDescription', + 'game' +] as const; + +/** + * POST /api/generate-card-from-photo + * Generates a TCG card from a photo using AI + */ export async function POST(request: NextRequest) { - try { - const body = await request.json(); - - // Validate required fields - including userId - const requiredFields = ['userId', 'photoDataUri', 'characterName', 'characterType', 'styleDescription', 'game']; - const missingFields = requiredFields.filter(field => !body[field]); - - if (missingFields.length > 0) { - return NextResponse.json( - { error: `Missing required fields: ${missingFields.join(', ')}` }, - { status: 400 } - ); - } - - // Call the AI flow - const result = await generateTCGCardFromPhoto(body); - - if (result.error) { - return NextResponse.json({ error: result.error }, { status: 500 }); - } - - return NextResponse.json(result); - } catch (error) { - console.error('API route error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } + return handleApiRequest(request, REQUIRED_FIELDS, generateTCGCardFromPhoto); } diff --git a/src/app/api/generate-card/route.ts b/src/app/api/generate-card/route.ts index 2766aa3..32fb3f5 100644 --- a/src/app/api/generate-card/route.ts +++ b/src/app/api/generate-card/route.ts @@ -1,36 +1,24 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; import { generateTCGCard } from '@/ai/flows/generate-tcg-card'; +import { handleApiRequest } from '@/lib/api-handler'; +/** + * Required fields for card generation + */ +const REQUIRED_FIELDS = [ + 'userId', + 'game', + 'characterName', + 'characterType', + 'backgroundDescription', + 'characterDescription' +] as const; + +/** + * POST /api/generate-card + * Generates a TCG card using AI based on provided parameters + */ export async function POST(request: NextRequest) { - try { - const body = await request.json(); - - // New multi-game flow - const requiredFields = ['userId', 'game', 'characterName', 'characterType', 'backgroundDescription', 'characterDescription']; - const missingFields = requiredFields.filter(field => !body[field]); - - if (missingFields.length > 0) { - return NextResponse.json( - { error: `Missing required fields: ${missingFields.join(', ')}` }, - { status: 400 } - ); - } - - // Call the new AI flow - const result = await generateTCGCard(body); - - if (result.error) { - return NextResponse.json({ error: result.error }, { status: 500 }); - } - - return NextResponse.json(result); - - } catch (error) { - console.error('API route error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } + return handleApiRequest(request, REQUIRED_FIELDS, generateTCGCard); } diff --git a/src/app/api/generate-video/route.ts b/src/app/api/generate-video/route.ts index 2aed781..1ce279c 100644 --- a/src/app/api/generate-video/route.ts +++ b/src/app/api/generate-video/route.ts @@ -1,13 +1,19 @@ import { NextRequest, NextResponse } from 'next/server'; import { generateVideoForExistingCard } from '@/lib/firestore'; +import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '@/constants'; +/** + * POST /api/generate-video + * Starts video generation for an existing card + * This is a background process - the video generation happens asynchronously + */ export async function POST(request: NextRequest) { try { const { userId, cardId } = await request.json(); if (!userId || !cardId) { return NextResponse.json( - { error: 'User ID and Card ID are required' }, + { error: `${ERROR_MESSAGES.USER_ID_REQUIRED} and ${ERROR_MESSAGES.CARD_ID_REQUIRED}` }, { status: 400 } ); } @@ -17,13 +23,15 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, - message: 'Video generation started successfully' + message: SUCCESS_MESSAGES.VIDEO_GENERATED }); } catch (error) { console.error('Error in video generation API:', error); - const errorMessage = error instanceof Error ? error.message : 'Internal server error'; + const errorMessage = error instanceof Error + ? error.message + : ERROR_MESSAGES.VIDEO_GENERATION_FAILED; return NextResponse.json( { error: errorMessage }, diff --git a/src/app/dashboard/collection/[cardId]/edit/page.tsx b/src/app/dashboard/collection/[cardId]/edit/page.tsx index 0253092..433df71 100644 --- a/src/app/dashboard/collection/[cardId]/edit/page.tsx +++ b/src/app/dashboard/collection/[cardId]/edit/page.tsx @@ -226,7 +226,7 @@ export default function EditCardPage() { isSubmitting={isSubmitting} submitButtonText="Save Changes" formTitle="Update Card Information" - formDescription="Modify the details of your Pokémon card." + formDescription="Modify the details of your TCG card." onCancel={() => router.push('/dashboard/collection')} /> diff --git a/src/app/dashboard/collection/page.tsx b/src/app/dashboard/collection/page.tsx index 11a577a..264781e 100644 --- a/src/app/dashboard/collection/page.tsx +++ b/src/app/dashboard/collection/page.tsx @@ -216,7 +216,7 @@ export default function CollectionPage() {

Your collection is empty.

-

Start by scanning or generating your first Pokémon card!

+

Start by scanning or generating your first TCG card!