diff --git a/backend/server/adventures/views/global_search_view.py b/backend/server/adventures/views/global_search_view.py index 3780629..d2fa5d3 100644 --- a/backend/server/adventures/views/global_search_view.py +++ b/backend/server/adventures/views/global_search_view.py @@ -1,71 +1,73 @@ - from rest_framework import viewsets from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated +from django.db.models import Q +from django.contrib.postgres.search import SearchVector, SearchQuery from adventures.models import Adventure, Collection from adventures.serializers import AdventureSerializer, CollectionSerializer -from django.db.models import Q -from adventures.utils import pagination -from worldtravel.models import Country, Region, City -from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer +from worldtravel.models import Country, Region, City, VisitedCity, VisitedRegion +from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer, VisitedCitySerializer, VisitedRegionSerializer from users.models import CustomUser as User from users.serializers import CustomUserDetailsSerializer as UserSerializer class GlobalSearchView(viewsets.ViewSet): permission_classes = [IsAuthenticated] - pagination_class = pagination.StandardResultsSetPagination def list(self, request): - search_term = request.query_params.get('query', '') - # print(f"Searching for: {search_term}") # For debugging - + search_term = request.query_params.get('query', '').strip() if not search_term: return Response({"error": "Search query is required"}, status=400) - # Search for adventures - adventures = Adventure.objects.filter( - (Q(name__icontains=search_term) | Q(description__icontains=search_term) | Q(location__icontains=search_term)) & Q(user_id=request.user.id) - ) + # Initialize empty results + results = { + "adventures": [], + "collections": [], + "users": [], + "countries": [], + "regions": [], + "cities": [], + "visited_regions": [], + "visited_cities": [] + } - # Search for collections + # Adventures: Full-Text Search + adventures = Adventure.objects.annotate( + search=SearchVector('name', 'description', 'location') + ).filter(search=SearchQuery(search_term), user_id=request.user) + results["adventures"] = AdventureSerializer(adventures, many=True).data + + # Collections: Partial Match Search collections = Collection.objects.filter( - Q(name__icontains=search_term) & Q(user_id=request.user.id) + Q(name__icontains=search_term) & Q(user_id=request.user) ) + results["collections"] = CollectionSerializer(collections, many=True).data - # Search for users + # Users: Public Profiles Only users = User.objects.filter( - (Q(username__icontains=search_term) | Q(first_name__icontains=search_term) | Q(last_name__icontains=search_term)) & Q(public_profile=True) + (Q(username__icontains=search_term) | + Q(first_name__icontains=search_term) | + Q(last_name__icontains=search_term)) & Q(public_profile=True) ) + results["users"] = UserSerializer(users, many=True).data - # Search for countries - countries = Country.objects.filter( - Q(name__icontains=search_term) | Q(country_code__icontains=search_term) - ) + # Countries: Full-Text Search + countries = Country.objects.annotate( + search=SearchVector('name', 'country_code') + ).filter(search=SearchQuery(search_term)) + results["countries"] = CountrySerializer(countries, many=True).data - # Search for regions - regions = Region.objects.filter( - Q(name__icontains=search_term) | Q(country__name__icontains=search_term) - ) + # Regions and Cities: Partial Match Search + regions = Region.objects.filter(Q(name__icontains=search_term)) + results["regions"] = RegionSerializer(regions, many=True).data - # Search for cities - cities = City.objects.filter( - Q(name__icontains=search_term) | Q(region__name__icontains=search_term) | Q(region__country__name__icontains=search_term) - ) + cities = City.objects.filter(Q(name__icontains=search_term)) + results["cities"] = CitySerializer(cities, many=True).data - # Serialize the results - adventure_serializer = AdventureSerializer(adventures, many=True) - collection_serializer = CollectionSerializer(collections, many=True) - user_serializer = UserSerializer(users, many=True) - country_serializer = CountrySerializer(countries, many=True) - region_serializer = RegionSerializer(regions, many=True) - city_serializer = CitySerializer(cities, many=True) + # Visited Regions and Cities + visited_regions = VisitedRegion.objects.filter(user_id=request.user) + results["visited_regions"] = VisitedRegionSerializer(visited_regions, many=True).data - return Response({ - "adventures": adventure_serializer.data, - "collections": collection_serializer.data, - "users": user_serializer.data, - "countries": country_serializer.data, - "regions": region_serializer.data, - "cities": city_serializer.data - }) - \ No newline at end of file + visited_cities = VisitedCity.objects.filter(user_id=request.user) + results["visited_cities"] = VisitedCitySerializer(visited_cities, many=True).data + + return Response(results) diff --git a/frontend/src/lib/components/RegionCard.svelte b/frontend/src/lib/components/RegionCard.svelte index 5acd3a9..a5fc538 100644 --- a/frontend/src/lib/components/RegionCard.svelte +++ b/frontend/src/lib/components/RegionCard.svelte @@ -7,7 +7,7 @@ import { t } from 'svelte-i18n'; export let region: Region; - export let visited: boolean; + export let visited: boolean | undefined; function goToCity() { console.log(region); @@ -64,12 +64,12 @@
- {#if !visited} + {#if !visited && visited !== undefined} {/if} - {#if visited} + {#if visited && visited !== undefined} {/if} {#if region.num_cities > 0} diff --git a/frontend/src/routes/search/+page.server.ts b/frontend/src/routes/search/+page.server.ts index 8ff7e23..250bc96 100644 --- a/frontend/src/routes/search/+page.server.ts +++ b/frontend/src/routes/search/+page.server.ts @@ -8,7 +8,6 @@ const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { const query = event.url.searchParams.get('query'); - const property = event.url.searchParams.get('property') || 'all'; if (!query) { return { data: [] }; @@ -16,15 +15,12 @@ export const load = (async (event) => { let sessionId = event.cookies.get('sessionid'); - let res = await fetch( - `${serverEndpoint}/api/adventures/search/?query=${query}&property=${property}`, - { - headers: { - 'Content-Type': 'application/json', - Cookie: `sessionid=${sessionId}` - } + let res = await fetch(`${serverEndpoint}/api/search/?query=${query}`, { + headers: { + 'Content-Type': 'application/json', + Cookie: `sessionid=${sessionId}` } - ); + }); if (!res.ok) { console.error('Failed to fetch search data'); @@ -32,27 +28,16 @@ export const load = (async (event) => { return { error: error.error }; } - let adventures: Adventure[] = await res.json(); - - let osmRes = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=jsonv2`, { - headers: { - 'User-Agent': `AdventureLog / ${appVersion} ` - } - }); - - if (!osmRes.ok) { - console.error('Failed to fetch OSM data'); - let error = await res.json(); - return { error: error.error }; - } - - let osmData = (await osmRes.json()) as OpenStreetMapPlace[]; + let data = await res.json(); return { - props: { - adventures, - query, - osmData - } + adventures: data.adventures, + collections: data.collections, + users: data.users, + countries: data.countries, + regions: data.regions, + cities: data.cities, + visited_cities: data.visited_cities, + visited_regions: data.visited_regions }; }) satisfies PageServerLoad; diff --git a/frontend/src/routes/search/+page.svelte b/frontend/src/routes/search/+page.svelte index 6c35530..f45e6be 100644 --- a/frontend/src/routes/search/+page.svelte +++ b/frontend/src/routes/search/+page.svelte @@ -1,184 +1,102 @@ -{#if isAdventureModalOpen} - (isAdventureModalOpen = false)} - on:save={filterByProperty} - /> -{/if} +

Search{query ? `: ${query}` : ''}

-{#if myAdventures.length === 0 && osmResults.length === 0} - -{/if} - -{#if myAdventures.length !== 0} -

{$t('search.adventurelog_results')}

-
-
- (property = 'all')} - /> - (property = 'name')} - /> - (property = 'location')} - /> - (property = 'description')} - /> - (property = 'activity_types')} - /> -
- +{#if adventures.length > 0} +

Adventures

+
+ {#each adventures as adventure} + + {/each}
{/if} -{#if myAdventures.length > 0} -

{$t('adventures.my_adventures')}

-
- {#each myAdventures as adventure} - 0} +

Collections

+
+ {#each collections as collection} + + {/each} +
+{/if} + +{#if countries.length > 0} +

Countries

+
+ {#each countries as country} + + {/each} +
+{/if} + +{#if regions.length > 0} +

Regions

+
+ {#each regions as region} + visitedRegion.region === region.id)} /> {/each}
{/if} -{#if publicAdventures.length > 0} -

{$t('search.public_adventures')}

-
- {#each publicAdventures as adventure} - +{#if cities.length > 0} +

Cities

+
+ {#each cities as city} + visitedCity.city === city.id)} + /> {/each}
{/if} -{#if myAdventures.length > 0 && osmResults.length > 0 && publicAdventures.length > 0} -
-{/if} -{#if osmResults.length > 0} -

{$t('search.online_results')}

-
- {#each osmResults as result} -
-

{result.display_name}

-

{result.type}

-

{result.lat}, {result.lon}

-
+ +{#if users.length > 0} +

Users

+
+ {#each users as user} + {/each}
{/if} +{#if adventures.length === 0 && regions.length === 0 && cities.length === 0 && countries.length === 0 && collections.length === 0 && users.length === 0} +

+ {$t('adventures.no_results')} +

+{/if} + Search{query ? `: ${query}` : ''}