Event sources

Merge several sources into one calendar. Each source is refetched for the visible range when the user navigates, and the instance exposes a loading flag while any source is pending.

EventSources.tsx
EventSources.tsx
import {
  Calendar,
  DayGrid,
  List,
  TimeGrid,
  useCalendar,
  useCalendarController,
  type EventInput,
  type EventSourceInput,
  type FetchInfo,
} from 'z-cal/react'
import type { JSX } from 'react'
import { useScheme } from '../DemoFrame'
import { today, todayDate } from '../shared/dates'

const PLUGINS = [DayGrid, TimeGrid, List]

// Source 1: a static array. Wrapped as a function source so it merges with the others
// (`events` alone is only consulted when there are no `eventSources`).
const STATIC: EventInput[] = [
  { id: 'sync', title: 'Team sync', start: today(0, 10), end: today(0, 11) },
  { id: 'conf', title: 'Conference', start: todayDate(1), end: todayDate(4) },
]

// Source 2: an async function with ~300 ms latency. It receives the visible range and
// generates events relative to it, so paging always yields data.
const asyncSource: EventSourceInput = {
  events: (info: FetchInfo) =>
    new Promise<EventInput[]>((resolve) => {
      const first = info.start.toPlainDate()
      setTimeout(
        () =>
          resolve([
            {
              id: 'yoga',
              title: 'Yoga (async)',
              start: `${first.add({ days: 1 }).toString()}T07:00`,
              end: `${first.add({ days: 1 }).toString()}T08:00`,
              color: 'oklch(60% 0.15 150)',
            },
            {
              id: 'review',
              title: 'Design review (async)',
              start: `${first.add({ days: 3 }).toString()}T16:00`,
              end: `${first.add({ days: 3 }).toString()}T17:00`,
              color: 'oklch(60% 0.15 150)',
            },
          ]),
        300,
      )
    }),
}

// Source 3: a JSON feed, fetched with `?start=&end=&timeZone=` for the visible range.
const SOURCES: EventSourceInput[] = [
  { events: (_info, success) => success(STATIC) },
  asyncSource,
  { url: '/events.json' },
]

export function EventSourcesDemo(): JSX.Element {
  const { ref, calendar } = useCalendar()
  // The controller mirrors instance state (view, title, loading) into React.
  const controller = useCalendarController(calendar)
  return (
    <>
      <p className="controls">
        <span>Sources: static array, async function, JSON feed</span>
        <output className="loading-status">{controller.loading ? 'loading…' : ''}</output>
      </p>
      <div className="card">
        <Calendar
          ref={ref}
          plugins={PLUGINS}
          view="dayGridMonth"
          colorScheme={useScheme()}
          headerToolbar={{
            start: 'title',
            center: '',
            end: 'today prev,next dayGridMonth,timeGridWeek,listWeek',
          }}
          buttonText={{ listWeek: 'list' }}
          eventSources={SOURCES}
          height="600px"
        />
      </div>
    </>
  )
}

What to look at

  • A function source receives the visible range as Temporal values and returns events, either through the success callback or a promise.
  • A URL source is fetched with start, end, and timeZone query parameters. This demo reads a static JSON file, so paging past the year shows nothing from the feed.
  • useCalendarController mirrors instance state into React. The "loading…" text above the calendar is bound to it.