mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-23 14:59:36 +02:00
feat: add OverpassViewSet and implement osmTagToEmoji function; update requirements and aria-labels
This commit is contained in:
parent
0588555707
commit
62efa2478e
6 changed files with 512 additions and 5 deletions
|
@ -1,6 +1,6 @@
|
|||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet, IcsCalendarGeneratorViewSet
|
||||
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet, IcsCalendarGeneratorViewSet, OverpassViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'adventures', AdventureViewSet, basename='adventures')
|
||||
|
@ -15,6 +15,7 @@ router.register(r'images', AdventureImageViewSet, basename='images')
|
|||
router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode')
|
||||
router.register(r'categories', CategoryViewSet, basename='categories')
|
||||
router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-calendar')
|
||||
router.register(r'overpass', OverpassViewSet, basename='overpass')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -21,6 +21,7 @@ from icalendar import Calendar, Event, vText, vCalAddress
|
|||
from django.http import HttpResponse
|
||||
from datetime import datetime
|
||||
from django.db.models import Max
|
||||
from overpy import Overpass
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -1329,3 +1330,184 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
|
|||
response = HttpResponse(cal.to_ical(), content_type='text/calendar')
|
||||
response['Content-Disposition'] = 'attachment; filename=adventures.ics'
|
||||
return response
|
||||
|
||||
class OverpassViewSet(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
BASE_URL = "https://overpass-api.de/api/interpreter"
|
||||
HEADERS = {'User-Agent': 'AdventureLog Server'}
|
||||
|
||||
def make_overpass_query(self, query):
|
||||
"""
|
||||
Sends a query to the Overpass API and returns the response data.
|
||||
Args:
|
||||
query (str): The Overpass QL query string.
|
||||
Returns:
|
||||
dict: Parsed JSON response from the Overpass API.
|
||||
Raises:
|
||||
Response: DRF Response object with an error message in case of failure.
|
||||
"""
|
||||
url = f"{self.BASE_URL}?data={query}"
|
||||
try:
|
||||
response = requests.get(url, headers=self.HEADERS)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
return Response({"error": str(e)}, status=500)
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
return Response({"error": "Invalid response from Overpass API"}, status=400)
|
||||
|
||||
def parse_overpass_response(self, data, request):
|
||||
"""
|
||||
Parses the JSON response from the Overpass API and extracts relevant data,
|
||||
turning it into an adventure-structured object.
|
||||
|
||||
Args:
|
||||
response (dict): The JSON response from the Overpass API.
|
||||
|
||||
Returns:
|
||||
list: A list of adventure objects with structured data.
|
||||
"""
|
||||
# Extract elements (nodes/ways/relations) from the response
|
||||
nodes = data.get('elements', [])
|
||||
adventures = []
|
||||
|
||||
# include all entries, even the ones that do not have lat long
|
||||
all = request.query_params.get('all', False)
|
||||
|
||||
for node in nodes:
|
||||
# Ensure we are working with a "node" type (can also handle "way" or "relation" if needed)
|
||||
if node.get('type') not in ['node', 'way', 'relation']:
|
||||
continue
|
||||
|
||||
# Extract tags and general data
|
||||
tags = node.get('tags', {})
|
||||
adventure = {
|
||||
"id": node.get('id'), # Include the unique OSM ID
|
||||
"type": node.get('type'), # Type of element (node, way, relation)
|
||||
"name": tags.get('name', tags.get('official_name', '')), # Fallback to 'official_name'
|
||||
"description": tags.get('description', None), # Additional descriptive information
|
||||
"latitude": node.get('lat', None), # Use None for consistency with missing values
|
||||
"longitude": node.get('lon', None),
|
||||
"address": {
|
||||
"city": tags.get('addr:city', None),
|
||||
"housenumber": tags.get('addr:housenumber', None),
|
||||
"postcode": tags.get('addr:postcode', None),
|
||||
"state": tags.get('addr:state', None),
|
||||
"street": tags.get('addr:street', None),
|
||||
"country": tags.get('addr:country', None), # Add 'country' if available
|
||||
"suburb": tags.get('addr:suburb', None), # Add 'suburb' for more granularity
|
||||
},
|
||||
"feature_id": tags.get('gnis:feature_id', None),
|
||||
"tag": next((tags.get(key, None) for key in ['leisure', 'tourism', 'natural', 'historic', 'amenity'] if key in tags), None),
|
||||
"contact": {
|
||||
"phone": tags.get('phone', None),
|
||||
"email": tags.get('contact:email', None),
|
||||
"website": tags.get('website', None),
|
||||
"facebook": tags.get('contact:facebook', None), # Social media links
|
||||
"twitter": tags.get('contact:twitter', None),
|
||||
},
|
||||
# "tags": tags, # Include all raw tags for future use
|
||||
}
|
||||
|
||||
# Filter out adventures with no meaningful data
|
||||
if any([
|
||||
adventure["name"],
|
||||
adventure["latitude"],
|
||||
adventure["longitude"],
|
||||
]
|
||||
) or all:
|
||||
adventures.append(adventure)
|
||||
|
||||
return adventures
|
||||
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def query(self, request):
|
||||
"""
|
||||
Radius-based search for tourism-related locations around given coordinates.
|
||||
"""
|
||||
lat = request.query_params.get('lat')
|
||||
lon = request.query_params.get('lon')
|
||||
radius = request.query_params.get('radius', '1000') # Default radius: 1000 meters
|
||||
|
||||
valid_categories = ['lodging', 'food', 'tourism']
|
||||
category = request.query_params.get('category', 'all')
|
||||
if category not in valid_categories:
|
||||
return Response({"error": f"Invalid category. Valid categories: {', '.join(valid_categories)}"}, status=400)
|
||||
|
||||
if category == 'tourism':
|
||||
query = f"""
|
||||
[out:json];
|
||||
(
|
||||
node(around:{radius},{lat},{lon})["tourism"];
|
||||
node(around:{radius},{lat},{lon})["leisure"];
|
||||
node(around:{radius},{lat},{lon})["historic"];
|
||||
node(around:{radius},{lat},{lon})["sport"];
|
||||
node(around:{radius},{lat},{lon})["natural"];
|
||||
node(around:{radius},{lat},{lon})["attraction"];
|
||||
node(around:{radius},{lat},{lon})["museum"];
|
||||
node(around:{radius},{lat},{lon})["zoo"];
|
||||
node(around:{radius},{lat},{lon})["aquarium"];
|
||||
);
|
||||
out;
|
||||
"""
|
||||
if category == 'lodging':
|
||||
query = f"""
|
||||
[out:json];
|
||||
(
|
||||
node(around:{radius},{lat},{lon})["tourism"="hotel"];
|
||||
node(around:{radius},{lat},{lon})["tourism"="motel"];
|
||||
node(around:{radius},{lat},{lon})["tourism"="guest_house"];
|
||||
node(around:{radius},{lat},{lon})["tourism"="hostel"];
|
||||
node(around:{radius},{lat},{lon})["tourism"="camp_site"];
|
||||
node(around:{radius},{lat},{lon})["tourism"="caravan_site"];
|
||||
node(around:{radius},{lat},{lon})["tourism"="chalet"];
|
||||
node(around:{radius},{lat},{lon})["tourism"="alpine_hut"];
|
||||
node(around:{radius},{lat},{lon})["tourism"="apartment"];
|
||||
);
|
||||
out;
|
||||
"""
|
||||
if category == 'food':
|
||||
query = f"""
|
||||
[out:json];
|
||||
(
|
||||
node(around:{radius},{lat},{lon})["amenity"="restaurant"];
|
||||
node(around:{radius},{lat},{lon})["amenity"="cafe"];
|
||||
node(around:{radius},{lat},{lon})["amenity"="fast_food"];
|
||||
node(around:{radius},{lat},{lon})["amenity"="pub"];
|
||||
node(around:{radius},{lat},{lon})["amenity"="bar"];
|
||||
node(around:{radius},{lat},{lon})["amenity"="food_court"];
|
||||
node(around:{radius},{lat},{lon})["amenity"="ice_cream"];
|
||||
node(around:{radius},{lat},{lon})["amenity"="bakery"];
|
||||
node(around:{radius},{lat},{lon})["amenity"="confectionery"];
|
||||
);
|
||||
out;
|
||||
"""
|
||||
|
||||
# Validate required parameters
|
||||
if not lat or not lon:
|
||||
return Response(
|
||||
{"error": "Latitude and longitude parameters are required."}, status=400
|
||||
)
|
||||
|
||||
data = self.make_overpass_query(query)
|
||||
adventures = self.parse_overpass_response(data, request)
|
||||
return Response(adventures)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='search')
|
||||
def search(self, request):
|
||||
"""
|
||||
Name-based search for nodes with the specified name.
|
||||
"""
|
||||
name = request.query_params.get('name')
|
||||
|
||||
# Validate required parameter
|
||||
if not name:
|
||||
return Response({"error": "Name parameter is required."}, status=400)
|
||||
|
||||
# Construct Overpass API query
|
||||
query = f'[out:json];node["name"~"{name}",i];out;'
|
||||
data = self.make_overpass_query(query)
|
||||
|
||||
adventures = self.parse_overpass_response(data, request)
|
||||
return Response(adventures)
|
||||
|
|
|
@ -20,3 +20,4 @@ django-ical==1.9.2
|
|||
icalendar==6.1.0
|
||||
ijson==3.3.0
|
||||
tqdm==4.67.1
|
||||
overpy==0.7
|
|
@ -347,3 +347,120 @@ export let themes = [
|
|||
{ name: 'aestheticDark', label: 'Aesthetic Dark' },
|
||||
{ name: 'northernLights', label: 'Northern Lights' }
|
||||
];
|
||||
|
||||
export function osmTagToEmoji(tag: string) {
|
||||
switch (tag) {
|
||||
case 'camp_site':
|
||||
return '🏕️';
|
||||
case 'slipway':
|
||||
return '🛳️';
|
||||
case 'playground':
|
||||
return '🛝';
|
||||
case 'viewpoint':
|
||||
return '👀';
|
||||
case 'cape':
|
||||
return '🏞️';
|
||||
case 'beach':
|
||||
return '🏖️';
|
||||
case 'park':
|
||||
return '🌳';
|
||||
case 'museum':
|
||||
return '🏛️';
|
||||
case 'theme_park':
|
||||
return '🎢';
|
||||
case 'nature_reserve':
|
||||
return '🌲';
|
||||
case 'memorial':
|
||||
return '🕊️';
|
||||
case 'monument':
|
||||
return '🗿';
|
||||
case 'wood':
|
||||
return '🌲';
|
||||
case 'zoo':
|
||||
return '🦁';
|
||||
case 'attraction':
|
||||
return '🎡';
|
||||
case 'ruins':
|
||||
return '🏚️';
|
||||
case 'bay':
|
||||
return '🌊';
|
||||
case 'hotel':
|
||||
return '🏨';
|
||||
case 'motel':
|
||||
return '🏩';
|
||||
case 'pub':
|
||||
return '🍺';
|
||||
case 'restaurant':
|
||||
return '🍽️';
|
||||
case 'cafe':
|
||||
return '☕';
|
||||
case 'bakery':
|
||||
return '🥐';
|
||||
case 'archaeological_site':
|
||||
return '🏺';
|
||||
case 'lighthouse':
|
||||
return '🗼';
|
||||
case 'tree':
|
||||
return '🌳';
|
||||
case 'cliff':
|
||||
return '⛰️';
|
||||
case 'water':
|
||||
return '💧';
|
||||
case 'fishing':
|
||||
return '🎣';
|
||||
case 'golf_course':
|
||||
return '⛳';
|
||||
case 'swimming_pool':
|
||||
return '🏊';
|
||||
case 'stadium':
|
||||
return '🏟️';
|
||||
case 'cave_entrance':
|
||||
return '🕳️';
|
||||
case 'anchor':
|
||||
return '⚓';
|
||||
case 'garden':
|
||||
return '🌼';
|
||||
case 'disc_golf_course':
|
||||
return '🥏';
|
||||
case 'natural':
|
||||
return '🌿';
|
||||
case 'ice_rink':
|
||||
return '⛸️';
|
||||
case 'horse_riding':
|
||||
return '🐎';
|
||||
case 'wreck':
|
||||
return '🚢';
|
||||
case 'water_park':
|
||||
return '💦';
|
||||
case 'picnic_site':
|
||||
return '🧺';
|
||||
case 'axe_throwing':
|
||||
return '🪓';
|
||||
case 'fort':
|
||||
return '🏰';
|
||||
case 'amusement_arcade':
|
||||
return '🕹️';
|
||||
case 'tepee':
|
||||
return '🏕️';
|
||||
case 'track':
|
||||
return '🏃';
|
||||
case 'trampoline_park':
|
||||
return '🤸';
|
||||
case 'dojo':
|
||||
return '🥋';
|
||||
case 'tree_stump':
|
||||
return '🪵';
|
||||
case 'peak':
|
||||
return '🏔️';
|
||||
case 'fitness_centre':
|
||||
return '🏋️';
|
||||
case 'artwork':
|
||||
return '🎨';
|
||||
case 'fast_food':
|
||||
return '🍔';
|
||||
case 'ice_cream':
|
||||
return '🍦';
|
||||
default:
|
||||
return '📍'; // Default placeholder emoji for unknown tags
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
groupAdventuresByDate,
|
||||
groupNotesByDate,
|
||||
groupTransportationsByDate,
|
||||
groupChecklistsByDate
|
||||
groupChecklistsByDate,
|
||||
osmTagToEmoji
|
||||
} from '$lib';
|
||||
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
||||
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
||||
|
@ -240,6 +241,42 @@
|
|||
isAdventureModalOpen = false;
|
||||
}
|
||||
|
||||
let isPopupOpen = false;
|
||||
|
||||
function togglePopup() {
|
||||
isPopupOpen = !isPopupOpen;
|
||||
}
|
||||
|
||||
let recomendationsData: any;
|
||||
let loadingRecomendations: boolean = false;
|
||||
let recomendationsRange: number = 1600;
|
||||
let recomendationType: string = 'tourism';
|
||||
let recomendationTags: string[] = [];
|
||||
let selectedRecomendationTag: string = '';
|
||||
|
||||
async function getRecomendations(adventure: Adventure) {
|
||||
recomendationsData = null;
|
||||
loadingRecomendations = true;
|
||||
let res = await fetch(
|
||||
`/api/overpass/query/?lat=${adventure.latitude}&lon=${adventure.longitude}&radius=${recomendationsRange}&category=${recomendationType}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
console.log('Error fetching recommendations');
|
||||
return;
|
||||
}
|
||||
let data = await res.json();
|
||||
recomendationsData = data;
|
||||
|
||||
console.log(data);
|
||||
if (recomendationsData) {
|
||||
recomendationTags = [
|
||||
...new Set(recomendationsData.map((r: any) => r.tag).filter(Boolean))
|
||||
] as string[];
|
||||
}
|
||||
loadingRecomendations = false;
|
||||
console.log(recomendationTags);
|
||||
}
|
||||
|
||||
function saveOrCreateTransportation(event: CustomEvent<Transportation>) {
|
||||
if (transportations.find((transportation) => transportation.id === event.detail.id)) {
|
||||
// Update existing transportation
|
||||
|
@ -476,7 +513,7 @@
|
|||
{#if collection.id}
|
||||
<div class="flex justify-center mx-auto">
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<div role="tablist" class="tabs tabs-boxed tabs-lg max-w-xl">
|
||||
<div role="tablist" class="tabs tabs-boxed tabs-lg max-w-full">
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
{#if collection.start_date}
|
||||
<a
|
||||
|
@ -508,6 +545,14 @@
|
|||
on:click={() => (currentView = 'map')}
|
||||
on:keydown={(e) => e.key === 'Enter' && (currentView = 'map')}>Map</a
|
||||
>
|
||||
<a
|
||||
role="tab"
|
||||
class="tab {currentView === 'recommendations' ? 'tab-active' : ''}"
|
||||
tabindex="0"
|
||||
on:click={() => (currentView = 'recommendations')}
|
||||
on:keydown={(e) => e.key === 'Enter' && (currentView = 'recommendations')}
|
||||
>Recommendations</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -800,6 +845,167 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if currentView == 'recommendations'}
|
||||
<div class="card bg-base-200 shadow-xl my-8 mx-auto w-10/12">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-3xl justify-center mb-4">Adventure Recommendations</h2>
|
||||
{#each adventures as adventure}
|
||||
<button on:click={() => getRecomendations(adventure)} class="btn btn-neutral"
|
||||
>{adventure.name}</button
|
||||
>
|
||||
{/each}
|
||||
<div class="mt-4">
|
||||
<input
|
||||
type="range"
|
||||
min="1600"
|
||||
max="80467"
|
||||
class="range"
|
||||
step="1600"
|
||||
bind:value={recomendationsRange}
|
||||
/>
|
||||
<div class="flex w-full justify-between px-2">
|
||||
<span class="text-lg"
|
||||
>{Math.round(recomendationsRange / 1600)} mile ({(
|
||||
(recomendationsRange / 1600) *
|
||||
1.6
|
||||
).toFixed(1)} km)</span
|
||||
>
|
||||
</div>
|
||||
<div class="join flex items-center justify-center mt-4">
|
||||
<input
|
||||
class="join-item btn"
|
||||
type="radio"
|
||||
name="options"
|
||||
aria-label="Tourism"
|
||||
checked={recomendationType == 'tourism'}
|
||||
on:click={() => (recomendationType = 'tourism')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn"
|
||||
type="radio"
|
||||
name="options"
|
||||
aria-label="Food"
|
||||
checked={recomendationType == 'food'}
|
||||
on:click={() => (recomendationType = 'food')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn"
|
||||
type="radio"
|
||||
name="options"
|
||||
aria-label="Lodging"
|
||||
checked={recomendationType == 'lodging'}
|
||||
on:click={() => (recomendationType = 'lodging')}
|
||||
/>
|
||||
</div>
|
||||
{#if recomendationTags.length > 0}
|
||||
<select class="select select-bordered w-full max-w-xs">
|
||||
<option disabled selected>Select a tag</option>
|
||||
{#each recomendationTags as tag}
|
||||
<option on:click={() => (selectedRecomendationTag = tag)}>{tag}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if recomendationsData}
|
||||
<MapLibre
|
||||
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||
class="aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max
|
||||
-h-full w-full rounded-lg"
|
||||
standardControls
|
||||
center={{ lng: recomendationsData[0].longitude, lat: recomendationsData[0].latitude }}
|
||||
zoom={12}
|
||||
>
|
||||
{#each recomendationsData as recomendation}
|
||||
{#if recomendation.longitude && recomendation.latitude && recomendation.name}
|
||||
<Marker
|
||||
lngLat={[recomendation.longitude, recomendation.latitude]}
|
||||
class="grid h-8 w-8 place-items-center rounded-full border border-gray-200 bg-blue-300 text-black focus:outline-6 focus:outline-black"
|
||||
on:click={togglePopup}
|
||||
>
|
||||
<span class="text-xl">
|
||||
{osmTagToEmoji(recomendation.tag)}
|
||||
</span>
|
||||
{#if isPopupOpen}
|
||||
<Popup openOn="click" offset={[0, -10]} on:close={() => (isPopupOpen = false)}>
|
||||
<div class="text-lg text-black font-bold">{recomendation.name}</div>
|
||||
|
||||
<p class="font-semibold text-black text-md">
|
||||
{`${recomendation.tag} ${osmTagToEmoji(recomendation.tag)}`}
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="btn btn-neutral btn-wide btn-sm mt-4"
|
||||
on:click={() =>
|
||||
window.open(
|
||||
`https://www.openstreetmap.org/node/${recomendation.id}`,
|
||||
'_blank'
|
||||
)}>{$t('map.view_details')}</button
|
||||
>
|
||||
</Popup>
|
||||
{/if}
|
||||
</Marker>
|
||||
{/if}
|
||||
{/each}
|
||||
</MapLibre>
|
||||
{#each recomendationsData as recomendation}
|
||||
{#if recomendation.name && recomendation.longitude && recomendation.latitude}
|
||||
<div class="card bg-base-100 shadow-xl my-4 w-full">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-xl font-bold">
|
||||
{recomendation.name || 'Recommendation'}
|
||||
</h2>
|
||||
<div class="badge badge-primary">{recomendation.tag}</div>
|
||||
<p class="text-md">{recomendation.description || 'No description available.'}</p>
|
||||
{#if recomendation.address}
|
||||
<p class="text-md">
|
||||
<strong>Address:</strong>
|
||||
{recomendation.address.housenumber}
|
||||
{recomendation.address.street}, {recomendation.address.city}, {recomendation
|
||||
.address.state}
|
||||
{recomendation.address.postcode}
|
||||
</p>
|
||||
{/if}
|
||||
{#if recomendation.contact}
|
||||
<p class="text-md">
|
||||
<strong>Contact:</strong>
|
||||
{#if recomendation.contact.phone}
|
||||
Phone: {recomendation.contact.phone}
|
||||
{/if}
|
||||
{#if recomendation.contact.email}
|
||||
Email: {recomendation.contact.email}
|
||||
{/if}
|
||||
{#if recomendation.contact.website}
|
||||
Website: <a
|
||||
href={recomendation.contact.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{recomendation.contact.website}</a
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{#if loadingRecomendations}
|
||||
<div class="card bg-base-100 shadow-xl my-4 w-full">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<span class="loading loading-ring loading-lg"></span>
|
||||
<div class="mt-2">
|
||||
<p class="text-center text-lg">
|
||||
Discovering hidden gems for your next adventure...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
class="join-item btn"
|
||||
type="radio"
|
||||
name="filter"
|
||||
aria-label={$t('adventures.activity_types')}
|
||||
aria-label={$t('adventures.tags')}
|
||||
id="activity_types"
|
||||
on:change={() => (property = 'activity_types')}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue