mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-04 20:55:19 +02:00
Merge branch 'development' into main
This commit is contained in:
commit
44ede92b92
31 changed files with 939 additions and 270 deletions
|
@ -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