diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index 2eb2573..10cdb1a 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -1,12 +1,13 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription +from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription, ActivityTypesView router = DefaultRouter() router.register(r'adventures', AdventureViewSet, basename='adventures') router.register(r'collections', CollectionViewSet, basename='collections') router.register(r'stats', StatsViewSet, basename='stats') router.register(r'generate', GenerateDescription, basename='generate') +router.register(r'activity-types', ActivityTypesView, basename='activity-types') urlpatterns = [ diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index cd64725..d0a1b0c 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -139,6 +139,17 @@ class AdventureViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) + + @action(detail=False, methods=['get']) + def search(self, request): + query = self.request.query_params.get('query', '') + queryset = Adventure.objects.filter( + (Q(name__icontains=query) | Q(description__icontains=query) | Q(location__icontains=query) | Q(activity_types__icontains=query)) & + (Q(user_id=request.user.id) | Q(is_public=True)) +) + queryset = self.apply_sorting(queryset) + adventures = self.paginate_and_respond(queryset, request) + return adventures def paginate_and_respond(self, queryset, request): paginator = self.pagination_class() @@ -330,4 +341,31 @@ class GenerateDescription(viewsets.ViewSet): if extract.get('original') is None: return Response({"error": "No image found"}, status=400) return Response(extract["original"]) - \ No newline at end of file + + +class ActivityTypesView(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + + @action(detail=False, methods=['get']) + def types(self, request): + """ + Retrieve a list of distinct activity types for adventures associated with the current user. + + Args: + request (HttpRequest): The HTTP request object. + + Returns: + Response: A response containing a list of distinct activity types. + """ + types = Adventure.objects.filter(user_id=request.user.id).values_list('activity_types', flat=True).distinct() + + allTypes = [] + + for i in types: + if not i: + continue + for x in i: + if x and x not in allTypes: + allTypes.append(x) + + return Response(allTypes) diff --git a/frontend/src/lib/components/ActivityComplete.svelte b/frontend/src/lib/components/ActivityComplete.svelte new file mode 100644 index 0000000..ac04ea6 --- /dev/null +++ b/frontend/src/lib/components/ActivityComplete.svelte @@ -0,0 +1,91 @@ + + +
+ { + if (e.key === 'Enter') { + e.preventDefault(); + addActivity(); + } + }} + /> + {#if inputVal && filteredItems.length > 0} + + {/if} +
+ +
+ +
diff --git a/frontend/src/lib/components/Avatar.svelte b/frontend/src/lib/components/Avatar.svelte index bb3833e..84b3141 100644 --- a/frontend/src/lib/components/Avatar.svelte +++ b/frontend/src/lib/components/Avatar.svelte @@ -31,6 +31,7 @@

Hi, {user.first_name} {user.last_name}

  • +
  • diff --git a/frontend/src/lib/components/EditAdventure.svelte b/frontend/src/lib/components/EditAdventure.svelte index d6d62ee..b3fe39e 100644 --- a/frontend/src/lib/components/EditAdventure.svelte +++ b/frontend/src/lib/components/EditAdventure.svelte @@ -27,6 +27,7 @@ import Earth from '~icons/mdi/earth'; import Wikipedia from '~icons/mdi/wikipedia'; import ImageFetcher from './ImageFetcher.svelte'; + import ActivityComplete from './ActivityComplete.svelte'; onMount(async () => { modal = document.getElementById('my_modal_1') as HTMLDialogElement; @@ -213,9 +214,11 @@ type="text" id="activity_types" name="activity_types" + hidden bind:value={adventureToEdit.activity_types} class="input input-bordered w-full max-w-xs mt-1" /> +

    diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index 1d9da85..b54c292 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -12,6 +12,9 @@ import Water from '~icons/mdi/water'; import AboutModal from './AboutModal.svelte'; import Avatar from './Avatar.svelte'; + import { page } from '$app/stores'; + + let query: string = ''; let isAboutModalOpen: boolean = false; @@ -22,6 +25,22 @@ document.documentElement.setAttribute('data-theme', theme); } }; + + const searchGo = async (e: Event) => { + e.preventDefault(); + let reload: boolean = false; + + if ($page.url.pathname === '/search') { + reload = true; + } + + if (query) { + await goto(`/search?query=${query}`); + if (reload) { + window.location.reload(); + } + } + }; {#if isAboutModalOpen} @@ -96,6 +115,24 @@
  • + + {/if} {#if !data.user} diff --git a/frontend/src/lib/components/NewAdventure.svelte b/frontend/src/lib/components/NewAdventure.svelte index b1f15bc..65216d5 100644 --- a/frontend/src/lib/components/NewAdventure.svelte +++ b/frontend/src/lib/components/NewAdventure.svelte @@ -10,6 +10,8 @@ export let type: string = 'visited'; import Wikipedia from '~icons/mdi/wikipedia'; + import ClipboardList from '~icons/mdi/clipboard-list'; + import ActivityComplete from './ActivityComplete.svelte'; let newAdventure: Adventure = { id: NaN, @@ -218,17 +220,18 @@
    - Activity Types
    +
    {/if} + {#if currentView == 'table'} + + + + + + + + + + + + {#each adventures as adventure} + + + + + + + + {/each} + +
    NameDateRatingTypeActions
    {adventure.name}{adventure.date}{adventure.rating}{adventure.type} + + +
    + {/if}
    {#if next || previous}
    diff --git a/frontend/src/routes/search/+page.server.ts b/frontend/src/routes/search/+page.server.ts new file mode 100644 index 0000000..cfd6636 --- /dev/null +++ b/frontend/src/routes/search/+page.server.ts @@ -0,0 +1,39 @@ +import type { Adventure } from '$lib/types'; +import type { PageServerLoad } from './$types'; + +const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + +export const load = (async (event) => { + // get url param query + const query = event.url.searchParams.get('query'); + + if (!query) { + return { data: [] }; + } + + let res = await fetch(`${serverEndpoint}/api/adventures/search/?query=${query}`, { + headers: { + 'Content-Type': 'application/json', + Cookie: `${event.cookies.get('auth')}` + } + }); + + if (res.ok) { + let data = await res.json(); + console.log('Search data:', data); + + return { + props: { + adventures: data.results as Adventure[], + nextPage: data.next, + prevPage: data.previous, + total: data.count, + query + } + }; + } else { + console.error('Failed to fetch search data'); + return { data: [] }; + } +}) satisfies PageServerLoad; diff --git a/frontend/src/routes/search/+page.svelte b/frontend/src/routes/search/+page.svelte new file mode 100644 index 0000000..7311e7c --- /dev/null +++ b/frontend/src/routes/search/+page.svelte @@ -0,0 +1,28 @@ + + +{#if adventures.length === 0} + +{:else} +
    + {#each adventures as adventure} + + {/each} +
    +{/if} diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 66c4e89..7ea4660 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -3,9 +3,10 @@ import { goto, invalidateAll } from '$app/navigation'; import { page } from '$app/stores'; import { addToast } from '$lib/toasts'; - import type { User } from '$lib/types.js'; + import type { Adventure, Collection, User } from '$lib/types.js'; import { onMount } from 'svelte'; import { browser } from '$app/environment'; + import { exportData } from '$lib'; export let data; let user: User; @@ -33,6 +34,16 @@ addToast('error', 'Error updating settings'); } } + + async function exportAdventures() { + const url = await exportData(); + + const a = document.createElement('a'); + a.href = url; + a.download = 'adventure-log-export.json'; + a.click(); + URL.revokeObjectURL(url); + }

    Settings Page

    @@ -141,6 +152,11 @@
    +
    +

    Data Export

    + +

    This may take a few seconds...

    +
    For Debug Use: Server PK={user.pk} | Date Joined: {user.date_joined