Merge branch 'development' into main

This commit is contained in:
Sean Morley
2025-03-21 17:34:03 -04:00
committed by GitHub
31 changed files with 939 additions and 270 deletions

View File

@@ -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 isnt provided, assume its 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"

View File

@@ -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')}

View File

@@ -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',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) {