initial commit
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||
|
||||
import { type GeoLocation } from '$lib/stores/settings';
|
||||
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
|
||||
export let label: string = 'Search Locations...';
|
||||
|
||||
interface ResultSet {
|
||||
results: GeoLocation[] | undefined;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let debounceTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
let searchQuery = '';
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(debounceTimeout);
|
||||
});
|
||||
|
||||
let scrollY: number | undefined;
|
||||
|
||||
const closeModal = () => {
|
||||
dialogOpen = false;
|
||||
if (scrollY) {
|
||||
window.scrollTo({ top: scrollY, behavior: 'instant' });
|
||||
}
|
||||
};
|
||||
|
||||
const selectLocation = (location: GeoLocation) => {
|
||||
searchQuery = '';
|
||||
closeModal();
|
||||
dispatch('location', location);
|
||||
};
|
||||
|
||||
$: results = (async () => {
|
||||
if (debounceTimeout) {
|
||||
clearTimeout(debounceTimeout);
|
||||
}
|
||||
if (searchQuery.length < 2) {
|
||||
return { results: [] };
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
debounceTimeout = setTimeout(resolve, 300);
|
||||
});
|
||||
|
||||
if (searchQuery.toLowerCase() == 'gps') {
|
||||
let position: GeolocationPosition = await new Promise((resolve, reject) =>
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, {})
|
||||
);
|
||||
const latitude = position.coords.latitude;
|
||||
const longitude = position.coords.longitude;
|
||||
return {
|
||||
results: [
|
||||
{
|
||||
id: 100000000 + Math.floor(latitude * 100 + longitude + 1000),
|
||||
name: `GPS ${latitude.toFixed(2)}°N ${longitude.toFixed(2)}°E`,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
elevation: position.coords.altitude ?? NaN,
|
||||
feature_code: '',
|
||||
country_code: undefined,
|
||||
admin1_id: undefined,
|
||||
admin3_id: undefined,
|
||||
admin4_id: undefined,
|
||||
timezone: '',
|
||||
population: undefined,
|
||||
postcodes: undefined,
|
||||
country_id: undefined,
|
||||
country: undefined,
|
||||
admin1: undefined,
|
||||
admin3: undefined,
|
||||
admin4: undefined
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Always set format=json to fetch data
|
||||
const url = 'https://geocoding-api.open-meteo.com/v1/search';
|
||||
const fetchUrl = `${url}?${new URLSearchParams({ name: searchQuery })}`;
|
||||
const result = await fetch(fetchUrl);
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(await result.text());
|
||||
}
|
||||
|
||||
return (await result.json()) as ResultSet;
|
||||
})();
|
||||
|
||||
let dialogOpen = false;
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={dialogOpen}>
|
||||
<Dialog.Trigger
|
||||
id="location_search"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
dialogOpen = !dialogOpen;
|
||||
}}
|
||||
class="flex h-12 w-full cursor-pointer items-center justify-center rounded-md border border-border px-5 pr-6 duration-200 hover:bg-accent"
|
||||
><svg
|
||||
class="lucide lucide-search mr-[5px]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
{label}</Dialog.Trigger
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="bg-black/5" />
|
||||
|
||||
<Dialog.Content
|
||||
class="top-[10%] flex max-h-[calc(100vh-10%)] min-h-[400px] translate-y-0 flex-col overflow-y-auto border-border sm:max-w-[600px]"
|
||||
>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Search Locations</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div>
|
||||
<div class="flex gap-3">
|
||||
<Input
|
||||
type="search"
|
||||
id="location_search_input"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
aria-label="Search Location"
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
<Button
|
||||
id="location_search_gps"
|
||||
variant="outline"
|
||||
title="Detect Location via GPS"
|
||||
onclick={() => (searchQuery = 'GPS')}
|
||||
><svg
|
||||
class="lucide lucide-mouse-pointer-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"
|
||||
/>
|
||||
</svg></Button
|
||||
>
|
||||
</div>
|
||||
{#await results}
|
||||
<div class="mt-4 flex h-full items-center justify-center">
|
||||
<Alert.Root class="my-auto w-[unset] border-border">
|
||||
<Alert.Description>Loading...</Alert.Description>
|
||||
</Alert.Root>
|
||||
</div>
|
||||
{:then results}
|
||||
{#if results.results && results.results.length === 0}
|
||||
{#if searchQuery.length < 2}
|
||||
<Alert.Root class="my-auto mt-4 w-[unset] border-border">
|
||||
<Alert.Description>Start typing to search for locations</Alert.Description>
|
||||
</Alert.Root>
|
||||
{:else}
|
||||
<Alert.Root class="my-auto !mt-4 w-[unset] border-border">
|
||||
<Alert.Description>No locations found</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
{:else if !results.results}
|
||||
<Alert.Root class="my-auto !mt-4 w-[unset] border-border">
|
||||
<Alert.Description>No locations found</Alert.Description>
|
||||
</Alert.Root>
|
||||
{:else}
|
||||
<div class="list-group mt-4">
|
||||
<div id="location_search_results" class="rounded-lg border border-border">
|
||||
{#each results.results || [] as location, i (i)}
|
||||
<Button
|
||||
variant="outline"
|
||||
class="location-search-result flex h-[unset] w-full justify-between gap-0 rounded-none py-2 pr-1 pl-3 not-last:border-b md:pr-2 {i ===
|
||||
0
|
||||
? 'rounded-t-md'
|
||||
: ''} {results.results && i === results.results.length - 1
|
||||
? 'rounded-b-md'
|
||||
: ''}"
|
||||
onclick={() => selectLocation(location)}
|
||||
>
|
||||
<div class="pointer-events-none flex flex-col gap-1 truncate">
|
||||
<div class="flex items-center gap-2 truncate text-lg">
|
||||
<img
|
||||
height="24"
|
||||
width="24"
|
||||
src="/images/country-flags/{(
|
||||
location.country_code || 'united_nations'
|
||||
).toLowerCase()}.svg"
|
||||
title={location.country}
|
||||
alt={location.country_code}
|
||||
/>
|
||||
{location.name}
|
||||
</div>
|
||||
|
||||
<div class="truncate text-left text-sm text-muted-foreground">
|
||||
{location.admin1 || ''} ({location.latitude.toFixed(2)}°N {location.longitude.toFixed(
|
||||
2
|
||||
)}°E{#if location.elevation}{' ' +
|
||||
location.elevation.toFixed(0) +
|
||||
'm asl'}{/if})
|
||||
</div>
|
||||
</div>
|
||||
<div class="-mr-1 flex justify-self-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="px-2 duration-200 hover:brightness-[140%] md:px-3"
|
||||
href="https://www.openstreetmap.org/#map=13/{location.latitude}/{location.longitude}"
|
||||
target="_blank"
|
||||
title="Show on map"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-map"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"
|
||||
/>
|
||||
<path d="M15 5.764v15" />
|
||||
<path d="M9 3.236v15" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<Alert.Root variant="destructive" class="my-auto mt-4 w-[unset]">
|
||||
<Alert.Description>{error.message}</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/await}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
Reference in New Issue
Block a user