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
117 lines
3.7 KiB
Svelte
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}
|