1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-22 14:29:36 +02:00

feat: Update Visit model to use DateTimeField for start and end dates, and enhance AdventureModal with datetime-local inputs

This commit is contained in:
Sean Morley 2025-03-18 14:04:31 -04:00
parent 6f720a154f
commit 9fd2a142cb
6 changed files with 152 additions and 35 deletions

View file

@ -0,0 +1,23 @@
# Generated by Django 5.0.8 on 2025-03-17 21:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0024_alter_attachment_file'),
]
operations = [
migrations.AlterField(
model_name='visit',
name='end_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='visit',
name='start_date',
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -76,8 +76,8 @@ User = get_user_model()
class Visit(models.Model): class Visit(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits') adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits')
start_date = models.DateField(null=True, blank=True) start_date = models.DateTimeField(null=True, blank=True)
end_date = models.DateField(null=True, blank=True) end_date = models.DateTimeField(null=True, blank=True)
notes = models.TextField(blank=True, null=True) notes = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)

View file

@ -136,9 +136,11 @@ class AdventureSerializer(CustomModelSerializer):
def get_is_visited(self, obj): def get_is_visited(self, obj):
current_date = timezone.now().date() current_date = timezone.now().date()
for visit in obj.visits.all(): for visit in obj.visits.all():
if visit.start_date and visit.end_date and (visit.start_date <= current_date): start_date = visit.start_date.date() if isinstance(visit.start_date, timezone.datetime) else visit.start_date
end_date = visit.end_date.date() if isinstance(visit.end_date, timezone.datetime) else visit.end_date
if start_date and end_date and (start_date <= current_date):
return True return True
elif visit.start_date and not visit.end_date and (visit.start_date <= current_date): elif start_date and not end_date and (start_date <= current_date):
return True return True
return False return False

View file

@ -6,6 +6,16 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let collection: Collection | null = null; export let collection: Collection | null = null;
let fullStartDate: string = '';
let fullEndDate: string = '';
let allDay: boolean = false;
// 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`;
}
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let images: { id: string; image: string; is_primary: boolean }[] = []; let images: { id: string; image: string; is_primary: boolean }[] = [];
@ -72,7 +82,7 @@
import ActivityComplete from './ActivityComplete.svelte'; import ActivityComplete from './ActivityComplete.svelte';
import CategoryDropdown from './CategoryDropdown.svelte'; import CategoryDropdown from './CategoryDropdown.svelte';
import { findFirstValue } from '$lib'; import { findFirstValue, isAllDay } from '$lib';
import MarkdownEditor from './MarkdownEditor.svelte'; import MarkdownEditor from './MarkdownEditor.svelte';
import ImmichSelect from './ImmichSelect.svelte'; import ImmichSelect from './ImmichSelect.svelte';
import Star from '~icons/mdi/star'; import Star from '~icons/mdi/star';
@ -379,7 +389,10 @@
let new_start_date: string = ''; let new_start_date: string = '';
let new_end_date: string = ''; let new_end_date: string = '';
let new_notes: string = ''; let new_notes: string = '';
// Function to add a new visit.
function addNewVisit() { function addNewVisit() {
// If an end date isnt provided, assume its the same as start.
if (new_start_date && !new_end_date) { if (new_start_date && !new_end_date) {
new_end_date = new_start_date; new_end_date = new_start_date;
} }
@ -391,15 +404,31 @@
addToast('error', $t('adventures.no_start_date')); addToast('error', $t('adventures.no_start_date'));
return; 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 = [
...adventure.visits, ...adventure.visits,
{ {
start_date: new_start_date, start_date: new_start_date,
end_date: new_end_date, end_date: new_end_date,
notes: new_notes, notes: new_notes,
id: '' id: '' // or generate an id as needed
} }
]; ];
// Clear the input fields.
new_start_date = ''; new_start_date = '';
new_end_date = ''; new_end_date = '';
new_notes = ''; new_notes = '';
@ -669,13 +698,23 @@
on:change={() => (constrainDates = !constrainDates)} on:change={() => (constrainDates = !constrainDates)}
/> />
{/if} {/if}
<span class="label-text">All Day</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (allDay = !allDay)}
/>
</label> </label>
<div class="flex gap-2 mb-1"> <div class="flex gap-2 mb-1">
{#if !constrainDates} {#if !allDay}
<input <input
type="date" type="datetime-local"
class="input input-bordered w-full" 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} bind:value={new_start_date}
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
@ -685,10 +724,12 @@
}} }}
/> />
<input <input
type="date" type="datetime-local"
class="input input-bordered w-full" class="input input-bordered w-full"
placeholder={$t('adventures.end_date')} placeholder={$t('adventures.end_date')}
bind:value={new_end_date} bind:value={new_end_date}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
@ -701,8 +742,8 @@
type="date" type="date"
class="input input-bordered w-full" class="input input-bordered w-full"
placeholder={$t('adventures.start_date')} placeholder={$t('adventures.start_date')}
min={collection?.start_date} min={constrainDates ? fullStartDate : ''}
max={collection?.end_date} max={constrainDates ? fullEndDate : ''}
bind:value={new_start_date} bind:value={new_start_date}
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
@ -716,8 +757,8 @@
class="input input-bordered w-full" class="input input-bordered w-full"
placeholder={$t('adventures.end_date')} placeholder={$t('adventures.end_date')}
bind:value={new_end_date} bind:value={new_end_date}
min={collection?.start_date} min={constrainDates ? fullStartDate : ''}
max={collection?.end_date} max={constrainDates ? fullEndDate : ''}
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
@ -742,6 +783,30 @@
></textarea> ></textarea>
</div> </div>
<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>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="button" class="btn btn-neutral" on:click={addNewVisit} <button type="button" class="btn btn-neutral" on:click={addNewVisit}
>{$t('adventures.add')}</button >{$t('adventures.add')}</button
@ -749,23 +814,33 @@
</div> </div>
{#if adventure.visits.length > 0} {#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} {#each adventure.visits as visit}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2 items-center">
<p> <p>
{new Date(visit.start_date).toLocaleDateString(undefined, { {#if isAllDay(visit.start_date)}
timeZone: 'UTC' <!-- 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> </p>
{#if visit.end_date && visit.end_date !== visit.start_date} {#if visit.end_date && visit.end_date !== visit.start_date}
<p> <p>
{new Date(visit.end_date).toLocaleDateString(undefined, { {new Date(visit.end_date).toLocaleDateString(undefined, {
timeZone: 'UTC' timeZone: 'UTC'
})} })}
{#if !isAllDay(visit.end_date)}
({new Date(visit.end_date).toLocaleTimeString()})
{/if}
</p> </p>
{/if} {/if}
<div> <div>
<button <button
type="button" type="button"

View file

@ -338,6 +338,12 @@ export let LODGING_TYPES_ICONS = {
other: '❓' other: '❓'
}; };
// Helper to check if a given date string represents midnight (all-day)
export function isAllDay(dateStr: string | string[]) {
// Checks for the pattern "T00:00:00.000Z"
return dateStr.includes('T00:00:00Z') || dateStr.includes('T00:00:00.000Z');
}
export function getAdventureTypeLabel(type: string) { export function getAdventureTypeLabel(type: string) {
// return the emoji ADVENTURE_TYPE_ICONS label for the given type if not found return ? emoji // return the emoji ADVENTURE_TYPE_ICONS label for the given type if not found return ? emoji
if (type in ADVENTURE_TYPE_ICONS) { if (type in ADVENTURE_TYPE_ICONS) {

View file

@ -91,6 +91,7 @@
import AdventureModal from '$lib/components/AdventureModal.svelte'; import AdventureModal from '$lib/components/AdventureModal.svelte';
import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte'; import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte';
import AttachmentCard from '$lib/components/AttachmentCard.svelte'; import AttachmentCard from '$lib/components/AttachmentCard.svelte';
import { isAllDay } from '$lib';
onMount(async () => { onMount(async () => {
if (data.props.adventure) { if (data.props.adventure) {
@ -410,23 +411,33 @@
</p> </p>
<!-- show each visit start and end date as well as notes --> <!-- show each visit start and end date as well as notes -->
{#each adventure.visits as visit} {#each adventure.visits as visit}
<div class="grid gap-2"> <div class="flex flex-col gap-2">
<p class="text-sm text-muted-foreground"> <div class="flex gap-2 items-center">
{visit.start_date <p>
? new Date(visit.start_date).toLocaleDateString(undefined, { {#if isAllDay(visit.start_date)}
<!-- For all-day events, show just the date -->
{new Date(visit.start_date).toLocaleDateString(undefined, {
timeZone: 'UTC' timeZone: 'UTC'
}) })}
: ''} {:else}
{visit.end_date && <!-- For timed events, show date and time -->
visit.end_date !== '' && {new Date(visit.start_date).toLocaleDateString()} ({new Date(
visit.end_date !== visit.start_date visit.start_date
? ' - ' + ).toLocaleTimeString()})
new Date(visit.end_date).toLocaleDateString(undefined, { {/if}
</p>
{#if visit.end_date && visit.end_date !== visit.start_date}
<p>
- {new Date(visit.end_date).toLocaleDateString(undefined, {
timeZone: 'UTC' timeZone: 'UTC'
}) })}
: ''} {#if !isAllDay(visit.end_date)}
</p> ({new Date(visit.end_date).toLocaleTimeString()})
<p class="text-sm text-muted-foreground -mt-2 mb-2">{visit.notes}</p> {/if}
</p>
{/if}
</div>
<p class="whitespace-pre-wrap -mt-2 mb-2">{visit.notes}</p>
</div> </div>
{/each} {/each}
</div> </div>