Vame Fútbol
Una tienda de camisetas de fútbol hecha para cómo compran sus clientes: desde el celular y cerrando por WhatsApp.
- Rol
- Fullstack
- Año
- 2026
- Stack
Contexto y problema
Vame Fútbol vende camisetas de fútbol retro y actuales. El punto de partida era un único HTML estático con 427 productos escritos a mano en el código.
La migración a React se hizo en 8 PRs encadenados, con un commit base para poder volver atrás en cualquier momento. Después, el catálogo pasó a vivir solo en Supabase, las fotos en Supabase Storage, y el stock empezó a sincronizarse por talle. En total fueron 121 commits entre julio y septiembre de 2026.
Arquitectura
copia desactualizada si Supabase falla
El inventario lo maneja otra aplicación, kitstock-pro, que es dueña de la vista storefront_products. La tienda la consulta con la anon key, que solo tiene permiso de select sobre esa vista.
Cada vez que el catálogo llega bien, se guarda en localStorage. Si Supabase falla, la tienda muestra esa última copia en lugar de una página vacía. Para el estado no hay librería: alcanzan dos contextos, CartProvider y CatalogFilterProvider.
Tres decisiones técnicas
Stock por talle, sin que un agotado corte la venta
- Decisión
- Las columnas
stock_sastock_xxxlse convierten en unstockBySize, y cada producto tiene unstock_mode:inmediatooencargo. Un talle sin stock sigue siendo seleccionable: aparece la nota “se hace por encargo” y el botón pasa a “Realizar encargo”. Al entrar, queda elegido el primer talle con stock. - Por qué
- Un talle agotado no es una venta perdida, es un encargo. En cambio, el filtro por talle responde otra pregunta, “qué puedo comprar ahora”, y solo muestra stock confirmado mayor a cero. Los productos con stock se ordenan primero con un sort estable, y cuando queda una sola unidad aparece el aviso “Última unidad”.
- Tradeoff
- El mapa por talle se arma solo si todas las columnas traen números. Si falta alguno, todos los talles se tratan como disponibles: prefiero no esconder un producto por un dato incompleto, aunque eso puede terminar en un encargo en vez de una entrega inmediata.
Carrito en el dispositivo y cierre por WhatsApp
- Decisión
- El carrito vive en
localStorage(vame:cart) a través de un hook genéricouseLocalStoragecon un validador de tipos: si el JSON está roto, tiene otra forma o el storage no está disponible, vuelve al valor por defecto. El checkout arma un mensaje por línea (equipo, temporada, variante, talle, dorsal y cantidad) más el total, y lo abre en WhatsApp. - Por qué
- Así compran los clientes de Vame: desde el celular y conversando. Cada línea se identifica por
key|size|dorsalName|dorsalNumber, lo que corrigió un bug heredado: cantidad y borrar usaban solo el producto, así que dos líneas de la misma camiseta se pisaban. El mensaje se codifica una sola vez conencodeURIComponent, para que un dorsal con&,#o%no rompa la URL. - Tradeoff
- No hay pago en el sitio: el pedido se confirma a mano, fuera de la tienda. Y el carrito queda en ese dispositivo, sin sincronizarse entre celular y computadora.
Imágenes pensadas para el celular
- Decisión
- Las URLs de Supabase Storage se reescriben de
/object/a/render/image/con una caja cuadrada,resize=containyquality=75. El componenteKitImagecarga en diferido por defecto; conprioritypasa a eager confetchPriority="high", y mientras tanto muestra un skeleton con shimmer. La imagen del hero se precarga enindex.htmlcon sustitución de variables de Vite, y las fuentes son propias y precargadas. - Por qué
- Casi todo el tráfico llega desde el celular. Durante el desarrollo vi fotos de unos 6 MB bajar a menos de 150 KB con la transformación, y un LCP de más de 19 segundos que dejó de aparecer. Son observaciones hechas mientras desarrollaba, no un benchmark auditado.
- Tradeoff
- Las fotos pasan a depender del servicio de transformación de Supabase, y la caja cuadrada con
containfuerza un mismo formato para todas, aunque el original no sea cuadrado.
Problemas difíciles
Supabase no adivina la otra dimensión
Al transformar una imagen, Supabase no infiere la dimensión que falta. Pedir solo un ancho no alcanzaba: la solución fue una caja cuadrada explícita con
resize=contain.Rutas del cliente en Vercel
En una SPA, el router vive en el navegador. Sin un rewrite hacia la app en Vercel, abrir un link directo a una página interna no encontraba nada que servir.
Una anon key que solo puede leer una vista
Un incidente previo de RLS con la anon key en el mismo proyecto de Supabase dejó una regla: la anon key solo tiene
selectsobrestorefront_products, y lo que escribe usa la service role desde el servidor.Extensiones .js en el servidor
Las funciones de Node en Vercel necesitan la extensión
.jsexplícita en los imports. Sin ella, el import no resuelve.
Cómo se testea
200+tests unitarios
Más de 200 tests unitarios y de componentes con Vitest, jsdom y React Testing Library, repartidos en 26 archivos.
Arriba de eso, Playwright recorre la tienda de punta a punta: 6 specs y 19 tests que corren contra el build de producción, no contra el servidor de desarrollo. Por ahora se ejecutan de forma local; todavía no hay CI.
Resultados
- visitantes
- 5.800+
- páginas vistas
- 19.000
- tráfico mobile
- 97%
Son los números del primer mes. El 97% de tráfico mobile confirma lo que guió el diseño: una tienda que tiene que ser liviana y cómoda en el celular, y que cierra la venta donde el cliente ya conversa.
En camino: checkout online
El siguiente paso es cobrar dentro del sitio. Está construido en una rama aparte (feat/checkout-pagos) y todavía no está mergeado. Mientras tanto, WhatsApp sigue siendo el canal principal hasta que el nuevo demuestre que funciona.
En la rama
- Checkout en
/checkoutcon transferencia bancaria api/orders/createvalida el pedido y recalcula precio, descuento y recargo por dorsal en el servidor: el cliente nunca manda un precio- Chequeo de stock para productos
inmediato, con un409si no alcanza - Los pedidos vencen a las 24 h con un cron protegido por
CRON_SECRET
Todavía no
- Pago con tarjeta vía Mercado Pago
- Reserva de stock
Sin construir todavía.
¿Querés verla funcionando?
Vame está en producción. También podés volver a los demás proyectos.