nomadweb/frontend/src/lib/planIcs.ts
eric 9a6474a69a Wire plan to real visas (CN stay keys) and add ICS calendar export.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 03:37:52 -05:00

77 lines
2.3 KiB
TypeScript

import { rangeDates } from "@/lib/planMeta";
import type { TripItem } from "@/lib/types";
function pad(n: number) {
return String(n).padStart(2, "0");
}
function formatDate(d: Date): string {
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
}
function formatStamp(d = new Date()): string {
return `${formatDate(d)}T${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}Z`;
}
function escapeText(s: string): string {
return s.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\n/g, "\\n");
}
export interface IcsStop extends TripItem {
offset: number;
}
/** Build an ICS calendar from plan stops (requires startMonth YYYY-MM). */
export function buildPlanIcs(opts: {
title: string;
startMonth: string;
stops: IcsStop[];
}): string | null {
if (!opts.startMonth || opts.stops.length === 0) return null;
const now = formatStamp();
const lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//nomadro//Move Plan//CN",
"CALSCALE:GREGORIAN",
"METHOD:PUBLISH",
`X-WR-CALNAME:${escapeText(opts.title || "nomadro 旅居计划")}`,
];
opts.stops.forEach((stop, i) => {
const range = rangeDates(opts.startMonth, stop.offset, stop.months);
if (!range) return;
const summary = `${stop.emoji} ${stop.name}, ${stop.country}`;
const descParts = [
`停留 ${stop.months} 个月`,
`约 ¥${(stop.cost * stop.months).toLocaleString()}`,
stop.note ? `备注:${stop.note}` : "",
"由 nomadro 旅居计划中心生成",
].filter(Boolean);
lines.push(
"BEGIN:VEVENT",
`UID:nomadro-${stop.slug}-${i}@nomadro.com`,
`DTSTAMP:${now}`,
`DTSTART;VALUE=DATE:${formatDate(range.start)}`,
`DTEND;VALUE=DATE:${formatDate(range.endExclusive)}`,
`SUMMARY:${escapeText(summary)}`,
`DESCRIPTION:${escapeText(descParts.join(" · "))}`,
`LOCATION:${escapeText(`${stop.name}, ${stop.country}`)}`,
"END:VEVENT"
);
});
lines.push("END:VCALENDAR");
return lines.join("\r\n");
}
export function downloadPlanIcs(filename: string, ics: string) {
const blob = new Blob([ics], { type: "text/calendar;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}