Files
nebula/src/lib/utils.ts
T
florian.wildeandClaude Opus 5 c9e09a2762 feat(core): Verschluesselung, Validierung und gemeinsame Helfer
- 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>
2026-07-25 19:33:35 +02:00

97 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 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);
}