diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index 00294996..7ac68427 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 @@ def image_display(self, obj): 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 !== ''} - Adventure Image + {#if adventure.images && adventure.images.length > 0} + {:else} + No image available + 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 535337ab..53d76f26 100644 --- a/frontend/src/lib/components/CollectionCard.svelte +++ b/frontend/src/lib/components/CollectionCard.svelte @@ -111,50 +111,51 @@

{/if}
- + {/if}
diff --git a/frontend/src/lib/components/EditAdventure.svelte b/frontend/src/lib/components/EditAdventure.svelte deleted file mode 100644 index 6654fba4..00000000 --- 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 4f114f73..00000000 --- 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 b00cf354..2a299dd7 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 ed0e38a1..829a890a 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 5882cc5f..c4f9cefa 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 @@ - +