Add account hierarchy framework with migrations, RLS policies, and UI components

This commit is contained in:
T. Zehetbauer
2026-03-31 22:18:04 +02:00
parent 7e7da0b465
commit 59546ad6d2
262 changed files with 11671 additions and 3927 deletions

View File

@@ -14,13 +14,16 @@
"./utils": "./src/utils.ts",
"./events": "./src/events/index.tsx",
"./registry": "./src/registry/index.ts",
"./env": "./src/env/index.ts"
"./env": "./src/env/index.ts",
"./dates": "./src/dates/index.ts",
"./formatters": "./src/dates/index.ts"
},
"scripts": {
"clean": "git clean -xdf .turbo node_modules",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"date-fns": "catalog:",
"next-runtime-env": "catalog:",
"pino": "catalog:"
},

View File

@@ -0,0 +1,149 @@
import { type Locale, format, isValid, parseISO } from 'date-fns';
import { de, enUS } from 'date-fns/locale';
const LOCALES: Record<string, Locale> = {
de,
'de-DE': de,
en: enUS,
'en-US': enUS,
};
function getLocale(locale?: string): Locale {
if (locale && locale in LOCALES) {
return LOCALES[locale]!;
}
return de;
}
function safeParse(value: string | Date | null | undefined): Date | null {
if (!value) return null;
if (value instanceof Date) return value;
try {
return parseISO(value);
} catch {
return null;
}
}
/**
* Format a date for display in the UI (e.g. "31.03.2026").
* Returns '—' for null/invalid values.
*/
export function formatDate(
value: string | Date | null | undefined,
locale?: string,
): string {
const date = safeParse(value);
if (!date || !isValid(date)) return '—';
return format(date, 'P', { locale: getLocale(locale) });
}
/**
* Format a date with long month (e.g. "31. März 2026").
*/
export function formatDateLong(
value: string | Date | null | undefined,
locale?: string,
): string {
const date = safeParse(value);
if (!date || !isValid(date)) return '—';
return format(date, 'PP', { locale: getLocale(locale) });
}
/**
* Format a date with weekday and long month (e.g. "Montag, 31. März 2026").
*/
export function formatDateFull(
value: string | Date | null | undefined,
locale?: string,
): string {
const date = safeParse(value);
if (!date || !isValid(date)) return '—';
return format(date, 'EEEE, PP', { locale: getLocale(locale) });
}
/**
* Format a date with time (e.g. "31.03.2026, 14:30").
*/
export function formatDateTime(
value: string | Date | null | undefined,
locale?: string,
): string {
const date = safeParse(value);
if (!date || !isValid(date)) return '—';
return format(date, 'Pp', { locale: getLocale(locale) });
}
/**
* Format a date for HTML date input (YYYY-MM-DD).
*/
export function formatDateInput(
value: string | Date | null | undefined,
): string {
const date = safeParse(value);
if (!date || !isValid(date)) return '';
return format(date, 'yyyy-MM-dd');
}
/**
* Format just the month (short), e.g. "Mär" or "Mar".
*/
export function formatMonthShort(
value: string | Date | null | undefined,
locale?: string,
): string {
const date = safeParse(value);
if (!date || !isValid(date)) return '—';
return format(date, 'MMM', { locale: getLocale(locale) });
}
/**
* Format a number for display using locale (e.g. 1.234,56).
*/
export function formatNumber(
value: number | string | null | undefined,
locale = 'de-DE',
): string {
if (value == null) return '—';
return Number(value).toLocaleString(locale);
}
/**
* Format a currency amount (e.g. "1.234,56 €").
*/
export function formatCurrencyAmount(
value: number | string | null | undefined,
locale = 'de-DE',
currency = 'EUR',
): string {
if (value == null) return '—';
return Number(value).toLocaleString(locale, {
style: 'currency',
currency,
minimumFractionDigits: 2,
});
}
/**
* Get today's date as YYYY-MM-DD string (UTC).
*/
export function todayISO(): string {
return new Date().toISOString().split('T')[0]!;
}