import type { ModuleManifest, WidgetContext } from "../_core/module"; import { z } from "zod"; import { listCalendars, listEvents, searchCalendars, searchEvents } from "./server/queries"; const calendarIdsSchema = z.union([z.literal("all"), z.array(z.string().uuid())]); const upcomingConfigSchema = z.object({ calendarIds: calendarIdsSchema, days: z.number().int().min(1).max(30), }); const monthConfigSchema = z.object({ calendarIds: calendarIdsSchema }); async function UpcomingEventsWidget({ config, }: { config: unknown; ctx: WidgetContext; }) { const parsed = upcomingConfigSchema.parse(config); const now = new Date(); const end = new Date(now.getTime() + parsed.days * 24 * 60 * 60 * 1000); const events = await listEvents({ from: now, to: end, calendarIds: parsed.calendarIds }); if (events.length === 0) { return (

No events in the next {parsed.days} day{parsed.days !== 1 ? "s" : ""}

); } return ( ); } async function MonthWidget({ config }: { config: unknown; ctx: WidgetContext }) { const parsed = monthConfigSchema.parse(config); const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); const events = await listEvents({ from: monthStart, to: monthEnd, calendarIds: parsed.calendarIds }); const monthName = now.toLocaleDateString(undefined, { month: "long", year: "numeric" }); return (

{monthName}

{events.length === 0 ? (

No events this month

) : ( )}
); } const manifest: ModuleManifest = { id: "calendar", name: "Calendar", nav: { href: "/calendar", label: "Calendar", icon: "calendar" }, entities: [ { type: "calendar.calendar", label: { singular: "Calendar", plural: "Calendars" }, share: { canShare: true, defaultCapabilities: ["read"] }, search: { search: searchCalendars }, resolveUrl: (id) => `/calendar?id=${id}`, renderActivity: (entry) => { const name = entry.payload?.name as string | undefined; if (entry.action === "create") return `Created calendar${name ? ` "${name}"` : ""}`; if (entry.action === "delete") return "Deleted calendar"; return `Updated calendar${name ? ` "${name}"` : ""}`; }, }, { type: "calendar.event", label: { singular: "Event", plural: "Events" }, share: { canShare: true, defaultCapabilities: ["read"] }, reminder: { canRemind: true }, search: { search: searchEvents }, resolveUrl: (id) => `/calendar/events/${id}`, renderActivity: (entry) => { const title = entry.payload?.title as string | undefined; if (entry.action === "create") return `Created event${title ? ` "${title}"` : ""}`; if (entry.action === "delete") return "Deleted event"; return `Updated event${title ? ` "${title}"` : ""}`; }, }, ], dashboardWidgets: [ { id: "calendar.upcoming", title: "Upcoming events", description: "Events from selected calendars over the next few days.", category: "Calendar", defaultSize: { w: 4, h: 3 }, minSize: { w: 3, h: 2 }, defaultPriority: 10, configSchema: upcomingConfigSchema, defaultConfig: { calendarIds: "all", days: 3 }, resolveConfigOptions: async () => ({ calendars: (await listCalendars()).map((calendar) => ({ id: calendar.id, name: calendar.name, visibility: calendar.visibility, })), }), render: (props) => , }, { id: "calendar.month", title: "Month calendar", description: "A compact month view for selected calendars.", category: "Calendar", defaultSize: { w: 6, h: 5 }, minSize: { w: 4, h: 4 }, defaultPriority: 20, configSchema: monthConfigSchema, defaultConfig: { calendarIds: "all" }, resolveConfigOptions: async () => ({ calendars: (await listCalendars()).map((calendar) => ({ id: calendar.id, name: calendar.name, visibility: calendar.visibility, })), }), render: (props) => , }, ], quickAdds: [ { id: "calendar.new-event", label: "New event", icon: "calendar-plus", url: "/calendar", }, { id: "calendar.new-calendar", label: "New calendar", icon: "calendar-days", url: "/calendar", }, ], }; export default manifest;