Calendar Integration
Never miss a renewal deadline by adding benefit dates to your preferred calendar app.
Overview
Pathfinder generates calendar events for:- Redetermination dates: When benefits need to be renewed
- Document expiration dates: When documents need to be replaced
- Application deadlines: When applications must be submitted
ICS File Generation
The core calendar functionality generates RFC 5545 compliant ICS files.Copy
// lib/calendar/ics-generator.ts
interface CalendarEvent {
uid: string; // Unique identifier
title: string; // Event title
description?: string; // Event description
startDate: Date; // Event date
endDate?: Date; // Optional end date (defaults to same day)
location?: string; // Physical or URL location
url?: string; // Link to more info
reminders?: Reminder[]; // VALARM reminders
}
interface Reminder {
type: "email" | "display";
daysBefore: number;
}
export function generateICS(event: CalendarEvent): string {
const lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Pathfinder//Benefits Calendar//EN",
"CALSCALE:GREGORIAN",
"METHOD:PUBLISH",
"",
"BEGIN:VEVENT",
`UID:${event.uid}@pathfinder.withunify.org`,
`DTSTAMP:${formatICSDate(new Date())}`,
`DTSTART;VALUE=DATE:${formatICSDate(event.startDate, true)}`,
`DTEND;VALUE=DATE:${formatICSDate(event.endDate || event.startDate, true)}`,
`SUMMARY:${escapeICS(event.title)}`,
];
if (event.description) {
lines.push(`DESCRIPTION:${escapeICS(event.description)}`);
}
if (event.location) {
lines.push(`LOCATION:${escapeICS(event.location)}`);
}
if (event.url) {
lines.push(`URL:${event.url}`);
}
// Add reminders
for (const reminder of event.reminders || []) {
lines.push(
"",
"BEGIN:VALARM",
`ACTION:${reminder.type.toUpperCase()}`,
`TRIGGER:-P${reminder.daysBefore}D`,
`DESCRIPTION:${escapeICS(event.title)}`,
"END:VALARM"
);
}
lines.push("END:VEVENT", "", "END:VCALENDAR");
return lines.join("\r\n");
}
function formatICSDate(date: Date, dateOnly = false): string {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
const day = String(date.getUTCDate()).padStart(2, "0");
if (dateOnly) {
return `${year}${month}${day}`;
}
const hours = String(date.getUTCHours()).padStart(2, "0");
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
}
function escapeICS(text: string): string {
return text
.replace(/\\/g, "\\\\")
.replace(/,/g, "\\,")
.replace(/;/g, "\\;")
.replace(/\n/g, "\\n");
}
Calendar URL Generators
For web-based calendars, generate direct add URLs.Copy
// lib/calendar/google-calendar.ts
interface CalendarURLParams {
title: string;
description?: string;
location?: string;
startDate: Date;
endDate?: Date;
}
export function generateGoogleCalendarURL(params: CalendarURLParams): string {
const { title, description, location, startDate, endDate } = params;
const start = formatGoogleDate(startDate);
const end = formatGoogleDate(endDate || startDate);
const url = new URL("https://calendar.google.com/calendar/render");
url.searchParams.set("action", "TEMPLATE");
url.searchParams.set("text", title);
url.searchParams.set("dates", `${start}/${end}`);
if (description) {
url.searchParams.set("details", description);
}
if (location) {
url.searchParams.set("location", location);
}
return url.toString();
}
export function generateOutlookCalendarURL(params: CalendarURLParams): string {
const { title, description, location, startDate, endDate } = params;
const url = new URL("https://outlook.live.com/calendar/0/action/compose");
url.searchParams.set("subject", title);
url.searchParams.set("startdt", startDate.toISOString());
url.searchParams.set("enddt", (endDate || startDate).toISOString());
url.searchParams.set("allday", "true");
if (description) {
url.searchParams.set("body", description);
}
if (location) {
url.searchParams.set("location", location);
}
return url.toString();
}
export function generateYahooCalendarURL(params: CalendarURLParams): string {
const { title, description, location, startDate } = params;
const url = new URL("https://calendar.yahoo.com/");
url.searchParams.set("v", "60");
url.searchParams.set("title", title);
url.searchParams.set("st", formatYahooDate(startDate));
url.searchParams.set("dur", "allday");
if (description) {
url.searchParams.set("desc", description);
}
if (location) {
url.searchParams.set("in_loc", location);
}
return url.toString();
}
function formatGoogleDate(date: Date): string {
return date.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
}
function formatYahooDate(date: Date): string {
return date.toISOString().replace(/[-:]/g, "").split("T")[0];
}
Add to Calendar Component
Dropdown component for adding events to any calendar.Copy
// components/calendar/add-to-calendar.tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@unify/ui";
import { Calendar, Download } from "lucide-react";
interface AddToCalendarProps {
event: CalendarEvent;
triggerClassName?: string;
}
export function AddToCalendar({ event, triggerClassName }: AddToCalendarProps) {
const handleDownloadICS = () => {
const icsContent = generateICS(event);
const blob = new Blob([icsContent], { type: "text/calendar;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${event.title.replace(/\s+/g, "-")}.ics`;
link.click();
URL.revokeObjectURL(url);
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className={triggerClassName}>
<Calendar className="h-4 w-4 mr-2" />
Add to Calendar
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<a
href={generateGoogleCalendarURL(event)}
target="_blank"
rel="noopener noreferrer"
>
<img src="/icons/google-calendar.svg" className="h-4 w-4 mr-2" />
Google Calendar
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a
href={generateOutlookCalendarURL(event)}
target="_blank"
rel="noopener noreferrer"
>
<img src="/icons/outlook.svg" className="h-4 w-4 mr-2" />
Outlook Calendar
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a
href={generateYahooCalendarURL(event)}
target="_blank"
rel="noopener noreferrer"
>
<img src="/icons/yahoo.svg" className="h-4 w-4 mr-2" />
Yahoo Calendar
</a>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleDownloadICS}>
<Download className="h-4 w-4 mr-2" />
Download .ics file
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Preset Components
Pre-configured components for common calendar scenarios.Renewal Calendar
Copy
// components/calendar/add-renewal-to-calendar.tsx
export function AddRenewalToCalendar({
application,
}: {
application: Application;
}) {
if (!application.renewalDate) return null;
const event: CalendarEvent = {
uid: `renewal-${application.id}`,
title: `Renew ${application.programName} Benefits`,
description: [
`Your ${application.programName} benefits need to be renewed.`,
"",
`Application ID: ${application.id}`,
`Renewal Deadline: ${formatDate(application.renewalDate)}`,
"",
"Visit Pathfinder to start your renewal:",
`https://pathfinder.withunify.org/applications/${application.id}/renew`,
].join("\n"),
startDate: application.renewalDate,
url: `https://pathfinder.withunify.org/applications/${application.id}`,
reminders: [
{ type: "display", daysBefore: 30 },
{ type: "display", daysBefore: 14 },
{ type: "display", daysBefore: 7 },
{ type: "display", daysBefore: 1 },
],
};
return <AddToCalendar event={event} />;
}
Document Expiration Calendar
Copy
// components/calendar/add-document-expiration-to-calendar.tsx
export function AddDocumentExpirationToCalendar({
document,
}: {
document: Document;
}) {
if (!document.expirationDate) return null;
const event: CalendarEvent = {
uid: `doc-expiry-${document.id}`,
title: `${document.documentType} Expires`,
description: [
`Your ${document.documentType} expires on this date.`,
"",
"Upload a new document before expiration to avoid issues with your applications.",
"",
"Manage documents:",
"https://pathfinder.withunify.org/documents",
].join("\n"),
startDate: document.expirationDate,
url: "https://pathfinder.withunify.org/documents",
reminders: [
{ type: "display", daysBefore: 30 },
{ type: "display", daysBefore: 7 },
],
};
return <AddToCalendar event={event} />;
}
Server Action for Scheduled Reminders
Copy
// app/actions/calendar-reminders.ts
"use server";
import { safeAuth } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/dal";
import type { ActionResult } from "@/types/api";
export async function scheduleCalendarReminder(
applicationId: string,
reminderDate: Date
): Promise<ActionResult<{ id: string }>> {
const { user } = await safeAuth();
if (!user) {
return { success: false, error: "Not authenticated" };
}
// Verify application belongs to user
const { data: application } = await supabaseAdmin
.from("applications")
.select("id, user_id, program_name, renewal_date")
.eq("id", applicationId)
.single();
if (!application || application.user_id !== user.id) {
return { success: false, error: "Application not found" };
}
// Create notification schedule
const { data, error } = await supabaseAdmin
.from("scheduled_notifications")
.insert({
user_id: user.id,
type: "calendar_reminder",
scheduled_for: reminderDate.toISOString(),
payload: {
applicationId,
programName: application.program_name,
renewalDate: application.renewal_date,
},
})
.select("id")
.single();
if (error) {
return { success: false, error: error.message };
}
return { success: true, data: { id: data.id } };
}
Usage Examples
On Application Detail Page
Copy
// app/(dashboard)/applications/[id]/page.tsx
export default async function ApplicationDetailPage({ params }) {
const application = await getApplication(params.id);
return (
<div>
<h1>{application.programName}</h1>
{application.renewalDate && (
<div className="mt-6 p-4 bg-amber-50 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium">Renewal Due</h3>
<p className="text-sm text-stone-500">
{formatDate(application.renewalDate)}
</p>
</div>
<AddRenewalToCalendar application={application} />
</div>
</div>
)}
</div>
);
}
On Documents Page
Copy
// components/documents/document-card.tsx
export function DocumentCard({ document }) {
const daysUntilExpiry = document.expirationDate
? differenceInDays(document.expirationDate, new Date())
: null;
return (
<Card>
<CardContent>
<h3>{document.documentType}</h3>
{document.expirationDate && (
<div className="mt-4 flex items-center justify-between">
<div className={cn(
"text-sm",
daysUntilExpiry < 30 && "text-amber-600",
daysUntilExpiry < 7 && "text-red-600"
)}>
{daysUntilExpiry > 0
? `Expires in ${daysUntilExpiry} days`
: "Expired"}
</div>
<AddDocumentExpirationToCalendar document={document} />
</div>
)}
</CardContent>
</Card>
);
}