Páginas web
Frontend en Next.js 16 App Router (tcgcards-web). Las rutas se definen por archivos page.tsx en src/app/**. Las carpetas con paréntesis como (public), (dashboard), (auth) son route groups y NO aparecen en la URL.
URL pública: https://tcgcards.cl
Páginas públicas (no requieren login)
Sección titulada «Páginas públicas (no requieren login)»| URL | Archivo | Qué hace |
|---|---|---|
/ | src/app/(public)/page.tsx | Home: carrusel de banners, exploración por TCG, sets recientes |
/about | src/app/(public)/about/page.tsx | Sobre tcgcards: qué es, cómo funciona el modelo |
/accesorios | src/app/(public)/accesorios/page.tsx | Catálogo de accesorios con filtros |
/banned | src/app/(public)/banned/page.tsx | Aviso para cuentas baneadas |
/cartas | src/app/(public)/cartas/page.tsx | Feed de cartas individuales más recientes. Filtros de Rareza e Idioma solo con TCG seleccionado (jul 2026; sin TCG el API devuelve facetas vacías y el web ni las renderiza); idioma = chips con conteos y etiquetas en español (CARD_LANGUAGE_LABELS), single-select por URL; cambiar de TCG limpia rareza/atributos/idioma |
/cartas/[tcg] | src/app/(public)/cartas/[tcg]/page.tsx | Hub SEO por TCG (Tier 2, 2026-07-13): H1 “Cartas de {TCG} en Chile”, intro única (contenido estático en src/lib/data/tcg-hubs.ts), grilla de 24 recientes (ISR 300 s), chips de sets. Un hub existe si y solo si el slug está en TCG_HUBS; slug desconocido → 404. Enlazadas desde el footer (“Cartas por juego”), los tiles de la home y el sitemap |
/cards/[id] | src/app/(public)/cards/[id]/page.tsx | Detalle de carta del catálogo: ofertas, ventas recientes. Al pie, bloque de cartas relacionadas (RelatedCards): prioriza el mismo set y rellena con el mismo TCG (con stock, fresco por visita; fail-soft) + link al hub del TCG. La ficha cachea 60 s y las ventas recientes 300 s; las publicaciones siguen sin cachear (ago 2026, ver rate limiting por visitante). Un 429 de la API muestra un aviso de tráfico (SobrecargaAviso) en vez de la pantalla de error genérica |
/challa | src/app/(public)/challa/page.tsx | Catálogo de lotes (challa) |
/checkout | src/app/(dashboard)/checkout/page.tsx | Carrito + confirmación + integración MP. Selector de método de entrega por vendedor (ver Métodos de entrega) |
/como-funciona | src/app/(public)/como-funciona/page.tsx | Guía visual de 5 pasos |
/contact | src/app/(public)/contact/page.tsx | Página de contacto |
/cookies | src/app/(public)/cookies/page.tsx | Política de cookies |
/help | src/app/(public)/help/page.tsx | FAQs |
/help/precios-dinamicos | src/app/(public)/help/precios-dinamicos/page.tsx | Ayuda del feature Precios dinámicos: cómo funciona, regla de redondeo y disclaimer |
/listings/[id] | src/app/(public)/listings/[id]/page.tsx | Detalle de publicación: galería, precio, vendedor, comprar. SEO: los listings de carta de catálogo llevan canonical → /cards/{cardId} (consolida señales en la ficha permanente); customs/accesorios/lotes/sellados/mazos mantienen canonical propio |
/mazos | src/app/(public)/mazos/page.tsx | Feed público de mazos pre-armados. Buscador por nombre, filtro por TCG, sort recent/price-asc/price-desc |
/novedades | src/app/(public)/novedades/page.tsx | Novedades: changelog/roadmap de hitos no técnicos en línea de tiempo (zig-zag en escritorio, columna en móvil) |
/privacy | src/app/(public)/privacy/page.tsx | Política de privacidad |
/search | src/app/(public)/search/page.tsx | Búsqueda global con filtros (incluye filtros por atributo según el TCG, en columna) |
/sellado | src/app/(public)/sellado/page.tsx | Catálogo de productos sellados |
/sets | src/app/(public)/sets/page.tsx | Listado de sets agrupados por TCG |
/sets/[tcg]/[slug] | src/app/(public)/sets/[tcg]/[slug]/page.tsx | Detalle de un set: grilla de cartas + panel de filtros por TCG colapsable |
/tarifas | src/app/(public)/tarifas/page.tsx | Comisiones y tarifas |
/terms | src/app/(public)/terms/page.tsx | Términos y condiciones |
/u/[username] | src/app/(dashboard)/u/[username]/page.tsx | Perfil público de un vendedor. Incluye el bloque “Métodos de entrega” (DeliveryMethodsCard): chips de métodos ofrecidos + puntos de retiro activos como “Título · Comuna, Región” (jamás dirección/instrucciones) |
/u/[username]/publicaciones | src/app/(dashboard)/u/[username]/publicaciones/page.tsx | Todas las publicaciones del vendedor |
Filtros por TCG (TcgFilters)
Sección titulada «Filtros por TCG (TcgFilters)»/search, el detalle de set y me/publicaciones (cuando el filtro es por carta y hay un TCG elegido) muestran filtros por atributo según el TCG vía src/components/catalog/TcgFilters.tsx. Renderiza un grupo de chips por filtro (con conteos) a partir de facets.attributes que devuelve la API. Es multi-select y vive en la URL (un parámetro por filtro, valores separados por coma) → compartible y SSR-friendly.
- En
/searchy mis-publicaciones va como columna (siempre visible). - En el detalle de set va colapsable (
collapsible): barra delgada con contador de filtros activos que se expande al click, para no comerse el alto de la pantalla.
Placeholder “Image Coming Soon” de TCGplayer
Sección titulada «Placeholder “Image Coming Soon” de TCGplayer»Algunas cartas del catálogo apuntan al placeholder genérico de TCGplayer (imagen fija de 1000×573 px). En vez de mostrar ese cartel, src/lib/cardImage.ts#isTcgplayerComingSoon(w,h) lo detecta por sus dimensiones exactas en el onLoad de la imagen (CardCard y CardDetailImage) y cae a nuestro placeholder propio. Se usa el match exacto de dimensiones (no “es horizontal”) porque hay cartas legítimamente horizontales (ej. ubicaciones de Lorcana, BREAK de Pokémon).
Novedades (/novedades)
Sección titulada «Novedades (/novedades)»Sección pública tipo changelog/roadmap con los hitos no técnicos de la plataforma (no se listan bugs, salvo una corrección pedida por usuarios que el usuario marque explícitamente). Línea de tiempo: zig-zag con spine central en escritorio, columna única con spine a la izquierda en móvil; más reciente arriba.
- Contenido: archivo estático
src/lib/data/novedades.ts(arrayNOVEDADESde{ id, date, title, description }, ordenado por fecha descendente en código). Para agregar una novedad se añade un objeto y se despliega — no hay CMS ni fuente en runtime. - Página estática (
force-static, sinrevalidate): el contenido se hornea en el build, así que una novedad nueva aparece al desplegar a prod, sin limpiar caché. (Si se reactivara el caché de páginas de Cloudflare, habría que purgar esa ruta tras desplegar.) - Helpers en
src/lib/novedades.ts(formatNovedadDateTZ-safe,esReciente); pill “Nuevo” en entradas ≤14 días (calculado en cliente para no quedar obsoleto en estático); la entrada más reciente va destacada. Reveal al scroll víasrc/lib/useReveal.ts(respetaprefers-reduced-motion, queda visible si no hayIntersectionObserver). - Accesos: footer (columna “Ayuda”), menú móvil y menú de usuario.
| URL | Archivo | Qué hace |
|---|---|---|
/login | src/app/(auth)/login/page.tsx | Login con Google |
/api/auth/[...nextauth] | src/app/api/auth/[...nextauth]/route.ts | API NextAuth (callbacks, logout, etc.) |
Dashboard del usuario (requiere login)
Sección titulada «Dashboard del usuario (requiere login)»Todas bajo src/app/(dashboard)/me/...
| URL | Qué hace |
|---|---|
/me | Resumen del perfil: actividad (calificación, ventas, compras) + estado de la configuración. Ver Perfil |
/me/perfil | Portada, foto, nombre, usuario, bio y redes |
/me/datos | Datos privados: región/comuna, calle y número, RUT, WhatsApp y avisos por correo |
/me/venta | Índice de venta: entrega, precios dinámicos, wallet, datos bancarios, retiros, publicaciones |
/me/cuenta | Correo de acceso, miembro desde, tema y zona de peligro |
/me/bank-account | Cuenta bancaria (encrypted) para retiros |
/me/compras | Mis compras (filtradas por estado) |
/me/compras/[id] | Detalle de compra: chat, fotos, disputar |
/me/entrega | Configuración de métodos de entrega del vendedor: 4 métodos + CRUD de hasta 5 puntos de retiro (ver Métodos de entrega) |
/me/listings | Mis publicaciones (con filtros) |
/me/listings/new | Crear publicación (5 tipos: card, accessory, bulk, sealed, deck) |
/me/listings/[id]/edit | Editar publicación existente |
/me/mensajes | Inbox de conversaciones |
/me/ventas | Mis ventas (filtradas por estado) |
/me/ventas/[id] | Detalle de venta: chat, aceptar/enviar |
/me/wallet | Saldo + retiros + historial |
/me/withdrawals | Historial de retiros |
/me/withdrawals/new | Solicitar retiro |
Publicar un mazo
Sección titulada «Publicar un mazo»/me/listings/new?kind=deck carga el componente DeckListingForm. Campos requeridos:
- Nombre del mazo (
deckTitle) — texto libre, 5-80 caracteres. - TCG (
tcg) — slug del TCG (pokémon, magic, etc.). - Cantidad de cartas (
deckCardCount) — entero 1-500. - Fotos (
photos) — 1 a 5 imágenes via Cloudinary. - Precio (
price) — CLP cents. - Cantidad (
quantity) — stock disponible. - Descripción (
description) — 10-500 caracteres.
Fotos propias en cartas de catálogo (opcional)
Sección titulada «Fotos propias en cartas de catálogo (opcional)»Al publicar una carta del catálogo (/me/listings/new), el vendedor puede subir 0-2 fotos propias de su carta real (frente/dorso), de forma opcional y modesta (disclosure ”+ Agregar fotos de tu carta” en ListingForm). Se guardan en photos[] del listing y se muestran como thumbnails chicos clickeables —junto a la imagen del catálogo, sin reemplazarla— en la fila de vendedores (/cards/[id]), en mis-publicaciones (lista y grilla) y en el perfil público. Al hacer click abren un visor con zoom (ZoomLightbox, lazy, yet-another-react-lightbox + plugin Zoom) para inspeccionar bordes/detalle en mobile y desktop. El flujo de cartas custom (con customPhoto) no cambia.
Precios dinámicos (/me y /me/listings)
Sección titulada «Precios dinámicos (/me y /me/listings)»Función opt-in: el vendedor puede activar que sus cartas del catálogo de TCGplayer se repricen solas una vez al día. El precio en CLP se calcula a partir del precio de referencia de TCGplayer (USD) y el tipo de cambio que el vendedor define, redondeado al múltiplo de $100 más cercano. Solo aplica a cartas de catálogo de TCGplayer: no se tocan cartas de Mitos y Leyendas, productos sellados, accesorios ni cartas custom. Superficies visibles:
- Perfil (
/me) — sección “Precios dinámicos”: checkbox maestro para activar la función + input del dólar (CLP). Si el nuevo tipo de cambio difiere mucho del anterior, aparece una confirmación con el impacto antes de guardar. Incluye el enlace de ayuda “¿Cómo funcionan los precios dinámicos?”. - Mis publicaciones (
/me/listings) — sección “Precios dinámicos” sobre la tabla, con el botón “Activar en todas mis cartas” (abre un diálogo que avisa cuántas cartas se afectan y es cancelable) y el enlace “Cambiar mi tipo de cambio” (lleva a la configuración del perfil). Cada carta tiene un toggle “Precio dinámico”; cuando una carta queda fuera (no es elegible o su precio de referencia no está fresco) muestra el badge “Fuera de precios dinámicos”. Al recalcular en masa, un overlay de carga + un destello indican el avance. - Ayuda (
/help/precios-dinamicos) — página de ayuda completa: cómo funciona, la regla de redondeo (al múltiplo de $100 más cercano) y el disclaimer.
El lanzamiento también figura como hito público en /novedades.
Perfil privado y público (jul 2026)
Sección titulada «Perfil privado y público (jul 2026)»/me pasó de página única a grupo de secciones con cabecera compartida (src/app/(dashboard)/me/(perfil)/). La cabecera —portada, avatar, identidad— se renderiza una vez en el layout; las secciones son rutas hijas y no la re-montan. Las páginas que ya existían (/me/entrega, wallet, banco, publicaciones) no se movieron: la sección Venta es su índice, lo que de paso rescató /me/bank-account, que solo se alcanzaba desde el flujo de retiro.
El perfil público /u/[username] usa la misma cabecera y pestañas Publicaciones · Información · Reseñas.
Portada. Subible desde /me/perfil con un paso de encuadre: al elegir el archivo no se sube nada; se abre un marco donde se arrastra y se acerca, con vista previa de la cabecera real. Recién al confirmar se recorta a 1500×500 y se sube. Sin portada se dibuja una trama de rombos en CSS puro, con ángulo y tinte derivados del nombre de usuario; el avatar sin foto usa iniciales sobre el mismo hash. Nadie ve un rectángulo vacío.
Contraste. Los tokens --primary, --success y --warning están pensados como color de relleno. Usados como color de texto no cumplen AA en tema claro (2,3:1, 3,3:1 y 2,0:1). Para texto existen --primary-text, --success-text y --warning-text, tonos más profundos de la misma familia. Es un error que se coló tres veces en el mismo trabajo, siempre por revisar en tema oscuro, donde el original rinde 9:1.
Métodos de entrega y puntos de retiro
Sección titulada «Métodos de entrega y puntos de retiro»En producción desde 2026-07-20. Cada vendedor configura qué métodos de entrega ofrece y el comprador elige método por vendedor al pagar.
- Configuración (
/me/entrega) — 4 interruptores (envío a domicilio, entrega presencial, coordinar con el vendedor, punto de retiro) + CRUD de hasta 5 puntos de retiro (título, región/comuna, dirección, instrucciones, activo). Validación local espejo del API; al guardar se revalidan el hub/mey el perfil público. Acceso SOLO desde la tarjeta del hub/me(sin entrada en el UserMenu, decisión del dueño). Defaults = comportamiento previo: los 3 métodos clásicos activados, retiro apagado. - Checkout (
/checkout) — selector de método por grupo de vendedor con solo los métodos que ese vendedor ofrece; “Punto de retiro” despliega los puntos activos como “Título · Comuna, Región” con la nota “La dirección exacta del punto la verás al concretar la compra”. El candado de datos de envío (RUT/teléfono/dirección) aplica por grupo y enlaza a/me#direccion; el bloque “Tu dirección de envío” solo aparece si algún grupo eligió envío. El payload mandadeliveriespor vendedor (el server re-valida todo). - Perfil público (
/u/[username]) — bloque “Métodos de entrega” (DeliveryMethodsCard): chips de métodos + títulos de puntos activos con comuna y región. La dirección y las instrucciones jamás se muestran ahí. - Detalle de orden — con la compra pagada, AMBAS partes ven el panel “Punto de retiro” (dirección completa e instrucciones). El vendedor despacha con “Dejé el pedido en el punto de retiro” (comprobante opcional, a diferencia del envío). El stepper y el badge dicen “Lista para retiro” desde el primer paso en órdenes de retiro.
El lanzamiento figura como hito público en /novedades (“Métodos de entrega y puntos de retiro”).
Admin (requiere login + user.isAdmin === true)
Sección titulada «Admin (requiere login + user.isAdmin === true)»Todas bajo src/app/(dashboard)/admin/...
| URL | Qué hace |
|---|---|
/admin | Dashboard principal con KPIs |
/admin/banners | Gestión de banners del home |
/admin/banners/new | Crear banner |
/admin/banners/[id] | Editar banner |
/admin/disputes | Cola de disputas |
/admin/disputes/[id] | Resolver disputa |
/admin/orders | Búsqueda de órdenes |
/admin/orders/[id] | Detalle de orden (admin view) |
/admin/reconciliation | Reconcilia wallet vs ledger |
/admin/reports | Cola de reportes |
/admin/reports/[id] | Resolver reporte (warn/suspend/ban) |
/admin/scanner-usage | Consumo del cupo de OCR del escáner (hoy / mes / últimos 30 días, con status ok/warning/critical) |
/admin/users | Usuarios moderados |
/admin/users/[username] | Detalle de usuario (perfil + moderation) |
/admin/users/[username]/bank-account | Cuenta bancaria del user (decrypted) |
/admin/users/[username]/wallet | Wallet del user (credit/debit manual) |
/admin/wallet-system | System wallet (comisiones acumuladas) |
/admin/finanzas | Panel financiero: sobres (ganancia / IVA / fondo usuarios / comisión MP / en tránsito), chequeo de salud (esperado vs saldo real MP), desglose por mes y movimientos; registrar pago de IVA y retiro de ganancia |
/admin/withdrawals | Cola de retiros |
/admin/withdrawals/[id] | Detalle de retiro (procesar/cancelar) |
Escáner de cartas (modal, no es una página)
Sección titulada «Escáner de cartas (modal, no es una página)»El escáner no es una ruta — es un modal (CardScannerModal) que se abre desde 3 entry points: el header en desktop, el header en mobile, y el formulario de venta (/me/listings/new). Se renderiza solo cuando el feature flag NEXT_PUBLIC_SCANNER_ENABLED === 'true' (ver envs) y solo en dispositivos táctiles (mobile/tablet, vía (pointer: coarse)); en computadores no aparece.
Flujo del modal: selección de TCG → cámara (el usuario encuadra la carta en la guía y toca Capturar; el botón espera ~1s a que la cámara enfoque, y una píldora superior recuerda capturar cuando la carta se vea nítida) → loader “Leyendo carta…” mientras corre el OCR → confirmación (1 match) o picker (varios) → navega a /cards/[id] (modo lookup) o rellena el form (modo autofill). Manda la imagen a POST /api/v1/scan/identify-image (ver endpoints). El código vive en src/components/scanner/ y src/lib/scanner/.
API routes propias de Next.js
Sección titulada «API routes propias de Next.js»Estas viven dentro de tcgcards-web (no en tcgcards-api). Son endpoints que el cliente llama y que después hacen requests al backend o a servicios externos directamente.
| URL | Archivo | Qué hace |
|---|---|---|
/api/auth/[...nextauth] | src/app/api/auth/[...nextauth]/route.ts | NextAuth — login/logout/session/callback Google |
/api/banners/sign-upload | src/app/api/banners/sign-upload/route.ts | Firma upload a Cloudinary para banners (admin) |
/api/orders/[id]/resume-payment | src/app/api/orders/[id]/resume-payment/route.ts | Reanuda pago en MP de una orden en awaiting_payment |
Convenciones del App Router
Sección titulada «Convenciones del App Router»- Carpetas con paréntesis (
(public),(dashboard),(auth)) son route groups: agrupan páginas con un layout común sin afectar la URL. [param]= ruta dinámica (un solo segmento).[...slug]= catch-all (cero o más segmentos).layout.tsxdefine el layout compartido por todas las páginas de esa carpeta y sub-carpetas.route.tsson API endpoints (NO páginas).loading.tsx,error.tsx,not-found.tsxson archivos especiales para UI de estados.
Middleware de dominio (ago 2026)
Sección titulada «Middleware de dominio (ago 2026)»Vercel publica todo despliegue también bajo su propio dominio (tcgcards-web.vercel.app, el nombre del proyecto) y bajo cada vista previa (tcgcards-web-git-<rama>-....vercel.app), además de tcgcards.cl. Un middleware (src/middleware.ts) redirige con 308 cualquier Host que no sea tcgcards.cl, www.tcgcards.cl, staging.tcgcards.cl, localhost/127.0.0.1 o una vista previa que empiece con tcgcards-web-git- (así un host de otro proyecto con -git- en el nombre no cuela, y la URL de producción de Vercel —que no lleva -git-— queda excluida). El matcher del middleware incluye .txt/.xml a propósito, para cerrar también robots.txt y sitemap.xml; excluye assets estáticos e imágenes.
Por qué (2026-08-07): se verificó que https://tcgcards-web.vercel.app/ servía producción completa, con 200 y sin login, saltándose Cloudflare —que solo protege tcgcards.cl—. Eso abría dos problemas a la vez: de seguridad, porque por ese camino se podía inventar la cabecera CF-Connecting-IP (normalmente inviolable, la pone Cloudflare) y evadir el rate limiting por visitante; y de SEO, porque esa copia declaraba Allow: / en su robots.txt y ofrecía a Google un sitio duplicado (la etiqueta canónica es una sugerencia, no una orden). Se usa 308 y no 302 para que el método se conserve y Google consolide el duplicado en el dominio bueno.
Las vistas previas de Vercel se siguen dejando pasar sin redirigir: son las que se usan para revisar cambios antes de publicarlos.
| Tipo de página | Requiere |
|---|---|
(public)/* | Nada |
(auth)/login | No estar logueado |
(dashboard)/me/* | Sesión activa (cookie Auth.js) |
(dashboard)/admin/* | Sesión activa + user.isAdmin === true |
(dashboard)/checkout | Sesión activa + items en carrito |
(dashboard)/u/[username] | Nada (es público, pero comparte layout con dashboard) |
El control se hace via middleware o en cada page.tsx con auth() de Auth.js.