尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Los lugares que has visitado en el mundo, con lo que hiciste en cada uno. En el globo eliges un país, luego el departamento y luego el municipio, y ahí escribes la visita como un post de blog: el relato, las fotos y los lugares exactos donde estuviste. El mapa se va pintando a medida que viajas (Nicaragua: 9 de 17 departamentos). Si quieres, publicas una visita o un viaje, y otras personas guardan esos lugares en su lista "Quiero ir". Producción: https://traveltime-tan.vercel.app (un push a main despliega solo).檢視完整 README ↗
Los lugares que has visitado en el mundo, con lo que hiciste en cada uno. En el globo eliges un país, luego el departamento y luego el municipio, y ahí escribes la visita como un post de blog: el relato, las fotos y los lugares exactos donde estuviste. El mapa se va pintando a medida que viajas (Nicaragua: 9 de 17 departamentos). Si quieres, publicas una visita o un viaje, y otras personas guardan esos lugares en su lista "Quiero ir".
Producción: https://traveltime-tan.vercel.app (un push a main despliega solo).
Mismo stack que deudas: sin build, funciones serverless de Vercel y Turso/libSQL. Las fotos van a Cloudflare Images y los mapas a S3 detrás de CloudFront.
Portada (#/) | Encabezado con el globo girando detrás, cómo funciona, recomendaciones, últimos lugares publicados y, con cuenta, tu resumen y tu lista "Quiero ir". Botón "Instalar app" |
Mapa (#/mundo) | Globo → país → departamento → ciudad para 198 países, búsqueda, filtro en las listas largas, tres estilos de fondo |
Visita (#/NIC.granada.granada/v/12) | Relato por bloques, galería, lugares (pines), sugerencia de fechas desde las fotos, publicar, viaje al que pertenece |
Modo escritura (…/v/12/editar) | Editor tipo blog a pantalla completa, con borrador en el teléfono |
Viaje (#/viaje/3) | Visitas agrupadas: ruta numerada en el mapa y línea de tiempo |
Publicado (#/p/12, #/t/3) | Una visita o un viaje vistos por cualquiera, con "Quiero ir", "Ya estuve", compartir y PDF |
| Endpoint | Qué hace |
|---|---|
api/auth.js | Entrar con Google, salir, y el perfil (nombre, avatar, "Sobre mí"). La contraseña queda solo para cuentas de antes que aún no conectan Google |
api/marks.js | Lo visitado: lo marcado más los lugares que tienen visitas |
api/visits.js | Visitas de un lugar o de lo que hay dentro (por prefijo del id); publicar; "Ya estuve" (fromPinId) |
api/photos.js | Fotos de una visita: pedir la subida, confirmarla, pie de foto y borrar |
api/pins.js | Lugares puntuales de una visita (ver, comer, dormir, hacer, otro) |
api/trips.js | Viajes: crear, renombrar, meter y sacar visitas, publicar; compañeros (invitar por usuario, aceptar, sacar, salir) |
api/wishes.js | "Quiero ir": desde un lugar publicado o un municipio entero |
api/public.js | Lo publicado, sin sesión: portada, visita, viaje y lugares dentro de un lugar |
El código compartido va en api/_lib/, no en un lib/ en la raíz: Vercel publica como archivo estático todo lo de la raíz, y en el primer despliegue /lib/auth.js se podía descargar. api/ no se publica, y lo que empieza con _ no se vuelve función. Son 8 funciones de las 12 del plan Hobby.
npm test # ~215 pruebas contra los handlers reales: base descartable y Cloudflare simulado
npm run dev # http://localhost:3100, con las claves del .env (contra Turso si estan)
scripts/dev.mjs aplica las mismas cabeceras de vercel.json, así que lo que choca con la CSP falla en local igual que en producción. Los handlers se importan una vez: si tocas api/, reinicia el servidor. Para pruebas de punta a punta sin ensuciar Turso, levanta otro servidor con un .env sin TURSO_* ni RESEND_* (PORT=3101).
GOOGLE_CLIENT_ID, el ID de cliente OAuth de tipo Aplicación web; no hace falta el secreto). El navegador recibe de Google un ID token firmado y el servidor lo verifica (api/_lib/google.js): firma RS256 con las claves públicas de Google, aud, emisor, vencimiento y correo verificado. El usuario sale del correo (rosa.perez, rosa.perez2…). El registro con contraseña está cerrado (ALLOW_PASSWORD_SIGNUP=1 lo abre, solo lo usan las pruebas) y la contraseña solo abre cuentas que todavía no conectaron Google: mientras quede alguna, la ventana de entrar muestra "¿Tu cuenta es de antes?" (legacyLogin en ?config=1) y desaparece sola. Una cuenta de Google solo se une sola a una existente si el correo de esa cuenta estaba verificado: si no, quien se registró con un correo ajeno se quedaría con la cuenta de su dueño.#/perfil, y el público en #/u/usuario): avatar (inicial, ícono con color, la foto de Google, que se guarda en cada entrada, o una foto subida a Cloudflare, validada como propia), nombre, y el "Sobre mí" (presentación, dónde vive, idiomas, intereses de una lista fija, viaje soñado; api/_lib/profile.js). El público muestra solo eso y lo publicado: nunca marcas, visitas privadas, correo ni "Quiero ir".api/_lib/public.js. Sale el título, el lugar, las fechas, el relato, las fotos, los lugares y el @usuario. No salen el GPS ni la hora exacta de las fotos (solo el día), el correo, el nombre completo ni ids internos. Las pruebas buscan esas cadenas en la respuesta.@usuario; la invitación aparece en la portada y en el panel. Al aceptar, el compañero lee todas las visitas del viaje (relato, fotos y lugares, sin el GPS de las fotos, que es solo de su dueño) y suma las suyas. No edita, borra, publica ni saca del viaje lo ajeno, y no renombra, publica ni borra el viaje. Salirse (o que lo saquen) deja sus visitas suyas, fuera del viaje. Un invitado que no aceptó no ve nada. La regla vive en tripRole y readableVisit (api/_lib/trips.js).no-cache) y el CDN de Vercel guarda un minuto (Vercel-CDN-Cache-Control). Con stale-while-revalidate en Cache-Control, Chrome seguía mostrando hasta 5 minutos una visita despublicada. Un 404 no lo guarda nadie, porque si no ocultaría una visita recién publicada.Editor.js (en vendor/editorjs/, se carga al abrir el modo escritura): texto, títulos, listas (con viñetas, numeradas y de tareas), citas, separadores, y tres bloques propios: aviso con ícono, foto de la galería (o subida ahí mismo) y lugar de la visita. Al seleccionar texto: negrita, cursiva, resaltado y enlace.
api/_lib/story.js) y el navegador vuelve a limpiar al mostrar (js/story.js). Son dos cercos, porque lo publicado lo lee cualquiera.Viven en Cloudflare Images; en la base queda lo necesario para listarlas.
requireSignedURLs): cada URL va firmada con HMAC y vence a las 4 horas. Sin firma o con la firma vencida, Cloudflare responde 403. Esto está probado contra Cloudflare real.npm run cf:setup, una vez): ttthumb 400², ttcard 960×640 y ttfull hasta 2560. Llevan el prefijo tt porque las variantes son de toda la cuenta de Cloudflare, que se comparte con otros proyectos. Se entregan sin metadatos.El esquema se crea solo (CREATE TABLE IF NOT EXISTS y ALTER idempotentes). Las FOREIGN KEY no son una cascada confiable en Turso: el PRAGMA es por conexión y el cliente web no la mantiene. Por eso borrar una visita borra a mano sus fotos, lugares y viaje.
país → división 1 → "ciudad" → lugares (pines)
NIC Granada Granada Convento San Francisco, Isletas…
name:es. Motor: MapLibre en vendor/.scripts/build-geo.mjs: municipio (ADM2) en Nicaragua, comuna (ADM3) en Chile, cantón en Costa Rica, municipio (ADM3) en España, condado en EE. UU.NIC › NIC.granada › NIC.granada.granada. Lo visitado se cuenta por prefijo. El id sale del nombre limpio, no del shapeID de geoBoundaries, que cambia entre versiones y haría perder las visitas.geo/<ISO>/c/<depto>.json). Brasil tiene 5.570 municipios y España 8.205.lx/ly), no en el centroide, que en una media luna cae afuera.merge funde las partes de una ciudad dentro de un departamento grande (las 34 comunas del Gran Santiago, los 33 boroughs de Londres, los 23 distritos de Budapest) y whole deja sin partir los departamentos que ya son una ciudad (Ciudad de México, CABA, Montevideo, Seúl, Tokio, Bangkok…). Se eligieron a mano: juntar todo lo que cae en una misma mancha urbana fundía también la Costa Brava o Kamakura con Yokohama.fixMojibake).npm run geo # todo (~15 min; baja a .geo-cache/)
npm run geo -- NIC BRA # solo esos países
npm run geo:upload # sube geo/ a S3 (claves de AWS en .env)
npm run geo:upload -- --prune # y borra las versiones viejas (deja la actual y la anterior)
npm run vendor # después de actualizar maplibre-gl o @editorjs/*
Son ~140 MB y ~3.100 archivos, así que no van en git ni en Vercel (geo/ está en .gitignore y en .vercelignore). Van al bucket traveltimealfa (us-east-2) y la app los lee de https://d1kbjmlkyfoz97.cloudfront.net (js/config.js).
geo/<versión>/…), con caché de un año. geo/version.json se sube al final y sin caché, así que nadie ve una versión a medias.search.json: 2,7 MB → 746 KB)./geo. Con ?geo=remoto se lee de CloudFront, para probar una subida sin desplegar.manifest.webmanifest, íconos (scripts/make-icons.mjs, dibujados píxel a píxel sin dependencias) y sw.js:
/api y las fotos nunca se guardan en el teléfono.geo.js usa la última versión de los mapas que conoció, que es la que el service worker tiene.Coral para lo visitado y el progreso; tierra y agua frías a propósito. Globo con atmósfera en el mundo. Tres fondos (Claro, Oscuro, Colorido) más Automático, y la interfaz sigue al mapa. Animaciones al hacer scroll, con prefers-reduced-motion respetado. "Quiero ir" en el mapa es un anillo ámbar.
node scripts/place-renames.mjs <copia-de-geo-vieja> > renames.jsonnode --env-file=.env scripts/migrate-place-ids.mjs renames.json--apply