nidus-sync/ts/components/AddressSuggestion.vue
Eli Ribble 2d5dca3fb5
Add proxied autocomplete for Stadia
This allows me to make the format consistent and to cache the
intermediate results, which is useful for speed and testing
2026-04-05 21:57:30 +00:00

194 lines
4.4 KiB
Vue

<style scoped>
.address-input-wrapper {
position: relative;
}
.detail-label {
font-size: 0.8rem;
text-transform: uppercase;
color: #6c757d;
margin-bottom: 2px;
font-weight: 600;
}
.detail-value {
font-weight: 500;
}
.main-address {
font-weight: 500;
}
.place-info {
font-size: 0.85rem;
color: #6c757d;
margin-top: 2px;
}
.suggestions-container {
position: absolute;
width: 100%;
max-height: 300px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background: white;
}
.suggestion-item {
cursor: pointer;
padding: 10px 12px;
border-bottom: 1px solid #f0f0f0;
}
.suggestion-item:hover {
background-color: #f8f9fa;
}
</style>
<template>
<div class="address-input-wrapper">
<label for="addressInput" class="form-label">Enter address</label>
<input
autocomplete="off"
id="addressInput"
v-model="searchText"
class="form-control"
type="text"
:placeholder="placeholder"
maxlength="200"
@input="handleInput"
/>
<div v-if="suggestions.length > 0" class="suggestions-container list-group">
<div
v-for="(suggestion, index) in suggestions"
:key="suggestion.properties.gid || index"
class="suggestion-item list-group-item"
@click="selectSuggestion(suggestion)"
>
<div class="main-address">{{ suggestion.properties.name }}</div>
<div class="place-info">
{{ suggestion.properties.coarse_location }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { Address } from "@/type/stadia";
// Props
interface Props {
modelValue?: Address | null;
placeholder?: string;
apiKey?: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: null,
placeholder: "Enter address",
apiKey: "",
});
// Emits
const emit = defineEmits<{
"update:modelValue": [value: Address | null];
"address-selected": [address: Address];
}>();
// State
const searchText = ref("");
const suggestions = ref<Address[]>([]);
const debounceTimer = ref<ReturnType<typeof setTimeout> | null>(null);
// Watch for external changes to modelValue
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
searchText.value = formatAddressDisplay(newValue);
} else {
searchText.value = "";
}
},
{ immediate: true },
);
// Methods
function handleInput() {
const text = searchText.value.trim();
// Clear previous timer
if (debounceTimer.value) {
clearTimeout(debounceTimer.value);
}
// Clear suggestions if input is less than 3 characters
if (text.length < 3) {
suggestions.value = [];
return;
}
// Debounce API calls (wait 300ms after typing stops)
debounceTimer.value = setTimeout(async () => {
await fetchAddressSuggestions(text);
}, 300);
}
async function fetchAddressSuggestions(text: string) {
try {
const q = encodeURIComponent(text);
//const url = `https://api.stadiamaps.com/geocoding/v2/autocomplete?text=${encodeURIComponent(
//text,
//)}&focus.point.lat=35&focus.point.lon=-115`;
const url = `/api/geocode/suggestion?query=${q}`;
const response = await fetch(url);
const data = await response.json();
suggestions.value = data.features || [];
} catch (error) {
console.error("Error fetching geocoding suggestions:", error);
suggestions.value = [];
}
}
async function selectSuggestion(suggestion: Address) {
try {
// Fetch full details for the selected suggestion
const url = `https://api.stadiamaps.com/geocoding/v2/place_details?ids=${suggestion.properties.gid}`;
const response = await fetch(url);
const data = await response.json();
const fullAddress: Address = data.features[0];
// Update display text
searchText.value = formatAddressDisplay(fullAddress);
// Clear suggestions
suggestions.value = [];
// Emit the full address object
emit("update:modelValue", fullAddress);
emit("address-selected", fullAddress);
} catch (error) {
console.error("Error fetching place details:", error);
}
}
function formatAddressDisplay(address: Address): string {
const props = address.properties;
if (props.formatted_address_line) {
return props.formatted_address_line;
} else if (props.address_components) {
const num = props.address_components.number ?? "";
const street = props.address_components.street ?? "";
const location = props.coarse_location ?? "";
return `${num} ${street}, ${location}`.trim();
} else if (props.name != "") {
return `${props.name ?? ""}, ${props.coarse_location ?? ""}`.trim();
} else {
return "???";
}
}
</script>