Fix 7-day forecast showing yesterday: UTC date-parsing bug
new Date("YYYY-MM-DD") parses bare date strings as UTC midnight per
spec, then toLocaleDateString() rendered that in the browser's local
timezone — for any zone behind UTC (all of the Americas), that rolls
the displayed date back a full day, making "today" look like it
already passed. Added parseDateOnly() (year/month/day constructor,
builds the date in local time instead of round-tripping through UTC)
and use it for the daily forecast headings.
Also switched the heading format to "Friday July, 24th, 2026" per
request, which needed the row layout to drop its fixed 40px width for
the old "Thu" abbreviation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c1L8ghNBFjfiH64UMViP8
This commit is contained in:
@@ -21,6 +21,36 @@ export function exactTime(iso: string): string {
|
||||
return `${mm}/${dd}/${yyyy} - ${String(hours).padStart(2, '0')}:${minutes} ${ampm}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open-Meteo's daily forecast returns bare "YYYY-MM-DD" dates (no time component) meaning
|
||||
* that calendar day in the forecast location's own timezone. `new Date("YYYY-MM-DD")` parses
|
||||
* that as UTC midnight per the ECMAScript spec — displaying it in the browser's local
|
||||
* timezone (anything behind UTC, i.e. all of the Americas) then rolls it back to the
|
||||
* *previous* day, making "today" look like it already passed. Parsing via the (year, month,
|
||||
* day) constructor instead builds the date in local time, avoiding the UTC round-trip.
|
||||
*/
|
||||
export function parseDateOnly(dateOnly: string): Date {
|
||||
const [year, month, day] = dateOnly.split('-').map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
function ordinal(n: number): string {
|
||||
const lastDigit = n % 10;
|
||||
const lastTwoDigits = n % 100;
|
||||
if (lastDigit === 1 && lastTwoDigits !== 11) return `${n}st`;
|
||||
if (lastDigit === 2 && lastTwoDigits !== 12) return `${n}nd`;
|
||||
if (lastDigit === 3 && lastTwoDigits !== 13) return `${n}rd`;
|
||||
return `${n}th`;
|
||||
}
|
||||
|
||||
/** "Friday July, 24th, 2026" — full weekday/month name with an ordinal day, for the 7-day forecast headings. */
|
||||
export function formatDayHeading(dateOnly: string): string {
|
||||
const d = parseDateOnly(dateOnly);
|
||||
const weekday = d.toLocaleDateString([], { weekday: 'long' });
|
||||
const month = d.toLocaleDateString([], { month: 'long' });
|
||||
return `${weekday} ${month}, ${ordinal(d.getDate())}, ${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
export function slugify(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { timeAgo } from '$lib/format';
|
||||
import { timeAgo, formatDayHeading } from '$lib/format';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const weather = $derived(data.weather);
|
||||
@@ -91,7 +91,7 @@
|
||||
<div class="daily-list">
|
||||
{#each weather.daily as day (day.date)}
|
||||
<div class="day-row">
|
||||
<span class="day-name">{new Date(day.date).toLocaleDateString([], { weekday: 'short' })}</span>
|
||||
<span class="day-name">{formatDayHeading(day.date)}</span>
|
||||
<span class="day-icon">{day.icon}</span>
|
||||
<span class="day-condition">{day.conditionText}</span>
|
||||
<span class="day-range">{Math.round(day.tempMax)}° / {Math.round(day.tempMin)}°</span>
|
||||
@@ -257,12 +257,13 @@
|
||||
.daily-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 480px;
|
||||
max-width: 640px;
|
||||
}
|
||||
.day-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 14px;
|
||||
padding: 10px 0;
|
||||
border-top: 0.5px solid var(--border);
|
||||
}
|
||||
@@ -272,7 +273,8 @@
|
||||
.day-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
width: 40px;
|
||||
white-space: nowrap;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.day-icon {
|
||||
font-size: 20px;
|
||||
|
||||
Reference in New Issue
Block a user