diff --git a/.gitignore b/.gitignore index dd616b5..44f8308 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ AGENTS.md config.js -data/routes.json -data/routes.json.tmp diff --git a/README.md b/README.md index 3166c5d..ee46abe 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ A small static webpage for saving and sharing Google Maps route links. - Optional date, description, and uploaded picture - Click route names to open saved Google Maps links - Edit, remove, share, or copy route links -- Routes are stored on the webserver in `data/routes.json` +- Data is stored locally in the browser with `localStorage` - Simple secret gate before the app opens -Deploy the files to a PHP-enabled webserver and open `index.html` in a browser. +Open `index.html` in a browser to use it. ## Secret @@ -25,6 +25,4 @@ cp config.example.js config.js window.ROUTES_SECRET = "your-secret"; ``` -`config.js` is gitignored. `api.php` reads the same secret and stores shared routes in `data/routes.json`, which is also gitignored. - -Make sure the webserver user can write to the `data/` directory. This is a simple shared-secret gate, not strong security. +`config.js` is gitignored. This is a simple client-side gate, not strong server-side security. diff --git a/app.js b/app.js index c0cab44..7266d35 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,6 @@ const STORAGE_KEY = "routes.links.v1"; const AUTH_SESSION_KEY = "routes.authenticated.v1"; const AUTH_SECRET = window.ROUTES_SECRET; -const API_URL = window.ROUTES_API_URL || "api.php"; const authScreen = document.querySelector("#auth-screen"); const authForm = document.querySelector("#auth-form"); @@ -30,7 +29,7 @@ const archiveCount = document.querySelector("#archive-count"); const archivedRoutesList = document.querySelector("#archived-routes-list"); const template = document.querySelector("#route-card-template"); -let routes = []; +let routes = sortRoutes(loadRoutes()); authForm.addEventListener("submit", authenticate); logoutButton.addEventListener("click", lockApp); @@ -69,20 +68,12 @@ function authenticate(event) { unlockApp(); } -async function unlockApp() { +function unlockApp() { authScreen.hidden = true; appShell.hidden = false; authSecretInput.value = ""; authMessage.textContent = ""; - routeCount.textContent = "Loading routes..."; - - try { - routes = await loadRoutesFromBackend(); - renderRoutes(); - } catch (error) { - routeCount.textContent = "Could not load routes."; - alert(error.message); - } + renderRoutes(); } function lockApp() { @@ -93,7 +84,7 @@ function lockApp() { authSecretInput.focus(); } -function loadLocalRoutes() { +function loadRoutes() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) ?? []; } catch { @@ -101,45 +92,8 @@ function loadLocalRoutes() { } } -async function loadRoutesFromBackend() { - const response = await fetch(API_URL, { - headers: { "X-Routes-Secret": AUTH_SECRET }, - cache: "no-store", - }); - - if (!response.ok) { - throw new Error(`Could not load routes (${response.status}).`); - } - - const data = await response.json(); - const backendRoutes = sortRoutes(data.routes ?? []); - const localRoutes = sortRoutes(loadLocalRoutes()); - - if (!backendRoutes.length && localRoutes.length) { - routes = localRoutes; - await storeRoutes(); - return localRoutes; - } - - localStorage.setItem(STORAGE_KEY, JSON.stringify(backendRoutes)); - return backendRoutes; -} - -async function storeRoutes() { +function storeRoutes() { localStorage.setItem(STORAGE_KEY, JSON.stringify(routes)); - - const response = await fetch(API_URL, { - method: "PUT", - headers: { - "Content-Type": "application/json", - "X-Routes-Secret": AUTH_SECRET, - }, - body: JSON.stringify({ routes }), - }); - - if (!response.ok) { - throw new Error(`Could not save routes (${response.status}).`); - } } function sortRoutes(routeList) { @@ -198,14 +152,9 @@ async function saveRoute(event) { } routes = sortRoutes(routes); - - try { - await storeRoutes(); - renderRoutes(); - resetForm(); - } catch (error) { - alert(error.message); - } + storeRoutes(); + renderRoutes(); + resetForm(); } function renderRoutes() { @@ -240,7 +189,6 @@ function renderRoutes() { function createRouteCard(route) { const card = template.content.firstElementChild.cloneNode(true); const image = card.querySelector(".route-image"); - const picture = card.querySelector(".route-picture"); const meta = card.querySelector(".route-meta"); const title = card.querySelector(".route-title"); const description = card.querySelector(".route-description"); @@ -257,7 +205,7 @@ function createRouteCard(route) { if (route.picture) { image.hidden = false; - picture.src = route.picture; + image.style.backgroundImage = `url("${cssUrlEscape(route.picture)}")`; } return card; @@ -330,7 +278,7 @@ function handleRouteAction(event) { } } -async function toggleArchive(id) { +function toggleArchive(id) { const route = routes.find((item) => item.id === id); if (!route) { return; @@ -338,17 +286,11 @@ async function toggleArchive(id) { route.archived = !route.archived; routes = sortRoutes(routes); + storeRoutes(); + renderRoutes(); - try { - await storeRoutes(); - renderRoutes(); - - if (idInput.value === id) { - resetForm(); - } - } catch (error) { - route.archived = !route.archived; - alert(error.message); + if (idInput.value === id) { + resetForm(); } } @@ -377,43 +319,29 @@ function editRoute(route) { nameInput.focus(); } -async function removeRoute(id) { +function removeRoute(id) { const route = routes.find((item) => item.id === id); if (!route || !confirm(`Remove "${route.name}"?`)) { return; } - const previousRoutes = routes; routes = routes.filter((item) => item.id !== id); - - try { - await storeRoutes(); - renderRoutes(); - if (idInput.value === id) { - resetForm(); - } - } catch (error) { - routes = previousRoutes; - alert(error.message); + storeRoutes(); + renderRoutes(); + if (idInput.value === id) { + resetForm(); } } -async function removeAllRoutes() { +function removeAllRoutes() { if (!routes.length || !confirm("Remove all saved routes?")) { return; } - const previousRoutes = routes; routes = []; - - try { - await storeRoutes(); - renderRoutes(); - resetForm(); - } catch (error) { - routes = previousRoutes; - alert(error.message); - } + storeRoutes(); + renderRoutes(); + resetForm(); } async function shareRoute(route) { @@ -500,3 +428,7 @@ function isValidUrl(value) { function formatDate(value) { return new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(new Date(`${value}T00:00:00`)); } + +function cssUrlEscape(value) { + return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"'); +} diff --git a/config.example.js b/config.example.js index f2e6439..7b4a660 100644 --- a/config.example.js +++ b/config.example.js @@ -1,3 +1 @@ window.ROUTES_SECRET = "change-this-secret"; -// Optional if api.php is not in the same directory as index.html: -// window.ROUTES_API_URL = "api.php"; diff --git a/index.html b/index.html index 34d4a78..6d4ce57 100644 --- a/index.html +++ b/index.html @@ -94,7 +94,7 @@