feat(calendar): add feed subscription, bulk export, share & Google Cal
- /api/kalender.ics SSR route: ICS feed of all calendar items (webcal-subscribable, Cache 1h) - buildICSFeed() / downloadICSFeed() for multi-event ICS export - googleCalUrl() deep-link pre-fills Google Calendar create form - Web Share API per event (navigator.share on mobile, clipboard fallback on desktop) — "Kopiert\!" toast per event for 2s after clipboard write - Actions bar (subscribe + bulk export) only rendered after onMount — Apple platforms: webcal:// link opens Calendar.app natively — All others: direct ICS download via <a download> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { getCalendarItems } from '$lib/cms';
|
||||
import { buildICSFeed, type CalendarItemICS } from '$lib/calendar-ics';
|
||||
import type { CalendarItemData } from '$lib/block-types';
|
||||
|
||||
function parseDate(iso: string | undefined | null): Date | null {
|
||||
if (!iso) return null;
|
||||
const d = new Date(iso);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
function locationText(loc: unknown): string {
|
||||
if (typeof loc === 'string') return loc;
|
||||
if (loc && typeof loc === 'object') {
|
||||
const t = (loc as { text?: unknown }).text;
|
||||
if (typeof t === 'string') return t;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const siteName = env.PUBLIC_SITE_NAME || 'Windwiderstand';
|
||||
|
||||
let rawItems: Awaited<ReturnType<typeof getCalendarItems>> = [];
|
||||
try {
|
||||
rawItems = await getCalendarItems();
|
||||
} catch {
|
||||
rawItems = [];
|
||||
}
|
||||
|
||||
const icsItems: CalendarItemICS[] = rawItems
|
||||
.flatMap((raw) => {
|
||||
const item = raw as unknown as CalendarItemData;
|
||||
const start = parseDate(item.terminZeit);
|
||||
if (!start) return [];
|
||||
const end = parseDate(item.terminEnde ?? null) ?? undefined;
|
||||
const entry: CalendarItemICS = {
|
||||
title: item.title || 'Termin',
|
||||
start,
|
||||
end,
|
||||
description: (item as { description?: string }).description ?? undefined,
|
||||
location: locationText((item as { location?: unknown }).location) || undefined,
|
||||
};
|
||||
return [entry];
|
||||
});
|
||||
|
||||
const body = buildICSFeed(icsItems, `${siteName} – Termine`);
|
||||
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
'Content-Type': 'text/calendar; charset=utf-8',
|
||||
'Content-Disposition': 'inline; filename="windwiderstand-termine.ics"',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user