mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-19 12:59:36 +02:00
Merge branch 'development' into main
This commit is contained in:
commit
44ede92b92
31 changed files with 939 additions and 270 deletions
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.0.8 on 2025-03-17 21:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('adventures', '0024_alter_attachment_file'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='visit',
|
||||
name='end_date',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='visit',
|
||||
name='start_date',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -76,8 +76,8 @@ User = get_user_model()
|
|||
class Visit(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||
adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits')
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
start_date = models.DateTimeField(null=True, blank=True)
|
||||
end_date = models.DateTimeField(null=True, blank=True)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
|
|
@ -136,9 +136,11 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
def get_is_visited(self, obj):
|
||||
current_date = timezone.now().date()
|
||||
for visit in obj.visits.all():
|
||||
if visit.start_date and visit.end_date and (visit.start_date <= current_date):
|
||||
start_date = visit.start_date.date() if isinstance(visit.start_date, timezone.datetime) else visit.start_date
|
||||
end_date = visit.end_date.date() if isinstance(visit.end_date, timezone.datetime) else visit.end_date
|
||||
if start_date and end_date and (start_date <= current_date):
|
||||
return True
|
||||
elif visit.start_date and not visit.end_date and (visit.start_date <= current_date):
|
||||
elif start_date and not end_date and (start_date <= current_date):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
order_by = self.request.query_params.get('order_by', 'name')
|
||||
order_direction = self.request.query_params.get('order_direction', 'asc')
|
||||
|
||||
valid_order_by = ['name', 'upated_at']
|
||||
valid_order_by = ['name', 'upated_at', 'start_date']
|
||||
if order_by not in valid_order_by:
|
||||
order_by = 'updated_at'
|
||||
|
||||
|
@ -35,6 +35,12 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
ordering = 'lower_name'
|
||||
if order_direction == 'desc':
|
||||
ordering = f'-{ordering}'
|
||||
elif order_by == 'start_date':
|
||||
ordering = 'start_date'
|
||||
if order_direction == 'asc':
|
||||
ordering = 'start_date'
|
||||
else:
|
||||
ordering = '-start_date'
|
||||
else:
|
||||
order_by == 'updated_at'
|
||||
ordering = 'updated_at'
|
||||
|
|
|
@ -3,7 +3,6 @@ from allauth.socialaccount.models import SocialAccount
|
|||
|
||||
class NoPasswordAuthBackend(ModelBackend):
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
print("NoPasswordAuthBackend")
|
||||
# First, attempt normal authentication
|
||||
user = super().authenticate(request, username=username, password=password, **kwargs)
|
||||
if user is None:
|
||||
|
|
|
@ -84,6 +84,16 @@ export default defineConfig({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Usage",
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: "How to use AdventureLog",
|
||||
link: "/docs/usage/usage",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Configuration",
|
||||
collapsed: false,
|
||||
|
@ -134,6 +144,10 @@ export default defineConfig({
|
|||
text: "No Images Displaying",
|
||||
link: "/docs/troubleshooting/no_images",
|
||||
},
|
||||
{
|
||||
text: "Login and Registration Unresponsive",
|
||||
link: "/docs/troubleshooting/login_unresponsive",
|
||||
},
|
||||
{
|
||||
text: "Failed to Start Nginx",
|
||||
link: "/docs/troubleshooting/nginx_failed",
|
||||
|
|
|
@ -27,4 +27,6 @@ AdventureLog is open-source software, licensed under the GPL-3.0 license. This m
|
|||
|
||||
## About the Maintainer
|
||||
|
||||
AdventureLog is created and maintained by [Sean Morley](https://seanmorley.com), a Computer Science student at the University of Connecticut. Sean is passionate about open-source software and building modern tools that help people solve real-world problems.
|
||||
Hi, I'm [Sean Morley](https://seanmorley.com), the creator of AdventureLog. I'm a Computer Science student at the University of Connecticut, and I'm passionate about open-source software and building modern tools that help people solve real-world problems. I created AdventureLog to solve a problem: the lack of a modern, open-source, user-friendly travel companion. Many existing travel apps are either too complex, too expensive, or too closed-off to be useful for the average traveler. AdventureLog aims to be the opposite: simple, beautiful, and open to everyone.
|
||||
|
||||
I hope you enjoy using AdventureLog as much as I enjoy creating it! If you have any questions, feedback, or suggestions, feel free to reach out to me via the email address listed on my website. I'm always happy to hear from users and help in any way I can. Thank you for using AdventureLog, and happy travels! 🌍
|
||||
|
|
19
documentation/docs/troubleshooting/login_unresponsive.md
Normal file
19
documentation/docs/troubleshooting/login_unresponsive.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Troubleshooting: Login and Registration Unresponsive
|
||||
|
||||
When you encounter issues with the login and registration pages being unresponsive in AdventureLog, it can be due to various reasons. This guide will help you troubleshoot and resolve the unresponsive login and registration pages in AdventureLog.
|
||||
|
||||
1. Check to make sure the backend container is running and accessible.
|
||||
|
||||
- Check the backend container logs to see if there are any errors or issues blocking the contianer from running.
|
||||
|
||||
2. Check the connection between the frontend and backend containers.
|
||||
|
||||
- Attempt login with the browser console network tab open to see if there are any errors or issues with the connection between the frontend and backend containers. If there is a connection issue, the code will show an error like `Failed to load resource: net::ERR_CONNECTION_REFUSED`. If this is the case, check the `PUBLIC_SERVER_URL` in the frontend container and refer to the installation docs to ensure the correct URL is set.
|
||||
- If the error is `403`, continue to the next step.
|
||||
|
||||
3. The error most likely is due to a CSRF security config issue in either the backend or frontend.
|
||||
|
||||
- Check that the `ORIGIN` variable in the frontend is set to the URL where the frontend is access and you are accessing the app from currently.
|
||||
- Check that the `CSRF_TRUSTED_ORIGINS` variable in the backend is set to a comma separated list of the origins where you use your backend server and frontend. One of these values should match the `ORIGIN` variable in the frontend.
|
||||
|
||||
4. If you are still experiencing issues, please refer to the [AdventureLog Discord Server](https://discord.gg/wRbQ9Egr8C) for further assistance, providing as much detail as possible about the issue you are experiencing!
|
29
documentation/docs/usage/usage.md
Normal file
29
documentation/docs/usage/usage.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# How to use AdventureLog
|
||||
|
||||
Welcome to AdventureLog! This guide will help you get started with AdventureLog and provide you with an overview of the features available to you.
|
||||
|
||||
## Key Terms
|
||||
|
||||
#### Adventures
|
||||
|
||||
- **Adventure**: think of an adventure as a point on a map, a location you want to visit, or a place you want to explore. An adventure can be anything you want it to be, from a local park to a famous landmark.
|
||||
- **Visit**: a visit is added to an adventure. It contains a date and notes about when the adventure was visited. If an adventure is visited multiple times, multiple visits can be added. If there are no visits on an adventure or the date of all visits is in the future, the adventure is considered planned. If the date of the visit is in the past, the adventure is considered completed.
|
||||
- **Category**: a category is a way to group adventures together. For example, you could have a category for parks, a category for museums, and a category for restaurants.
|
||||
- **Tag**: a tag is a way to add additional information to an adventure. For example, you could have a tag for the type of cuisine at a restaurant or the type of art at a museum. Multiple tags can be added to an adventure.
|
||||
- **Image**: an image is a photo that is added to an adventure. Images can be added to an adventure to provide a visual representation of the location or to capture a memory of the visit. These can be uploded from your device or with a service like [Immich](/docs/configuration/immich_integration) if the integration is enabled.
|
||||
- **Attachment**: an attachment is a file that is added to an adventure. Attachments can be added to an adventure to provide additional information, such as a map of the location or a brochure from the visit.
|
||||
|
||||
#### Collections
|
||||
|
||||
- **Collection**: a collection is a way to group adventures together. Collections are flexible and can be used in many ways. When no start or end date is added to a collection, it acts like a folder to group adventures together. When a start and end date is added to a collection, it acts like a trip to group adventures together that were visited during that time period. With start and end dates, the collection is transformed into a full itinerary with a map showing the route taken between adventures.
|
||||
- **Transportation**: a transportation is a collection exclusive feature that allows you to add transportation information to your trip. This can be used to show the route taken between locations and the mode of transportation used. It can also be used to track flight information, such as flight number and departure time.
|
||||
- **Lodging**: a lodging is a collection exclusive feature that allows you to add lodging information to your trip. This can be used to plan where you will stay during your trip and add notes about the lodging location. It can also be used to track reservation information, such as reservation number and check-in time.
|
||||
- **Note**: a note is a collection exclusive feature that allows you to add notes to your trip. This can be used to add additional information about your trip, such as a summary of the trip or a list of things to do. Notes can be assigned to a specific day of the trip to help organize the information.
|
||||
- **Checklist**: a checklist is a collection exclusive feature that allows you to add a checklist to your trip. This can be used to create a list of things to do during your trip or for planning purposes like packing lists. Checklists can be assigned to a specific day of the trip to help organize the information.
|
||||
|
||||
#### World Travel
|
||||
|
||||
- **World Travel**: the world travel feature of AdventureLog allows you to track the countries, regions, and cities you have visited during your lifetime. You can add visits to countries, regions, and cities, and view statistics about your travels. The world travel feature is a fun way to visualize where you have been and where you want to go next.
|
||||
- **Country**: a country is a geographical area that is recognized as an independent nation. You can add visits to countries to track where you have been.
|
||||
- **Region**: a region is a geographical area that is part of a country. You can add visits to regions to track where you have been within a country.
|
||||
- **City**: a city is a geographical area that is a populated urban center. You can add visits to cities to track where you have been within a region.
|
|
@ -6,6 +6,20 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
export let collection: Collection | null = null;
|
||||
|
||||
let fullStartDate: string = '';
|
||||
let fullEndDate: string = '';
|
||||
let fullStartDateOnly: string = '';
|
||||
let fullEndDateOnly: string = '';
|
||||
let allDay: boolean = true;
|
||||
|
||||
// Set full start and end dates from collection
|
||||
if (collection && collection.start_date && collection.end_date) {
|
||||
fullStartDate = `${collection.start_date}T00:00`;
|
||||
fullEndDate = `${collection.end_date}T23:59`;
|
||||
fullStartDateOnly = collection.start_date;
|
||||
fullEndDateOnly = collection.end_date;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let images: { id: string; image: string; is_primary: boolean }[] = [];
|
||||
|
@ -72,7 +86,7 @@
|
|||
|
||||
import ActivityComplete from './ActivityComplete.svelte';
|
||||
import CategoryDropdown from './CategoryDropdown.svelte';
|
||||
import { findFirstValue } from '$lib';
|
||||
import { findFirstValue, isAllDay } from '$lib';
|
||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||
import ImmichSelect from './ImmichSelect.svelte';
|
||||
import Star from '~icons/mdi/star';
|
||||
|
@ -379,7 +393,10 @@
|
|||
let new_start_date: string = '';
|
||||
let new_end_date: string = '';
|
||||
let new_notes: string = '';
|
||||
|
||||
// Function to add a new visit.
|
||||
function addNewVisit() {
|
||||
// If an end date isn’t provided, assume it’s the same as start.
|
||||
if (new_start_date && !new_end_date) {
|
||||
new_end_date = new_start_date;
|
||||
}
|
||||
|
@ -391,15 +408,31 @@
|
|||
addToast('error', $t('adventures.no_start_date'));
|
||||
return;
|
||||
}
|
||||
// Convert input to UTC if not already.
|
||||
if (new_start_date && !new_start_date.includes('Z')) {
|
||||
new_start_date = new Date(new_start_date).toISOString();
|
||||
}
|
||||
if (new_end_date && !new_end_date.includes('Z')) {
|
||||
new_end_date = new Date(new_end_date).toISOString();
|
||||
}
|
||||
|
||||
// If the visit is all day, force the times to midnight.
|
||||
if (allDay) {
|
||||
new_start_date = new_start_date.split('T')[0] + 'T00:00:00.000Z';
|
||||
new_end_date = new_end_date.split('T')[0] + 'T00:00:00.000Z';
|
||||
}
|
||||
|
||||
adventure.visits = [
|
||||
...adventure.visits,
|
||||
{
|
||||
start_date: new_start_date,
|
||||
end_date: new_end_date,
|
||||
notes: new_notes,
|
||||
id: ''
|
||||
id: '' // or generate an id as needed
|
||||
}
|
||||
];
|
||||
|
||||
// Clear the input fields.
|
||||
new_start_date = '';
|
||||
new_end_date = '';
|
||||
new_notes = '';
|
||||
|
@ -669,13 +702,23 @@
|
|||
on:change={() => (constrainDates = !constrainDates)}
|
||||
/>
|
||||
{/if}
|
||||
<span class="label-text">{$t('adventures.all_day')}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
id="constrain_dates"
|
||||
name="constrain_dates"
|
||||
bind:checked={allDay}
|
||||
/>
|
||||
</label>
|
||||
<div class="flex gap-2 mb-1">
|
||||
{#if !constrainDates}
|
||||
{#if !allDay}
|
||||
<input
|
||||
type="date"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Start Date"
|
||||
placeholder={$t('adventures.start_date')}
|
||||
min={constrainDates ? fullStartDate : ''}
|
||||
max={constrainDates ? fullEndDate : ''}
|
||||
bind:value={new_start_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
@ -685,10 +728,12 @@
|
|||
}}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
placeholder={$t('adventures.end_date')}
|
||||
bind:value={new_end_date}
|
||||
min={constrainDates ? fullStartDate : ''}
|
||||
max={constrainDates ? fullEndDate : ''}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
@ -701,8 +746,8 @@
|
|||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
placeholder={$t('adventures.start_date')}
|
||||
min={collection?.start_date}
|
||||
max={collection?.end_date}
|
||||
min={constrainDates ? fullStartDateOnly : ''}
|
||||
max={constrainDates ? fullEndDateOnly : ''}
|
||||
bind:value={new_start_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
@ -716,8 +761,8 @@
|
|||
class="input input-bordered w-full"
|
||||
placeholder={$t('adventures.end_date')}
|
||||
bind:value={new_end_date}
|
||||
min={collection?.start_date}
|
||||
max={collection?.end_date}
|
||||
min={constrainDates ? fullStartDateOnly : ''}
|
||||
max={constrainDates ? fullEndDateOnly : ''}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
@ -741,6 +786,31 @@
|
|||
}}
|
||||
></textarea>
|
||||
</div>
|
||||
{#if !allDay}
|
||||
<div role="alert" class="alert shadow-lg bg-neutral mt-2 mb-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info h-6 w-6 shrink-0"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>
|
||||
{$t('lodging.current_timezone')}:
|
||||
{(() => {
|
||||
const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const [continent, city] = tz.split('/');
|
||||
return `${continent} (${city.replace('_', ' ')})`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="btn btn-neutral" on:click={addNewVisit}
|
||||
|
@ -749,24 +819,86 @@
|
|||
</div>
|
||||
|
||||
{#if adventure.visits.length > 0}
|
||||
<h2 class=" font-bold text-xl mt-2">{$t('adventures.my_visits')}</h2>
|
||||
<h2 class="font-bold text-xl mt-2">{$t('adventures.my_visits')}</h2>
|
||||
{#each adventure.visits as visit}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<p>
|
||||
{new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})}
|
||||
{#if isAllDay(visit.start_date)}
|
||||
<!-- For all-day events, show just the date -->
|
||||
{new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})}
|
||||
{:else}
|
||||
<!-- For timed events, show date and time -->
|
||||
{new Date(visit.start_date).toLocaleDateString()} ({new Date(
|
||||
visit.start_date
|
||||
).toLocaleTimeString()})
|
||||
{/if}
|
||||
</p>
|
||||
{#if visit.end_date && visit.end_date !== visit.start_date}
|
||||
<p>
|
||||
{new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})}
|
||||
{#if isAllDay(visit.end_date)}
|
||||
<!-- For all-day events, show just the date -->
|
||||
{new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})}
|
||||
{:else}
|
||||
<!-- For timed events, show date and time -->
|
||||
{new Date(visit.end_date).toLocaleDateString()} ({new Date(
|
||||
visit.end_date
|
||||
).toLocaleTimeString()})
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-neutral"
|
||||
on:click={() => {
|
||||
// Determine if this is an all-day event
|
||||
const isAllDayEvent = isAllDay(visit.start_date);
|
||||
allDay = isAllDayEvent;
|
||||
|
||||
if (isAllDayEvent) {
|
||||
// For all-day events, use date only
|
||||
new_start_date = visit.start_date.split('T')[0];
|
||||
new_end_date = visit.end_date.split('T')[0];
|
||||
} else {
|
||||
// For timed events, format properly for datetime-local input
|
||||
const startDate = new Date(visit.start_date);
|
||||
const endDate = new Date(visit.end_date);
|
||||
|
||||
// Format as yyyy-MM-ddThh:mm
|
||||
new_start_date =
|
||||
startDate.getFullYear() +
|
||||
'-' +
|
||||
String(startDate.getMonth() + 1).padStart(2, '0') +
|
||||
'-' +
|
||||
String(startDate.getDate()).padStart(2, '0') +
|
||||
'T' +
|
||||
String(startDate.getHours()).padStart(2, '0') +
|
||||
':' +
|
||||
String(startDate.getMinutes()).padStart(2, '0');
|
||||
|
||||
new_end_date =
|
||||
endDate.getFullYear() +
|
||||
'-' +
|
||||
String(endDate.getMonth() + 1).padStart(2, '0') +
|
||||
'-' +
|
||||
String(endDate.getDate()).padStart(2, '0') +
|
||||
'T' +
|
||||
String(endDate.getHours()).padStart(2, '0') +
|
||||
':' +
|
||||
String(endDate.getMinutes()).padStart(2, '0');
|
||||
}
|
||||
|
||||
new_notes = visit.notes;
|
||||
adventure.visits = adventure.visits.filter((v) => v !== visit);
|
||||
}}
|
||||
>
|
||||
{$t('lodging.edit')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error"
|
||||
|
|
|
@ -189,10 +189,31 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Actions -->
|
||||
|
||||
{#if !collection.start_date && !collection.end_date}
|
||||
<div class="mt-4">
|
||||
<div role="alert" class="alert alert-neutral">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>{$t('adventures.collection_no_start_end_date')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{$t('adventures.save_next')}
|
||||
{$t('notes.save')}
|
||||
</button>
|
||||
<button type="button" class="btn" on:click={close}>
|
||||
{$t('about.close')}
|
||||
|
|
|
@ -124,7 +124,7 @@
|
|||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm">{$t('adventures.dates')}:</span>
|
||||
<p>
|
||||
{new Date(lodging.check_in).toLocaleString('en-US', {
|
||||
{new Date(lodging.check_in).toLocaleString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
|
@ -132,7 +132,7 @@
|
|||
minute: 'numeric'
|
||||
})}
|
||||
-
|
||||
{new Date(lodging.check_out).toLocaleString('en-US', {
|
||||
{new Date(lodging.check_out).toLocaleString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
|
|
|
@ -7,7 +7,15 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
import DeleteWarning from './DeleteWarning.svelte';
|
||||
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
|
||||
import { TRANSPORTATION_TYPES_ICONS } from '$lib';
|
||||
|
||||
function getTransportationIcon(type: string) {
|
||||
if (type in TRANSPORTATION_TYPES_ICONS) {
|
||||
return TRANSPORTATION_TYPES_ICONS[type as keyof typeof TRANSPORTATION_TYPES_ICONS];
|
||||
} else {
|
||||
return '🚗';
|
||||
}
|
||||
}
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let transportation: Transportation;
|
||||
|
@ -106,7 +114,9 @@
|
|||
<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}`)}
|
||||
{$t(`transportation.modes.${transportation.type}`) +
|
||||
' ' +
|
||||
getTransportationIcon(transportation.type)}
|
||||
</div>
|
||||
{#if transportation.type == 'plane' && transportation.flight_number}
|
||||
<div class="badge badge-neutral-200">{transportation.flight_number}</div>
|
||||
|
@ -128,7 +138,7 @@
|
|||
{#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>
|
||||
<p>{new Date(transportation.date).toLocaleString()}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -146,7 +156,7 @@
|
|||
{#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>
|
||||
<p>{new Date(transportation.end_date).toLocaleString()}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -16,10 +16,15 @@
|
|||
|
||||
let constrainDates: boolean = false;
|
||||
|
||||
// Format date as local datetime
|
||||
// Convert an ISO date to a datetime-local value in local time.
|
||||
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
|
||||
// Adjust the time by subtracting the timezone offset.
|
||||
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
|
||||
// Return format YYYY-MM-DDTHH:mm
|
||||
return date.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
let transportation: Transportation = {
|
||||
|
@ -185,6 +190,14 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Convert local dates to UTC
|
||||
if (transportation.date && !transportation.date.includes('Z')) {
|
||||
transportation.date = new Date(transportation.date).toISOString();
|
||||
}
|
||||
if (transportation.end_date && !transportation.end_date.includes('Z')) {
|
||||
transportation.end_date = new Date(transportation.end_date).toISOString();
|
||||
}
|
||||
|
||||
if (transportation.type != 'plane') {
|
||||
transportation.flight_number = '';
|
||||
}
|
||||
|
@ -422,6 +435,29 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div role="alert" class="alert shadow-lg bg-neutral mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info h-6 w-6 shrink-0"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>
|
||||
{$t('lodging.current_timezone')}:
|
||||
{(() => {
|
||||
const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const [continent, city] = tz.split('/');
|
||||
return `${continent} (${city.replace('_', ' ')})`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -70,34 +70,65 @@ export function groupAdventuresByDate(
|
|||
// Initialize all days in the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedAdventures[dateString] = [];
|
||||
}
|
||||
|
||||
adventures.forEach((adventure) => {
|
||||
adventure.visits.forEach((visit) => {
|
||||
if (visit.start_date) {
|
||||
const adventureDate = new Date(visit.start_date).toISOString().split('T')[0];
|
||||
if (visit.end_date) {
|
||||
const endDate = new Date(visit.end_date).toISOString().split('T')[0];
|
||||
// Check if this is an all-day event (both start and end at midnight)
|
||||
const isAllDayEvent =
|
||||
isAllDay(visit.start_date) && (visit.end_date ? isAllDay(visit.end_date) : false);
|
||||
|
||||
// Loop through all days and include adventure if it falls within the range
|
||||
// For all-day events, we need to handle dates differently
|
||||
if (isAllDayEvent && visit.end_date) {
|
||||
// Extract just the date parts without time
|
||||
const startDateStr = visit.start_date.split('T')[0];
|
||||
const endDateStr = visit.end_date.split('T')[0];
|
||||
|
||||
// Loop through all days in the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const currentDateStr = getLocalDateString(currentDate);
|
||||
|
||||
// Include the current day if it falls within the adventure date range
|
||||
if (dateString >= adventureDate && dateString <= endDate) {
|
||||
if (groupedAdventures[dateString]) {
|
||||
groupedAdventures[dateString].push(adventure);
|
||||
if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) {
|
||||
if (groupedAdventures[currentDateStr]) {
|
||||
groupedAdventures[currentDateStr].push(adventure);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (groupedAdventures[adventureDate]) {
|
||||
// If there's no end date, add adventure to the start date only
|
||||
groupedAdventures[adventureDate].push(adventure);
|
||||
} else {
|
||||
// Handle regular events with time components
|
||||
const adventureStartDate = new Date(visit.start_date);
|
||||
const adventureDateStr = getLocalDateString(adventureStartDate);
|
||||
|
||||
if (visit.end_date) {
|
||||
const adventureEndDate = new Date(visit.end_date);
|
||||
const endDateStr = getLocalDateString(adventureEndDate);
|
||||
|
||||
// Loop through all days and include adventure if it falls within the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
|
||||
// Include the current day if it falls within the adventure date range
|
||||
if (dateString >= adventureDateStr && dateString <= endDateStr) {
|
||||
if (groupedAdventures[dateString]) {
|
||||
groupedAdventures[dateString].push(adventure);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there's no end date, add adventure to the start date only
|
||||
if (groupedAdventures[adventureDateStr]) {
|
||||
groupedAdventures[adventureDateStr].push(adventure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -106,6 +137,20 @@ export function groupAdventuresByDate(
|
|||
return groupedAdventures;
|
||||
}
|
||||
|
||||
function getLocalDateString(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// Helper to check if a given date string represents midnight (all-day)
|
||||
// Improved isAllDay function to handle different ISO date formats
|
||||
export function isAllDay(dateStr: string): boolean {
|
||||
// Check for various midnight formats in UTC
|
||||
return dateStr.endsWith('T00:00:00Z') || dateStr.endsWith('T00:00:00.000Z');
|
||||
}
|
||||
|
||||
export function groupTransportationsByDate(
|
||||
transportations: Transportation[],
|
||||
startDate: Date,
|
||||
|
@ -116,22 +161,22 @@ export function groupTransportationsByDate(
|
|||
// Initialize all days in the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedTransportations[dateString] = [];
|
||||
}
|
||||
|
||||
transportations.forEach((transportation) => {
|
||||
if (transportation.date) {
|
||||
const transportationDate = new Date(transportation.date).toISOString().split('T')[0];
|
||||
const transportationDate = getLocalDateString(new Date(transportation.date));
|
||||
if (transportation.end_date) {
|
||||
const endDate = new Date(transportation.end_date).toISOString().split('T')[0];
|
||||
|
||||
// Loop through all days and include transportation if it falls within the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
|
||||
// Include the current day if it falls within the transportation date range
|
||||
if (dateString >= transportationDate && dateString <= endDate) {
|
||||
|
@ -157,35 +202,32 @@ export function groupLodgingByDate(
|
|||
): Record<string, Lodging[]> {
|
||||
const groupedTransportations: Record<string, Lodging[]> = {};
|
||||
|
||||
// Initialize all days in the range
|
||||
// Initialize all days in the range using local dates
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedTransportations[dateString] = [];
|
||||
}
|
||||
|
||||
transportations.forEach((transportation) => {
|
||||
if (transportation.check_in) {
|
||||
const transportationDate = new Date(transportation.check_in).toISOString().split('T')[0];
|
||||
// Use local date string conversion
|
||||
const transportationDate = getLocalDateString(new Date(transportation.check_in));
|
||||
if (transportation.check_out) {
|
||||
const endDate = new Date(transportation.check_out).toISOString().split('T')[0];
|
||||
const endDate = getLocalDateString(new Date(transportation.check_out));
|
||||
|
||||
// Loop through all days and include transportation if it falls within the range
|
||||
// Loop through all days and include transportation if it falls within the transportation date range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
|
||||
// Include the current day if it falls within the transportation date range
|
||||
if (dateString >= transportationDate && dateString <= endDate) {
|
||||
if (groupedTransportations[dateString]) {
|
||||
groupedTransportations[dateString].push(transportation);
|
||||
}
|
||||
groupedTransportations[dateString].push(transportation);
|
||||
}
|
||||
}
|
||||
} else if (groupedTransportations[transportationDate]) {
|
||||
// If there's no end date, add transportation to the start date only
|
||||
groupedTransportations[transportationDate].push(transportation);
|
||||
}
|
||||
}
|
||||
|
@ -201,19 +243,18 @@ export function groupNotesByDate(
|
|||
): Record<string, Note[]> {
|
||||
const groupedNotes: Record<string, Note[]> = {};
|
||||
|
||||
// Initialize all days in the range
|
||||
// Initialize all days in the range using local dates
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedNotes[dateString] = [];
|
||||
}
|
||||
|
||||
notes.forEach((note) => {
|
||||
if (note.date) {
|
||||
const noteDate = new Date(note.date).toISOString().split('T')[0];
|
||||
|
||||
// Add note to the appropriate date group if it exists
|
||||
// Use the date string as is since it's already in "YYYY-MM-DD" format.
|
||||
const noteDate = note.date;
|
||||
if (groupedNotes[noteDate]) {
|
||||
groupedNotes[noteDate].push(note);
|
||||
}
|
||||
|
@ -230,19 +271,18 @@ export function groupChecklistsByDate(
|
|||
): Record<string, Checklist[]> {
|
||||
const groupedChecklists: Record<string, Checklist[]> = {};
|
||||
|
||||
// Initialize all days in the range
|
||||
// Initialize all days in the range using local dates
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedChecklists[dateString] = [];
|
||||
}
|
||||
|
||||
checklists.forEach((checklist) => {
|
||||
if (checklist.date) {
|
||||
const checklistDate = new Date(checklist.date).toISOString().split('T')[0];
|
||||
|
||||
// Add checklist to the appropriate date group if it exists
|
||||
// Use the date string as is since it's already in "YYYY-MM-DD" format.
|
||||
const checklistDate = checklist.date;
|
||||
if (groupedChecklists[checklistDate]) {
|
||||
groupedChecklists[checklistDate].push(checklist);
|
||||
}
|
||||
|
@ -338,6 +378,17 @@ export let LODGING_TYPES_ICONS = {
|
|||
other: '❓'
|
||||
};
|
||||
|
||||
export let TRANSPORTATION_TYPES_ICONS = {
|
||||
car: '🚗',
|
||||
plane: '✈️',
|
||||
train: '🚆',
|
||||
bus: '🚌',
|
||||
boat: '⛵',
|
||||
bike: '🚲',
|
||||
walking: '🚶',
|
||||
other: '❓'
|
||||
};
|
||||
|
||||
export function getAdventureTypeLabel(type: string) {
|
||||
// return the emoji ADVENTURE_TYPE_ICONS label for the given type if not found return ? emoji
|
||||
if (type in ADVENTURE_TYPE_ICONS) {
|
||||
|
|
|
@ -247,7 +247,12 @@
|
|||
"price": "Preis",
|
||||
"reservation_number": "Reservierungsnummer",
|
||||
"welcome_map_info": "Frei zugängliche Abenteuer auf diesem Server",
|
||||
"open_in_maps": "In Karten öffnen"
|
||||
"open_in_maps": "In Karten öffnen",
|
||||
"all_day": "Den ganzen Tag",
|
||||
"collection_no_start_end_date": "Durch das Hinzufügen eines Start- und Enddatums zur Sammlung werden Reiseroutenplanungsfunktionen auf der Sammlungsseite freigegeben.",
|
||||
"date_itinerary": "Datumstrecke",
|
||||
"no_ordered_items": "Fügen Sie der Sammlung Elemente mit Daten hinzu, um sie hier zu sehen.",
|
||||
"ordered_itinerary": "Reiseroute bestellt"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Entdecken, planen und erkunden Sie mühelos",
|
||||
|
|
|
@ -131,6 +131,7 @@
|
|||
"search_for_location": "Search for a location",
|
||||
"clear_map": "Clear map",
|
||||
"search_results": "Searh results",
|
||||
"collection_no_start_end_date": "Adding a start and end date to the collection will unlock itinerary planning features in the collection page.",
|
||||
"no_results": "No results found",
|
||||
"wiki_desc": "Pulls excerpt from Wikipedia article matching the name of the adventure.",
|
||||
"attachments": "Attachments",
|
||||
|
@ -250,6 +251,10 @@
|
|||
"show_map": "Show Map",
|
||||
"emoji_picker": "Emoji Picker",
|
||||
"download_calendar": "Download Calendar",
|
||||
"all_day": "All Day",
|
||||
"ordered_itinerary": "Ordered Itinerary",
|
||||
"date_itinerary": "Date Itinerary",
|
||||
"no_ordered_items": "Add items with dates to the collection to see them here.",
|
||||
"date_information": "Date Information",
|
||||
"flight_information": "Flight Information",
|
||||
"out_of_range": "Not in itinerary date range",
|
||||
|
|
|
@ -295,7 +295,12 @@
|
|||
"region": "Región",
|
||||
"reservation_number": "Número de reserva",
|
||||
"welcome_map_info": "Aventuras públicas en este servidor",
|
||||
"open_in_maps": "Abrir en mapas"
|
||||
"open_in_maps": "Abrir en mapas",
|
||||
"all_day": "Todo el día",
|
||||
"collection_no_start_end_date": "Agregar una fecha de inicio y finalización a la colección desbloqueará las funciones de planificación del itinerario en la página de colección.",
|
||||
"date_itinerary": "Itinerario de fecha",
|
||||
"no_ordered_items": "Agregue elementos con fechas a la colección para verlos aquí.",
|
||||
"ordered_itinerary": "Itinerario ordenado"
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "Todo",
|
||||
|
|
|
@ -247,7 +247,12 @@
|
|||
"region": "Région",
|
||||
"reservation_number": "Numéro de réservation",
|
||||
"welcome_map_info": "Aventures publiques sur ce serveur",
|
||||
"open_in_maps": "Ouvert dans les cartes"
|
||||
"open_in_maps": "Ouvert dans les cartes",
|
||||
"all_day": "Toute la journée",
|
||||
"collection_no_start_end_date": "L'ajout d'une date de début et de fin à la collection débloquera les fonctionnalités de planification de l'itinéraire dans la page de collection.",
|
||||
"date_itinerary": "Itinéraire de date",
|
||||
"no_ordered_items": "Ajoutez des articles avec des dates à la collection pour les voir ici.",
|
||||
"ordered_itinerary": "Itinéraire ordonné"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
||||
|
|
|
@ -247,7 +247,12 @@
|
|||
"region": "Regione",
|
||||
"welcome_map_info": "Avventure pubbliche su questo server",
|
||||
"reservation_number": "Numero di prenotazione",
|
||||
"open_in_maps": "Aperto in mappe"
|
||||
"open_in_maps": "Aperto in mappe",
|
||||
"all_day": "Tutto il giorno",
|
||||
"collection_no_start_end_date": "L'aggiunta di una data di inizio e fine alla raccolta sbloccherà le funzionalità di pianificazione dell'itinerario nella pagina di raccolta.",
|
||||
"date_itinerary": "Itinerario della data",
|
||||
"no_ordered_items": "Aggiungi articoli con date alla collezione per vederli qui.",
|
||||
"ordered_itinerary": "Itinerario ordinato"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
||||
|
|
|
@ -247,7 +247,12 @@
|
|||
"region": "지역",
|
||||
"reservation_number": "예약 번호",
|
||||
"welcome_map_info": "이 서버의 공개 모험",
|
||||
"open_in_maps": "지도에서 열립니다"
|
||||
"open_in_maps": "지도에서 열립니다",
|
||||
"all_day": "하루 종일",
|
||||
"collection_no_start_end_date": "컬렉션에 시작 및 종료 날짜를 추가하면 컬렉션 페이지에서 여정 계획 기능이 잠금 해제됩니다.",
|
||||
"date_itinerary": "날짜 일정",
|
||||
"no_ordered_items": "컬렉션에 날짜가있는 항목을 추가하여 여기에서 확인하십시오.",
|
||||
"ordered_itinerary": "주문한 여정"
|
||||
},
|
||||
"auth": {
|
||||
"both_passwords_required": "두 암호 모두 필요합니다",
|
||||
|
|
|
@ -247,7 +247,12 @@
|
|||
"lodging_information": "Informatie overliggen",
|
||||
"price": "Prijs",
|
||||
"region": "Regio",
|
||||
"open_in_maps": "Open in kaarten"
|
||||
"open_in_maps": "Open in kaarten",
|
||||
"all_day": "De hele dag",
|
||||
"collection_no_start_end_date": "Als u een start- en einddatum aan de collectie toevoegt, ontgrendelt u de functies van de planning van de route ontgrendelen in de verzamelpagina.",
|
||||
"date_itinerary": "Datumroute",
|
||||
"no_ordered_items": "Voeg items toe met datums aan de collectie om ze hier te zien.",
|
||||
"ordered_itinerary": "Besteld reisschema"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Ontdek, plan en verken met gemak",
|
||||
|
|
|
@ -295,7 +295,12 @@
|
|||
"region": "Region",
|
||||
"reservation_number": "Numer rezerwacji",
|
||||
"welcome_map_info": "Publiczne przygody na tym serwerze",
|
||||
"open_in_maps": "Otwarte w mapach"
|
||||
"open_in_maps": "Otwarte w mapach",
|
||||
"all_day": "Cały dzień",
|
||||
"collection_no_start_end_date": "Dodanie daty rozpoczęcia i końca do kolekcji odblokuje funkcje planowania planu podróży na stronie kolekcji.",
|
||||
"date_itinerary": "Trasa daty",
|
||||
"no_ordered_items": "Dodaj przedmioty z datami do kolekcji, aby je zobaczyć tutaj.",
|
||||
"ordered_itinerary": "Zamówiono trasę"
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Lista krajów",
|
||||
|
|
|
@ -247,7 +247,12 @@
|
|||
"price": "Pris",
|
||||
"region": "Område",
|
||||
"reservation_number": "Bokningsnummer",
|
||||
"open_in_maps": "Kappas in"
|
||||
"open_in_maps": "Kappas in",
|
||||
"all_day": "Hela dagen",
|
||||
"collection_no_start_end_date": "Att lägga till ett start- och slutdatum till samlingen kommer att låsa upp planeringsfunktioner för resplan på insamlingssidan.",
|
||||
"date_itinerary": "Datum resplan",
|
||||
"no_ordered_items": "Lägg till objekt med datum i samlingen för att se dem här.",
|
||||
"ordered_itinerary": "Beställd resplan"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Upptäck, planera och utforska med lätthet",
|
||||
|
|
|
@ -295,7 +295,12 @@
|
|||
"lodging_information": "住宿信息",
|
||||
"price": "价格",
|
||||
"reservation_number": "预订号",
|
||||
"open_in_maps": "在地图上打开"
|
||||
"open_in_maps": "在地图上打开",
|
||||
"all_day": "整天",
|
||||
"collection_no_start_end_date": "在集合页面中添加开始日期和结束日期将在“收集”页面中解锁行程计划功能。",
|
||||
"date_itinerary": "日期行程",
|
||||
"no_ordered_items": "将带有日期的项目添加到集合中,以便在此处查看它们。",
|
||||
"ordered_itinerary": "订购了行程"
|
||||
},
|
||||
"auth": {
|
||||
"forgot_password": "忘记密码?",
|
||||
|
|
|
@ -91,6 +91,7 @@
|
|||
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
||||
import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte';
|
||||
import AttachmentCard from '$lib/components/AttachmentCard.svelte';
|
||||
import { isAllDay } from '$lib';
|
||||
|
||||
onMount(async () => {
|
||||
if (data.props.adventure) {
|
||||
|
@ -410,23 +411,33 @@
|
|||
</p>
|
||||
<!-- show each visit start and end date as well as notes -->
|
||||
{#each adventure.visits as visit}
|
||||
<div class="grid gap-2">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{visit.start_date
|
||||
? new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<p>
|
||||
{#if isAllDay(visit.start_date)}
|
||||
<!-- For all-day events, show just the date -->
|
||||
{new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
: ''}
|
||||
{visit.end_date &&
|
||||
visit.end_date !== '' &&
|
||||
visit.end_date !== visit.start_date
|
||||
? ' - ' +
|
||||
new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||
})}
|
||||
{:else}
|
||||
<!-- For timed events, show date and time -->
|
||||
{new Date(visit.start_date).toLocaleDateString()} ({new Date(
|
||||
visit.start_date
|
||||
).toLocaleTimeString()})
|
||||
{/if}
|
||||
</p>
|
||||
{#if visit.end_date && visit.end_date !== visit.start_date}
|
||||
<p>
|
||||
- {new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
: ''}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground -mt-2 mb-2">{visit.notes}</p>
|
||||
})}
|
||||
{#if !isAllDay(visit.end_date)}
|
||||
({new Date(visit.end_date).toLocaleTimeString()})
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="whitespace-pre-wrap -mt-2 mb-2">{visit.notes}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -445,12 +456,14 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<a
|
||||
class="btn btn-neutral btn-sm max-w-32"
|
||||
href={`https://maps.apple.com/?q=${adventure.latitude},${adventure.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{$t('adventures.open_in_maps')}</a
|
||||
>
|
||||
{#if adventure.longitude && adventure.latitude}
|
||||
<a
|
||||
class="btn btn-neutral btn-sm max-w-32"
|
||||
href={`https://maps.apple.com/?q=${adventure.latitude},${adventure.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{$t('adventures.open_in_maps')}</a
|
||||
>
|
||||
{/if}
|
||||
<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 rounded-lg"
|
||||
|
|
|
@ -208,7 +208,7 @@ export const actions: Actions = {
|
|||
const order_direction = formData.get('order_direction') as string;
|
||||
const order_by = formData.get('order_by') as string;
|
||||
|
||||
console.log(order_direction, order_by);
|
||||
// console.log(order_direction, order_by);
|
||||
|
||||
let adventures: Adventure[] = [];
|
||||
|
||||
|
@ -242,7 +242,7 @@ export const actions: Actions = {
|
|||
previous = res.previous;
|
||||
count = res.count;
|
||||
adventures = [...adventures, ...visited];
|
||||
console.log(next, previous, count);
|
||||
// console.log(next, previous, count);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
|
||||
let collections: Collection[] = data.props.adventures || [];
|
||||
|
||||
let currentSort = { attribute: 'name', order: 'asc' };
|
||||
|
||||
let newType: string = '';
|
||||
|
||||
let resultsPerPage: number = 25;
|
||||
|
@ -235,17 +233,36 @@
|
|||
aria-label={$t(`adventures.descending`)}
|
||||
/>
|
||||
</div>
|
||||
<p class="text-lg font-semibold mt-2 mb-2">{$t('adventures.order_by')}</p>
|
||||
<div class="join">
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="order_by"
|
||||
id="upated_at"
|
||||
value="upated_at"
|
||||
aria-label={$t('adventures.updated')}
|
||||
checked
|
||||
/>
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="order_by"
|
||||
id="start_date"
|
||||
value="start_date"
|
||||
aria-label={$t('adventures.start_date')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="order_by"
|
||||
id="name"
|
||||
value="name"
|
||||
aria-label={$t('adventures.name')}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
name="order_by"
|
||||
id="name"
|
||||
class="radio radio-primary"
|
||||
checked
|
||||
value="name"
|
||||
hidden
|
||||
/>
|
||||
<button type="submit" class="btn btn-success btn-primary mt-4"
|
||||
>{$t(`adventures.sort`)}</button
|
||||
>
|
||||
|
|
|
@ -133,9 +133,70 @@
|
|||
}
|
||||
|
||||
let currentView: string = 'itinerary';
|
||||
let currentItineraryView: string = 'date';
|
||||
|
||||
let adventures: Adventure[] = [];
|
||||
|
||||
// Add this after your existing MapLibre markers
|
||||
|
||||
// Add this after your existing MapLibre markers
|
||||
|
||||
// Create line data from orderedItems
|
||||
$: lineData = createLineData(orderedItems);
|
||||
|
||||
// Function to create GeoJSON line data from ordered items
|
||||
function createLineData(
|
||||
items: Array<{
|
||||
item: Adventure | Transportation | Lodging | Note | Checklist;
|
||||
start: string;
|
||||
end: string;
|
||||
}>
|
||||
) {
|
||||
if (items.length < 2) return null;
|
||||
|
||||
const coordinates: [number, number][] = [];
|
||||
|
||||
// Extract coordinates from each item
|
||||
for (const orderItem of items) {
|
||||
const item = orderItem.item;
|
||||
|
||||
if (
|
||||
'origin_longitude' in item &&
|
||||
'origin_latitude' in item &&
|
||||
'destination_longitude' in item &&
|
||||
'destination_latitude' in item &&
|
||||
item.origin_longitude &&
|
||||
item.origin_latitude &&
|
||||
item.destination_longitude &&
|
||||
item.destination_latitude
|
||||
) {
|
||||
// For Transportation, add both origin and destination points
|
||||
coordinates.push([item.origin_longitude, item.origin_latitude]);
|
||||
coordinates.push([item.destination_longitude, item.destination_latitude]);
|
||||
} else if ('longitude' in item && 'latitude' in item && item.longitude && item.latitude) {
|
||||
// Handle Adventure and Lodging types
|
||||
coordinates.push([item.longitude, item.latitude]);
|
||||
}
|
||||
}
|
||||
|
||||
// Only create line data if we have at least 2 coordinates
|
||||
if (coordinates.length >= 2) {
|
||||
return {
|
||||
type: 'Feature' as const,
|
||||
properties: {
|
||||
name: 'Itinerary Path',
|
||||
description: 'Path connecting chronological items'
|
||||
},
|
||||
geometry: {
|
||||
type: 'LineString' as const,
|
||||
coordinates: coordinates
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
let numVisited: number = 0;
|
||||
let numAdventures: number = 0;
|
||||
|
||||
|
@ -169,6 +230,63 @@
|
|||
}
|
||||
}
|
||||
|
||||
let orderedItems: Array<{
|
||||
item: Adventure | Transportation | Lodging;
|
||||
type: 'adventure' | 'transportation' | 'lodging';
|
||||
start: string; // ISO date string
|
||||
end: string; // ISO date string
|
||||
}> = [];
|
||||
|
||||
$: {
|
||||
// Reset ordered items
|
||||
orderedItems = [];
|
||||
|
||||
// Add Adventures (using visit dates)
|
||||
adventures.forEach((adventure) => {
|
||||
adventure.visits.forEach((visit) => {
|
||||
orderedItems.push({
|
||||
item: adventure,
|
||||
start: visit.start_date,
|
||||
end: visit.end_date,
|
||||
type: 'adventure'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add Transportation
|
||||
transportations.forEach((transport) => {
|
||||
if (transport.date) {
|
||||
// Only add if date exists
|
||||
orderedItems.push({
|
||||
item: transport,
|
||||
start: transport.date,
|
||||
end: transport.end_date || transport.date, // Use end_date if available, otherwise use date,
|
||||
type: 'transportation'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add Lodging
|
||||
lodging.forEach((lodging) => {
|
||||
if (lodging.check_in) {
|
||||
// Only add if check_in exists
|
||||
orderedItems.push({
|
||||
item: lodging,
|
||||
start: lodging.check_in,
|
||||
end: lodging.check_out || lodging.check_in, // Use check_out if available, otherwise use check_in,
|
||||
type: 'lodging'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort all items chronologically by start date
|
||||
orderedItems.sort((a, b) => {
|
||||
const dateA = new Date(a.start).getTime();
|
||||
const dateB = new Date(b.start).getTime();
|
||||
return dateA - dateB;
|
||||
});
|
||||
}
|
||||
|
||||
$: {
|
||||
numAdventures = adventures.length;
|
||||
numVisited = adventures.filter((adventure) => adventure.is_visited).length;
|
||||
|
@ -186,6 +304,7 @@
|
|||
} else {
|
||||
notFound = true;
|
||||
}
|
||||
|
||||
if (collection.start_date && collection.end_date) {
|
||||
numberOfDays =
|
||||
Math.floor(
|
||||
|
@ -806,133 +925,265 @@
|
|||
})}</span
|
||||
>
|
||||
</p>
|
||||
<div class="join mt-2">
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="options"
|
||||
aria-label={$t('adventures.date_itinerary')}
|
||||
checked={currentItineraryView == 'date'}
|
||||
on:change={() => (currentItineraryView = 'date')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="options"
|
||||
aria-label={$t('adventures.ordered_itinerary')}
|
||||
checked={currentItineraryView == 'ordered'}
|
||||
on:change={() => (currentItineraryView = 'ordered')}
|
||||
/>
|
||||
</div>
|
||||
</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]}
|
||||
{#if currentItineraryView == 'date'}
|
||||
<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 dayLodging =
|
||||
groupLodgingByDate(lodging, 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
|
||||
] || []}
|
||||
{@const dayAdventures =
|
||||
groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays + 1)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayTransportations =
|
||||
groupTransportationsByDate(
|
||||
transportations,
|
||||
new Date(collection.start_date),
|
||||
numberOfDays + 1
|
||||
)[dateString] || []}
|
||||
{@const dayLodging =
|
||||
groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays + 1)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayNotes =
|
||||
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays + 1)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayChecklists =
|
||||
groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays + 1)[
|
||||
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 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 dayLodging.length > 0}
|
||||
{#each dayLodging as hotel}
|
||||
<LodgingCard
|
||||
lodging={hotel}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
lodging = lodging.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={editLodging}
|
||||
/>
|
||||
{/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>
|
||||
</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}
|
||||
{collection}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayTransportations.length > 0}
|
||||
{#each dayTransportations as transportation}
|
||||
<TransportationCard
|
||||
{transportation}
|
||||
{collection}
|
||||
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}
|
||||
{collection}
|
||||
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 dayLodging.length > 0}
|
||||
{#each dayLodging as hotel}
|
||||
<LodgingCard
|
||||
lodging={hotel}
|
||||
{collection}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
lodging = lodging.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={editLodging}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayChecklists.length > 0}
|
||||
{#each dayChecklists as checklist}
|
||||
<ChecklistCard
|
||||
{checklist}
|
||||
{collection}
|
||||
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 dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0 && dayLodging.length == 0}
|
||||
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0 && dayLodging.length == 0}
|
||||
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-full max-w-4xl relative">
|
||||
<!-- Vertical timeline line that spans the entire height -->
|
||||
{#if orderedItems.length > 0}
|
||||
<div class="absolute left-8 top-0 bottom-0 w-1 bg-primary"></div>
|
||||
{/if}
|
||||
<ul class="relative">
|
||||
{#each orderedItems as orderedItem, index}
|
||||
<li class="relative pl-20 mb-8">
|
||||
<!-- Timeline Icon -->
|
||||
<div
|
||||
class="absolute left-0 top-0 flex items-center justify-center w-16 h-16 bg-base-200 rounded-full border-2 border-primary"
|
||||
>
|
||||
{#if orderedItem.type === 'adventure' && orderedItem.item && 'category' in orderedItem.item && orderedItem.item.category && 'icon' in orderedItem.item.category}
|
||||
<span class="text-2xl">{orderedItem.item.category.icon}</span>
|
||||
{:else if orderedItem.type === 'transportation' && orderedItem.item && 'origin_latitude' in orderedItem.item}
|
||||
<span class="text-2xl">{getTransportationEmoji(orderedItem.item.type)}</span
|
||||
>
|
||||
{:else if orderedItem.type === 'lodging' && orderedItem.item && 'reservation_number' in orderedItem.item}
|
||||
<span class="text-2xl">{getLodgingIcon(orderedItem.item.type)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Card Content -->
|
||||
<div class="bg-base-200 p-6 rounded-lg shadow-lg">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="badge badge-lg">{$t(`adventures.${orderedItem.type}`)}</span>
|
||||
<div class="text-sm opacity-80 text-right">
|
||||
{new Date(orderedItem.start).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
{#if orderedItem.start !== orderedItem.end}
|
||||
<div>
|
||||
{new Date(orderedItem.start).toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
-
|
||||
{new Date(orderedItem.end).toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<!-- Duration -->
|
||||
{Math.round(
|
||||
(new Date(orderedItem.end).getTime() -
|
||||
new Date(orderedItem.start).getTime()) /
|
||||
1000 /
|
||||
60 /
|
||||
60
|
||||
)}h
|
||||
{Math.round(
|
||||
((new Date(orderedItem.end).getTime() -
|
||||
new Date(orderedItem.start).getTime()) /
|
||||
1000 /
|
||||
60 /
|
||||
60 -
|
||||
Math.floor(
|
||||
(new Date(orderedItem.end).getTime() -
|
||||
new Date(orderedItem.start).getTime()) /
|
||||
1000 /
|
||||
60 /
|
||||
60
|
||||
)) *
|
||||
60
|
||||
)}m
|
||||
</div>
|
||||
{:else}
|
||||
<p>{$t('adventures.all_day')} ⏱️</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if orderedItem.type === 'adventure' && orderedItem.item && 'images' in orderedItem.item}
|
||||
<AdventureCard
|
||||
user={data.user}
|
||||
on:edit={editAdventure}
|
||||
on:delete={deleteAdventure}
|
||||
adventure={orderedItem.item}
|
||||
{collection}
|
||||
/>
|
||||
{:else if orderedItem.type === 'transportation' && orderedItem.item && 'origin_latitude' in orderedItem.item}
|
||||
<TransportationCard
|
||||
transportation={orderedItem.item}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
transportations = transportations.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={editTransportation}
|
||||
{collection}
|
||||
/>
|
||||
{:else if orderedItem.type === 'lodging' && orderedItem.item && 'reservation_number' in orderedItem.item}
|
||||
<LodgingCard
|
||||
lodging={orderedItem.item}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
lodging = lodging.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={editLodging}
|
||||
{collection}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if orderedItems.length === 0}
|
||||
<div class="alert alert-info">
|
||||
<p class="text-center text-lg">{$t('adventures.no_ordered_items')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
@ -999,6 +1250,19 @@
|
|||
</Marker>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if lineData}
|
||||
<GeoJSON data={lineData}>
|
||||
<LineLayer
|
||||
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
||||
paint={{
|
||||
'line-width': 4,
|
||||
'line-color': '#0088CC', // Blue line to distinguish from transportation lines
|
||||
'line-opacity': 0.8,
|
||||
'line-dasharray': [2, 1] // Dashed line to differentiate from direct transportation lines
|
||||
}}
|
||||
/>
|
||||
</GeoJSON>
|
||||
{/if}
|
||||
{#each transportations as transportation}
|
||||
{#if transportation.origin_latitude && transportation.origin_longitude && transportation.destination_latitude && transportation.destination_longitude}
|
||||
<!-- Origin Marker -->
|
||||
|
@ -1040,34 +1304,6 @@
|
|||
</p>
|
||||
</Popup>
|
||||
</Marker>
|
||||
|
||||
<!-- Line connecting origin and destination -->
|
||||
<GeoJSON
|
||||
data={{
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
name: transportation.name,
|
||||
type: transportation.type
|
||||
},
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: [
|
||||
[transportation.origin_longitude, transportation.origin_latitude],
|
||||
[transportation.destination_longitude, transportation.destination_latitude]
|
||||
]
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LineLayer
|
||||
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
||||
paint={{
|
||||
'line-width': 3,
|
||||
'line-color': '#898989', // customize your line color here
|
||||
'line-opacity': 0.8
|
||||
// 'line-dasharray': [5, 2]
|
||||
}}
|
||||
/>
|
||||
</GeoJSON>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
|
@ -1233,13 +1469,16 @@
|
|||
{recomendation.name || $t('recomendations.recommendation')}
|
||||
</h2>
|
||||
<div class="badge badge-primary">{recomendation.tag}</div>
|
||||
{#if recomendation.address}
|
||||
{#if recomendation.address && (recomendation.address.housenumber || recomendation.address.street || recomendation.address.city || recomendation.address.state || recomendation.address.postcode)}
|
||||
<p class="text-md">
|
||||
<strong>{$t('recomendations.address')}:</strong>
|
||||
{recomendation.address.housenumber}
|
||||
{recomendation.address.street}, {recomendation.address.city}, {recomendation
|
||||
.address.state}
|
||||
{recomendation.address.postcode}
|
||||
{#if recomendation.address.housenumber}{recomendation.address
|
||||
.housenumber}{/if}
|
||||
{#if recomendation.address.street}
|
||||
{recomendation.address.street}{/if}
|
||||
{#if recomendation.address.city}, {recomendation.address.city}{/if}
|
||||
{#if recomendation.address.state}, {recomendation.address.state}{/if}
|
||||
{#if recomendation.address.postcode}, {recomendation.address.postcode}{/if}
|
||||
</p>
|
||||
{/if}
|
||||
{#if recomendation.contact}
|
||||
|
|
|
@ -74,8 +74,6 @@ export const actions: Actions = {
|
|||
} else {
|
||||
const setCookieHeader = loginFetch.headers.get('Set-Cookie');
|
||||
|
||||
console.log('setCookieHeader:', setCookieHeader);
|
||||
|
||||
if (setCookieHeader) {
|
||||
// Regular expression to match sessionid cookie and its expiry
|
||||
const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/;
|
||||
|
|
|
@ -164,6 +164,14 @@
|
|||
|
||||
{#if filteredCountries.length === 0}
|
||||
<p class="text-center font-bold text-2xl mt-12">{$t('worldtravel.no_countries_found')}</p>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/updating.html#updating-the-region-data"
|
||||
target="_blank">{$t('settings.documentation_link')}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue