diff --git a/bun.lock b/bun.lock index d6d4d17..e3a6a4b 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "ical.js": "^2.2.1", "idb": "^8.0.3", "lucide-react": "^0.539.0", "nanoid": "^5.1.5", @@ -851,6 +852,8 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "ical.js": ["ical.js@2.2.1", "", {}, "sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg=="], + "idb": ["idb@8.0.3", "", {}, "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], diff --git a/package.json b/package.json index 0f0003e..beb630e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "ical.js": "^2.2.1", "idb": "^8.0.3", "lucide-react": "^0.539.0", "nanoid": "^5.1.5", diff --git a/src/app/page.tsx b/src/app/page.tsx index 1b72083..671f622 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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([]) @@ -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 ( -
-
+
+
- {events.length > 0 && } + {events.length > 0 && ( + <> + + + + )} +
    diff --git a/src/lib/ical.ts b/src/lib/ical.ts new file mode 100644 index 0000000..1e7b2a8 --- /dev/null +++ b/src/lib/ical.ts @@ -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(); +}