Red social y marketplace para intercambiar figuritas de álbumes coleccionables (Panini Mundial 2026). Los usuarios gestionan su colección, siguen a otros coleccionistas, coordinan intercambios inteligentes por chat y participan en comunidades.
El sistema cruza automáticamente tus figuritas repetidas con las faltantes de otros usuarios, mostrando exactamente cuántas y cuáles pueden intercambiar sin necesidad de publicar nada manualmente.
- Stack tecnológico
- Requisitos previos
- Configuración local
- Estructura del proyecto
- Arquitectura y patrones
- Base de datos
- Autenticación
- Rutas y páginas
- Componentes
- Tiempo real (Realtime)
- Despliegue
- Qué NO tocar
| Capa | Tecnología | Versión |
|---|---|---|
| Framework | Next.js (App Router) | 14.2 |
| Lenguaje | TypeScript | 5 |
| Estilos | Tailwind CSS | 3.4 |
| Backend / DB | Supabase (Postgres + Auth + Storage + Realtime) | 2.x |
| Despliegue | Vercel | — |
No hay librerías de UI externas. Todo el diseño es Tailwind puro. No hay test runner configurado.
- Node.js 18+
- Una cuenta y proyecto en Supabase
- (Opcional) CLI de Supabase para manejar migraciones
- Clonar el repositorio e instalar dependencias:
npm install- Crear el archivo
.env.localen la raíz con estas variables:
NEXT_PUBLIC_SUPABASE_URL=https://<tu-proyecto>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key>
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>Las claves se obtienen en Supabase Dashboard → Project Settings → API.
-
Aplicar las migraciones de base de datos en orden (ver sección Base de datos).
-
Cargar los datos iniciales (seed):
Ir a Supabase Dashboard → SQL Editor y ejecutar el contenido de supabase/seed.sql.
- Iniciar el servidor de desarrollo:
npm run dev
# Disponible en http://localhost:3000SWAP/
├── app/
│ ├── (auth)/ # Páginas públicas de autenticación
│ │ ├── login/
│ │ ├── register/
│ │ ├── forgot-password/
│ │ └── reset-password/
│ ├── (protected)/ # Páginas que requieren sesión activa
│ │ ├── layout.tsx # Layout compartido: sidebar + bottom nav
│ │ ├── colecciones/ # Gestión del álbum personal
│ │ │ ├── page.tsx
│ │ │ └── [albumId]/
│ │ ├── marketplace/ # Publicaciones de intercambio
│ │ │ ├── page.tsx
│ │ │ └── [listingId]/
│ │ ├── chat/
│ │ │ └── [tradeId]/ # Chat de negociación por trade o DM
│ │ ├── chats/ # Lista de todas las conversaciones
│ │ ├── perfil/ # Perfil propio (Instagram-style)
│ │ ├── perfil/[userId]/ # Perfil público de otro usuario
│ │ │ └── album/[albumId]/ # Álbum de otro usuario (solo lectura)
│ │ ├── amigos/ # Buscador de usuarios
│ │ ├── comunidades/ # Lista de comunidades
│ │ └── comunidades/[communityId]/ # Chat grupal de una comunidad
│ ├── auth/callback/ # Callback OAuth de Supabase
│ ├── layout.tsx # Layout raíz
│ ├── page.tsx # Landing: redirige a /colecciones si hay sesión
│ └── globals.css
├── components/
│ ├── album/
│ │ ├── AlbumView.tsx # Grilla de figuritas con filtros
│ │ ├── StickerModal.tsx # Modal para marcar figurita
│ │ └── ImportarModal.tsx # Importar colección desde otra app
│ ├── chat/
│ │ └── ChatClient.tsx # Chat con Realtime, reply, borrar mensajes
│ ├── comunidades/
│ │ └── CommunityChatClient.tsx # Chat grupal con Realtime
│ ├── amigos/
│ │ └── AmigosClient.tsx # Buscador de usuarios
│ ├── layout/
│ │ ├── Sidebar.tsx # Navegación lateral (desktop)
│ │ ├── BottomNav.tsx # Navegación inferior (mobile)
│ │ └── UnreadBadge.tsx # Badge de mensajes no leídos (Realtime)
│ ├── marketplace/
│ │ ├── MarketplaceClient.tsx
│ │ ├── ListingCard.tsx
│ │ ├── ListingModal.tsx
│ │ └── NewListingModal.tsx # Crea publicación con repetidas/faltantes automático
│ ├── perfil/
│ │ ├── ProfileClient.tsx # Perfil propio editable
│ │ ├── PublicProfileClient.tsx # Perfil público con seguir/mensaje
│ │ └── FollowListModal.tsx # Modal de seguidores/siguiendo
│ └── ui/
│ └── CityAutocomplete.tsx
├── lib/
│ ├── supabase/
│ │ ├── client.ts
│ │ ├── server.ts
│ │ └── middleware.ts
│ ├── types/
│ │ └── index.ts # Todas las interfaces TypeScript
│ ├── albumCovers.ts # Mapeo nombre de álbum → imagen local
│ ├── importParser.ts # Parser de mensajes WhatsApp de otras apps
│ └── cities.ts # Ciudades colombianas para autocompletado
├── supabase/
│ ├── migrations/
│ └── seed.sql
├── public/
│ ├── albums/ # Portadas de álbumes
│ │ └── panini-mundial-2026.jpg
│ ├── manifest.json
│ └── icons/
└── middleware.ts
page.tsx(Server Component): Obtiene datos de Supabase con el cliente de servidor y los pasa como props al componente cliente.*Client.tsx(Client Component): Maneja estado interactivo y llama a Supabase directamente desde el browser.
Hay dos clientes y no son intercambiables:
lib/supabase/client.ts→ usar en componentes con'use client'lib/supabase/server.ts→ usar enpage.tsx,layout.tsx(requiereawait)
La app tiene dos sistemas de navegación sincronizados:
Sidebar.tsx: visible en pantallasmdy mayoresBottomNav.tsx: visible en pantallas menores amd
Si se agrega una sección nueva, actualizar ambos componentes. Ambos reciben userId desde el layout para el badge de no leídos.
El target de compilación no es es2015, por lo que nunca usar [...new Set()]. Siempre usar Array.from(new Set(...)).
Siempre usar un nombre único por instancia de componente para evitar el error "cannot add callbacks after subscribe()":
const channelName = useRef(`mi-canal-${Date.now()}`).currentLas migraciones viven en supabase/migrations/ y se aplican en orden desde Supabase Dashboard → SQL Editor.
| Archivo | Descripción |
|---|---|
001_initial_schema.sql |
Esquema base: tablas, RLS, trigger de perfil, storage |
002_add_sticker_code.sql |
Columna code en stickers |
003_add_rarity.sql |
Columna rarity (NORMAL / FOIL) en stickers |
004_enable_messages_realtime.sql |
Habilita Realtime en messages |
005_add_communities.sql |
Tablas communities y community_messages con RLS + Realtime |
007_add_follows_and_bio.sql |
Tabla follows + columna bio en profiles |
008_public_user_stickers.sql |
RLS: cualquier usuario autenticado puede leer user_stickers ajenas |
009_add_trade_reads.sql |
Tabla trade_reads + función get_unread_count() para badges |
010_allow_direct_trades.sql |
trades.listing_id pasa a nullable para mensajes directos |
011_add_reply_to_and_unread_per_trade.sql |
messages.reply_to_id + función get_unread_counts_per_trade() |
012_delete_policies.sql |
RLS: remitente puede borrar sus mensajes; participantes pueden borrar trades |
013_add_updated_at_to_user_stickers.sql |
Columna updated_at en user_stickers con índice para ordenar por actividad reciente |
014_add_notifications_seen_at.sql |
Columna notifications_seen_at en profiles para el badge de notificaciones |
015_push_subscriptions.sql |
Tabla push_subscriptions para notificaciones push web (VAPID) |
profiles — Datos públicos del usuario (username, full_name, avatar_url, bio)
albums — Álbumes coleccionables
stickers — Figuritas de cada álbum (número, código, jugador, sección, rareza)
user_stickers — Relación usuario ↔ figurita: owned y repeated_count
listings — Publicaciones de intercambio (ciudad + oferta/demanda)
listing_offers — Figuritas que el usuario ofrece en una publicación
listing_wants — Figuritas que el usuario busca en una publicación
trades — Intercambio o mensaje directo entre dos usuarios
(listing_id es null para DMs)
messages — Mensajes de chat con reply_to_id opcional
follows — Relación seguidor ↔ seguido
communities — Comunidades temáticas
community_messages — Mensajes de chat grupal de una comunidad
trade_reads — Último mensaje leído por usuario en cada trade (para badges)
push_subscriptions — Suscripciones VAPID para notificaciones push web
user_stickers: cualquier usuario autenticado puede leer; solo el propietario escribe.messages: solo participantes del trade pueden leer/escribir; el remitente puede borrar sus propios mensajes.trades: solo participantes pueden leer/actualizar/borrar.follows: cualquier autenticado puede leer; solo el seguidor gestiona sus propias relaciones.profiles,albums,stickers: lectura pública para autenticados.
middleware.tsintercepta todos los requests, refresca el token y redirige a/loginsi no hay sesión.- Al registrarse con email, Supabase envía un correo de confirmación — la sesión no se inicia hasta validar.
- Al registrarse con Google (OAuth), la sesión se inicia directamente.
- El trigger
on_auth_user_createdcrea automáticamente una fila enprofiles.
| Ruta | Descripción |
|---|---|
/login |
Login con email/contraseña o Google |
/register |
Registro con confirmación de email |
/forgot-password |
Solicitar reset de contraseña |
/reset-password |
Nueva contraseña desde link de email |
/colecciones |
Lista de álbumes disponibles |
/colecciones/[albumId] |
Grilla de figuritas del álbum personal |
/marketplace |
Perfiles de usuarios ordenados por compatibilidad de intercambio |
/marketplace/[listingId] |
Detalle de publicación (legacy) |
/chats |
Lista de conversaciones con contador de no leídos |
/chat/[tradeId] |
Chat en tiempo real (trade o DM) |
/perfil |
Perfil propio editable (Instagram-style) |
/perfil/[userId] |
Perfil público de otro usuario |
/perfil/[userId]/album/[albumId] |
Álbum de otro usuario en modo lectura |
/amigos |
Buscador de usuarios para seguir |
/comunidades |
Lista de comunidades |
/comunidades/[communityId] |
Chat grupal de una comunidad |
Grilla de figuritas con filtros (todos / faltan / completo / repetidos), buscador y barra de progreso. Soporta modo readOnly para ver álbumes de otros usuarios. Incluye botón "Importar colección" que abre ImportarModal.
Modal de 3 pasos para importar colección desde mensajes de WhatsApp de otras apps. Parsea números, códigos (COL1, FWC3...) y rangos (1-5). Usa lib/importParser.ts.
Chat completo con: Realtime (mensajes en vivo), separadores de fecha, reply a mensajes específicos, borrar mensajes propios, borrar conversación, badge de no leídos al salir. Fondo verde suave.
Badge rojo en el ícono de Chats. Se suscribe a inserciones en messages via Realtime con nombre de canal único por instancia. Se resetea al entrar a un chat y se refresca al salir.
Perfil propio estilo Instagram: avatar editable, @username editable con validación de unicidad, bio, stats (álbumes/seguidores/siguiendo) clickeables, grilla de álbumes con portada.
Perfil público de otros usuarios. Botones Seguir/Siguiendo y Mensaje. Botón Intercambiar que muestra un modal con dos tabs: figuritas que puedes darle (tus repetidas que le faltan) y figuritas que recibirías (sus repetidas que te faltan), con un CTA que abre el chat directamente. Stats clickeables que abren FollowListModal.
Modal que muestra la lista de seguidores o siguiendo de un usuario. Se carga al abrirse.
Lista todos los usuarios ordenados por compatibilidad de intercambio (suma de figuritas que pueden darse mutuamente). Chips verdes "Te da N" y azules "Le das N". Filtro para mostrar solo usuarios con intercambio posible. Al hacer clic navega al perfil público donde se puede ver el detalle exacto.
Crea una publicación en el marketplace (legacy). Solo pide ciudad; calcula automáticamente las repetidas (a ofrecer) y faltantes (a buscar) de la colección actual del usuario.
Chat grupal con Realtime. Muestra nombre y avatar del remitente cuando cambia de persona. Avatar linkea al perfil.
Buscador de usuarios por nombre o @username. Solo muestra resultados cuando hay texto — sin sugerencias iniciales.
Tres tablas usan Realtime:
messages— chat de trades y DMs (migración 004)community_messages— chat de comunidades (migración 005)messagestambién para elUnreadBadgedel nav
Requisito: las tablas deben estar en la publicación de Realtime y tener replica identity full. Si el Realtime deja de funcionar, verificar en Supabase Dashboard → Database → Replication.
El proyecto está en Vercel. Cada push a master dispara un deploy automático.
Variables de entorno en Vercel → Settings → Environment Variables:
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY
Verificar build antes de hacer push:
npm run build| Archivo / Carpeta | Por qué |
|---|---|
supabase/migrations/001_initial_schema.sql |
Define toda la base de datos base. Crear siempre una nueva migración numerada en su lugar. |
supabase/seed.sql |
~670 figuritas del álbum. Modificarlo afecta datos de todos los usuarios. |
lib/supabase/middleware.ts |
Maneja el refresco de sesión. Tocarlo puede romper la autenticación. |
middleware.ts |
Define rutas protegidas. Cambiar el matcher puede dejar rutas expuestas. |
.env.local |
Variables sensibles. Nunca commitear. |
app/auth/callback/route.ts |
Callback de Supabase Auth. No modificar. |