chore: Projektgeruest mit Next.js, TypeScript und Tailwind
Grundgeruest der Anwendung: Next.js 15 (App Router), TypeScript im strict-Modus, Tailwind mit shadcn/ui-Farbtokens. Bewusste Festlegungen: - Nur Systemschriften, keine externen Fonts, kein CDN. - Grundschrift eine Stufe groesser, Fokusring immer sichtbar, Touchziele mindestens 44px. - Ampelfarben fuer Fristen als eigene Design-Tokens. - Betraege werden als Cent-Ganzzahl gefuehrt, damit die Rechnungslegung keine Rundungsfehler erbt. - .env.example dokumentiert alle Geheimnisse; npm run schluessel erzeugt sie. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
/** Tailwind-Klassen zusammenfuehren, spaetere gewinnen. */
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
const DATUM_FORMAT = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
const DATUM_ZEIT_FORMAT = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
|
||||
const EURO_FORMAT = new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
})
|
||||
|
||||
/** 22.08.2026 - oder ein Gedankenstrich, wenn nichts da ist. */
|
||||
export function formatDatum(datum: Date | string | null | undefined): string {
|
||||
if (!datum) return '–'
|
||||
const d = typeof datum === 'string' ? new Date(datum) : datum
|
||||
if (Number.isNaN(d.getTime())) return '–'
|
||||
return DATUM_FORMAT.format(d)
|
||||
}
|
||||
|
||||
/** 22.08.2026, 14:30 */
|
||||
export function formatDatumZeit(datum: Date | string | null | undefined): string {
|
||||
if (!datum) return '–'
|
||||
const d = typeof datum === 'string' ? new Date(datum) : datum
|
||||
if (Number.isNaN(d.getTime())) return '–'
|
||||
return DATUM_ZEIT_FORMAT.format(d)
|
||||
}
|
||||
|
||||
/**
|
||||
* Betraege liegen in der Datenbank als Cent-Ganzzahl (BigInt), damit
|
||||
* keine Rundungsfehler in die Rechnungslegung wandern.
|
||||
*/
|
||||
export function formatCent(cent: bigint | number | null | undefined): string {
|
||||
if (cent === null || cent === undefined) return '–'
|
||||
const zahl = typeof cent === 'bigint' ? Number(cent) : cent
|
||||
return EURO_FORMAT.format(zahl / 100)
|
||||
}
|
||||
|
||||
/** Tage zwischen heute (Mitternacht) und einem Datum. Negativ = ueberfaellig. */
|
||||
export function tageBis(datum: Date | string): number {
|
||||
const ziel = typeof datum === 'string' ? new Date(datum) : datum
|
||||
const heute = new Date()
|
||||
heute.setHours(0, 0, 0, 0)
|
||||
const zielTag = new Date(ziel)
|
||||
zielTag.setHours(0, 0, 0, 0)
|
||||
return Math.round((zielTag.getTime() - heute.getTime()) / 86_400_000)
|
||||
}
|
||||
|
||||
/** "vor 3 Tagen", "in 2 Wochen" - fuer die Dashboard-Kacheln. */
|
||||
export function relativeTage(tage: number): string {
|
||||
if (tage === 0) return 'heute'
|
||||
if (tage === 1) return 'morgen'
|
||||
if (tage === -1) return 'gestern'
|
||||
if (tage < 0) return `vor ${Math.abs(tage)} Tagen`
|
||||
return `in ${tage} Tagen`
|
||||
}
|
||||
|
||||
/** Aus "Erika Mustermann" wird "EM" - fuer Avatare in der Navigation. */
|
||||
export function initialen(name: string): string {
|
||||
return name
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((teil) => teil[0]?.toUpperCase() ?? '')
|
||||
.join('')
|
||||
}
|
||||
Reference in New Issue
Block a user