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!