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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user