Ir al contenido

Multi-TCG

tcgcards.cl es multi-TCG desde el día uno. Esto impone reglas firmes en cómo diseñamos UI y copy: ninguna franquicia tiene prioridad visual sobre las demás.

  • Pokémon TCG
  • One Piece Card Game
  • Magic: The Gathering
  • Disney Lorcana TCG
  • Riftbound: League of Legends TCG
  • Gundam Card Game
  • Flesh and Blood
  • Yu-Gi-Oh! TCG

Más se agregan periódicamente. Nunca hardcodear “los TCGs disponibles” en una constante — siempre fetchear con getTcgs() desde la API.

Nunca asumir que el TCG default es Pokémon (ni ninguno otro). Si el usuario llega sin elegir TCG, mostrar:

  • Selector explícito (“Elige tu TCG”) o
  • Vista multi-TCG mezclada (todos los TCGs en la misma grilla) o
  • Empty state que invita a elegir

❌ Mal:

const tcg = searchParams.tcg ?? 'pokemon';

✅ Bien:

const tcg = searchParams.tcg; // puede ser undefined
if (!tcg) return <TcgSelector availableTcgs={tcgs} />;

Si una página o componente necesita la lista de TCGs disponibles, el server component la fetchea y propaga availableTcgs como prop a los client components.

// app/cards/page.tsx (Server Component)
import { getTcgs } from '@/lib/api/catalog';
export default async function Page() {
const availableTcgs = await getTcgs(); // solo los enabled
return <CardsClient availableTcgs={availableTcgs} />;
}

Razón: evita fetches duplicados desde múltiples client components y permite revalidación 5min vía next: { revalidate: 300 } en getTcgs().

3. Placeholders sin iconografía de franquicia

Sección titulada «3. Placeholders sin iconografía de franquicia»

Cuando una carta no tiene imagen disponible, el placeholder es neutral: silueta de carta genérica o icono Image de lucide.

❌ Mal:

  • Pokeball gigante.
  • Logo de Magic.
  • Iconos específicos de One Piece.

✅ Bien:

  • Silueta de carta tipo “trading card” con dimensiones estándar.
  • Icono Image (lucide-react) centrado en gris muted.
  • Texto “Sin imagen disponible”.

Header, footer, navegación, dashboard, admin — nada de logos de TCGs específicos. Solo el logo de tcgcards.

Los logos/imágenes de franquicias aparecen únicamente:

  • En el detalle de cada TCG (/sets?tcg=pokemon).
  • Como label del set/carta (donde corresponde mostrarlo).
  • En selectores de TCG.
Contexto❌ Específico✅ Genérico
CTA principal”Compra Pokémon""Compra cartas TCG”
Empty state”Aún no vendes cartas Pokémon""Aún no vendes cartas”
Resultados”0 cartas Pokémon""Sin resultados”
Email subject”Tu compra Pokémon""Tu compra en tcgcards”

Excepción: páginas filtradas por TCG (ej: /sets?tcg=lorcana) pueden y deben mencionar el TCG en H1 y meta.

Cuando se agrega un TCG nuevo (proceso completo en tcgcards-api), del lado del frontend hay que:

  1. getTcgs() ya devuelve el nuevo TCG automáticamente — selectores y filtros lo toman solos.
  2. Verificar que aparece en selectores (/sets, /cards, filtros).
  3. Verificar que el TCG_LABELS en src/app/(public)/cards/[id]/page.tsx y similar tiene el nuevo slug → nombre amigable.
  4. Agregar su entrada en src/lib/data/tcg-hubs.ts (SEO Tier 2, 2026-07-13). Un hub /cartas/{slug} existe si y solo si el slug está en TCG_HUBS; si falta, el tile de “Explora por TCG” de la home apuntaría a un 404. La entrada define nombre, meta description y la intro con formato fijo (“Cartas sueltas de {TCG}: {tipos de carta}…” + cierre común); las reglas del copy están en el comentario del archivo y las fija el test tcg-hubs.test.ts.
  5. Agregar el orden del TCG nuevo en src/lib/tcg-order.ts y su logo en ExploreByTcgSection.

Mapa local en componentes que necesitan el nombre amigable a partir del slug:

const TCG_LABELS: Record<string, string> = {
pokemon: 'Pokémon',
'one-piece': 'One Piece',
riftbound: 'Riftbound',
lorcana: 'Lorcana',
magic: 'Magic',
gundam: 'Gundam',
'flesh-and-blood': 'Flesh and Blood',
yugioh: 'Yu-Gi-Oh!',
};
function tcgLabel(slug: string): string {
return TCG_LABELS[slug] ?? slug; // fallback al slug si no está mapeado
}

Regla: el fallback es seguro (devuelve el slug crudo). Pero al agregar un TCG nuevo, agregar la entrada en TODOS los componentes con TCG_LABELS para que se vea bonito.

El buscador global (/search) opera sobre todos los TCGs. La grilla resultante mezcla cartas de todos los TCGs por relevancia, no por TCG. Si el usuario quiere filtrar por TCG hay un checkbox multi-select en el sidebar (diferido — ver memoria del proyecto).

Cuando el usuario llega a /search sin query (ej: por marcador), no hacer browse mode automático. Mostrar placeholder con CTA “Explora por TCG” que lleva a /sets?tcg=... (la aggregation de browse por releaseDate es lenta sin índice). Aplicado a 2026-05-08.

Cuando se agrega un kind nuevo (sellado, packs, etc.) revisar TODOS los if/else por kind en cart/orders/checkout. Bug histórico (2026-05-19): el kind “bulk” caía silently al else en CartService/OrderService.

Patrón seguro: usar switch exhaustivo con default: assertNever(kind) para forzar TypeScript a marcar cuando falta un caso.

function processListing(listing: Listing) {
switch (listing.kind) {
case 'card': return processCard(listing);
case 'sealed': return processSealed(listing);
case 'bulk': return processBulk(listing);
case 'accessory': return processAccessory(listing);
default: {
const _exhaustive: never = listing.kind;
throw new Error(`Unhandled kind: ${_exhaustive}`);
}
}
}

Documentado en runbooks. Mencionado acá porque afecta múltiples capas del front (modal de compra, carrito, checkout, emails).