1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-02 19:55:18 +02:00

Add timezone support for visits, transportation, and lodging

- Introduced TIMEZONES constant in models.py to store valid timezone options.
- Updated Visit, Transportation, and Lodging models to include timezone fields.
- Modified serializers to include timezone fields in VisitSerializer, TransportationSerializer, and LodgingSerializer.
- Enhanced DateRangeCollapse component to handle timezone selection and formatting.
- Implemented timezone formatting functions in LodgingCard and TransportationCard components.
- Updated LodgingModal and TransportationModal to bind timezone data.
- Added VALID_TIMEZONES to dateUtils for consistent timezone management across the application.
This commit is contained in:
Sean Morley 2025-05-10 11:59:56 -04:00
parent b30d6df964
commit 1323d91e32
12 changed files with 1056 additions and 45 deletions

View file

@ -10,8 +10,8 @@
export let type: 'adventure' | 'transportation' | 'lodging' = 'adventure';
// Initialize with browser's timezone
let selectedStartTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
let selectedEndTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
export let selectedStartTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
export let selectedEndTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
let allDay: boolean = false;
@ -25,10 +25,19 @@
start_date: string;
end_date: string;
notes: string;
start_timezone?: string;
end_timezone?: string;
timezone: string | null;
};
export let visits: Visit[] | null = null;
type TransportationVisit = {
id: string;
start_date: string;
end_date: string;
notes: string;
start_timezone: string;
end_timezone: string;
};
export let visits: (Visit | TransportationVisit)[] | null = null;
// Local display values
let localStartDate: string = '';
@ -52,6 +61,13 @@
utcDate: utcEndDate,
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
}).localDate;
if (!selectedStartTimezone) {
selectedStartTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
}
if (!selectedEndTimezone) {
selectedEndTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
}
});
// Set the full date range for constraining purposes
@ -60,6 +76,22 @@
fullEndDate = `${collection.end_date}T23:59`;
}
function formatDateInTimezone(utcDate: string, timezone: string): string {
try {
return new Intl.DateTimeFormat(undefined, {
timeZone: timezone,
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
}).format(new Date(utcDate));
} catch {
return new Date(utcDate).toLocaleString(); // fallback
}
}
// Get constraint dates in the right format based on allDay setting
$: constraintStartDate = allDay
? fullStartDate && fullStartDate.includes('T')
@ -108,22 +140,27 @@
}).utcDate;
}
// Create a visit object with appropriate timezone information
function createVisitObject() {
const newVisit: Visit = {
id: crypto.randomUUID(),
start_date: utcStartDate ?? '',
end_date: utcEndDate ?? utcStartDate ?? '',
notes: note ?? ''
};
// For transportation, add timezone information
function createVisitObject(): Visit | TransportationVisit {
if (type === 'transportation') {
newVisit.start_timezone = selectedStartTimezone;
newVisit.end_timezone = selectedEndTimezone;
const transportVisit: TransportationVisit = {
id: crypto.randomUUID(),
start_date: utcStartDate ?? '',
end_date: utcEndDate ?? utcStartDate ?? '',
notes: note ?? '',
start_timezone: selectedStartTimezone,
end_timezone: selectedEndTimezone
};
return transportVisit;
} else {
const regularVisit: Visit = {
id: crypto.randomUUID(),
start_date: utcStartDate ?? '',
end_date: utcEndDate ?? utcStartDate ?? '',
notes: note ?? '',
timezone: selectedStartTimezone
};
return regularVisit;
}
return newVisit;
}
</script>
@ -369,15 +406,31 @@
{#if isAllDay(visit.start_date)}
<span class="badge badge-outline mr-2">{$t('adventures.all_day')}</span>
{visit.start_date.split('T')[0]} {visit.end_date.split('T')[0]}
{:else if 'start_timezone' in visit}
{formatDateInTimezone(visit.start_date, visit.start_timezone)} {formatDateInTimezone(
visit.end_date,
visit.end_timezone
)}
{:else if visit.timezone}
{formatDateInTimezone(visit.start_date, visit.timezone)} {formatDateInTimezone(
visit.end_date,
visit.timezone
)}
{:else}
{new Date(visit.start_date).toLocaleString()} {new Date(
visit.end_date
).toLocaleString()}
<!-- showe timezones badge -->
{/if}
{#if 'timezone' in visit && visit.timezone}
<span class="badge badge-outline ml-2">{visit.timezone}</span>
{/if}
</p>
<!-- -->
<!-- Display timezone information for transportation visits -->
{#if visit.start_timezone && visit.end_timezone && visit.start_timezone !== visit.end_timezone}
{#if 'start_timezone' in visit && 'end_timezone' in visit && visit.start_timezone !== visit.end_timezone}
<p class="text-xs text-base-content">
{visit.start_timezone}{visit.end_timezone}
</p>
@ -399,8 +452,14 @@
allDay = isAllDayEvent;
// Set timezone information if available
if (visit.start_timezone) selectedStartTimezone = visit.start_timezone;
if (visit.end_timezone) selectedEndTimezone = visit.end_timezone;
if ('start_timezone' in visit) {
// TransportationVisit
selectedStartTimezone = visit.start_timezone;
selectedEndTimezone = visit.end_timezone;
} else if (visit.timezone) {
// Visit
selectedStartTimezone = visit.timezone;
}
if (isAllDayEvent) {
localStartDate = visit.start_date.split('T')[0];
@ -414,7 +473,8 @@
localEndDate = updateLocalDate({
utcDate: visit.end_date,
timezone: visit.end_timezone || selectedStartTimezone
timezone:
'end_timezone' in visit ? visit.end_timezone : selectedStartTimezone
}).localDate;
}

View file

@ -18,6 +18,23 @@
}
}
function formatDateInTimezone(utcDate: string, timezone?: string): string {
if (!utcDate) return '';
try {
return new Intl.DateTimeFormat(undefined, {
timeZone: timezone,
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
}).format(new Date(utcDate));
} catch {
return new Date(utcDate).toLocaleString();
}
}
export let lodging: Lodging;
export let user: User | null = null;
export let collection: Collection | null = null;
@ -119,21 +136,9 @@
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.dates')}:</span>
<p>
{new Date(lodging.check_in).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric'
})}
-
{new Date(lodging.check_out).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric'
})}
{formatDateInTimezone(lodging.check_in ?? '', lodging.timezone ?? undefined)}
{formatDateInTimezone(lodging.check_out ?? '', lodging.timezone ?? undefined)}
<span class="text-xs opacity-60 ml-1">({lodging.timezone})</span>
</p>
</div>
{/if}

View file

@ -56,7 +56,8 @@
is_public: lodgingToEdit?.is_public || false,
collection: lodgingToEdit?.collection || collection.id,
created_at: lodgingToEdit?.created_at || '',
updated_at: lodgingToEdit?.updated_at || ''
updated_at: lodgingToEdit?.updated_at || '',
timezone: lodgingToEdit?.timezone || ''
};
}
@ -303,6 +304,7 @@
type="lodging"
bind:utcStartDate={lodging.check_in}
bind:utcEndDate={lodging.check_out}
bind:selectedStartTimezone={lodging.timezone}
/>
<!-- Location Information -->

View file

@ -18,6 +18,23 @@
}
const dispatch = createEventDispatcher();
function formatDateInTimezone(utcDate: string, timezone?: string): string {
if (!utcDate) return '';
try {
return new Intl.DateTimeFormat(undefined, {
timeZone: timezone,
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
}).format(new Date(utcDate));
} catch {
return new Date(utcDate).toLocaleString();
}
}
export let transportation: Transportation;
export let user: User | null = null;
export let collection: Collection | null = null;
@ -136,7 +153,12 @@
{#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()}</p>
<p>
{formatDateInTimezone(transportation.date, transportation.start_timezone ?? undefined)}
{#if transportation.start_timezone}
<span class="text-xs opacity-60 ml-1">({transportation.start_timezone})</span>
{/if}
</p>
</div>
{/if}
</div>
@ -154,7 +176,15 @@
{#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()}</p>
<p>
{formatDateInTimezone(
transportation.end_date,
transportation.end_timezone || undefined
)}
{#if transportation.end_timezone}
<span class="text-xs opacity-60 ml-1">({transportation.end_timezone})</span>
{/if}
</p>
</div>
{/if}
</div>

View file

@ -36,7 +36,9 @@
origin_latitude: transportationToEdit?.origin_latitude || NaN,
origin_longitude: transportationToEdit?.origin_longitude || NaN,
destination_latitude: transportationToEdit?.destination_latitude || NaN,
destination_longitude: transportationToEdit?.destination_longitude || NaN
destination_longitude: transportationToEdit?.destination_longitude || NaN,
start_timezone: transportationToEdit?.start_timezone || '',
end_timezone: transportationToEdit?.end_timezone || ''
};
let starting_airport: string = '';
@ -343,6 +345,9 @@
type="transportation"
bind:utcStartDate={transportation.date}
bind:utcEndDate={transportation.end_date}
bind:selectedStartTimezone={transportation.start_timezone}
bind:selectedEndTimezone={transportation.end_timezone}
{collection}
/>
<!-- Flight Information -->

View file

@ -120,3 +120,423 @@ export function formatUTCDate(utcDate: string | null): string {
if (!utcDate) return '';
return DateTime.fromISO(utcDate).toISO().slice(0, 16).replace('T', ' ');
}
export const VALID_TIMEZONES = [
'Africa/Abidjan',
'Africa/Accra',
'Africa/Addis_Ababa',
'Africa/Algiers',
'Africa/Asmera',
'Africa/Bamako',
'Africa/Bangui',
'Africa/Banjul',
'Africa/Bissau',
'Africa/Blantyre',
'Africa/Brazzaville',
'Africa/Bujumbura',
'Africa/Cairo',
'Africa/Casablanca',
'Africa/Ceuta',
'Africa/Conakry',
'Africa/Dakar',
'Africa/Dar_es_Salaam',
'Africa/Djibouti',
'Africa/Douala',
'Africa/El_Aaiun',
'Africa/Freetown',
'Africa/Gaborone',
'Africa/Harare',
'Africa/Johannesburg',
'Africa/Juba',
'Africa/Kampala',
'Africa/Khartoum',
'Africa/Kigali',
'Africa/Kinshasa',
'Africa/Lagos',
'Africa/Libreville',
'Africa/Lome',
'Africa/Luanda',
'Africa/Lubumbashi',
'Africa/Lusaka',
'Africa/Malabo',
'Africa/Maputo',
'Africa/Maseru',
'Africa/Mbabane',
'Africa/Mogadishu',
'Africa/Monrovia',
'Africa/Nairobi',
'Africa/Ndjamena',
'Africa/Niamey',
'Africa/Nouakchott',
'Africa/Ouagadougou',
'Africa/Porto-Novo',
'Africa/Sao_Tome',
'Africa/Tripoli',
'Africa/Tunis',
'Africa/Windhoek',
'America/Adak',
'America/Anchorage',
'America/Anguilla',
'America/Antigua',
'America/Araguaina',
'America/Argentina/La_Rioja',
'America/Argentina/Rio_Gallegos',
'America/Argentina/Salta',
'America/Argentina/San_Juan',
'America/Argentina/San_Luis',
'America/Argentina/Tucuman',
'America/Argentina/Ushuaia',
'America/Aruba',
'America/Asuncion',
'America/Bahia',
'America/Bahia_Banderas',
'America/Barbados',
'America/Belem',
'America/Belize',
'America/Blanc-Sablon',
'America/Boa_Vista',
'America/Bogota',
'America/Boise',
'America/Buenos_Aires',
'America/Cambridge_Bay',
'America/Campo_Grande',
'America/Cancun',
'America/Caracas',
'America/Catamarca',
'America/Cayenne',
'America/Cayman',
'America/Chicago',
'America/Chihuahua',
'America/Ciudad_Juarez',
'America/Coral_Harbour',
'America/Cordoba',
'America/Costa_Rica',
'America/Creston',
'America/Cuiaba',
'America/Curacao',
'America/Danmarkshavn',
'America/Dawson',
'America/Dawson_Creek',
'America/Denver',
'America/Detroit',
'America/Dominica',
'America/Edmonton',
'America/Eirunepe',
'America/El_Salvador',
'America/Fort_Nelson',
'America/Fortaleza',
'America/Glace_Bay',
'America/Godthab',
'America/Goose_Bay',
'America/Grand_Turk',
'America/Grenada',
'America/Guadeloupe',
'America/Guatemala',
'America/Guayaquil',
'America/Guyana',
'America/Halifax',
'America/Havana',
'America/Hermosillo',
'America/Indiana/Knox',
'America/Indiana/Marengo',
'America/Indiana/Petersburg',
'America/Indiana/Tell_City',
'America/Indiana/Vevay',
'America/Indiana/Vincennes',
'America/Indiana/Winamac',
'America/Indianapolis',
'America/Inuvik',
'America/Iqaluit',
'America/Jamaica',
'America/Jujuy',
'America/Juneau',
'America/Kentucky/Monticello',
'America/Kralendijk',
'America/La_Paz',
'America/Lima',
'America/Los_Angeles',
'America/Louisville',
'America/Lower_Princes',
'America/Maceio',
'America/Managua',
'America/Manaus',
'America/Marigot',
'America/Martinique',
'America/Matamoros',
'America/Mazatlan',
'America/Mendoza',
'America/Menominee',
'America/Merida',
'America/Metlakatla',
'America/Mexico_City',
'America/Miquelon',
'America/Moncton',
'America/Monterrey',
'America/Montevideo',
'America/Montserrat',
'America/Nassau',
'America/New_York',
'America/Nome',
'America/Noronha',
'America/North_Dakota/Beulah',
'America/North_Dakota/Center',
'America/North_Dakota/New_Salem',
'America/Ojinaga',
'America/Panama',
'America/Paramaribo',
'America/Phoenix',
'America/Port-au-Prince',
'America/Port_of_Spain',
'America/Porto_Velho',
'America/Puerto_Rico',
'America/Punta_Arenas',
'America/Rankin_Inlet',
'America/Recife',
'America/Regina',
'America/Resolute',
'America/Rio_Branco',
'America/Santarem',
'America/Santiago',
'America/Santo_Domingo',
'America/Sao_Paulo',
'America/Scoresbysund',
'America/Sitka',
'America/St_Barthelemy',
'America/St_Johns',
'America/St_Kitts',
'America/St_Lucia',
'America/St_Thomas',
'America/St_Vincent',
'America/Swift_Current',
'America/Tegucigalpa',
'America/Thule',
'America/Tijuana',
'America/Toronto',
'America/Tortola',
'America/Vancouver',
'America/Whitehorse',
'America/Winnipeg',
'America/Yakutat',
'Antarctica/Casey',
'Antarctica/Davis',
'Antarctica/DumontDUrville',
'Antarctica/Macquarie',
'Antarctica/Mawson',
'Antarctica/McMurdo',
'Antarctica/Palmer',
'Antarctica/Rothera',
'Antarctica/Syowa',
'Antarctica/Troll',
'Antarctica/Vostok',
'Arctic/Longyearbyen',
'Asia/Aden',
'Asia/Almaty',
'Asia/Amman',
'Asia/Anadyr',
'Asia/Aqtau',
'Asia/Aqtobe',
'Asia/Ashgabat',
'Asia/Atyrau',
'Asia/Baghdad',
'Asia/Bahrain',
'Asia/Baku',
'Asia/Bangkok',
'Asia/Barnaul',
'Asia/Beirut',
'Asia/Bishkek',
'Asia/Brunei',
'Asia/Calcutta',
'Asia/Chita',
'Asia/Colombo',
'Asia/Damascus',
'Asia/Dhaka',
'Asia/Dili',
'Asia/Dubai',
'Asia/Dushanbe',
'Asia/Famagusta',
'Asia/Gaza',
'Asia/Hebron',
'Asia/Hong_Kong',
'Asia/Hovd',
'Asia/Irkutsk',
'Asia/Jakarta',
'Asia/Jayapura',
'Asia/Jerusalem',
'Asia/Kabul',
'Asia/Kamchatka',
'Asia/Karachi',
'Asia/Katmandu',
'Asia/Khandyga',
'Asia/Krasnoyarsk',
'Asia/Kuala_Lumpur',
'Asia/Kuching',
'Asia/Kuwait',
'Asia/Macau',
'Asia/Magadan',
'Asia/Makassar',
'Asia/Manila',
'Asia/Muscat',
'Asia/Nicosia',
'Asia/Novokuznetsk',
'Asia/Novosibirsk',
'Asia/Omsk',
'Asia/Oral',
'Asia/Phnom_Penh',
'Asia/Pontianak',
'Asia/Pyongyang',
'Asia/Qatar',
'Asia/Qostanay',
'Asia/Qyzylorda',
'Asia/Rangoon',
'Asia/Riyadh',
'Asia/Saigon',
'Asia/Sakhalin',
'Asia/Samarkand',
'Asia/Seoul',
'Asia/Shanghai',
'Asia/Singapore',
'Asia/Srednekolymsk',
'Asia/Taipei',
'Asia/Tashkent',
'Asia/Tbilisi',
'Asia/Tehran',
'Asia/Thimphu',
'Asia/Tokyo',
'Asia/Tomsk',
'Asia/Ulaanbaatar',
'Asia/Urumqi',
'Asia/Ust-Nera',
'Asia/Vientiane',
'Asia/Vladivostok',
'Asia/Yakutsk',
'Asia/Yekaterinburg',
'Asia/Yerevan',
'Atlantic/Azores',
'Atlantic/Bermuda',
'Atlantic/Canary',
'Atlantic/Cape_Verde',
'Atlantic/Faeroe',
'Atlantic/Madeira',
'Atlantic/Reykjavik',
'Atlantic/South_Georgia',
'Atlantic/St_Helena',
'Atlantic/Stanley',
'Australia/Adelaide',
'Australia/Brisbane',
'Australia/Broken_Hill',
'Australia/Darwin',
'Australia/Eucla',
'Australia/Hobart',
'Australia/Lindeman',
'Australia/Lord_Howe',
'Australia/Melbourne',
'Australia/Perth',
'Australia/Sydney',
'Europe/Amsterdam',
'Europe/Andorra',
'Europe/Astrakhan',
'Europe/Athens',
'Europe/Belgrade',
'Europe/Berlin',
'Europe/Bratislava',
'Europe/Brussels',
'Europe/Bucharest',
'Europe/Budapest',
'Europe/Busingen',
'Europe/Chisinau',
'Europe/Copenhagen',
'Europe/Dublin',
'Europe/Gibraltar',
'Europe/Guernsey',
'Europe/Helsinki',
'Europe/Isle_of_Man',
'Europe/Istanbul',
'Europe/Jersey',
'Europe/Kaliningrad',
'Europe/Kiev',
'Europe/Kirov',
'Europe/Lisbon',
'Europe/Ljubljana',
'Europe/London',
'Europe/Luxembourg',
'Europe/Madrid',
'Europe/Malta',
'Europe/Mariehamn',
'Europe/Minsk',
'Europe/Monaco',
'Europe/Moscow',
'Europe/Oslo',
'Europe/Paris',
'Europe/Podgorica',
'Europe/Prague',
'Europe/Riga',
'Europe/Rome',
'Europe/Samara',
'Europe/San_Marino',
'Europe/Sarajevo',
'Europe/Saratov',
'Europe/Simferopol',
'Europe/Skopje',
'Europe/Sofia',
'Europe/Stockholm',
'Europe/Tallinn',
'Europe/Tirane',
'Europe/Ulyanovsk',
'Europe/Vaduz',
'Europe/Vatican',
'Europe/Vienna',
'Europe/Vilnius',
'Europe/Volgograd',
'Europe/Warsaw',
'Europe/Zagreb',
'Europe/Zurich',
'Indian/Antananarivo',
'Indian/Chagos',
'Indian/Christmas',
'Indian/Cocos',
'Indian/Comoro',
'Indian/Kerguelen',
'Indian/Mahe',
'Indian/Maldives',
'Indian/Mauritius',
'Indian/Mayotte',
'Indian/Reunion',
'Pacific/Apia',
'Pacific/Auckland',
'Pacific/Bougainville',
'Pacific/Chatham',
'Pacific/Easter',
'Pacific/Efate',
'Pacific/Enderbury',
'Pacific/Fakaofo',
'Pacific/Fiji',
'Pacific/Funafuti',
'Pacific/Galapagos',
'Pacific/Gambier',
'Pacific/Guadalcanal',
'Pacific/Guam',
'Pacific/Honolulu',
'Pacific/Kiritimati',
'Pacific/Kosrae',
'Pacific/Kwajalein',
'Pacific/Majuro',
'Pacific/Marquesas',
'Pacific/Midway',
'Pacific/Nauru',
'Pacific/Niue',
'Pacific/Norfolk',
'Pacific/Noumea',
'Pacific/Pago_Pago',
'Pacific/Palau',
'Pacific/Pitcairn',
'Pacific/Ponape',
'Pacific/Port_Moresby',
'Pacific/Rarotonga',
'Pacific/Saipan',
'Pacific/Tahiti',
'Pacific/Tarawa',
'Pacific/Tongatapu',
'Pacific/Truk',
'Pacific/Wake',
'Pacific/Wallis'
];

View file

@ -1,3 +1,5 @@
import { VALID_TIMEZONES } from './dateUtils';
export type User = {
pk: number;
username: string;
@ -30,6 +32,7 @@ export type Adventure = {
id: string;
start_date: string;
end_date: string;
timezone: string | null;
notes: string;
}[];
collection?: string | null;
@ -160,6 +163,8 @@ export type Transportation = {
link: string | null;
date: string | null; // ISO 8601 date string
end_date: string | null; // ISO 8601 date string
start_timezone: string | null;
end_timezone: string | null;
flight_number: string | null;
from_location: string | null;
to_location: string | null;
@ -288,6 +293,7 @@ export type Lodging = {
link: string | null;
check_in: string | null; // ISO 8601 date string
check_out: string | null; // ISO 8601 date string
timezone: string | null;
reservation_number: string | null;
price: number | null;
latitude: number | null;