feat: Add bulma search
This commit is contained in:
		@@ -18,7 +18,7 @@
 | 
			
		||||
 | 
			
		||||
    <div id="navbarBasicExample" class="navbar-menu">
 | 
			
		||||
        <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' %}
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -45,6 +45,7 @@ urlpatterns = [
 | 
			
		||||
 | 
			
		||||
    # ex: /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"),
 | 
			
		||||
    # ex: /map/
 | 
			
		||||
    path("map/", views.map, name="map"),
 | 
			
		||||
 
 | 
			
		||||
@@ -221,7 +221,11 @@ def search_important_locations(request, important_location_slug):
 | 
			
		||||
    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
 | 
			
		||||
    # searched. This will toggle the "subscribe" button
 | 
			
		||||
    searched = False
 | 
			
		||||
@@ -260,7 +264,7 @@ def search(request):
 | 
			
		||||
               "search_radius": search.max_distance,
 | 
			
		||||
               "zoom_level": zoom_level_for_radius(search.max_distance),
 | 
			
		||||
               "geocoding_api_url": settings.GEOCODING_API_URL, }
 | 
			
		||||
    return render(request, 'fellchensammlung/search.html', context=context)
 | 
			
		||||
    return render(request, templatename, context=context)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user