-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathStationSearch.svelte
146 lines (126 loc) · 3.57 KB
/
StationSearch.svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<script lang="ts">
import type { Station } from "$models/station";
import { onMount } from "svelte";
import { env } from "$env/dynamic/public";
import Search from "lucide-svelte/icons/search";
let { station = $bindable(undefined) }: { station?: Station } = $props();
let open = $state<boolean>(false);
let selectedIndex = $state<number>(-1);
let stations: Station[] = $state([]);
let inputElement: HTMLInputElement;
const clickOutside = (event: MouseEvent) => {
if (inputElement && inputElement.contains(event.target as Node)) return;
open = false;
selectedIndex = -1;
};
const searchStations = async (query: string) => {
const searchParams = new URLSearchParams({ query }).toString();
const response = await fetch(`${env.PUBLIC_BACKEND_BASE_URL}/api/v1/stations?${searchParams}`, { method: "GET" });
if (!response.ok) return;
const jsonData = await response.json();
if (!Array.isArray(jsonData)) return;
stations = jsonData as Station[];
open = stations.length > 0;
};
const selectStation = (index: number) => {
station = stations[index];
inputElement.value = station?.name ?? "";
open = false;
selectedIndex = -1;
};
let debounce: number;
const handleInput = () => {
if (!inputElement) return;
const value = inputElement.value;
if (value.length === 0) {
station = undefined;
return;
}
if (!station || value !== station?.name) {
clearTimeout(debounce);
debounce = setTimeout(() => {
if (value) {
searchStations(value);
} else {
open = false;
}
}, 500);
}
};
const handleKeyInput = (event: KeyboardEvent) => {
if (!open || inputElement.value.length === 0) return;
switch (event.key) {
case "ArrowDown":
case "Tab":
event.preventDefault();
selectedIndex = selectedIndex === stations.length - 1 ? 0 : selectedIndex + 1;
break;
case "ArrowUp":
event.preventDefault();
selectedIndex = selectedIndex === 0 ? stations.length - 1 : selectedIndex - 1;
break;
case "Enter":
if (selectedIndex < 0 || selectedIndex > stations.length) return;
selectStation(selectedIndex);
break;
case "Escape":
open = false;
break;
default:
break;
}
};
onMount(() => {
document.addEventListener("click", clickOutside);
return () => document.removeEventListener("click", clickOutside);
});
</script>
<div class="text-text placeholder:text-text relative flex w-full flex-col">
<div
class="bg-primary-dark focus-within:ring-accent flex flex-row items-center gap-x-1 rounded-2xl px-2 font-medium focus-within:ring-2 md:text-2xl"
>
<Search size={44} />
<input
bind:this={inputElement}
type="text"
class="bg-primary-dark w-full border-none p-2 outline-hidden"
placeholder="Search for a station"
onclick={() => (open = true)}
oninput={handleInput}
onkeydown={handleKeyInput}
/>
</div>
{#if open && stations.length > 0}
<div
class="border-text bg-primary-dark absolute top-full left-0 z-50 mt-1 flex h-fit max-w-96 flex-col rounded-lg border p-2"
>
{#each stations as station, index (station)}
<button
tabindex="0"
onclick={() => selectStation(index)}
onfocus={() => (selectedIndex = index)}
class:bg-secondary={selectedIndex === index}
class="w-full cursor-pointer rounded-md p-1 text-left"
>
{station?.name}
</button>
{/each}
</div>
{/if}
</div>
<style lang="postcss">
::placeholder {
color: var(--text);
opacity: 0.75;
}
::-webkit-input-placeholder {
color: var(--text);
}
::-moz-placeholder {
color: var(--text);
opacity: 0.75;
}
:-ms-input-placeholder {
color: var(--text);
}
</style>