mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-23 14:59:36 +02:00
fix: update RegionCard component to handle undefined visited state and refactor global search API to return structured results
This commit is contained in:
parent
d60945d5b7
commit
f10e171a8e
4 changed files with 129 additions and 224 deletions
|
@ -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
|
||||
})
|
||||
visited_cities = VisitedCity.objects.filter(user_id=request.user)
|
||||
results["visited_cities"] = VisitedCitySerializer(visited_cities, many=True).data
|
||||
|
||||
return Response(results)
|
||||
|
|
|
@ -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 @@
|
|||
</div>
|
||||
<div class="card-actions justify-end">
|
||||
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
|
||||
{#if !visited}
|
||||
{#if !visited && visited !== undefined}
|
||||
<button class="btn btn-primary" on:click={markVisited}
|
||||
>{$t('adventures.mark_visited')}</button
|
||||
>
|
||||
{/if}
|
||||
{#if visited}
|
||||
{#if visited && visited !== undefined}
|
||||
<button class="btn btn-warning" on:click={removeVisit}>{$t('adventures.remove')}</button>
|
||||
{/if}
|
||||
{#if region.num_cities > 0}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,184 +1,102 @@
|
|||
<script lang="ts">
|
||||
import AdventureCard from '$lib/components/AdventureCard.svelte';
|
||||
import NotFound from '$lib/components/NotFound.svelte';
|
||||
import type { Adventure, OpenStreetMapPlace } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import RegionCard from '$lib/components/RegionCard.svelte';
|
||||
import CityCard from '$lib/components/CityCard.svelte';
|
||||
import CountryCard from '$lib/components/CountryCard.svelte';
|
||||
import CollectionCard from '$lib/components/CollectionCard.svelte';
|
||||
import UserCard from '$lib/components/UserCard.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
function deleteAdventure(event: CustomEvent<string>) {
|
||||
myAdventures = myAdventures.filter((adventure) => adventure.id !== event.detail);
|
||||
}
|
||||
let adventures = data.adventures;
|
||||
let collections = data.collections;
|
||||
let users = data.users;
|
||||
let countries = data.countries;
|
||||
let regions = data.regions;
|
||||
let cities = data.cities;
|
||||
|
||||
let osmResults: OpenStreetMapPlace[] = [];
|
||||
let myAdventures: Adventure[] = [];
|
||||
let publicAdventures: Adventure[] = [];
|
||||
let visited_regions: { region: any }[] = data.visited_regions;
|
||||
let visited_cities: { city: any }[] = data.visited_cities;
|
||||
|
||||
let query: string | null = '';
|
||||
let property: string = 'all';
|
||||
|
||||
// on chage of property, console log the property
|
||||
|
||||
function filterByProperty() {
|
||||
let url = new URL(window.location.href);
|
||||
url.searchParams.set('property', property);
|
||||
goto(url.toString(), { invalidateAll: true });
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
query = urlParams.get('query');
|
||||
});
|
||||
|
||||
console.log(data);
|
||||
$: {
|
||||
if (data.props) {
|
||||
myAdventures = data.props.adventures;
|
||||
publicAdventures = data.props.adventures;
|
||||
|
||||
if (data.user?.uuid != null) {
|
||||
myAdventures = myAdventures.filter((adventure) => adventure.user_id === data.user?.uuid);
|
||||
} else {
|
||||
myAdventures = [];
|
||||
}
|
||||
|
||||
publicAdventures = publicAdventures.filter(
|
||||
(adventure) => adventure.user_id !== data.user?.uuid
|
||||
);
|
||||
|
||||
if (data.props.osmData) {
|
||||
osmResults = data.props.osmData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let adventureToEdit: Adventure;
|
||||
let isAdventureModalOpen: boolean = false;
|
||||
|
||||
function editAdventure(event: CustomEvent<Adventure>) {
|
||||
adventureToEdit = event.detail;
|
||||
isAdventureModalOpen = true;
|
||||
}
|
||||
|
||||
function saveEdit(event: CustomEvent<Adventure>) {
|
||||
console.log(event.detail);
|
||||
myAdventures = myAdventures.map((adventure) => {
|
||||
if (adventure.id === event.detail.id) {
|
||||
return event.detail;
|
||||
}
|
||||
return adventure;
|
||||
});
|
||||
isAdventureModalOpen = false;
|
||||
console.log(myAdventures);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isAdventureModalOpen}
|
||||
<AdventureModal
|
||||
{adventureToEdit}
|
||||
on:close={() => (isAdventureModalOpen = false)}
|
||||
on:save={filterByProperty}
|
||||
/>
|
||||
{/if}
|
||||
<h1 class="text-4xl font-bold text-center m-4">Search{query ? `: ${query}` : ''}</h1>
|
||||
|
||||
{#if myAdventures.length === 0 && osmResults.length === 0}
|
||||
<NotFound error={data.error} />
|
||||
{/if}
|
||||
|
||||
{#if myAdventures.length !== 0}
|
||||
<h2 class="text-center font-bold text-2xl mb-4">{$t('search.adventurelog_results')}</h2>
|
||||
<div class="flex items-center justify-center mt-2 mb-2">
|
||||
<div class="join">
|
||||
<input
|
||||
class="join-item btn"
|
||||
type="radio"
|
||||
name="filter"
|
||||
aria-label={$t('adventures.all')}
|
||||
id="all"
|
||||
checked
|
||||
on:change={() => (property = 'all')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn"
|
||||
type="radio"
|
||||
name="filter"
|
||||
aria-label={$t('adventures.name')}
|
||||
id="name"
|
||||
on:change={() => (property = 'name')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn"
|
||||
type="radio"
|
||||
name="filter"
|
||||
aria-label={$t('adventures.location')}
|
||||
id="location"
|
||||
on:change={() => (property = 'location')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn"
|
||||
type="radio"
|
||||
name="filter"
|
||||
aria-label={$t('adventures.description')}
|
||||
id="description"
|
||||
on:change={() => (property = 'description')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn"
|
||||
type="radio"
|
||||
name="filter"
|
||||
aria-label={$t('adventures.tags')}
|
||||
id="activity_types"
|
||||
on:change={() => (property = 'activity_types')}
|
||||
/>
|
||||
</div>
|
||||
<button class="btn btn-primary ml-2" type="button" on:click={filterByProperty}
|
||||
>{$t('adventures.filter')}</button
|
||||
>
|
||||
{#if adventures.length > 0}
|
||||
<h2 class="text-3xl font-bold text-center m-4">Adventures</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
||||
{#each adventures as adventure}
|
||||
<AdventureCard {adventure} user={data.user} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if myAdventures.length > 0}
|
||||
<h2 class="text-center font-bold text-2xl mb-4">{$t('adventures.my_adventures')}</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||
{#each myAdventures as adventure}
|
||||
<AdventureCard
|
||||
user={data.user}
|
||||
{adventure}
|
||||
on:delete={deleteAdventure}
|
||||
on:edit={editAdventure}
|
||||
{#if collections.length > 0}
|
||||
<h2 class="text-3xl font-bold text-center m-4">Collections</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
||||
{#each collections as collection}
|
||||
<CollectionCard {collection} type={''} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if countries.length > 0}
|
||||
<h2 class="text-3xl font-bold text-center m-4">Countries</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
||||
{#each countries as country}
|
||||
<CountryCard {country} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if regions.length > 0}
|
||||
<h2 class="text-3xl font-bold text-center m-4">Regions</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
||||
{#each regions as region}
|
||||
<RegionCard
|
||||
{region}
|
||||
visited={visited_regions.some((visitedRegion) => visitedRegion.region === region.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if publicAdventures.length > 0}
|
||||
<h2 class="text-center font-bold text-2xl mb-4">{$t('search.public_adventures')}</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||
{#each publicAdventures as adventure}
|
||||
<AdventureCard user={null} {adventure} on:delete={deleteAdventure} on:edit={editAdventure} />
|
||||
{#if cities.length > 0}
|
||||
<h2 class="text-3xl font-bold text-center m-4">Cities</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
||||
{#each cities as city}
|
||||
<CityCard
|
||||
{city}
|
||||
visited={visited_cities.some((visitedCity) => visitedCity.city === city.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if myAdventures.length > 0 && osmResults.length > 0 && publicAdventures.length > 0}
|
||||
<div class="divider"></div>
|
||||
{/if}
|
||||
{#if osmResults.length > 0}
|
||||
<h2 class="text-center font-bold mt-2 text-2xl mb-4">{$t('search.online_results')}</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||
{#each osmResults as result}
|
||||
<div class="bg-base-300 rounded-lg shadow-md p-4 w-96 mb-2">
|
||||
<h2 class="text-xl font-bold">{result.display_name}</h2>
|
||||
<p>{result.type}</p>
|
||||
<p>{result.lat}, {result.lon}</p>
|
||||
</div>
|
||||
|
||||
{#if users.length > 0}
|
||||
<h2 class="text-3xl font-bold text-center m-4">Users</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
||||
{#each users as user}
|
||||
<UserCard {user} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if adventures.length === 0 && regions.length === 0 && cities.length === 0 && countries.length === 0 && collections.length === 0 && users.length === 0}
|
||||
<p class="text-center text-lg m-4">
|
||||
{$t('adventures.no_results')}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
<title>Search{query ? `: ${query}` : ''}</title>
|
||||
<meta name="description" content="Search your adventures." />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue