S&T Marketplace
S&T Marketplace: shop products from verified sellers, pay with mobile money or your wallet and follow your delivery live.
Todos los links de lo que te querés comprar, en un solo lugar.
Translation pending
CommerceTypeScript · CSSGitHub @calavarelaOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Wishlist personal de links de distintas tiendas, en formato de tarjetas con vista previa automática (título, imagen y precio), categorías, estados y búsqueda.
gru1 (São Paulo, vercel.json): la base de
Supabase está en sa-east-1, y con el default de Vercel (Washington) cada consulta cruzaba
el continente.npm install
npm run dev
Necesita un archivo .env.local (ver .env.example):
NEXT_PUBLIC_SUPABASE_URL=https://<proyecto>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<publishable key>
src/lib/preview.ts lee la página del link y saca los datos de Open Graph y del
JSON-LD de tipo Product. Si la tienda rechaza el navegador headless, reintenta con el
user-agent del crawler de redes sociales, que muchas sí permiten (así funciona Mercado
Libre). Si aun así no hay nada, la tarjeta se completa a mano.share.google, a.co, amzn.to, meli.la, bit.ly...) se siguen hasta
la página real antes de leerla (resolveShortLink), y los links de salida de Instagram y
Facebook se abren desde su parámetro u. Así se guarda la tienda y no el acortador.src/lib/url.ts limpia los parámetros de tracking (utm_*, ref, etc.) para guardar una
URL canónica estable y detectar duplicados.src/proxy.ts (antes middleware) refresca la sesión y protege las rutas privadas.items y categories está protegida por RLS: solo la ve su dueño.Las migraciones ya están aplicadas en el proyecto de Supabase:
| Tabla | Para qué |
|---|---|
categories | Categorías del usuario, con emoji y orden |
items | Links guardados: url canónica, título, imagen, precio, estado, nota, tags |
share_links | Links públicos de solo lectura (toda la lista o una categoría), con token aleatorio |
Al crearse un usuario nuevo, un trigger le carga las categorías iniciales (Ropa, Tecno, Casa, Regalos, Otros).
El botón Compartir lista crea un link /w/<token> (de toda la lista o de una categoría)
que se puede abrir sin cuenta. La página pública lee con la función get_shared_wishlist
(security definer), que devuelve solo los items pendientes y solo campos públicos: sin
notas, tags ni email. Las policies de items no cambian. Desactivar el link borra la fila y
el token deja de funcionar.
Las tarjetas no apuntan directo a la tienda sino a /go/<id> (o /go/<id>?s=<token> desde
una lista compartida). Esa ruta registra el clic (items.last_opened_at) y redirige al link
guardado, pasándolo antes por src/lib/affiliate.ts. Ahí hay una regla por programa de
afiliados, que se activa con su variable de entorno (por ahora AMAZON_ASSOCIATE_TAG). En la
base siempre queda el link original. La ruta solo redirige a URLs guardadas, así que no sirve
como redirección abierta.
Afiliados ajenos: si el link guardado ya trae la atribución de otra persona, sale tal cual
y no se le aplica ningún programa propio (foreignAttribution). Se detectan el tag de
Amazon (cualquier marketplace), los matt_* de Mercado Libre, los parámetros de Awin, Impact,
CJ, Rakuten, Partnerize y ShareASale, y nombres genéricos como aff_id o affiliate_id. Si el
tag es el nuestro, cuenta como propio. ref y source no cuentan, porque las tiendas los usan
para navegación. Los links cortos (amzn.to, meli.la) nunca se tocan: las reglas solo
aplican al dominio de la tienda. Las reglas nuevas tienen que definir isOurs para reconocer
el código propio.
Un Vercel Cron (vercel.json) llama una vez por día a /api/cron/prices, que vuelve a leer
la página de los items pendientes (primero los que hace más que no se revisan) con el
mismo fetchPreview. Si el precio cambió y está en la misma moneda, actualiza
price_amount y guarda el anterior en previous_price_amount: la tarjeta muestra el precio
nuevo con la etiqueta de % de suba o baja.
price_history, que escribe un trigger de items
(también al crear el item y al editar el precio a mano). La usuaria solo puede leerla.previous_price_amount: una corrección no
es una baja de precio.Authorization: Bearer $CRON_SECRET y usa SUPABASE_SERVICE_ROLE_KEY
(src/lib/supabase/admin.ts), así que las dos variables tienen que estar en Vercel.items.list_price_amount guarda el precio de lista (tachado) cuando el producto está en
promo. Lo saca fetchPreview al agregar el link y el chequeo diario lo reescribe en cada
lectura buena, así una promo que terminó vuelve a null. Cómo se detecta según la tienda:
priceSpecification con
priceType StrikethroughPrice o ListPrice.tiendanube:price el que se paga. Solo cuenta si la oferta es de la misma URL, porque
el JSON-LD también trae productos relacionados.compare_at_price de /products/<handle>.js (en centavos), de la
variante del link si tiene ?variant=.Mercado Libre bloquea la lectura, así que no se detecta. La tarjeta muestra la etiqueta
"Promo -X%" y el precio tachado, y el panel de filtros tiene "Solo en promo" (?promo=1).
La tabla discount_codes guarda códigos por tienda (domain, el mismo de stores), con
descripción y vencimiento opcionales. Se cruzan con los productos con isSameStore. En la
página de la tienda cada código muestra cuánto le queda ("Vence hoy · quedan 5 h", "Quedan
2 días") y los vencidos quedan al final, tachados; en las tarjetas solo se ofrecen los
vigentes. El vencimiento se cuenta en hora de Argentina (src/lib/code-expiry.ts).
source = manual): se cargan en la página de la tienda. Las policies solo
dejan insertar con este origen.source = detected): el chequeo diario lee la página principal de cada
tienda que alguien tiene en su lista (productos pendientes o favoritas) y busca menciones
tipo "15% OFF con el código HOLA15" (src/lib/discount-codes.ts). Es estricto a propósito:
el código tiene que estar en mayúsculas y no ser una palabra de banner ("CUPÓN REGALO",
"código postal"). Se renuevan en cada lectura buena y se borran si dejan de aparecer; los
manuales no se tocan.En la tarjeta, un chip con un ticket copia el código vigente de su tienda.
Aparte de las categorías personales: /listas muestra las listas de las que sos miembro y
deja crear una nueva; /listas/<id> es la lista, con sus productos por estado.
shared_lists (nombre, emoji, invite_token) y shared_list_members (owner o
editor). Un trigger suma a quien la crea como owner.items.shared_list_id; user_id es quien lo agregó y
no llevan categoría. La lista personal, /tiendas y el link público de solo lectura filtran
shared_list_id is null.items dejan ver y editar lo personal solo a su dueña y lo compartido a
cualquier miembro (is_list_member, security definer para no caer en recursión de RLS).
Solo la dueña renombra, borra la lista, maneja el link y saca gente; cada miembro puede
salir./unirse/<token>. Quien lo abre inicia sesión o se
registra (vuelve al link gracias a next), ve la lista con preview_shared_list y se suma
con join_shared_list. Desactivar el link no saca a nadie."Continuar con Google" (src/app/login/google-button.tsx) llama a signInWithOAuth desde el
navegador y la vuelta pasa por /auth/confirm, que canjea el code y respeta next. Si ya
había una cuenta con el mismo mail, Supabase las une. Configuración: cliente OAuth "Aplicación
web" en Google Cloud (proyecto wish-links-512623, app publicada) con la callback de Supabase
como URI de redireccionamiento, y el proveedor Google activado en Supabase con ese Client ID y
secreto.
/recuperar pide el mail y llama a resetPasswordForEmail
(siempre responde lo mismo, para no revelar qué mails tienen cuenta). El link del mail va a
/auth/confirm, que acepta token_hash + type (sirve en cualquier dispositivo) o code
(PKCE, solo en el mismo navegador), deja la cookie wl_password_recovery por 15 minutos y
manda a /perfil/contrasena para elegir una nueva sin pedir la actual./perfil/contrasena pide la actual y la verifica
antes de llamar a updateUser.…/auth/confirm tiene que estar en Authentication → URL
Configuration → Redirect URLs; para que el link sirva en otro dispositivo, la plantilla
"Reset password" tiene que apuntar a
{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=recovery&next=/perfil/contrasena;
y para mandar mails a personas que no son del equipo del proyecto hace falta un SMTP propio.Al cargar un link, src/lib/categorize.ts preselecciona una categoría a partir del título
(y algo de la descripción), sin red ni IA. Suma puntos por el nombre de la categoría en el
título, por palabras de un diccionario según el tipo de categoría (ropa, tecno, casa,
escritorio…) y por palabras que ya aparecen en títulos que la usuaria puso en esa categoría,
así aprende las categorías propias. Si ninguna gana con claridad, no sugiere nada. El
selector muestra "sugerida" y se puede cambiar.
tags es el catálogo de cada usuaria (nombres en minúscula, únicos); los productos guardan
los nombres en items.tags. Al cargar o editar un producto se eligen tocando chips
(TagPicker) o se crea una nueva, que entra al catálogo al guardar. En el panel de filtros
se filtra por etiqueta (?tag=) y desde "Editar" se renombran o borran: rename_tag y
delete_tag cambian el catálogo y todos los productos propios a la vez.
La campanita del header abre los avisos de la tabla notifications. Hoy hay un solo tipo,
price_drop: lo crea el trigger notify_price_drop cuando el chequeo diario baja el precio
de un item pendiente (una edición a mano no avisa). Cada aviso guarda en data el precio
anterior, el nuevo y la moneda de ese momento, y hay uno solo sin leer por item. Al abrir el
panel se marcan como leídos. La usuaria puede leerlos, marcarlos y borrarlos, pero no crearlos.
public/manifest.json declara un share_target: al instalar la web como app en Android,
Wish Links aparece en el menú Compartir de cualquier app y abre el formulario con el
link ya cargado.