Fixes time zone based stuff

This commit is contained in:
2026-05-18 20:59:55 -05:00
parent aae0c3ff8d
commit 1772081495
7 changed files with 38 additions and 47 deletions

View File

@@ -29,7 +29,9 @@
const seed = untrack(() => {
if (initialValue) {
const d = new Date(initialValue)
// Append Z if no timezone indicator is present so it's always parsed as UTC
const normalized = /Z$|[+-]\d{2}:?\d{2}$/.test(initialValue) ? initialValue : initialValue + 'Z'
const d = new Date(normalized)
return { month: d.getMonth(), day: d.getDate(), year: d.getFullYear(), timeIndex: d.getHours() * 4 + Math.round(d.getMinutes() / 15) }
}
const today = new Date()
@@ -48,10 +50,9 @@
})
$effect(() => {
const pad = (n: number) => n.toString().padStart(2, '0')
const hour = Math.floor(timeIndex / 4)
const minute = (timeIndex % 4) * 15
value = new Date(`${year}-${pad(month + 1)}-${pad(day)}T${pad(hour)}:${pad(minute)}`).toISOString()
value = new Date(year, month, day, hour, minute).toISOString()
})
</script>

23
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,23 @@
import type { HuntResponse } from './api/types'
// Ensures API datetime strings are always parsed as UTC, even if the Z suffix is missing.
export function parseUTC(iso: string): Date {
const normalized = /Z$|[+-]\d{2}:?\d{2}$/.test(iso) ? iso : iso + 'Z'
return new Date(normalized)
}
export function huntStatus(hunt: HuntResponse): 'ONGOING' | 'UNSTARTED' | 'CLOSED' {
if (hunt.isTerminated) return 'CLOSED'
const now = Date.now()
if (now < parseUTC(hunt.startDateTime).getTime()) return 'UNSTARTED'
if (now > parseUTC(hunt.endDateTime).getTime()) return 'CLOSED'
return 'ONGOING'
}
export function formatDateTime(iso: string): string {
return parseUTC(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
}
export function formatDate(iso: string): string {
return parseUTC(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
}

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import {push} from 'svelte-spa-router'
import {auth} from '../../lib/stores/auth.svelte'
import {huntStatus, formatDate} from '../../lib/utils'
import {apiGetAllHunts} from '../../lib/api/index'
import type {HuntResponse} from '../../lib/api/types'
import StatusBadge from '../../lib/components/StatusBadge.svelte'
@@ -22,17 +23,6 @@
.finally(() => { loading = false })
})
function huntStatus(h: HuntResponse): 'ONGOING' | 'UNSTARTED' | 'CLOSED' {
if (h.isTerminated) return 'CLOSED'
const now = Date.now()
if (now < new Date(h.startDateTime).getTime()) return 'UNSTARTED'
if (now > new Date(h.endDateTime).getTime()) return 'CLOSED'
return 'ONGOING'
}
function formatDate(dt: string) {
return new Date(dt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
}
</script>
<div class="p-4 pb-6">

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import {push} from 'svelte-spa-router'
import {auth} from '../../lib/stores/auth.svelte'
import {huntStatus} from '../../lib/utils'
import {
apiAddItem,
apiCreateTeam,
@@ -151,13 +152,6 @@
}
}
function huntStatus(h: HuntResponse): 'ONGOING' | 'UNSTARTED' | 'CLOSED' {
if (h.isTerminated) return 'CLOSED'
const now = Date.now()
if (now < new Date(h.startDateTime).getTime()) return 'UNSTARTED'
if (now > new Date(h.endDateTime).getTime()) return 'CLOSED'
return 'ONGOING'
}
</script>
<div class="p-4 pb-6">

View File

@@ -2,6 +2,7 @@
import {push} from 'svelte-spa-router'
import {auth} from '../../lib/stores/auth.svelte'
import {apiGetHunt, apiGetItemPhotos, apiGetItems, apiListTeams, apiReviewPhoto} from '../../lib/api/index'
import {parseUTC} from '../../lib/utils'
import type {HuntResponse, ItemResponse, PhotoResponse, TeamResponse} from '../../lib/api/types'
import StatusBadge from '../../lib/components/StatusBadge.svelte'
import AuthImage from '../../lib/components/AuthImage.svelte'
@@ -83,7 +84,7 @@
<div class="flex items-center justify-between px-4 py-3 shrink-0">
<div>
<p class="font-semibold text-white">{photo.hunterName} <span class="text-white/50 font-normal">· {photo.teamName}</span></p>
<p class="text-xs text-white/40">{new Date(photo.photoUploadDateTime).toLocaleString()}</p>
<p class="text-xs text-white/40">{parseUTC(photo.photoUploadDateTime).toLocaleString()}</p>
</div>
<div class="flex items-center gap-3">
<StatusBadge status={photo.photoStatus} />
@@ -181,7 +182,7 @@
<div>
<p class="font-semibold">{photo.hunterName} <span class="text-base-content/50 font-normal">· {photo.teamName}</span></p>
<p class="text-xs text-base-content/50">
{new Date(photo.photoUploadDateTime).toLocaleString()}
{parseUTC(photo.photoUploadDateTime).toLocaleString()}
</p>
</div>
<StatusBadge status={photo.photoStatus} />

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import {push} from 'svelte-spa-router'
import {auth} from '../../lib/stores/auth.svelte'
import {huntStatus, formatDateTime} from '../../lib/utils'
import {
apiCreateTeam,
apiGetHunterTeam,
@@ -127,19 +128,6 @@
// ── Helpers ──────────────────────────────────────────────────────────────────
function huntStatus(hunt: HuntResponse): 'ONGOING' | 'UNSTARTED' | 'CLOSED' {
if (hunt.isTerminated) return 'CLOSED'
const now = Date.now()
const start = new Date(hunt.startDateTime).getTime()
const end = new Date(hunt.endDateTime).getTime()
if (now < start) return 'UNSTARTED'
if (now > end) return 'CLOSED'
return 'ONGOING'
}
function formatDate(dt: string) {
return new Date(dt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
}
</script>
<div class="p-4 pb-20">
@@ -184,7 +172,7 @@
<StatusBadge status={huntStatus(hunt)} />
</div>
<p class="text-xs text-base-content/50">
{formatDate(hunt.startDateTime)} {formatDate(hunt.endDateTime)}
{formatDateTime(hunt.startDateTime)} {formatDateTime(hunt.endDateTime)}
</p>
</div>
</button>
@@ -212,7 +200,7 @@
<StatusBadge status={huntStatus(hunt)} />
</div>
<p class="text-xs text-base-content/50">
{formatDate(hunt.startDateTime)} {formatDate(hunt.endDateTime)}
{formatDateTime(hunt.startDateTime)} {formatDateTime(hunt.endDateTime)}
</p>
{#if myTeam}
<p class="text-sm font-medium text-primary">Team: {myTeam.name}</p>
@@ -236,7 +224,7 @@
<div class="p-4 border-b border-base-200 flex items-center justify-between sticky top-0 bg-base-100">
<div>
<h3 class="font-bold text-lg">{sheetHunt.title}</h3>
<p class="text-xs text-base-content/50">{formatDate(sheetHunt.startDateTime)} {formatDate(sheetHunt.endDateTime)}</p>
<p class="text-xs text-base-content/50">{formatDateTime(sheetHunt.startDateTime)} {formatDateTime(sheetHunt.endDateTime)}</p>
</div>
<button class="btn btn-ghost btn-sm btn-circle" onclick={closeSheet}>✕</button>
</div>

View File

@@ -2,6 +2,7 @@
import {push} from 'svelte-spa-router'
import {fade} from 'svelte/transition'
import {auth} from '../../lib/stores/auth.svelte'
import {huntStatus, parseUTC} from '../../lib/utils'
import {
apiCreateTeam,
apiGetHunt,
@@ -163,13 +164,6 @@
}
}
function huntStatus(h: HuntResponse): 'ONGOING' | 'UNSTARTED' | 'CLOSED' {
if (h.isTerminated) return 'CLOSED'
const now = Date.now()
if (now < new Date(h.startDateTime).getTime()) return 'UNSTARTED'
if (now > new Date(h.endDateTime).getTime()) return 'CLOSED'
return 'ONGOING'
}
const approved = $derived(Object.values(itemStatuses).filter(s => s.itemFoundStatus === 'APPROVED').length)
const submitted = $derived(Object.values(itemStatuses).filter(s => s.itemFoundStatus === 'SUBMITTED').length)
@@ -414,7 +408,7 @@
<div class="flex items-center justify-between px-4 py-3 shrink-0">
<div>
<p class="font-semibold text-white">{photo.hunterName}</p>
<p class="text-xs text-white/40">{new Date(photo.photoUploadDateTime).toLocaleString()}</p>
<p class="text-xs text-white/40">{parseUTC(photo.photoUploadDateTime).toLocaleString()}</p>
</div>
<div class="flex items-center gap-3">
<StatusBadge status={photo.photoStatus} />