calendar-tsx

Quick start

A month calendar with navigation, in one component.

App.tsx
import type { Calendar_EventInput, Calendar_State } from '@calendar-tsx/core';
import { calendarViews, calendarFeatures, dayGridView, timeGridView } from '@calendar-tsx/core';
import {
  CalendarProvider,
  DayGridView,
  DayGridViewDay,
  DayGridViewRow,
  DayGridViewRowEvent,
  useCalendar,
  useCalendarSelector,
  DayGridViewSection,
} from '@calendar-tsx/react';
import '@calendar-tsx/themes/default.css';
import { useState } from 'react';
import { Temporal } from 'temporal-polyfill';

type Booking = { title: string };

// module scope: views and features are compared by reference
const features = calendarFeatures({});
const views = calendarViews({
  features: features,
  views: {
    month: dayGridView({ period: 'month', maxEventsPerDay: 3 }),
    week: timeGridView({ days: 7 }),
  },
});

export const App = ({ events }: { events: Calendar_EventInput<Booking>[] }) => {
  const [view, setView] = useState<keyof typeof views>('month');
  const [date, setDate] = useState(() => Temporal.Now.plainDateISO());

  const calendar = useCalendar({
    timeZone: 'America/New_York',
    views,
    features,
    view,
    date,
    events,
    onNavigate: next => {
      setView(next.view);
      setDate(next.date);
    },
  });

  return (
    <CalendarProvider value={calendar}>
      <button onClick={() => calendar.api.prev()}>Previous</button>
      <button onClick={() => calendar.api.today()}>Today</button>
      <button onClick={() => calendar.api.next()}>Next</button>
      <Month />
    </CalendarProvider>
  );
};

// the context erases the instance's types; annotate the selector to get them back
type State = Calendar_State<Booking, unknown, typeof views>;

const Month = () => {
  const layout = useCalendarSelector((state: State) => state.layout);

  if (layout.kind !== 'dayGrid') return null;

  return (
    <DayGridView model={layout}>
      {layout.sections.map(section => (
        <DayGridViewSection key={section.key} section={section}>
          {section.rows.map(week => (
            <DayGridViewRow key={week.key} row={week}>
              {week.days.map(day => (
                <DayGridViewDay key={day.key} day={day}>
                  {day.date.day}
                </DayGridViewDay>
              ))}
              {week.segments.map(segment => (
                <DayGridViewRowEvent key={segment.key} segment={segment}>
                  {segment.event.data.title}
                </DayGridViewRowEvent>
              ))}
            </DayGridViewRow>
          ))}
        </DayGridViewSection>
      ))}
    </DayGridView>
  );
};

What it does

  1. calendarFeatures configures the features once, calendarViews binds the views to them, and useCalendar receives both.
  2. view and date live in React state. The buttons call calendar.api, which reports the next view and date to onNavigate; setting them moves the calendar.
  3. CalendarProvider makes the calendar available to the parts.
  4. Month selects the layout, checks its kind and renders the day-grid parts. The selector's state type is annotated, because the context does not carry the calendar's types.

Let the types infer

Don't pass type arguments such as useCalendar<Booking>(…): TypeScript then stops inferring the rest, and view and onNavigate widen to string. Type events instead, for example as Calendar_EventInput<Booking>[].

Next