phase 3 - ical import/export
This commit is contained in:
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { type CalendarEvent } from '@/lib/types'
|
||||
|
||||
import { addEvent, deleteEvent, getAllEvents, clearEvents } from '@/lib/db'
|
||||
import { parseICS, generateICS } from '@/lib/ical'
|
||||
|
||||
export default function HomePage() {
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([])
|
||||
@@ -15,7 +16,6 @@ export default function HomePage() {
|
||||
const [title, setTitle] = useState('')
|
||||
const [start, setStart] = useState('')
|
||||
|
||||
// Load events only in the browser
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const stored = await getAllEvents()
|
||||
@@ -42,11 +42,61 @@ export default function HomePage() {
|
||||
setEvents([])
|
||||
}
|
||||
|
||||
// --- IMPORT ---
|
||||
const handleImport = async (file: File) => {
|
||||
const text = await file.text()
|
||||
const parsed = parseICS(text)
|
||||
|
||||
// Save to DB and update state
|
||||
for (const ev of parsed) {
|
||||
await addEvent(ev)
|
||||
}
|
||||
const stored = await getAllEvents()
|
||||
setEvents(stored)
|
||||
}
|
||||
|
||||
// --- EXPORT ---
|
||||
const handleExport = () => {
|
||||
const icsData = generateICS(events)
|
||||
const blob = new Blob([icsData], { type: 'text/calendar' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'events.ics'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button onClick={() => setOpen(true)}>Add Event</Button>
|
||||
{events.length > 0 && <Button variant="destructive" onClick={handleClearAll}>Clear All</Button>}
|
||||
{events.length > 0 && (
|
||||
<>
|
||||
<Button variant="secondary" onClick={handleExport}>
|
||||
Export .ics
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleClearAll}>
|
||||
Clear All
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<label className="cursor-pointer">
|
||||
<span className="px-3 py-2 bg-blue-500 text-white rounded">Import .ics</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".ics"
|
||||
hidden
|
||||
onChange={e => {
|
||||
if (e.target.files?.length) {
|
||||
handleImport(e.target.files[0])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ul className="mt-4 space-y-2">
|
||||
|
||||
43
src/lib/ical.ts
Normal file
43
src/lib/ical.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import ICAL from "ical.js";
|
||||
import type { CalendarEvent } from "@/lib/types";
|
||||
|
||||
export function parseICS(icsString: string): CalendarEvent[] {
|
||||
const jcalData = ICAL.parse(icsString);
|
||||
const comp = new ICAL.Component(jcalData);
|
||||
const vevents = comp.getAllSubcomponents("vevent");
|
||||
|
||||
return vevents.map((v) => {
|
||||
const ev = new ICAL.Event(v);
|
||||
return {
|
||||
id: ev.uid || crypto.randomUUID(),
|
||||
title: ev.summary || "Untitled Event",
|
||||
start: ev.startDate.toJSDate().toISOString().split("T")[0],
|
||||
end: ev.endDate
|
||||
? ev.endDate.toJSDate().toISOString().split("T")[0]
|
||||
: undefined,
|
||||
description: ev.description || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function generateICS(events: CalendarEvent[]): string {
|
||||
const comp = new ICAL.Component(["vcalendar", [], []]);
|
||||
comp.addPropertyWithValue("version", "2.0");
|
||||
comp.addPropertyWithValue("prodid", "-//YourAppName//EN");
|
||||
|
||||
events.forEach((ev) => {
|
||||
const vevent = new ICAL.Component("vevent");
|
||||
vevent.addPropertyWithValue("uid", ev.id);
|
||||
vevent.addPropertyWithValue("summary", ev.title);
|
||||
vevent.addPropertyWithValue("dtstart", ICAL.Time.fromDateString(ev.start));
|
||||
if (ev.end) {
|
||||
vevent.addPropertyWithValue("dtend", ICAL.Time.fromDateString(ev.end));
|
||||
}
|
||||
if (ev.description) {
|
||||
vevent.addPropertyWithValue("description", ev.description);
|
||||
}
|
||||
comp.addSubcomponent(vevent);
|
||||
});
|
||||
|
||||
return comp.toString();
|
||||
}
|
||||
Reference in New Issue
Block a user