PVR Tech Studio

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.

4 min read
Updated July 15, 2026

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.

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

AreaWhat it does
InteractionDrag-to-reschedule + resize (editable), click-drag-to-create (selectable), click-to-edit / quick-view (eventClick).
TemplatesDrag a category chip from the sidebar onto the grid to create an event.
RecurrenceDaily / weekly / monthly rules via the @fullcalendar/rrule plugin.
Import/exportExport all events or a single event, and import from an .ics file.
SidebarMini-calendar navigator, category filters, drag-to-create templates, and an Upcoming list (occurrence-expanded so recurrences show).
ThemingNo 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

FileResponsibility
src/pages/apps/CalendarPage.tsxThe full-bleed screen — composes the toolbar, sidebar, calendar and the two modals.
src/components/calendar/useCalendar.tsThe view-model hook — all state and behavior (events, view, selection, CRUD, .ics).

Presentational components (src/components/calendar/)

FileResponsibility
CalendarView.tsxThin FullCalendar wrapper — plugin set and shared options.
CalendarToolbar.tsxThe page's own toolbar — view control, prev/today/next, title, search, add, .ics.
CalendarSidebar.tsxMini-calendar, category filters, drag-to-create templates, and the Upcoming list.
EventModal.tsxCreate / edit an event.
EventDetail.tsxRead-only quick view — Edit / Duplicate / Delete / Export .ics.

Data & helpers

FileResponsibility
src/data/calendar.tsTypes, seed, persistence, and recurrence + display helpers.
src/lib/ics.tsPure-JS .ics reader/writer (eventsToIcs / downloadIcs / parseIcs).
src/styles/_calendar.scssMaps --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.

  • CalendarEventid, title, description?, start / end? (local ISO strings — YYYY-MM-DD all-day, YYYY-MM-DDTHH:mm:ss timed), allDay, categoryId, location?, attendeeIds, and recurrence?.
  • 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.

Was this page helpful?