From 3d39d2327875360a24ce658a344851fbbb766347 Mon Sep 17 00:00:00 2001 From: vkpdeveloper Date: Fri, 6 Nov 2026 00:14:55 +0530 Subject: [PATCH 1/2] feat(examples): browser playground on StackBlitz A Vite page with two in-memory calendars, a recurring standup with one occurrence moved, and the free hours across both, so people can try calsdk without installing anything or connecting an account. Linked from the README and the quickstart. --- README.md | 4 +- apps/web/content/docs/quickstart.mdx | 2 + bun.lock | 12 +++ examples/playground/README.md | 12 +++ examples/playground/index.html | 70 +++++++++++++++++ examples/playground/package.json | 15 ++++ examples/playground/src/main.ts | 109 +++++++++++++++++++++++++++ 7 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 examples/playground/README.md create mode 100644 examples/playground/index.html create mode 100644 examples/playground/package.json create mode 100644 examples/playground/src/main.ts 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..4811d21 --- /dev/null +++ b/examples/playground/README.md @@ -0,0 +1,12 @@ +# 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 here: + +```sh +bun install +bun run --cwd examples/playground dev +``` + +Edit `src/main.ts` and the page redraws. To use a real calendar, replace a `mockCalendar(...)` line with `google({ accessToken })` from `@cal-sdk/google` or `microsoft({ accessToken })` from `@cal-sdk/microsoft`, and run it on a server, since tokens shouldn't reach the browser. 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..7611056 --- /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. +// For real ones, change the provider lines and nothing else: +// import { google } from '@cal-sdk/google'; const work = google({ accessToken }); +// import { microsoft } from '@cal-sdk/microsoft'; const home = microsoft({ accessToken }); +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) { + const start = event.start.dateTime ?? event.start.date ?? ''; + 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); + } +} From 1733db7776837367b32ee0fdb351b3a52997e07c Mon Sep 17 00:00:00 2001 From: vkpdeveloper Date: Fri, 6 Nov 2026 00:24:41 +0530 Subject: [PATCH 2/2] fix(examples): keep all-day events on their day and tokens off the page Place all-day events at midnight in the playground's zone rather than UTC, add the build step a fresh checkout needs, and point real providers at server code instead of the browser page. --- examples/playground/README.md | 5 +++-- examples/playground/src/main.ts | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/playground/README.md b/examples/playground/README.md index 4811d21..46ad4f7 100644 --- a/examples/playground/README.md +++ b/examples/playground/README.md @@ -2,11 +2,12 @@ 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 here: +[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. To use a real calendar, replace a `mockCalendar(...)` line with `google({ accessToken })` from `@cal-sdk/google` or `microsoft({ accessToken })` from `@cal-sdk/microsoft`, and run it on a server, since tokens shouldn't reach the browser. +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/src/main.ts b/examples/playground/src/main.ts index 7611056..a8e1162 100644 --- a/examples/playground/src/main.ts +++ b/examples/playground/src/main.ts @@ -2,9 +2,8 @@ import { createEvent, findFreeTime, listEvents, updateEvent } from '@cal-sdk/cor import { mockCalendar } from '@cal-sdk/mock'; // Two in-memory calendars stand in for real accounts, so nothing here needs a login. -// For real ones, change the provider lines and nothing else: -// import { google } from '@cal-sdk/google'; const work = google({ accessToken }); -// import { microsoft } from '@cal-sdk/microsoft'; const home = microsoft({ accessToken }); +// 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`, @@ -62,7 +61,8 @@ 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) { - const start = event.start.dateTime ?? event.start.date ?? ''; + // 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 }); } }