diff --git a/README.md b/README.md index 01161a0..f16e380 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,14 @@ Events, free time and sync for nine providers, including Google Calendar, Outlook and iCloud, in one typed TypeScript API. -[Website](https://www.cal-sdk.com) · [Docs](https://www.cal-sdk.com/docs) · [Quickstart](https://www.cal-sdk.com/docs/quickstart) · [FAQ](https://www.cal-sdk.com/docs/faq) +[Website](https://www.cal-sdk.com) · [Docs](https://www.cal-sdk.com/docs) · [Quickstart](https://www.cal-sdk.com/docs/quickstart) · [Playground](https://stackblitz.com/github/vkpdeveloper/calsdk/tree/main/examples/playground?file=src/main.ts) · [FAQ](https://www.cal-sdk.com/docs/faq) The website opens with a live demo. It runs in your browser on six sample calendars, using the real `listEvents`, `findFreeTime` and `createEvent`. ## Try it +To edit code without installing anything, open the [playground on StackBlitz](https://stackblitz.com/github/vkpdeveloper/calsdk/tree/main/examples/playground?file=src/main.ts). Or install it: + ```bash npm i @cal-sdk/core @cal-sdk/mock ``` diff --git a/apps/web/content/docs/quickstart.mdx b/apps/web/content/docs/quickstart.mdx index 4aec3c3..9822116 100644 --- a/apps/web/content/docs/quickstart.mdx +++ b/apps/web/content/docs/quickstart.mdx @@ -5,6 +5,8 @@ description: Create a recurring event, move one occurrence and find free time, w Start with a calendar you can't break: `@cal-sdk/mock`, which keeps events in memory. You'll create a weekday standup, move Wednesday's to later in the morning, and find a free slot around it. +To skip the install, open the [playground on StackBlitz](https://stackblitz.com/github/vkpdeveloper/calsdk/tree/main/examples/playground?file=src/main.ts): the same steps across two calendars, drawn as a week in your browser. + diff --git a/bun.lock b/bun.lock index adf558f..060a3b5 100644 --- a/bun.lock +++ b/bun.lock @@ -36,6 +36,16 @@ "tailwindcss": "4.3.4", }, }, + "examples/playground": { + "name": "calsdk-playground", + "dependencies": { + "@cal-sdk/core": "^0.1.0", + "@cal-sdk/mock": "^0.1.0", + }, + "devDependencies": { + "vite": "8.3.5", + }, + }, "examples/quickstart": { "name": "calsdk-quickstart", "dependencies": { @@ -641,6 +651,8 @@ "cac": ["cac@7.1.0", "", {}, "sha512-LMDU/U2yA4IPIY8KT0NsNPWEYZvAMkbgyspxJ6btJaYV+WMDOHAZCmB0lsTObiHaRS1V/ztKAl7+TSkTqXyeoA=="], + "calsdk-playground": ["calsdk-playground@workspace:examples/playground"], + "calsdk-quickstart": ["calsdk-quickstart@workspace:examples/quickstart"], "caniuse-lite": ["caniuse-lite@1.0.30001820", "", {}, "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w=="], diff --git a/examples/playground/README.md b/examples/playground/README.md new file mode 100644 index 0000000..46ad4f7 --- /dev/null +++ b/examples/playground/README.md @@ -0,0 +1,13 @@ +# calsdk playground + +Two in-memory calendars in a browser page: a recurring standup with one occurrence moved, events from both calendars in one week view, and the hours when both are free. No accounts or credentials. + +[Open it on StackBlitz](https://stackblitz.com/github/vkpdeveloper/calsdk/tree/main/examples/playground?file=src/main.ts), or run it from this repo: + +```sh +bun install +bun run build +bun run --cwd examples/playground dev +``` + +Edit `src/main.ts` and the page redraws. Real providers such as `google({ accessToken })` belong in server code, since a token gives full access to someone's calendar: create them behind your own API and have the page call that. The calsdk functions are the same either way. diff --git a/examples/playground/index.html b/examples/playground/index.html new file mode 100644 index 0000000..9e1c798 --- /dev/null +++ b/examples/playground/index.html @@ -0,0 +1,70 @@ + + + + + + calsdk playground + + + +

calsdk playground

+

Two in-memory calendars, one API. Edit src/main.ts and the week redraws.

+
+ + + diff --git a/examples/playground/package.json b/examples/playground/package.json new file mode 100644 index 0000000..e2b06ff --- /dev/null +++ b/examples/playground/package.json @@ -0,0 +1,15 @@ +{ + "name": "calsdk-playground", + "private": true, + "type": "module", + "scripts": { + "dev": "vite" + }, + "dependencies": { + "@cal-sdk/core": "^0.1.0", + "@cal-sdk/mock": "^0.1.0" + }, + "devDependencies": { + "vite": "8.3.5" + } +} diff --git a/examples/playground/src/main.ts b/examples/playground/src/main.ts new file mode 100644 index 0000000..a8e1162 --- /dev/null +++ b/examples/playground/src/main.ts @@ -0,0 +1,109 @@ +import { createEvent, findFreeTime, listEvents, updateEvent } from '@cal-sdk/core'; +import { mockCalendar } from '@cal-sdk/mock'; + +// Two in-memory calendars stand in for real accounts, so nothing here needs a login. +// In a real app you'd create `google({ accessToken })` or `microsoft({ accessToken })` instead, +// in server code, since a token gives full access to someone's calendar. The calls stay the same. +const timeZone = 'America/New_York'; +const at = (day: number, time: string) => ({ + dateTime: `2026-11-${String(day).padStart(2, '0')}T${time}:00-05:00`, + timeZone, +}); + +const work = mockCalendar({ + events: [ + { title: 'Design review', start: at(10, '14:00'), end: at(10, '15:30') }, + { title: 'Customer call', start: at(12, '11:00'), end: at(12, '12:00') }, + ], +}); +const home = mockCalendar({ + events: [{ title: 'Dentist', start: at(11, '12:30'), end: at(11, '13:30') }], +}); + +// A standup every weekday, then Wednesday's moved half an hour later. Only that one changes. +const { event: standup } = await createEvent({ + calendar: work, + event: { + title: 'Standup', + start: at(9, '09:30'), + end: at(9, '09:45'), + recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR'], + }, +}); + +const week = { timeMin: '2026-11-09T00:00:00-05:00', timeMax: '2026-11-14T00:00:00-05:00' }; +const workWeek = await listEvents({ calendar: work, ...week, expandRecurring: true }); +const wednesday = workWeek.events.find( + (event) => event.seriesId === standup.id && event.start.dateTime?.startsWith('2026-11-11'), +); +if (wednesday) { + await updateEvent({ + calendar: work, + eventId: wednesday.id, + patch: { start: at(11, '10:00'), end: at(11, '10:15') }, + }); +} + +// Every hour-long slot when both calendars are free, inside working hours. +const { slots } = await findFreeTime({ + calendars: [{ calendar: work }, { calendar: home }], + ...week, + duration: 60, + slotInterval: 60, + workingHours: { start: '09:00', end: '17:00', timeZone }, +}); + +const sources = [ + { calendar: work, name: 'Work', color: '#4f7cff' }, + { calendar: home, name: 'Home', color: '#f59e0b' }, +]; +const items: { start: string; title: string; color: string; free?: boolean }[] = []; +for (const { calendar, name, color } of sources) { + const { events } = await listEvents({ calendar, ...week, expandRecurring: true }); + for (const event of events) { + // An all-day event has only a date. Place it at midnight here, not in UTC, so it keeps its day. + const start = event.start.dateTime ?? `${event.start.date}T00:00:00-05:00`; + items.push({ start, title: `${event.title} (${name})`, color }); + } +} +for (const slot of slots) + items.push({ start: slot.start, title: 'Free for an hour', color: '#86efac', free: true }); + +render(items); + +function render(list: typeof items) { + const day = new Intl.DateTimeFormat('en-CA', { timeZone, dateStyle: 'short' }); + const dayName = new Intl.DateTimeFormat('en-US', { + timeZone, + weekday: 'long', + month: 'short', + day: 'numeric', + }); + const time = new Intl.DateTimeFormat('en-US', { timeZone, hour: 'numeric', minute: '2-digit' }); + + const byDay = new Map(); + for (const item of list.toSorted((a, b) => Date.parse(a.start) - Date.parse(b.start))) { + const key = day.format(new Date(item.start)); + byDay.set(key, [...(byDay.get(key) ?? []), item]); + } + const week = document.querySelector('#week'); + if (!week) return; + week.replaceChildren(); + for (const dayItems of byDay.values()) { + const column = document.createElement('section'); + column.className = 'day'; + const heading = document.createElement('h2'); + heading.textContent = dayName.format(new Date(dayItems[0]?.start ?? '')); + column.append(heading); + for (const item of dayItems) { + const row = document.createElement('div'); + row.className = item.free ? 'item free' : 'item'; + row.style.setProperty('--color', item.color); + const when = document.createElement('time'); + when.textContent = time.format(new Date(item.start)); + row.append(when, item.title); + column.append(row); + } + week.append(column); + } +}