Aseryx
License your data to AI companies. Every dataset gets a quality certificate teams review before anything is shared.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 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.