feat: add search-as-you-type functionality

This commit is contained in:
moanos [he/him] 2025-01-08 09:18:48 +01:00
parent 4576ac68e0
commit cc97fe32aa
4 changed files with 70 additions and 2 deletions

View File

@ -196,4 +196,4 @@ class AdoptionNoticeSearchForm(forms.Form):
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False, sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
initial=SexChoicesWithAll.ALL) initial=SexChoicesWithAll.ALL)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius")) max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius"))
location_string = forms.CharField(max_length=20, label=_("Stadt"), required=False) location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)

View File

@ -909,6 +909,25 @@ div.announcement {
width: 49%; width: 49%;
} }
#results {
margin-top: 10px;
list-style-type: none;
padding: 0;
}
.result-item {
padding: 8px;
margin: 4px 0;
background-color: #ddd1a5;
cursor: pointer;
border-radius: 8px;
}
.result-item:hover {
background-color: #ede1b5;
}
/************************/ /************************/
/* GENERAL HIGHLIGHTING */ /* GENERAL HIGHLIGHTING */
/************************/ /************************/
@ -939,7 +958,8 @@ div.announcement {
} }
.animal-shelter-marker { .animal-shelter-marker {
background-image: url('../img/animal_shelter.png'); !important; background-image: url('../img/animal_shelter.png');
!important;
} }
.maplibregl-popup { .maplibregl-popup {

View File

@ -9,7 +9,9 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="longitude" maxlength="200" id="longitude"> <input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude"> <input type="hidden" name="latitude" maxlength="200" id="latitude">
<input type="hidden" id="place_id" name="place_id">
{{ search_form.as_p }} {{ search_form.as_p }}
<ul id="results"></ul>
<div class="container-edit-buttons"> <div class="container-edit-buttons">
<button class="btn" type="submit" value="search" name="search"> <button class="btn" type="submit" value="search" name="search">
<i class="fas fa-search"></i> {% trans 'Suchen' %} <i class="fas fa-search"></i> {% trans 'Suchen' %}
@ -40,4 +42,47 @@
</div> </div>
</div> </div>
{% include "fellchensammlung/lists/list-adoption-notices.html" %} {% include "fellchensammlung/lists/list-adoption-notices.html" %}
<script>
const locationInput = document.getElementById('id_location_string');
const resultsList = document.getElementById('results');
const placeIdInput = document.getElementById('place_id');
locationInput.addEventListener('input', async function () {
const query = locationInput.value.trim();
if (query.length < 3) {
resultsList.innerHTML = ''; // Don't search for or show results if input is less than 3 characters
return;
}
try {
const response = await fetch(`https://photon.hyteck.de/api/?q=${encodeURIComponent(query)}&limit=5`);
const data = await response.json();
if (data && data.features) {
const locations = data.features.slice(0, 5); // Show only the first 5 results
resultsList.innerHTML = ''; // Clear previous results
locations.forEach(location => {
const listItem = document.createElement('li');
listItem.classList.add('result-item');
listItem.textContent = location.properties.name;
// Add event when user clicks on a result location
listItem.addEventListener('click', () => {
locationInput.value = location.properties.name; // Set input field to selected location
resultsList.innerHTML = ''; // Clear the results after selecting a location
});
resultsList.appendChild(listItem);
});
}
} catch (error) {
console.error('Error fetching location data:', error);
resultsList.innerHTML = '<li class="result-item">Error fetching data. Please try again.</li>';
}
});
</script>
{% endblock %} {% endblock %}

View File

@ -39,6 +39,8 @@ class Search:
self.location = None # Can either be Location (DjangoModel) or LocationProxy self.location = None # Can either be Location (DjangoModel) or LocationProxy
self.place_not_found = False # Indicates that a location was given but could not be geocoded self.place_not_found = False # Indicates that a location was given but could not be geocoded
self.search_form = None self.search_form = None
# Either place_id or location string must be set for area search
self.location_string = None
if request: if request:
self.search_from_request(request) self.search_from_request(request)
@ -106,6 +108,7 @@ class Search:
self.search_form = AdoptionNoticeSearchForm(request.POST) self.search_form = AdoptionNoticeSearchForm(request.POST)
self.search_form.is_valid() self.search_form.is_valid()
self.sex = self.search_form.cleaned_data["sex"] self.sex = self.search_form.cleaned_data["sex"]
if self.search_form.cleaned_data["location_string"] != "" and self.search_form.cleaned_data[ if self.search_form.cleaned_data["location_string"] != "" and self.search_form.cleaned_data[
"max_distance"] != "": "max_distance"] != "":
self.area_search = True self.area_search = True