Merge branch 'phase/002-design-layout'
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Datums- und Zeitformate der Oberfläche (#002): TT.MM.JJJJ, 24-h-Zeit, deutsch.
|
||||
*
|
||||
* Die Zeitzone ist fest Europe/Berlin und nicht die des Prozesses: Server (UTC im
|
||||
* Container) und Browser sollen dieselbe Zeit anzeigen, sonst springen Zeitstempel
|
||||
* zwischen Server- und Client-Rendering um Stunden.
|
||||
*/
|
||||
|
||||
export const ZEITZONE = "Europe/Berlin";
|
||||
|
||||
type Eingabe = Date | string | number;
|
||||
|
||||
const DATUM = new Intl.DateTimeFormat("de-DE", {
|
||||
timeZone: ZEITZONE,
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const UHRZEIT = new Intl.DateTimeFormat("de-DE", {
|
||||
timeZone: ZEITZONE,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
});
|
||||
|
||||
function alsDatum(wert: Eingabe): Date {
|
||||
const d = wert instanceof Date ? wert : new Date(wert);
|
||||
if (Number.isNaN(d.getTime())) throw new RangeError(`Ungültiges Datum: ${String(wert)}`);
|
||||
return d;
|
||||
}
|
||||
|
||||
/** „12.10.2026“ */
|
||||
export function datum(wert: Eingabe): string {
|
||||
return DATUM.format(alsDatum(wert));
|
||||
}
|
||||
|
||||
/** „08:42“ */
|
||||
export function uhrzeit(wert: Eingabe): string {
|
||||
return UHRZEIT.format(alsDatum(wert));
|
||||
}
|
||||
|
||||
/** „12.10.2026, 08:42“ */
|
||||
export function datumZeit(wert: Eingabe): string {
|
||||
const d = alsDatum(wert);
|
||||
return `${DATUM.format(d)}, ${UHRZEIT.format(d)}`;
|
||||
}
|
||||
|
||||
const MINUTE = 60_000;
|
||||
const STUNDE = 60 * MINUTE;
|
||||
const TAG = 24 * STUNDE;
|
||||
|
||||
/**
|
||||
* Grobe Relativangabe für Listen: „gerade eben“, „vor 5 Min.“, „vor 3 Std.“,
|
||||
* „vor 2 Tagen“, „in 12 Tagen“. Ab 30 Tagen Abstand wird das Datum gezeigt, weil
|
||||
* „vor 47 Tagen“ schlechter einzuordnen ist als ein Datum.
|
||||
*
|
||||
* `jetzt` ist ein Parameter, damit Tests und statisch gerenderte Seiten einen
|
||||
* festen Bezugspunkt haben.
|
||||
*/
|
||||
export function relativ(wert: Eingabe, jetzt: Eingabe = new Date()): string {
|
||||
const abstand = alsDatum(wert).getTime() - alsDatum(jetzt).getTime();
|
||||
const betrag = Math.abs(abstand);
|
||||
const zukunft = abstand > 0;
|
||||
|
||||
if (betrag < MINUTE) return "gerade eben";
|
||||
if (betrag >= 30 * TAG) return `am ${datum(wert)}`;
|
||||
|
||||
let text: string;
|
||||
if (betrag < STUNDE) {
|
||||
text = `${Math.floor(betrag / MINUTE)} Min.`;
|
||||
} else if (betrag < TAG) {
|
||||
text = `${Math.floor(betrag / STUNDE)} Std.`;
|
||||
} else {
|
||||
const tage = Math.floor(betrag / TAG);
|
||||
// „vor“ und „in“ verlangen beide den Dativ: „vor 1 Tag“, „in 2 Tagen“.
|
||||
text = tage === 1 ? "1 Tag" : `${tage} Tagen`;
|
||||
}
|
||||
return zukunft ? `in ${text}` : `vor ${text}`;
|
||||
}
|
||||
|
||||
/** Ganze Kalendertage bis `wert` (negativ, wenn vorbei) – Grundlage für Ablaufhinweise. */
|
||||
export function tageBis(wert: Eingabe, jetzt: Eingabe = new Date()): number {
|
||||
const tagesnummer = (d: Date) => {
|
||||
const [t, m, j] = DATUM.format(d).split(".").map(Number);
|
||||
return Date.UTC(j ?? 0, (m ?? 1) - 1, t ?? 1) / TAG;
|
||||
};
|
||||
return Math.round(tagesnummer(alsDatum(wert)) - tagesnummer(alsDatum(jetzt)));
|
||||
}
|
||||
Reference in New Issue
Block a user