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,199 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireCurrentUser, UnauthorizedError } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { normalizePositiveInteger, normalizeTitle } from "@/lib/validation";
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
function normalizeOpacity(value: unknown, fallback: number): number {
|
||||
return normalizePositiveInteger(value, fallback, 20, 100);
|
||||
}
|
||||
|
||||
|
||||
function normalizeWidgetFontSize(value: unknown, fallback: number): number {
|
||||
const numberValue = typeof value === "number" ? value : Number(value);
|
||||
|
||||
if (!Number.isFinite(numberValue)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return Math.max(70, Math.min(140, Math.round(numberValue)));
|
||||
}
|
||||
|
||||
|
||||
function normalizeViewMode(value: unknown, fallback: string): string {
|
||||
if (value === "grid" || value === "list") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return fallback === "grid" ? "grid" : "list";
|
||||
}
|
||||
|
||||
async function normalizeWidgetPositions(userId: string, tabId: string | null) {
|
||||
const widgets = await prisma.widget.findMany({
|
||||
where: {
|
||||
userId,
|
||||
tabId
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
y: "asc"
|
||||
},
|
||||
{
|
||||
x: "asc"
|
||||
},
|
||||
{
|
||||
createdAt: "asc"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
widgets.map((widget, index) =>
|
||||
prisma.widget.update({
|
||||
where: {
|
||||
id: widget.id
|
||||
},
|
||||
data: {
|
||||
position: index
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function PATCH(request: Request, context: RouteContext) {
|
||||
try {
|
||||
const user = await requireCurrentUser();
|
||||
const params = await context.params;
|
||||
|
||||
const body = (await request.json().catch(() => null)) as {
|
||||
title?: unknown;
|
||||
position?: unknown;
|
||||
x?: unknown;
|
||||
y?: unknown;
|
||||
w?: unknown;
|
||||
h?: unknown;
|
||||
viewMode?: unknown;
|
||||
opacity?: unknown;
|
||||
fontSize?: unknown;
|
||||
calendarNextEventsCount?: unknown;
|
||||
} | null;
|
||||
|
||||
const existingWidget = await prisma.widget.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingWidget) {
|
||||
return NextResponse.json({ error: "Widget nicht gefunden." }, { status: 404 });
|
||||
}
|
||||
|
||||
const hasTitle = body && Object.prototype.hasOwnProperty.call(body, "title");
|
||||
const title = hasTitle ? normalizeTitle(body?.title) : existingWidget.title;
|
||||
|
||||
if (!title) {
|
||||
return NextResponse.json({ error: "Widget-Titel ist ungültig." }, { status: 400 });
|
||||
}
|
||||
|
||||
const widget = await prisma.widget.update({
|
||||
where: {
|
||||
id: existingWidget.id
|
||||
},
|
||||
data: {
|
||||
title,
|
||||
position: normalizePositiveInteger(body?.position, existingWidget.position, 0, 10000),
|
||||
x: normalizePositiveInteger(body?.x, existingWidget.x, 0, 47),
|
||||
y: normalizePositiveInteger(body?.y, existingWidget.y, 0, 40000),
|
||||
w: normalizePositiveInteger(body?.w, existingWidget.w, 1, 48),
|
||||
h: normalizePositiveInteger(body?.h, existingWidget.h, 1, 180),
|
||||
opacity: normalizeOpacity(body?.opacity, existingWidget.opacity ?? 100),
|
||||
fontSize: normalizeWidgetFontSize(body?.fontSize, existingWidget.fontSize ?? 100),
|
||||
calendarNextEventsCount: normalizePositiveInteger(
|
||||
body?.calendarNextEventsCount,
|
||||
existingWidget.calendarNextEventsCount ?? 3,
|
||||
0,
|
||||
10
|
||||
),
|
||||
viewMode: normalizeViewMode(body?.viewMode, existingWidget.viewMode ?? "list")
|
||||
}
|
||||
});
|
||||
|
||||
if (widget.type === "note") {
|
||||
await prisma.noteBoardItem.updateMany({
|
||||
where: {
|
||||
id: widget.id,
|
||||
userId: user.id
|
||||
},
|
||||
data: {
|
||||
title
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
widget
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Widget konnte nicht gespeichert werden." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: Request, context: RouteContext) {
|
||||
try {
|
||||
const user = await requireCurrentUser();
|
||||
const params = await context.params;
|
||||
|
||||
const existingWidget = await prisma.widget.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId: user.id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
tabId: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingWidget) {
|
||||
return NextResponse.json({ error: "Widget nicht gefunden." }, { status: 404 });
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.noteBoardItem.deleteMany({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId: user.id
|
||||
}
|
||||
}),
|
||||
prisma.widget.deleteMany({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
await normalizeWidgetPositions(user.id, existingWidget.tabId);
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Widget konnte nicht gelöscht werden." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user