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:
commit
bc6cf42b8e
56 changed files with 2944 additions and 1991 deletions
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
8
documentation/pnpm-lock.yaml
generated
8
documentation/pnpm-lock.yaml
generated
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
32
frontend/pnpm-lock.yaml
generated
32
frontend/pnpm-lock.yaml
generated
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
204
frontend/src/lib/components/CollectionModal.svelte
Normal file
204
frontend/src/lib/components/CollectionModal.svelte
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
81
frontend/src/lib/components/MarkdownEditor.svelte
Normal file
81
frontend/src/lib/components/MarkdownEditor.svelte
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
605
frontend/src/lib/components/TransportationModal.svelte
Normal file
605
frontend/src/lib/components/TransportationModal.svelte
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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!",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "日期",
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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
|
||||
|
|
BIN
frontend/static/backgrounds/adventurelog_christmas.webp
Normal file
BIN
frontend/static/backgrounds/adventurelog_christmas.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 179 KiB |
BIN
frontend/static/backgrounds/adventurelog_new_year.webp
Normal file
BIN
frontend/static/backgrounds/adventurelog_new_year.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 340 KiB |
BIN
frontend/static/backgrounds/adventurelog_showcase_5.webp
Normal file
BIN
frontend/static/backgrounds/adventurelog_showcase_5.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 347 KiB |
Loading…
Add table
Add a link
Reference in a new issue