Repository navigation
feat(examples): browser playground on StackBlitz #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>calsdk playground</title> | ||
| <style> | ||
| :root { | ||
| color-scheme: dark; | ||
| font-family: ui-sans-serif, system-ui, sans-serif; | ||
| background: #09090a; | ||
| color: #e5e5e5; | ||
| } | ||
| body { | ||
| margin: 0; | ||
| padding: 24px 16px 48px; | ||
| } | ||
| h1 { | ||
| margin: 0 0 4px; | ||
| font-size: 20px; | ||
| } | ||
| p { | ||
| margin: 0 0 20px; | ||
| color: #a3a3a3; | ||
| font-size: 14px; | ||
| } | ||
| #week { | ||
| display: grid; | ||
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | ||
| gap: 8px; | ||
| } | ||
| .day { | ||
| border: 1px solid #222226; | ||
| border-radius: 10px; | ||
| padding: 10px; | ||
| background: #0f0f11; | ||
| } | ||
| .day h2 { | ||
| margin: 0 0 8px; | ||
| font-size: 13px; | ||
| color: #a3a3a3; | ||
| font-weight: 500; | ||
| } | ||
| .item { | ||
| margin-top: 6px; | ||
| padding: 6px 8px; | ||
| border-radius: 6px; | ||
| border-left: 3px solid var(--color); | ||
| background: #18181b; | ||
| font-size: 13px; | ||
| } | ||
| .item time { | ||
| display: block; | ||
| font: 11px ui-monospace, monospace; | ||
| color: #a3a3a3; | ||
| } | ||
| .free { | ||
| border-left-style: dashed; | ||
| background: transparent; | ||
| color: #86efac; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>calsdk playground</h1> | ||
| <p>Two in-memory calendars, one API. Edit <code>src/main.ts</code> and the week redraws.</p> | ||
| <div id="week"></div> | ||
| <script type="module" src="/src/main.ts"></script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, typeof list>(); | ||
| 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); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: vkpdeveloper/calsdk
Length of output: 3212
Script executed:
Repository: vkpdeveloper/calsdk
Length of output: 5789
Script executed:
Repository: vkpdeveloper/calsdk
Length of output: 2613
Web query:
Vite 8 resolve.conditions default conditions package.json exports custom conditionResult:
Build the SDK packages before running the playground.
Bun links
@cal-sdk/coreand@cal-sdk/mockto the local workspace packages. Their exports point todist/, which is gitignored and not built bybun install. On a fresh checkout, Vite cannot resolve these imports, so the page does not load.Suggested README change
bun install +bun run build bun run --cwd examples/playground dev