feat: Add bulma search

This commit is contained in:
moanos [he/him] 2025-05-09 17:14:34 +02:00
parent 13a0da6e46
commit 965e055ef1
7 changed files with 295 additions and 3 deletions

View File

@ -18,7 +18,7 @@
<div id="navbarBasicExample" class="navbar-menu"> <div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start"> <div class="navbar-start">
<a class="navbar-item" href="{% url 'search' %}"> <a class="navbar-item" href="{% url 'search-bulma' %}">
<i class="fas fa-search"></i> {% translate 'Suchen' %} <i class="fas fa-search"></i> {% translate 'Suchen' %}
</a> </a>

View File

@ -0,0 +1,89 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Suche" %}</title>{% endblock %}
{% block content %}
{% get_current_language as LANGUAGE_CODE_CURRENT %}
<div class="columns">
<div class="column">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
<form class="column" method="post">
{% csrf_token %}
<input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude">
<input type="hidden" id="place_id" name="place_id">
<!--- https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{{ search_form.as_p }}
<ul id="results"></ul>
<div class="container-edit-buttons">
<button class="btn" type="submit" value="search" name="search">
<i class="fas fa-search"></i> {% trans 'Suchen' %}
</button>
{% if searched %}
{% if subscribed_search %}
<button class="btn" type="submit" value="{{ subscribed_search.pk }}"
name="unsubscribe_to_search">
<i class="fas fa-bell-slash"></i> {% trans 'Suche nicht mehr abonnieren' %}
</button>
{% else %}
<button class="btn" type="submit" name="subscribe_to_search">
<i class="fas fa-bell"></i> {% trans 'Suche abonnieren' %}
</button>
{% endif %}
{% endif %}
</div>
{% if place_not_found %}
<p class="error">
{% trans 'Ort nicht gefunden' %}
</p>
{% endif %}
</form>
</div>
{% include "fellchensammlung/lists/bulma-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(`{{ geocoding_api_url }}/?q=${encodeURIComponent(query)}&limit=5&lang={{ LANGUAGE_CODE_CURRENT }}`);
const data = await response.json();
if (data && data.features) {
resultsList.innerHTML = ''; // Clear previous results
const locations = data.features.slice(0, 5); // Show only the first 5 results
locations.forEach(location => {
const listItem = document.createElement('li');
listItem.classList.add('result-item');
listItem.textContent = geojson_to_summary(location);
// Add event when user clicks on a result location
listItem.addEventListener('click', () => {
locationInput.value = geojson_to_searchable_string(location); // 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 %}

View File

@ -0,0 +1,10 @@
{% load i18n %}
<div class="container-cards">
{% if adoption_notices %}
{% for adoption_notice in adoption_notices %}
{% include "fellchensammlung/partials/bulma-partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>

View File

@ -0,0 +1,42 @@
{% load custom_tags %}
{% load i18n %}
<div class="card">
<div class="header-card-adoption-notice">
<h2 class="heading-card-adoption-notice">
<a href="{{ adoption_notice.get_absolute_url }}"> {{ adoption_notice.name }}</a>
</h2>
</div>
<div class="grid">
<div class="cell">
<!--- General Information --->
<div class="grid">
<div class="cell">
<p>
<b><i class="fa-solid fa-location-dot"></i></b>
{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}</p>
</div>
<div class="cell">
{% include "fellchensammlung/partials/bulma-sex-overview.html" %}
</div>
</div>
</div>
</div>
{% if adoption_notice.get_photo %}
<div class="adoption-notice-img img-small">
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
alt="{{ adoption_notice.get_photo.alt_text }}">
</div>
{% endif %}
</div>

View File

@ -0,0 +1,146 @@
{% load static %}
{% load custom_tags %}
{% load i18n %}
<!-- add MapLibre JavaScript and CSS -->
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.js"></script>
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
<!-- add Turf see https://maplibre.org/maplibre-gl-js/docs/examples/draw-a-radius/ -->
<script src="{% static 'fellchensammlung/js/turf.min.js' %}"></script>
<!-- add container for the map -->
<div id="map" style="width:100%;aspect-ratio:16/9"></div>
<!-- start map -->
<script>
{% if zoom_level %}
var zoom_level = {{ zoom_level }};
{% else %}
var zoom_level = 4;
{% endif %}
{% if map_center %}
var map_center = [{{ map_center.longitude | pointdecimal }}, {{ map_center.latitude | pointdecimal }}];
{% else %}
var map_center = [10.49, 50.68]; <!-- Point middle of Germany -->
zoom_level = 4; //Overwrite zoom level when no place is found
{% endif %}
let map = new maplibregl.Map({
container: 'map',
style: '{% static "fellchensammlung/map/styles/colorful/style.json" %}',
center: map_center,
zoom: zoom_level
}).addControl(new maplibregl.NavigationControl());
{% for adoption_notice in adoption_notices_map %}
{% if adoption_notice.location %}
// create the popup
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/bulma-partial-adoption-notice-minimal.html" %}`);
// create DOM element for the marker
const el_{{ forloop.counter }} = document.createElement('div');
el_{{ forloop.counter }}.id = 'marker_{{ forloop.counter }}';
el_{{ forloop.counter }}.classList.add('marker');
const location_popup_{{ forloop.counter }} = [{{ adoption_notice.location.longitude | pointdecimal }}, {{ adoption_notice.location.latitude | pointdecimal }}];
// create the marker
new maplibregl.Marker({element: el_{{ forloop.counter }}})
.setLngLat(location_popup_{{ forloop.counter }})
.setPopup(popup_{{ forloop.counter }}) // sets a popup on this marker
.addTo(map);
{% endif %}
{% endfor %}
{% for rescue_organization in rescue_organizations %}
{% if rescue_organization.location %}
// create the popup
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/partial-rescue-organization.html" %}`);
// create DOM element for the marker
const el_{{ forloop.counter }} = document.createElement('div');
el_{{ forloop.counter }}.id = 'marker_{{ forloop.counter }}';
el_{{ forloop.counter }}.classList.add('animal-shelter-marker', 'marker');
const location_popup_{{ forloop.counter }} = [{{ rescue_organization.location.longitude | pointdecimal }}, {{ rescue_organization.location.latitude | pointdecimal }}];
// create the marker
new maplibregl.Marker({element: el_{{ forloop.counter }}})
.setLngLat(location_popup_{{ forloop.counter }})
.setPopup(popup_{{ forloop.counter }}) // sets a popup on this marker
.addTo(map);
{% endif %}
{% endfor %}
map.on('load', async () => {
image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}');
map.addImage('pin', image.data);
{% for map_pin in map_pins %}
map.addSource('point_{{ forloop.counter }}', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [{{ map_pin.location.longitude | pointdecimal }}, {{ map_pin.location.latitude | pointdecimal }}]
}
}
]
}
});
map.addLayer({
'id': 'point_{{ forloop.counter }}',
'type': 'circle',
'source': 'point_{{ forloop.counter }}',
'paint': {
'circle-radius': 18,
'circle-color': '#ff878980'
}
});
{% endfor %}
});
{% if search_center %}
var search_center = [{{ search_center.longitude | pointdecimal }}, {{ search_center.latitude | pointdecimal }}];
map.on('load', () => {
const radius = {{ search_radius }}; // kilometer
const options = {
steps: 64,
units: 'kilometers'
};
const circle = turf.circle(search_center, radius, options);
// Add the circle as a GeoJSON source
map.addSource('location-radius', {
type: 'geojson',
data: circle
});
// Add a fill layer with some transparency
map.addLayer({
id: 'location-radius',
type: 'fill',
source: 'location-radius',
paint: {
'fill-color': 'rgba(140,207,255,0.3)',
'fill-opacity': 0.5
}
});
// Add a line layer to draw the circle outline
map.addLayer({
id: 'location-radius-outline',
type: 'line',
source: 'location-radius',
paint: {
'line-color': '#0094ff',
'line-width': 3
}
});
});
{% endif %}
</script>

View File

@ -45,6 +45,7 @@ urlpatterns = [
# ex: /search/ # ex: /search/
path("suchen/", views.search, name="search"), path("suchen/", views.search, name="search"),
path("bulma/suchen/", views.search_bulma, name="search-bulma"),
path("suchen/<slug:important_location_slug>", views.search_important_locations, name="search-by-location"), path("suchen/<slug:important_location_slug>", views.search_important_locations, name="search-by-location"),
# ex: /map/ # ex: /map/
path("map/", views.map, name="map"), path("map/", views.map, name="map"),

View File

@ -221,7 +221,11 @@ def search_important_locations(request, important_location_slug):
return render(request, 'fellchensammlung/search.html', context=context) return render(request, 'fellchensammlung/search.html', context=context)
def search(request): def search_bulma(request):
return search(request, "fellchensammlung/bulma-search.html")
def search(request, templatename="fellchensammlung/search.html"):
# A user just visiting the search site did not search, only upon completing the search form a user has really # A user just visiting the search site did not search, only upon completing the search form a user has really
# searched. This will toggle the "subscribe" button # searched. This will toggle the "subscribe" button
searched = False searched = False
@ -260,7 +264,7 @@ def search(request):
"search_radius": search.max_distance, "search_radius": search.max_distance,
"zoom_level": zoom_level_for_radius(search.max_distance), "zoom_level": zoom_level_for_radius(search.max_distance),
"geocoding_api_url": settings.GEOCODING_API_URL, } "geocoding_api_url": settings.GEOCODING_API_URL, }
return render(request, 'fellchensammlung/search.html', context=context) return render(request, templatename, context=context)
@login_required @login_required