14.2 KiB · text History ee891dc
// We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss).

// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
import "@fortawesome/fontawesome-free/js/all"

// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

// Import compression libraries for map data optimization
import pako from 'pako'
import { Base64 } from 'js-base64'

// Import Web Components for Leaflet maps
import './leaflet/leaflet-map'
import './leaflet/leaflet-marker'
import './leaflet/leaflet-icon'

// Make compression libraries available globally for leaflet hook
window.pako = pako;
window.Base64 = Base64;

// import './leaflet/leaflet-map'
// import './leaflet/leaflet-marker'
// import './leaflet/leaflet-icon'

import L from 'leaflet'
import 'leaflet-centermarker'
import LeafletMap from './hooks/leaflet_map.js'

import LeafletMapBridge from './hooks/leaflet_map_bridge.js'

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")

let Hooks = {};

// Theme detection and UI updates
Hooks.ThemeDetector = {
  mounted() {
    this.updateThemeIndicators();
    
    // Listen for theme changes
    window.addEventListener("phx:set-theme", () => {
      setTimeout(() => this.updateThemeIndicators(), 100);
    });
    
    // Listen for storage changes (cross-tab)
    window.addEventListener("storage", (e) => {
      if (e.key === "apollos:theme") {
        setTimeout(() => this.updateThemeIndicators(), 100);
      }
    });
    
    // Listen for system theme changes
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
      if (!localStorage.getItem("apollos:theme")) {
        setTimeout(() => this.updateThemeIndicators(), 100);
      }
    });
  },
  
  updateThemeIndicators() {
    const currentTheme = this.getCurrentTheme();
    const themeCards = this.el.querySelectorAll('.theme-card');
    
    themeCards.forEach(card => {
      const themeName = card.getAttribute('data-theme-name');
      const indicator = card.querySelector('.theme-indicator');
      
      if (themeName === currentTheme) {
        // Show as current theme
        card.classList.add('border-blue-500', 'ring-2', 'ring-blue-500');
        card.classList.remove('border-gray-200', 'dark:border-gray-700');
        indicator.classList.remove('hidden');
      } else {
        // Show as selectable theme
        card.classList.remove('border-blue-500', 'ring-2', 'ring-blue-500');
        card.classList.add('border-gray-200', 'dark:border-gray-700');
        indicator.classList.add('hidden');
      }
    });
  },
  
  getCurrentTheme() {
    const savedTheme = localStorage.getItem("apollos:theme");
    if (savedTheme) {
      return savedTheme;
    }
    
    // Check if system theme is being used
    const currentDataTheme = document.documentElement.getAttribute("data-theme");
    if (!savedTheme && currentDataTheme) {
      // This means system theme is active, determine what it resolved to
      const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      if (currentDataTheme === (isDark ? 'dark' : 'lofi')) {
        return 'system';
      }
    }
    
    return currentDataTheme || 'system';
  }
};

// Add our new LeafletMap hook
Hooks.LeafletMap = LeafletMap;
Hooks.MapPush = {
    mounted() {
        window.addEventListener("focus", e => {

            this.pushEvent("load-more", {})

        })
    },
}
Hooks.mkMap = {
    mounted() {
        const markers = {}
        let searchMarker = null;

        let coords;
        let latlng = document.getElementById('map').getAttribute('data-latlng')
        if (latlng != null) {
            coords = JSON.parse(latlng)
        } else {
            coords = [42.3736, -71.1097]
        }

        var map = L.map('map', {keyboard: true}).setView(coords, 13);

        const view = this;
        var centerMarker = L.centerMarker(map).on("newposition", function() {
            var latlng = centerMarker.getLatLng()
            console.log("New position: " + latlng.lat + ", " + latlng.lng);
            view.pushEventTo("#foci-form", "map-update", { latlng: latlng });
        });
        centerMarker.addTo(map);

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
            attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
        }).addTo(map);

        // Search functionality
        const searchInput = document.getElementById('location-search');
        const searchResults = document.getElementById('search-results');
        let searchTimeout;

        const performSearch = async (query) => {
            if (query.length < 3) {
                searchResults.classList.add('hidden');
                return;
            }

            try {
                const response = await fetch(
                    `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&addressdetails=1`
                );
                const results = await response.json();

                displaySearchResults(results);
            } catch (error) {
                console.error('Search error:', error);
                searchResults.classList.add('hidden');
            }
        };

        const displaySearchResults = (results) => {
            searchResults.innerHTML = '';

            if (results.length === 0) {
                searchResults.innerHTML = '<div class="p-3 text-gray-500">No results found</div>';
                searchResults.classList.remove('hidden');
                return;
            }

            results.forEach(result => {
                const resultDiv = document.createElement('div');
                resultDiv.className = 'p-3 hover:bg-gray-100 cursor-pointer border-b border-gray-200 last:border-b-0';
                resultDiv.innerHTML = `
                    <div class="font-medium">${result.display_name}</div>
                    <div class="text-sm text-gray-600">${result.type} • ${result.class}</div>
                `;

                resultDiv.addEventListener('click', () => {
                    selectSearchResult(result);
                });

                searchResults.appendChild(resultDiv);
            });

            searchResults.classList.remove('hidden');
        };

        const selectSearchResult = (result) => {
            const lat = parseFloat(result.lat);
            const lon = parseFloat(result.lon);

            // Update search input
            searchInput.value = result.display_name;
            searchResults.classList.add('hidden');

            // Remove previous search marker
            if (searchMarker) {
                map.removeLayer(searchMarker);
            }

            // Add new search marker
            searchMarker = L.marker([lat, lon], {
                icon: L.icon({
                    iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
                    shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
                    iconSize: [25, 41],
                    iconAnchor: [12, 41],
                    popupAnchor: [1, -34],
                    shadowSize: [41, 41]
                })
            }).addTo(map);

            // Add popup with result info
            searchMarker.bindPopup(`
                <div>
                    <strong>${result.display_name}</strong><br>
                    <small>${result.type} • ${result.class}</small><br>
                    <button onclick="window.selectThisLocation(${lat}, ${lon})" class="btn btn-sm btn-primary mt-2">
                        Select This Location
                    </button>
                </div>
            `).openPopup();

            // Pan to the result
            map.setView([lat, lon], 16);
        };

        // Global function to select a location from popup
        window.selectThisLocation = (lat, lon) => {
            // Move center marker to selected location
            centerMarker.setLatLng([lat, lon]);
            map.setView([lat, lon], 16);

            // Trigger the position update
            view.pushEventTo("#foci-form", "map-update", {latlng: {lat: lat, lng: lon}});

            // Close popup
            if (searchMarker) {
                searchMarker.closePopup();
            }
        };

        // Search input event listeners
        if (searchInput) {
            searchInput.addEventListener('input', (e) => {
                clearTimeout(searchTimeout);
                const query = e.target.value.trim();

                if (query.length === 0) {
                    searchResults.classList.add('hidden');
                    return;
                }

                searchTimeout = setTimeout(() => {
                    performSearch(query);
                }, 300);
            });

            // Hide search results when clicking outside
            document.addEventListener('click', (e) => {
                if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
                    searchResults.classList.add('hidden');
                }
            });

            // Show search results when focusing on input
            searchInput.addEventListener('focus', () => {
                if (searchInput.value.trim().length >= 3) {
                    performSearch(searchInput.value.trim());
                }
            });
        }

        this.handleEvent("add_marker", ({lat, lon}) => {
            const marker = L.marker(L.latLng(lat, lon))
            marker.addTo(map)
        })
    }
}

Hooks.mkTesterMap = {
    latlng() { return this.el.dataset.latlng },
    mounted() {
        var latlng = this.latlng()
        console.log("MOUNTEd")
        console.log(latlng)
        if (latlng == null) {
            let map = L.map('map', {keyboard: true}).setView([42.3736, -71.1097], 13);
        } else {
            let map = L.map("map", { keyboard: true }).setView(JSON.parse(this.latlng()), 13);
        }
        const markers = {}

        const view = this;
        // var marker = L.centerMarker(map).on("newposition", function() {
        //     var latlng = marker.getLatLng()
        //     console.log("New position: " + latlng.lat + ", " + latlng.lng);
        //     view.pushEventTo("#foci-form", "map-update", {latlng: latlng})
        //
        // });
        // marker.addTo(map);

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
            attribution: '&copy; <a href="https//:openstreetmap.org/copyright">OpenStreetMap contributors</a>'
        }).addTo(map);

        this.handleEvent("add_marker", ({lat, lon}) => {
            const marker = L.marker(L.latLng(lat, lon))
            marker.addTo(map)
        })
    }
}

Hooks.mkShowMap = {
    latlng() { return this.el.dataset.latlng },
    mounted() {
        var latlng = this.latlng()
        console.log("Show Map Mounted")
        console.log(latlng)

        let coords;
        if (latlng == null) {
            coords = [42.3736, -71.1097];
        } else {
            coords = JSON.parse(latlng);
        }

        // Create static map for show view with unique ID
        let map = L.map(this.el.id, {
            keyboard: false,
            dragging: false,
            touchZoom: false,
            scrollWheelZoom: false,
            doubleClickZoom: false,
            boxZoom: false,
            tap: false
        }).setView(coords, 15);

        // Add a marker at the location if coordinates exist
        if (latlng != null) {
            L.marker(coords).addTo(map);
        }

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
            attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
        }).addTo(map);
    }
}

// let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: {...Hooks, LeafletMap, LeafletMapBridge}
})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#b57979"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

// Handle file downloads
window.addEventListener("phx:download", (event) => {
  const { data, filename, mime_type } = event.detail;
  const blob = new Blob([data], { type: mime_type });
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  window.URL.revokeObjectURL(url);
  document.body.removeChild(a);
});

// Handle file uploads
window.addEventListener("phx:upload_file", (event) => {
  const input = document.createElement("input");
  input.type = "file";
  input.accept = ".json";
  input.addEventListener("change", (e) => {
    const file = e.target.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const fileData = e.target.result;
        liveSocket.pushEvent("process-import", { file_data: fileData });
      };
      reader.readAsText(file);
    }
  });
  input.click();
});

// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket