phase 3 - ical import/export
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -8,6 +8,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"ical.js": "^2.2.1",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
@@ -851,6 +852,8 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"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=="],
|
"idb": ["idb@8.0.3", "", {}, "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="],
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"ical.js": "^2.2.1",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { type CalendarEvent } from '@/lib/types'
|
import { type CalendarEvent } from '@/lib/types'
|
||||||
|
|
||||||
import { addEvent, deleteEvent, getAllEvents, clearEvents } from '@/lib/db'
|
import { addEvent, deleteEvent, getAllEvents, clearEvents } from '@/lib/db'
|
||||||
|
import { parseICS, generateICS } from '@/lib/ical'
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [events, setEvents] = useState<CalendarEvent[]>([])
|
const [events, setEvents] = useState<CalendarEvent[]>([])
|
||||||
@@ -15,7 +16,6 @@ export default function HomePage() {
|
|||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [start, setStart] = useState('')
|
const [start, setStart] = useState('')
|
||||||
|
|
||||||
// Load events only in the browser
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const stored = await getAllEvents()
|
const stored = await getAllEvents()
|
||||||
@@ -42,11 +42,61 @@ export default function HomePage() {
|
|||||||
setEvents([])
|
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 (
|
return (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<Button onClick={() => setOpen(true)}>Add Event</Button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<ul className="mt-4 space-y-2">
|
<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