diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json
index e256bdd4..3339e5ab 100644
--- a/public/locales/de/translation.json
+++ b/public/locales/de/translation.json
@@ -12,7 +12,10 @@
"close": "Schließen",
"delete": "Löschen",
"searchField": {
- "placeholder": "Geben Sie Ihre Adresse oder Koordinaten ein - z.B. Lange Point 20, Freising"
+ "placeholder": "Geben Sie Ihre Adresse oder Koordinaten ein - z.B. Lange Point 20, Freising",
+ "clear": "Eingabe löschen",
+ "noResults": "Keine Vorschläge gefunden",
+ "enterHouseNumber": "Bitte geben Sie eine Hausnummer ein"
},
"about": {
"title": "Über openpv.de",
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index fdc9fcf3..711f9c05 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -12,7 +12,10 @@
"close": "Close",
"delete": "Delete",
"searchField": {
- "placeholder": "Enter your address or coordinates - e.g. Lange Point 20, Freising"
+ "placeholder": "Enter your address or coordinates - e.g. Lange Point 20, Freising",
+ "clear": "Clear input",
+ "noResults": "No suggestions found",
+ "enterHouseNumber": "Please enter a house number"
},
"about": {
"title": "About the Project",
diff --git a/src/features/map/components/SearchField.jsx b/src/features/map/components/SearchField.jsx
index b2040085..2f0132a6 100644
--- a/src/features/map/components/SearchField.jsx
+++ b/src/features/map/components/SearchField.jsx
@@ -1,5 +1,13 @@
-import { Button, Input, List } from '@chakra-ui/react'
+import {
+ Button,
+ IconButton,
+ Input,
+ InputGroup,
+ List,
+ Spinner,
+} from '@chakra-ui/react'
import React, { useEffect, useRef, useState } from 'react'
+import { LuSearch, LuX } from 'react-icons/lu'
import { useTranslation } from 'react-i18next'
import { processAddress } from '@/features/simulation/core/location'
@@ -7,14 +15,17 @@ export default function SearchField({ callback }) {
const [inputValue, setInputValue] = useState('')
const [suggestions, setSuggestions] = useState([])
const [suggestionsVisible, setSuggestionsVisible] = useState(false)
- // isSelectedAdress is used so that if an adress is already selected,
+ // isSelectedAddress is used so that if an adress is already selected,
// the autocomplete does stop to run
- const [isSelectedAdress, setIsSelectedAdress] = useState(false)
+ const [isSelectedAddress, setIsSelectedAddress] = useState(false)
+ const [isFetching, setIsFetching] = useState(false)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [submitError, setSubmitError] = useState(false)
+ const [needsHouseNumber, setNeedsHouseNumber] = useState(false)
const suggestionsRef = useRef([])
const inputRef = useRef()
const formRef = useRef()
const [focusedIndex, setFocusedIndex] = useState(-1)
- window.searchFieldInput = inputValue
const { t } = useTranslation()
useEffect(() => {
@@ -31,19 +42,20 @@ export default function SearchField({ callback }) {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('touchstart', handleClickOutside)
}
- })
+ }, [])
useEffect(() => {
const fetchSuggestions = async () => {
if (inputValue.length < 3) {
// If the input is deleted or replaced with one
// charakter, the autocomplete should start again
- setIsSelectedAdress(false)
+ setIsSelectedAddress(false)
}
- if (isSelectedAdress) {
+ if (isSelectedAddress) {
return
}
if (inputValue.length > 2) {
+ setIsFetching(true)
try {
const inputValueParts = inputValue.split(' ')
let streetAddressNumber = null
@@ -54,7 +66,7 @@ export default function SearchField({ callback }) {
//drop last character (ie the comma)
inputPart = inputPart.slice(0, -1)
}
- if (inputPart.length == 5) {
+ if (inputPart.length === 5) {
// continue if it has the length of a zip code
continue
}
@@ -71,50 +83,95 @@ export default function SearchField({ callback }) {
)}&bbox=5.98865807458,47.3024876979,15.0169958839,54.983104153&limit=5&lang=de&layer=street`,
)
const data = await response.json()
- console.log('data', data)
-
- setSuggestions(
- data.features.map((feature) => {
- let suggestion = feature.properties.name
- if (streetAddressNumber) {
- suggestion += ' ' + streetAddressNumber
- }
- suggestion +=
- ', ' +
- feature.properties.postcode +
- ' ' +
- feature.properties.city
- return suggestion
- }),
- )
+
+ const fetchedSuggestions = data.features.map((feature) => {
+ const streetName = feature.properties.name
+ const postcode = feature.properties.postcode
+ const city = feature.properties.city
+ let display = streetName
+ if (streetAddressNumber) {
+ display += ' ' + streetAddressNumber
+ }
+ display += ', ' + postcode + ' ' + city
+ return {
+ display,
+ streetName,
+ postcode,
+ city,
+ houseNumber: streetAddressNumber,
+ }
+ })
+ setSuggestions(fetchedSuggestions)
+ setSuggestionsVisible(true)
} catch (error) {
console.error('Error fetching suggestions:', error)
+ } finally {
+ setIsFetching(false)
}
} else {
setSuggestions([])
+ setSuggestionsVisible(false)
}
- setSuggestionsVisible(suggestions.length > 0)
}
const debounceTimer = setTimeout(fetchSuggestions, 200)
return () => clearTimeout(debounceTimer)
- }, [inputValue, isSelectedAdress])
+ }, [inputValue, isSelectedAddress])
+
+ const submitAddress = async (address) => {
+ setIsSubmitting(true)
+ setSubmitError(false)
+ try {
+ const locations = await processAddress(address)
+ if (locations.length === 0) {
+ setSubmitError(true)
+ } else {
+ callback(locations)
+ }
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
- const handleSubmit = async (event) => {
+ const handleSubmit = (event) => {
event.preventDefault()
- const locations = await processAddress(inputValue)
- console.warn(locations)
- callback(locations)
+ submitAddress(inputValue)
}
const handleSuggestionClick = (suggestion) => {
- setInputValue(suggestion)
- processAddress(suggestion).then((locations) => {
- console.warn(locations)
- callback(locations)
- })
+ const { streetName, postcode, city, houseNumber } = suggestion
+ if (houseNumber) {
+ // House number already known — fill completely and submit
+ const fullAddress = `${streetName} ${houseNumber}, ${postcode} ${city}`
+ setInputValue(fullAddress)
+ setSuggestions([])
+ setSuggestionsVisible(false)
+ setIsSelectedAddress(true)
+ setNeedsHouseNumber(false)
+ submitAddress(fullAddress)
+ } else {
+ // No house number yet — ask user to type it
+ const newValue = `${streetName} , ${postcode} ${city}`
+ const cursorPos = streetName.length + 1
+ setInputValue(newValue)
+ setSuggestions([])
+ setSuggestionsVisible(false)
+ setIsSelectedAddress(true)
+ setNeedsHouseNumber(true)
+ setTimeout(() => {
+ inputRef.current?.focus()
+ inputRef.current?.setSelectionRange(cursorPos, cursorPos)
+ }, 0)
+ }
+ }
+
+ const handleClear = () => {
+ setInputValue('')
setSuggestions([])
- setIsSelectedAdress(true)
+ setSuggestionsVisible(false)
+ setIsSelectedAddress(false)
+ setNeedsHouseNumber(false)
+ inputRef.current?.focus()
}
const handleKeyDown = (event) => {
@@ -140,6 +197,20 @@ export default function SearchField({ callback }) {
}
}, [focusedIndex])
+ const startElement = isFetching ?