1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-22 22:39: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:
Sean Morley 2025-01-18 12:57:56 -05:00
parent d60945d5b7
commit f10e171a8e
4 changed files with 129 additions and 224 deletions

View file

@ -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)

View file

@ -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}

View file

@ -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}`,
{
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;

View file

@ -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." />