synops/frontend/src/lib/components/LocationShare.svelte
vegard 6729a35435 Lokasjon-input: del posisjon i chat med kartvisning (oppgave 29.9)
Ny «Del posisjon»-knapp i ChatInput ved siden av tale/video-knappene.
Bruker Geolocation API for å hente brukerens posisjon, oppretter en
content-node med metadata.location { lat, lon, address }.

Reverse geocoding via Nominatim (best-effort) gir adresse i metadata.
Kartvisning i chat via Leaflet/OpenStreetMap viser posisjonen inline.

Komponenter:
- LocationShare.svelte: knapp + geolocation + geocoding + node-opprettelse
- LocationMap.svelte: Leaflet-kart med markør og adresse-popup
- Leaflet lastes via CDN (unpkg) i app.html
2026-03-18 22:36:08 +00:00

117 lines
3.7 KiB
Svelte

<script lang="ts">
import { createNode } from '$lib/api';
interface Props {
accessToken?: string;
/** Context node (e.g. communication node) for belongs_to edge */
contextId?: string;
disabled?: boolean;
onerror?: (message: string) => void;
}
let { accessToken, contextId, disabled = false, onerror }: Props = $props();
type LocationState = 'idle' | 'requesting' | 'geocoding' | 'saving';
let locState: LocationState = $state('idle');
async function shareLocation() {
if (!accessToken) {
onerror?.('Ikke innlogget — kan ikke dele posisjon');
return;
}
if (!navigator.geolocation) {
onerror?.('Geolokalisering støttes ikke i denne nettleseren');
return;
}
locState = 'requesting';
try {
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 60000,
});
});
const lat = Math.round(position.coords.latitude * 1e6) / 1e6;
const lon = Math.round(position.coords.longitude * 1e6) / 1e6;
// Try reverse geocoding via Nominatim (best-effort)
let address: string | undefined;
locState = 'geocoding';
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=16&addressdetails=0`,
{ headers: { 'User-Agent': 'Synops/1.0' } }
);
if (res.ok) {
const data = await res.json();
address = data.display_name;
}
} catch {
// Geocoding is optional — continue without address
}
locState = 'saving';
const title = address
? `Posisjon: ${address}`
: `Posisjon: ${lat}, ${lon}`;
await createNode(accessToken, {
node_kind: 'content',
title,
content: address || `${lat}, ${lon}`,
visibility: 'hidden',
context_id: contextId,
metadata: {
source: 'geolocation',
location: { lat, lon, ...(address ? { address } : {}) },
},
});
} catch (e) {
if (e instanceof GeolocationPositionError) {
const msgs: Record<number, string> = {
1: 'Posisjonstilgang avslått. Sjekk nettleserinnstillinger.',
2: 'Kunne ikke hente posisjon. Prøv igjen.',
3: 'Tidsavbrudd ved posisjonshenting. Prøv igjen.',
};
onerror?.(msgs[e.code] || 'Ukjent posisjonsfeil');
} else {
onerror?.(e instanceof Error ? e.message : 'Feil ved deling av posisjon');
}
} finally {
locState = 'idle';
}
}
</script>
{#if locState === 'idle'}
<button
onclick={shareLocation}
disabled={disabled || !accessToken}
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-gray-400 transition-colors hover:bg-green-50 hover:text-green-500 disabled:cursor-not-allowed disabled:opacity-40"
aria-label="Del posisjon"
title="Del posisjon"
>
<!-- Map pin icon -->
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
{:else}
<div class="flex items-center gap-1.5">
<svg class="h-4 w-4 animate-spin text-green-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<span class="text-xs text-green-600">
{#if locState === 'requesting'}Henter posisjon…{:else if locState === 'geocoding'}Finner adresse…{:else}Lagrer…{/if}
</span>
</div>
{/if}