const STORAGE_KEY = "routes.links.v1"; const AUTH_SESSION_KEY = "routes.authenticated.v1"; // Simple shared secret. Change this before publishing if needed. const AUTH_SECRET = "change-me"; const authScreen = document.querySelector("#auth-screen"); const authForm = document.querySelector("#auth-form"); const authSecretInput = document.querySelector("#auth-secret"); const authMessage = document.querySelector("#auth-message"); const appShell = document.querySelector("#app-shell"); const logoutButton = document.querySelector("#logout-button"); const form = document.querySelector("#route-form"); const idInput = document.querySelector("#route-id"); const nameInput = document.querySelector("#route-name"); const linkInput = document.querySelector("#route-link"); const dateInput = document.querySelector("#route-date"); const pictureInput = document.querySelector("#route-picture"); const descriptionInput = document.querySelector("#route-description"); const saveButton = document.querySelector("#save-button"); const cancelEditButton = document.querySelector("#cancel-edit"); const clearAllButton = document.querySelector("#clear-all"); const routeCount = document.querySelector("#route-count"); const emptyState = document.querySelector("#empty-state"); const routesList = document.querySelector("#routes-list"); const template = document.querySelector("#route-card-template"); let routes = loadRoutes(); authForm.addEventListener("submit", authenticate); logoutButton.addEventListener("click", lockApp); form.addEventListener("submit", saveRoute); cancelEditButton.addEventListener("click", resetForm); clearAllButton.addEventListener("click", removeAllRoutes); routesList.addEventListener("click", handleRouteAction); if (sessionStorage.getItem(AUTH_SESSION_KEY) === "yes") { unlockApp(); } else { lockApp(); } function authenticate(event) { event.preventDefault(); authMessage.textContent = ""; if (authSecretInput.value.trim() !== AUTH_SECRET) { authMessage.textContent = "Wrong secret."; authSecretInput.select(); return; } sessionStorage.setItem(AUTH_SESSION_KEY, "yes"); unlockApp(); } function unlockApp() { authScreen.hidden = true; appShell.hidden = false; authSecretInput.value = ""; authMessage.textContent = ""; renderRoutes(); } function lockApp() { sessionStorage.removeItem(AUTH_SESSION_KEY); appShell.hidden = true; authScreen.hidden = false; resetForm(); authSecretInput.focus(); } function loadRoutes() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) ?? []; } catch { return []; } } function storeRoutes() { localStorage.setItem(STORAGE_KEY, JSON.stringify(routes)); } function saveRoute(event) { event.preventDefault(); const route = { id: idInput.value || crypto.randomUUID(), name: nameInput.value.trim(), link: linkInput.value.trim(), date: dateInput.value, picture: pictureInput.value.trim(), description: descriptionInput.value.trim(), updatedAt: new Date().toISOString(), }; if (!route.name || !route.link) { return; } if (!isValidUrl(route.link) || (route.picture && !isValidUrl(route.picture))) { alert("Please enter valid URLs for the map link and picture."); return; } const existingIndex = routes.findIndex((item) => item.id === route.id); if (existingIndex >= 0) { routes[existingIndex] = route; } else { routes.unshift(route); } storeRoutes(); renderRoutes(); resetForm(); } function renderRoutes() { routesList.innerHTML = ""; routeCount.textContent = routes.length === 1 ? "1 route saved." : `${routes.length} routes saved.`; emptyState.classList.toggle("hidden", routes.length > 0); clearAllButton.hidden = routes.length === 0; for (const route of routes) { const card = template.content.firstElementChild.cloneNode(true); const image = card.querySelector(".route-image"); const meta = card.querySelector(".route-meta"); const title = card.querySelector(".route-title"); const description = card.querySelector(".route-description"); const openLink = card.querySelector(".open-link"); card.dataset.id = route.id; meta.textContent = route.date ? formatDate(route.date) : "No date"; title.textContent = route.name; title.href = route.link; openLink.href = route.link; description.textContent = route.description || "No description added."; if (route.picture) { image.style.backgroundImage = `url("${cssUrlEscape(route.picture)}")`; } routesList.append(card); } } function handleRouteAction(event) { const button = event.target.closest("button"); if (!button) { return; } const card = button.closest(".route-card"); const route = routes.find((item) => item.id === card?.dataset.id); if (!route) { return; } if (button.classList.contains("edit-route")) { editRoute(route); } if (button.classList.contains("remove-route")) { removeRoute(route.id); } if (button.classList.contains("share-route")) { shareRoute(route); } } function editRoute(route) { idInput.value = route.id; nameInput.value = route.name; linkInput.value = route.link; dateInput.value = route.date; pictureInput.value = route.picture; descriptionInput.value = route.description; saveButton.textContent = "Save changes"; cancelEditButton.hidden = false; form.scrollIntoView({ behavior: "smooth", block: "start" }); nameInput.focus(); } function removeRoute(id) { const route = routes.find((item) => item.id === id); if (!route || !confirm(`Remove "${route.name}"?`)) { return; } routes = routes.filter((item) => item.id !== id); storeRoutes(); renderRoutes(); if (idInput.value === id) { resetForm(); } } function removeAllRoutes() { if (!routes.length || !confirm("Remove all saved routes?")) { return; } routes = []; storeRoutes(); renderRoutes(); resetForm(); } async function shareRoute(route) { const shareData = { title: route.name, text: route.description || route.name, url: route.link, }; if (navigator.share) { try { await navigator.share(shareData); return; } catch (error) { if (error.name === "AbortError") { return; } } } await navigator.clipboard.writeText(route.link); alert("Route link copied to clipboard."); } function resetForm() { form.reset(); idInput.value = ""; saveButton.textContent = "Add route"; cancelEditButton.hidden = true; } function isValidUrl(value) { try { const url = new URL(value); return url.protocol === "http:" || url.protocol === "https:"; } catch { return false; } } function formatDate(value) { return new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(new Date(`${value}T00:00:00`)); } function cssUrlEscape(value) { return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"'); }