fix(dates): use UTC parsing and display for date-only strings (#351)

Date-only strings parsed with new Date(dateStr + 'T00:00:00') were
interpreted relative to the local timezone, causing off-by-one day
display for users west of UTC. Fixed across 16 files by parsing as
UTC ('T00:00:00Z') and displaying with timeZone: 'UTC'.
This commit is contained in:
Maurice
2026-04-03 21:18:56 +02:00
parent ba14636c1d
commit 897e1bff26
16 changed files with 43 additions and 43 deletions

View File

@@ -489,7 +489,7 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro
const d = currencyDecimals(currency) const d = currencyDecimals(currency)
const fmtPrice = (v: number | null | undefined) => v != null ? v.toFixed(d) : '' const fmtPrice = (v: number | null | undefined) => v != null ? v.toFixed(d) : ''
const fmtDate = (iso: string) => { if (!iso) return ''; const d = new Date(iso + 'T00:00:00'); return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }) } const fmtDate = (iso: string) => { if (!iso) return ''; const d = new Date(iso + 'T00:00:00Z'); return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: 'UTC' }) }
const header = ['Category', 'Name', 'Date', 'Total (' + currency + ')', 'Persons', 'Days', 'Per Person', 'Per Day', 'Per Person/Day', 'Note'] const header = ['Category', 'Name', 'Date', 'Total (' + currency + ')', 'Persons', 'Days', 'Per Person', 'Per Day', 'Per Person/Day', 'Note']
const rows = [header.join(sep)] const rows = [header.join(sep)]

View File

@@ -23,7 +23,7 @@ function formatDayLabel(date, t, locale) {
if (d.toDateString() === now.toDateString()) return t('collab.whatsNext.today') || 'Today' if (d.toDateString() === now.toDateString()) return t('collab.whatsNext.today') || 'Today'
if (d.toDateString() === tomorrow.toDateString()) return t('collab.whatsNext.tomorrow') || 'Tomorrow' if (d.toDateString() === tomorrow.toDateString()) return t('collab.whatsNext.tomorrow') || 'Tomorrow'
return d.toLocaleDateString(locale || undefined, { weekday: 'short', day: 'numeric', month: 'short' }) return new Date(date + 'T00:00:00Z').toLocaleDateString(locale || undefined, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })
} }
interface TripMember { interface TripMember {

View File

@@ -61,15 +61,15 @@ function categoryIconSvg(iconName, color = '#6366f1', size = 24) {
function shortDate(d, locale) { function shortDate(d, locale) {
if (!d) return '' if (!d) return ''
return new Date(d + 'T00:00:00').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' }) return new Date(d + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })
} }
function longDateRange(days, locale) { function longDateRange(days, locale) {
const dd = [...days].filter(d => d.date).sort((a, b) => a.day_number - b.day_number) const dd = [...days].filter(d => d.date).sort((a, b) => a.day_number - b.day_number)
if (!dd.length) return null if (!dd.length) return null
const f = new Date(dd[0].date + 'T00:00:00') const f = new Date(dd[0].date + 'T00:00:00Z')
const l = new Date(dd[dd.length - 1].date + 'T00:00:00') const l = new Date(dd[dd.length - 1].date + 'T00:00:00Z')
return `${f.toLocaleDateString(locale, { day: 'numeric', month: 'long' })} ${l.toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric' })}` return `${f.toLocaleDateString(locale, { day: 'numeric', month: 'long', timeZone: 'UTC' })} ${l.toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' })}`
} }
function dayCost(assignments, dayId, locale) { function dayCost(assignments, dayId, locale) {

View File

@@ -213,5 +213,5 @@ function PhotoThumbnail({ photo, days, places, onClick }: PhotoThumbnailProps) {
function formatDate(dateStr, locale) { function formatDate(dateStr, locale) {
if (!dateStr) return '' if (!dateStr) return ''
return new Date(dateStr + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' }) return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' })
} }

View File

@@ -154,9 +154,9 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
if (!day) return null if (!day) return null
const formattedDate = day.date ? new Date(day.date + 'T00:00:00').toLocaleDateString( const formattedDate = day.date ? new Date(day.date + 'T00:00:00Z').toLocaleDateString(
getLocaleForLanguage(language), getLocaleForLanguage(language),
{ weekday: 'long', day: 'numeric', month: 'long' } { weekday: 'long', day: 'numeric', month: 'long', timeZone: 'UTC' }
) : null ) : null
const placesWithCoords = places.filter(p => p.lat && p.lng) const placesWithCoords = places.filter(p => p.lat && p.lng)
@@ -445,7 +445,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
onChange={v => setHotelDayRange(prev => ({ start: v, end: Math.max(v, prev.end) }))} onChange={v => setHotelDayRange(prev => ({ start: v, end: Math.max(v, prev.end) }))}
options={days.map((d, i) => ({ options={days.map((d, i) => ({
value: d.id, value: d.id,
label: `${d.title || t('planner.dayN', { n: i + 1 })}${d.date ? `${new Date(d.date + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' })}` : ''}`, label: `${d.title || t('planner.dayN', { n: i + 1 })}${d.date ? `${new Date(d.date + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' })}` : ''}`,
}))} }))}
size="sm" size="sm"
/> />
@@ -457,7 +457,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
onChange={v => setHotelDayRange(prev => ({ start: Math.min(prev.start, v), end: v }))} onChange={v => setHotelDayRange(prev => ({ start: Math.min(prev.start, v), end: v }))}
options={days.map((d, i) => ({ options={days.map((d, i) => ({
value: d.id, value: d.id,
label: `${d.title || t('planner.dayN', { n: i + 1 })}${d.date ? `${new Date(d.date + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' })}` : ''}`, label: `${d.title || t('planner.dayN', { n: i + 1 })}${d.date ? `${new Date(d.date + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' })}` : ''}`,
}))} }))}
size="sm" size="sm"
/> />

View File

@@ -743,7 +743,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
<div style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)', lineHeight: '1.3' }}>{trip?.title}</div> <div style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)', lineHeight: '1.3' }}>{trip?.title}</div>
{(trip?.start_date || trip?.end_date) && ( {(trip?.start_date || trip?.end_date) && (
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 3 }}> <div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 3 }}>
{[trip.start_date, trip.end_date].filter(Boolean).map(d => new Date(d + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' })).join(' ')} {[trip.start_date, trip.end_date].filter(Boolean).map(d => new Date(d + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' })).join(' ')}
{days.length > 0 && ` · ${days.length} ${t('dayplan.days')}`} {days.length > 0 && ` · ${days.length} ${t('dayplan.days')}`}
</div> </div>
)} )}
@@ -1671,7 +1671,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
{res.reservation_time?.includes('T') {res.reservation_time?.includes('T')
? new Date(res.reservation_time).toLocaleString(locale, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' }) ? new Date(res.reservation_time).toLocaleString(locale, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })
: res.reservation_time : res.reservation_time
? new Date(res.reservation_time + 'T00:00:00').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' }) ? new Date(res.reservation_time + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })
: '' : ''
} }
{res.reservation_end_time?.includes('T') && ` ${new Date(res.reservation_end_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })}`} {res.reservation_end_time?.includes('T') && ` ${new Date(res.reservation_end_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })}`}

View File

@@ -373,7 +373,7 @@ export default function PlaceInspector({
{res.reservation_time && ( {res.reservation_time && (
<div> <div>
<div style={{ fontSize: 8, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase' }}>{t('reservations.date')}</div> <div style={{ fontSize: 8, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase' }}>{t('reservations.date')}</div>
<div style={{ fontSize: 10, fontWeight: 500, color: 'var(--text-primary)', marginTop: 1 }}>{new Date(res.reservation_time).toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })}</div> <div style={{ fontSize: 10, fontWeight: 500, color: 'var(--text-primary)', marginTop: 1 }}>{new Date((res.reservation_time.includes('T') ? res.reservation_time.split('T')[0] : res.reservation_time) + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}</div>
</div> </div>
)} )}
{res.reservation_time?.includes('T') && ( {res.reservation_time?.includes('T') && (

View File

@@ -424,7 +424,7 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
<div style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--bg-tertiary)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: 'var(--text-primary)', flexShrink: 0 }}>{i + 1}</div> <div style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--bg-tertiary)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: 'var(--text-primary)', flexShrink: 0 }}>{i + 1}</div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)' }}>{day.title || t('dayplan.dayN', { n: i + 1 })}</div> <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)' }}>{day.title || t('dayplan.dayN', { n: i + 1 })}</div>
{day.date && <div style={{ fontSize: 11, color: 'var(--text-faint)' }}>{new Date(day.date + 'T00:00:00').toLocaleDateString()}</div>} {day.date && <div style={{ fontSize: 11, color: 'var(--text-faint)' }}>{new Date(day.date + 'T00:00:00Z').toLocaleDateString(undefined, { timeZone: 'UTC' })}</div>}
</div> </div>
{(assignments[String(day.id)] || []).some(a => a.place?.id === dayPickerPlace.id) && <Check size={14} color="var(--text-faint)" />} {(assignments[String(day.id)] || []).some(a => a.place?.id === dayPickerPlace.id) && <Check size={14} color="var(--text-faint)" />}
</button> </button>

View File

@@ -572,6 +572,6 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
function formatDate(dateStr, locale) { function formatDate(dateStr, locale) {
if (!dateStr) return '' if (!dateStr) return ''
const d = new Date(dateStr + 'T00:00:00') const d = new Date(dateStr + 'T00:00:00Z')
return d.toLocaleDateString(locale || undefined, { day: 'numeric', month: 'short' }) return d.toLocaleDateString(locale || undefined, { day: 'numeric', month: 'short', timeZone: 'UTC' })
} }

View File

@@ -84,8 +84,8 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
} }
const fmtDate = (str) => { const fmtDate = (str) => {
const d = new Date(str) const dateOnly = str.includes('T') ? str.split('T')[0] : str
return d.toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' }) return new Date(dateOnly + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })
} }
const fmtTime = (str) => { const fmtTime = (str) => {
const d = new Date(str) const d = new Date(str)

View File

@@ -197,10 +197,10 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
if (!prev.end_date || prev.end_date < value) { if (!prev.end_date || prev.end_date < value) {
next.end_date = value next.end_date = value
} else if (prev.start_date) { } else if (prev.start_date) {
const oldStart = new Date(prev.start_date + 'T00:00:00') const oldStart = new Date(prev.start_date + 'T00:00:00Z')
const oldEnd = new Date(prev.end_date + 'T00:00:00') const oldEnd = new Date(prev.end_date + 'T00:00:00Z')
const duration = Math.round((oldEnd - oldStart) / 86400000) const duration = Math.round((oldEnd - oldStart) / 86400000)
const newEnd = new Date(value + 'T00:00:00') const newEnd = new Date(value + 'T00:00:00Z')
newEnd.setDate(newEnd.getDate() + duration) newEnd.setDate(newEnd.getDate() + duration)
next.end_date = newEnd.toISOString().split('T')[0] next.end_date = newEnd.toISOString().split('T')[0]
} }

View File

@@ -104,18 +104,18 @@ export function getHolidays(year: number, bundesland: string = 'NW'): Record<str
} }
export function isWeekend(dateStr: string, weekendDays: number[] = [0, 6]): boolean { export function isWeekend(dateStr: string, weekendDays: number[] = [0, 6]): boolean {
const d = new Date(dateStr + 'T00:00:00') const d = new Date(dateStr + 'T00:00:00Z')
return weekendDays.includes(d.getDay()) return weekendDays.includes(d.getUTCDay())
} }
export function getWeekday(dateStr: string): string { export function getWeekday(dateStr: string): string {
const d = new Date(dateStr + 'T00:00:00') const d = new Date(dateStr + 'T00:00:00Z')
return ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][d.getDay()] return ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][d.getUTCDay()]
} }
export function getWeekdayFull(dateStr: string): string { export function getWeekdayFull(dateStr: string): string {
const d = new Date(dateStr + 'T00:00:00') const d = new Date(dateStr + 'T00:00:00Z')
return ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'][d.getDay()] return ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'][d.getUTCDay()]
} }
export function daysInMonth(year: number, month: number): number { export function daysInMonth(year: number, month: number): number {
@@ -123,8 +123,8 @@ export function daysInMonth(year: number, month: number): number {
} }
export function formatDate(dateStr: string, locale?: string): string { export function formatDate(dateStr: string, locale?: string): string {
const d = new Date(dateStr + 'T00:00:00') const d = new Date(dateStr + 'T00:00:00Z')
return d.toLocaleDateString(locale || undefined, { weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' }) return d.toLocaleDateString(locale || undefined, { weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric', timeZone: 'UTC' })
} }
export { BUNDESLAENDER } export { BUNDESLAENDER }

View File

@@ -21,9 +21,9 @@ export function CustomDatePicker({ value, onChange, placeholder, style = {}, com
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const dropRef = useRef<HTMLDivElement>(null) const dropRef = useRef<HTMLDivElement>(null)
const parsed = value ? new Date(value + 'T00:00:00') : null const parsed = value ? new Date(value + 'T00:00:00Z') : null
const [viewYear, setViewYear] = useState(parsed?.getFullYear() || new Date().getFullYear()) const [viewYear, setViewYear] = useState(parsed?.getUTCFullYear() || new Date().getFullYear())
const [viewMonth, setViewMonth] = useState(parsed?.getMonth() ?? new Date().getMonth()) const [viewMonth, setViewMonth] = useState(parsed?.getUTCMonth() ?? new Date().getMonth())
useEffect(() => { useEffect(() => {
const handler = (e: MouseEvent) => { const handler = (e: MouseEvent) => {
@@ -36,7 +36,7 @@ export function CustomDatePicker({ value, onChange, placeholder, style = {}, com
}, [open]) }, [open])
useEffect(() => { useEffect(() => {
if (open && parsed) { setViewYear(parsed.getFullYear()); setViewMonth(parsed.getMonth()) } if (open && parsed) { setViewYear(parsed.getUTCFullYear()); setViewMonth(parsed.getUTCMonth()) }
}, [open]) }, [open])
const prevMonth = () => { if (viewMonth === 0) { setViewMonth(11); setViewYear(y => y - 1) } else setViewMonth(m => m - 1) } const prevMonth = () => { if (viewMonth === 0) { setViewMonth(11); setViewYear(y => y - 1) } else setViewMonth(m => m - 1) }
@@ -47,7 +47,7 @@ export function CustomDatePicker({ value, onChange, placeholder, style = {}, com
const startDay = (getWeekday(viewYear, viewMonth, 1) + 6) % 7 const startDay = (getWeekday(viewYear, viewMonth, 1) + 6) % 7
const weekdays = Array.from({ length: 7 }, (_, i) => new Date(2024, 0, i + 1).toLocaleDateString(locale, { weekday: 'narrow' })) const weekdays = Array.from({ length: 7 }, (_, i) => new Date(2024, 0, i + 1).toLocaleDateString(locale, { weekday: 'narrow' }))
const displayValue = parsed ? parsed.toLocaleDateString(locale, compact ? { day: '2-digit', month: '2-digit', year: '2-digit' } : { day: 'numeric', month: 'short', year: 'numeric' }) : null const displayValue = parsed ? parsed.toLocaleDateString(locale, compact ? { day: '2-digit', month: '2-digit', year: '2-digit', timeZone: 'UTC' } : { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }) : null
const selectDay = (day: number) => { const selectDay = (day: number) => {
const y = String(viewYear) const y = String(viewYear)
@@ -57,7 +57,7 @@ export function CustomDatePicker({ value, onChange, placeholder, style = {}, com
setOpen(false) setOpen(false)
} }
const selectedDay = parsed && parsed.getFullYear() === viewYear && parsed.getMonth() === viewMonth ? parsed.getDate() : null const selectedDay = parsed && parsed.getUTCFullYear() === viewYear && parsed.getUTCMonth() === viewMonth ? parsed.getUTCDate() : null
const today = new Date() const today = new Date()
const isToday = (d: number) => today.getFullYear() === viewYear && today.getMonth() === viewMonth && today.getDate() === d const isToday = (d: number) => today.getFullYear() === viewYear && today.getMonth() === viewMonth && today.getDate() === d

View File

@@ -59,12 +59,12 @@ function getTripStatus(trip: DashboardTrip): string | null {
function formatDate(dateStr: string | null | undefined, locale: string = 'en-US'): string | null { function formatDate(dateStr: string | null | undefined, locale: string = 'en-US'): string | null {
if (!dateStr) return null if (!dateStr) return null
return new Date(dateStr + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' }) return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' })
} }
function formatDateShort(dateStr: string | null | undefined, locale: string = 'en-US'): string | null { function formatDateShort(dateStr: string | null | undefined, locale: string = 'en-US'): string | null {
if (!dateStr) return null if (!dateStr) return null
return new Date(dateStr + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' }) return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' })
} }
function sortTrips(trips: DashboardTrip[]): DashboardTrip[] { function sortTrips(trips: DashboardTrip[]): DashboardTrip[] {

View File

@@ -106,7 +106,7 @@ export default function SharedTripPage() {
{(trip.start_date || trip.end_date) && ( {(trip.start_date || trip.end_date) && (
<div style={{ marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 8, padding: '6px 14px', borderRadius: 20, background: 'rgba(255,255,255,0.08)', backdropFilter: 'blur(4px)', border: '1px solid rgba(255,255,255,0.08)' }}> <div style={{ marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 8, padding: '6px 14px', borderRadius: 20, background: 'rgba(255,255,255,0.08)', backdropFilter: 'blur(4px)', border: '1px solid rgba(255,255,255,0.08)' }}>
<span style={{ fontSize: 12, fontWeight: 500, opacity: 0.8 }}> <span style={{ fontSize: 12, fontWeight: 500, opacity: 0.8 }}>
{[trip.start_date, trip.end_date].filter(Boolean).map((d: string) => new Date(d + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' })).join(' — ')} {[trip.start_date, trip.end_date].filter(Boolean).map((d: string) => new Date(d + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' })).join(' — ')}
</span> </span>
{days?.length > 0 && <span style={{ fontSize: 11, opacity: 0.4 }}>·</span>} {days?.length > 0 && <span style={{ fontSize: 11, opacity: 0.4 }}>·</span>}
{days?.length > 0 && <span style={{ fontSize: 11, opacity: 0.5 }}>{days.length} {t('shared.days')}</span>} {days?.length > 0 && <span style={{ fontSize: 11, opacity: 0.5 }}>{days.length} {t('shared.days')}</span>}
@@ -199,7 +199,7 @@ export default function SharedTripPage() {
<div style={{ width: 28, height: 28, borderRadius: '50%', background: selectedDay === day.id ? '#111827' : '#f3f4f6', color: selectedDay === day.id ? 'white' : '#6b7280', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, flexShrink: 0 }}>{di + 1}</div> <div style={{ width: 28, height: 28, borderRadius: '50%', background: selectedDay === day.id ? '#111827' : '#f3f4f6', color: selectedDay === day.id ? 'white' : '#6b7280', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, flexShrink: 0 }}>{di + 1}</div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>{day.title || `Day ${day.day_number}`}</div> <div style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>{day.title || `Day ${day.day_number}`}</div>
{day.date && <div style={{ fontSize: 11, color: '#9ca3af', marginTop: 1 }}>{new Date(day.date + 'T00:00:00').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })}</div>} {day.date && <div style={{ fontSize: 11, color: '#9ca3af', marginTop: 1 }}>{new Date(day.date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}</div>}
</div> </div>
{dayAccs.map((acc: any) => ( {dayAccs.map((acc: any) => (
<span key={acc.id} style={{ fontSize: 9, padding: '2px 6px', borderRadius: 4, background: '#f3f4f6', color: '#6b7280', display: 'flex', alignItems: 'center', gap: 3 }}> <span key={acc.id} style={{ fontSize: 9, padding: '2px 6px', borderRadius: 4, background: '#f3f4f6', color: '#6b7280', display: 'flex', alignItems: 'center', gap: 3 }}>
@@ -274,7 +274,7 @@ export default function SharedTripPage() {
const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {}) const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {})
const TIcon = TRANSPORT_ICONS[r.type] || Ticket const TIcon = TRANSPORT_ICONS[r.type] || Ticket
const time = r.reservation_time?.includes('T') ? r.reservation_time.split('T')[1]?.substring(0, 5) : '' const time = r.reservation_time?.includes('T') ? r.reservation_time.split('T')[1]?.substring(0, 5) : ''
const date = r.reservation_time ? new Date(r.reservation_time.includes('T') ? r.reservation_time : r.reservation_time + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' }) : '' const date = r.reservation_time ? new Date((r.reservation_time.includes('T') ? r.reservation_time.split('T')[0] : r.reservation_time) + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }) : ''
return ( return (
<div key={r.id} style={{ background: 'var(--bg-card, white)', borderRadius: 10, padding: '12px 16px', border: '1px solid var(--border-faint, #e5e7eb)', display: 'flex', alignItems: 'center', gap: 12 }}> <div key={r.id} style={{ background: 'var(--bg-card, white)', borderRadius: 10, padding: '12px 16px', border: '1px solid var(--border-faint, #e5e7eb)', display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 32, height: 32, borderRadius: '50%', background: '#f3f4f6', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div style={{ width: 32, height: 32, borderRadius: '50%', background: '#f3f4f6', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>

View File

@@ -10,9 +10,9 @@ export function formatDate(dateStr: string | null | undefined, locale: string, t
if (!dateStr) return null if (!dateStr) return null
const opts: Intl.DateTimeFormatOptions = { const opts: Intl.DateTimeFormatOptions = {
weekday: 'short', day: 'numeric', month: 'short', weekday: 'short', day: 'numeric', month: 'short',
timeZone: timeZone || 'UTC',
} }
if (timeZone) opts.timeZone = timeZone return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, opts)
return new Date(dateStr + 'T00:00:00').toLocaleDateString(locale, opts)
} }
export function formatTime(timeStr: string | null | undefined, locale: string, timeFormat: string): string { export function formatTime(timeStr: string | null | undefined, locale: string, timeFormat: string): string {