Files
HealthLog/src/components/day/day-url.ts
T
Marc-André Bombeck bf83d414e2 feat: open any past day as a layer over the page
A date is now a door to its day (#613). `?day=YYYY-MM-DD` opens the day
over whichever page is open: docked beside it from 1280 px, the way the
Coach's conversations panel sits, a sheet from the right below that, and a
bottom sheet on a phone that opens at a little over half the height and
grows to full height from its handle. On /coach it is always a sheet, so
there is never more than one docked panel. Back and Escape close it, the
previous and next day replace the history entry instead of adding one,
Alt with an arrow steps a day, focus moves to the day's heading on open and
returns to whatever opened it on close, and a polite region names the day.
A malformed or future date is dropped from the URL.

The day shows the value it was opened from with its comparison, what ran
through the day, the readings against the person's own usual range on a
short number line (eight curated, the rest behind "All values"), and what
happened in order of the clock, each row linking to its record. Loading,
failure, an empty day and areas a grant does not cover each read as such.
The footer offers the timeline when that module is on and capture for the
day, with the capture forms starting on that date.

Doors into it: a click on a daily point of a metric chart (a tap shows the
value and the tooltip offers the day on touch screens), the dates in the
chart's data table, a quiet row of dots under the axis for days with
entries and a ring for notable ones, the lab chart and lab readings, the
measurement, mood and intake lists, symptom and illness day headings,
episode onset and recovery, document dates, workout dates, the workout
detail, the mood and adherence heatmaps, and the days a Coach answer's
get_day calls read, shown as chips. The next planned visit carries what
happened since the last one, each date opening its day, with a doctor
report for that window.

Not here, and frozen by a guard: the dashboard's today area, mini charts
in tiles, the cycle calendar, model-written text, future dates, the
clinician share link and the doctor-report PDF.
2026-10-08 14:20:01 +02:00

111 lines
3.8 KiB
TypeScript

/**
* The day layer's address: `?day=YYYY-MM-DD` on whatever page is open.
*
* Pure helpers, no React, so the rules the layer lives by (which dates open,
* how the URL is spelled, what a step to the neighbouring day lands on) can be
* pinned without mounting anything.
*
* A day opens only when it names a real calendar date that is not in the
* future of the record's own zone. A future day has no content to show; a
* link to one would be a dead end, so such a parameter is dropped quietly,
* the same way `?add=` is on the measurements page.
*/
import { DAY_QUERY_PARAM, type DateKey } from "@/lib/day/contract";
import { dateOnlyKey, isCalendarDateKey } from "@/lib/tz/date-only";
import { getDateTimeFormat } from "@/lib/tz/intl-cache";
/**
* The history-state marker of an entry the layer pushed itself. Closing such
* an entry goes back one step; closing a deep link only edits the URL, so the
* person stays on the page they arrived at.
*/
export const DAY_HISTORY_KEY = "__healthlogDayLayer";
/** Today's calendar date in `timeZone`, `YYYY-MM-DD`. */
export function todayKeyInZone(timeZone: string, now = new Date()): DateKey {
// en-CA spells a date as YYYY-MM-DD.
return getDateTimeFormat("en-CA", timeZone, {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(now);
}
/** The calendar date of an instant in `timeZone`. */
export function dateKeyOfInstant(
instant: Date | string | number,
timeZone: string,
): DateKey {
return todayKeyInZone(timeZone, new Date(instant));
}
/** True when `key` is a real date on or before `todayKey`. */
export function isOpenableDay(
key: string | null | undefined,
todayKey: DateKey,
): key is DateKey {
return typeof key === "string" && isCalendarDateKey(key) && key <= todayKey;
}
/** The parameter's day, or null when it names nothing the layer can open. */
export function parseDayParam(
raw: string | null,
todayKey: DateKey,
): DateKey | null {
return isOpenableDay(raw, todayKey) ? raw : null;
}
/** `key` moved by `delta` calendar days. */
export function shiftDateKey(key: DateKey, delta: number): DateKey {
const anchor = new Date(`${key}T12:00:00.000Z`);
anchor.setUTCDate(anchor.getUTCDate() + delta);
return dateOnlyKey(anchor);
}
/** Calendar days from `from` to `to` (negative when `to` is earlier). */
export function daysBetween(from: DateKey, to: DateKey): number {
const a = Date.parse(`${from}T12:00:00.000Z`);
const b = Date.parse(`${to}T12:00:00.000Z`);
return Math.round((b - a) / 86_400_000);
}
/** The current location with the day set, or removed when `key` is null. */
export function withDayHref(
pathname: string,
search: string,
key: DateKey | null,
): string {
const next = new URLSearchParams(search);
if (key === null) next.delete(DAY_QUERY_PARAM);
else next.set(DAY_QUERY_PARAM, key);
const query = next.toString();
return query ? `${pathname}?${query}` : pathname;
}
/**
* The link a day carries off its own page: the dashboard with the day open
* over it. Closing the layer there lands on today, which is what a link from
* outside the page (a Coach chip, a notification) should do.
*/
export function externalDayHref(key: DateKey): string {
return `/?${DAY_QUERY_PARAM}=${key}`;
}
/**
* The history state the layer writes. Only application state: Next copies its
* router fields onto the object itself, and copying `history.state` here would
* carry them over and make Next treat the write as its own.
*/
export function dayHistoryState(key: DateKey): Record<string, unknown> {
return { [DAY_HISTORY_KEY]: key };
}
/** True when the current history entry was pushed by the layer. */
export function historyEntryOwnedByLayer(state: unknown): boolean {
return (
state !== null &&
typeof state === "object" &&
typeof (state as Record<string, unknown>)[DAY_HISTORY_KEY] === "string"
);
}