Calendar
A full-bleed events calendar built on FullCalendar v6's MIT plugins — month/week/day/list views, drag-to-reschedule, recurring events and .ics import/export, themed entirely through design tokens.
The Calendar app (/apps/calendar) is a real events calendar, not a static demo. The route is
full-bleed and React.lazy-loaded — its FullCalendar chunk splits out of the main bundle — and
on small screens it opens in List view.
Licensing — MIT plugins only
The calendar uses only the standard, MIT-licensed FullCalendar v6 plugins: @fullcalendar/react,
daygrid, timegrid, list, interaction, and rrule (the rrule peer is BSD-3). Never add
@fullcalendar/scheduler or any resource-* plugin — those are premium/paid and would break the
licensing story (the same rule as AG Grid Community). This is a hard constraint, not a preference.
Views
The calendar's own toolbar switches between four views (FullCalendar's header toolbar is disabled;
the page renders a token-styled toolbar and drives the calendar via calendarRef.current.getApi()).
dayGridMonth — the default on desktop. A full month grid with per-day event chips and a
"+N more" overflow.
Features
| Area | What it does |
|---|---|
| Interaction | Drag-to-reschedule + resize (editable), click-drag-to-create (selectable), click-to-edit / quick-view (eventClick). |
| Templates | Drag a category chip from the sidebar onto the grid to create an event. |
| Recurrence | Daily / weekly / monthly rules via the @fullcalendar/rrule plugin. |
| Import/export | Export all events or a single event, and import from an .ics file. |
| Sidebar | Mini-calendar navigator, category filters, drag-to-create templates, and an Upcoming list (occurrence-expanded so recurrences show). |
| Theming | No hardcoded colors — the five event categories map to the five semantic tones. |
Architecture & files
The feature is split into a thin screen, a set of presentational components, and a single view-model hook that owns all state and behavior.
Screen & view-model
| File | Responsibility |
|---|---|
src/pages/apps/CalendarPage.tsx | The full-bleed screen — composes the toolbar, sidebar, calendar and the two modals. |
src/components/calendar/useCalendar.ts | The view-model hook — all state and behavior (events, view, selection, CRUD, .ics). |
Presentational components (src/components/calendar/)
| File | Responsibility |
|---|---|
CalendarView.tsx | Thin FullCalendar wrapper — plugin set and shared options. |
CalendarToolbar.tsx | The page's own toolbar — view control, prev/today/next, title, search, add, .ics. |
CalendarSidebar.tsx | Mini-calendar, category filters, drag-to-create templates, and the Upcoming list. |
EventModal.tsx | Create / edit an event. |
EventDetail.tsx | Read-only quick view — Edit / Duplicate / Delete / Export .ics. |
Data & helpers
| File | Responsibility |
|---|---|
src/data/calendar.ts | Types, seed, persistence, and recurrence + display helpers. |
src/lib/ics.ts | Pure-JS .ics reader/writer (eventsToIcs / downloadIcs / parseIcs). |
src/styles/_calendar.scss | Maps --fc-* → raw --* tokens; per-event fc-ev-<tone> classes. |
CalendarPage stays thin — it renders the toolbar, sidebar, CalendarView and the two modals, wiring
them all to a single useCalendar() hook that owns every piece of state and behavior.
// src/components/calendar/CalendarView.tsx (shape)
<FullCalendar
ref={calendarRef}
plugins={[dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin, rrulePlugin]}
initialView={initialView}
headerToolbar={false}
height="100%"
editable selectable selectMirror droppable nowIndicator expandRows navLinks
events={events}
select={onSelect} eventClick={onEventClick}
eventDrop={onEventDrop} eventResize={onEventResize}
eventReceive={onEventReceive} datesSet={onDatesSet}
/>Data & persistence
State persists to localStorage under STORAGE_KEY = 'calendar-state-v2' via loadCalendar() /
saveCalendar() / clearCalendar(); useCalendar saves on every change.
CalendarEvent—id,title,description?,start/end?(local ISO strings —YYYY-MM-DDall-day,YYYY-MM-DDTHH:mm:sstimed),allDay,categoryId,location?,attendeeIds, andrecurrence?.- Categories (5) map 1:1 to the semantic tones: meeting →
primary, personal →success, deadline →danger, holiday →warning, reminder →info. - Seed dates are relative to today, so the demo never looks stale.
occurrencesInRange(events, from, to)expands recurring rules — the sidebar mini-dots and the Upcoming list use it so recurrences appear there too.
Tip
calendar-state-v2 is registered in src/lib/appStorage.ts, so the shell's "Reset to defaults"
wipes it along with every other app store.
Related
Was this page helpful?
