Easy-to-manage touch screens
Free, open-source touch screens. Guided USB setup, a drag-and-drop editor and local control. Made for Home Assistant.
翻訳待ち
Busca restaurantes por ubicación, tipo de comida, precio y calificación. Si tienes un restaurante, publica tu menú, tus fotos y tus precios, y mantenlos siempre al día.
翻訳待ち
暮らしJavaScript · CSSGitHub @Yiipi24接続状況の再確認が必要です所有者未確認
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Loading the full README. The saved excerpt is shown below.
Landing de lanzamiento de menuabierto.com — cartas digitales por QR para restaurantes. Next.js 15 (App Router), sin dependencias externas. Se despliega en Vercel en cada push a main.Read the full README ↗
Landing de lanzamiento de menuabierto.com — cartas digitales por QR para restaurantes.
Next.js 15 (App Router), sin dependencias externas. Se despliega en Vercel en
cada push a main.
npm install
npm run dev
npm test # unitarias, con node:test; no necesitan red ni llaves
npm run lint # eslint con las reglas de Next y de React
npm run test:e2e # Playwright contra el sitio construido (npm run build antes)
Las unitarias cubren la lógica que decide dinero y direcciones: lib/precios,
lib/slug, lib/planes, lib/cobro, lib/cupones, lib/whatsapp, los
catálogos y las métricas, y la conversación entera del asistente de WhatsApp
(lib/asistente, lib/pedidos, lib/whatsapp-cloud). Viven en tests/unitarias/ y corren con el node --test de Node 22, sin framework: el único truco es _resolver.mjs, que le
agrega la extensión a los import "./precios" que Next resuelve solo.
Las de extremo a extremo (tests/e2e/) visitan lo que no puede romperse:
portada y búsqueda, una ficha publicada tomada del sitemap y su carta, el
panel mandando a /entrar, las páginas de /comida, y que el webhook de cobro
rechace lo que no viene firmado. Necesitan SUPABASE_URL y
SUPABASE_PUBLISHABLE_KEY porque las fichas son reales; con BASE_URL apuntan
a un despliegue en vez de levantar uno.
El workflow ci.yml corre lint, unitarias y build en cada push y pull request.
Las de extremo a extremo corren después, solo si el repo tiene esas dos llaves
como secretos; si no, el job avisa y se salta en vez de fallar.
Los errores de producción salen por instrumentation.js (onRequestError,
todo lo que revienta en el servidor) y por app/error.js (lo que revienta en
el navegador, vía POST /api/errores). Los dos pasan por lib/errores.js, que
los deja en el log de Vercel y, si existe ERRORES_WEBHOOK_URL, los manda a
un webhook de Slack o Discord, que es el lugar donde alguien los lee. Del lado
del comensal, @vercel/analytics en el layout cuenta páginas y orígenes sin
cookies; hay que encender Web Analytics en el proyecto de Vercel para que
empiece a guardar.
Guardamos precios estructurados por platillo, en centavos. Nadie más tiene ese
dato —Google Maps guarda el menú como PDF— y hasta ahora no se usaba para
nada. La migración inteligencia_de_precios lo convierte en tres cosas:
menu_item_price_history guarda cada cambio de precio con
su fecha (trigger menu_items_registrar_precio); lo que ya existía entró
como primer punto. El dueño ve el suyo./precios: quién vende qué, a cuánto y a qué
distancia (platillos_cerca, un platillo por restaurante, con tope de
precio opcional y "cerca de mí"), y cuánto cuesta comer en una zona
(precios_de_zona: la mediana de las medianas por restaurante, con su
rango típico). Aquí sí salen nombres: son los precios que cada carta ya
publica.posicion_de_precio): la mediana de su carta contra la de su
colonia (o su ciudad, si la colonia no llega) y contra la de su cocina en la
ciudad, y un aviso cuando queda a más de un cuarto por encima o por debajo
(UMBRAL_AVISO en lib/inteligencia-precios.js). Solo cifras agregadas:
nunca el precio de un competidor con nombre.Ninguna agregación sale con menos de tres restaurantes detrás
(minimo_para_agregar()): con menos, "la mediana de la colonia" es el precio
de alguien identificable. No hay rankings de restaurantes más caros o más
baratos por nombre, a propósito. precios es un segmento reservado.
Contra Google no se gana por volumen de reseñas sino por confianza, y el QR de
la mesa es la única prueba de visita que tenemos (el asistente de WhatsApp manda
el mismo enlace a quien recibió su pedido, que es la prueba de compra): nadie la copia sin pegar un
QR en cada mesa. Al escanear, /q/<codigo> —ahora una ruta y no una página,
porque tiene que poder poner la cookie del visitante— registra un pase de
visita (visit_passes) ligado a esa cookie y, si había sesión, a la
cuenta. Dura siete días (dias_de_pase()); volver a escanear renueva el
pase libre en vez de acumular otro.
Al guardar una reseña, canjear_pase() busca un pase vigente, sin usar y del
mismo local que sea de esa cookie o de esa cuenta; si lo hay, la reseña queda
con verified_at y el pase con used_review_id: un pase, una reseña. Sin
pase la reseña se guarda igual, solo sin marca. Editarla no se la quita, y
borrarla libera el pase mientras no haya caducado. Un trigger impide que el
autor ponga verified_at desde su propia política de update: la marca solo la
escribe la función.
En la ficha, las verificadas llevan la marca "✓ Verificada" junto al nombre, y
si hay alguna aparece su promedio aparte y dos controles: "Solo verificadas" y
"Verificadas primero" (lib/pases.js, con pruebas). Las no verificadas se
muestran siempre: se distinguen, no se bloquean. Los pases viejos se barren
con limpiar_pases().
Historias de 24 horas, seguir y una bandeja de avisos son mecánicas de
aplicación, y ahora el sitio se instala como una: app/manifest.js genera el
manifest, public/iconos/ trae los PNG (se regeneran desde app/icon.svg con
node scripts/generar-iconos.mjs), el layout lleva las etiquetas que iOS
exige, y public/sw.js es el service worker. Ese worker hace dos cosas y
nada más: enseña los avisos que llegan por push y guarda /sin-conexion para
cuando no hay red. No cachea fichas ni cartas a propósito: un precio viejo
servido desde caché es peor que un "sin conexión" honesto.
/instalar es la pantalla de instalación: en Android dispara el evento del
navegador con un botón; en iPhone explica los tres toques de Safari, porque
ahí no hay evento que disparar.
This is the beginning of the README. Read the rest at the original. Read the full README ↗