Initial commit: Personal Dashboard

Next.js 16 dashboard with configurable widgets (favorites, notes, calendar,
clock, calculator, search, domain-check), multi-tab support, user auth,
dark mode, and Docker deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-18 10:02:05 +02:00
commit a4051ae132
74 changed files with 18317 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
export function normalizeEmail(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const email = value.trim().toLowerCase();
if (!email.includes("@") || email.length > 254) {
return null;
}
return email;
}
export function normalizePassword(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
if (value.length < 10 || value.length > 128) {
return null;
}
return value;
}
export function normalizeDisplayName(value: unknown): string | null {
if (value === null || value === undefined || value === "") {
return null;
}
if (typeof value !== "string") {
return null;
}
const displayName = value.trim();
if (displayName.length < 1 || displayName.length > 80) {
return null;
}
return displayName;
}
export function normalizeUserRole(value: unknown): "ADMIN" | "USER" | null {
if (value === "ADMIN" || value === "USER") {
return value;
}
return null;
}
export function normalizeDashboardTitle(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const title = value.trim();
if (title.length < 1 || title.length > 80) {
return null;
}
return title;
}
export function normalizeOptionalDashboardSubtitle(value: unknown): string | null {
if (value === null || value === undefined || value === "") {
return null;
}
if (typeof value !== "string") {
return null;
}
const subtitle = value.trim();
if (subtitle.length > 120) {
return null;
}
return subtitle.length > 0 ? subtitle : null;
}
export function normalizeThemeColor(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const color = value.trim();
const shortHexMatch = /^#([0-9a-fA-F]{3})$/.exec(color);
if (shortHexMatch) {
const [r, g, b] = shortHexMatch[1].split("");
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
}
if (/^#[0-9a-fA-F]{6}$/.test(color)) {
return color.toLowerCase();
}
return null;
}
export function normalizeOptionalLogoUrl(value: unknown): string | null {
if (value === null || value === undefined || value === "") {
return null;
}
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (trimmed.length < 1 || trimmed.length > 2048) {
return null;
}
if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
return trimmed;
}
try {
const url = new URL(trimmed);
if (url.protocol !== "http:" && url.protocol !== "https:") {
return null;
}
return url.toString();
} catch {
return null;
}
}
export function normalizeOptionalImageUrl(value: unknown): string | null {
if (value === null || value === undefined || value === "") {
return null;
}
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (trimmed.length < 1 || trimmed.length > 2048) {
return null;
}
if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
return trimmed;
}
try {
const url = new URL(trimmed);
if (url.protocol !== "http:" && url.protocol !== "https:") {
return null;
}
return url.toString();
} catch {
return null;
}
}
export function normalizeTitle(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const title = value.trim();
if (title.length < 1 || title.length > 80) {
return null;
}
return title;
}
export function normalizeUrl(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (trimmed.length < 3 || trimmed.length > 2048) {
return null;
}
const withProtocol =
trimmed.startsWith("http://") || trimmed.startsWith("https://") ? trimmed : `https://${trimmed}`;
try {
const url = new URL(withProtocol);
if (url.protocol !== "http:" && url.protocol !== "https:") {
return null;
}
return url.toString();
} catch {
return null;
}
}
export function normalizeOptionalUrl(value: unknown): string | null {
if (value === null || value === undefined || value === "") {
return null;
}
return normalizeUrl(value);
}
export function normalizePositiveInteger(value: unknown, fallback: number, min: number, max: number): number {
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
return fallback;
}
return parsed;
}