Skip to main content

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
Users can export these to:

ICS File Generation

The core calendar functionality generates RFC 5545 compliant ICS files.
// 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.
// 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.
// 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

// 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

// 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

// 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

// 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

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

Next Steps