mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-05 05:05:17 +02:00
commit
2434e7669d
14 changed files with 407 additions and 7 deletions
|
@ -1,12 +1,13 @@
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription
|
from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription, ActivityTypesView
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'adventures', AdventureViewSet, basename='adventures')
|
router.register(r'adventures', AdventureViewSet, basename='adventures')
|
||||||
router.register(r'collections', CollectionViewSet, basename='collections')
|
router.register(r'collections', CollectionViewSet, basename='collections')
|
||||||
router.register(r'stats', StatsViewSet, basename='stats')
|
router.register(r'stats', StatsViewSet, basename='stats')
|
||||||
router.register(r'generate', GenerateDescription, basename='generate')
|
router.register(r'generate', GenerateDescription, basename='generate')
|
||||||
|
router.register(r'activity-types', ActivityTypesView, basename='activity-types')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -139,6 +139,17 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
|
||||||
return Response(serializer.data)
|
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):
|
def paginate_and_respond(self, queryset, request):
|
||||||
paginator = self.pagination_class()
|
paginator = self.pagination_class()
|
||||||
|
@ -330,4 +341,31 @@ class GenerateDescription(viewsets.ViewSet):
|
||||||
if extract.get('original') is None:
|
if extract.get('original') is None:
|
||||||
return Response({"error": "No image found"}, status=400)
|
return Response({"error": "No image found"}, status=400)
|
||||||
return Response(extract["original"])
|
return Response(extract["original"])
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
91
frontend/src/lib/components/ActivityComplete.svelte
Normal file
91
frontend/src/lib/components/ActivityComplete.svelte
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let activities: string[] | undefined | null;
|
||||||
|
|
||||||
|
let allActivities: string[] = [];
|
||||||
|
let inputVal: string = '';
|
||||||
|
|
||||||
|
if (activities == null || activities == undefined) {
|
||||||
|
activities = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
let res = await fetch('/api/activity-types/types/');
|
||||||
|
let data = await res.json();
|
||||||
|
if (data) {
|
||||||
|
allActivities = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function addActivity() {
|
||||||
|
if (inputVal && activities) {
|
||||||
|
const trimmedInput = inputVal.trim();
|
||||||
|
if (trimmedInput && !activities.includes(trimmedInput)) {
|
||||||
|
activities = [...activities, trimmedInput];
|
||||||
|
inputVal = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeActivity(item: string) {
|
||||||
|
if (activities) {
|
||||||
|
activities = activities.filter((activity) => activity !== item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: filteredItems = allActivities.filter(function (activity) {
|
||||||
|
return (
|
||||||
|
activity.toLowerCase().includes(inputVal.toLowerCase()) &&
|
||||||
|
(!activities || !activities.includes(activity))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Add an activity"
|
||||||
|
bind:value={inputVal}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
addActivity();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{#if inputVal && filteredItems.length > 0}
|
||||||
|
<ul class="absolute z-10 w-full bg-base-100 shadow-lg max-h-60 overflow-auto">
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
{#each filteredItems as item}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<li
|
||||||
|
class="p-2 hover:bg-base-200 cursor-pointer"
|
||||||
|
on:click={() => {
|
||||||
|
inputVal = item;
|
||||||
|
addActivity();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<ul class="space-y-2">
|
||||||
|
{#if activities}
|
||||||
|
{#each activities as activity}
|
||||||
|
<li class="flex items-center justify-between bg-base-200 p-2 rounded">
|
||||||
|
{activity}
|
||||||
|
<button class="btn btn-sm btn-error" on:click={() => removeActivity(activity)}>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</div>
|
|
@ -31,6 +31,7 @@
|
||||||
<p class="text-lg ml-4 font-bold">Hi, {user.first_name} {user.last_name}</p>
|
<p class="text-lg ml-4 font-bold">Hi, {user.first_name} {user.last_name}</p>
|
||||||
<li><button on:click={() => goto('/profile')}>Profile</button></li>
|
<li><button on:click={() => goto('/profile')}>Profile</button></li>
|
||||||
<li><button on:click={() => goto('/adventures')}>My Adventures</button></li>
|
<li><button on:click={() => goto('/adventures')}>My Adventures</button></li>
|
||||||
|
<li><button on:click={() => goto('/activities')}>My Activities</button></li>
|
||||||
<li><button on:click={() => goto('/settings')}>User Settings</button></li>
|
<li><button on:click={() => goto('/settings')}>User Settings</button></li>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<li><button formaction="/?/logout">Logout</button></li>
|
<li><button formaction="/?/logout">Logout</button></li>
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
import Earth from '~icons/mdi/earth';
|
import Earth from '~icons/mdi/earth';
|
||||||
import Wikipedia from '~icons/mdi/wikipedia';
|
import Wikipedia from '~icons/mdi/wikipedia';
|
||||||
import ImageFetcher from './ImageFetcher.svelte';
|
import ImageFetcher from './ImageFetcher.svelte';
|
||||||
|
import ActivityComplete from './ActivityComplete.svelte';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
|
@ -213,9 +214,11 @@
|
||||||
type="text"
|
type="text"
|
||||||
id="activity_types"
|
id="activity_types"
|
||||||
name="activity_types"
|
name="activity_types"
|
||||||
|
hidden
|
||||||
bind:value={adventureToEdit.activity_types}
|
bind:value={adventureToEdit.activity_types}
|
||||||
class="input input-bordered w-full max-w-xs mt-1"
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
/>
|
/>
|
||||||
|
<ActivityComplete bind:activities={adventureToEdit.activity_types} />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="image">Image </label><br />
|
<label for="image">Image </label><br />
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
import Water from '~icons/mdi/water';
|
import Water from '~icons/mdi/water';
|
||||||
import AboutModal from './AboutModal.svelte';
|
import AboutModal from './AboutModal.svelte';
|
||||||
import Avatar from './Avatar.svelte';
|
import Avatar from './Avatar.svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
let query: string = '';
|
||||||
|
|
||||||
let isAboutModalOpen: boolean = false;
|
let isAboutModalOpen: boolean = false;
|
||||||
|
|
||||||
|
@ -22,6 +25,22 @@
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isAboutModalOpen}
|
{#if isAboutModalOpen}
|
||||||
|
@ -96,6 +115,24 @@
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-neutral" on:click={() => goto('/map')}>Map</button>
|
<button class="btn btn-neutral" on:click={() => goto('/map')}>Map</button>
|
||||||
</li>
|
</li>
|
||||||
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
|
<form>
|
||||||
|
<input type="text" bind:value={query} class="grow" placeholder="Search" />
|
||||||
|
</form>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="h-4 w-4 opacity-70"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<button on:click={searchGo} type="submit" class="btn btn-neutral">Search</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !data.user}
|
{#if !data.user}
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
export let type: string = 'visited';
|
export let type: string = 'visited';
|
||||||
|
|
||||||
import Wikipedia from '~icons/mdi/wikipedia';
|
import Wikipedia from '~icons/mdi/wikipedia';
|
||||||
|
import ClipboardList from '~icons/mdi/clipboard-list';
|
||||||
|
import ActivityComplete from './ActivityComplete.svelte';
|
||||||
|
|
||||||
let newAdventure: Adventure = {
|
let newAdventure: Adventure = {
|
||||||
id: NaN,
|
id: NaN,
|
||||||
|
@ -218,17 +220,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="activity_types"
|
<label for="activityTypes"
|
||||||
>Activity Types <iconify-icon icon="mdi:clipboard-list" class="text-xl -mb-1"
|
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||||
></iconify-icon></label
|
|
||||||
><br />
|
><br />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="activity_types"
|
|
||||||
id="activity_types"
|
id="activity_types"
|
||||||
|
name="activity_types"
|
||||||
|
hidden
|
||||||
bind:value={newAdventure.activity_types}
|
bind:value={newAdventure.activity_types}
|
||||||
class="input input-bordered w-full max-w-xs mt-1"
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
/>
|
/>
|
||||||
|
<ActivityComplete bind:activities={newAdventure.activity_types} />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="rating"
|
<label for="rating"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import inspirationalQuotes from './json/quotes.json';
|
import inspirationalQuotes from './json/quotes.json';
|
||||||
|
import type { Adventure, Collection } from './types';
|
||||||
|
|
||||||
export function getRandomQuote() {
|
export function getRandomQuote() {
|
||||||
const quotes = inspirationalQuotes.quotes;
|
const quotes = inspirationalQuotes.quotes;
|
||||||
|
@ -19,3 +20,48 @@ export function checkLink(link: string) {
|
||||||
return 'http://' + link + '.com';
|
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<string> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
26
frontend/src/routes/activities/+page.server.ts
Normal file
26
frontend/src/routes/activities/+page.server.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
|
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
export const load = (async (event) => {
|
||||||
|
if (!event.locals.user) {
|
||||||
|
return redirect(302, '/login');
|
||||||
|
}
|
||||||
|
let allActivities: string[] = [];
|
||||||
|
let res = await fetch(`${endpoint}/api/activity-types/types/`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Cookie: `${event.cookies.get('auth')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let data = await res.json();
|
||||||
|
if (data) {
|
||||||
|
allActivities = data;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
activities: allActivities
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}) satisfies PageServerLoad;
|
33
frontend/src/routes/activities/+page.svelte
Normal file
33
frontend/src/routes/activities/+page.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
let activities: string[] = data.props.activities;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- make a table with pinned rows -->
|
||||||
|
<table class="table table-compact">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Activity</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each activities as activity}
|
||||||
|
<tr>
|
||||||
|
<td>{activity}</td>
|
||||||
|
<td>
|
||||||
|
<!-- <button
|
||||||
|
class="btn btn-sm btn-error"
|
||||||
|
on:click={() => {
|
||||||
|
activities = activities.filter((a) => a !== activity);
|
||||||
|
}}>Remove</button
|
||||||
|
> -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!-- </ul> -->
|
|
@ -193,6 +193,44 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if currentView == 'table'}
|
||||||
|
<table class="table table-compact w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Rating</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each adventures as adventure}
|
||||||
|
<tr>
|
||||||
|
<td>{adventure.name}</td>
|
||||||
|
<td>{adventure.date}</td>
|
||||||
|
<td>{adventure.rating}</td>
|
||||||
|
<td>{adventure.type}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
on:click={() => editAdventure(new CustomEvent('edit', { detail: adventure }))}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-error"
|
||||||
|
on:click={() =>
|
||||||
|
deleteAdventure(new CustomEvent('delete', { detail: adventure.id }))}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
<div class="join flex items-center justify-center mt-4">
|
<div class="join flex items-center justify-center mt-4">
|
||||||
{#if next || previous}
|
{#if next || previous}
|
||||||
<div class="join">
|
<div class="join">
|
||||||
|
|
39
frontend/src/routes/search/+page.server.ts
Normal file
39
frontend/src/routes/search/+page.server.ts
Normal file
|
@ -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;
|
28
frontend/src/routes/search/+page.svelte
Normal file
28
frontend/src/routes/search/+page.svelte
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import AdventureCard from '$lib/components/AdventureCard.svelte';
|
||||||
|
import NotFound from '$lib/components/NotFound.svelte';
|
||||||
|
import type { Adventure } from '$lib/types';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
function deleteAdventure(event: CustomEvent<number>) {
|
||||||
|
adventures = adventures.filter((adventure) => adventure.id !== event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
let adventures: Adventure[] = [];
|
||||||
|
if (data.props) {
|
||||||
|
adventures = data.props.adventures;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if adventures.length === 0}
|
||||||
|
<NotFound />
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#each adventures as adventure}
|
||||||
|
<AdventureCard type={adventure.type} {adventure} on:delete={deleteAdventure} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -3,9 +3,10 @@
|
||||||
import { goto, invalidateAll } from '$app/navigation';
|
import { goto, invalidateAll } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { addToast } from '$lib/toasts';
|
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 { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { exportData } from '$lib';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
let user: User;
|
let user: User;
|
||||||
|
@ -33,6 +34,16 @@
|
||||||
addToast('error', 'Error updating settings');
|
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);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-center font-extrabold text-4xl mb-6">Settings Page</h1>
|
<h1 class="text-center font-extrabold text-4xl mb-6">Settings Page</h1>
|
||||||
|
@ -141,6 +152,11 @@
|
||||||
<button class="py-2 px-4 btn btn-primary mt-2">Change Email</button>
|
<button class="py-2 px-4 btn btn-primary mt-2">Change Email</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">Data Export</h1>
|
||||||
|
<button class="btn btn-neutral mb-4" on:click={exportAdventures}> Export to JSON </button>
|
||||||
|
<p>This may take a few seconds...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<small class="text-center"
|
<small class="text-center"
|
||||||
><b>For Debug Use:</b> Server PK={user.pk} | Date Joined: {user.date_joined
|
><b>For Debug Use:</b> Server PK={user.pk} | Date Joined: {user.date_joined
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue