fix(frontend): fix off-by-one date shown in the backfill horizon picker
daysAgoToISODate built the date at local midnight but formatted it with toISOString(), which converts to UTC first -- in any timezone ahead of UTC (e.g. Europe/Paris), local midnight is still the previous day in UTC, so the picker displayed a date one day earlier than the one that was actually selected/stored. Format from the Date's local year/month/day components instead of going through UTC.
This commit is contained in:
@@ -31,6 +31,54 @@ function NumberField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renders/edits BackfillHorizonDays as a calendar date (the day sync should
|
||||||
|
// backfill through) rather than a raw day count, which is easier to reason
|
||||||
|
// about than counting days back -- the stored value is still a day count
|
||||||
|
// (what the backend actually consumes), just presented as its equivalent
|
||||||
|
// date, recomputed against "today" on every render/change.
|
||||||
|
function daysAgoToISODate(days: number): string {
|
||||||
|
const d = new Date();
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
d.setDate(d.getDate() - days);
|
||||||
|
// Deliberately not toISOString(): that formats in UTC, which in any
|
||||||
|
// timezone ahead of UTC shifts local midnight back to the previous day.
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isoDateToDaysAgo(iso: string): number {
|
||||||
|
const picked = new Date(iso + "T00:00:00");
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const diffMs = today.getTime() - picked.getTime();
|
||||||
|
return Math.max(0, Math.round(diffMs / (24 * 60 * 60 * 1000)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function BackfillHorizonField({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label>
|
||||||
|
Past sync (days)
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={daysAgoToISODate(value)}
|
||||||
|
max={daysAgoToISODate(0)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value) onChange(isoDateToDaysAgo(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="field-hint">{value} day{value === 1 ? "" : "s"} ago</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// How long to wait after the last edit before actually saving, so typing a
|
// How long to wait after the last edit before actually saving, so typing a
|
||||||
// name or a multi-digit number doesn't fire one request per keystroke --
|
// name or a multi-digit number doesn't fire one request per keystroke --
|
||||||
// only the last edit in a burst triggers a save, covering every field
|
// only the last edit in a burst triggers a save, covering every field
|
||||||
@@ -162,8 +210,7 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
|
|||||||
onChange={(e) => set("GarminPassword", e.target.value)}
|
onChange={(e) => set("GarminPassword", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<NumberField
|
<BackfillHorizonField
|
||||||
label="Past sync (days)"
|
|
||||||
value={profile.BackfillHorizonDays}
|
value={profile.BackfillHorizonDays}
|
||||||
onChange={(v) => set("BackfillHorizonDays", v)}
|
onChange={(v) => set("BackfillHorizonDays", v)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user