Date Fixes, Translations, Misc Bugs (#840)

* Translated using Weblate (Spanish)

Currently translated at 100.0% (956 of 956 strings)

Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/

* Added translation using Weblate (English (United States))

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (956 of 956 strings)

Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/nb_NO/

* Remove empty English (United States) locale file

* Translated using Weblate (Spanish)

Currently translated at 100.0% (956 of 956 strings)

Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/

* [BUG]Ordered Itinerary includes visits that are outside itinerary date range
Fixes #746

* [BUG] Server Error (500) when trying to access the API docs
Fixes #712

* [BUG] Single day Collections will think location visits are out of date range
Fixes #827

* Fixes #654

* Translated using Weblate (Spanish)

Currently translated at 100.0% (956 of 956 strings)

Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/

* Added Slovak translations (#815)

* Created sk.json

* Update Navbar.svelte

* Update +layout.svelte

---------

Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>

* Implement code changes to enhance functionality and improve performance

---------

Co-authored-by: Nikolai Eidsheim <nikolai.eidsheim@gmail.com>
Co-authored-by: Sergio <garcia.sergio@me.com>
Co-authored-by: fantastron27 <fantastron27@gmail.com>
This commit is contained in:
Sean Morley
2025-09-06 21:45:40 -04:00
committed by GitHub
parent 4a53b1fdfd
commit 96dfda1cfb
31 changed files with 5343 additions and 4242 deletions

View File

@@ -36,4 +36,8 @@ EMAIL_BACKEND='console'
# PGDATABASE='adventurelog' # PGDATABASE='adventurelog'
# PGUSER='admin' # PGUSER='admin'
# PGPASSWORD='admin' # PGPASSWORD='admin'
# For Sean's use:
# re-sync the development branch with main after doing squash merges
# git fetch origin && git checkout development && git reset --hard origin/main && git push origin development --force
# ------------------- # # ------------------- #

View File

@@ -26,4 +26,5 @@ geopy==2.4.1
psutil==6.1.1 psutil==6.1.1
geojson==3.2.0 geojson==3.2.0
gpxpy==1.6.2 gpxpy==1.6.2
pymemcache==4.0.0 pymemcache==4.0.0
legacy-cgi==2.6.3

View File

@@ -9,29 +9,18 @@
import TrashCan from '~icons/mdi/trash-can'; import TrashCan from '~icons/mdi/trash-can';
import Calendar from '~icons/mdi/calendar'; import Calendar from '~icons/mdi/calendar';
import DeleteWarning from './DeleteWarning.svelte'; import DeleteWarning from './DeleteWarning.svelte';
import { isEntityOutsideCollectionDateRange } from '$lib/dateUtils';
export let checklist: Checklist; export let checklist: Checklist;
export let user: User | null = null; export let user: User | null = null;
export let collection: Collection | null = null; export let collection: Collection;
let isWarningModalOpen: boolean = false; let isWarningModalOpen: boolean = false;
let unlinked: boolean = false; let outsideCollectionRange: boolean = false;
$: { $: {
if (collection?.start_date && collection.end_date) { outsideCollectionRange = isEntityOutsideCollectionDateRange(checklist, collection);
const startOutsideRange =
checklist.date &&
collection.start_date < checklist.date &&
collection.end_date < checklist.date;
const endOutsideRange =
checklist.date &&
collection.start_date > checklist.date &&
collection.end_date > checklist.date;
unlinked = !!(startOutsideRange || endOutsideRange || !checklist.date);
}
} }
function editChecklist() { function editChecklist() {
@@ -71,7 +60,7 @@
<h2 class="text-xl font-bold break-words">{checklist.name}</h2> <h2 class="text-xl font-bold break-words">{checklist.name}</h2>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<div class="badge badge-primary">{$t('adventures.checklist')}</div> <div class="badge badge-primary">{$t('adventures.checklist')}</div>
{#if unlinked} {#if outsideCollectionRange}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div> <div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if} {/if}
</div> </div>

View File

@@ -14,7 +14,7 @@
let items: ChecklistItem[] = []; let items: ChecklistItem[] = [];
let constrainDates: boolean = false; let constrainDates: boolean = true;
items = checklist?.items || []; items = checklist?.items || [];

View File

@@ -22,6 +22,7 @@
import StarOutline from '~icons/mdi/star-outline'; import StarOutline from '~icons/mdi/star-outline';
import Eye from '~icons/mdi/eye'; import Eye from '~icons/mdi/eye';
import EyeOff from '~icons/mdi/eye-off'; import EyeOff from '~icons/mdi/eye-off';
import { isEntityOutsideCollectionDateRange } from '$lib/dateUtils';
export let type: string | null = null; export let type: string | null = null;
export let user: User | null; export let user: User | null;
@@ -48,17 +49,13 @@
} }
} }
let unlinked: boolean = false; let outsideCollectionRange: boolean = false;
// Reactive block to update `unlinked` when dependencies change
$: { $: {
if (collection && collection?.start_date && collection.end_date) { if (collection) {
unlinked = adventure.visits.every((visit) => { outsideCollectionRange = adventure.visits.every((visit) =>
if (!visit.start_date || !visit.end_date) return true; isEntityOutsideCollectionDateRange(visit, collection)
const isBeforeVisit = collection.end_date && collection.end_date < visit.start_date; );
const isAfterVisit = collection.start_date && collection.start_date > visit.end_date;
return isBeforeVisit || isAfterVisit;
});
} }
} }
@@ -199,7 +196,7 @@
> >
{adventure.is_visited ? $t('adventures.visited') : $t('adventures.planned')} {adventure.is_visited ? $t('adventures.visited') : $t('adventures.planned')}
</div> </div>
{#if unlinked} {#if outsideCollectionRange}
<div class="badge badge-sm badge-error shadow-lg">{$t('adventures.out_of_range')}</div> <div class="badge badge-sm badge-error shadow-lg">{$t('adventures.out_of_range')}</div>
{/if} {/if}
</div> </div>

View File

@@ -7,7 +7,7 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import DeleteWarning from './DeleteWarning.svelte'; import DeleteWarning from './DeleteWarning.svelte';
import { LODGING_TYPES_ICONS } from '$lib'; import { LODGING_TYPES_ICONS } from '$lib';
import { formatDateInTimezone } from '$lib/dateUtils'; import { formatDateInTimezone, isEntityOutsideCollectionDateRange } from '$lib/dateUtils';
import { formatAllDayDate } from '$lib/dateUtils'; import { formatAllDayDate } from '$lib/dateUtils';
import { isAllDay } from '$lib'; import { isAllDay } from '$lib';
import CardCarousel from './CardCarousel.svelte'; import CardCarousel from './CardCarousel.svelte';
@@ -31,38 +31,11 @@
dispatch('edit', lodging); dispatch('edit', lodging);
} }
let unlinked: boolean = false; let outsideCollectionRange: boolean = false;
$: { $: {
if (collection?.start_date && collection.end_date) { if (collection) {
// Parse transportation dates outsideCollectionRange = isEntityOutsideCollectionDateRange(lodging, collection);
let transportationStartDate = lodging.check_in
? new Date(lodging.check_in.split('T')[0]) // Ensure proper date parsing
: null;
let transportationEndDate = lodging.check_out
? new Date(lodging.check_out.split('T')[0])
: null;
// Parse collection dates
let collectionStartDate = new Date(collection.start_date);
let collectionEndDate = new Date(collection.end_date);
// Check if the collection range is outside the transportation range
const startOutsideRange =
transportationStartDate &&
collectionStartDate < transportationStartDate &&
collectionEndDate < transportationStartDate;
const endOutsideRange =
transportationEndDate &&
collectionStartDate > transportationEndDate &&
collectionEndDate > transportationEndDate;
unlinked = !!(
startOutsideRange ||
endOutsideRange ||
(!transportationStartDate && !transportationEndDate)
);
} }
} }
@@ -120,7 +93,7 @@
{$t(`lodging.${lodging.type}`)} {$t(`lodging.${lodging.type}`)}
{getLodgingIcon(lodging.type)} {getLodgingIcon(lodging.type)}
</div> </div>
{#if unlinked} {#if outsideCollectionRange}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div> <div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if} {/if}
</div> </div>

View File

@@ -63,7 +63,8 @@
ru: 'Русский', ru: 'Русский',
ja: '日本語', ja: '日本語',
ar: 'العربية', ar: 'العربية',
'pt-br': 'Português (Brasil)' 'pt-br': 'Português (Brasil)',
'sk': 'Slovenský'
}; };
const submitLocaleChange = (event: Event) => { const submitLocaleChange = (event: Event) => {

View File

@@ -15,23 +15,18 @@
import TrashCan from '~icons/mdi/trash-can'; import TrashCan from '~icons/mdi/trash-can';
import Calendar from '~icons/mdi/calendar'; import Calendar from '~icons/mdi/calendar';
import DeleteWarning from './DeleteWarning.svelte'; import DeleteWarning from './DeleteWarning.svelte';
import { isEntityOutsideCollectionDateRange } from '$lib/dateUtils';
export let note: Note; export let note: Note;
export let user: User | null = null; export let user: User | null = null;
export let collection: Collection | null = null; export let collection: Collection | null = null;
let isWarningModalOpen: boolean = false; let isWarningModalOpen: boolean = false;
let unlinked: boolean = false; let outsideCollectionRange: boolean = false;
$: { $: {
if (collection?.start_date && collection.end_date) { if (collection) {
const startOutsideRange = outsideCollectionRange = isEntityOutsideCollectionDateRange(note, collection);
note.date && collection.start_date < note.date && collection.end_date < note.date;
const endOutsideRange =
note.date && collection.start_date > note.date && collection.end_date > note.date;
unlinked = !!(startOutsideRange || endOutsideRange || !note.date);
} }
} }
@@ -73,7 +68,7 @@
<h2 class="text-xl font-bold break-words">{note.name}</h2> <h2 class="text-xl font-bold break-words">{note.name}</h2>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<div class="badge badge-primary">{$t('adventures.note')}</div> <div class="badge badge-primary">{$t('adventures.note')}</div>
{#if unlinked} {#if outsideCollectionRange}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div> <div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if} {/if}
</div> </div>

View File

@@ -17,7 +17,7 @@
export let collection: Collection; export let collection: Collection;
export let user: User | null = null; export let user: User | null = null;
let constrainDates: boolean = false; let constrainDates: boolean = true;
let isReadOnly = let isReadOnly =
!(note && user?.uuid == note?.user) && !(note && user?.uuid == note?.user) &&

View File

@@ -8,7 +8,11 @@
import DeleteWarning from './DeleteWarning.svelte'; import DeleteWarning from './DeleteWarning.svelte';
// import ArrowDownThick from '~icons/mdi/arrow-down-thick'; // import ArrowDownThick from '~icons/mdi/arrow-down-thick';
import { TRANSPORTATION_TYPES_ICONS } from '$lib'; import { TRANSPORTATION_TYPES_ICONS } from '$lib';
import { formatAllDayDate, formatDateInTimezone } from '$lib/dateUtils'; import {
formatAllDayDate,
formatDateInTimezone,
isEntityOutsideCollectionDateRange
} from '$lib/dateUtils';
import { isAllDay } from '$lib'; import { isAllDay } from '$lib';
import CardCarousel from './CardCarousel.svelte'; import CardCarousel from './CardCarousel.svelte';
@@ -36,52 +40,11 @@
dispatch('edit', transportation); dispatch('edit', transportation);
} }
let unlinked: boolean = false; let outsideCollectionRange: boolean = false;
$: { $: {
if (collection?.start_date && collection.end_date) { if (collection) {
// Parse transportation dates outsideCollectionRange = isEntityOutsideCollectionDateRange(transportation, collection);
let transportationStartDate = transportation.date
? new Date(transportation.date.split('T')[0]) // Ensure proper date parsing
: null;
let transportationEndDate = transportation.end_date
? new Date(transportation.end_date.split('T')[0])
: null;
// Parse collection dates
let collectionStartDate = new Date(collection.start_date);
let collectionEndDate = new Date(collection.end_date);
// // Debugging outputs
// console.log(
// 'Transportation Start Date:',
// transportationStartDate,
// 'Transportation End Date:',
// transportationEndDate
// );
// console.log(
// 'Collection Start Date:',
// collectionStartDate,
// 'Collection End Date:',
// collectionEndDate
// );
// Check if the collection range is outside the transportation range
const startOutsideRange =
transportationStartDate &&
collectionStartDate < transportationStartDate &&
collectionEndDate < transportationStartDate;
const endOutsideRange =
transportationEndDate &&
collectionStartDate > transportationEndDate &&
collectionEndDate > transportationEndDate;
unlinked = !!(
startOutsideRange ||
endOutsideRange ||
(!transportationStartDate && !transportationEndDate)
);
} }
} }
@@ -165,7 +128,7 @@
{#if transportation.type === 'plane' && transportation.flight_number} {#if transportation.type === 'plane' && transportation.flight_number}
<div class="badge badge-neutral">{transportation.flight_number}</div> <div class="badge badge-neutral">{transportation.flight_number}</div>
{/if} {/if}
{#if unlinked} {#if outsideCollectionRange}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div> <div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if} {/if}
</div> </div>

View File

@@ -173,6 +173,10 @@
Math.round(transportation.destination_longitude * 1e6) / 1e6; Math.round(transportation.destination_longitude * 1e6) / 1e6;
} }
if (transportation.date && !transportation.end_date) {
transportation.end_date = transportation.date;
}
if (!transportation.type) { if (!transportation.type) {
transportation.type = 'other'; transportation.type = 'other';
} }

View File

@@ -28,6 +28,7 @@
import ArrowLeftIcon from '~icons/mdi/arrow-left'; import ArrowLeftIcon from '~icons/mdi/arrow-left';
import RunFastIcon from '~icons/mdi/run-fast'; import RunFastIcon from '~icons/mdi/run-fast';
import LoadingIcon from '~icons/mdi/loading'; import LoadingIcon from '~icons/mdi/loading';
import InfoIcon from '~icons/mdi/information';
import UploadIcon from '~icons/mdi/upload'; import UploadIcon from '~icons/mdi/upload';
import FileIcon from '~icons/mdi/file'; import FileIcon from '~icons/mdi/file';
import CloseIcon from '~icons/mdi/close'; import CloseIcon from '~icons/mdi/close';
@@ -1535,6 +1536,17 @@
</div> </div>
</div> </div>
<!-- if localStartDate and localEndDate are set, show a callout saying its not saved yet -->
{#if localStartDate || localEndDate}
<div class="alert alert-neutral">
<InfoIcon class="w-5 h-5" />
<div>
<div class="font-medium text-sm">{$t('adventures.dates_not_saved')}</div>
<div class="text-xs opacity-75">{$t('adventures.dates_not_saved_description')}</div>
</div>
</div>
{/if}
<div class="flex gap-3 justify-end pt-4"> <div class="flex gap-3 justify-end pt-4">
<button class="btn btn-neutral-200 gap-2" on:click={handleBack}> <button class="btn btn-neutral-200 gap-2" on:click={handleBack}>
<ArrowLeftIcon class="w-5 h-5" /> <ArrowLeftIcon class="w-5 h-5" />

View File

@@ -1,34 +1,27 @@
// @ts-ignore // @ts-ignore
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import type { Checklist, Collection, Lodging, Note, Transportation, Visit } from './types';
import { isAllDay } from '$lib';
/** /**
* Convert a UTC ISO date to a datetime-local value in the specified timezone * Convert a UTC ISO date to a datetime-local value in the specified timezone
* @param utcDate - UTC date in ISO format or null
* @param timezone - Target timezone (defaults to browser timezone)
* @returns Formatted local datetime string for input fields (YYYY-MM-DDTHH:MM)
*/ */
export function toLocalDatetime( export function toLocalDatetime(
utcDate: string | null, utcDate: string | null,
timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone
): string { ): string {
if (!utcDate) return ''; if (!utcDate) return '';
const dt = DateTime.fromISO(utcDate, { zone: 'UTC' }); const dt = DateTime.fromISO(utcDate, { zone: 'UTC' });
if (!dt.isValid) return ''; if (!dt.isValid) return '';
const isoString = dt.setZone(timezone).toISO({ const isoString = dt.setZone(timezone).toISO({
suppressSeconds: true, suppressSeconds: true,
includeOffset: false includeOffset: false
}); });
return isoString ? isoString.slice(0, 16) : ''; return isoString ? isoString.slice(0, 16) : '';
} }
/** /**
* Convert a local datetime to UTC * Convert a local datetime to UTC
* @param localDate - Local datetime string in ISO format
* @param timezone - Source timezone (defaults to browser timezone)
* @returns UTC datetime in ISO format or null
*/ */
export function toUTCDatetime( export function toUTCDatetime(
localDate: string, localDate: string,
@@ -36,15 +29,12 @@ export function toUTCDatetime(
allDay: boolean = false allDay: boolean = false
): string | null { ): string | null {
if (!localDate) return null; if (!localDate) return null;
if (allDay) { if (allDay) {
// Treat input as date-only, set UTC midnight manually // Treat as date only, set UTC midnight
return DateTime.fromISO(localDate, { zone: 'UTC' }) return DateTime.fromISO(localDate, { zone: 'UTC' })
.startOf('day') .startOf('day')
.toISO({ suppressMilliseconds: true }); .toISO({ suppressMilliseconds: true });
} }
// Normal timezone conversion for datetime-local input
return DateTime.fromISO(localDate, { zone: timezone }) return DateTime.fromISO(localDate, { zone: timezone })
.toUTC() .toUTC()
.toISO({ suppressMilliseconds: true }); .toISO({ suppressMilliseconds: true });
@@ -52,8 +42,6 @@ export function toUTCDatetime(
/** /**
* Updates local datetime values based on UTC date and timezone * Updates local datetime values based on UTC date and timezone
* @param params Object containing UTC date and timezone
* @returns Object with updated local datetime string
*/ */
export function updateLocalDate({ export function updateLocalDate({
utcDate, utcDate,
@@ -62,15 +50,11 @@ export function updateLocalDate({
utcDate: string | null; utcDate: string | null;
timezone: string; timezone: string;
}) { }) {
return { return { localDate: toLocalDatetime(utcDate, timezone) };
localDate: toLocalDatetime(utcDate, timezone)
};
} }
/** /**
* Updates UTC datetime values based on local datetime and timezone * Updates UTC datetime values based on local datetime and timezone
* @param params Object containing local date and timezone
* @returns Object with updated UTC datetime string
*/ */
export function updateUTCDate({ export function updateUTCDate({
localDate, localDate,
@@ -81,40 +65,27 @@ export function updateUTCDate({
timezone: string; timezone: string;
allDay?: boolean; allDay?: boolean;
}) { }) {
return { return { utcDate: toUTCDatetime(localDate, timezone, allDay) };
utcDate: toUTCDatetime(localDate, timezone, allDay)
};
} }
/** /**
* Validate date ranges using UTC comparison * Validate date ranges using UTC comparison
* @param startDate - Start date string in UTC (ISO format)
* @param endDate - End date string in UTC (ISO format)
* @returns Object with validation result and optional error message
*/ */
export function validateDateRange( export function validateDateRange(
startDate: string, startDate: string,
endDate: string endDate: string
): { valid: boolean; error?: string } { ): { valid: boolean; error?: string } {
if (endDate && !startDate) { if (endDate && !startDate) {
return { return { valid: false, error: 'Start date is required when end date is provided' };
valid: false,
error: 'Start date is required when end date is provided'
};
} }
if ( if (
startDate && startDate &&
endDate && endDate &&
DateTime.fromISO(startDate, { zone: 'utc' }).toMillis() > DateTime.fromISO(startDate, { zone: 'utc' }).toMillis() >
DateTime.fromISO(endDate, { zone: 'utc' }).toMillis() DateTime.fromISO(endDate, { zone: 'utc' }).toMillis()
) { ) {
return { return { valid: false, error: 'Start date must be before end date (based on UTC)' };
valid: false,
error: 'Start date must be before end date (based on UTC)'
};
} }
return { valid: true }; return { valid: true };
} }
@@ -135,11 +106,6 @@ export function formatDateInTimezone(utcDate: string, timezone: string | null):
} }
} }
/**
* Format UTC date for display
* @param utcDate - UTC date in ISO format
* @returns Formatted date string without seconds (YYYY-MM-DD HH:MM)
*/
export function formatUTCDate(utcDate: string | null): string { export function formatUTCDate(utcDate: string | null): string {
if (!utcDate) return ''; if (!utcDate) return '';
const dateTime = DateTime.fromISO(utcDate); const dateTime = DateTime.fromISO(utcDate);
@@ -147,18 +113,10 @@ export function formatUTCDate(utcDate: string | null): string {
return dateTime.toISO()?.slice(0, 16).replace('T', ' ') || ''; return dateTime.toISO()?.slice(0, 16).replace('T', ' ') || '';
} }
/**
* Format all-day date for display without timezone conversion
* @param dateString - Date string in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)
* @returns Formatted date string (e.g., "Jun 1, 2025")
*/
export function formatAllDayDate(dateString: string): string { export function formatAllDayDate(dateString: string): string {
if (!dateString) return ''; if (!dateString) return '';
// Extract just the date part and add midday time to avoid timezone issues
const datePart = dateString.split('T')[0]; const datePart = dateString.split('T')[0];
const dateWithMidday = `${datePart}T12:00:00`; const dateWithMidday = `${datePart}T12:00:00`;
return new Intl.DateTimeFormat('en-US', { return new Intl.DateTimeFormat('en-US', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
@@ -166,6 +124,150 @@ export function formatAllDayDate(dateString: string): string {
}).format(new Date(dateWithMidday)); }).format(new Date(dateWithMidday));
} }
// ==== FIXED TIMEZONE-AWARE DATE RANGE LOGIC ====
/**
* Extracts start and end dates from various entity types (Luxon DateTime)
* Returns also isAllDay flag for correct comparison logic
*/
function getEntityDateRange(entity: Visit | Transportation | Lodging | Note | Checklist): {
start: DateTime | null;
end: DateTime | null;
isAllDay: boolean;
} {
let start: DateTime | null = null;
let end: DateTime | null = null;
let isAllDayEvent = false;
try {
let timezone = (entity as Visit).timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
if ('start_date' in entity && 'end_date' in entity) {
// Check if all-day (no time portion)
isAllDayEvent = isAllDay(entity.start_date) && isAllDay(entity.end_date);
console;
if (isAllDayEvent) {
start = entity.start_date
? DateTime.fromISO(entity.start_date.split('T')[0], { zone: 'UTC' }).startOf('day')
: null;
end = entity.end_date
? DateTime.fromISO(entity.end_date.split('T')[0], { zone: 'UTC' }).endOf('day')
: null;
} else {
start = DateTime.fromISO(entity.start_date, { zone: 'UTC' }).setZone(timezone);
end = DateTime.fromISO(entity.end_date, { zone: 'UTC' }).setZone(timezone);
}
} else if ('date' in entity && 'end_date' in entity) {
isAllDayEvent = !!(entity.date && entity.date.length === 10);
if (isAllDayEvent) {
start = DateTime.fromISO(entity.date, { zone: 'UTC' }).startOf('day');
end = DateTime.fromISO(entity.end_date, { zone: 'UTC' }).endOf('day');
} else {
start = DateTime.fromISO(entity.date, { zone: 'UTC' }).setZone(timezone);
end = DateTime.fromISO(entity.end_date, { zone: 'UTC' }).setZone(timezone);
}
} else if ('check_in' in entity && 'check_out' in entity) {
isAllDayEvent = !!(entity.check_in && entity.check_in.length === 10);
if (isAllDayEvent) {
start = DateTime.fromISO(entity.check_in, { zone: 'UTC' }).startOf('day');
end = DateTime.fromISO(entity.check_out, { zone: 'UTC' }).endOf('day');
} else {
start = DateTime.fromISO(entity.check_in, { zone: 'UTC' }).setZone(timezone);
end = DateTime.fromISO(entity.check_out, { zone: 'UTC' }).setZone(timezone);
}
} else if ('date' in entity) {
isAllDayEvent = !!(entity.date && entity.date.length === 10);
if (isAllDayEvent) {
start = DateTime.fromISO(entity.date, { zone: 'UTC' }).startOf('day');
end = start;
} else {
start = DateTime.fromISO(entity.date, { zone: 'UTC' }).setZone(timezone);
end = start;
}
}
} catch (error) {
console.error('Error processing entity dates:', error);
}
return { start, end, isAllDay: isAllDayEvent };
}
/**
* Extract collection start/end as Luxon DateTime w/allDay logic
*/
function getCollectionDateRange(collection: Collection): {
start: DateTime | null;
end: DateTime | null;
isAllDay: boolean;
} {
if (!collection.start_date || !collection.end_date) {
return { start: null, end: null, isAllDay: false };
}
// Assume collection always uses full datetimes in ISO string
const isAllDay = collection.start_date.length === 10 && collection.end_date.length === 10;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const start = isAllDay
? DateTime.fromISO(collection.start_date, { zone: 'UTC' }).startOf('day')
: DateTime.fromISO(collection.start_date, { zone: 'UTC' }).setZone(timezone);
const end = isAllDay
? DateTime.fromISO(collection.end_date, { zone: 'UTC' }).endOf('day')
: DateTime.fromISO(collection.end_date, { zone: 'UTC' }).setZone(timezone);
return { start, end, isAllDay };
}
/**
* Checks if an entity falls within a collection's date range (timezone-safe, all-day-aware)
*/
export function isEntityInCollectionDateRange(
entity: Visit | Transportation | Lodging | Note | Checklist,
collection: Collection
): boolean {
if (!collection?.start_date || !collection.end_date) {
return false;
}
const { start: entityStart, end: entityEnd, isAllDay: entityAllDay } = getEntityDateRange(entity);
const {
start: collStart,
end: collEnd,
isAllDay: collAllDay
} = getCollectionDateRange(collection);
// If any dates are missing, don't match
if (!entityStart || !collStart) return false;
// If either side is all-day, use date comparison
if (entityAllDay || collAllDay) {
// Compare only date components
const entStartDate = entityStart.startOf('day');
const entEndDate = (entityEnd || entityStart).endOf('day');
const colStartDate = collStart.startOf('day');
const colEndDate = collEnd.endOf('day');
return entStartDate <= colEndDate && entEndDate >= colStartDate;
} else {
// Compare actual DateTimes
const entEnd = entityEnd || entityStart;
return entityStart <= collEnd && entEnd >= collStart;
}
}
export function isEntityOutsideCollectionDateRange(
entity: Visit | Transportation | Lodging | Note | Checklist,
collection: Collection
): boolean {
return !isEntityInCollectionDateRange(entity, collection);
}
export function getEntitiesInDateRange<
T extends Visit | Transportation | Lodging | Note | Checklist
>(entities: T[], collection: Collection): T[] {
return entities.filter((entity) => isEntityInCollectionDateRange(entity, collection));
}
export function getEntitiesOutsideDateRange<
T extends Visit | Transportation | Lodging | Note | Checklist
>(entities: T[], collection: Collection): T[] {
return entities.filter((entity) => isEntityOutsideCollectionDateRange(entity, collection));
}
export const VALID_TIMEZONES = [ export const VALID_TIMEZONES = [
'Africa/Abidjan', 'Africa/Abidjan',
'Africa/Accra', 'Africa/Accra',

View File

@@ -422,7 +422,9 @@
"recorded_sessions": "جلسات مسجلة", "recorded_sessions": "جلسات مسجلة",
"total_activities": "الأنشطة الكلية", "total_activities": "الأنشطة الكلية",
"total_climbed": "مجموع تسلق", "total_climbed": "مجموع تسلق",
"total_distance": "إجمالي المسافة" "total_distance": "إجمالي المسافة",
"dates_not_saved": "زيارة لم تتم إضافتها بعد",
"dates_not_saved_description": "انقر فوق إضافة زيارة إلى حفظ"
}, },
"auth": { "auth": {
"confirm_password": "تأكيد كلمة المرور", "confirm_password": "تأكيد كلمة المرور",

View File

@@ -423,7 +423,9 @@
"recorded_sessions": "Aufgenommene Sitzungen", "recorded_sessions": "Aufgenommene Sitzungen",
"total_activities": "Gesamtaktivitäten", "total_activities": "Gesamtaktivitäten",
"total_climbed": "Total bestiegen", "total_climbed": "Total bestiegen",
"total_distance": "Gesamtabstand" "total_distance": "Gesamtabstand",
"dates_not_saved": "Besuchen Sie noch nicht hinzugefügt",
"dates_not_saved_description": "Klicken Sie auf Besuchen Sie, um sie zu speichern"
}, },
"home": { "home": {
"desc_1": "Entdecken, planen und erkunden Sie mühelos", "desc_1": "Entdecken, planen und erkunden Sie mühelos",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -423,7 +423,9 @@
"recorded_sessions": "Sessions enregistrées", "recorded_sessions": "Sessions enregistrées",
"total_activities": "Activités totales", "total_activities": "Activités totales",
"total_climbed": "Total grimpé", "total_climbed": "Total grimpé",
"total_distance": "Distance totale" "total_distance": "Distance totale",
"dates_not_saved": "Visitez non encore ajouté",
"dates_not_saved_description": "Cliquez sur Ajouter une visite pour enregistrer"
}, },
"home": { "home": {
"desc_1": "Découvrez, planifiez et explorez en toute simplicité", "desc_1": "Découvrez, planifiez et explorez en toute simplicité",

View File

@@ -423,7 +423,9 @@
"recorded_sessions": "Sessioni registrate", "recorded_sessions": "Sessioni registrate",
"total_activities": "Attività totali", "total_activities": "Attività totali",
"total_climbed": "Totale scalato", "total_climbed": "Totale scalato",
"total_distance": "Distanza totale" "total_distance": "Distanza totale",
"dates_not_saved": "Visitare non ancora aggiunto",
"dates_not_saved_description": "Fai clic su Aggiungi visita per salvare"
}, },
"home": { "home": {
"desc_1": "Scopri, pianifica ed esplora con facilità", "desc_1": "Scopri, pianifica ed esplora con facilità",

View File

@@ -422,7 +422,9 @@
"recorded_sessions": "録音されたセッション", "recorded_sessions": "録音されたセッション",
"total_activities": "総アクティビティ", "total_activities": "総アクティビティ",
"total_climbed": "総登り", "total_climbed": "総登り",
"total_distance": "総距離" "total_distance": "総距離",
"dates_not_saved": "まだ追加されていません",
"dates_not_saved_description": "[訪問の追加]をクリックして保存します"
}, },
"auth": { "auth": {
"confirm_password": "パスワードを認証する", "confirm_password": "パスワードを認証する",

View File

@@ -423,7 +423,9 @@
"recorded_sessions": "기록 된 세션", "recorded_sessions": "기록 된 세션",
"total_activities": "총 활동", "total_activities": "총 활동",
"total_climbed": "총계가 올라 갔다", "total_climbed": "총계가 올라 갔다",
"total_distance": "총 거리" "total_distance": "총 거리",
"dates_not_saved": "아직 추가되지 않았습니다",
"dates_not_saved_description": "저장을 위해 방문 추가를 클릭하십시오"
}, },
"auth": { "auth": {
"confirm_password": "비밀번호 확인", "confirm_password": "비밀번호 확인",

View File

@@ -423,7 +423,9 @@
"recorded_sessions": "Opgenomen sessies", "recorded_sessions": "Opgenomen sessies",
"total_activities": "Totale activiteiten", "total_activities": "Totale activiteiten",
"total_climbed": "Totaal geklommen", "total_climbed": "Totaal geklommen",
"total_distance": "Totale afstand" "total_distance": "Totale afstand",
"dates_not_saved": "Bezoek nog niet toegevoegd",
"dates_not_saved_description": "Klik op Bezoek toevoegen om op te slaan"
}, },
"home": { "home": {
"desc_1": "Ontdek, plan en verken met gemak", "desc_1": "Ontdek, plan en verken met gemak",

File diff suppressed because it is too large Load Diff

View File

@@ -476,7 +476,9 @@
"recorded_sessions": "Nagrane sesje", "recorded_sessions": "Nagrane sesje",
"total_activities": "Całkowite działania", "total_activities": "Całkowite działania",
"total_climbed": "Całkowita wspinana", "total_climbed": "Całkowita wspinana",
"total_distance": "Całkowita odległość" "total_distance": "Całkowita odległość",
"dates_not_saved": "Wizyta jeszcze nie dodana",
"dates_not_saved_description": "Kliknij Dodaj wizytę, aby zapisać"
}, },
"worldtravel": { "worldtravel": {
"country_list": "Lista krajów", "country_list": "Lista krajów",

View File

@@ -422,7 +422,9 @@
"recorded_sessions": "Sessões gravadas", "recorded_sessions": "Sessões gravadas",
"total_activities": "Atividades totais", "total_activities": "Atividades totais",
"total_climbed": "Total escalou", "total_climbed": "Total escalou",
"total_distance": "Distância total" "total_distance": "Distância total",
"dates_not_saved": "Visite ainda não adicionado",
"dates_not_saved_description": "Clique em Adicionar visita para salvar"
}, },
"auth": { "auth": {
"confirm_password": "Confirme sua senha", "confirm_password": "Confirme sua senha",

File diff suppressed because it is too large Load Diff

1016
frontend/src/locales/sk.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -423,7 +423,9 @@
"recorded_sessions": "Inspelade sessioner", "recorded_sessions": "Inspelade sessioner",
"total_activities": "Totala aktiviteter", "total_activities": "Totala aktiviteter",
"total_climbed": "Total stigning", "total_climbed": "Total stigning",
"total_distance": "Totalt avstånd" "total_distance": "Totalt avstånd",
"dates_not_saved": "Besök ännu inte tillagd",
"dates_not_saved_description": "Klicka på Lägg till Besök för att spara"
}, },
"home": { "home": {
"desc_1": "Upptäck, planera och utforska med lätthet", "desc_1": "Upptäck, planera och utforska med lätthet",

View File

@@ -476,7 +476,9 @@
"recorded_sessions": "记录的会议", "recorded_sessions": "记录的会议",
"total_activities": "总活动", "total_activities": "总活动",
"total_climbed": "总攀登", "total_climbed": "总攀登",
"total_distance": "总距离" "total_distance": "总距离",
"dates_not_saved": "访问尚未添加",
"dates_not_saved_description": "点击添加访问以保存"
}, },
"auth": { "auth": {
"forgot_password": "忘记密码?", "forgot_password": "忘记密码?",

View File

@@ -20,6 +20,7 @@
register('ja', () => import('../locales/ja.json')); register('ja', () => import('../locales/ja.json'));
register('ar', () => import('../locales/ar.json')); register('ar', () => import('../locales/ar.json'));
register('pt-br', () => import('../locales/pt-br.json')); register('pt-br', () => import('../locales/pt-br.json'));
register('sk', () => import('../locales/sk.json'));
let locales = [ let locales = [
'en', 'en',
@@ -36,7 +37,8 @@
'ru', 'ru',
'ja', 'ja',
'ar', 'ar',
'pt-br' 'pt-br',
'sk'
]; ];
if (browser) { if (browser) {

View File

@@ -389,6 +389,21 @@
}); });
} }
$: filteredOrderedItems = orderedItems.filter((item) => {
if (!collection?.start_date || !collection?.end_date) {
return true; // If no date range is set, show all items
}
const collectionStart = new Date(collection.start_date);
const collectionEnd = new Date(collection.end_date);
const itemStart = new Date(item.start);
const itemEnd = new Date(item.end);
// Check if item overlaps with collection date range
// Item is included if it starts before collection ends AND ends after collection starts
return itemStart <= collectionEnd && itemEnd >= collectionStart;
});
$: { $: {
numAdventures = adventures.length; numAdventures = adventures.length;
numVisited = adventures.filter((adventure) => adventure.is_visited).length; numVisited = adventures.filter((adventure) => adventure.is_visited).length;
@@ -1176,11 +1191,11 @@
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div class="w-full max-w-4xl relative"> <div class="w-full max-w-4xl relative">
<!-- Vertical timeline line that spans the entire height --> <!-- Vertical timeline line that spans the entire height -->
{#if orderedItems.length > 0} {#if filteredOrderedItems.length > 0}
<div class="absolute left-8 top-0 bottom-0 w-1 bg-primary"></div> <div class="absolute left-8 top-0 bottom-0 w-1 bg-primary"></div>
{/if} {/if}
<ul class="relative"> <ul class="relative">
{#each orderedItems as orderedItem, index} {#each filteredOrderedItems as orderedItem, index}
<li class="relative pl-20 mb-8"> <li class="relative pl-20 mb-8">
<!-- Timeline Icon --> <!-- Timeline Icon -->
<div <div
@@ -1281,7 +1296,7 @@
</li> </li>
{/each} {/each}
</ul> </ul>
{#if orderedItems.length === 0} {#if filteredOrderedItems.length === 0}
<div class="alert alert-info"> <div class="alert alert-info">
<p class="text-center text-lg">{$t('adventures.no_ordered_items')}</p> <p class="text-center text-lg">{$t('adventures.no_ordered_items')}</p>
</div> </div>
@@ -1360,7 +1375,7 @@
</Marker> </Marker>
{/if} {/if}
{/each} {/each}
{#if lineData} {#if lineData && collection.start_date && collection.end_date}
<GeoJSON data={lineData}> <GeoJSON data={lineData}>
<LineLayer <LineLayer
layout={{ 'line-cap': 'round', 'line-join': 'round' }} layout={{ 'line-cap': 'round', 'line-join': 'round' }}