diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py
index 0029499..7ac6842 100644
--- a/backend/server/adventures/admin.py
+++ b/backend/server/adventures/admin.py
@@ -1,7 +1,7 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
-from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note
+from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage
from worldtravel.models import Country, Region, VisitedRegion
@@ -57,6 +57,20 @@ class CustomUserAdmin(UserAdmin):
else:
return
+class AdventureImageAdmin(admin.ModelAdmin):
+ list_display = ('user_id', 'image_display')
+
+ def image_display(self, obj):
+ if obj.image: # Ensure this field matches your model's image field
+ public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
+ public_url = public_url.replace("'", "")
+ return mark_safe(f'
-
- {#if adventure.image && adventure.image !== ''}
-
+ {#if adventure.images && adventure.images.length > 0}
+
+ {#each adventure.images as image, i}
+
+

+
+ {#each adventure.images as _, i}
+
+ {/each}
+
+
+ {/each}
+
{:else}
+
+ import { createEventDispatcher } from 'svelte';
+ import type { Adventure, OpenStreetMapPlace, Point } from '$lib/types';
+ import { onMount } from 'svelte';
+ import { enhance } from '$app/forms';
+ import { addToast } from '$lib/toasts';
+ import { deserialize } from '$app/forms';
+
+ export let longitude: number | null = null;
+ export let latitude: number | null = null;
+ export let collection_id: string | null = null;
+
+ import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
+ let markers: Point[] = [];
+ let query: string = '';
+ let places: OpenStreetMapPlace[] = [];
+ let images: { id: string; image: string }[] = [];
+
+ import Earth from '~icons/mdi/earth';
+ import ActivityComplete from './ActivityComplete.svelte';
+ import { appVersion } from '$lib/config';
+
+ export let startDate: string | null = null;
+ export let endDate: string | null = null;
+
+ let wikiError: string = '';
+
+ let noPlaces: boolean = false;
+
+ export let adventureToEdit: Adventure | null = null;
+
+ let adventure: Adventure = {
+ id: adventureToEdit?.id || '',
+ name: adventureToEdit?.name || '',
+ type: adventureToEdit?.type || 'visited',
+ date: adventureToEdit?.date || null,
+ link: adventureToEdit?.link || null,
+ description: adventureToEdit?.description || null,
+ activity_types: adventureToEdit?.activity_types || [],
+ rating: adventureToEdit?.rating || NaN,
+ is_public: adventureToEdit?.is_public || false,
+ latitude: adventureToEdit?.latitude || NaN,
+ longitude: adventureToEdit?.longitude || NaN,
+ location: adventureToEdit?.location || null,
+ images: adventureToEdit?.images || [],
+ user_id: adventureToEdit?.user_id || null,
+ collection: adventureToEdit?.collection || collection_id || null
+ };
+
+ let url: string = '';
+ let imageError: string = '';
+ let wikiImageError: string = '';
+
+ images = adventure.images || [];
+
+ if (adventure.longitude && adventure.latitude) {
+ markers = [
+ {
+ lngLat: { lng: adventure.longitude, lat: adventure.latitude },
+ location: adventure.location || '',
+ name: adventure.name,
+ activity_type: '',
+ lng: 0
+ }
+ ];
+ }
+
+ if (longitude && latitude) {
+ adventure.latitude = latitude;
+ adventure.longitude = longitude;
+ reverseGeocode();
+ }
+
+ $: {
+ if (!adventure.rating) {
+ adventure.rating = NaN;
+ }
+ }
+
+ let imageSearch: string = adventure.name || '';
+
+ async function removeImage(id: string) {
+ let res = await fetch(`/api/images/${id}/image_delete`, {
+ method: 'POST'
+ });
+ if (res.status === 204) {
+ images = images.filter((image) => image.id !== id);
+ adventure.images = images;
+ console.log(images);
+ addToast('success', 'Image removed');
+ } else {
+ addToast('error', 'Failed to remove image');
+ }
+ }
+
+ let isDetails: boolean = true;
+
+ function saveAndClose() {
+ dispatch('save', adventure);
+ close();
+ }
+
+ $: if (markers.length > 0) {
+ adventure.latitude = Math.round(markers[0].lngLat.lat * 1e6) / 1e6;
+ adventure.longitude = Math.round(markers[0].lngLat.lng * 1e6) / 1e6;
+ if (!adventure.location) {
+ adventure.location = markers[0].location;
+ }
+ if (!adventure.name) {
+ adventure.name = markers[0].name;
+ }
+ }
+
+ async function fetchImage() {
+ let res = await fetch(url);
+ let data = await res.blob();
+ if (!data) {
+ imageError = 'No image found at that URL.';
+ return;
+ }
+ let file = new File([data], 'image.jpg', { type: 'image/jpeg' });
+ let formData = new FormData();
+ formData.append('image', file);
+ formData.append('adventure', adventure.id);
+ let res2 = await fetch(`/adventures?/image`, {
+ method: 'POST',
+ body: formData
+ });
+ let data2 = await res2.json();
+ console.log(data2);
+ if (data2.type === 'success') {
+ images = [...images, data2];
+ adventure.images = images;
+ addToast('success', 'Image uploaded');
+ } else {
+ addToast('error', 'Failed to upload image');
+ }
+ }
+
+ async function fetchWikiImage() {
+ let res = await fetch(`/api/generate/img/?name=${imageSearch}`);
+ let data = await res.json();
+ if (!res.ok) {
+ wikiImageError = 'Failed to fetch image';
+ return;
+ }
+ if (data.source) {
+ let imageUrl = data.source;
+ let res = await fetch(imageUrl);
+ let blob = await res.blob();
+ let file = new File([blob], `${imageSearch}.jpg`, { type: 'image/jpeg' });
+ let formData = new FormData();
+ formData.append('image', file);
+ formData.append('adventure', adventure.id);
+ let res2 = await fetch(`/adventures?/image`, {
+ method: 'POST',
+ body: formData
+ });
+ if (res2.ok) {
+ let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
+ console.log(newData);
+ let newImage = { id: newData.data.id, image: newData.data.image };
+ console.log(newImage);
+ images = [...images, newImage];
+ adventure.images = images;
+ addToast('success', 'Image uploaded');
+ } else {
+ addToast('error', 'Failed to upload image');
+ wikiImageError = 'Failed to upload image';
+ }
+ }
+ }
+ async function geocode(e: Event | null) {
+ if (e) {
+ e.preventDefault();
+ }
+ if (!query) {
+ alert('Please enter a location');
+ return;
+ }
+ let res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=jsonv2`, {
+ headers: {
+ 'User-Agent': `AdventureLog / ${appVersion} `
+ }
+ });
+ console.log(res);
+ let data = (await res.json()) as OpenStreetMapPlace[];
+ places = data;
+ if (data.length === 0) {
+ noPlaces = true;
+ } else {
+ noPlaces = false;
+ }
+ }
+
+ async function reverseGeocode() {
+ let res = await fetch(
+ `https://nominatim.openstreetmap.org/search?q=${adventure.latitude},${adventure.longitude}&format=jsonv2`,
+ {
+ headers: {
+ 'User-Agent': `AdventureLog / ${appVersion} `
+ }
+ }
+ );
+ let data = (await res.json()) as OpenStreetMapPlace[];
+ if (data.length > 0) {
+ adventure.name = data[0]?.name || '';
+ adventure.activity_types?.push(data[0]?.type || '');
+ adventure.location = data[0]?.display_name || '';
+ if (longitude && latitude) {
+ markers = [
+ {
+ lngLat: { lng: longitude, lat: latitude },
+ location: data[0]?.display_name || '',
+ name: data[0]?.name || '',
+ activity_type: data[0]?.type || '',
+ lng: 0
+ }
+ ];
+ }
+ }
+ console.log(data);
+ }
+
+ let fileInput: HTMLInputElement;
+
+ const dispatch = createEventDispatcher();
+ let modal: HTMLDialogElement;
+
+ onMount(async () => {
+ modal = document.getElementById('my_modal_1') as HTMLDialogElement;
+ if (modal) {
+ modal.showModal();
+ }
+ });
+
+ function close() {
+ dispatch('close');
+ }
+
+ function handleKeydown(event: KeyboardEvent) {
+ if (event.key === 'Escape') {
+ close();
+ }
+ }
+
+ async function generateDesc() {
+ let res = await fetch(`/api/generate/desc/?name=${adventure.name}`);
+ let data = await res.json();
+ if (data.extract?.length > 0) {
+ adventure.description = data.extract;
+ } else {
+ wikiError = 'No description found';
+ }
+ }
+
+ function addMarker(e: CustomEvent) {
+ markers = [];
+ markers = [
+ ...markers,
+ {
+ lngLat: e.detail.lngLat,
+ name: '',
+ location: '',
+ activity_type: '',
+ lng: 0
+ }
+ ];
+ console.log(markers);
+ }
+
+ function imageSubmit() {
+ return async ({ result }: any) => {
+ if (result.type === 'success') {
+ if (result.data.id && result.data.image) {
+ adventure.images = [...adventure.images, result.data];
+ images = [...images, result.data];
+ addToast('success', 'Image uploaded');
+
+ fileInput.value = '';
+ console.log(adventure);
+ } else {
+ addToast('error', result.data.error || 'Failed to upload image');
+ }
+ }
+ };
+ }
+
+ async function handleSubmit(event: Event) {
+ event.preventDefault();
+ console.log(adventure);
+ if (adventure.id === '') {
+ let res = await fetch('/api/adventures', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(adventure)
+ });
+ let data = await res.json();
+ if (data.id) {
+ adventure = data as Adventure;
+ isDetails = false;
+ addToast('success', 'Adventure created');
+ } else {
+ addToast('error', 'Failed to create adventure');
+ }
+ } else {
+ let res = await fetch(`/api/adventures/${adventure.id}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(adventure)
+ });
+ let data = await res.json();
+ if (data.id) {
+ adventure = data as Adventure;
+ isDetails = false;
+ addToast('success', 'Adventure updated');
+ } else {
+ addToast('error', 'Failed to update adventure');
+ }
+ }
+ }
+
+
+
+
diff --git a/frontend/src/lib/components/CollectionCard.svelte b/frontend/src/lib/components/CollectionCard.svelte
index 535337a..53d76f2 100644
--- a/frontend/src/lib/components/CollectionCard.svelte
+++ b/frontend/src/lib/components/CollectionCard.svelte
@@ -111,50 +111,51 @@
{/if}
-
-
-
+ {#if type == 'link'}
+
+ {:else}
+
+
+
+
+
+
+ {#if type != 'link'}
+
+ {#if !collection.is_archived}
+
+ {/if}
+ {#if collection.is_archived}
+
+ {:else}
+
+ {/if}
+
+ {/if}
+
-
-
- {#if type != 'link'}
-
- {#if !collection.is_archived}
-
- {/if}
- {#if collection.is_archived}
-
- {:else}
-
- {/if}
-
- {/if}
- {#if type == 'link'}
-
- {/if}
-
-
+ {/if}
diff --git a/frontend/src/lib/components/EditAdventure.svelte b/frontend/src/lib/components/EditAdventure.svelte
deleted file mode 100644
index 6654fba..0000000
--- a/frontend/src/lib/components/EditAdventure.svelte
+++ /dev/null
@@ -1,363 +0,0 @@
-
-
-{#if isPointModalOpen}
- (isPointModalOpen = false)}
- on:submit={setLongLat}
- query={adventureToEdit.name}
- />
-{/if}
-
-{#if isImageFetcherOpen}
- (isImageFetcherOpen = false)}
- />
-{/if}
-
-
diff --git a/frontend/src/lib/components/NewAdventure.svelte b/frontend/src/lib/components/NewAdventure.svelte
deleted file mode 100644
index 4f114f7..0000000
--- a/frontend/src/lib/components/NewAdventure.svelte
+++ /dev/null
@@ -1,436 +0,0 @@
-
-
-{#if isPointModalOpen}
- (isPointModalOpen = false)}
- on:submit={setLongLat}
- bind:adventure={newAdventure}
- />
-{/if}
-
-{#if isImageFetcherOpen}
- (isImageFetcherOpen = false)}
- />
-{/if}
-
-
-
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts
index b00cf35..2a299dd 100644
--- a/frontend/src/lib/types.ts
+++ b/frontend/src/lib/types.ts
@@ -11,7 +11,7 @@ export type User = {
export type Adventure = {
id: string;
- user_id: number;
+ user_id: number | null;
type: string;
name: string;
location?: string | null;
@@ -19,7 +19,10 @@ export type Adventure = {
description?: string | null;
rating?: number | null;
link?: string | null;
- image?: string | null;
+ images: {
+ id: string;
+ image: string;
+ }[];
date?: string | null; // Assuming date is a string in 'YYYY-MM-DD' format
collection?: string | null;
latitude: number | null;
@@ -55,6 +58,8 @@ export type Point = {
lng: number;
};
name: string;
+ location: string;
+ activity_type: string;
};
export type Collection = {
diff --git a/frontend/src/routes/adventures/+page.server.ts b/frontend/src/routes/adventures/+page.server.ts
index ed0e38a..829a890 100644
--- a/frontend/src/routes/adventures/+page.server.ts
+++ b/frontend/src/routes/adventures/+page.server.ts
@@ -159,7 +159,7 @@ export const actions: Actions = {
}
formDataToSend.append('rating', rating ? rating.toString() : '');
formDataToSend.append('link', link || '');
- formDataToSend.append('image', image);
+ // formDataToSend.append('image', image);
// log each key-value pair in the FormData
for (let pair of formDataToSend.entries()) {
@@ -233,6 +233,21 @@ export const actions: Actions = {
let image_url = new_id.image;
let link_url = new_id.link;
+ if (image && image.size > 0) {
+ let imageForm = new FormData();
+ imageForm.append('image', image);
+ imageForm.append('adventure', id);
+ let imageRes = await fetch(`${serverEndpoint}/api/images/`, {
+ method: 'POST',
+ headers: {
+ Cookie: `${event.cookies.get('auth')}`
+ },
+ body: imageForm
+ });
+ let data = await imageRes.json();
+ console.log(data);
+ }
+
return { id, user_id, image_url, link };
},
edit: async (event) => {
@@ -410,5 +425,17 @@ export const actions: Actions = {
let image_url = adventure.image;
let link_url = adventure.link;
return { image_url, link_url };
+ },
+ image: async (event) => {
+ let formData = await event.request.formData();
+ let res = await fetch(`${serverEndpoint}/api/images/`, {
+ method: 'POST',
+ headers: {
+ Cookie: `${event.cookies.get('auth')}`
+ },
+ body: formData
+ });
+ let data = await res.json();
+ return data;
}
};
diff --git a/frontend/src/routes/adventures/+page.svelte b/frontend/src/routes/adventures/+page.svelte
index 5882cc5..c4f9cef 100644
--- a/frontend/src/routes/adventures/+page.svelte
+++ b/frontend/src/routes/adventures/+page.svelte
@@ -3,8 +3,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import AdventureCard from '$lib/components/AdventureCard.svelte';
- import EditAdventure from '$lib/components/EditAdventure.svelte';
- import NewAdventure from '$lib/components/NewAdventure.svelte';
+ import AdventureModal from '$lib/components/AdventureModal.svelte';
import NotFound from '$lib/components/NotFound.svelte';
import type { Adventure } from '$lib/types';
@@ -23,9 +22,6 @@
includeCollections: true
};
- let isShowingCreateModal: boolean = false;
- let newType: string = '';
-
let resultsPerPage: number = 25;
let count = data.props.count || 0;
@@ -95,31 +91,31 @@
}
}
- let adventureToEdit: Adventure;
- let isEditModalOpen: boolean = false;
+ let adventureToEdit: Adventure | null = null;
+ let isAdventureModalOpen: boolean = false;
function deleteAdventure(event: CustomEvent) {
adventures = adventures.filter((adventure) => adventure.id !== event.detail);
}
- function createAdventure(event: CustomEvent) {
- adventures = [event.detail, ...adventures];
- isShowingCreateModal = false;
+ // function that save changes to an existing adventure or creates a new one if it doesn't exist
+ function saveOrCreate(event: CustomEvent) {
+ if (adventures.find((adventure) => adventure.id === event.detail.id)) {
+ adventures = adventures.map((adventure) => {
+ if (adventure.id === event.detail.id) {
+ return event.detail;
+ }
+ return adventure;
+ });
+ } else {
+ adventures = [event.detail, ...adventures];
+ }
+ isAdventureModalOpen = false;
}
function editAdventure(event: CustomEvent) {
adventureToEdit = event.detail;
- isEditModalOpen = true;
- }
-
- function saveEdit(event: CustomEvent) {
- adventures = adventures.map((adventure) => {
- if (adventure.id === event.detail.id) {
- return event.detail;
- }
- return adventure;
- });
- isEditModalOpen = false;
+ isAdventureModalOpen = true;
}
let sidebarOpen = false;
@@ -129,19 +125,11 @@
}
-{#if isShowingCreateModal}
- (isShowingCreateModal = false)}
- />
-{/if}
-
-{#if isEditModalOpen}
- (isEditModalOpen = false)}
- on:saveEdit={saveEdit}
+ on:close={() => (isAdventureModalOpen = false)}
+ on:save={saveOrCreate}
/>
{/if}
@@ -160,21 +148,13 @@
-
+