1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-05 13:15:18 +02:00

feat: Implement location details page with server-side loading and deletion functionality

- Added +page.server.ts to handle server-side loading of additional location info.
- Created +page.svelte for displaying location details, including images, visits, and maps.
- Integrated GPX file handling and rendering on the map.
- Updated map route to link to locations instead of adventures.
- Refactored profile and search routes to use LocationCard instead of AdventureCard.
This commit is contained in:
Sean Morley 2025-06-23 22:29:37 -04:00
parent 8a5d7665df
commit 30c1e2deb6
33 changed files with 966 additions and 934 deletions

View file

@ -156,9 +156,17 @@
</footer> </footer>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"
></script>
<!-- jQuery (optional, used here for legacy script) --> <!-- jQuery (optional, used here for legacy script) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK"
crossorigin="anonymous"
></script>
<script> <script>
const error_response = (data) => { const error_response = (data) => {

View file

@ -31,7 +31,7 @@
section: 'main' section: 'main'
}, },
{ {
path: '/adventures', path: '/locations',
icon: MapMarker, icon: MapMarker,
label: 'locations.my_locations', label: 'locations.my_locations',
section: 'main' section: 'main'

View file

@ -24,7 +24,7 @@
import Filter from '~icons/mdi/filter-variant'; import Filter from '~icons/mdi/filter-variant';
// Component imports // Component imports
import AdventureCard from './AdventureCard.svelte'; import AdventureCard from './LocationCard.svelte';
import TransportationCard from './TransportationCard.svelte'; import TransportationCard from './TransportationCard.svelte';
import LodgingCard from './LodgingCard.svelte'; import LodgingCard from './LodgingCard.svelte';
import NoteCard from './NoteCard.svelte'; import NoteCard from './NoteCard.svelte';

View file

@ -228,7 +228,7 @@
<!-- Header Section --> <!-- Header Section -->
<div class="space-y-3"> <div class="space-y-3">
<button <button
on:click={() => goto(`/adventures/${adventure.id}`)} on:click={() => goto(`/locations/${adventure.id}`)}
class="text-xl font-bold text-left hover:text-primary transition-colors duration-200 line-clamp-2 group-hover:underline" class="text-xl font-bold text-left hover:text-primary transition-colors duration-200 line-clamp-2 group-hover:underline"
> >
{adventure.name} {adventure.name}
@ -274,7 +274,7 @@
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<button <button
class="btn btn-neutral btn-sm flex-1 mr-2" class="btn btn-neutral btn-sm flex-1 mr-2"
on:click={() => goto(`/adventures/${adventure.id}`)} on:click={() => goto(`/locations/${adventure.id}`)}
> >
<Launch class="w-4 h-4" /> <Launch class="w-4 h-4" />
{$t('adventures.open_details')} {$t('adventures.open_details')}

View file

@ -4,7 +4,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import AdventureCard from './AdventureCard.svelte'; import AdventureCard from './LocationCard.svelte';
let modal: HTMLDialogElement; let modal: HTMLDialogElement;
// Icons - following the worldtravel pattern // Icons - following the worldtravel pattern
@ -252,7 +252,7 @@
</div> </div>
{#if searchQuery || filterOption !== 'all'} {#if searchQuery || filterOption !== 'all'}
<h3 class="text-xl font-semibold text-base-content/70 mb-2"> <h3 class="text-xl font-semibold text-base-content/70 mb-2">
{$t('adventures.no_adventures_found')} {$t('adventures.no_locations_found')}
</h3> </h3>
<p class="text-base-content/50 text-center max-w-md mb-6"> <p class="text-base-content/50 text-center max-w-md mb-6">
{$t('collection.try_different_search')} {$t('collection.try_different_search')}

View file

@ -249,7 +249,7 @@
formData.append('name', attachmentName); formData.append('name', attachmentName);
try { try {
const res = await fetch('/adventures?/attachment', { const res = await fetch('/locations?/attachment', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
@ -326,9 +326,9 @@
async function uploadImage(file: File) { async function uploadImage(file: File) {
let formData = new FormData(); let formData = new FormData();
formData.append('image', file); formData.append('image', file);
formData.append('adventure', adventure.id); formData.append('location', adventure.id);
let res = await fetch(`/adventures?/image`, { let res = await fetch(`/locations?/image`, {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
@ -383,8 +383,8 @@
wikiImageError = ''; wikiImageError = '';
let formData = new FormData(); let formData = new FormData();
formData.append('image', file); formData.append('image', file);
formData.append('adventure', adventure.id); formData.append('location', adventure.id);
let res2 = await fetch(`/adventures?/image`, { let res2 = await fetch(`/locations?/image`, {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
@ -922,12 +922,12 @@
<p class=" font-semibold">{$t('adventures.share_location')}</p> <p class=" font-semibold">{$t('adventures.share_location')}</p>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="text-card-foreground font-mono"> <p class="text-card-foreground font-mono">
{window.location.origin}/adventures/{adventure.id} {window.location.origin}/locations/{adventure.id}
</p> </p>
<button <button
type="button" type="button"
on:click={() => { on:click={() => {
navigator.clipboard.writeText(`${window.location.origin}/adventures/${adventure.id}`); navigator.clipboard.writeText(`${window.location.origin}/locations/${adventure.id}`);
}} }}
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2" class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2"
> >

View file

@ -103,7 +103,7 @@
// Navigation items for better organization // Navigation items for better organization
const navigationItems = [ const navigationItems = [
{ path: '/adventures', icon: MapMarker, label: 'locations.locations' }, { path: '/locations', icon: MapMarker, label: 'locations.locations' },
{ path: '/collections', icon: FormatListBulletedSquare, label: 'navbar.collections' }, { path: '/collections', icon: FormatListBulletedSquare, label: 'navbar.collections' },
{ path: '/worldtravel', icon: Earth, label: 'navbar.worldtravel' }, { path: '/worldtravel', icon: Earth, label: 'navbar.worldtravel' },
{ path: '/map', icon: Map, label: 'navbar.map' }, { path: '/map', icon: Map, label: 'navbar.map' },

View file

@ -12,7 +12,7 @@
<img src={Lost} alt="Lost" class="w-1/2" /> <img src={Lost} alt="Lost" class="w-1/2" />
</div> </div>
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl"> <h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
{$t('adventures.no_adventures_found')} {$t('adventures.no_locations_found')}
</h1> </h1>
{#if !error} {#if !error}
<p class="mt-4 text-muted-foreground"> <p class="mt-4 text-muted-foreground">

View file

@ -249,7 +249,8 @@
"share_location": "Teilen Sie diesen Ort!", "share_location": "Teilen Sie diesen Ort!",
"visit_calendar": "Besuchen Sie den Kalender", "visit_calendar": "Besuchen Sie den Kalender",
"wiki_location_desc": "Zieht Auszug aus dem Wikipedia -Artikel, der dem Namen des Standorts entspricht.", "wiki_location_desc": "Zieht Auszug aus dem Wikipedia -Artikel, der dem Namen des Standorts entspricht.",
"will_be_marked_location": "wird als besucht markiert, sobald der Standort gespeichert ist." "will_be_marked_location": "wird als besucht markiert, sobald der Standort gespeichert ist.",
"no_locations_found": "Keine Standorte gefunden"
}, },
"home": { "home": {
"desc_1": "Entdecken, planen und erkunden Sie mühelos", "desc_1": "Entdecken, planen und erkunden Sie mühelos",

View file

@ -226,6 +226,7 @@
"adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!", "adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!",
"collection_contents": "Collection Contents", "collection_contents": "Collection Contents",
"no_adventures_found": "No adventures found", "no_adventures_found": "No adventures found",
"no_locations_found": "No locations found",
"no_adventures_message": "Start documenting your adventures and planning new ones. Every journey has a story worth telling.", "no_adventures_message": "Start documenting your adventures and planning new ones. Every journey has a story worth telling.",
"mark_visited": "Mark Visited", "mark_visited": "Mark Visited",
"error_updating_regions": "Error updating regions", "error_updating_regions": "Error updating regions",

View file

@ -301,7 +301,8 @@
"share_location": "¡Comparte esta ubicación!", "share_location": "¡Comparte esta ubicación!",
"visit_calendar": "Visitar el calendario", "visit_calendar": "Visitar el calendario",
"wiki_location_desc": "Extrae extracto del artículo de Wikipedia que coincide con el nombre de la ubicación.", "wiki_location_desc": "Extrae extracto del artículo de Wikipedia que coincide con el nombre de la ubicación.",
"will_be_marked_location": "se marcará según lo visitado una vez que se guarde la ubicación." "will_be_marked_location": "se marcará según lo visitado una vez que se guarde la ubicación.",
"no_locations_found": "No se encontraron ubicaciones"
}, },
"worldtravel": { "worldtravel": {
"all": "Todo", "all": "Todo",

View file

@ -249,7 +249,8 @@
"share_location": "Partagez cet emplacement!", "share_location": "Partagez cet emplacement!",
"visit_calendar": "Visiter le calendrier", "visit_calendar": "Visiter le calendrier",
"wiki_location_desc": "Tire un extrait de l'article de Wikipedia correspondant au nom de l'emplacement.", "wiki_location_desc": "Tire un extrait de l'article de Wikipedia correspondant au nom de l'emplacement.",
"will_be_marked_location": "sera marqué comme visité une fois l'emplacement enregistré." "will_be_marked_location": "sera marqué comme visité une fois l'emplacement enregistré.",
"no_locations_found": "Aucun emplacement trouvé"
}, },
"home": { "home": {
"desc_1": "Découvrez, planifiez et explorez en toute simplicité", "desc_1": "Découvrez, planifiez et explorez en toute simplicité",

View file

@ -249,7 +249,8 @@
"share_location": "Condividi questa posizione!", "share_location": "Condividi questa posizione!",
"visit_calendar": "Visita il calendario", "visit_calendar": "Visita il calendario",
"wiki_location_desc": "Estratto dall'articolo di Wikipedia che corrisponde al nome della posizione.", "wiki_location_desc": "Estratto dall'articolo di Wikipedia che corrisponde al nome della posizione.",
"will_be_marked_location": "sarà contrassegnato come visitato una volta salvata la posizione." "will_be_marked_location": "sarà contrassegnato come visitato una volta salvata la posizione.",
"no_locations_found": "Nessuna posizione trovata"
}, },
"home": { "home": {
"desc_1": "Scopri, pianifica ed esplora con facilità", "desc_1": "Scopri, pianifica ed esplora con facilità",

View file

@ -249,7 +249,8 @@
"share_location": "이 위치를 공유하십시오!", "share_location": "이 위치를 공유하십시오!",
"visit_calendar": "캘린더를 방문하십시오", "visit_calendar": "캘린더를 방문하십시오",
"wiki_location_desc": "위치 이름과 일치하는 Wikipedia 기사에서 발췌 한 내용을 가져옵니다.", "wiki_location_desc": "위치 이름과 일치하는 Wikipedia 기사에서 발췌 한 내용을 가져옵니다.",
"will_be_marked_location": "위치가 저장되면 방문한대로 표시됩니다." "will_be_marked_location": "위치가 저장되면 방문한대로 표시됩니다.",
"no_locations_found": "발견 된 위치는 없습니다"
}, },
"auth": { "auth": {
"confirm_password": "비밀번호 확인", "confirm_password": "비밀번호 확인",

View file

@ -249,7 +249,8 @@
"share_location": "Deel deze locatie!", "share_location": "Deel deze locatie!",
"visit_calendar": "Bezoek de agenda", "visit_calendar": "Bezoek de agenda",
"wiki_location_desc": "Haalt fragment uit het Wikipedia -artikel dat overeenkomt met de naam van de locatie.", "wiki_location_desc": "Haalt fragment uit het Wikipedia -artikel dat overeenkomt met de naam van de locatie.",
"will_be_marked_location": "wordt gemarkeerd als bezocht zodra de locatie is opgeslagen." "will_be_marked_location": "wordt gemarkeerd als bezocht zodra de locatie is opgeslagen.",
"no_locations_found": "Geen locaties gevonden"
}, },
"home": { "home": {
"desc_1": "Ontdek, plan en verken met gemak", "desc_1": "Ontdek, plan en verken met gemak",

View file

@ -301,7 +301,8 @@
"share_location": "Del dette stedet!", "share_location": "Del dette stedet!",
"visit_calendar": "Besøk kalenderen", "visit_calendar": "Besøk kalenderen",
"wiki_location_desc": "Trekker utdrag fra Wikipedia -artikkelen som samsvarer med navnet på stedet.", "wiki_location_desc": "Trekker utdrag fra Wikipedia -artikkelen som samsvarer med navnet på stedet.",
"will_be_marked_location": "vil bli merket som besøkt når stedet er lagret." "will_be_marked_location": "vil bli merket som besøkt når stedet er lagret.",
"no_locations_found": "Ingen steder funnet"
}, },
"worldtravel": { "worldtravel": {
"country_list": "Liste over land", "country_list": "Liste over land",

View file

@ -301,7 +301,8 @@
"share_location": "Udostępnij tę lokalizację!", "share_location": "Udostępnij tę lokalizację!",
"visit_calendar": "Odwiedź kalendarz", "visit_calendar": "Odwiedź kalendarz",
"wiki_location_desc": "Wyciąga fragment artykułu Wikipedii pasujący do nazwy lokalizacji.", "wiki_location_desc": "Wyciąga fragment artykułu Wikipedii pasujący do nazwy lokalizacji.",
"will_be_marked_location": "zostanie oznaczone jako odwiedzone po zapisaniu lokalizacji." "will_be_marked_location": "zostanie oznaczone jako odwiedzone po zapisaniu lokalizacji.",
"no_locations_found": "Nie znaleziono żadnych lokalizacji"
}, },
"worldtravel": { "worldtravel": {
"country_list": "Lista krajów", "country_list": "Lista krajów",

View file

@ -301,7 +301,8 @@
"share_location": "Поделитесь этим расположением!", "share_location": "Поделитесь этим расположением!",
"visit_calendar": "Посетите календарь", "visit_calendar": "Посетите календарь",
"wiki_location_desc": "Вытягивает отрывок из статьи Википедии, соответствующей названию места.", "wiki_location_desc": "Вытягивает отрывок из статьи Википедии, соответствующей названию места.",
"will_be_marked_location": "будет отмечен по посещению после сохранения местоположения." "will_be_marked_location": "будет отмечен по посещению после сохранения местоположения.",
"no_locations_found": "Никаких мест не найдено"
}, },
"worldtravel": { "worldtravel": {
"country_list": "Список стран", "country_list": "Список стран",

View file

@ -249,7 +249,8 @@
"share_location": "Dela den här platsen!", "share_location": "Dela den här platsen!",
"visit_calendar": "Besök kalendern", "visit_calendar": "Besök kalendern",
"wiki_location_desc": "Drar utdrag från Wikipedia -artikeln som matchar namnet på platsen.", "wiki_location_desc": "Drar utdrag från Wikipedia -artikeln som matchar namnet på platsen.",
"will_be_marked_location": "kommer att markeras som besöks när platsen har sparats." "will_be_marked_location": "kommer att markeras som besöks när platsen har sparats.",
"no_locations_found": "Inga platser hittades"
}, },
"home": { "home": {
"desc_1": "Upptäck, planera och utforska med lätthet", "desc_1": "Upptäck, planera och utforska med lätthet",

View file

@ -301,7 +301,8 @@
"share_location": "分享这个位置!", "share_location": "分享这个位置!",
"visit_calendar": "访问日历", "visit_calendar": "访问日历",
"wiki_location_desc": "从Wikipedia文章中提取摘录符合该位置的名称。", "wiki_location_desc": "从Wikipedia文章中提取摘录符合该位置的名称。",
"will_be_marked_location": "保存位置后,将被标记为访问。" "will_be_marked_location": "保存位置后,将被标记为访问。",
"no_locations_found": "找不到位置"
}, },
"auth": { "auth": {
"forgot_password": "忘记密码?", "forgot_password": "忘记密码?",

View file

@ -102,7 +102,7 @@
<div class="flex flex-col sm:flex-row gap-4 pt-4"> <div class="flex flex-col sm:flex-row gap-4 pt-4">
{#if data.user} {#if data.user}
<button <button
on:click={() => goto('/adventures')} on:click={() => goto('/locations')}
class="btn btn-primary btn-lg gap-3 shadow-lg hover:shadow-xl transition-all duration-300 group" class="btn btn-primary btn-lg gap-3 shadow-lg hover:shadow-xl transition-all duration-300 group"
> >
<PlayIcon class="w-5 h-5 group-hover:scale-110 transition-transform" /> <PlayIcon class="w-5 h-5 group-hover:scale-110 transition-transform" />

View file

@ -1,99 +1,5 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { Location } from '$lib/types';
import type { Actions } from '@sveltejs/kit'; export const load = (async (_event) => {
import { fetchCSRFToken } from '$lib/index.server'; return redirect(301, '/locations');
}) satisfies import('./$types').PageServerLoad;
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => {
if (!event.locals.user) {
return redirect(302, '/login');
} else {
let count = 0;
let adventures: Location[] = [];
let typeString = event.url.searchParams.get('types');
// If no type is specified, default to 'all'
if (!typeString) {
typeString = 'all';
}
const include_collections = event.url.searchParams.get('include_collections') || 'false';
const order_by = event.url.searchParams.get('order_by') || 'updated_at';
const order_direction = event.url.searchParams.get('order_direction') || 'asc';
const page = event.url.searchParams.get('page') || '1';
const is_visited = event.url.searchParams.get('is_visited') || 'all';
let initialFetch = await event.fetch(
`${serverEndpoint}/api/locations/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`,
{
headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}`
},
credentials: 'include'
}
);
if (!initialFetch.ok) {
let error_message = await initialFetch.json();
console.error(error_message);
console.error('Failed to fetch visited adventures');
return redirect(302, '/login');
} else {
let res = await initialFetch.json();
let visited = res.results as Location[];
count = res.count;
adventures = [...adventures, ...visited];
}
return {
props: {
adventures,
count
}
};
}
}) satisfies PageServerLoad;
export const actions: Actions = {
image: async (event) => {
let formData = await event.request.formData();
let csrfToken = await fetchCSRFToken();
let sessionId = event.cookies.get('sessionid');
let res = await fetch(`${serverEndpoint}/api/images/`, {
method: 'POST',
headers: {
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`,
'X-CSRFToken': csrfToken,
Referer: event.url.origin // Include Referer header
},
body: formData
});
let data = await res.json();
return data;
},
attachment: async (event) => {
let formData = await event.request.formData();
let csrfToken = await fetchCSRFToken();
let sessionId = event.cookies.get('sessionid');
let res = await fetch(`${serverEndpoint}/api/attachments/`, {
method: 'POST',
headers: {
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`,
'X-CSRFToken': csrfToken,
Referer: event.url.origin // Include Referer header
},
body: formData
});
let data = await res.json();
console.log(res);
console.log(data);
return data;
}
};

View file

@ -1,76 +1,7 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import { redirect } from '@sveltejs/kit';
import type { AdditionalLocation, Location, Collection } from '$lib/types';
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => { export const load = (async (event) => {
const id = event.params as { id: string }; const id = event.params as { id: string };
let request = await fetch(`${endpoint}/api/locations/${id.id}/additional-info/`, { return redirect(301, `/locations/${id.id}`);
headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}`
},
credentials: 'include'
});
if (!request.ok) {
console.error('Failed to fetch adventure ' + id.id);
return {
props: {
adventure: null
}
};
} else {
let adventure = (await request.json()) as AdditionalLocation;
return {
props: {
adventure
}
};
}
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
import { redirect, type Actions } from '@sveltejs/kit';
import { fetchCSRFToken } from '$lib/index.server';
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const actions: Actions = {
delete: async (event) => {
const id = event.params as { id: string };
const adventureId = id.id;
if (!event.locals.user) {
return redirect(302, '/login');
}
if (!adventureId) {
return {
status: 400,
error: new Error('Bad request')
};
}
let csrfToken = await fetchCSRFToken();
let res = await fetch(`${serverEndpoint}/api/locations/${event.params.id}`, {
method: 'DELETE',
headers: {
Referer: event.url.origin, // Include Referer header
Cookie: `sessionid=${event.cookies.get('sessionid')};
csrftoken=${csrfToken}`,
'X-CSRFToken': csrfToken
},
credentials: 'include'
});
console.log(res);
if (!res.ok) {
return {
status: res.status,
error: new Error('Failed to delete adventure')
};
} else {
return {
status: 204
};
}
}
};

View file

@ -26,7 +26,7 @@
return marked(markdown); return marked(markdown);
}; };
let adventures = data.props.adventures; let locations = data.props.adventures;
let allDates = data.props.dates; let allDates = data.props.dates;
let filteredDates = [...allDates]; let filteredDates = [...allDates];
@ -215,7 +215,7 @@
</div> </div>
<div class="stat py-2 px-4"> <div class="stat py-2 px-4">
<div class="stat-title text-xs">{$t('locations.locations')}</div> <div class="stat-title text-xs">{$t('locations.locations')}</div>
<div class="stat-value text-lg text-secondary">{adventures.length}</div> <div class="stat-value text-lg text-secondary">{locations.length}</div>
</div> </div>
</div> </div>
</div> </div>
@ -229,7 +229,7 @@
/> />
<input <input
type="text" type="text"
placeholder="Search adventures or locations..." placeholder={$t('adventures.search_for_location')}
class="input input-bordered w-full pl-10 pr-10 bg-base-100/80" class="input input-bordered w-full pl-10 pr-10 bg-base-100/80"
bind:value={searchFilter} bind:value={searchFilter}
/> />
@ -299,7 +299,7 @@
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="stat p-0"> <div class="stat p-0">
<div class="stat-title text-xs">{$t('locations.locations')}</div> <div class="stat-title text-xs">{$t('locations.locations')}</div>
<div class="stat-value text-lg text-primary">{adventures.length}</div> <div class="stat-value text-lg text-primary">{locations.length}</div>
</div> </div>
</div> </div>
@ -418,7 +418,7 @@
{#if selectedEvent.extendedProps.adventureId} {#if selectedEvent.extendedProps.adventureId}
<a <a
href={`/adventures/${selectedEvent.extendedProps.adventureId}`} href={`/locations/${selectedEvent.extendedProps.adventureId}`}
class="btn btn-neutral btn-block mt-4" class="btn btn-neutral btn-block mt-4"
> >
{$t('map.view_details')} {$t('map.view_details')}

View file

@ -15,8 +15,8 @@
import DayGrid from '@event-calendar/day-grid'; import DayGrid from '@event-calendar/day-grid';
import Plus from '~icons/mdi/plus'; import Plus from '~icons/mdi/plus';
import AdventureCard from '$lib/components/AdventureCard.svelte'; import AdventureCard from '$lib/components/LocationCard.svelte';
import AdventureLink from '$lib/components/AdventureLink.svelte'; import AdventureLink from '$lib/components/LocationLink.svelte';
import { MapLibre, Marker, Popup, LineLayer, GeoJSON } from 'svelte-maplibre'; import { MapLibre, Marker, Popup, LineLayer, GeoJSON } from 'svelte-maplibre';
import TransportationCard from '$lib/components/TransportationCard.svelte'; import TransportationCard from '$lib/components/TransportationCard.svelte';
import NoteCard from '$lib/components/NoteCard.svelte'; import NoteCard from '$lib/components/NoteCard.svelte';
@ -39,7 +39,7 @@
import ChecklistCard from '$lib/components/ChecklistCard.svelte'; import ChecklistCard from '$lib/components/ChecklistCard.svelte';
import ChecklistModal from '$lib/components/ChecklistModal.svelte'; import ChecklistModal from '$lib/components/ChecklistModal.svelte';
import AdventureModal from '$lib/components/AdventureModal.svelte'; import AdventureModal from '$lib/components/LocationModal.svelte';
import TransportationModal from '$lib/components/TransportationModal.svelte'; import TransportationModal from '$lib/components/TransportationModal.svelte';
import CardCarousel from '$lib/components/CardCarousel.svelte'; import CardCarousel from '$lib/components/CardCarousel.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -1323,7 +1323,7 @@
{/if} {/if}
<button <button
class="btn btn-neutral btn-wide btn-sm mt-4" class="btn btn-neutral btn-wide btn-sm mt-4"
on:click={() => goto(`/adventures/${adventure.id}`)} on:click={() => goto(`/locations/${adventure.id}`)}
>{$t('map.view_details')}</button >{$t('map.view_details')}</button
> >
</Popup> </Popup>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import AdventureCard from '$lib/components/AdventureCard.svelte'; import AdventureCard from '$lib/components/LocationCard.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -66,7 +66,7 @@
<!-- Quick Action --> <!-- Quick Action -->
<div class="flex flex-col sm:flex-row gap-3"> <div class="flex flex-col sm:flex-row gap-3">
<a <a
href="/adventures" href="/locations"
class="btn btn-primary btn-lg gap-2 shadow-lg hover:shadow-xl transition-all duration-300" class="btn btn-primary btn-lg gap-2 shadow-lg hover:shadow-xl transition-all duration-300"
> >
<Plus class="w-5 h-5" /> <Plus class="w-5 h-5" />
@ -169,7 +169,7 @@
<p class="text-base-content/60">{$t('home.latest_travel_experiences')}</p> <p class="text-base-content/60">{$t('home.latest_travel_experiences')}</p>
</div> </div>
</div> </div>
<a href="/adventures" class="btn btn-ghost gap-2"> <a href="/locations" class="btn btn-ghost gap-2">
{$t('dashboard.view_all')} {$t('dashboard.view_all')}
<span class="badge badge-primary">{stats.adventure_count}</span> <span class="badge badge-primary">{stats.adventure_count}</span>
</a> </a>
@ -208,7 +208,7 @@
<div class="flex flex-col sm:flex-row gap-4 justify-center"> <div class="flex flex-col sm:flex-row gap-4 justify-center">
<a <a
href="/adventures" href="/locations"
class="btn btn-primary btn-lg gap-2 shadow-lg hover:shadow-xl transition-all duration-300" class="btn btn-primary btn-lg gap-2 shadow-lg hover:shadow-xl transition-all duration-300"
> >
<Plus class="w-5 h-5" /> <Plus class="w-5 h-5" />

View file

@ -0,0 +1,99 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { Location } from '$lib/types';
import type { Actions } from '@sveltejs/kit';
import { fetchCSRFToken } from '$lib/index.server';
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => {
if (!event.locals.user) {
return redirect(302, '/login');
} else {
let count = 0;
let adventures: Location[] = [];
let typeString = event.url.searchParams.get('types');
// If no type is specified, default to 'all'
if (!typeString) {
typeString = 'all';
}
const include_collections = event.url.searchParams.get('include_collections') || 'false';
const order_by = event.url.searchParams.get('order_by') || 'updated_at';
const order_direction = event.url.searchParams.get('order_direction') || 'asc';
const page = event.url.searchParams.get('page') || '1';
const is_visited = event.url.searchParams.get('is_visited') || 'all';
let initialFetch = await event.fetch(
`${serverEndpoint}/api/locations/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`,
{
headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}`
},
credentials: 'include'
}
);
if (!initialFetch.ok) {
let error_message = await initialFetch.json();
console.error(error_message);
console.error('Failed to fetch visited adventures');
return redirect(302, '/login');
} else {
let res = await initialFetch.json();
let visited = res.results as Location[];
count = res.count;
adventures = [...adventures, ...visited];
}
return {
props: {
adventures,
count
}
};
}
}) satisfies PageServerLoad;
export const actions: Actions = {
image: async (event) => {
let formData = await event.request.formData();
let csrfToken = await fetchCSRFToken();
let sessionId = event.cookies.get('sessionid');
let res = await fetch(`${serverEndpoint}/api/images/`, {
method: 'POST',
headers: {
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`,
'X-CSRFToken': csrfToken,
Referer: event.url.origin // Include Referer header
},
body: formData
});
let data = await res.json();
return data;
},
attachment: async (event) => {
let formData = await event.request.formData();
let csrfToken = await fetchCSRFToken();
let sessionId = event.cookies.get('sessionid');
let res = await fetch(`${serverEndpoint}/api/attachments/`, {
method: 'POST',
headers: {
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`,
'X-CSRFToken': csrfToken,
Referer: event.url.origin // Include Referer header
},
body: formData
});
let data = await res.json();
console.log(res);
console.log(data);
return data;
}
};

View file

@ -2,8 +2,8 @@
import { enhance, deserialize } from '$app/forms'; import { enhance, deserialize } from '$app/forms';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import AdventureCard from '$lib/components/AdventureCard.svelte'; import AdventureCard from '$lib/components/LocationCard.svelte';
import AdventureModal from '$lib/components/AdventureModal.svelte'; import AdventureModal from '$lib/components/LocationModal.svelte';
import CategoryFilterDropdown from '$lib/components/CategoryFilterDropdown.svelte'; import CategoryFilterDropdown from '$lib/components/CategoryFilterDropdown.svelte';
import CategoryModal from '$lib/components/CategoryModal.svelte'; import CategoryModal from '$lib/components/CategoryModal.svelte';
import NotFound from '$lib/components/NotFound.svelte'; import NotFound from '$lib/components/NotFound.svelte';
@ -241,7 +241,7 @@
<Compass class="w-16 h-16 text-base-content/30" /> <Compass class="w-16 h-16 text-base-content/30" />
</div> </div>
<h3 class="text-xl font-semibold text-base-content/70 mb-2"> <h3 class="text-xl font-semibold text-base-content/70 mb-2">
{$t('adventures.no_adventures_found')} {$t('adventures.no_locations_found')}
</h3> </h3>
<p class="text-base-content/50 text-center max-w-md"> <p class="text-base-content/50 text-center max-w-md">
{$t('adventures.no_adventures_message')} {$t('adventures.no_adventures_message')}

View file

@ -0,0 +1,76 @@
import type { PageServerLoad } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { AdditionalLocation, Location, Collection } from '$lib/types';
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => {
const id = event.params as { id: string };
let request = await fetch(`${endpoint}/api/locations/${id.id}/additional-info/`, {
headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}`
},
credentials: 'include'
});
if (!request.ok) {
console.error('Failed to fetch adventure ' + id.id);
return {
props: {
adventure: null
}
};
} else {
let adventure = (await request.json()) as AdditionalLocation;
return {
props: {
adventure
}
};
}
}) satisfies PageServerLoad;
import { redirect, type Actions } from '@sveltejs/kit';
import { fetchCSRFToken } from '$lib/index.server';
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const actions: Actions = {
delete: async (event) => {
const id = event.params as { id: string };
const adventureId = id.id;
if (!event.locals.user) {
return redirect(302, '/login');
}
if (!adventureId) {
return {
status: 400,
error: new Error('Bad request')
};
}
let csrfToken = await fetchCSRFToken();
let res = await fetch(`${serverEndpoint}/api/locations/${event.params.id}`, {
method: 'DELETE',
headers: {
Referer: event.url.origin, // Include Referer header
Cookie: `sessionid=${event.cookies.get('sessionid')};
csrftoken=${csrfToken}`,
'X-CSRFToken': csrfToken
},
credentials: 'include'
});
console.log(res);
if (!res.ok) {
return {
status: res.status,
error: new Error('Failed to delete adventure')
};
} else {
return {
status: 204
};
}
}
};

View file

@ -16,7 +16,7 @@
import LightbulbOn from '~icons/mdi/lightbulb-on'; import LightbulbOn from '~icons/mdi/lightbulb-on';
import WeatherSunset from '~icons/mdi/weather-sunset'; import WeatherSunset from '~icons/mdi/weather-sunset';
import ClipboardList from '~icons/mdi/clipboard-list'; import ClipboardList from '~icons/mdi/clipboard-list';
import AdventureModal from '$lib/components/AdventureModal.svelte'; import AdventureModal from '$lib/components/LocationModal.svelte';
import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte'; import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte';
import AttachmentCard from '$lib/components/AttachmentCard.svelte'; import AttachmentCard from '$lib/components/AttachmentCard.svelte';
import { getBasemapUrl, isAllDay } from '$lib'; import { getBasemapUrl, isAllDay } from '$lib';

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import AdventureModal from '$lib/components/AdventureModal.svelte'; import AdventureModal from '$lib/components/LocationModal.svelte';
import { DefaultMarker, MapEvents, MapLibre, Popup, Marker } from 'svelte-maplibre'; import { DefaultMarker, MapEvents, MapLibre, Popup, Marker } from 'svelte-maplibre';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { Location, VisitedRegion } from '$lib/types.js'; import type { Location, VisitedRegion } from '$lib/types.js';
@ -268,7 +268,7 @@
{/if} {/if}
<button <button
class="btn btn-primary btn-sm gap-2" class="btn btn-primary btn-sm gap-2"
on:click={() => goto(`/adventures/${adventure.id}`)} on:click={() => goto(`/locations/${adventure.id}`)}
> >
<Eye class="w-4 h-4" /> <Eye class="w-4 h-4" />
{$t('map.view_details')} {$t('map.view_details')}

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
export let data; export let data;
import AdventureCard from '$lib/components/AdventureCard.svelte'; import AdventureCard from '$lib/components/LocationCard.svelte';
import CollectionCard from '$lib/components/CollectionCard.svelte'; import CollectionCard from '$lib/components/CollectionCard.svelte';
import type { Location, Collection, User } from '$lib/types.js'; import type { Location, Collection, User } from '$lib/types.js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import AdventureCard from '$lib/components/AdventureCard.svelte'; import AdventureCard from '$lib/components/LocationCard.svelte';
import RegionCard from '$lib/components/RegionCard.svelte'; import RegionCard from '$lib/components/RegionCard.svelte';
import CityCard from '$lib/components/CityCard.svelte'; import CityCard from '$lib/components/CityCard.svelte';
import CountryCard from '$lib/components/CountryCard.svelte'; import CountryCard from '$lib/components/CountryCard.svelte';