📄 src/routes/history/map/+page.svelte
<script lang="ts">
  import L from "leaflet";
  import "leaflet/dist/leaflet.css";
  import { get } from "svelte/store";
  import { historyStore } from "$lib/history";
  import { labelsStore } from "$lib/labels";

  function buildMap(node: HTMLElement) {
    const entries = get(historyStore).filter((e) => e.coords != null);
    const labels = get(labelsStore);

    const map = L.map(node, { zoomControl: true });

    L.tileLayer(
      "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
      {
        attribution:
          '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/">CARTO</a>',
        maxZoom: 19,
      },
    ).addTo(map);

    const birdIcon = L.divIcon({
      html: `<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
        <polygon points="13,9 20,6 18,13 13,12" fill="#c4253a"/>
        <ellipse cx="9" cy="11" rx="6" ry="4.5" fill="#c4253a"/>
        <circle cx="5" cy="8" r="4" fill="#c4253a"/>
        <polygon points="1,8 5,6.5 5,9.5" fill="#e89a3a"/>
        <circle cx="3.5" cy="7.5" r="0.9" fill="#fff4d6"/>
      </svg>`,
      className: "bird-pin",
      iconSize: [20, 20],
      iconAnchor: [10, 10],
      popupAnchor: [0, -14],
    });

    if (entries.length > 0) {
      const group = L.featureGroup();
      for (const entry of entries) {
        const name = labels[entry.labelIndex]?.commonName ?? "Unknown bird";
        L.marker([entry.coords!.lat, entry.coords!.lng], { icon: birdIcon })
          .bindPopup(name)
          .addTo(group);
      }
      group.addTo(map);
      map.fitBounds(group.getBounds(), { padding: [40, 40] });
    } else {
      map.setView([20, 0], 2);
    }

    return () => map.remove();
  }
</script>

<div class="map" {@attach buildMap}></div>

{#if !get(historyStore).some((e) => e.coords != null)}
  <p class="empty">No sightings with GPS yet.</p>
{/if}

<style>
  .map {
    position: fixed;
    inset: 0;
    bottom: calc(var(--nav-height) + 3rem + env(safe-area-inset-bottom));
  }

  .empty {
    position: fixed;
    bottom: calc(var(--nav-height) + 3rem + env(safe-area-inset-bottom) + 1rem);
    left: 0;
    right: 0;
    text-align: center;
    color: var(--bg-near-black);
    font-weight: 700;
    font-size: 0.9rem;
    pointer-events: none;
  }

  :global(.leaflet-popup-content-wrapper) {
    background: var(--bg-paper);
    border: 2px solid var(--bg-near-black);
    border-radius: 8px;
    box-shadow: 0 3px 0 var(--bg-near-black);
    padding: 0;
  }

  :global(.leaflet-popup-content) {
    margin: 0.4rem 0.75rem;
    font-family: system-ui, sans-serif;
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--bg-near-black);
  }

  :global(.leaflet-popup-tip-container) {
    display: none;
  }

  :global(.bird-pin) {
    background: none;
    border: none;
  }
</style>