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.
TCGs activos en producción
Sección titulada «TCGs activos en producción»- 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.
Principios firmes
Sección titulada «Principios firmes»1. Cero defaults a 'pokemon'
Sección titulada «1. Cero defaults a 'pokemon'»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 undefinedif (!tcg) return <TcgSelector availableTcgs={tcgs} />;2. Server components fetchean getTcgs()
Sección titulada «2. Server components fetchean getTcgs()»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”.
4. Sin iconography franchise en UI core
Sección titulada «4. Sin iconography franchise en UI core»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.
5. Copy genérico para acciones globales
Sección titulada «5. Copy genérico para acciones globales»| 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.
Patrón para nuevos TCGs
Sección titulada «Patrón para nuevos TCGs»Cuando se agrega un TCG nuevo (proceso completo en tcgcards-api), del lado del frontend hay que:
getTcgs()ya devuelve el nuevo TCG automáticamente — selectores y filtros lo toman solos.- Verificar que aparece en selectores (
/sets,/cards, filtros). - Verificar que el
TCG_LABELSensrc/app/(public)/cards/[id]/page.tsxy similar tiene el nuevo slug → nombre amigable. - 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á enTCG_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 testtcg-hubs.test.ts. - Agregar el orden del TCG nuevo en
src/lib/tcg-order.tsy su logo enExploreByTcgSection.
TCG_LABELS
Sección titulada «TCG_LABELS»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.
Atlas Search y multi-TCG
Sección titulada «Atlas Search y multi-TCG»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).
/search sin query
Sección titulada «/search sin query»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.
Checklist al agregar nuevo kind de listing
Sección titulada «Checklist al agregar nuevo kind de listing»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).