// ==================== MAP CODE ==================== (function () { // ==================== STATE ===================== let map; let currentJourney = { id: null, title: "", description: "", markers: [], }; let availableJourneys = []; let journeyPath; let activeMarker = null; // ==================== MAP INIT ================== function initMap() { map = L.map("map", { worldCopyJump: true, }).setView([46.8182, 8.2275], 8); L.tileLayer( "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© OpenStreetMap', }, ).addTo(map); journeyPath = L.polyline([], { color: "#4263eb", weight: 4, }).addTo(map); map.on("click", function (e) { if ( document .getElementById("mode-create") .classList.contains("active") ) { createMarker(normalizeLatLng(e.latlng), { title: "New Marker" }); } }); setTimeout(() => map.invalidateSize(), 300); } // ==================== MARKER FUNCTIONS ========== function normalizeLatLng(latlng) { return L.latLng( latlng.lat, ((((latlng.lng + 180) % 360) + 360) % 360) - 180, ); } function createMarker(latlng, content = {}) { latlng = normalizeLatLng(latlng); const marker = L.marker(latlng, { draggable: true, title: content.title || "Untitled", }).addTo(map); marker.bindPopup( `${content.title || "Untitled"}`, ); marker.on("click", () => openMarkerEditor(marker)); marker.on("dragend", () => { marker.setLatLng(normalizeLatLng(marker.getLatLng())); updateJourneyPath(); }); marker._content = { ...content, lat: latlng.lat, lng: latlng.lng, }; currentJourney.markers.push(marker); updateJourneyPath(); addMarkerToList(marker, latlng, content); return marker; } function addMarkerToList(marker, latlng, content) { const container = document.getElementById( "current-markers-container", ); const empty = container.querySelector(".empty-message"); if (empty) empty.remove(); const div = document.createElement("div"); div.className = "marker-item"; div.dataset.lat = latlng.lat; div.dataset.lng = latlng.lng; div.innerHTML = `
Nothing to preview yet.
'; } async function uploadMarkerImages(files) { if (!files.length) return []; const formData = new FormData(); files.forEach((file) => formData.append("images", file)); const res = await fetch(`${API_BASE}/uploads/images`, { method: "POST", body: formData, credentials: "include", }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Image upload failed"); return data.images || []; } // ==================== MODAL ===================== function openMarkerEditor(marker) { activeMarker = marker; const latlng = marker.getLatLng(); const content = marker._content || {}; document.getElementById("marker-title").value = content.title || ""; document.getElementById("marker-date").value = content.date || ""; document.getElementById("marker-text").value = content.description || ""; updateMarkerMarkdownPreview(content.description || ""); document.getElementById("marker-images").value = ""; renderMarkerImages(content.images || []); document.getElementById("marker-coords").value = `${latlng.lat.toFixed(6)}, ${latlng.lng.toFixed(6)}`; document .getElementById("marker-modal") .classList.add("active"); } function closeModal() { document .getElementById("marker-modal") .classList.remove("active"); activeMarker = null; } async function saveMarker() { if (!activeMarker) return; const title = document.getElementById("marker-title").value || "Untitled"; const date = document.getElementById("marker-date").value; const description = document.getElementById("marker-text").value; const imageInput = document.getElementById("marker-images"); const existingImages = activeMarker._content?.images || []; let images = existingImages; try { const uploadedImages = await uploadMarkerImages(Array.from(imageInput.files)); images = [...existingImages, ...uploadedImages]; } catch (err) { showToast(err.message, "error"); return; } // Update marker's tooltip title and popup activeMarker.options.title = title; activeMarker.setPopupContent(`${title}`); // Store content for saving activeMarker._content = { title, date, description, images }; // Update the list item in the sidebar const latlng = activeMarker.getLatLng(); const items = document.querySelectorAll("#current-markers-container .marker-item"); for (let item of items) { if (item.dataset.lat == latlng.lat && item.dataset.lng == latlng.lng) { item.querySelector(".marker-title").textContent = title; break; } } closeModal(); showToast("Saving journey..."); await saveJourneyToAPI(); } function deleteMarkerFromMap() { if (!activeMarker) return; const latlng = activeMarker.getLatLng(); map.removeLayer(activeMarker); const idx = currentJourney.markers.indexOf(activeMarker); if (idx > -1) currentJourney.markers.splice(idx, 1); const items = document.querySelectorAll( "#current-markers-container .marker-item", ); for (let item of items) { if ( item.dataset.lat == latlng.lat && item.dataset.lng == latlng.lng ) { item.remove(); break; } } if (currentJourney.markers.length === 0) { document.getElementById( "current-markers-container", ).innerHTML = ''; } updateJourneyPath(); closeModal(); showToast("Marker deleted"); } // ==================== API INTERACTION (with credentials) ==================== async function fetchJourneys() { try { const res = await fetch(`${API_BASE}/journeys`, { credentials: "include", }); if (!res.ok) throw new Error("Failed to fetch"); const journeys = await res.json(); availableJourneys = journeys; populateJourneySelect(journeys); } catch (err) { showToast("Error loading journeys", "error"); } } function populateJourneySelect(journeys) { const select = document.getElementById("journey-select"); select.innerHTML = ''; journeys.forEach((j) => { const opt = document.createElement("option"); opt.value = j.id; opt.textContent = j.title; select.appendChild(opt); }); } async function loadJourney(journeyId) { try { const res = await fetch( `${API_BASE}/journeys/${journeyId}`, { credentials: "include" }, ); if (!res.ok) throw new Error("Failed to load"); const journey = await res.json(); displayJourneyInfo(journey); } catch (err) { showToast("Error loading journey", "error"); } } function displayJourneyInfo(journey) { document.getElementById("info-title").textContent = journey.title; document.getElementById("info-description").textContent = journey.description || "-"; document.getElementById("info-marker-count").textContent = journey.markers ? journey.markers.length : 0; document.getElementById("info-date").textContent = new Date(journey.created_at).toLocaleDateString() || "-"; document.getElementById( "load-journey-btn", ).dataset.journey = JSON.stringify(journey); document.getElementById("edit-journey-btn").disabled = false; document.getElementById("delete-journey-btn").disabled = false; } function displayAllJourneysInfo() { const markerCount = availableJourneys.reduce( (total, journey) => total + (journey.markers ? journey.markers.length : 0), 0, ); document.getElementById("info-title").textContent = "All Journeys"; document.getElementById("info-description").textContent = `${availableJourneys.length} journeys selected`; document.getElementById("info-marker-count").textContent = markerCount; document.getElementById("info-date").textContent = "-"; document.getElementById("load-journey-btn").dataset.journey = JSON.stringify({ type: "all", }); document.getElementById("edit-journey-btn").disabled = true; document.getElementById("delete-journey-btn").disabled = true; } async function saveJourneyToAPI() { const title = document .getElementById("journey-title") .value.trim(); if (!title) { showToast("Please enter a title", "error"); return; } const description = document.getElementById( "journey-description", ).value; const markers = currentJourney.markers.map((m) => { const latlng = normalizeLatLng(m.getLatLng()); m.setLatLng(latlng); const content = m._content || {}; return { lat: latlng.lat, lng: latlng.lng, title: content.title || "", date: content.date || "", description: content.description || "", images: content.images || [], }; }); const visibility = document.getElementById('journey-visibility').value; const payload = { title, description, markers, visibility }; try { let res; if (currentJourney.id) { res = await fetch( `${API_BASE}/journeys/${currentJourney.id}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), credentials: "include", }, ); } else { res = await fetch(`${API_BASE}/journeys`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), credentials: "include", }); } if (!res.ok) throw new Error("Save failed"); const data = await res.json(); currentJourney.id = data.id; showToast("Journey saved!"); fetchJourneys(); } catch (err) { showToast("Error saving journey", "error"); } } async function deleteJourneyFromAPI(journeyId) { if (!confirm("Delete this journey?")) return; try { const res = await fetch( `${API_BASE}/journeys/${journeyId}`, { method: "DELETE", credentials: "include", }, ); if (!res.ok) throw new Error("Delete failed"); showToast("Journey deleted"); fetchJourneys(); document.getElementById("info-title").textContent = "-"; document.getElementById( "info-description", ).textContent = "-"; document.getElementById( "info-marker-count", ).textContent = "0"; document.getElementById("info-date").textContent = "-"; } catch (err) { showToast("Error deleting journey", "error"); } } function loadJourneyMarkers(journey) { currentJourney.markers.forEach((m) => map.removeLayer(m)); currentJourney.markers = []; document.getElementById('journey-visibility').value = journey.visibility || 'private'; document.getElementById( "current-markers-container", ).innerHTML = ''; if (journey.markers) { journey.markers.forEach((m) => { const latlng = L.latLng(m.lat, m.lng); const marker = createMarker(latlng, { title: m.title, date: m.date, description: m.description, images: m.images || [], }); marker._content = { ...m }; }); } currentJourney.id = journey.id; document.getElementById("journey-title").value = journey.title; document.getElementById("journey-description").value = journey.description || ""; setMode("create"); showToast("Journey loaded on map"); } function loadAllJourneyMarkers() { currentJourney.markers.forEach((m) => map.removeLayer(m)); currentJourney.markers = []; currentJourney.id = null; currentJourney.title = "All Journeys"; currentJourney.description = ""; document.getElementById("current-markers-container").innerHTML = ''; availableJourneys.forEach((journey) => { (journey.markers || []).forEach((m) => { const latlng = L.latLng(m.lat, m.lng); const markerTitle = m.title || "Untitled"; const displayTitle = `${journey.title}: ${markerTitle}`; const marker = createMarker(latlng, { title: displayTitle, date: m.date, description: m.description, images: m.images || [], }); marker._content = { ...m, title: displayTitle, journeyId: journey.id, journeyTitle: journey.title, }; }); }); document.getElementById("journey-title").value = "All Journeys"; document.getElementById("journey-description").value = ""; setMode("view"); if (currentJourney.markers.length > 0) { journeyPath.setLatLngs([]); map.fitBounds(L.featureGroup(currentJourney.markers).getBounds()); showToast("All journey markers loaded"); } else { updateJourneyPath(); showToast("No markers found", "warning"); } } // ==================== UI HELPERS ================ function showToast(msg, type = "success") { const toast = document.getElementById("toast"); document.getElementById("toast-message").textContent = msg; toast.style.display = "block"; setTimeout(() => { toast.style.display = "none"; }, 3000); } function setMode(mode) { const createBtn = document.getElementById("mode-create"); const viewBtn = document.getElementById("mode-view"); const createPanel = document.getElementById("create-panel"); const viewPanel = document.getElementById("view-panel"); const indicator = document.getElementById("mode-indicator"); if (mode === "create") { createBtn.classList.add("active"); viewBtn.classList.remove("active"); createPanel.classList.add("active-panel"); viewPanel.classList.remove("active-panel"); indicator.querySelector(".indicator-text").textContent = "Creating: New Journey"; indicator.querySelector(".indicator-dot").className = "indicator-dot creating"; } else { viewBtn.classList.add("active"); createBtn.classList.remove("active"); viewPanel.classList.add("active-panel"); createPanel.classList.remove("active-panel"); indicator.querySelector(".indicator-text").textContent = "Viewing Journeys"; indicator.querySelector(".indicator-dot").className = "indicator-dot viewing"; } } function clearAllMarkers() { currentJourney.markers.forEach((m) => map.removeLayer(m)); currentJourney.markers = []; document.getElementById( "current-markers-container", ).innerHTML = ''; updateJourneyPath(); showToast("All markers cleared"); } // ==================== EVENT LISTENERS (run after auth) ==================== function bindEventListeners() { document .getElementById("mode-create") .addEventListener("click", () => setMode("create")); document .getElementById("mode-view") .addEventListener("click", () => { setMode("view"); fetchJourneys(); }); document .getElementById("clear-markers-btn") .addEventListener("click", clearAllMarkers); document .getElementById("save-journey-btn") .addEventListener("click", saveJourneyToAPI); document .getElementById("close-modal") .addEventListener("click", closeModal); document .getElementById("close-modal-footer") .addEventListener("click", closeModal); document .getElementById("save-marker") .addEventListener("click", saveMarker); document .getElementById("delete-marker") .addEventListener("click", deleteMarkerFromMap); document .getElementById("toggle-sidebar") .addEventListener("click", () => { document .querySelector(".sidebar") .classList.toggle("collapsed"); }); document .getElementById("zoom-in") .addEventListener("click", () => map.zoomIn()); document .getElementById("zoom-out") .addEventListener("click", () => map.zoomOut()); document .getElementById("fit-all") .addEventListener("click", () => { if (currentJourney.markers.length > 0) { const group = L.featureGroup( currentJourney.markers, ); map.fitBounds(group.getBounds()); } else { showToast("No markers to fit", "warning"); } }); document .getElementById("locate-me") .addEventListener("click", () => { map.locate({ setView: true, maxZoom: 16 }); }); document .getElementById("journey-select") .addEventListener("change", (e) => { const val = e.target.value; if (val === "all") { displayAllJourneysInfo(); } else if (val) { loadJourney(val); } }); document .getElementById("load-journey-btn") .addEventListener("click", () => { const journeyData = document.getElementById("load-journey-btn") .dataset.journey; if (!journeyData) return; const journey = JSON.parse(journeyData); if (journey.type === "all") { loadAllJourneyMarkers(); } else { loadJourneyMarkers(journey); } }); document .getElementById("edit-journey-btn") .addEventListener("click", () => { const journeyData = document.getElementById("load-journey-btn") .dataset.journey; if (journeyData) loadJourneyMarkers(JSON.parse(journeyData)); }); document .getElementById("delete-journey-btn") .addEventListener("click", () => { const journeyData = document.getElementById("load-journey-btn") .dataset.journey; if (journeyData) deleteJourneyFromAPI( JSON.parse(journeyData).id, ); }); document .getElementById("add-marker-btn") .addEventListener("click", () => { setMode("create"); showToast("Click on the map to add a marker"); }); document .getElementById("marker-text") .addEventListener("input", (e) => updateMarkerMarkdownPreview(e.target.value)); } // Expose functions globally (optional) window.createMarker = createMarker; window.updateJourneyPath = updateJourneyPath; window.clearAllMarkers = clearAllMarkers; window.setMode = setMode; // ==================== START ==================== function startMap() { initMap(); // Check for journey parameter const urlParams = new URLSearchParams(window.location.search); const journeyToLoad = urlParams.get('journey'); if (journeyToLoad) { fetch(`${API_BASE}/journeys/${journeyToLoad}`, { credentials: 'include' }) .then(res => res.json()) .then(journey => { if (journey.can_edit) { loadJourneyMarkers(journey); } else { showToast('You cannot edit this journey', true); } }) .catch(err => console.error(err)); } bindEventListeners(); } window.startMap = startMap; })(); // end of IIFE // ==================== SIDEBAR HELPER (outside IIFE) ==================== function initSidebar() { const sidebar = document.querySelector('.sidebar'); if (!sidebar) return; const isMobile = window.innerWidth <= 768; if (isMobile) { sidebar.classList.add('collapsed'); } else { sidebar.classList.remove('collapsed'); } } // ==================== AUTHENTICATION CHECK ==================== document.addEventListener("DOMContentLoaded", async () => { const authenticated = await checkAuthAndRedirect(); if (authenticated) { updateUserMenu(); window.addEventListener('resize', initSidebar); initSidebar(); window.startMap(); } });