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
+150
View File
@@ -0,0 +1,150 @@
"use client";
import type { ComponentType, ReactNode } from "react";
import { useMemo } from "react";
import ReactGridLayoutBase, { WidthProvider } from "react-grid-layout/legacy";
import type { DashboardGridWidget, DashboardLayoutItem } from "@/lib/dashboard-layout";
export type DashboardGridProps = {
widgets: DashboardGridWidget[];
editMode: boolean;
activeMenuWidgetId?: string | null;
renderWidget: (widget: DashboardGridWidget) => ReactNode;
onLayoutChange: (layout: DashboardLayoutItem[]) => void;
};
const WidthAwareGridLayout = WidthProvider(ReactGridLayoutBase) as ComponentType<any>;
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
function getWidgetMinimumSize(widget: DashboardGridWidget): { minW: number; minH: number } {
if (widget.type === "search") {
return {
minW: 8,
minH: 5
};
}
if (widget.type === "clock") {
return {
minW: 4,
minH: 4
};
}
if (widget.type === "calendar") {
return {
minW: 8,
minH: 6
};
}
if (widget.type === "calculator") {
return {
minW: 8,
minH: 6
};
}
if (widget.type === "note") {
return {
minW: 4,
minH: 5
};
}
return {
minW: 4,
minH: 4
};
}
function widgetsToLayout(widgets: DashboardGridWidget[]): DashboardLayoutItem[] {
return widgets.map((widget) => {
const minimumSize = getWidgetMinimumSize(widget);
const width = clamp(widget.w, minimumSize.minW, 48);
const height = clamp(widget.h, minimumSize.minH, 180);
const x = clamp(widget.x, 0, 48 - width);
const y = clamp(widget.y, 0, 10000);
return {
i: widget.id,
x,
y,
w: width,
h: height,
minW: minimumSize.minW,
minH: minimumSize.minH,
maxW: 48,
maxH: 180
};
});
}
function normalizeLayout(layout: readonly DashboardLayoutItem[]): DashboardLayoutItem[] {
return layout.map((item) => ({
i: String(item.i),
x: Number.isFinite(item.x) ? item.x : 0,
y: Number.isFinite(item.y) ? item.y : 0,
w: Number.isFinite(item.w) ? item.w : 1,
h: Number.isFinite(item.h) ? item.h : 1,
minW: item.minW,
minH: item.minH,
maxW: item.maxW,
maxH: item.maxH
}));
}
export default function DashboardGrid({
widgets,
editMode,
activeMenuWidgetId = null,
renderWidget,
onLayoutChange
}: DashboardGridProps) {
const layout = useMemo(() => widgetsToLayout(widgets), [widgets]);
return (
<div className="widgetGridShell">
{widgets.length === 0 ? (
<div className="emptyDashboard">
<h2>Keine Widgets aktiv</h2>
<p className="muted">Schalte den Bearbeitungsmodus ein und füge Widgets hinzu.</p>
</div>
) : null}
{widgets.length > 0 ? (
<WidthAwareGridLayout
className="widgetGrid"
layout={layout}
cols={48}
rowHeight={8}
margin={[12, 12]}
containerPadding={[0, 0]}
compactType={null}
preventCollision={true}
isBounded={false}
autoSize={true}
isDraggable={editMode}
isResizable={editMode}
draggableHandle=".widgetDragHandle"
draggableCancel=".widgetNoDrag"
resizeHandles={["se"]}
measureBeforeMount={true}
onLayoutChange={(nextLayout: DashboardLayoutItem[]) => onLayoutChange(normalizeLayout(nextLayout))}
>
{widgets.map((widget) => (
<div
key={widget.id}
className={activeMenuWidgetId === widget.id ? "gridItemMenuOpen" : ""}
>
{renderWidget(widget)}
</div>
))}
</WidthAwareGridLayout>
) : null}
</div>
);
}