initial commit

This commit is contained in:
terraputix
2026-01-07 22:24:02 +01:00
commit 2d31df88f0
315 changed files with 14484 additions and 0 deletions
@@ -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>