- crypto.ts: AES-256-GCM fuer die Git-Access-Tokens, Schluessel per scrypt aus APP_SECRET abgeleitet. Chiffrat ist versioniert (v1.<iv>.<tag>.<data>), damit ein spaeterer Formatwechsel moeglich bleibt. Fehlt APP_SECRET, warnt die App und faellt auf einen Entwicklungsschluessel zurueck. - validation.ts: Zod-Schemas fuer alle Eingaben an der API-Grenze, inklusive Umwandlung leerer Strings zu null und Feldfehlern fuer die Oberflaeche. - status.ts: Metadaten zu Status, Prioritaeten und Akzentfarben an einer Stelle, damit Board, Karte und Detailseite nicht auseinanderlaufen. - utils.ts: Slug-Erzeugung, deutsche Datumsformate mit fester Zeitzone (verhindert Abweichungen zwischen Server-Render und Hydration) und relative Zeitangaben. - projects.ts: wiederverwendete Prisma-Selects. Die Verbindungsauswahl laesst tokenCipher bewusst weg, damit kein Token in eine Client-Komponente gelangt. - db.ts: Prisma-Client mit Cache ueber globalThis gegen Verbindungslecks beim Hot Reload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
/** Kleine Helfer ohne Abhaengigkeiten. */
|
||
|
||
export function cn(...classes: Array<string | false | null | undefined>): string {
|
||
return classes.filter(Boolean).join(" ");
|
||
}
|
||
|
||
export function slugify(input: string): string {
|
||
return input
|
||
.toLowerCase()
|
||
.replace(/ä/g, "ae")
|
||
.replace(/ö/g, "oe")
|
||
.replace(/ü/g, "ue")
|
||
.replace(/ß/g, "ss")
|
||
.normalize("NFD")
|
||
.replace(/[̀-ͯ]/g, "")
|
||
.replace(/[^a-z0-9]+/g, "-")
|
||
.replace(/^-+|-+$/g, "")
|
||
.slice(0, 60);
|
||
}
|
||
|
||
const DATE_FORMAT = new Intl.DateTimeFormat("de-DE", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
timeZone: "Europe/Berlin",
|
||
});
|
||
|
||
const DATETIME_FORMAT = new Intl.DateTimeFormat("de-DE", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
timeZone: "Europe/Berlin",
|
||
});
|
||
|
||
export function formatDate(value: Date | string | null | undefined): string {
|
||
if (!value) return "–";
|
||
const date = typeof value === "string" ? new Date(value) : value;
|
||
if (Number.isNaN(date.getTime())) return "–";
|
||
return DATE_FORMAT.format(date);
|
||
}
|
||
|
||
export function formatDateTime(value: Date | string | null | undefined): string {
|
||
if (!value) return "–";
|
||
const date = typeof value === "string" ? new Date(value) : value;
|
||
if (Number.isNaN(date.getTime())) return "–";
|
||
return DATETIME_FORMAT.format(date);
|
||
}
|
||
|
||
/**
|
||
* "vor 3 Tagen". Bewusst grob gerundet – kleine Abweichungen zwischen
|
||
* Server-Render und Hydration fallen damit nicht auf.
|
||
*/
|
||
export function formatRelative(value: Date | string | null | undefined): string {
|
||
if (!value) return "nie";
|
||
const date = typeof value === "string" ? new Date(value) : value;
|
||
if (Number.isNaN(date.getTime())) return "nie";
|
||
|
||
const seconds = Math.round((Date.now() - date.getTime()) / 1000);
|
||
if (seconds < 60) return "gerade eben";
|
||
const minutes = Math.round(seconds / 60);
|
||
if (minutes < 60) return `vor ${minutes} Min.`;
|
||
const hours = Math.round(minutes / 60);
|
||
if (hours < 24) return `vor ${hours} Std.`;
|
||
const days = Math.round(hours / 24);
|
||
if (days < 31) return `vor ${days} ${days === 1 ? "Tag" : "Tagen"}`;
|
||
const months = Math.round(days / 30);
|
||
if (months < 12) return `vor ${months} Mon.`;
|
||
return `vor ${Math.round(months / 12)} J.`;
|
||
}
|
||
|
||
export function truncate(value: string, max = 120): string {
|
||
if (value.length <= max) return value;
|
||
return `${value.slice(0, max - 1).trimEnd()}…`;
|
||
}
|
||
|
||
export function firstLine(value: string | null | undefined): string {
|
||
if (!value) return "";
|
||
return value.split("\n")[0] ?? "";
|
||
}
|
||
|
||
export function parseTags(input: string): string[] {
|
||
return Array.from(
|
||
new Set(
|
||
input
|
||
.split(",")
|
||
.map((tag) => tag.trim().toLowerCase())
|
||
.filter(Boolean),
|
||
),
|
||
).slice(0, 12);
|
||
}
|
||
|
||
export function formatNumber(value: number): string {
|
||
return new Intl.NumberFormat("de-DE").format(value);
|
||
}
|