Files
personal-dashboard/src/app/api/widgets/[id]/route.ts
T
Claude a4051ae132 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>
2026-06-18 10:02:05 +02:00

200 lines
5.0 KiB
TypeScript

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 });
}
}