2026-09-24 19:58:16 +00:00
|
|
|
<script lang="ts">
|
|
|
|
|
import "leaflet/dist/leaflet.css"
|
|
|
|
|
|
|
|
|
|
import {mount, onMount, unmount} from "svelte"
|
2026-09-25 22:44:16 +00:00
|
|
|
import type {Map as LeafletMap, Marker, TileLayer} from "leaflet"
|
2026-09-24 19:58:16 +00:00
|
|
|
import type {Maybe} from "@welshman/lib"
|
|
|
|
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
|
|
|
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
|
|
|
|
import {pushModal} from "@app/modal"
|
|
|
|
|
import {activeTheme} from "@app/theme"
|
|
|
|
|
import type {MemberPin} from "@app/social"
|
|
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
|
url: string
|
|
|
|
|
pins: MemberPin[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {url, pins}: Props = $props()
|
|
|
|
|
|
2026-09-25 22:44:16 +00:00
|
|
|
const canvas = $derived($activeTheme === "dark" ? "Dark" : "Light")
|
|
|
|
|
|
|
|
|
|
const tileUrls = $derived(
|
|
|
|
|
["Base", "Reference"].map(
|
|
|
|
|
layer =>
|
|
|
|
|
`https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_${canvas}_Gray_${layer}/MapServer/tile/{z}/{y}/{x}`,
|
|
|
|
|
),
|
2026-09-24 19:58:16 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const openProfile = (pubkey: string) => pushModal(ProfileDetail, {pubkey, url})
|
|
|
|
|
|
|
|
|
|
let element: HTMLElement
|
|
|
|
|
let leaflet: Maybe<typeof import("leaflet")> = $state()
|
|
|
|
|
let map: Maybe<LeafletMap> = $state()
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (leaflet) {
|
|
|
|
|
const created = leaflet
|
|
|
|
|
.map(element, {worldCopyJump: true, zoomControl: false})
|
|
|
|
|
.setView([25, 0], 2)
|
|
|
|
|
|
|
|
|
|
leaflet.control.zoom({position: "bottomright"}).addTo(created)
|
|
|
|
|
map = created
|
|
|
|
|
|
|
|
|
|
return () => created.remove()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
2026-09-25 22:44:16 +00:00
|
|
|
const tiles: TileLayer[] = []
|
|
|
|
|
|
2026-09-24 19:58:16 +00:00
|
|
|
if (leaflet && map) {
|
2026-09-25 22:44:16 +00:00
|
|
|
for (const tileUrl of tileUrls) {
|
|
|
|
|
tiles.push(
|
|
|
|
|
leaflet
|
|
|
|
|
.tileLayer(tileUrl, {
|
|
|
|
|
maxZoom: 12,
|
|
|
|
|
attribution:
|
|
|
|
|
'Esri, HERE, Garmin, <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
|
|
|
})
|
|
|
|
|
.addTo(map),
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-09-24 19:58:16 +00:00
|
|
|
}
|
2026-09-25 22:44:16 +00:00
|
|
|
|
|
|
|
|
return () => tiles.forEach(tile => tile.remove())
|
2026-09-24 19:58:16 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
const markers: Marker[] = []
|
|
|
|
|
const avatars: Record<string, unknown>[] = []
|
|
|
|
|
|
|
|
|
|
if (leaflet && map) {
|
|
|
|
|
for (const pin of pins) {
|
|
|
|
|
const target = document.createElement("div")
|
|
|
|
|
|
|
|
|
|
avatars.push(
|
|
|
|
|
mount(ProfileCircle, {
|
|
|
|
|
target,
|
|
|
|
|
props: {
|
|
|
|
|
pubkey: pin.pubkey,
|
|
|
|
|
url,
|
|
|
|
|
size: 10,
|
|
|
|
|
class: "bg-surface ring-2 ring-surface shadow-lg",
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
markers.push(
|
|
|
|
|
leaflet
|
|
|
|
|
.marker([pin.latitude, pin.longitude], {
|
|
|
|
|
title: pin.display,
|
|
|
|
|
// An empty className replaces the white box leaflet gives a div icon.
|
|
|
|
|
icon: leaflet.divIcon({html: target, className: "", iconSize: [40, 40]}),
|
|
|
|
|
})
|
|
|
|
|
.on("click", () => openProfile(pin.pubkey))
|
|
|
|
|
.addTo(map),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (markers.length > 0) {
|
|
|
|
|
map.fitBounds(leaflet.featureGroup(markers).getBounds(), {maxZoom: 8, padding: [40, 40]})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
markers.forEach(marker => marker.remove())
|
|
|
|
|
avatars.forEach(avatar => unmount(avatar))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onMount(async () => {
|
|
|
|
|
leaflet = await import("leaflet")
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<!-- Leaflet's panes sit at z-index 400, which covers a modal without a stacking context here. -->
|
|
|
|
|
<div bind:this={element} class="isolate min-h-96 flex-1 overflow-hidden rounded-2xl"></div>
|