fix date timezone issues: use JST for GW events, local time for user dates

This commit is contained in:
Justin Edmund 2025-12-18 00:49:23 -08:00
parent 472672ebc8
commit 77cb109dd2
10 changed files with 65 additions and 86 deletions

View file

@ -13,6 +13,7 @@
import ModalBody from '$lib/components/ui/ModalBody.svelte' import ModalBody from '$lib/components/ui/ModalBody.svelte'
import Button from '$lib/components/ui/Button.svelte' import Button from '$lib/components/ui/Button.svelte'
import Icon from '$lib/components/Icon.svelte' import Icon from '$lib/components/Icon.svelte'
import { formatDate } from '$lib/utils/date'
import type { CrewInvitation, PhantomPlayer } from '$lib/types/api/crew' import type { CrewInvitation, PhantomPlayer } from '$lib/types/api/crew'
interface Props { interface Props {
@ -98,15 +99,6 @@
} }
} }
// Format date
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
// Check if invitation is expired // Check if invitation is expired
function isExpired(expiresAt: string): boolean { function isExpired(expiresAt: string): boolean {
return new Date(expiresAt) < new Date() return new Date(expiresAt) < new Date()

View file

@ -6,6 +6,7 @@
import DropdownMenu from '$lib/components/ui/DropdownMenu.svelte' import DropdownMenu from '$lib/components/ui/DropdownMenu.svelte'
import { DropdownMenu as DropdownMenuBase } from 'bits-ui' import { DropdownMenu as DropdownMenuBase } from 'bits-ui'
import { crewStore } from '$lib/stores/crew.store.svelte' import { crewStore } from '$lib/stores/crew.store.svelte'
import { formatDate } from '$lib/utils/date'
import type { CrewMembership } from '$lib/types/api/crew' import type { CrewMembership } from '$lib/types/api/crew'
interface Props { interface Props {
@ -40,14 +41,6 @@
} }
} }
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
const canShowOfficerActions = $derived( const canShowOfficerActions = $derived(
crewStore.isOfficer && crewStore.isOfficer &&
crewStore.canActOnMember(member.role) && crewStore.canActOnMember(member.role) &&

View file

@ -5,6 +5,7 @@
import DropdownMenu from '$lib/components/ui/DropdownMenu.svelte' import DropdownMenu from '$lib/components/ui/DropdownMenu.svelte'
import { DropdownMenu as DropdownMenuBase } from 'bits-ui' import { DropdownMenu as DropdownMenuBase } from 'bits-ui'
import { crewStore } from '$lib/stores/crew.store.svelte' import { crewStore } from '$lib/stores/crew.store.svelte'
import { formatDate } from '$lib/utils/date'
import type { PhantomPlayer } from '$lib/types/api/crew' import type { PhantomPlayer } from '$lib/types/api/crew'
interface Props { interface Props {
@ -20,14 +21,6 @@
const { phantom, currentUserId, onEdit, onDelete, onAssign, onAccept, onDecline }: Props = const { phantom, currentUserId, onEdit, onDelete, onAssign, onAccept, onDecline }: Props =
$props() $props()
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
// Status badge type // Status badge type
type ClaimStatus = 'unclaimed' | 'pending' | 'claimed' type ClaimStatus = 'unclaimed' | 'pending' | 'claimed'

49
src/lib/utils/date.ts Normal file
View file

@ -0,0 +1,49 @@
/**
* Format a date string in JST (Japan Standard Time).
* Use this for game-related dates like GW events, as Granblue Fantasy uses JST.
*/
export function formatDateJST(
dateString: string,
options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
day: 'numeric'
}
): string {
return new Date(dateString).toLocaleDateString(undefined, {
...options,
timeZone: 'Asia/Tokyo'
})
}
/**
* Format a date string in local time.
* Use this for user-related dates like join dates, invitation expiries, etc.
*
* For date-only strings (YYYY-MM-DD), appends T00:00:00 to parse as local midnight
* instead of UTC midnight, preventing the date from shifting.
*/
export function formatDate(
dateString: string,
options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
day: 'numeric'
}
): string {
// If it's a date-only string (YYYY-MM-DD), parse as local time
const dateToFormat =
dateString.length === 10 ? new Date(dateString + 'T00:00:00') : new Date(dateString)
return dateToFormat.toLocaleDateString(undefined, options)
}
/**
* Format a date string with long month format in JST (e.g., "June 21, 2025")
*/
export function formatDateLongJST(dateString: string): string {
return formatDateJST(dateString, {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}

View file

@ -14,6 +14,7 @@
import ModalFooter from '$lib/components/ui/ModalFooter.svelte' import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
import Input from '$lib/components/ui/Input.svelte' import Input from '$lib/components/ui/Input.svelte'
import CrewHeader from '$lib/components/crew/CrewHeader.svelte' import CrewHeader from '$lib/components/crew/CrewHeader.svelte'
import { formatDateJST } from '$lib/utils/date'
import type { PageData } from './$types' import type { PageData } from './$types'
interface Props { interface Props {
@ -159,15 +160,6 @@
settingsError = null settingsError = null
} }
// Helper for formatting dates
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
// Helper for formatting scores with commas // Helper for formatting scores with commas
function formatScore(score: number): string { function formatScore(score: number): string {
return score.toLocaleString() return score.toLocaleString()
@ -296,7 +288,7 @@
</span> </span>
</div> </div>
<span class="event-dates"> <span class="event-dates">
{formatDate(event.startDate)} {formatDate(event.endDate)} {formatDateJST(event.startDate)} {formatDateJST(event.endDate)}
</span> </span>
<span class="event-score"> <span class="event-score">
{#if event.crewTotalScore !== undefined} {#if event.crewTotalScore !== undefined}

View file

@ -21,6 +21,7 @@
import EditCrewScoreModal from '$lib/components/crew/EditCrewScoreModal.svelte' import EditCrewScoreModal from '$lib/components/crew/EditCrewScoreModal.svelte'
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte' import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
import Segment from '$lib/components/ui/segmented-control/Segment.svelte' import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
import { formatDateJST } from '$lib/utils/date'
import { import {
GW_ROUND_LABELS, GW_ROUND_LABELS,
type GwRound, type GwRound,
@ -159,15 +160,6 @@
return parseInt(value.replace(/,/g, ''), 10) return parseInt(value.replace(/,/g, ''), 10)
} }
// Format date
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
// Navigate back // Navigate back
function handleBack() { function handleBack() {
goto('/crew') goto('/crew')
@ -406,7 +398,7 @@
{elementLabels[gwEvent.element] ?? 'Unknown'} {elementLabels[gwEvent.element] ?? 'Unknown'}
</span> </span>
<span class="event-dates"> <span class="event-dates">
{formatDate(gwEvent.startDate)} {formatDate(gwEvent.endDate)} {formatDateJST(gwEvent.startDate)} {formatDateJST(gwEvent.endDate)}
</span> </span>
</div> </div>
<div class="tab-control"> <div class="tab-control">

View file

@ -8,6 +8,7 @@
import { useAcceptInvitation, useRejectInvitation } from '$lib/api/mutations/crew.mutations' import { useAcceptInvitation, useRejectInvitation } from '$lib/api/mutations/crew.mutations'
import { crewStore } from '$lib/stores/crew.store.svelte' import { crewStore } from '$lib/stores/crew.store.svelte'
import Button from '$lib/components/ui/Button.svelte' import Button from '$lib/components/ui/Button.svelte'
import { formatDate } from '$lib/utils/date'
import type { PageData } from './$types' import type { PageData } from './$types'
interface Props { interface Props {
@ -61,15 +62,6 @@
} }
} }
// Format date
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
// Check if invitation is expired // Check if invitation is expired
function isExpired(expiresAt: string): boolean { function isExpired(expiresAt: string): boolean {
return new Date(expiresAt) < new Date() return new Date(expiresAt) < new Date()

View file

@ -29,6 +29,7 @@
import AssignPhantomModal from '$lib/components/crew/AssignPhantomModal.svelte' import AssignPhantomModal from '$lib/components/crew/AssignPhantomModal.svelte'
import ConfirmClaimModal from '$lib/components/crew/ConfirmClaimModal.svelte' import ConfirmClaimModal from '$lib/components/crew/ConfirmClaimModal.svelte'
import { DropdownMenu as DropdownMenuBase } from 'bits-ui' import { DropdownMenu as DropdownMenuBase } from 'bits-ui'
import { formatDate } from '$lib/utils/date'
import type { import type {
MemberFilter, MemberFilter,
CrewMembership, CrewMembership,
@ -303,15 +304,6 @@
} }
} }
// Format date
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
// Check if invitation is expired // Check if invitation is expired
function isInvitationExpired(expiresAt: string): boolean { function isInvitationExpired(expiresAt: string): boolean {
return new Date(expiresAt) < new Date() return new Date(expiresAt) < new Date()

View file

@ -7,6 +7,7 @@
import { createQuery } from '@tanstack/svelte-query' import { createQuery } from '@tanstack/svelte-query'
import { gwAdapter } from '$lib/api/adapters/gw.adapter' import { gwAdapter } from '$lib/api/adapters/gw.adapter'
import Button from '$lib/components/ui/Button.svelte' import Button from '$lib/components/ui/Button.svelte'
import { formatDateJST } from '$lib/utils/date'
import type { GwEvent } from '$lib/types/api/gw' import type { GwEvent } from '$lib/types/api/gw'
import type { PageData } from './$types' import type { PageData } from './$types'
@ -61,15 +62,6 @@
) )
}) })
// Format date for display
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
// Navigate to event detail/edit // Navigate to event detail/edit
function handleRowClick(event: GwEvent) { function handleRowClick(event: GwEvent) {
goto(`/database/gw-events/${event.id}`) goto(`/database/gw-events/${event.id}`)
@ -124,7 +116,7 @@
</td> </td>
<td class="col-dates"> <td class="col-dates">
<span class="dates"> <span class="dates">
{formatDate(event.startDate)} - {formatDate(event.endDate)} {formatDateJST(event.startDate)} - {formatDateJST(event.endDate)}
</span> </span>
</td> </td>
</tr> </tr>

View file

@ -9,6 +9,7 @@
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte' import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
import DetailItem from '$lib/components/ui/DetailItem.svelte' import DetailItem from '$lib/components/ui/DetailItem.svelte'
import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte' import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte'
import { formatDateJST, formatDateLongJST } from '$lib/utils/date'
import type { PageData } from './$types' import type { PageData } from './$types'
interface Props { interface Props {
@ -52,15 +53,6 @@
6: 'light' 6: 'light'
} }
// Format date for display
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
// Navigate to edit // Navigate to edit
function handleEdit() { function handleEdit() {
goto(`/database/gw-events/${eventId}/edit`) goto(`/database/gw-events/${eventId}/edit`)
@ -102,15 +94,15 @@
{elementLabels[event.element] ?? 'Unknown'} {elementLabels[event.element] ?? 'Unknown'}
</span> </span>
</DetailItem> </DetailItem>
<DetailItem label="Start Date" value={formatDate(event.startDate)} /> <DetailItem label="Start Date" value={formatDateLongJST(event.startDate)} />
<DetailItem label="End Date" value={formatDate(event.endDate)} /> <DetailItem label="End Date" value={formatDateLongJST(event.endDate)} />
</DetailsContainer> </DetailsContainer>
{#if event.createdAt} {#if event.createdAt}
<DetailsContainer title="Metadata"> <DetailsContainer title="Metadata">
<DetailItem label="Created" value={formatDate(event.createdAt)} /> <DetailItem label="Created" value={formatDateJST(event.createdAt)} />
{#if event.updatedAt} {#if event.updatedAt}
<DetailItem label="Updated" value={formatDate(event.updatedAt)} /> <DetailItem label="Updated" value={formatDateJST(event.updatedAt)} />
{/if} {/if}
</DetailsContainer> </DetailsContainer>
{/if} {/if}