Die Farben des Tresor-Designs stehen als CSS-Variablen in :root und werden per @theme inline an Tailwind gereicht, damit Inline-SVGs und Utility-Klassen dieselbe Quelle nutzen. Schriften kommen selbst gehostet aus @fontsource, nur in den benutzten Gewichten und im Latin-Ausschnitt. src/lib/format.ts rechnet fest in Europe/Berlin: der Server laeuft im Container auf UTC, die Anzeige soll aber ueberall dieselbe Ortszeit zeigen. relativ() nimmt den Bezugspunkt als Parameter, damit statische Seiten und Tests deterministisch bleiben. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
/**
|
||
* 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)));
|
||
}
|