1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-24 15:29:36 +02:00

Markdown, New Transportation Modals/Card and Collection Improvements

This commit is contained in:
Sean Morley 2024-12-28 15:10:50 -05:00 committed by GitHub
commit bc6cf42b8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2944 additions and 1991 deletions

View file

@ -1,5 +1,5 @@
AdventureLog: Self-hostable travel tracker and trip planner.
Copyright (C) 2024 Sean Morley
Copyright (C) 2023-2025 Sean Morley
Contact: contact@seanmorley.com
This program is free software: you can redistribute it and/or modify

View file

@ -39,4 +39,4 @@ python manage.py download-countries
cat /code/adventurelog.txt
# Start gunicorn
gunicorn main.wsgi:application --bind 0.0.0.0:8000
gunicorn main.wsgi:application --bind 0.0.0.0:8000 --timeout 120 --workers 2

View file

@ -0,0 +1,33 @@
# Generated by Django 5.0.8 on 2024-12-19 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0014_alter_category_unique_together'),
]
operations = [
migrations.AddField(
model_name='transportation',
name='destination_latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='transportation',
name='destination_longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='transportation',
name='origin_latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='transportation',
name='origin_longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
]

View file

@ -167,6 +167,10 @@ class Transportation(models.Model):
end_date = models.DateTimeField(blank=True, null=True)
flight_number = models.CharField(max_length=100, blank=True, null=True)
from_location = models.CharField(max_length=200, blank=True, null=True)
origin_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
origin_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
destination_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
destination_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
to_location = models.CharField(max_length=200, blank=True, null=True)
is_public = models.BooleanField(default=False)
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)

View file

@ -170,7 +170,7 @@ class TransportationSerializer(CustomModelSerializer):
fields = [
'id', 'user_id', 'type', 'name', 'description', 'rating',
'link', 'date', 'flight_number', 'from_location', 'to_location',
'is_public', 'collection', 'created_at', 'updated_at', 'end_date'
'is_public', 'collection', 'created_at', 'updated_at', 'end_date', 'origin_latitude', 'origin_longitude', 'destination_latitude', 'destination_longitude'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']

View file

@ -41,7 +41,7 @@ export default defineConfig({
footer: {
message: "AdventureLog",
copyright: "Copyright © 2023-2024 Sean Morley",
copyright: "Copyright © 2023-2025 Sean Morley",
},
logo: "/adventurelog.png",

View file

@ -612,8 +612,8 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@ -1352,7 +1352,7 @@ snapshots:
mitt@3.0.1: {}
nanoid@3.3.7: {}
nanoid@3.3.8: {}
oniguruma-to-es@0.4.1:
dependencies:
@ -1366,7 +1366,7 @@ snapshots:
postcss@8.4.49:
dependencies:
nanoid: 3.3.7
nanoid: 3.3.8
picocolors: 1.1.1
source-map-js: 1.2.1

View file

@ -41,6 +41,7 @@
"dependencies": {
"@lukulent/svelte-umami": "^0.0.3",
"emoji-picker-element": "^1.26.0",
"marked": "^15.0.4",
"qrcode": "^1.5.4",
"svelte-i18n": "^4.0.1",
"svelte-maplibre": "^0.9.8"

View file

@ -14,6 +14,9 @@ importers:
emoji-picker-element:
specifier: ^1.26.0
version: 1.26.0
marked:
specifier: ^15.0.4
version: 15.0.4
qrcode:
specifier: ^1.5.4
version: 1.5.4
@ -899,8 +902,8 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
css-selector-tokenizer@0.8.0:
@ -1351,6 +1354,11 @@ packages:
resolution: {integrity: sha512-qOS1hn4d/pn2i0uva4S5Oz+fACzTkgBKq+NpwT/Tqzi4MSyzcWNtDELzLUSgWqHfNIkGCl5CZ/w7dtis+t4RCw==}
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
marked@15.0.4:
resolution: {integrity: sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw==}
engines: {node: '>= 18'}
hasBin: true
mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
@ -1432,8 +1440,8 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@ -2358,7 +2366,7 @@ snapshots:
'@jsdevtools/ez-spawn@3.0.4':
dependencies:
call-me-maybe: 1.0.2
cross-spawn: 7.0.3
cross-spawn: 7.0.6
string-argv: 0.3.2
type-detect: 4.0.8
@ -2819,7 +2827,7 @@ snapshots:
cookie@0.6.0: {}
cross-spawn@7.0.3:
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
@ -3000,7 +3008,7 @@ snapshots:
execa@5.1.1:
dependencies:
cross-spawn: 7.0.3
cross-spawn: 7.0.6
get-stream: 6.0.1
human-signals: 2.1.0
is-stream: 2.0.1
@ -3057,7 +3065,7 @@ snapshots:
foreground-child@3.2.1:
dependencies:
cross-spawn: 7.0.3
cross-spawn: 7.0.6
signal-exit: 4.1.0
fraction.js@4.3.7: {}
@ -3318,6 +3326,8 @@ snapshots:
tinyqueue: 2.0.3
vt-pbf: 3.1.3
marked@15.0.4: {}
mdn-data@2.0.30: {}
memoizee@0.4.17:
@ -3394,7 +3404,7 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.7: {}
nanoid@3.3.8: {}
next-tick@1.1.0: {}
@ -3548,13 +3558,13 @@ snapshots:
postcss@8.4.38:
dependencies:
nanoid: 3.3.7
nanoid: 3.3.8
picocolors: 1.0.1
source-map-js: 1.2.0
postcss@8.4.47:
dependencies:
nanoid: 3.3.7
nanoid: 3.3.8
picocolors: 1.1.0
source-map-js: 1.2.1

View file

@ -1,13 +1,14 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
let modal: HTMLDialogElement;
import { t } from 'svelte-i18n';
import { appVersion, copyrightYear, versionChangelog } from '$lib/config';
const dispatch = createEventDispatcher();
let modal: HTMLDialogElement;
onMount(() => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
modal = document.getElementById('about_modal') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
@ -24,56 +25,126 @@
}
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<dialog id="about_modal" class="modal backdrop-blur-md bg-opacity-70">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">
{$t('about.about')} AdventureLog<span class=" inline-block"
><img src="/favicon.png" alt="Map Logo" class="w-10 -mb-3 ml-2" /></span
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div
class="modal-box rounded-xl shadow-lg backdrop-blur-lg bg-white/80 dark:bg-gray-900/80 transition-transform duration-300 ease-out transform scale-100"
role="dialog"
on:keydown={handleKeydown}
tabindex="0"
>
<!-- Branding -->
<div class="text-center">
<h3
class="text-2xl font-extrabold text-gray-800 dark:text-white flex items-center justify-center"
>
</h3>
<p class="py-1">
AdventureLog <a
target="_blank"
rel="noopener noreferrer"
class="text-primary-500 underline"
href={versionChangelog}>{appVersion}</a
>
</p>
<p class="py-1">
© {copyrightYear}
<a
href="https://seanmorley.com"
target="_blank"
rel="noopener noreferrer"
class="text-primary-500 underline">Sean Morley</a
>
</p>
<p class="py-1">{$t('about.license')}</p>
<p class="py-1">
<a
href="https://github.com/seanmorley15/AdventureLog"
target="_blank"
rel="noopener noreferrer"
class="text-primary-500 underline">{$t('about.source_code')}</a
>
</p>
<p class="py-1">{$t('about.message')}</p>
<div class="divider"></div>
<h3 class="font-bold text-md">{$t('about.oss_attributions')}</h3>
<p class="py-1 mb-4">
{$t('about.nominatim_1')}
<a
target="_blank"
rel="noopener noreferrer"
class="text-primary-500 underline"
href="https://operations.osmfoundation.org/policies/nominatim/">OpenStreepMap</a
>. {$t('about.nominatim_2')}
<br />
{$t('about.other_attributions')}
</p>
{$t('about.about')} AdventureLog
<img src="/favicon.png" alt="Map Logo" class="w-12 h-12 ml-3 inline-block" />
</h3>
<p class="mt-2 text-gray-500 dark:text-gray-300 text-sm">
AdventureLog
<a
href={versionChangelog}
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:underline"
>
{appVersion}
</a>
</p>
</div>
<button class="btn btn-primary" on:click={close}>{$t('about.close')}</button>
<!-- Links and Details -->
<div class="mt-4 text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
© {copyrightYear}
<a
href="https://seanmorley.com"
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:underline"
>
Sean Morley
</a>
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">{$t('about.license')}</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
<a
href="https://github.com/seanmorley15/AdventureLog"
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:underline"
>
{$t('about.source_code')}
</a>
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">{$t('about.message')}</p>
</div>
<!-- Divider -->
<div class="my-6 border-t border-gray-200 dark:border-gray-700"></div>
<!-- OSS Acknowledgments -->
<div class="text-left">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white">
{$t('about.oss_attributions')}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{$t('about.nominatim_1')}
<a
href="https://operations.osmfoundation.org/policies/nominatim/"
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:underline"
>
OpenStreetMap
</a>
. {$t('about.nominatim_2')}
</p>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{$t('about.other_attributions')}</p>
</div>
<!-- Close Button -->
<div class="flex justify-center mt-6">
<button
class="px-6 py-2 text-sm font-medium text-white bg-primary rounded-full shadow-md hover:shadow-lg hover:scale-105 transform transition"
on:click={close}
>
{$t('about.close')}
</button>
</div>
</div>
</dialog>
<style>
.modal {
display: grid;
place-items: center;
background: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.3s ease-in-out;
}
.modal-box {
max-width: 600px;
padding: 2rem;
animation: slideUp 0.4s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(20%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>

View file

@ -41,6 +41,24 @@
}
}
let unlinked: boolean = false;
// Reactive block to update `unlinked` when dependencies change
$: {
if (collection && collection?.start_date && collection.end_date) {
unlinked = adventure.visits.every((visit) => {
// Check if visit dates exist
if (!visit.start_date || !visit.end_date) return true; // Consider "unlinked" for incomplete visit data
// Check if collection dates are completely outside this visit's range
const isBeforeVisit = collection.end_date && collection.end_date < visit.start_date;
const isAfterVisit = collection.start_date && collection.start_date > visit.end_date;
return isBeforeVisit || isAfterVisit;
});
}
}
async function deleteAdventure() {
let res = await fetch(`/adventures/${adventure.id}?/delete`, {
method: 'POST',
@ -140,6 +158,9 @@
{adventure.is_public ? $t('adventures.public') : $t('adventures.private')}
</div>
</div>
{#if unlinked}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if}
{#if adventure.location && adventure.location !== ''}
<div class="inline-flex items-center">
<MapMarker class="w-5 h-5 mr-1" />

View file

@ -32,6 +32,7 @@
import { appVersion } from '$lib/config';
import CategoryDropdown from './CategoryDropdown.svelte';
import { findFirstValue } from '$lib';
import MarkdownEditor from './MarkdownEditor.svelte';
let wikiError: string = '';
@ -577,15 +578,10 @@
</div>
<div>
<label for="description">{$t('adventures.description')}</label><br />
<textarea
id="description"
name="description"
bind:value={adventure.description}
class="textarea textarea-bordered w-full h-32"
></textarea>
<MarkdownEditor bind:text={adventure.description} />
<div class="mt-2">
<div class="tooltip tooltip-right" data-tip={$t('adventures.wiki_desc')}>
<button type="button" class="btn btn-neutral" on:click={generateDesc}
<button type="button" class="btn btn-neutral mt-2" on:click={generateDesc}
>{$t('adventures.generate_desc')}</button
>
</div>
@ -687,7 +683,7 @@
<div>
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
standardControls
>
<!-- MapEvents gives you access to map events even from other components inside the map,

View file

@ -8,11 +8,32 @@
import Launch from '~icons/mdi/launch';
import TrashCan from '~icons/mdi/trash-can';
import Calendar from '~icons/mdi/calendar';
import DeleteWarning from './DeleteWarning.svelte';
export let checklist: Checklist;
export let user: User | null = null;
export let collection: Collection | null = null;
let isWarningModalOpen: boolean = false;
let unlinked: boolean = false;
$: {
if (collection?.start_date && collection.end_date) {
const startOutsideRange =
checklist.date &&
collection.start_date < checklist.date &&
collection.end_date < checklist.date;
const endOutsideRange =
checklist.date &&
collection.start_date > checklist.date &&
collection.end_date > checklist.date;
unlinked = !!(startOutsideRange || endOutsideRange || !checklist.date);
}
}
function editChecklist() {
dispatch('edit', checklist);
}
@ -23,6 +44,7 @@
});
if (res.ok) {
addToast('success', $t('checklist.checklist_deleted'));
isWarningModalOpen = false;
dispatch('delete', checklist.id);
} else {
addToast($t('checklist.checklist_delete_error'), 'error');
@ -30,6 +52,17 @@
}
</script>
{#if isWarningModalOpen}
<DeleteWarning
title={$t('adventures.delete_checklist')}
button_text="Delete"
description={$t('adventures.checklist_delete_confirm')}
is_warning={false}
on:close={() => (isWarningModalOpen = false)}
on:confirm={deleteChecklist}
/>
{/if}
<div
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl overflow-hidden"
>
@ -46,6 +79,9 @@
{checklist.items.length > 1 ? $t('checklist.items') : $t('checklist.item')}
</p>
{/if}
{#if unlinked}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if}
{#if checklist.date && checklist.date !== ''}
<div class="inline-flex items-center">
<Calendar class="w-5 h-5 mr-1" />
@ -56,12 +92,12 @@
<button class="btn btn-neutral-200 mb-2" on:click={editChecklist}>
<Launch class="w-6 h-6" />{$t('notes.open')}
</button>
{#if checklist.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))}
{#if checklist.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
<button
id="delete_adventure"
data-umami-event="Delete Checklist"
class="btn btn-warning"
on:click={deleteChecklist}><TrashCan class="w-6 h-6" /></button
on:click={() => (isWarningModalOpen = true)}><TrashCan class="w-6 h-6" /></button
>
{/if}
</div>

View file

@ -12,10 +12,16 @@
let items: ChecklistItem[] = [];
let constrainDates: boolean = false;
items = checklist?.items || [];
let warning: string | null = '';
let isReadOnly =
!(checklist && user?.uuid == checklist?.user_id) &&
!(user && collection && collection.shared_with && collection.shared_with.includes(user.uuid)) &&
!!checklist;
let newStatus: boolean = false;
let newItem: string = '';
@ -56,8 +62,6 @@
is_public: collection.is_public
};
let initialName: string = checklist?.name || '';
onMount(() => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
@ -127,86 +131,130 @@
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg mb-2">{$t('checklist.checklist_editor')}</h3>
{#if initialName}
<p class="font-semibold text-md mb-2">{$t('checklist.editing_checklist')} {initialName}</p>
{/if}
{#if (checklist && user?.uuid == checklist?.user_id) || (user && collection && collection.shared_with.includes(user.uuid)) || !checklist}
<form on:submit|preventDefault>
<div class="form-control mb-2">
<label for="name">{$t('adventures.name')}</label>
<input
type="text"
id="name"
class="input input-bordered w-full max-w-xs"
bind:value={newChecklist.name}
/>
</div>
<div class="form-control mb-2">
<label for="content">{$t('adventures.date')}</label>
<input
type="date"
id="date"
name="date"
min={collection.start_date || ''}
max={collection.end_date || ''}
bind:value={newChecklist.date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="form-control mb-2 flex flex-row">
<input type="checkbox" bind:checked={newStatus} class="checkbox mt-4 mr-2" />
<input
type="text"
id="new_item"
placeholder={$t('checklist.new_item')}
name="new_item"
bind:value={newItem}
class="input input-bordered w-full max-w-xs mt-1"
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addItem();
}
}}
/>
<button
type="button"
class="btn btn-sm btn-primary absolute right-0 mt-2.5 mr-4"
on:click={addItem}
>
{$t('adventures.add')}
</button>
</div>
{#if items.length > 0}
<div class="divider"></div>
<h2 class=" text-xl font-semibold mb-4 -mt-3">{$t('checklist.items')}</h2>
{/if}
{#each items as item, i}
<div class="form-control mb-2 flex flex-row">
<input type="checkbox" bind:checked={item.is_checked} class="checkbox mt-4 mr-2" />
<input
type="text"
id="item_{i}"
name="item_{i}"
bind:value={item.name}
class="input input-bordered w-full max-w-xs mt-1"
/>
<button
type="button"
class="btn btn-sm btn-error absolute right-0 mt-2.5 mr-4"
on:click={() => removeItem(i)}
>
{$t('adventures.remove')}
</button>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-2xl">
{#if checklist?.id}
<p class="font-semibold text-md mb-2">
{$t('checklist.checklist_editor')}
</p>
{:else}
{$t('checklist.new_checklist')}
{/if}
</h3>
<div class="modal-action items-center">
<form method="post" style="width: 100%;" on:submit|preventDefault>
<!-- Basic Information Section -->
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" id="collapse-plus-1" checked />
<div class="collapse-title text-lg font-bold">
{$t('adventures.basic_information')}
</div>
{/each}
<div class="collapse-content">
<div class="form-control mb-2">
<label for="name">{$t('adventures.name')}</label>
<input
type="text"
id="name"
class="input input-bordered w-full max-w-xs"
bind:value={newChecklist.name}
readonly={isReadOnly}
/>
</div>
<div class="form-control mb-2">
<label for="content">{$t('adventures.date')}</label>
{#if collection && collection.start_date && collection.end_date && !isReadOnly}<label
class="label cursor-pointer flex items-start space-x-2"
>
<span class="label-text">{$t('adventures.date_constrain')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (constrainDates = !constrainDates)}
/></label
>
{/if}
<input
type="date"
id="date"
name="date"
min={constrainDates ? collection.start_date : ''}
max={constrainDates ? collection.end_date : ''}
bind:value={newChecklist.date}
class="input input-bordered w-full max-w-xs mt-1"
readonly={isReadOnly}
/>
</div>
</div>
</div>
<!-- Items Section -->
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" id="collapse-plus-2" checked />
<div class="collapse-title text-lg font-bold">
{$t('checklist.items')}
</div>
<div class="collapse-content">
{#if !isReadOnly}
<div class="form-control mb-2 flex flex-row">
<input type="checkbox" bind:checked={newStatus} class="checkbox mt-4 mr-2" />
<input
type="text"
id="new_item"
placeholder={$t('checklist.new_item')}
name="new_item"
bind:value={newItem}
class="input input-bordered w-full max-w-xs mt-1"
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addItem();
}
}}
/>
<button
type="button"
class="btn btn-sm btn-primary absolute right-0 mt-2.5 mr-4"
on:click={addItem}
>
{$t('adventures.add')}
</button>
</div>
{/if}
{#if items.length > 0}
<div class="divider"></div>
<h2 class=" text-xl font-semibold mb-4 -mt-3">{$t('checklist.items')}</h2>
{/if}
{#each items as item, i}
<div class="form-control mb-2 flex flex-row">
<input
type="checkbox"
bind:checked={item.is_checked}
class="checkbox mt-4 mr-2"
readonly={isReadOnly}
/>
<input
type="text"
id="item_{i}"
name="item_{i}"
bind:value={item.name}
class="input input-bordered w-full max-w-xs mt-1"
readonly={isReadOnly}
/>
<button
type="button"
class="btn btn-sm btn-error absolute right-0 mt-2.5 mr-4"
on:click={() => removeItem(i)}
disabled={isReadOnly}
>
{$t('adventures.remove')}
</button>
</div>
{/each}
</div>
</div>
{#if warning}
<div role="alert" class="alert alert-error">
<svg
@ -225,10 +273,6 @@
<span>{warning}</span>
</div>
{/if}
<button class="btn btn-primary mr-1" on:click={save}>{$t('notes.save')}</button>
<button class="btn btn-neutral" on:click={close}>{$t('about.close')}</button>
{#if collection.is_public}
<div role="alert" class="alert mt-4">
<svg
@ -247,60 +291,13 @@
<span>{$t('checklist.checklist_public')}</span>
</div>
{/if}
</form>
{:else}
<form>
<div class="form-control mb-2">
<label for="name">{$t('adventures.name')}</label>
<input
type="text"
id="name"
class="input input-bordered w-full max-w-xs"
bind:value={newChecklist.name}
readonly
/>
<div class="mt-4">
<button class="btn btn-primary mr-1" disabled={isReadOnly} on:click={save}
>{$t('notes.save')}</button
><button class="btn btn-neutral" on:click={close}>{$t('about.close')}</button>
</div>
<div class="form-control mb-2">
<label for="content">{$t('adventures.date')}</label>
<input
type="date"
id="date"
name="date"
min={collection.start_date || ''}
max={collection.end_date || ''}
bind:value={newChecklist.date}
readonly
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
{#if items.length > 0}
<div class="divider"></div>
<h2 class=" text-xl font-semibold mb-4 -mt-3">{$t('checklist.items')}</h2>
{/if}
{#each items as item, i}
<div class="form-control mb-2 flex flex-row">
<input
type="checkbox"
checked={item.is_checked}
class="checkbox mt-4 mr-2"
readonly={true}
disabled
/>
<input
type="text"
id="item_{i}"
name="item_{i}"
bind:value={item.name}
readonly
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
{/each}
<button class="btn btn-neutral" on:click={close}>{$t('about.close')}</button>
</form>
{/if}
</div>
</div>
</dialog>

View file

@ -0,0 +1,204 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Collection, Transportation } from '$lib/types';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
import { addToast } from '$lib/toasts';
let modal: HTMLDialogElement;
import { t } from 'svelte-i18n';
import MarkdownEditor from './MarkdownEditor.svelte';
export let collectionToEdit: Collection | null = null;
let collection: Collection = {
id: collectionToEdit?.id || '',
name: collectionToEdit?.name || '',
description: collectionToEdit?.description || '',
start_date: collectionToEdit?.start_date || null,
end_date: collectionToEdit?.end_date || null,
user_id: collectionToEdit?.user_id || '',
is_public: collectionToEdit?.is_public || false,
adventures: collectionToEdit?.adventures || [],
link: collectionToEdit?.link || '',
shared_with: undefined
};
console.log(collection);
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 handleSubmit(event: Event) {
event.preventDefault();
console.log(collection);
if (collection.start_date && !collection.end_date) {
collection.end_date = collection.start_date;
}
if (
collection.start_date &&
collection.end_date &&
collection.start_date > collection.end_date
) {
addToast('error', $t('adventures.start_before_end_error'));
return;
}
if (!collection.start_date && collection.end_date) {
collection.start_date = collection.end_date;
}
if (!collection.start_date && !collection.end_date) {
collection.start_date = null;
collection.end_date = null;
}
if (collection.id === '') {
let res = await fetch('/api/collections', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(collection)
});
let data = await res.json();
if (data.id) {
collection = data as Collection;
addToast('success', $t('collection.collection_created'));
dispatch('save', collection);
} else {
console.error(data);
addToast('error', $t('collection.error_creating_collection'));
}
} else {
let res = await fetch(`/api/collections/${collection.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(collection)
});
let data = await res.json();
if (data.id) {
collection = data as Collection;
addToast('success', $t('collection.collection_edit_success'));
dispatch('save', collection);
} else {
addToast('error', $t('collection.error_editing_collection'));
}
}
}
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-2xl">
{collectionToEdit ? $t('adventures.edit_collection') : $t('collection.new_collection')}
</h3>
<div class="modal-action items-center">
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
<!-- Basic Information Section -->
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.basic_information')}
</div>
<div class="collapse-content">
<!-- Name -->
<div>
<label for="name">
{$t('adventures.name')}<span class="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
bind:value={collection.name}
class="input input-bordered w-full"
required
/>
</div>
<!-- Description -->
<div>
<label for="description">{$t('adventures.description')}</label><br />
<MarkdownEditor bind:text={collection.description} editor_height={'h-32'} />
</div>
<!-- Start Date -->
<div>
<label for="start_date">{$t('adventures.start_date')}</label>
<input
type="date"
id="start_date"
name="start_date"
bind:value={collection.start_date}
class="input input-bordered w-full"
/>
</div>
<!-- End Date -->
<div>
<label for="end_date">{$t('adventures.end_date')}</label>
<input
type="date"
id="end_date"
name="end_date"
bind:value={collection.end_date}
class="input input-bordered w-full"
/>
</div>
<!-- Public -->
<div>
<label class="label cursor-pointer flex items-start space-x-2">
<span class="label-text">{$t('collection.public_collection')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="is_public"
name="is_public"
bind:checked={collection.is_public}
/>
</label>
</div>
<!-- Link -->
<div>
<label for="link">{$t('adventures.link')}</label>
<input
type="text"
id="link"
name="link"
bind:value={collection.link}
class="input input-bordered w-full"
/>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="mt-4">
<button type="submit" class="btn btn-primary">
{$t('adventures.save_next')}
</button>
<button type="button" class="btn" on:click={close}>
{$t('about.close')}
</button>
</div>
</form>
</div>
</div>
</dialog>

View file

@ -1,209 +0,0 @@
<script lang="ts">
export let collectionToEdit: Collection;
import { createEventDispatcher } from 'svelte';
import type { Collection } from '$lib/types';
import { t } from 'svelte-i18n';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
import { addToast } from '$lib/toasts';
let modal: HTMLDialogElement;
console.log(collectionToEdit.id);
let originalName = collectionToEdit.name;
import Calendar from '~icons/mdi/calendar';
import Notebook from '~icons/mdi/notebook';
import Earth from '~icons/mdi/earth';
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
});
function submit() {}
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
close();
}
}
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
if (collectionToEdit.end_date && collectionToEdit.start_date) {
if (new Date(collectionToEdit.start_date) > new Date(collectionToEdit.end_date)) {
addToast('error', $t('adventures.start_before_end_error'));
return;
}
}
if (collectionToEdit.end_date && !collectionToEdit.start_date) {
addToast('error', $t('adventures.no_start_date'));
return;
}
if (collectionToEdit.start_date && !collectionToEdit.end_date) {
addToast('error', $t('adventures.no_end_date'));
return;
}
const response = await fetch(form.action, {
method: form.method,
body: formData
});
if (response.ok) {
const result = await response.json();
const data = JSON.parse(result.data);
console.log(data);
if (data) {
addToast('success', $t('collection.collection_edit_success'));
dispatch('saveEdit', collectionToEdit);
close();
} else {
addToast('warning', $t('collection.error_editing_collection'));
console.log('Error editing collection');
}
}
}
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">{$t('adventures.edit_collection')}: {originalName}</h3>
<div
class="modal-action items-center"
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
>
<form method="post" style="width: 100%;" on:submit={handleSubmit} action="/collections?/edit">
<div class="mb-2">
<input
type="text"
id="adventureId"
name="adventureId"
hidden
readonly
bind:value={collectionToEdit.id}
class="input input-bordered w-full max-w-xs mt-1"
/>
<label for="name">{$t('adventures.name')}</label><br />
<input
type="text"
name="name"
id="name"
bind:value={collectionToEdit.name}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="date"
>{$t('adventures.description')}
<Notebook class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<div class="flex">
<input
type="text"
id="description"
name="description"
bind:value={collectionToEdit.description}
class="input input-bordered w-full max-w-xs mt-1 mb-2"
/>
<!-- <button
class="btn btn-neutral ml-2"
type="button"
on:click={generate}
><iconify-icon icon="mdi:wikipedia" class="text-xl -mb-1"
></iconify-icon>Generate Description</button
> -->
</div>
<div class="mb-2">
<label for="start_date"
>{$t('adventures.start_date')} <Calendar class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="date"
id="start_date"
name="start_date"
bind:value={collectionToEdit.start_date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="end_date"
>{$t('adventures.end_date')} <Calendar class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="date"
id="end_date"
name="end_date"
bind:value={collectionToEdit.end_date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="end_date">{$t('adventures.link')} </label><br />
<input
type="url"
id="link"
name="link"
bind:value={collectionToEdit.link}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
<div class="mb-2">
<label for="is_public"
>{$t('adventures.public')} <Earth class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<input
type="checkbox"
class="toggle toggle-primary"
id="is_public"
name="is_public"
bind:checked={collectionToEdit.is_public}
/>
</div>
{#if collectionToEdit.is_public}
<div class="bg-neutral p-4 rounded-md shadow-sm">
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>
<div class="flex items-center justify-between">
<p class="text-card-foreground font-mono">
{window.location.origin}/collections/{collectionToEdit.id}
</p>
<button
type="button"
on:click={() => {
navigator.clipboard.writeText(
`${window.location.origin}/collections/${collectionToEdit.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"
>
{$t('adventures.copy_link')}
</button>
</div>
</div>
{/if}
<button type="submit" class="btn btn-primary mr-4 mt-4" on:click={submit}>Edit</button>
<!-- if there is a button in form, it will close the modal -->
<button class="btn mt-4" on:click={close}>{$t('about.close')}</button>
</form>
<div class="flex items-center justify-center flex-wrap gap-4 mt-4"></div>
</div>
</div>
</dialog>

View file

@ -1,281 +0,0 @@
<script lang="ts">
export let transportationToEdit: Transportation;
import { createEventDispatcher } from 'svelte';
import type { Transportation } from '$lib/types';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
import { addToast } from '$lib/toasts';
let modal: HTMLDialogElement;
import { t } from 'svelte-i18n';
console.log(transportationToEdit.id);
let originalName = transportationToEdit.name;
export let startDate: string | null = null;
export let endDate: string | null = null;
let fullStartDate: string = '';
let fullEndDate: string = '';
if (startDate && endDate) {
fullStartDate = `${startDate}T00:00`;
fullEndDate = `${endDate}T23:59`;
}
import MapMarker from '~icons/mdi/map-marker';
import Calendar from '~icons/mdi/calendar';
import Notebook from '~icons/mdi/notebook';
import Star from '~icons/mdi/star';
import PlaneCar from '~icons/mdi/plane-car';
import LinkVariant from '~icons/mdi/link-variant';
import Airplane from '~icons/mdi/airplane';
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
});
if (transportationToEdit.date) {
transportationToEdit.date = transportationToEdit.date.slice(0, 19);
}
if (transportationToEdit.end_date) {
transportationToEdit.end_date = transportationToEdit.end_date.slice(0, 19);
}
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
close();
}
}
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
// make sure end_date is not before start_date
if (
transportationToEdit.end_date &&
transportationToEdit.date &&
transportationToEdit.date > transportationToEdit.end_date
) {
addToast('error', $t('adventures.start_before_end_error'));
return;
}
// make sure end_date has a start_date
if (transportationToEdit.end_date && !transportationToEdit.date) {
transportationToEdit.end_date = null;
formData.set('end_date', '');
}
const response = await fetch(`/api/transportations/${transportationToEdit.id}/`, {
method: 'PUT',
body: formData
});
if (response.ok) {
const result = await response.json();
transportationToEdit = result;
addToast('success', $t('transportation.transportation_edit_success'));
dispatch('saveEdit', transportationToEdit);
close();
} else {
addToast('error', $t('transportation.error_editing_transportation'));
}
}
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">{$t('transportation.edit_transportation')}: {originalName}</h3>
<div
class="modal-action items-center"
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
>
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
<div class="mb-2">
<input
type="text"
id="id"
name="id"
hidden
readonly
bind:value={transportationToEdit.id}
class="input input-bordered w-full max-w-xs mt-1"
/>
<input
type="text"
id="is_public"
name="is_public"
hidden
readonly
bind:value={transportationToEdit.is_public}
class="input input-bordered w-full max-w-xs mt-1"
/>
<div class="mb-2">
<label for="type"
>{$t('transportation.type')} <PlaneCar class="inline-block mb-1 w-6 h-6" /></label
><br />
<select
class="select select-bordered w-full max-w-xs"
name="type"
id="type"
bind:value={transportationToEdit.type}
>
<option disabled selected>{$t('transportation.type')}</option>
<option value="car">{$t('transportation.modes.car')}</option>
<option value="plane">{$t('transportation.modes.plane')}</option>
<option value="train">{$t('transportation.modes.train')}</option>
<option value="bus">{$t('transportation.modes.bus')}</option>
<option value="boat">{$t('transportation.modes.boat')}</option>
<option value="bike">{$t('transportation.modes.bike')}</option>
<option value="walking">{$t('transportation.modes.walking')}</option>
<option value="other">{$t('transportation.modes.other')}</option>
</select>
</div>
<label for="name">{$t('adventures.name')}</label><br />
<input
type="text"
name="name"
id="name"
bind:value={transportationToEdit.name}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="date"
>{$t('adventures.description')}
<Notebook class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<div class="flex">
<input
type="text"
id="description"
name="description"
bind:value={transportationToEdit.description}
class="input input-bordered w-full max-w-xs mt-1 mb-2"
/>
</div>
<div class="mb-2">
<label for="start_date"
>{transportationToEdit.date ? `${$t('transportation.start')} ` : ''}{$t(
'transportation.date_and_time'
)}
<Calendar class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="datetime-local"
id="date"
name="date"
min={fullStartDate || ''}
max={fullEndDate || ''}
bind:value={transportationToEdit.date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
{#if transportationToEdit.date}
<div class="mb-2">
<label for="end_date"
>{$t('transportation.end_date_time')}
<Calendar class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="datetime-local"
id="end_date"
name="end_date"
min={fullStartDate || ''}
max={fullEndDate || ''}
bind:value={transportationToEdit.end_date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
{/if}
<div class="mb-2">
<label for="rating"
>{$t('adventures.rating')} <Star class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="number"
max="5"
min="0"
id="rating"
name="rating"
bind:value={transportationToEdit.rating}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="rating"
>{$t('adventures.link')} <LinkVariant class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="url"
id="link"
name="link"
bind:value={transportationToEdit.link}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
{#if transportationToEdit.type == 'plane'}
<div class="mb-2">
<label for="flight_number"
>{$t('transportation.flight_number')}
<Airplane class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="flight_number"
name="flight_number"
bind:value={transportationToEdit.flight_number}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
{/if}
<div class="mb-2">
<label for="rating"
>{$t('transportation.from_location')}
<MapMarker class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="from_location"
name="from_location"
bind:value={transportationToEdit.from_location}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="rating"
>{$t('transportation.to_location')}
<MapMarker class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="to_location"
name="to_location"
bind:value={transportationToEdit.to_location}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
<button type="submit" class="btn btn-primary mr-4 mt-4">{$t('transportation.edit')}</button>
<!-- if there is a button in form, it will close the modal -->
<button class="btn mt-4" on:click={close}>{$t('about.close')}</button>
</form>
<div class="flex items-center justify-center flex-wrap gap-4 mt-4"></div>
</div>
</div>
</dialog>

View file

@ -0,0 +1,81 @@
<script lang="ts">
import { marked } from 'marked'; // Import the markdown parser
import { t } from 'svelte-i18n';
export let text: string | null | undefined = ''; // Markdown text
export let editor_height: string = 'h-64'; // Editor height
let is_preview: boolean = false; // Toggle between Edit and Preview mode
// Function to parse markdown to HTML
const renderMarkdown = (markdown: string) => {
return marked(markdown);
};
// References for scroll syncing
let editorRef: HTMLTextAreaElement | null = null;
let previewRef: HTMLElement | null = null;
// Sync scrolling between editor and preview
const syncScroll = () => {
if (editorRef && previewRef) {
const ratio = editorRef.scrollTop / (editorRef.scrollHeight - editorRef.clientHeight);
previewRef.scrollTop = ratio * (previewRef.scrollHeight - previewRef.clientHeight);
}
};
</script>
<div class="join justify-center mt-2">
<button
type="button"
class="join-item btn btn-sm btn-outline"
on:click={() => (is_preview = false)}
class:btn-active={!is_preview}
>
{$t('transportation.edit')}
</button>
<button
type="button"
class="join-item btn btn-sm btn-outline"
on:click={() => (is_preview = true)}
class:btn-active={is_preview}
>
{$t('adventures.preview')}
</button>
</div>
<div class="flex flex-col mt-4 gap-4">
<!-- Markdown Editor -->
{#if !is_preview}
<textarea
class="textarea textarea-bordered resize-none {editor_height} w-full"
bind:this={editorRef}
bind:value={text}
placeholder={$t('adventures.md_instructions')}
on:scroll={syncScroll}
></textarea>
{/if}
<!-- Markdown Preview -->
{#if is_preview}
<article
class="prose overflow-auto h-96 max-w-full w-full p-4 border border-base-300 rounded-lg bg-base-300"
bind:this={previewRef}
>
{@html renderMarkdown(text || '')}
</article>
{/if}
</div>
<style>
/* Optional: Smooth scrolling for synced scroll effect */
textarea,
article {
scroll-behavior: smooth;
}
/* Force both editor and preview to have equal width */
textarea,
article {
width: 100%;
}
</style>

View file

@ -1,180 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Adventure, Collection } from '$lib/types';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { addToast } from '$lib/toasts';
import Calendar from '~icons/mdi/calendar';
let newCollection: Collection = {
user_id: '',
id: '',
name: '',
description: '',
adventures: [] as Adventure[],
is_public: false,
shared_with: [],
link: ''
};
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 handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
// make sure that start_date is before end_date
if (new Date(newCollection.start_date ?? '') > new Date(newCollection.end_date ?? '')) {
addToast('error', $t('adventures.start_before_end_error'));
return;
}
// make sure end date has a start date
if (newCollection.end_date && !newCollection.start_date) {
addToast('error', $t('adventures.no_start_date'));
return;
}
if (newCollection.start_date && !newCollection.end_date) {
addToast('error', $t('adventures.no_end_date'));
return;
}
const response = await fetch(form.action, {
method: form.method,
body: formData
});
if (response.ok) {
const result = await response.json();
const data = JSON.parse(result.data); // Parsing the JSON string in the data field
if (data[1] !== undefined) {
// these two lines here are wierd, because the data[1] is the id of the new adventure and data[2] is the user_id of the new adventure
console.log(data);
let id = data[1];
let user_id = data[2];
if (id !== undefined && user_id !== undefined) {
newCollection.id = id;
newCollection.user_id = user_id;
console.log(newCollection);
dispatch('create', newCollection);
addToast('success', $t('collection.collection_created'));
close();
} else {
addToast('error', $t('collection.error_creating_collection'));
}
}
}
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">{$t('collection.new_collection')}</h3>
<div
class="modal-action items-center"
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
>
<form
method="post"
style="width: 100%;"
on:submit={handleSubmit}
action="/collections?/create"
>
<div class="mb-2">
<label for="name">{$t('adventures.name')}</label><br />
<input
type="text"
id="name"
name="name"
bind:value={newCollection.name}
class="input input-bordered w-full max-w-xs mt-1"
required
/>
</div>
<div class="mb-2">
<label for="description"
>{$t('adventures.description')}<iconify-icon
icon="mdi:notebook"
class="text-lg ml-1 -mb-0.5"
></iconify-icon></label
><br />
<div class="flex">
<input
type="text"
id="description"
name="description"
bind:value={newCollection.description}
class="input input-bordered w-full max-w-xs mt-1 mb-2"
/>
</div>
<div class="mb-2">
<label for="start_date"
>{$t('adventures.start_date')} <Calendar class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="date"
id="start_date"
name="start_date"
bind:value={newCollection.start_date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="end_date"
>{$t('adventures.end_date')} <Calendar class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="date"
id="end_date"
name="end_date"
bind:value={newCollection.end_date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="end_date">{$t('adventures.link')} </label><br />
<input
type="url"
id="link"
name="link"
bind:value={newCollection.link}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<button type="submit" class="btn btn-primary mr-4 mt-4">
{$t('collection.create')}
</button>
<button type="button" class="btn mt-4" on:click={close}>{$t('about.close')}</button>
</div>
</div>
</form>
</div>
</div>
</dialog>

View file

@ -1,256 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Collection, Transportation } from '$lib/types';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
import { addToast } from '$lib/toasts';
let modal: HTMLDialogElement;
import { t } from 'svelte-i18n';
export let collection: Collection;
import MapMarker from '~icons/mdi/map-marker';
import Calendar from '~icons/mdi/calendar';
import Notebook from '~icons/mdi/notebook';
import Star from '~icons/mdi/star';
import PlaneCar from '~icons/mdi/plane-car';
import LinkVariant from '~icons/mdi/link-variant';
import Airplane from '~icons/mdi/airplane';
export let startDate: string | null = null;
export let endDate: string | null = null;
let fullStartDate: string = '';
let fullEndDate: string = '';
if (startDate && endDate) {
fullStartDate = `${startDate}T00:00`;
fullEndDate = `${endDate}T23:59`;
}
let type: string = '';
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
});
// if (newTransportation.date) {
// newTransportation.date = newTransportation.date.slice(0, 19);
// }
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
close();
}
}
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
// make sure there is a start date if there is an end date
if (formData.get('end_date') && !formData.get('date')) {
addToast('error', $t('transportation.provide_start_date'));
return;
}
const response = await fetch(`/api/transportations/`, {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
addToast('success', $t('transportation.transportation_added'));
dispatch('add', result);
close();
} else {
addToast('error', $t('transportation.error_editing_transportation'));
}
}
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">{$t('transportation.new_transportation')}</h3>
<div
class="modal-action items-center"
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
>
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
<div class="mb-2">
<input
type="text"
id="collection"
name="collection"
hidden
readonly
bind:value={collection.id}
class="input input-bordered w-full max-w-xs mt-1"
/>
<input
type="text"
id="is_public"
name="is_public"
hidden
readonly
bind:value={collection.is_public}
class="input input-bordered w-full max-w-xs mt-1"
/>
<div class="mb-2">
<label for="type"
>{$t('transportation.type')} <PlaneCar class="inline-block mb-1 w-6 h-6" /></label
><br />
<select
class="select select-bordered w-full max-w-xs"
name="type"
id="type"
bind:value={type}
>
<option disabled selected>{$t('transportation.type')}</option>
<option value="car">{$t('transportation.modes.car')}</option>
<option value="plane">{$t('transportation.modes.plane')}</option>
<option value="train">{$t('transportation.modes.train')}</option>
<option value="bus">{$t('transportation.modes.bus')}</option>
<option value="boat">{$t('transportation.modes.boat')}</option>
<option value="bike">{$t('transportation.modes.bike')}</option>
<option value="walking">{$t('transportation.modes.walking')}</option>
<option value="other">{$t('transportation.modes.other')}</option>
</select>
</div>
<label for="name">{$t('adventures.name')}</label><br />
<input
type="text"
name="name"
id="name"
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="date"
>{$t('adventures.description')}
<Notebook class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<div class="flex">
<input
type="text"
id="description"
name="description"
class="input input-bordered w-full max-w-xs mt-1 mb-2"
/>
</div>
<div class="mb-2">
<label for="start_date"
>{$t('transportation.date_time')}
<Calendar class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="datetime-local"
id="date"
name="date"
min={fullStartDate || ''}
max={fullEndDate || ''}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="end_date"
>{$t('transportation.end_date_time')}
<Calendar class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="datetime-local"
id="end_date"
name="end_date"
min={fullStartDate || ''}
max={fullEndDate || ''}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="rating"
>{$t('adventures.rating')} <Star class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="number"
max="5"
min="0"
id="rating"
name="rating"
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="rating"
>{$t('adventures.link')} <LinkVariant class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="url"
id="link"
name="link"
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
{#if type == 'plane'}
<div class="mb-2">
<label for="flight_number"
>{$t('transportation.flight_number')}
<Airplane class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="flight_number"
name="flight_number"
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
{/if}
<div class="mb-2">
<label for="rating"
>{$t('transportation.from_location')}
<MapMarker class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="from_location"
name="from_location"
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="rating"
>{$t('transportation.to_location')}
<MapMarker class="inline-block mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="to_location"
name="to_location"
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
<button type="submit" class="btn btn-primary mr-4 mt-4">{$t('collection.create')}</button>
<!-- if there is a button in form, it will close the modal -->
<button class="btn mt-4" on:click={close}>{$t('about.close')}</button>
</form>
<div class="flex items-center justify-center flex-wrap gap-4 mt-4"></div>
</div>
</div>
</dialog>

View file

@ -8,11 +8,27 @@
import Launch from '~icons/mdi/launch';
import TrashCan from '~icons/mdi/trash-can';
import Calendar from '~icons/mdi/calendar';
import DeleteWarning from './DeleteWarning.svelte';
export let note: Note;
export let user: User | null = null;
export let collection: Collection | null = null;
let isWarningModalOpen: boolean = false;
let unlinked: boolean = false;
$: {
if (collection?.start_date && collection.end_date) {
const startOutsideRange =
note.date && collection.start_date < note.date && collection.end_date < note.date;
const endOutsideRange =
note.date && collection.start_date > note.date && collection.end_date > note.date;
unlinked = !!(startOutsideRange || endOutsideRange || !note.date);
}
}
function editNote() {
dispatch('edit', note);
}
@ -23,6 +39,7 @@
});
if (res.ok) {
addToast('success', $t('notes.note_deleted'));
isWarningModalOpen = false;
dispatch('delete', note.id);
} else {
addToast($t('notes.note_delete_error'), 'error');
@ -30,6 +47,17 @@
}
</script>
{#if isWarningModalOpen}
<DeleteWarning
title={$t('adventures.delete_note')}
button_text="Delete"
description={$t('adventures.note_delete_confirm')}
is_warning={false}
on:close={() => (isWarningModalOpen = false)}
on:confirm={deleteNote}
/>
{/if}
<div
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md overflow-hidden bg-neutral text-neutral-content shadow-xl"
>
@ -40,6 +68,9 @@
</h2>
</div>
<div class="badge badge-primary">{$t('adventures.note')}</div>
{#if unlinked}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if}
{#if note.links && note.links.length > 0}
<p>
{note.links.length}
@ -59,12 +90,12 @@
<button class="btn btn-neutral-200 mb-2" on:click={editNote}>
<Launch class="w-6 h-6" />{$t('notes.open')}
</button>
{#if note.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))}
{#if note.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
<button
id="delete_adventure"
data-umami-event="Delete Adventure"
class="btn btn-warning"
on:click={deleteNote}><TrashCan class="w-6 h-6" /></button
on:click={() => (isWarningModalOpen = true)}><TrashCan class="w-6 h-6" /></button
>
{/if}
</div>

View file

@ -5,12 +5,25 @@
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import MarkdownEditor from './MarkdownEditor.svelte';
let modal: HTMLDialogElement;
import { marked } from 'marked'; // Import the markdown parser
const renderMarkdown = (markdown: string) => {
return marked(markdown);
};
export let note: Note | null = null;
export let collection: Collection;
export let user: User | null = null;
let constrainDates: boolean = false;
let isReadOnly =
!(note && user?.uuid == note?.user_id) &&
!(user && collection && collection.shared_with && collection.shared_with.includes(user.uuid)) &&
!!note;
let warning: string | null = '';
let newLink: string = '';
@ -105,85 +118,137 @@
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">{$t('notes.note_editor')}</h3>
{#if initialName}
<p class="font-semibold text-md mb-2">{$t('notes.editing_note')} {initialName}</p>
{/if}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-2xl">
{#if note?.id && !isReadOnly}
<p class="font-semibold text-md mb-2">
{$t('notes.editing_note')}
{initialName}
</p>
{:else if !isReadOnly}
{$t('notes.note_editor')}
{:else}
{$t('notes.note_viewer')}
{/if}
</h3>
{#if (note && user?.uuid == note?.user_id) || (collection && user && collection.shared_with.includes(user.uuid)) || !note}
<form on:submit|preventDefault>
<div class="form-control mb-2">
<label for="name">{$t('adventures.name')}</label>
<input
type="text"
id="name"
class="input input-bordered w-full max-w-xs"
bind:value={newNote.name}
/>
</div>
<div class="form-control mb-2">
<label for="content">{$t('adventures.date')}</label>
<input
type="date"
id="date"
name="date"
min={collection.start_date || ''}
max={collection.end_date || ''}
bind:value={newNote.date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="form-control mb-2">
<label for="content">{$t('notes.content')}</label>
<textarea
id="content"
class="textarea textarea-bordered"
bind:value={newNote.content}
rows="5"
></textarea>
</div>
<div class="form-control mb-2">
<label for="content">{$t('adventures.links')}</label>
<input
type="url"
class="input input-bordered w-full mb-1"
placeholder="{$t('notes.add_a_link')} (e.g. https://example.com)"
bind:value={newLink}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addLink();
}
}}
/>
<button type="button" class="btn btn-sm btn-primary" on:click={addLink}
>{$t('adventures.add')}</button
>
</div>
{#if newNote.links.length > 0}
<ul class="list-none">
{#each newNote.links as link, i}
<li class="mb-4">
<a href={link} class="link link-primary" target="_blank">{link}</a>
<button
type="button"
class="btn btn-sm btn-error absolute right-0 mr-4"
on:click={() => {
newNote.links = newNote.links.filter((_, index) => index !== i);
}}
<div class="modal-action items-center">
<form method="post" style="width: 100%;" on:submit|preventDefault>
<!-- Basic Information Section -->
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" id="collapse-plus-1" checked />
<div class="collapse-title text-lg font-bold">
{$t('adventures.basic_information')}
</div>
<div class="collapse-content">
<!-- Name Input -->
<div class="form-control mb-2">
<label for="name">{$t('adventures.name')}</label>
<input
type="text"
id="name"
readonly={isReadOnly}
class="input input-bordered w-full max-w-xs"
bind:value={newNote.name}
/>
</div>
<!-- Date Input -->
<div class="form-control mb-2">
<label for="content">{$t('adventures.date')}</label>
{#if collection && collection.start_date && collection.end_date && !isReadOnly}<label
class="label cursor-pointer flex items-start space-x-2"
>
{$t('adventures.remove')}
</button>
</li>
{/each}
</ul>
{/if}
<span class="label-text">{$t('adventures.date_constrain')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (constrainDates = !constrainDates)}
/></label
>
{/if}
<input
type="date"
id="date"
name="date"
readonly={isReadOnly}
min={constrainDates ? collection.start_date : ''}
max={constrainDates ? collection.end_date : ''}
bind:value={newNote.date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<!-- Content Textarea -->
<div>
<label for="content">{$t('notes.content')}</label><br />
{#if !isReadOnly}
<MarkdownEditor bind:text={newNote.content} editor_height={'h-32'} />
{:else if note}
<p class="text-sm text-muted-foreground" style="white-space: pre-wrap;"></p>
<article
class="prose overflow-auto h-full max-w-full p-4 border border-base-300 rounded-lg mb-4 mt-4"
>
{@html renderMarkdown(note.content || '')}
</article>
{/if}
</div>
<!-- Links Section -->
{#if !isReadOnly}
<div class="form-control mb-2">
<label for="content">{$t('adventures.links')}</label>
<input
type="url"
class="input input-bordered w-full mb-1"
placeholder="{$t('notes.add_a_link')} (e.g. https://example.com)"
bind:value={newLink}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addLink();
}
}}
/>
<button type="button" class="btn btn-sm btn-primary mt-1" on:click={addLink}>
{$t('adventures.add')}
</button>
</div>
{/if}
<!-- Links List -->
{#if newNote.links.length > 0}
<ul class="list-none">
{#each newNote.links as link, i}
<li class="mb-4 flex justify-between items-center">
<a href={link} class="link link-primary" target="_blank">
{link}
</a>
<button
type="button"
class="btn btn-sm btn-error"
disabled={isReadOnly}
on:click={() => {
newNote.links = newNote.links.filter((_, index) => index !== i);
}}
>
{$t('adventures.remove')}
</button>
</li>
{/each}
</ul>
{/if}
</div>
</div>
<!-- Warning Message -->
{#if warning}
<div role="alert" class="alert alert-error">
<div role="alert" class="alert alert-error mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
@ -201,11 +266,9 @@
</div>
{/if}
<button class="btn btn-primary mr-1" on:click={save}>{$t('notes.save')}</button>
<button class="btn btn-neutral" on:click={close}>{$t('about.close')}</button>
<!-- Public Note Alert -->
{#if collection.is_public}
<div role="alert" class="alert mt-4">
<div role="alert" class="alert mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@ -222,57 +285,16 @@
<span>{$t('notes.note_public')}</span>
</div>
{/if}
</form>
{:else}
<form>
<div class="form-control mb-2">
<label for="name">{$t('adventures.public')}</label>
<input
type="text"
id="name"
class="input input-bordered w-full max-w-xs"
bind:value={newNote.name}
readonly
/>
</div>
<div class="form-control mb-2">
<label for="content">{$t('adventures.date')}</label>
<input
type="date"
id="date"
name="date"
min={collection.start_date || ''}
max={collection.end_date || ''}
bind:value={newNote.date}
class="input input-bordered w-full max-w-xs mt-1"
readonly
/>
</div>
<div class="form-control mb-2">
<label for="content">{$t('notes.content')}</label>
<textarea
id="content"
class="textarea textarea-bordered"
bind:value={newNote.content}
rows="5"
readonly
></textarea>
</div>
<div class="form-control mb-2">
<label for="content">{$t('adventures.links')}</label>
</div>
{#if newNote.links.length > 0}
<ul class="list-none">
{#each newNote.links as link, i}
<li class="mb-1">
<a href={link} target="_blank">{link}</a>
</li>
{/each}
</ul>
{/if}
<button class="btn btn-neutral" on:click={close}>{$t('about.close')}</button>
<!-- Action Buttons -->
<div class="mt-4">
<button class="btn btn-primary mr-1" disabled={isReadOnly} on:click={save}>
{$t('notes.save')}
</button><button class="btn btn-neutral" on:click={close}>
{$t('about.close')}
</button>
</div>
</form>
{/if}
</div>
</div>
</dialog>

View file

@ -111,7 +111,7 @@
<h3 class="font-bold text-lg mb-4">Choose a Point</h3>
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
standardControls
>
<!-- MapEvents gives you access to map events even from other components inside the map,

View file

@ -24,7 +24,11 @@
});
if (res.ok) {
sharedWithUsers = sharedWithUsers.concat(user);
collection.shared_with.push(user.uuid);
if (collection.shared_with) {
collection.shared_with.push(user.uuid);
} else {
collection.shared_with = [user.uuid];
}
notSharedWithUsers = notSharedWithUsers.filter((u) => u.uuid !== user.uuid);
addToast(
'success',
@ -42,7 +46,9 @@
});
if (res.ok) {
notSharedWithUsers = notSharedWithUsers.concat(user);
collection.shared_with = collection.shared_with.filter((u) => u !== user.uuid);
if (collection.shared_with) {
collection.shared_with = collection.shared_with.filter((u) => u !== user.uuid);
}
sharedWithUsers = sharedWithUsers.filter((u) => u.uuid !== user.uuid);
addToast(
'success',
@ -60,8 +66,12 @@
if (res.ok) {
let data = await res.json();
allUsers = data;
sharedWithUsers = allUsers.filter((user) => collection.shared_with.includes(user.uuid));
notSharedWithUsers = allUsers.filter((user) => !collection.shared_with.includes(user.uuid));
sharedWithUsers = allUsers.filter((user) =>
(collection.shared_with ?? []).includes(user.uuid)
);
notSharedWithUsers = allUsers.filter(
(user) => !(collection.shared_with ?? []).includes(user.uuid)
);
console.log(sharedWithUsers);
console.log(notSharedWithUsers);
}

View file

@ -5,8 +5,8 @@
import type { Collection, Transportation, User } from '$lib/types';
import { addToast } from '$lib/toasts';
import { t } from 'svelte-i18n';
import ArrowDownThick from '~icons/mdi/arrow-down-thick';
import DeleteWarning from './DeleteWarning.svelte';
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
const dispatch = createEventDispatcher();
@ -14,10 +14,61 @@
export let user: User | null = null;
export let collection: Collection | null = null;
let isWarningModalOpen: boolean = false;
function editTransportation() {
dispatch('edit', transportation);
}
let unlinked: boolean = false;
$: {
if (collection?.start_date && collection.end_date) {
// Parse transportation dates
let transportationStartDate = transportation.date
? new Date(transportation.date.split('T')[0]) // Ensure proper date parsing
: null;
let transportationEndDate = transportation.end_date
? new Date(transportation.end_date.split('T')[0])
: null;
// Parse collection dates
let collectionStartDate = new Date(collection.start_date);
let collectionEndDate = new Date(collection.end_date);
// // Debugging outputs
// console.log(
// 'Transportation Start Date:',
// transportationStartDate,
// 'Transportation End Date:',
// transportationEndDate
// );
// console.log(
// 'Collection Start Date:',
// collectionStartDate,
// 'Collection End Date:',
// collectionEndDate
// );
// Check if the collection range is outside the transportation range
const startOutsideRange =
transportationStartDate &&
collectionStartDate < transportationStartDate &&
collectionEndDate < transportationStartDate;
const endOutsideRange =
transportationEndDate &&
collectionStartDate > transportationEndDate &&
collectionEndDate > transportationEndDate;
unlinked = !!(
startOutsideRange ||
endOutsideRange ||
(!transportationStartDate && !transportationEndDate)
);
}
}
async function deleteTransportation() {
let res = await fetch(`/api/transportations/${transportation.id}`, {
method: 'DELETE',
@ -29,43 +80,95 @@
console.log($t('transportation.transportation_delete_error'));
} else {
addToast('info', $t('transportation.transportation_deleted'));
isWarningModalOpen = false;
dispatch('delete', transportation.id);
}
}
</script>
{#if isWarningModalOpen}
<DeleteWarning
title={$t('adventures.delete_transportation')}
button_text="Delete"
description={$t('adventures.transportation_delete_confirm')}
is_warning={false}
on:close={() => (isWarningModalOpen = false)}
on:confirm={deleteTransportation}
/>
{/if}
<div
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl"
>
<div class="card-body">
<h2 class="card-title overflow-ellipsis">{transportation.name}</h2>
<div class="badge badge-secondary">{$t(`transportation.modes.${transportation.type}`)}</div>
<div>
{#if transportation.from_location}
<p class="break-words text-wrap">{transportation.from_location}</p>
{/if}
{#if transportation.to_location}
<ArrowDownThick class="w-6 h-6" />
<p class="break-words text-wrap">{transportation.to_location}</p>
{/if}
<div class="card-body space-y-4">
<!-- Title and Type -->
<div class="flex items-center justify-between">
<h2 class="card-title text-lg font-semibold truncate">{transportation.name}</h2>
<div class="flex items-center gap-2">
<div class="badge badge-secondary">
{$t(`transportation.modes.${transportation.type}`)}
</div>
{#if transportation.type == 'plane' && transportation.flight_number}
<div class="badge badge-neutral-200">{transportation.flight_number}</div>
{/if}
</div>
</div>
<div>
{#if transportation.date}
<p>{new Date(transportation.date).toLocaleString(undefined, { timeZone: 'UTC' })}</p>
{#if unlinked}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if}
<!-- Locations -->
<div class="space-y-2">
{#if transportation.from_location}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.from')}:</span>
<p class="break-words">{transportation.from_location}</p>
</div>
{/if}
{#if transportation.end_date}
<ArrowDownThick class="w-6 h-6" />
<p>{new Date(transportation.end_date).toLocaleString(undefined, { timeZone: 'UTC' })}</p>
{#if transportation.date}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.start')}:</span>
<p>{new Date(transportation.date).toLocaleString(undefined, { timeZone: 'UTC' })}</p>
</div>
{/if}
</div>
{#if transportation.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))}
<!-- Dates -->
<div class="space-y-2">
{#if transportation.to_location}
<!-- <ArrowDownThick class="w-4 h-4" /> -->
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.to')}:</span>
<p class="break-words">{transportation.to_location}</p>
</div>
{/if}
{#if transportation.end_date}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.end')}:</span>
<p>{new Date(transportation.end_date).toLocaleString(undefined, { timeZone: 'UTC' })}</p>
</div>
{/if}
</div>
<!-- Actions -->
{#if transportation.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
<div class="card-actions justify-end">
<button on:click={deleteTransportation} class="btn btn-secondary"
><TrashCanOutline class="w-5 h-5 mr-1" /></button
<button
class="btn btn-primary btn-sm flex items-center gap-1"
on:click={editTransportation}
title="Edit"
>
<button class="btn btn-primary" on:click={editTransportation}>
<FileDocumentEdit class="w-6 h-6" />
<FileDocumentEdit class="w-5 h-5" />
<span>{$t('transportation.edit')}</span>
</button>
<button
on:click={() => (isWarningModalOpen = true)}
class="btn btn-secondary btn-sm flex items-center gap-1"
title="Delete"
>
<TrashCanOutline class="w-5 h-5" />
<span>{$t('adventures.delete')}</span>
</button>
</div>
{/if}

View file

@ -0,0 +1,605 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Collection, Transportation } from '$lib/types';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
import { addToast } from '$lib/toasts';
let modal: HTMLDialogElement;
import { t } from 'svelte-i18n';
import MarkdownEditor from './MarkdownEditor.svelte';
import { appVersion } from '$lib/config';
import { DefaultMarker, MapLibre } from 'svelte-maplibre';
export let collection: Collection;
export let transportationToEdit: Transportation | null = null;
let constrainDates: boolean = false;
function toLocalDatetime(value: string | null): string {
if (!value) return '';
const date = new Date(value);
return date.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:mm
}
let transportation: Transportation = {
id: transportationToEdit?.id || '',
type: transportationToEdit?.type || '',
name: transportationToEdit?.name || '',
description: transportationToEdit?.description || '',
date: transportationToEdit?.date ? toLocalDatetime(transportationToEdit.date) : null,
end_date: transportationToEdit?.end_date
? toLocalDatetime(transportationToEdit.end_date)
: null,
rating: transportationToEdit?.rating || 0,
link: transportationToEdit?.link || '',
flight_number: transportationToEdit?.flight_number || '',
from_location: transportationToEdit?.from_location || '',
to_location: transportationToEdit?.to_location || '',
user_id: transportationToEdit?.user_id || '',
is_public: transportationToEdit?.is_public || false,
collection: transportationToEdit?.collection || collection.id,
created_at: transportationToEdit?.created_at || '',
updated_at: transportationToEdit?.updated_at || '',
origin_latitude: transportationToEdit?.origin_latitude || NaN,
origin_longitude: transportationToEdit?.origin_longitude || NaN,
destination_latitude: transportationToEdit?.destination_latitude || NaN,
destination_longitude: transportationToEdit?.destination_longitude || NaN
};
let fullStartDate: string = '';
let fullEndDate: string = '';
let starting_airport: string = '';
let ending_airport: string = '';
if (collection.start_date && collection.end_date) {
fullStartDate = `${collection.start_date}T00:00`;
fullEndDate = `${collection.end_date}T23:59`;
}
$: {
if (!transportation.rating) {
transportation.rating = NaN;
}
}
console.log(transportation);
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 geocode(e: Event | null) {
if (e) {
e.preventDefault();
}
const fetchLocation = async (query: string) => {
let res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=jsonv2`, {
headers: {
'User-Agent': `AdventureLog / ${appVersion} `
}
});
let data = await res.json();
return data;
};
let startingData = null;
let endingData = null;
if (transportation.type == 'plane') {
if (!starting_airport || !ending_airport) {
alert($t('adventures.no_location'));
return;
}
startingData = await fetchLocation(starting_airport + ' Airport');
endingData = await fetchLocation(ending_airport + ' Airport');
} else {
if (!transportation.from_location || !transportation.to_location) {
alert($t('adventures.no_location'));
return;
}
startingData = await fetchLocation(transportation?.from_location || '');
endingData = await fetchLocation(transportation?.to_location || '');
}
if (startingData.length === 0 || endingData.length === 0) {
alert($t('adventures.no_location_found'));
return;
}
if (transportation.type == 'plane') {
transportation.from_location =
startingData[0].name + ' (' + starting_airport.toUpperCase() + ')';
transportation.to_location = endingData[0].name + ' (' + ending_airport.toUpperCase() + ')';
} else {
transportation.from_location = startingData[0].display_name;
transportation.to_location = endingData[0].display_name;
}
transportation.origin_latitude = startingData[0].lat;
transportation.origin_longitude = startingData[0].lon;
transportation.destination_latitude = endingData[0].lat;
transportation.destination_longitude = endingData[0].lon;
}
async function handleSubmit(event: Event) {
event.preventDefault();
console.log(transportation);
// If the user has entered airport codes, but not location names, fetch the location names
if (
starting_airport &&
ending_airport &&
(!transportation.from_location || !transportation.to_location)
) {
transportation.from_location = starting_airport;
transportation.to_location = ending_airport;
}
// Round coordinates to 6 decimal places
if (transportation.origin_latitude) {
transportation.origin_latitude = Math.round(transportation.origin_latitude * 1e6) / 1e6;
}
if (transportation.origin_longitude) {
transportation.origin_longitude = Math.round(transportation.origin_longitude * 1e6) / 1e6;
}
if (transportation.destination_latitude) {
transportation.destination_latitude =
Math.round(transportation.destination_latitude * 1e6) / 1e6;
}
if (transportation.destination_longitude) {
transportation.destination_longitude =
Math.round(transportation.destination_longitude * 1e6) / 1e6;
}
if (transportation.end_date && !transportation.date) {
transportation.date = null;
transportation.end_date = null;
}
if (transportation.date && !transportation.end_date) {
transportation.end_date = transportation.date;
}
if (
transportation.date &&
transportation.end_date &&
transportation.date > transportation.end_date
) {
addToast('error', $t('adventures.start_before_end_error'));
return;
}
if (transportation.type != 'plane') {
transportation.flight_number = '';
}
if (transportation.id === '') {
let res = await fetch('/api/transportations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(transportation)
});
let data = await res.json();
if (data.id) {
transportation = data as Transportation;
addToast('success', $t('adventures.adventure_created'));
dispatch('save', transportation);
} else {
console.error(data);
addToast('error', $t('adventures.adventure_create_error'));
}
} else {
let res = await fetch(`/api/transportations/${transportation.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(transportation)
});
let data = await res.json();
if (data.id) {
transportation = data as Transportation;
addToast('success', $t('adventures.adventure_updated'));
dispatch('save', transportation);
} else {
addToast('error', $t('adventures.adventure_update_error'));
}
}
}
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-2xl">
{transportationToEdit
? $t('transportation.edit_transportation')
: $t('transportation.new_transportation')}
</h3>
<div class="modal-action items-center">
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
<!-- Basic Information Section -->
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.basic_information')}
</div>
<div class="collapse-content">
<!-- Type selection -->
<div>
<label for="type">
{$t('transportation.type')}<span class="text-red-500">*</span>
</label>
<div>
<select
class="select select-bordered w-full max-w-xs"
name="type"
id="type"
bind:value={transportation.type}
>
<option disabled selected>{$t('transportation.type')}</option>
<option value="car">{$t('transportation.modes.car')}</option>
<option value="plane">{$t('transportation.modes.plane')}</option>
<option value="train">{$t('transportation.modes.train')}</option>
<option value="bus">{$t('transportation.modes.bus')}</option>
<option value="boat">{$t('transportation.modes.boat')}</option>
<option value="bike">{$t('transportation.modes.bike')}</option>
<option value="walking">{$t('transportation.modes.walking')}</option>
<option value="other">{$t('transportation.modes.other')}</option>
</select>
</div>
</div>
<!-- Name -->
<div>
<label for="name">
{$t('adventures.name')}<span class="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
bind:value={transportation.name}
class="input input-bordered w-full"
required
/>
</div>
<!-- Description -->
<div>
<label for="description">{$t('adventures.description')}</label><br />
<MarkdownEditor bind:text={transportation.description} editor_height={'h-32'} />
</div>
<!-- Rating -->
<div>
<label for="rating">{$t('adventures.rating')}</label><br />
<input
type="number"
min="0"
max="5"
hidden
bind:value={transportation.rating}
id="rating"
name="rating"
class="input input-bordered w-full max-w-xs mt-1"
/>
<div class="rating -ml-3 mt-1">
<input
type="radio"
name="rating-2"
class="rating-hidden"
checked={Number.isNaN(transportation.rating)}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (transportation.rating = 1)}
checked={transportation.rating === 1}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (transportation.rating = 2)}
checked={transportation.rating === 2}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (transportation.rating = 3)}
checked={transportation.rating === 3}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (transportation.rating = 4)}
checked={transportation.rating === 4}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (transportation.rating = 5)}
checked={transportation.rating === 5}
/>
{#if transportation.rating}
<button
type="button"
class="btn btn-sm btn-error ml-2"
on:click={() => (transportation.rating = NaN)}
>
{$t('adventures.remove')}
</button>
{/if}
</div>
</div>
<!-- Link -->
<div>
<label for="link">{$t('adventures.link')}</label>
<input
type="url"
id="link"
name="link"
bind:value={transportation.link}
class="input input-bordered w-full"
/>
</div>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.date_information')}
</div>
<div class="collapse-content">
<!-- Start Date -->
<div>
<label for="date">
{$t('adventures.start_date')}
</label>
{#if collection && collection.start_date && collection.end_date}<label
class="label cursor-pointer flex items-start space-x-2"
>
<span class="label-text">{$t('adventures.date_constrain')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (constrainDates = !constrainDates)}
/></label
>
{/if}
<div>
<input
type="datetime-local"
id="date"
name="date"
bind:value={transportation.date}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
<!-- End Date -->
{#if transportation.date}
<div>
<label for="end_date">
{$t('adventures.end_date')}
</label>
<div>
<input
type="datetime-local"
id="end_date"
name="end_date"
min={constrainDates ? transportation.date : ''}
max={constrainDates ? fullEndDate : ''}
bind:value={transportation.end_date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
{/if}
</div>
</div>
<!-- Flight Information -->
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{#if transportation?.type == 'plane'}
{$t('adventures.flight_information')}
{:else}
{$t('adventures.location_information')}
{/if}
</div>
<div class="collapse-content">
{#if transportation?.type == 'plane'}
<!-- Flight Number -->
<div class="mb-4">
<label for="flight_number" class="label">
<span class="label-text">{$t('transportation.flight_number')}</span>
</label>
<input
type="text"
id="flight_number"
name="flight_number"
bind:value={transportation.flight_number}
class="input input-bordered w-full"
/>
</div>
<!-- Starting Airport -->
{#if !transportation.from_location || !transportation.to_location}
<div class="mb-4">
<label for="starting_airport" class="label">
<span class="label-text">{$t('adventures.starting_airport')}</span>
</label>
<input
type="text"
id="starting_airport"
bind:value={starting_airport}
name="starting_airport"
class="input input-bordered w-full"
placeholder="Enter starting airport code (e.g., JFK)"
/>
<label for="ending_airport" class="label">
<span class="label-text">{$t('adventures.ending_airport')}</span>
</label>
<input
type="text"
id="ending_airport"
bind:value={ending_airport}
name="ending_airport"
class="input input-bordered w-full"
placeholder="Enter ending airport code (e.g., LAX)"
/>
<button type="button" class="btn btn-primary mt-2" on:click={geocode}>
Fetch Location Information
</button>
</div>
{/if}
{#if transportation.from_location && transportation.to_location}
<!-- From Location -->
<div class="mb-4">
<label for="from_location" class="label">
<span class="label-text">{$t('transportation.from_location')}</span>
</label>
<input
type="text"
id="from_location"
name="from_location"
bind:value={transportation.from_location}
class="input input-bordered w-full"
/>
</div>
<!-- To Location -->
<div class="mb-4">
<label for="to_location" class="label">
<span class="label-text">{$t('transportation.to_location')}</span>
</label>
<input
type="text"
id="to_location"
name="to_location"
bind:value={transportation.to_location}
class="input input-bordered w-full"
/>
</div>
{/if}
{:else}
<!-- From Location -->
<div class="mb-4">
<label for="from_location" class="label">
<span class="label-text">{$t('transportation.from_location')}</span>
</label>
<input
type="text"
id="from_location"
name="from_location"
bind:value={transportation.from_location}
class="input input-bordered w-full"
/>
</div>
<!-- To Location -->
<div class="mb-4">
<label for="to_location" class="label">
<span class="label-text">{$t('transportation.to_location')}</span>
</label>
<input
type="text"
id="to_location"
name="to_location"
bind:value={transportation.to_location}
class="input input-bordered w-full"
/>
</div>
<button type="button" class="btn btn-primary mt-2" on:click={geocode}>
Fetch Location Information
</button>
{/if}
<div class="mt-4">
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
standardControls
>
<!-- MapEvents gives you access to map events even from other components inside the map,
where you might not have access to the top-level `MapLibre` component. In this case
it would also work to just use on:click on the MapLibre component itself. -->
<!-- @ts-ignore -->
{#if transportation.origin_latitude && transportation.origin_longitude}
<DefaultMarker
lngLat={[transportation.origin_longitude, transportation.origin_latitude]}
/>
{/if}
{#if transportation.destination_latitude && transportation.destination_longitude}
<DefaultMarker
lngLat={[
transportation.destination_longitude,
transportation.destination_latitude
]}
/>
{/if}
</MapLibre>
<!-- button to clear to and from location -->
</div>
{#if transportation.from_location || transportation.to_location}
<button
type="button"
class="btn btn-error btn-sm mt-2"
on:click={() => {
transportation.from_location = '';
transportation.to_location = '';
starting_airport = '';
ending_airport = '';
transportation.origin_latitude = NaN;
transportation.origin_longitude = NaN;
transportation.destination_latitude = NaN;
transportation.destination_longitude = NaN;
}}
>
{$t('adventures.clear_location')}
</button>
{/if}
</div>
</div>
<!-- Form Actions -->
<div class="mt-4">
<button type="submit" class="btn btn-primary">
{$t('notes.save')}
</button>
<button type="button" class="btn" on:click={close}>
{$t('about.close')}
</button>
</div>
</form>
</div>
</div>
</dialog>

View file

@ -17,34 +17,46 @@
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl"
>
<div class="card-body">
<div>
<!-- Profile Picture and User Info -->
<div class="flex flex-col items-center">
{#if user.profile_pic}
<div class="avatar">
<div class="w-24 rounded-full">
<div class="avatar mb-4">
<div class="w-24 rounded-full ring ring-primary ring-offset-neutral ring-offset-2">
<img src={user.profile_pic} alt={user.username} />
</div>
</div>
{/if}
<h2 class="card-title overflow-ellipsis">{user.first_name} {user.last_name}</h2>
<h2 class="card-title text-center text-lg font-bold">
{user.first_name}
{user.last_name}
</h2>
<p class="text-sm text-center">{user.username}</p>
<!-- Admin Badge -->
{#if user.is_staff}
<div class="badge badge-primary mt-2">Admin</div>
{/if}
</div>
<p class="text-sm text-neutral-content">{user.username}</p>
{#if user.is_staff}
<div class="badge badge-primary">Admin</div>
{/if}
<!-- member since -->
<div class="flex items-center space-x-2">
<Calendar class="w-4 h-4 mr-1" />
<p class="text-sm text-neutral-content">
<!-- Member Since -->
<div class="flex items-center justify-center mt-4 space-x-2 text-sm">
<Calendar class="w-5 h-5 text-primary" />
<p>
{user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''}
</p>
</div>
<div class="card-actions justify-end">
<!-- Card Actions -->
<div class="card-actions justify-center mt-6">
{#if !sharing}
<button class="btn btn-primary" on:click={() => goto(`/user/${user.uuid}`)}>View</button>
<button class="btn btn-primary" on:click={() => goto(`/user/${user.uuid}`)}>
View Profile
</button>
{:else if shared_with && !shared_with.includes(user.uuid)}
<button class="btn btn-primary" on:click={() => dispatch('share', user)}>Share</button>
<button class="btn btn-success" on:click={() => dispatch('share', user)}> Share </button>
{:else}
<button class="btn btn-primary" on:click={() => dispatch('unshare', user)}>Unshare</button>
<button class="btn btn-error" on:click={() => dispatch('unshare', user)}> Unshare </button>
{/if}
</div>
</div>

View file

@ -1,4 +1,4 @@
export let appVersion = 'Web v0.7.1';
export let appVersion = 'v0.7.1';
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.7.1';
export let appTitle = 'AdventureLog';
export let copyrightYear = '2024';
export let copyrightYear = '2023-2025';

View file

@ -289,6 +289,37 @@ export function getAdventureTypeLabel(type: string) {
}
export function getRandomBackground() {
const today = new Date();
// Special dates for specific backgrounds
// New Years week
const newYearsStart = new Date(today.getFullYear() - 1, 11, 31);
newYearsStart.setHours(0, 0, 0, 0);
const newYearsEnd = new Date(today.getFullYear(), 0, 7);
newYearsEnd.setHours(23, 59, 59, 999);
if (today >= newYearsStart && today <= newYearsEnd) {
return {
url: 'backgrounds/adventurelog_new_year.webp',
author: 'Roven Images',
location: "Happy New Year's from the AdventureLog team!"
} as Background;
}
// Christmas 12/24 - 12/25
const christmasStart = new Date(today.getFullYear(), 11, 24);
christmasStart.setHours(0, 0, 0, 0);
const christmasEnd = new Date(today.getFullYear(), 11, 25);
christmasEnd.setHours(23, 59, 59, 999);
if (today >= christmasStart && today <= christmasEnd) {
return {
url: 'backgrounds/adventurelog_christmas.webp',
author: 'Annie Spratt',
location: 'Merry Christmas from the AdventureLog team!'
} as Background;
}
const randomIndex = Math.floor(Math.random() * randomBackgrounds.backgrounds.length);
return randomBackgrounds.backgrounds[randomIndex] as Background;
}

View file

@ -19,6 +19,11 @@
"url": "backgrounds/adventurelog_showcase_4.webp",
"author": "Sean Morley",
"location": "Great Sand Dunes National Park, Colorado, USA"
},
{
"url": "backgrounds/adventurelog_showcase_5.webp",
"author": "Sean Morley",
"location": "Hoboken, New Jersey, USA"
}
]
}

View file

@ -86,14 +86,14 @@ export type Collection = {
description: string;
is_public: boolean;
adventures: Adventure[];
created_at?: string;
start_date?: string;
end_date?: string;
created_at?: string | null;
start_date: string | null;
end_date: string | null;
transportations?: Transportation[];
notes?: Note[];
checklists?: Checklist[];
is_archived?: boolean;
shared_with: string[];
shared_with: string[] | undefined;
link?: string | null;
};
@ -127,8 +127,12 @@ export type Transportation = {
flight_number: string | null;
from_location: string | null;
to_location: string | null;
origin_latitude: number | null;
origin_longitude: number | null;
destination_latitude: number | null;
destination_longitude: number | null;
is_public: boolean;
collection: Collection | null;
collection: Collection | null | string;
created_at: string; // ISO 8601 date string
updated_at: string; // ISO 8601 date string
};

View file

@ -195,7 +195,27 @@
"emoji_picker": "Emoji-Picker",
"hide": "Verstecken",
"show": "Zeigen",
"download_calendar": "Kalender herunterladen"
"download_calendar": "Kalender herunterladen",
"md_instructions": "Schreiben Sie hier Ihren Abschlag...",
"preview": "Vorschau",
"checklist_delete_confirm": "Sind Sie sicher, dass Sie diese Checkliste löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
"clear_location": "Standort löschen",
"date_information": "Datumsinformationen",
"delete_checklist": "Checkliste löschen",
"delete_note": "Notiz löschen",
"delete_transportation": "Transport löschen",
"end": "Ende",
"ending_airport": "Endflughafen",
"flight_information": "Fluginformationen",
"from": "Aus",
"no_location_found": "Kein Standort gefunden",
"note_delete_confirm": "Sind Sie sicher, dass Sie diese Notiz löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
"out_of_range": "Nicht im Datumsbereich der Reiseroute",
"show_region_labels": "Regionsbeschriftungen anzeigen",
"start": "Start",
"starting_airport": "Startflughafen",
"to": "Zu",
"transportation_delete_confirm": "Sind Sie sicher, dass Sie diesen Transport löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden."
},
"home": {
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
@ -362,7 +382,9 @@
"item_cannot_be_empty": "Das Element darf nicht leer sein",
"items": "Artikel",
"new_item": "Neuer Artikel",
"save": "Speichern"
"save": "Speichern",
"checklist_viewer": "Checklisten-Viewer",
"new_checklist": "Neue Checkliste"
},
"collection": {
"collection_created": "Sammlung erfolgreich erstellt!",
@ -371,7 +393,8 @@
"edit_collection": "Sammlung bearbeiten",
"error_creating_collection": "Fehler beim Erstellen der Sammlung",
"error_editing_collection": "Fehler beim Bearbeiten der Sammlung",
"new_collection": "Neue Kollektion"
"new_collection": "Neue Kollektion",
"public_collection": "Öffentliche Sammlung"
},
"notes": {
"add_a_link": "Fügen Sie einen Link hinzu",
@ -384,7 +407,8 @@
"note_public": "Diese Notiz ist öffentlich, da sie sich in einer öffentlichen Sammlung befindet.",
"open": "Offen",
"save": "Speichern",
"invalid_url": "Ungültige URL"
"invalid_url": "Ungültige URL",
"note_viewer": "Notizenbetrachter"
},
"transportation": {
"date_and_time": "Datum",

View file

@ -64,6 +64,12 @@
"no_image_found": "No image found",
"collection_link_error": "Error linking adventure to collection",
"adventure_delete_confirm": "Are you sure you want to delete this adventure? This action cannot be undone.",
"checklist_delete_confirm": "Are you sure you want to delete this checklist? This action cannot be undone.",
"note_delete_confirm": "Are you sure you want to delete this note? This action cannot be undone.",
"transportation_delete_confirm": "Are you sure you want to delete this transportation? This action cannot be undone.",
"delete_checklist": "Delete Checklist",
"delete_note": "Delete Note",
"delete_transportation": "Delete Transportation",
"open_details": "Open Details",
"edit_adventure": "Edit Adventure",
"remove_from_collection": "Remove from Collection",
@ -97,6 +103,7 @@
"rating": "Rating",
"my_images": "My Images",
"add_an_activity": "Add an activity",
"show_region_labels": "Show Region Labels",
"no_images": "No Images",
"upload_images_here": "Upload images here",
"share_adventure": "Share this Adventure!",
@ -216,8 +223,21 @@
"copy_failed": "Copy failed",
"show": "Show",
"hide": "Hide",
"clear_location": "Clear Location",
"starting_airport": "Starting Airport",
"ending_airport": "Ending Airport",
"no_location_found": "No location found",
"from": "From",
"to": "To",
"start": "Start",
"end": "End",
"emoji_picker": "Emoji Picker",
"download_calendar": "Download Calendar",
"date_information": "Date Information",
"flight_information": "Flight Information",
"out_of_range": "Not in itinerary date range",
"preview": "Preview",
"md_instructions": "Write your markdown here...",
"days": "days",
"activities": {
"general": "General 🌍",
@ -271,7 +291,7 @@
"public_profile": "Public Profile",
"public_tooltip": "With a public profile, users can share collections with you and view your profile on the users page.",
"email_required": "Email is required",
"new_password": "New Password",
"new_password": "New Password (6+ characters)",
"both_passwords_required": "Both passwords are required",
"reset_failed": "Failed to reset password"
},
@ -356,7 +376,8 @@
"create": "Create",
"collection_edit_success": "Collection edited successfully!",
"error_editing_collection": "Error editing collection",
"edit_collection": "Edit Collection"
"edit_collection": "Edit Collection",
"public_collection": "Public Collection"
},
"notes": {
"note_deleted": "Note deleted successfully!",
@ -364,6 +385,7 @@
"open": "Open",
"failed_to_save": "Failed to save note",
"note_editor": "Note Editor",
"note_viewer": "Note Viewer",
"editing_note": "Editing note",
"content": "Content",
"save": "Save",
@ -376,7 +398,9 @@
"checklist_delete_error": "Error deleting checklist",
"failed_to_save": "Failed to save checklist",
"checklist_editor": "Checklist Editor",
"checklist_viewer": "Checklist Viewer",
"editing_checklist": "Editing checklist",
"new_checklist": "New Checklist",
"item": "Item",
"items": "Items",
"add_item": "Add Item",

View file

@ -242,7 +242,27 @@
"emoji_picker": "Selector de emojis",
"hide": "Esconder",
"show": "Espectáculo",
"download_calendar": "Descargar Calendario"
"download_calendar": "Descargar Calendario",
"md_instructions": "Escriba su descuento aquí...",
"preview": "Avance",
"checklist_delete_confirm": "¿Está seguro de que desea eliminar esta lista de verificación? \nEsta acción no se puede deshacer.",
"clear_location": "Borrar ubicación",
"date_information": "Información de fecha",
"delete_checklist": "Eliminar lista de verificación",
"delete_note": "Eliminar nota",
"delete_transportation": "Eliminar transporte",
"end": "Fin",
"ending_airport": "Aeropuerto final",
"flight_information": "Información de vuelo",
"from": "De",
"no_location_found": "No se encontró ninguna ubicación",
"note_delete_confirm": "¿Estás seguro de que deseas eliminar esta nota? \nEsta acción no se puede deshacer.",
"out_of_range": "No en el rango de fechas del itinerario",
"show_region_labels": "Mostrar etiquetas de región",
"start": "Comenzar",
"starting_airport": "Aeropuerto de inicio",
"to": "A",
"transportation_delete_confirm": "¿Está seguro de que desea eliminar este transporte? \nEsta acción no se puede deshacer."
},
"worldtravel": {
"all": "Todo",
@ -362,7 +382,9 @@
"item_cannot_be_empty": "El artículo no puede estar vacío",
"items": "Elementos",
"new_item": "Nuevo artículo",
"save": "Ahorrar"
"save": "Ahorrar",
"checklist_viewer": "Visor de lista de verificación",
"new_checklist": "Nueva lista de verificación"
},
"collection": {
"collection_created": "¡Colección creada con éxito!",
@ -371,7 +393,8 @@
"edit_collection": "Editar colección",
"error_creating_collection": "Error al crear la colección",
"error_editing_collection": "Error al editar la colección",
"new_collection": "Nueva colección"
"new_collection": "Nueva colección",
"public_collection": "Colección pública"
},
"notes": {
"add_a_link": "Agregar un enlace",
@ -384,7 +407,8 @@
"note_public": "Esta nota es pública porque está en una colección pública.",
"open": "Abierto",
"save": "Ahorrar",
"invalid_url": "URL no válida"
"invalid_url": "URL no válida",
"note_viewer": "Visor de notas"
},
"transportation": {
"date_and_time": "Fecha",

View file

@ -195,7 +195,27 @@
"emoji_picker": "Sélecteur d'émoticônes",
"hide": "Cacher",
"show": "Montrer",
"download_calendar": "Télécharger le calendrier"
"download_calendar": "Télécharger le calendrier",
"md_instructions": "Écrivez votre démarque ici...",
"preview": "Aperçu",
"checklist_delete_confirm": "Êtes-vous sûr de vouloir supprimer cette liste de contrôle ? \nCette action ne peut pas être annulée.",
"clear_location": "Effacer l'emplacement",
"date_information": "Informations sur les dates",
"delete_checklist": "Supprimer la liste de contrôle",
"delete_note": "Supprimer la note",
"delete_transportation": "Supprimer le transport",
"end": "Fin",
"ending_airport": "Aéroport de fin",
"flight_information": "Informations sur le vol",
"from": "Depuis",
"no_location_found": "Aucun emplacement trouvé",
"note_delete_confirm": "Êtes-vous sûr de vouloir supprimer cette note ? \nCette action ne peut pas être annulée.",
"out_of_range": "Pas dans la plage de dates de l'itinéraire",
"show_region_labels": "Afficher les étiquettes de région",
"start": "Commencer",
"starting_airport": "Aéroport de départ",
"to": "À",
"transportation_delete_confirm": "Etes-vous sûr de vouloir supprimer ce transport ? \nCette action ne peut pas être annulée."
},
"home": {
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
@ -362,7 +382,9 @@
"item_cannot_be_empty": "L'élément ne peut pas être vide",
"items": "Articles",
"new_item": "Nouvel article",
"save": "Sauvegarder"
"save": "Sauvegarder",
"checklist_viewer": "Visionneuse de liste de contrôle",
"new_checklist": "Nouvelle liste de contrôle"
},
"collection": {
"collection_created": "Collection créée avec succès !",
@ -371,7 +393,8 @@
"edit_collection": "Modifier la collection",
"error_creating_collection": "Erreur lors de la création de la collection",
"error_editing_collection": "Erreur lors de la modification de la collection",
"new_collection": "Nouvelle collection"
"new_collection": "Nouvelle collection",
"public_collection": "Collection publique"
},
"notes": {
"add_a_link": "Ajouter un lien",
@ -384,7 +407,8 @@
"note_public": "Cette note est publique car elle fait partie d'une collection publique.",
"open": "Ouvrir",
"save": "Sauvegarder",
"invalid_url": "URL invalide"
"invalid_url": "URL invalide",
"note_viewer": "Visionneuse de notes"
},
"transportation": {
"date_time": "Date de début",

View file

@ -195,7 +195,27 @@
"emoji_picker": "Selettore di emoji",
"hide": "Nascondere",
"show": "Spettacolo",
"download_calendar": "Scarica Calendario"
"download_calendar": "Scarica Calendario",
"md_instructions": "Scrivi qui il tuo ribasso...",
"preview": "Anteprima",
"checklist_delete_confirm": "Sei sicuro di voler eliminare questa lista di controllo? \nQuesta azione non può essere annullata.",
"clear_location": "Cancella posizione",
"date_information": "Informazioni sulla data",
"delete_checklist": "Elimina lista di controllo",
"delete_note": "Elimina nota",
"delete_transportation": "Elimina trasporto",
"end": "FINE",
"ending_airport": "Fine dell'aeroporto",
"flight_information": "Informazioni sul volo",
"from": "Da",
"no_location_found": "Nessuna posizione trovata",
"note_delete_confirm": "Sei sicuro di voler eliminare questa nota? \nQuesta azione non può essere annullata.",
"out_of_range": "Non nell'intervallo di date dell'itinerario",
"show_region_labels": "Mostra etichette regione",
"start": "Inizio",
"starting_airport": "Inizio aeroporto",
"to": "A",
"transportation_delete_confirm": "Sei sicuro di voler eliminare questo trasporto? \nQuesta azione non può essere annullata."
},
"home": {
"desc_1": "Scopri, pianifica ed esplora con facilità",
@ -362,7 +382,9 @@
"item_cannot_be_empty": "L'articolo non può essere vuoto",
"items": "Elementi",
"save": "Salva",
"new_item": "Nuovo articolo"
"new_item": "Nuovo articolo",
"checklist_viewer": "Visualizzatore della lista di controllo",
"new_checklist": "Nuova lista di controllo"
},
"collection": {
"edit_collection": "Modifica raccolta",
@ -371,7 +393,8 @@
"new_collection": "Nuova collezione",
"collection_created": "Collezione creata con successo!",
"collection_edit_success": "Raccolta modificata con successo!",
"create": "Creare"
"create": "Creare",
"public_collection": "Collezione pubblica"
},
"notes": {
"add_a_link": "Aggiungi un collegamento",
@ -384,7 +407,8 @@
"note_public": "Questa nota è pubblica perché è in una collezione pubblica.",
"open": "Aprire",
"save": "Salva",
"invalid_url": "URL non valido"
"invalid_url": "URL non valido",
"note_viewer": "Visualizzatore di note"
},
"transportation": {
"date_and_time": "Data",

View file

@ -195,7 +195,27 @@
"emoji_picker": "Emoji-kiezer",
"hide": "Verbergen",
"show": "Show",
"download_calendar": "Agenda downloaden"
"download_calendar": "Agenda downloaden",
"md_instructions": "Schrijf hier uw korting...",
"preview": "Voorbeeld",
"checklist_delete_confirm": "Weet u zeker dat u deze checklist wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
"clear_location": "Locatie wissen",
"date_information": "Datuminformatie",
"delete_checklist": "Controlelijst verwijderen",
"delete_note": "Notitie verwijderen",
"delete_transportation": "Transport verwijderen",
"end": "Einde",
"flight_information": "Vluchtinformatie",
"from": "Van",
"no_location_found": "Geen locatie gevonden",
"note_delete_confirm": "Weet u zeker dat u deze notitie wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
"out_of_range": "Niet binnen het datumbereik van het reisplan",
"show_region_labels": "Toon regiolabels",
"start": "Begin",
"starting_airport": "Startende luchthaven",
"to": "Naar",
"transportation_delete_confirm": "Weet u zeker dat u dit transport wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
"ending_airport": "Einde luchthaven"
},
"home": {
"desc_1": "Ontdek, plan en verken met gemak",
@ -362,7 +382,9 @@
"item_cannot_be_empty": "Artikel mag niet leeg zijn",
"items": "Artikelen",
"new_item": "Nieuw artikel",
"save": "Opslaan"
"save": "Opslaan",
"checklist_viewer": "Controlelijstviewer",
"new_checklist": "Nieuwe checklist"
},
"collection": {
"collection_created": "Collectie succesvol aangemaakt!",
@ -371,7 +393,8 @@
"edit_collection": "Collectie bewerken",
"error_creating_collection": "Fout bij aanmaken collectie",
"error_editing_collection": "Fout bij bewerken collectie",
"new_collection": "Nieuwe collectie"
"new_collection": "Nieuwe collectie",
"public_collection": "Openbare collectie"
},
"notes": {
"add_a_link": "Voeg een link toe",
@ -384,7 +407,8 @@
"note_public": "Deze opmerking is openbaar omdat deze zich in een openbare collectie bevindt.",
"open": "Open",
"save": "Opslaan",
"invalid_url": "Ongeldige URL"
"invalid_url": "Ongeldige URL",
"note_viewer": "Notitieviewer"
},
"transportation": {
"date_and_time": "Datum",

View file

@ -242,7 +242,27 @@
"emoji_picker": "Wybór emoji",
"hide": "Ukrywać",
"show": "Pokazywać",
"download_calendar": "Pobierz Kalendarz"
"download_calendar": "Pobierz Kalendarz",
"md_instructions": "Napisz tutaj swoją przecenę...",
"preview": "Zapowiedź",
"checklist_delete_confirm": "Czy na pewno chcesz usunąć tę listę kontrolną? \nTej akcji nie można cofnąć.",
"clear_location": "Wyczyść lokalizację",
"date_information": "Informacje o dacie",
"delete_checklist": "Usuń listę kontrolną",
"delete_note": "Usuń notatkę",
"delete_transportation": "Usuń transport",
"end": "Koniec",
"ending_airport": "Kończy się lotnisko",
"flight_information": "Informacje o locie",
"from": "Z",
"no_location_found": "Nie znaleziono lokalizacji",
"note_delete_confirm": "Czy na pewno chcesz usunąć tę notatkę? \nTej akcji nie można cofnąć.",
"out_of_range": "Nie mieści się w zakresie dat planu podróży",
"show_region_labels": "Pokaż etykiety regionów",
"start": "Start",
"starting_airport": "Początkowe lotnisko",
"to": "Do",
"transportation_delete_confirm": "Czy na pewno chcesz usunąć ten transport? \nTej akcji nie można cofnąć."
},
"worldtravel": {
"country_list": "Lista krajów",
@ -356,7 +376,8 @@
"create": "Utwórz",
"collection_edit_success": "Kolekcja została pomyślnie edytowana!",
"error_editing_collection": "Błąd podczas edytowania kolekcji",
"edit_collection": "Edytuj kolekcję"
"edit_collection": "Edytuj kolekcję",
"public_collection": "Kolekcja publiczna"
},
"notes": {
"note_deleted": "Notatka została pomyślnie usunięta!",
@ -369,7 +390,8 @@
"save": "Zapisz",
"note_public": "Ta notatka jest publiczna, ponieważ znajduje się w publicznej kolekcji.",
"add_a_link": "Dodaj link",
"invalid_url": "Nieprawidłowy URL"
"invalid_url": "Nieprawidłowy URL",
"note_viewer": "Przeglądarka notatek"
},
"checklist": {
"checklist_deleted": "Lista kontrolna została pomyślnie usunięta!",
@ -384,7 +406,9 @@
"save": "Zapisz",
"checklist_public": "Ta lista kontrolna jest publiczna, ponieważ znajduje się w publicznej kolekcji.",
"item_cannot_be_empty": "Element nie może być pusty",
"item_already_exists": "Element już istnieje"
"item_already_exists": "Element już istnieje",
"checklist_viewer": "Przeglądarka listy kontrolnej",
"new_checklist": "Nowa lista kontrolna"
},
"transportation": {
"transportation_deleted": "Transport został pomyślnie usunięty!",

View file

@ -195,7 +195,27 @@
"emoji_picker": "Emoji-väljare",
"hide": "Dölja",
"show": "Visa",
"download_calendar": "Ladda ner kalender"
"download_calendar": "Ladda ner kalender",
"md_instructions": "Skriv din avskrivning här...",
"preview": "Förhandsvisning",
"checklist_delete_confirm": "Är du säker på att du vill ta bort den här checklistan? \nDenna åtgärd kan inte ångras.",
"clear_location": "Rensa plats",
"date_information": "Datuminformation",
"delete_checklist": "Ta bort checklista",
"delete_note": "Ta bort anteckning",
"delete_transportation": "Ta bort Transport",
"end": "Avsluta",
"ending_airport": "Slutar flygplats",
"flight_information": "Flyginformation",
"from": "Från",
"no_location_found": "Ingen plats hittades",
"note_delete_confirm": "Är du säker på att du vill ta bort den här anteckningen? \nDenna åtgärd kan inte ångras.",
"out_of_range": "Inte inom resplanens datumintervall",
"show_region_labels": "Visa regionetiketter",
"start": "Start",
"starting_airport": "Startar flygplats",
"to": "Till",
"transportation_delete_confirm": "Är du säker på att du vill ta bort denna transport? \nDenna åtgärd kan inte ångras."
},
"home": {
"desc_1": "Upptäck, planera och utforska med lätthet",
@ -362,7 +382,9 @@
"item_cannot_be_empty": "Objektet får inte vara tomt",
"items": "Föremål",
"new_item": "Nytt föremål",
"save": "Spara"
"save": "Spara",
"checklist_viewer": "Checklista Viewer",
"new_checklist": "Ny checklista"
},
"collection": {
"collection_created": "Samlingen har skapats!",
@ -371,7 +393,8 @@
"edit_collection": "Redigera samling",
"error_creating_collection": "Det gick inte att skapa samlingen",
"error_editing_collection": "Fel vid redigering av samling",
"new_collection": "Ny samling"
"new_collection": "Ny samling",
"public_collection": "Offentlig samling"
},
"notes": {
"add_a_link": "Lägg till en länk",
@ -384,7 +407,8 @@
"note_public": "Den här anteckningen är offentlig eftersom den finns i en offentlig samling.",
"open": "Öppna",
"save": "Spara",
"invalid_url": "Ogiltig URL"
"invalid_url": "Ogiltig URL",
"note_viewer": "Note Viewer"
},
"transportation": {
"date_and_time": "Datum",

View file

@ -195,7 +195,27 @@
"emoji_picker": "表情符号选择器",
"hide": "隐藏",
"show": "展示",
"download_calendar": "下载日历"
"download_calendar": "下载日历",
"md_instructions": "在这里写下你的标记...",
"preview": "预览",
"checklist_delete_confirm": "您确定要删除此清单吗?\n此操作无法撤消。",
"clear_location": "明确的位置",
"date_information": "日期信息",
"delete_checklist": "删除清单",
"delete_note": "删除注释",
"delete_transportation": "删除交通",
"end": "结尾",
"ending_airport": "结束机场",
"flight_information": "航班信息",
"from": "从",
"no_location_found": "没有找到位置",
"note_delete_confirm": "您确定要删除此注释吗?\n此操作无法撤消。",
"out_of_range": "不在行程日期范围内",
"show_region_labels": "显示区域标签",
"start": "开始",
"starting_airport": "出发机场",
"to": "到",
"transportation_delete_confirm": "您确定要删除此交通工具吗?\n此操作无法撤消。"
},
"home": {
"desc_1": "轻松发现、规划和探索",
@ -362,7 +382,9 @@
"item_cannot_be_empty": "项目不能为空",
"items": "项目",
"new_item": "新商品",
"save": "节省"
"save": "节省",
"checklist_viewer": "清单查看器",
"new_checklist": "新清单"
},
"collection": {
"collection_created": "收藏创建成功!",
@ -371,7 +393,8 @@
"edit_collection": "编辑收藏",
"error_creating_collection": "创建集合时出错",
"error_editing_collection": "编辑集合时出错",
"new_collection": "新系列"
"new_collection": "新系列",
"public_collection": "公共收藏"
},
"notes": {
"add_a_link": "添加链接",
@ -384,7 +407,8 @@
"note_public": "该笔记是公开的,因为它属于公共收藏。",
"open": "打开",
"save": "节省",
"invalid_url": "无效网址"
"invalid_url": "无效网址",
"note_viewer": "笔记查看器"
},
"transportation": {
"date_and_time": "日期",

View file

@ -6,6 +6,11 @@
import Lost from '$lib/assets/undraw_lost.svg';
import { DefaultMarker, MapLibre, Popup } from 'svelte-maplibre';
import { t } from 'svelte-i18n';
import { marked } from 'marked'; // Import the markdown parser
const renderMarkdown = (markdown: string) => {
return marked(markdown);
};
export let data: PageData;
console.log(data);
@ -244,11 +249,12 @@
{/if}
</div>
{#if adventure.description}
<div class="grid gap-2">
<p class="text-sm text-muted-foreground" style="white-space: pre-wrap;">
{adventure.description}
</p>
</div>
<p class="text-sm text-muted-foreground" style="white-space: pre-wrap;"></p>
<article
class="prose overflow-auto h-full max-w-full p-4 border border-base-300 rounded-lg"
>
{@html renderMarkdown(adventure.description)}
</article>
{/if}
</div>
</div>
@ -323,7 +329,7 @@
</div>
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="flex items-center self-center justify-center aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12"
class="flex items-center self-center justify-center aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
standardControls
center={{ lng: adventure.longitude, lat: adventure.latitude }}
zoom={12}

View file

@ -23,6 +23,7 @@
view: 'dayGridMonth',
events: [...dates]
};
console.log(dates);
</script>
<h1 class="text-center text-2xl font-bold">{$t('adventures.adventure_calendar')}</h1>

View file

@ -2,8 +2,8 @@
import { enhance } from '$app/forms';
import { goto } from '$app/navigation';
import CollectionCard from '$lib/components/CollectionCard.svelte';
import EditCollection from '$lib/components/EditCollection.svelte';
import NewCollection from '$lib/components/NewCollection.svelte';
import CollectionLink from '$lib/components/CollectionLink.svelte';
import CollectionModal from '$lib/components/CollectionModal.svelte';
import NotFound from '$lib/components/NotFound.svelte';
import type { Collection } from '$lib/types';
import { t } from 'svelte-i18n';
@ -17,10 +17,10 @@
let currentSort = { attribute: 'name', order: 'asc' };
let isShowingCreateModal: boolean = false;
let newType: string = '';
let resultsPerPage: number = 25;
let isShowingCollectionModal: boolean = false;
let next: string | null = data.props.next || null;
let previous: string | null = data.props.previous || null;
@ -74,33 +74,37 @@
collections = collections.filter((collection) => collection.id !== event.detail);
}
function sort({ attribute, order }: { attribute: string; order: string }) {
currentSort.attribute = attribute;
currentSort.order = order;
if (attribute === 'name') {
if (order === 'asc') {
collections = collections.sort((a, b) => b.name.localeCompare(a.name));
} else {
collections = collections.sort((a, b) => a.name.localeCompare(b.name));
}
// function sort({ attribute, order }: { attribute: string; order: string }) {
// currentSort.attribute = attribute;
// currentSort.order = order;
// if (attribute === 'name') {
// if (order === 'asc') {
// collections = collections.sort((a, b) => b.name.localeCompare(a.name));
// } else {
// collections = collections.sort((a, b) => a.name.localeCompare(b.name));
// }
// }
// }
let collectionToEdit: Collection | null = null;
function saveOrCreate(event: CustomEvent<Collection>) {
if (collections.find((collection) => collection.id === event.detail.id)) {
collections = collections.map((collection) => {
if (collection.id === event.detail.id) {
return event.detail;
}
return collection;
});
} else {
collections = [event.detail, ...collections];
}
}
let collectionToEdit: Collection;
let isEditModalOpen: boolean = false;
function deleteAdventure(event: CustomEvent<string>) {
collections = collections.filter((adventure) => adventure.id !== event.detail);
}
function createAdventure(event: CustomEvent<Collection>) {
collections = [event.detail, ...collections];
isShowingCreateModal = false;
isShowingCollectionModal = false;
}
function editCollection(event: CustomEvent<Collection>) {
collectionToEdit = event.detail;
isEditModalOpen = true;
isShowingCollectionModal = true;
}
function saveEdit(event: CustomEvent<Collection>) {
@ -110,7 +114,7 @@
}
return adventure;
});
isEditModalOpen = false;
isShowingCollectionModal = false;
}
let sidebarOpen = false;
@ -120,18 +124,14 @@
}
</script>
{#if isShowingCreateModal}
<NewCollection on:create={createAdventure} on:close={() => (isShowingCreateModal = false)} />
{/if}
{#if isEditModalOpen}
<EditCollection
{#if isShowingCollectionModal}
<CollectionModal
{collectionToEdit}
on:close={() => (isEditModalOpen = false)}
on:close={() => (isShowingCollectionModal = false)}
on:saveEdit={saveEdit}
on:save={saveOrCreate}
/>
{/if}
<div class="fixed bottom-4 right-4 z-[999]">
<div class="flex flex-row items-center justify-center gap-4">
<div class="dropdown dropdown-top dropdown-end">
@ -147,17 +147,13 @@
<button
class="btn btn-primary"
on:click={() => {
isShowingCreateModal = true;
collectionToEdit = null;
isShowingCollectionModal = true;
newType = 'visited';
}}
>
{$t(`adventures.collection`)}</button
>
<!-- <button
class="btn btn-primary"
on:click={() => (isShowingNewTrip = true)}>Trip Planner</button
> -->
</ul>
</div>
</div>
@ -267,6 +263,6 @@
</div>
<svelte:head>
<title>{$t(`navbar.collections`)}</title>
<title>Collections</title>
<meta name="description" content="View your adventure collections." />
</svelte:head>

View file

@ -2,18 +2,23 @@
import type { Adventure, Checklist, Collection, Note, Transportation } from '$lib/types';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import { goto } from '$app/navigation';
import Lost from '$lib/assets/undraw_lost.svg';
import { marked } from 'marked'; // Import the markdown parser
import { t } from 'svelte-i18n';
// @ts-ignore
import Calendar from '@event-calendar/core';
// @ts-ignore
import TimeGrid from '@event-calendar/time-grid';
// @ts-ignore
import DayGrid from '@event-calendar/day-grid';
import Plus from '~icons/mdi/plus';
import AdventureCard from '$lib/components/AdventureCard.svelte';
import AdventureLink from '$lib/components/AdventureLink.svelte';
import NotFound from '$lib/components/NotFound.svelte';
import { DefaultMarker, MapLibre, Popup } from 'svelte-maplibre';
import { DefaultMarker, MapLibre, Marker, Popup } from 'svelte-maplibre';
import TransportationCard from '$lib/components/TransportationCard.svelte';
import EditTransportation from '$lib/components/EditTransportation.svelte';
import NewTransportation from '$lib/components/NewTransportation.svelte';
import NoteCard from '$lib/components/NoteCard.svelte';
import NoteModal from '$lib/components/NoteModal.svelte';
@ -26,12 +31,79 @@
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
import AdventureModal from '$lib/components/AdventureModal.svelte';
import TransportationModal from '$lib/components/TransportationModal.svelte';
export let data: PageData;
console.log(data);
const renderMarkdown = (markdown: string) => {
return marked(markdown);
};
let collection: Collection;
// add christmas and new years
// dates = Array.from({ length: 25 }, (_, i) => {
// const date = new Date();
// date.setMonth(11);
// date.setDate(i + 1);
// return {
// id: i.toString(),
// start: date.toISOString(),
// end: date.toISOString(),
// title: '🎄'
// };
// });
let dates: Array<{
id: string;
start: string;
end: string;
title: string;
backgroundColor?: string;
}> = [];
// Initialize calendar plugins and options
let plugins = [TimeGrid, DayGrid];
let options = {
view: 'dayGridMonth',
events: dates // Assign `dates` reactively
};
// Compute `dates` array reactively
$: {
dates = [];
if (adventures) {
dates = dates.concat(
adventures.flatMap((adventure) =>
adventure.visits.map((visit) => ({
id: adventure.id,
start: visit.start_date || '', // Ensure it's a string
end: visit.end_date || visit.start_date || '', // Ensure it's a string
title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '')
}))
)
);
}
if (transportations) {
dates = dates.concat(
transportations.map((transportation) => ({
id: transportation.id,
start: transportation.date || '', // Ensure it's a string
end: transportation.end_date || transportation.date || '', // Ensure it's a string
title: transportation.name + (transportation.type ? ` (${transportation.type})` : '')
}))
);
}
// Update `options.events` when `dates` changes
options = { ...options, events: dates };
}
let currentView: string = 'itinerary';
let adventures: Adventure[] = [];
let numVisited: number = 0;
@ -43,6 +115,29 @@
let numberOfDays: number = NaN;
function getTransportationEmoji(type: string): string {
switch (type) {
case 'car':
return '🚗';
case 'plane':
return '✈️';
case 'train':
return '🚆';
case 'bus':
return '🚌';
case 'boat':
return '⛵';
case 'bike':
return '🚲';
case 'walking':
return '🚶';
case 'other':
return '🚀';
default:
return '🚀';
}
}
$: {
numAdventures = adventures.length;
numVisited = adventures.filter((adventure) => adventure.is_visited).length;
@ -76,6 +171,11 @@
if (collection.checklists) {
checklists = collection.checklists;
}
if (!collection.start_date) {
currentView = 'all';
} else {
currentView = 'itinerary';
}
});
function deleteAdventure(event: CustomEvent<string>) {
@ -108,9 +208,8 @@
}
let adventureToEdit: Adventure | null = null;
let transportationToEdit: Transportation;
let transportationToEdit: Transportation | null = null;
let isAdventureModalOpen: boolean = false;
let isTransportationEditModalOpen: boolean = false;
let isNoteModalOpen: boolean = false;
let noteToEdit: Note | null;
let checklistToEdit: Checklist | null;
@ -122,17 +221,12 @@
isAdventureModalOpen = true;
}
function saveNewTransportation(event: CustomEvent<Transportation>) {
transportations = transportations.map((transportation) => {
if (transportation.id === event.detail.id) {
return event.detail;
}
return transportation;
});
isTransportationEditModalOpen = false;
function editTransportation(event: CustomEvent<Transportation>) {
transportationToEdit = event.detail;
isShowingTransportationModal = true;
}
function saveOrCreate(event: CustomEvent<Adventure>) {
function saveOrCreateAdventure(event: CustomEvent<Adventure>) {
if (adventures.find((adventure) => adventure.id === event.detail.id)) {
adventures = adventures.map((adventure) => {
if (adventure.id === event.detail.id) {
@ -145,6 +239,22 @@
}
isAdventureModalOpen = false;
}
function saveOrCreateTransportation(event: CustomEvent<Transportation>) {
if (transportations.find((transportation) => transportation.id === event.detail.id)) {
// Update existing transportation
transportations = transportations.map((transportation) => {
if (transportation.id === event.detail.id) {
return event.detail;
}
return transportation;
});
} else {
// Create new transportation
transportations = [event.detail, ...transportations];
}
isShowingTransportationModal = false;
}
</script>
{#if isShowingLinkModal}
@ -157,13 +267,12 @@
/>
{/if}
{#if isTransportationEditModalOpen}
<EditTransportation
{#if isShowingTransportationModal}
<TransportationModal
{transportationToEdit}
on:close={() => (isTransportationEditModalOpen = false)}
on:saveEdit={saveNewTransportation}
startDate={collection.start_date}
endDate={collection.end_date}
on:close={() => (isShowingTransportationModal = false)}
on:save={saveOrCreateTransportation}
{collection}
/>
{/if}
@ -171,7 +280,7 @@
<AdventureModal
{adventureToEdit}
on:close={() => (isAdventureModalOpen = false)}
on:save={saveOrCreate}
on:save={saveOrCreateAdventure}
{collection}
/>
{/if}
@ -221,49 +330,13 @@
/>
{/if}
{#if isShowingTransportationModal}
<NewTransportation
on:close={() => (isShowingTransportationModal = false)}
on:add={(event) => {
transportations = [event.detail, ...transportations];
isShowingTransportationModal = false;
}}
{collection}
startDate={collection.start_date}
endDate={collection.end_date}
/>
{/if}
{#if notFound}
<div
class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8 -mt-20"
>
<div class="mx-auto max-w-md text-center">
<div class="flex items-center justify-center">
<img src={Lost} alt="Lost" class="w-1/2" />
</div>
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
{$t('adventures.not_found')}
</h1>
<p class="mt-4 text-muted-foreground">
{$t('adventures.not_found_desc')}
</p>
<div class="mt-6">
<button class="btn btn-primary" on:click={() => goto('/')}
>{$t('adventures.homepage')}</button
>
</div>
</div>
</div>
{/if}
{#if !collection && !notFound}
<div class="flex justify-center items-center w-full mt-16">
<span class="loading loading-spinner w-24 h-24"></span>
</div>
{/if}
{#if collection}
{#if data.user && data.user.uuid && (data.user.uuid == collection.user_id || collection.shared_with.includes(data.user.uuid)) && !collection.is_archived}
{#if data.user && data.user.uuid && (data.user.uuid == collection.user_id || (collection.shared_with && collection.shared_with.includes(data.user.uuid))) && !collection.is_archived}
<div class="fixed bottom-4 right-4 z-[999]">
<div class="flex flex-row items-center justify-center gap-4">
<div class="dropdown dropdown-top dropdown-end">
@ -300,6 +373,8 @@
<button
class="btn btn-primary"
on:click={() => {
// Reset the transportation object for creating a new one
transportationToEdit = null;
isShowingTransportationModal = true;
newType = '';
}}
@ -367,9 +442,21 @@
</div>
{/if}
{#if collection.description}
<p class="text-center text-lg mb-2">{collection.description}</p>
{#if collection && !collection.start_date && adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0}
<NotFound error={undefined} />
{/if}
{#if collection.description}
<div class="flex justify-center mt-4 max-w-screen-lg mx-auto">
<article
class="prose overflow-auto max-h-96 max-w-full p-4 border border-base-300 rounded-lg bg-base-300 mb-4"
style="overflow-y: auto;"
>
{@html renderMarkdown(collection.description)}
</article>
</div>
{/if}
{#if adventures.length > 0}
<div class="flex items-center justify-center mb-4">
<div class="stats shadow bg-base-300">
@ -386,219 +473,332 @@
</div>
{/if}
{#if adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0}
<NotFound error={undefined} />
{/if}
{#if adventures.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-2">{$t('adventures.linked_adventures')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each adventures as adventure}
<AdventureCard
user={data.user}
on:edit={editAdventure}
on:delete={deleteAdventure}
{adventure}
{collection}
/>
{/each}
{#if collection.id}
<div class="flex justify-center mx-auto">
<!-- svelte-ignore a11y-missing-attribute -->
<div role="tablist" class="tabs tabs-boxed tabs-lg max-w-xl">
<!-- svelte-ignore a11y-missing-attribute -->
{#if collection.start_date}
<a
role="tab"
class="tab {currentView === 'itinerary' ? 'tab-active' : ''}"
tabindex="0"
on:click={() => (currentView = 'itinerary')}
on:keydown={(e) => e.key === 'Enter' && (currentView = 'itinerary')}>Itinerary</a
>
{/if}
<a
role="tab"
class="tab {currentView === 'all' ? 'tab-active' : ''}"
tabindex="0"
on:click={() => (currentView = 'all')}
on:keydown={(e) => e.key === 'Enter' && (currentView = 'all')}>All Linked Items</a
>
<a
role="tab"
class="tab {currentView === 'calendar' ? 'tab-active' : ''}"
tabindex="0"
on:click={() => (currentView = 'calendar')}
on:keydown={(e) => e.key === 'Enter' && (currentView = 'calendar')}>Calendar</a
>
<a
role="tab"
class="tab {currentView === 'map' ? 'tab-active' : ''}"
tabindex="0"
on:click={() => (currentView = 'map')}
on:keydown={(e) => e.key === 'Enter' && (currentView = 'map')}>Map</a
>
</div>
</div>
{/if}
{#if transportations.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.transportations')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each transportations as transportation}
<TransportationCard
{transportation}
user={data?.user}
on:delete={(event) => {
transportations = transportations.filter((t) => t.id != event.detail);
}}
on:edit={(event) => {
transportationToEdit = event.detail;
isTransportationEditModalOpen = true;
}}
{collection}
/>
{/each}
</div>
{/if}
{#if currentView == 'all'}
{#if adventures.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-2">{$t('adventures.linked_adventures')}</h1>
{#if notes.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.notes')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each notes as note}
<NoteCard
{note}
user={data.user || null}
on:edit={(event) => {
noteToEdit = event.detail;
isNoteModalOpen = true;
}}
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
{collection}
/>
{/each}
</div>
{/if}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each adventures as adventure}
<AdventureCard
user={data.user}
on:edit={editAdventure}
on:delete={deleteAdventure}
{adventure}
{collection}
/>
{/each}
</div>
{/if}
{#if checklists.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.checklists')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each checklists as checklist}
<ChecklistCard
{checklist}
user={data.user || null}
on:delete={(event) => {
checklists = checklists.filter((n) => n.id != event.detail);
}}
on:edit={(event) => {
checklistToEdit = event.detail;
isShowingChecklistModal = true;
}}
{collection}
/>
{/each}
</div>
{#if transportations.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.transportations')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each transportations as transportation}
<TransportationCard
{transportation}
user={data?.user}
on:delete={(event) => {
transportations = transportations.filter((t) => t.id != event.detail);
}}
on:edit={editTransportation}
{collection}
/>
{/each}
</div>
{/if}
{#if notes.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.notes')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each notes as note}
<NoteCard
{note}
user={data.user || null}
on:edit={(event) => {
noteToEdit = event.detail;
isNoteModalOpen = true;
}}
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
{collection}
/>
{/each}
</div>
{/if}
{#if checklists.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.checklists')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each checklists as checklist}
<ChecklistCard
{checklist}
user={data.user || null}
on:delete={(event) => {
checklists = checklists.filter((n) => n.id != event.detail);
}}
on:edit={(event) => {
checklistToEdit = event.detail;
isShowingChecklistModal = true;
}}
{collection}
/>
{/each}
</div>
{/if}
<!-- if none found -->
{#if adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0}
<NotFound error={undefined} />
{/if}
{/if}
{#if collection.start_date && collection.end_date}
<div class="divider"></div>
<h1 class="text-center font-bold text-4xl mt-4">{$t('adventures.itineary_by_date')}</h1>
{#if numberOfDays}
<p class="text-center text-lg pl-16 pr-16">
{$t('adventures.duration')}: {numberOfDays}
{$t('adventures.days')}
</p>
{/if}
<p class="text-center text-lg pl-16 pr-16">
Dates: {new Date(collection.start_date).toLocaleDateString(undefined, { timeZone: 'UTC' })} - {new Date(
collection.end_date
).toLocaleDateString(undefined, { timeZone: 'UTC' })}
</p>
{#each Array(numberOfDays) as _, i}
{@const startDate = new Date(collection.start_date)}
{@const tempDate = new Date(startDate.getTime())}
<!-- Clone startDate -->
{@const adjustedDate = new Date(tempDate.setUTCDate(tempDate.getUTCDate() + i))}
<!-- Add i days in UTC -->
{@const dateString = adjustedDate.toISOString().split('T')[0]}
{@const dayAdventures =
groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays)[
dateString
] || []}
{@const dayTransportations =
groupTransportationsByDate(transportations, new Date(collection.start_date), numberOfDays)[
dateString
] || []}
{@const dayNotes =
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] || []}
{@const dayChecklists =
groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays)[
dateString
] || []}
<h2 class="text-center font-bold text-3xl mt-4">
{$t('adventures.day')}
{i + 1}
</h2>
<h3 class="text-center text-xl mb-2">
{adjustedDate.toLocaleDateString(undefined, { timeZone: 'UTC' })}
</h3>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#if dayAdventures.length > 0}
{#each dayAdventures as adventure}
<AdventureCard
user={data.user}
on:edit={editAdventure}
on:delete={deleteAdventure}
{adventure}
/>
{/each}
{/if}
{#if dayTransportations.length > 0}
{#each dayTransportations as transportation}
<TransportationCard
{transportation}
user={data?.user}
on:delete={(event) => {
transportations = transportations.filter((t) => t.id != event.detail);
}}
on:edit={(event) => {
transportationToEdit = event.detail;
isTransportationEditModalOpen = true;
}}
/>
{/each}
{/if}
{#if dayNotes.length > 0}
{#each dayNotes as note}
<NoteCard
{note}
user={data.user || null}
on:edit={(event) => {
noteToEdit = event.detail;
isNoteModalOpen = true;
}}
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
/>
{/each}
{/if}
{#if dayChecklists.length > 0}
{#each dayChecklists as checklist}
<ChecklistCard
{checklist}
user={data.user || null}
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
on:edit={(event) => {
checklistToEdit = event.detail;
isShowingChecklistModal = true;
}}
/>
{/each}
{/if}
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0}
<p class="text-center text-lg mt-2">{$t('adventures.nothing_planned')}</p>
{/if}
</div>
{/each}
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="flex items-center self-center justify-center aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 mt-4"
standardControls
>
<!-- MapEvents gives you access to map events even from other components inside the map,
where you might not have access to the top-level `MapLibre` component. In this case
it would also work to just use on:click on the MapLibre component itself. -->
<!-- <MapEvents on:click={addMarker} /> -->
{#each adventures as adventure}
{#if adventure.longitude && adventure.latitude}
<DefaultMarker lngLat={{ lng: adventure.longitude, lat: adventure.latitude }}>
<Popup openOn="click" offset={[0, -10]}>
<div class="text-lg text-black font-bold">{adventure.name}</div>
<p class="font-semibold text-black text-md">
{adventure.category?.display_name + ' ' + adventure.category?.icon}
{#if currentView == 'itinerary'}
<div class="hero bg-base-200 py-8 mt-8">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold mb-4">{$t('adventures.itineary_by_date')}</h1>
{#if numberOfDays}
<p class="text-lg mb-2">
{$t('adventures.duration')}:
<span class="badge badge-primary">{numberOfDays} {$t('adventures.days')}</span>
</p>
</Popup>
</DefaultMarker>
{/if}
{/each}
</MapLibre>
{/if}
<p class="text-lg">
Dates: <span class="font-semibold"
>{new Date(collection.start_date).toLocaleDateString(undefined, {
timeZone: 'UTC'
})} -
{new Date(collection.end_date).toLocaleDateString(undefined, {
timeZone: 'UTC'
})}</span
>
</p>
</div>
</div>
</div>
<div class="container mx-auto px-4">
{#each Array(numberOfDays) as _, i}
{@const startDate = new Date(collection.start_date)}
{@const tempDate = new Date(startDate.getTime())}
{@const adjustedDate = new Date(tempDate.setUTCDate(tempDate.getUTCDate() + i))}
{@const dateString = adjustedDate.toISOString().split('T')[0]}
{@const dayAdventures =
groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays)[
dateString
] || []}
{@const dayTransportations =
groupTransportationsByDate(
transportations,
new Date(collection.start_date),
numberOfDays
)[dateString] || []}
{@const dayNotes =
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] ||
[]}
{@const dayChecklists =
groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays)[
dateString
] || []}
<div class="card bg-base-100 shadow-xl my-8">
<div class="card-body bg-base-200">
<h2 class="card-title text-3xl justify-center g">
{$t('adventures.day')}
{i + 1}
<div class="badge badge-lg">
{adjustedDate.toLocaleDateString(undefined, { timeZone: 'UTC' })}
</div>
</h2>
<div class="divider"></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#if dayAdventures.length > 0}
{#each dayAdventures as adventure}
<AdventureCard
user={data.user}
on:edit={editAdventure}
on:delete={deleteAdventure}
{adventure}
/>
{/each}
{/if}
{#if dayTransportations.length > 0}
{#each dayTransportations as transportation}
<TransportationCard
{transportation}
user={data?.user}
on:delete={(event) => {
transportations = transportations.filter((t) => t.id != event.detail);
}}
on:edit={(event) => {
transportationToEdit = event.detail;
isShowingTransportationModal = true;
}}
/>
{/each}
{/if}
{#if dayNotes.length > 0}
{#each dayNotes as note}
<NoteCard
{note}
user={data.user || null}
on:edit={(event) => {
noteToEdit = event.detail;
isNoteModalOpen = true;
}}
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
/>
{/each}
{/if}
{#if dayChecklists.length > 0}
{#each dayChecklists as checklist}
<ChecklistCard
{checklist}
user={data.user || null}
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
on:edit={(event) => {
checklistToEdit = event.detail;
isShowingChecklistModal = true;
}}
/>
{/each}
{/if}
</div>
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0}
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
{/if}
</div>
</div>
{/each}
</div>
{/if}
{/if}
{#if currentView == 'map'}
<div class="card bg-base-200 shadow-xl my-8 mx-auto w-10/12">
<div class="card-body">
<h2 class="card-title text-3xl justify-center mb-4">Trip Map</h2>
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-full rounded-lg"
standardControls
>
{#each adventures as adventure}
{#if adventure.longitude && adventure.latitude}
<DefaultMarker lngLat={{ lng: adventure.longitude, lat: adventure.latitude }}>
<Popup openOn="click" offset={[0, -10]}>
<div class="text-lg text-black font-bold">{adventure.name}</div>
<p class="font-semibold text-black text-md">
{adventure.category?.display_name + ' ' + adventure.category?.icon}
</p>
</Popup>
</DefaultMarker>
{/if}
{/each}
{#each transportations as transportation}
{#if transportation.destination_latitude && transportation.destination_longitude}
<Marker
lngLat={{
lng: transportation.destination_longitude,
lat: transportation.destination_latitude
}}
class="grid h-8 w-8 place-items-center rounded-full border border-gray-200
bg-red-300 text-black focus:outline-6 focus:outline-black"
>
<span class="text-xl">
{getTransportationEmoji(transportation.type)}
</span>
<Popup openOn="click" offset={[0, -10]}>
<div class="text-lg text-black font-bold">{transportation.name}</div>
<p class="font-semibold text-black text-md">
{transportation.type}
</p>
</Popup>
</Marker>
{/if}
{#if transportation.origin_latitude && transportation.origin_longitude}
<Marker
lngLat={{
lng: transportation.origin_longitude,
lat: transportation.origin_latitude
}}
class="grid h-8 w-8 place-items-center rounded-full border border-gray-200
bg-green-300 text-black focus:outline-6 focus:outline-black"
>
<span class="text-xl">
{getTransportationEmoji(transportation.type)}
</span>
<Popup openOn="click" offset={[0, -10]}>
<div class="text-lg text-black font-bold">{transportation.name}</div>
<p class="font-semibold text-black text-md">
{transportation.type}
</p>
</Popup>
</Marker>
{/if}
{/each}
</MapLibre>
</div>
</div>
{/if}
{#if currentView == 'calendar'}
<div class="card bg-base-200 shadow-xl my-8 mx-auto w-10/12">
<div class="card-body">
<h2 class="card-title text-3xl justify-center mb-4">
{$t('adventures.adventure_calendar')}
</h2>
<Calendar {plugins} {options} />
</div>
</div>
{/if}
{/if}

View file

@ -3,7 +3,6 @@
import { DefaultMarker, MapEvents, MapLibre, Popup, Marker } from 'svelte-maplibre';
import { t } from 'svelte-i18n';
import type { Adventure, VisitedRegion } from '$lib/types.js';
import { getAdventureTypeLabel } from '$lib';
import CardCarousel from '$lib/components/CardCarousel.svelte';
import { goto } from '$app/navigation';
export let data;

View file

@ -11,82 +11,88 @@
total_countries: number;
} | null;
if (data.stats) {
stats = data.stats;
} else {
stats = null;
}
console.log(stats);
stats = data.stats || null;
</script>
{#if data.user.profile_pic}
<div class="avatar flex items-center justify-center">
<div class="w-24 rounded">
<!-- svelte-ignore a11y-missing-attribute -->
<img src={data.user.profile_pic} class="w-24 rounded-full" />
</div>
<section class="min-h-screen bg-base-100 py-8 px-4">
<div class="flex flex-col items-center">
<!-- Profile Picture -->
{#if data.user.profile_pic}
<div class="avatar">
<div
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
>
<img src={data.user.profile_pic} alt="Profile" />
</div>
</div>
{/if}
<!-- User Name -->
{#if data.user && data.user.first_name && data.user.last_name}
<h1 class="text-4xl font-bold text-primary mt-4">
{data.user.first_name}
{data.user.last_name}
</h1>
{/if}
<p class="text-lg text-base-content mt-2">{data.user.username}</p>
<!-- Member Since -->
{#if data.user && data.user.date_joined}
<div class="mt-4 flex items-center text-center text-base-content">
<p class="text-lg font-medium">{$t('profile.member_since')}</p>
<div class="flex items-center ml-2">
<iconify-icon icon="mdi:calendar" class="text-2xl text-primary"></iconify-icon>
<p class="ml-2 text-lg">
{new Date(data.user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })}
</p>
</div>
</div>
{/if}
</div>
{/if}
{#if data.user && data.user.first_name && data.user.last_name}
<h1 class="text-center text-4xl font-bold">
{data.user.first_name}
{data.user.last_name}
</h1>
{/if}
<p class="text-center text-lg mt-2">{data.user.username}</p>
<!-- Stats Section -->
{#if stats}
<div class="divider my-8"></div>
{#if data.user && data.user.date_joined}
<p class="ml-1 text-lg text-center mt-4">{$t('profile.member_since')}</p>
<div class="flex items-center justify-center text-center">
<iconify-icon icon="mdi:calendar" class="text-2xl"></iconify-icon>
<p class="ml-1 text-xl">
{new Date(data.user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })}
</p>
</div>
{/if}
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('profile.user_stats')}
</h2>
{#if stats}
<!-- divider -->
<div class="divider pr-8 pl-8"></div>
<h1 class="text-center text-2xl font-bold mt-8 mb-2">{$t('profile.user_stats')}</h1>
<div class="flex justify-center items-center">
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
<div class="stat">
<div class="stat-title">{$t('navbar.adventures')}</div>
<div class="stat-value text-center">{stats.adventure_count}</div>
<!-- <div class="stat-desc">Jan 1st - Feb 1st</div> -->
</div>
<div class="stat">
<div class="stat-title">{$t('navbar.collections')}</div>
<div class="stat-value text-center">{stats.trips_count}</div>
<!-- <div class="stat-desc">↘︎ 90 (14%)</div> -->
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_countries')}</div>
<div class="stat-value text-center">
{Math.round((stats.country_count / stats.total_countries) * 100)}%
<div class="flex justify-center">
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
<div class="stat">
<div class="stat-title">{$t('navbar.adventures')}</div>
<div class="stat-value text-center">{stats.adventure_count}</div>
</div>
<div class="stat-desc">
{stats.country_count}/{stats.total_countries}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_regions')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}%
<div class="stat">
<div class="stat-title">{$t('navbar.collections')}</div>
<div class="stat-value text-center">{stats.trips_count}</div>
</div>
<div class="stat-desc">
{stats.visited_region_count}/{stats.total_regions}
<div class="stat">
<div class="stat-title">{$t('profile.visited_countries')}</div>
<div class="stat-value text-center">
{Math.round((stats.country_count / stats.total_countries) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.country_count}/{stats.total_countries}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_regions')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_region_count}/{stats.total_regions}
</div>
</div>
</div>
</div>
</div>
{/if}
{/if}
</section>
<svelte:head>
<title>Profile | AdventureLog</title>

View file

@ -110,14 +110,6 @@
id="name"
on:change={() => (property = 'name')}
/>
<input
class="join-item btn"
type="radio"
name="filter"
aria-label={$t('transportation.type')}
id="type"
on:change={() => (property = 'type')}
/>
<input
class="join-item btn"
type="radio"

View file

@ -7,6 +7,7 @@
import { browser } from '$app/environment';
import { t } from 'svelte-i18n';
import TotpModal from '$lib/components/TOTPModal.svelte';
import { appTitle, appVersion } from '$lib/config.js';
export let data;
let user: User;
@ -154,195 +155,220 @@
/>
{/if}
<h1 class="text-center font-extrabold text-4xl mb-6">{$t('settings.settings_page')}</h1>
<h1 class="text-center font-extrabold text-xl">{$t('settings.account_settings')}</h1>
<div class="flex justify-center">
<form
method="post"
action="?/changeDetails"
use:enhance
class="w-full max-w-xs"
enctype="multipart/form-data"
>
<label for="username">{$t('auth.username')}</label>
<input
bind:value={user.username}
name="username"
id="username"
class="block mb-2 input input-bordered w-full max-w-xs"
/><br />
<label for="first_name">{$t('auth.first_name')}</label>
<input
type="text"
bind:value={user.first_name}
name="first_name"
id="first_name"
class="block mb-2 input input-bordered w-full max-w-xs"
/><br />
<label for="last_name">{$t('auth.last_name')}</label>
<input
type="text"
bind:value={user.last_name}
name="last_name"
id="last_name"
class="block mb-2 input input-bordered w-full max-w-xs"
/><br />
<label for="profilePicture">{$t('auth.profile_picture')}</label>
<input
type="file"
name="profile_pic"
id="profile_pic"
class="file-input file-input-bordered w-full max-w-xs mb-2"
/><br />
<div class="form-control">
<div class="tooltip tooltip-info" data-tip={$t('auth.public_tooltip')}>
<label class="label cursor-pointer">
<span class="label-text">{$t('auth.public_profile')}</span>
<div class="container mx-auto p-6 max-w-4xl">
<h1 class="text-3xl font-extrabold text-center mb-6">
{$t('settings.settings_page')}
</h1>
<!-- Account Settings Section -->
<section class="space-y-8">
<h2 class="text-2xl font-semibold text-center">
{$t('settings.account_settings')}
</h2>
<div class=" bg-neutral p-6 rounded-lg shadow-md">
<form
method="post"
action="?/changeDetails"
use:enhance
enctype="multipart/form-data"
class="space-y-6"
>
<div>
<label for="username" class="text-sm font-medium">{$t('auth.username')}</label>
<input
type="text"
id="username"
name="username"
bind:value={user.username}
class="block w-full mt-1 input input-bordered input-primary"
/>
</div>
<div>
<label for="first_name" class="text-sm font-medium">{$t('auth.first_name')}</label>
<input
type="text"
id="first_name"
name="first_name"
bind:value={user.first_name}
class="block w-full mt-1 input input-bordered input-primary"
/>
</div>
<div>
<label for="last_name" class="text-sm font-medium">{$t('auth.last_name')}</label>
<input
type="text"
id="last_name"
name="last_name"
bind:value={user.last_name}
class="block w-full mt-1 input input-bordered input-primary"
/>
</div>
<div>
<label for="profile_pic" class="text-sm font-medium">{$t('auth.profile_picture')}</label>
<input
type="file"
id="profile_pic"
name="profile_pic"
class="file-input file-input-bordered file-input-primary mt-1 w-full"
/>
</div>
<div class="flex items-center">
<input
type="checkbox"
id="public_profile"
name="public_profile"
type="checkbox"
class="toggle"
checked={user.public_profile}
bind:checked={user.public_profile}
class="toggle toggle-primary"
/>
</label>
</div>
<label for="public_profile" class="ml-2 text-sm">{$t('auth.public_profile')}</label>
</div>
<button class="w-full mt-4 btn btn-primary py-2">{$t('settings.update')}</button>
</form>
</div>
<button class="py-2 mt-2 px-4 btn btn-primary">{$t('settings.update')}</button>
</form>
</div>
</section>
{#if $page.form?.message}
<div class="text-center text-error mt-4">
{$t($page.form.message)}
</div>
{/if}
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.password_change')}</h1>
<div class="flex justify-center">
<form action="?/changePassword" method="post" class="w-full max-w-xs" use:enhance>
<input
type="password"
name="current_password"
placeholder={$t('settings.current_password')}
id="current_password"
class="block mb-2 input input-bordered w-full max-w-xs"
/>
<br />
<input
type="password"
name="password1"
placeholder={$t('settings.new_password')}
id="password1"
class="block mb-2 input input-bordered w-full max-w-xs"
/>
<br />
<input
type="password"
name="password2"
id="password2"
placeholder={$t('settings.confirm_new_password')}
class="block mb-2 input input-bordered w-full max-w-xs"
/>
<div class="tooltip tooltip-warning" data-tip={$t('settings.password_change_lopout_warning')}>
<button class="py-2 px-4 btn btn-primary mt-2">{$t('settings.password_change')}</button>
</div>
<br />
</form>
</div>
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.email_change')}</h1>
<div class="flex justify-center mb-4">
<div>
{#each emails as email}
<p class="mb-2">
{email.email}
{#if email.verified}
<div class="badge badge-success">{$t('settings.verified')}</div>
{:else}
<div class="badge badge-error">{$t('settings.not_verified')}</div>
{/if}
{#if email.primary}
<div class="badge badge-primary">{$t('settings.primary')}</div>
{/if}
{#if !email.verified}
<button class="btn btn-sm btn-secondary ml-2" on:click={() => verifyEmail(email)}
>{$t('settings.verify')}</button
<!-- Password Change Section -->
<section class="space-y-8">
<h2 class="text-2xl font-semibold text-center mt-8">
{$t('settings.password_change')}
</h2>
<div class="bg-neutral p-6 rounded-lg shadow-md">
<form method="post" action="?/changePassword" use:enhance class="space-y-6">
<div>
<label for="current_password" class="text-sm font-medium"
>{$t('settings.current_password')}</label
>
{/if}
{#if !email.primary}
<button class="btn btn-sm btn-secondary ml-2" on:click={() => primaryEmail(email)}
>{$t('settings.make_primary')}</button
<input
type="password"
id="current_password"
name="current_password"
class="block w-full mt-1 input input-bordered input-primary"
/>
</div>
<div>
<label for="password1" class="text-sm font-medium">{$t('settings.new_password')}</label>
<input
type="password"
id="password1"
name="password1"
class="block w-full mt-1 input input-bordered input-primary"
/>
</div>
<div>
<label for="password2" class="text-sm font-medium"
>{$t('settings.confirm_new_password')}</label
>
{/if}
<button class="btn btn-sm btn-warning ml-2" on:click={() => removeEmail(email)}
>{$t('adventures.remove')}</button
<input
type="password"
id="password2"
name="password2"
class="block w-full mt-1 input input-bordered input-primary"
/>
</div>
<div
class="tooltip tooltip-warning"
data-tip={$t('settings.password_change_lopout_warning')}
>
</p>
{/each}
{#if emails.length === 0}
<p>{$t('settings.no_emai_set')}</p>
{/if}
</div>
</div>
<div class="flex justify-center mt-4">
<form class="w-full max-w-xs" on:submit={addEmail}>
<div class="mb-4">
<input
type="email"
name="new_email"
placeholder={$t('settings.new_email')}
bind:value={new_email}
id="new_email"
class="block mb-2 input input-bordered w-full max-w-xs"
/>
<button class="w-full btn btn-primary py-2 mt-4">{$t('settings.password_change')}</button>
</div>
</form>
</div>
<div>
<button class="py-2 px-4 mb-4 btn btn-primary">{$t('settings.email_change')}</button>
</section>
<!-- Email Change Section -->
<section class="space-y-8">
<h2 class="text-2xl font-semibold text-center mt-8">
{$t('settings.email_change')}
</h2>
<div class="bg-neutral p-6 rounded-lg shadow-md">
<div>
{#each emails as email}
<div class="flex items-center space-x-2 mb-2">
<span>{email.email}</span>
{#if email.verified}
<div class="badge badge-success">{$t('settings.verified')}</div>
{:else}
<div class="badge badge-error">{$t('settings.not_verified')}</div>
{/if}
{#if email.primary}
<div class="badge badge-primary">{$t('settings.primary')}</div>
{/if}
{#if !email.verified}
<button class="btn btn-sm btn-secondary" on:click={() => verifyEmail(email)}
>{$t('settings.verify')}</button
>
{/if}
{#if !email.primary}
<button class="btn btn-sm btn-secondary" on:click={() => primaryEmail(email)}
>{$t('settings.make_primary')}</button
>
{/if}
<button class="btn btn-sm btn-warning" on:click={() => removeEmail(email)}
>{$t('adventures.remove')}</button
>
</div>
{/each}
{#if emails.length === 0}
<p class="text-center">{$t('settings.no_email_set')}</p>
{/if}
</div>
<form class="mt-4" on:submit={addEmail}>
<input
type="email"
id="new_email"
name="new_email"
bind:value={new_email}
placeholder={$t('settings.new_email')}
class="block w-full input input-bordered input-primary"
/>
<button class="w-full mt-4 btn btn-primary py-2">{$t('settings.email_change')}</button>
</form>
</div>
</form>
</section>
<!-- MFA Section -->
<section class="space-y-8">
<h2 class="text-2xl font-semibold text-center mt-8">
{$t('settings.mfa_page_title')}
</h2>
<div class="bg-neutral p-6 rounded-lg shadow-md text-center">
{#if !data.props.authenticators}
<p>{$t('settings.mfa_not_enabled')}</p>
<button class="btn btn-primary mt-4" on:click={() => (isMFAModalOpen = true)}
>{$t('settings.enable_mfa')}</button
>
{:else}
<button class="btn btn-warning mt-4" on:click={disableMfa}
>{$t('settings.disable_mfa')}</button
>
{/if}
</div>
</section>
<!-- Visited Region Check Section -->
<section class="text-center mt-8">
<h2 class="text-2xl font-semibold">{$t('adventures.visited_region_check')}</h2>
<p>{$t('adventures.visited_region_check_desc')}</p>
<button class="btn btn-neutral mt-4" on:click={checkVisitedRegions}
>{$t('adventures.update_visited_regions')}</button
>
</section>
<small class="text-center block mt-8">
<b>For Debug Use:</b> UUID={user.uuid} | Staff user: {user.is_staff} | {appTitle}
{appVersion}
</small>
</div>
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.mfa_page_title')}</h1>
<div class="flex justify-center mb-4">
<div>
{#if !data.props.authenticators}
<p>{$t('settings.mfa_not_enabled')}</p>
<button class="btn btn-primary mt-2" on:click={() => (isMFAModalOpen = true)}
>{$t('settings.enable_mfa')}</button
>
{:else}
<button class="btn btn-warning mt-2" on:click={disableMfa}
>{$t('settings.disable_mfa')}</button
>
{/if}
</div>
</div>
<div class="flex flex-col items-center mt-4">
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">
{$t('adventures.visited_region_check')}
</h1>
<p>
{$t('adventures.visited_region_check_desc')}
</p>
<p>{$t('adventures.update_visited_regions_disclaimer')}</p>
<button class="btn btn-neutral mt-2 mb-2" on:click={checkVisitedRegions}
>{$t('adventures.update_visited_regions')}</button
>
</div>
<small class="text-center"
><b>For Debug Use:</b> UUID={user.uuid} | Staff user: {user.is_staff}</small
>
<svelte:head>
<title>User Settings | AdventureLog</title>
<meta

View file

@ -4,32 +4,47 @@
import { t } from 'svelte-i18n';
</script>
<h1 class="text-center font-extrabold text-4xl mb-6">{$t('settings.reset_password')}</h1>
<section class="flex flex-col items-center justify-center min-h-screen px-4 py-8 bg-base-100">
<h1 class="text-4xl font-bold text-center mb-6 text-primary">{$t('settings.reset_password')}</h1>
<div class="flex justify-center">
<form method="post" action="?/forgotPassword" class="w-full max-w-xs" use:enhance>
<label for="email">{$t('auth.email')}</label>
<input
name="email"
type="email"
id="email"
class="block mb-2 input input-bordered w-full max-w-xs"
/><br />
<button class="py-2 px-4 btn btn-primary mr-2">{$t('settings.reset_password')}</button>
{#if $page.form?.message}
<div class="text-center text-error mt-4">
{$t(`settings.${$page.form?.message}`)}
<div class="w-full max-w-md p-6 shadow-lg rounded-lg bg-base-200">
<form method="post" action="?/forgotPassword" class="flex flex-col space-y-4" use:enhance>
<div class="form-control">
<label for="email" class="label">
<span class="label-text">{$t('auth.email')}</span>
</label>
<input
name="email"
type="email"
id="email"
placeholder="Enter your email"
class="input input-bordered w-full"
required
/>
</div>
{/if}
{#if $page.form?.success}
<div class="text-center text-success mt-4">
{$t('settings.possible_reset')}
<div class="form-control mt-4">
<button type="submit" class="btn btn-primary w-full">
{$t('settings.reset_password')}
</button>
</div>
{/if}
</form>
</div>
{#if $page.form?.message}
<div class="mt-4 text-center text-error">
{$t(`settings.${$page.form?.message}`)}
</div>
{/if}
{#if $page.form?.success}
<div class="mt-4 text-center text-success">
{$t('settings.possible_reset')}
</div>
{/if}
</form>
</div>
</section>
<svelte:head>
<title>Forgot Password</title>
<title>Reset Password</title>
<meta name="description" content="Reset your password for AdventureLog." />
</svelte:head>

View file

@ -1,53 +1,66 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import type { PageData } from '../../../$types';
// import type { PageData } from '../../../$types';
import { t } from 'svelte-i18n';
export let data: PageData;
// export let data: PageData;
</script>
<h1 class="text-center font-bold text-4xl mb-4">{$t('settings.change_password')}</h1>
<section class="flex flex-col items-center justify-center min-h-screen px-4 py-8 bg-base-100">
<h1 class="text-4xl font-bold text-center mb-6 text-primary">
{$t('settings.change_password')}
</h1>
<form method="POST" use:enhance class="flex flex-col items-center justify-center space-y-4">
<div class="w-full max-w-xs">
<label for="password" class="label">
<span class="label-text">{$t('auth.new_password')}</span>
</label>
<input
type="password"
id="password"
name="password"
required
class="input input-bordered w-full"
/>
<div class="w-full max-w-md p-6 shadow-lg rounded-lg bg-base-200">
<form method="POST" use:enhance class="flex flex-col space-y-6">
<div class="form-control">
<label for="password" class="label">
<span class="label-text">{$t('auth.new_password')}</span>
</label>
<input
type="password"
id="password"
name="password"
placeholder="Enter new password"
required
class="input input-bordered w-full"
/>
</div>
<div class="form-control">
<label for="confirm_password" class="label">
<span class="label-text">{$t('auth.confirm_password')}</span>
</label>
<input
type="password"
id="confirm_password"
name="confirm_password"
placeholder="Confirm new password"
required
class="input input-bordered w-full"
/>
</div>
<div class="form-control mt-4">
<button type="submit" class="btn btn-primary w-full">
{$t('settings.reset_password')}
</button>
</div>
{#if $page.form?.message}
<div class="mt-4 text-center text-error">
{$t($page.form?.message)}
</div>
{/if}
</form>
</div>
<div class="w-full max-w-xs">
<label for="confirm_password" class="label">
<span class="label-text">{$t('auth.confirm_password')}</span>
</label>
<input
type="password"
id="confirm_password"
name="confirm_password"
required
class="input input-bordered w-full"
/>
</div>
<button type="submit" class="btn btn-primary">
{$t('settings.reset_password')}
</button>
{#if $page.form?.message}
<div class="text-error">
{$t($page.form?.message)}
</div>
{/if}
</form>
</section>
<svelte:head>
<title>Password Reset Confirm</title>
<meta name="description" content="Confirm your password reset and make a new password." />
<title>Change Password</title>
<meta
name="description"
content="Confirm your password reset and create a new password for AdventureLog."
/>
</svelte:head>

View file

@ -5,10 +5,27 @@
export let data: PageData;
</script>
{#if data.verified}
<h1>{$t('settings.email_verified')}</h1>
<p>{$t('settings.email_verified_success')}</p>
{:else}
<h1>{$t('settings.email_verified_error')}</h1>
<p>{$t('settings.email_verified_erorr_desc')}</p>
{/if}
<section class="flex flex-col items-center justify-center min-h-screen px-4 py-8 bg-base-100">
<div class="w-full max-w-lg p-6 shadow-lg rounded-lg bg-base-200 text-center">
{#if data.verified}
<h1 class="text-4xl font-bold text-success mb-4">
{$t('settings.email_verified')}
</h1>
<p class="text-lg text-base-content">
{$t('settings.email_verified_success')}
</p>
{:else}
<h1 class="text-4xl font-bold text-error mb-4">
{$t('settings.email_verified_error')}
</h1>
<p class="text-lg text-base-content">
{$t('settings.email_verified_erorr_desc')}
</p>
{/if}
</div>
</section>
<svelte:head>
<title>Email Verification</title>
<meta name="description" content="View your email verification status for AdventureLog." />
</svelte:head>

View file

@ -6,7 +6,7 @@ import type { PageServerLoad } from './$types';
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => {
const id = event.params.id;
const id = event.params.id.toUpperCase();
let regions: Region[] = [];
let visitedRegions: VisitedRegion[] = [];

View file

@ -1,7 +1,10 @@
<script lang="ts">
import RegionCard from '$lib/components/RegionCard.svelte';
import type { Region, VisitedRegion } from '$lib/types';
import { MapLibre, Marker } from 'svelte-maplibre';
import type { PageData } from './$types';
import { addToast } from '$lib/toasts';
import { t } from 'svelte-i18n';
export let data: PageData;
let regions: Region[] = data.props?.regions || [];
@ -10,6 +13,70 @@
const country = data.props?.country || null;
console.log(data);
let showGeo: boolean = true;
function togleVisited(region: Region) {
return () => {
const visitedRegion = visitedRegions.find(
(visitedRegion) => visitedRegion.region === region.id
);
if (visitedRegion) {
visitedRegions = visitedRegions.filter(
(visitedRegion) => visitedRegion.region !== region.id
);
removeVisit(region, visitedRegion.id);
} else {
markVisited(region);
}
};
}
async function markVisited(region: Region) {
let res = await fetch(`/worldtravel?/markVisited`, {
method: 'POST',
body: JSON.stringify({ regionId: region.id })
});
if (res.ok) {
// visited = true;
const result = await res.json();
const data = JSON.parse(result.data);
if (data[1] !== undefined) {
console.log('New adventure created with id:', data[3]);
let visit_id = data[3];
let region_id = data[5];
let user_id = data[4];
visitedRegions = [
...visitedRegions,
{
id: visit_id,
region: region_id,
user_id: user_id,
longitude: 0,
latitude: 0,
name: ''
}
];
addToast('success', `Visit to ${region.name} marked`);
}
} else {
console.error('Failed to mark region as visited');
addToast('error', `Failed to mark visit to ${region.name}`);
}
}
async function removeVisit(region: Region, visitId: number) {
let res = await fetch(`/worldtravel?/removeVisited`, {
method: 'POST',
body: JSON.stringify({ visitId: visitId })
});
if (res.ok) {
addToast('info', `Visit to ${region.name} removed`);
} else {
console.error('Failed to remove visit');
}
}
let numRegions: number = country?.num_regions || 0;
let numVisitedRegions: number = country?.num_visits || 0;
@ -49,6 +116,54 @@
{/each}
</div>
<div class="flex justify-center border-neutral p-4 rounded-lg border-4 max-w-lg m-auto mt-4">
<label for="show-geo">{$t('adventures.show_region_labels')}</label>
<input
type="checkbox"
id="show-geo"
name="show-geo"
class="checkbox ml-2"
bind:checked={showGeo}
on:click={() => (showGeo = !showGeo)}
/>
</div>
<div class="mt-4 mb-4 flex justify-center">
<!-- checkbox to toggle marker -->
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
standardControls
center={[regions[0]?.longitude || 0, regions[0]?.latitude || 0]}
zoom={2}
>
<!-- MapEvents gives you access to map events even from other components inside the map,
where you might not have access to the top-level `MapLibre` component. In this case
it would also work to just use on:click on the MapLibre component itself. -->
<!-- <MapEvents on:click={addMarker} /> -->
{#each regions as region}
{#if region.latitude && region.longitude && showGeo}
<Marker
lngLat={[region.longitude, region.latitude]}
class="grid px-2 py-1 place-items-center rounded-full border border-gray-200 {visitedRegions.some(
(visitedRegion) => visitedRegion.region === region.id
)
? 'bg-red-300'
: 'bg-blue-300'} text-black focus:outline-6 focus:outline-black"
on:click={togleVisited(region)}
>
<span class="text-xs">
{region.name}
</span>
</Marker>
{/if}
{/each}
</MapLibre>
<!-- button to clear to and from location -->
</div>
<svelte:head>
<title
>{data.props && data.props.country ? `Regions in ${data.props.country.name}` : 'Regions'}</title

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB