From 824f5a6e2af1a7ea6299c47761f7fbb2a1d3a2dd Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 16 Jul 2024 21:24:18 -0400 Subject: [PATCH 1/7] data export --- frontend/src/routes/settings/+page.svelte | 34 ++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 66c4e89..4c0da6f 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -3,7 +3,7 @@ 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'; @@ -33,6 +33,33 @@ addToast('error', 'Error updating settings'); } } + + async function exportAdventures() { + let res = await fetch('/api/adventures/all'); + let adventures = (await res.json()) as Adventure[]; + + res = await fetch('/api/collections/all'); + let collections = (await res.json()) as Collection[]; + + res = await fetch('/api/visitedregion'); + let visitedRegions = await res.json(); + + const data = { + adventures, + collections, + visitedRegions + }; + + const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); + + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = 'adventure-log-export.json'; + a.click(); + URL.revokeObjectURL(url); + }

Settings Page

@@ -141,6 +168,11 @@ +
+

Data Export

+ +

This may take a few seconds...

+
For Debug Use: Server PK={user.pk} | Date Joined: {user.date_joined From aa03c4997982ffab12cad318ffcec67c6d98ea57 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 17 Jul 2024 10:35:42 -0400 Subject: [PATCH 2/7] export --- frontend/src/lib/index.ts | 46 +++++++++++++++++++++++ frontend/src/routes/settings/+page.svelte | 20 +--------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index da0c4dd..db4f293 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -1,4 +1,5 @@ import inspirationalQuotes from './json/quotes.json'; +import type { Adventure, Collection } from './types'; export function getRandomQuote() { const quotes = inspirationalQuotes.quotes; @@ -19,3 +20,48 @@ export function checkLink(link: string) { return 'http://' + link + '.com'; } } + +export async function exportData() { + let res = await fetch('/api/adventures/all'); + let adventures = (await res.json()) as Adventure[]; + + res = await fetch('/api/collections/all'); + let collections = (await res.json()) as Collection[]; + + res = await fetch('/api/visitedregion'); + let visitedRegions = await res.json(); + + const data = { + adventures, + collections, + visitedRegions + }; + + async function convertImages() { + const promises = data.adventures.map(async (adventure, i) => { + if (adventure.image) { + const res = await fetch(adventure.image); + const blob = await res.blob(); + const base64 = await blobToBase64(blob); + adventure.image = base64; + data.adventures[i].image = adventure.image; + } + }); + + await Promise.all(promises); + } + + function blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = (error) => reject(error); + }); + } + + await convertImages(); + + const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); + return URL.createObjectURL(blob); +} diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 4c0da6f..7ea4660 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -6,6 +6,7 @@ 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; @@ -35,24 +36,7 @@ } async function exportAdventures() { - let res = await fetch('/api/adventures/all'); - let adventures = (await res.json()) as Adventure[]; - - res = await fetch('/api/collections/all'); - let collections = (await res.json()) as Collection[]; - - res = await fetch('/api/visitedregion'); - let visitedRegions = await res.json(); - - const data = { - adventures, - collections, - visitedRegions - }; - - const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); - - const url = URL.createObjectURL(blob); + const url = await exportData(); const a = document.createElement('a'); a.href = url; From 7431d4512476f7dd0a97628d600c0f5fc55e9b06 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 17 Jul 2024 17:36:05 -0400 Subject: [PATCH 3/7] search api --- backend/server/adventures/views.py | 11 ++++++ docker-compose.yml | 8 ++--- frontend/src/routes/adventures/+page.svelte | 38 ++++++++++++++++++++ frontend/src/routes/search/+page.server.ts | 39 +++++++++++++++++++++ frontend/src/routes/search/+page.svelte | 22 ++++++++++++ 5 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 frontend/src/routes/search/+page.server.ts create mode 100644 frontend/src/routes/search/+page.svelte diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index cd64725..156138a 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() diff --git a/docker-compose.yml b/docker-compose.yml index 7ffadeb..90e3194 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,8 @@ version: "3.9" services: web: - #build: ./frontend/ - image: ghcr.io/seanmorley15/adventurelog-frontend:latest + build: ./frontend/ + #image: ghcr.io/seanmorley15/adventurelog-frontend:latest environment: - PUBLIC_SERVER_URL=http://server:8000 - ORIGIN=http://localhost:8080 @@ -23,8 +23,8 @@ services: - postgres_data:/var/lib/postgresql/data/ server: - #build: ./backend/ - image: ghcr.io/seanmorley15/adventurelog-backend:latest + build: ./backend/ + #image: ghcr.io/seanmorley15/adventurelog-backend:latest environment: - PGHOST=db - PGDATABASE=database diff --git a/frontend/src/routes/adventures/+page.svelte b/frontend/src/routes/adventures/+page.svelte index cc819ab..39c333d 100644 --- a/frontend/src/routes/adventures/+page.svelte +++ b/frontend/src/routes/adventures/+page.svelte @@ -193,6 +193,44 @@ {/each} {/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..e3d3f24 --- /dev/null +++ b/frontend/src/routes/search/+page.svelte @@ -0,0 +1,22 @@ + + +{#if adventures.length === 0} + +{:else} + {#each adventures as adventure} + + {/each} +{/if} From 2b3c96bb8d758d15d487a219460103ca6e30b605 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 17 Jul 2024 19:35:02 -0400 Subject: [PATCH 4/7] activity types for autocomplete in the future --- backend/server/adventures/urls.py | 3 +- backend/server/adventures/views.py | 29 +++++++++++++++++- frontend/src/lib/components/Navbar.svelte | 37 +++++++++++++++++++++++ frontend/src/routes/search/+page.svelte | 8 +++-- 4 files changed, 72 insertions(+), 5 deletions(-) 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 156138a..d0a1b0c 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -341,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/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/routes/search/+page.svelte b/frontend/src/routes/search/+page.svelte index e3d3f24..73de6e3 100644 --- a/frontend/src/routes/search/+page.svelte +++ b/frontend/src/routes/search/+page.svelte @@ -16,7 +16,9 @@ {#if adventures.length === 0} {:else} - {#each adventures as adventure} - - {/each} +
    + {#each adventures as adventure} + + {/each} +
    {/if} From f03c338935ccff92ad29804a37fe0ab0aa99a574 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 17 Jul 2024 21:51:27 -0400 Subject: [PATCH 5/7] activity type overhaul --- .../lib/components/ActivityComplete.svelte | 89 +++++++++++++++++++ .../src/lib/components/EditAdventure.svelte | 3 + 2 files changed, 92 insertions(+) create mode 100644 frontend/src/lib/components/ActivityComplete.svelte diff --git a/frontend/src/lib/components/ActivityComplete.svelte b/frontend/src/lib/components/ActivityComplete.svelte new file mode 100644 index 0000000..53d4cd2 --- /dev/null +++ b/frontend/src/lib/components/ActivityComplete.svelte @@ -0,0 +1,89 @@ + + +
    + { + if (e.key === 'Enter') { + e.preventDefault(); + addActivity(); + } + }} + /> + {#if inputVal && filteredItems.length > 0} +
      + {#each filteredItems as item} + + +
    • { + inputVal = item; + addActivity(); + }} + > + {item} +
    • + {/each} +
    + {/if} +
    + +
    +
      + {#if activities} + {#each activities as activity} +
    • + {activity} + +
    • + {/each} + {/if} +
    +
    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" /> +

    From f38062e2509318c0439a30f297a4d186d7aa1c73 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 17 Jul 2024 22:06:55 -0400 Subject: [PATCH 6/7] activity type overhaul --- .../lib/components/ActivityComplete.svelte | 6 ++-- frontend/src/lib/components/Avatar.svelte | 1 + .../src/lib/components/NewAdventure.svelte | 11 ++++--- .../src/routes/activities/+page.server.ts | 26 +++++++++++++++ frontend/src/routes/activities/+page.svelte | 33 +++++++++++++++++++ frontend/src/routes/search/+page.svelte | 6 +++- 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 frontend/src/routes/activities/+page.server.ts create mode 100644 frontend/src/routes/activities/+page.svelte diff --git a/frontend/src/lib/components/ActivityComplete.svelte b/frontend/src/lib/components/ActivityComplete.svelte index 53d4cd2..ac04ea6 100644 --- a/frontend/src/lib/components/ActivityComplete.svelte +++ b/frontend/src/lib/components/ActivityComplete.svelte @@ -20,8 +20,9 @@ function addActivity() { if (inputVal && activities) { - if (!activities.includes(inputVal) && allActivities.includes(inputVal)) { - activities = [...activities, inputVal]; + const trimmedInput = inputVal.trim(); + if (trimmedInput && !activities.includes(trimmedInput)) { + activities = [...activities, trimmedInput]; inputVal = ''; } } @@ -56,6 +57,7 @@ /> {#if inputVal && filteredItems.length > 0}
      + {#each filteredItems as item} 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/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
    +