import ical from "ical-generator"; import { NextResponse } from "next/server"; import { prisma } from "./prisma"; const DEFAULT_APP_NAME = "Vereinskalender"; const toFilename = (value: string) => value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, "") || "kalender"; export async function getIcalResponse(request: Request, token: string) { const view = await prisma.userView.findUnique({ where: { token }, include: { items: { include: { event: true } }, categories: true, exclusions: true, user: true } }); if (!view) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } const appNameSetting = await prisma.setting.findUnique({ where: { key: "app_name" } }); const appName = appNameSetting?.value || DEFAULT_APP_NAME; const url = new URL(request.url); const pastDaysParam = Number(url.searchParams.get("pastDays")); const rawPastDays = Number.isFinite(pastDaysParam) && pastDaysParam >= 0 ? pastDaysParam : Number.isFinite(view.icalPastDays) && view.icalPastDays >= 0 ? view.icalPastDays : 14; const pastDays = Math.min(365, Math.floor(rawPastDays)); const cutoff = new Date(Date.now() - pastDays * 24 * 60 * 60 * 1000); const calendar = ical({ name: appName, timezone: "Europe/Berlin" }); const excludedIds = new Set(view.exclusions.map((item) => item.eventId)); const explicitEvents = view.items .map((item) => item.event) .filter((event) => event.status === "APPROVED"); const categoryIds = view.categories.map((item) => item.categoryId); const categoryEvents = categoryIds.length > 0 ? await prisma.event.findMany({ where: { categoryId: { in: categoryIds }, status: "APPROVED" } }) : []; const combined = [...explicitEvents, ...categoryEvents].filter( (event, index, all) => all.findIndex((item) => item.id === event.id) === index && !excludedIds.has(event.id) && event.startAt >= cutoff ); combined.forEach((event) => { const start = event.startAt; const end = event.endAt || new Date(event.startAt.getTime() + 3 * 60 * 60 * 1000); calendar.createEvent({ id: event.id, summary: event.title, description: event.description || undefined, location: event.location || undefined, start, end }); }); const filename = `${toFilename(appName)}.ical`; return new NextResponse(calendar.toString(), { headers: { "Content-Type": "text/calendar; charset=utf-8", "Content-Disposition": `attachment; filename="${filename}"` } }); }