react-resource-view

Calendar & timeline

Laying a collection out over time.

Both lay the same collection out along a time axis, and both need to be told which fields carry the dates — there is no naming convention to guess at.

Calendar#

A day, a week or a month. Records with an end date are drawn as blocks spanning their duration; without one they sit at a point in time.

import { calendarViewOptionFactory } from "react-resource-view"

viewVariants: [
  calendarViewOptionFactory({
    dateKey: "startAt",     // when it starts
    endDateKey: "endAt",    // when it ends — omit for point-in-time events
    titleKey: "title",      // what is written on the event
    colorKey: "track",      // one colour per distinct value
    mode: "week",           // "day" | "week" | "month"
    hourStart: 8,
    hourEnd: 18,
  }),
]

A week of conference sessions

NameTypeDefaultDescription
dateKeystring"dueDate"The field the event is placed by. Almost always worth setting.
endDateKeystringMakes the event a block rather than a point.
titleKeystringWhat is written on the event.
colorKeystringOne colour per distinct value of this field.
mode"day" | "week" | "month""month"The span on screen.
hourStart / hourEndnumber7 / 21The vertical window in day and week modes — no point drawing the small hours.
getIcon(row) => LucideIcon | undefinedAn icon per event, resolved by the resource so the package stays generic.

Timeline#

Time along the top, one band per resource down the side: rooms, staff, vehicles, machines. Where a calendar answers “what is happening on Tuesday”, a timeline answers “is room B free on Tuesday”.

import { timelineViewOptionFactory } from "react-resource-view"

viewVariants: [
  timelineViewOptionFactory<Session>({
    startDateKey: "startAt",
    endDateKey: "endAt",
    titleKey: "title",
    groupKey: "room",          // one band per room
    groupsLabel: "Rooms",      // header of the left-hand column
    statusKey: "status",
    colorByStatus: { confirmed: "#3b82f6", hold: "#f59e0b" },
    daysToShow: 5,
    showUnassigned: false,
  }),
]

The same sessions, banded by room

NameTypeDefaultDescription
startDateKey / endDateKeystring"startDate" / "endDate"The span each band segment covers.
groupKeystringThe field whose distinct values become the bands.
groupsLabelstringHeader of the left-hand column — “Rooms”, “Staff”.
statusKey / colorByStatusstring / Record<string, string>Colours a segment by a status field.
daysToShownumber14How wide the window is.
showUnassignedbooleantrueA band collecting records with no group. unassignedLabel names it.
resolveGroups / resolveGroupForRowfunctionsBands computed rather than derived. See below.

Bands that are not just a key#

groupKey derives the bands from the records, which means an empty room does not exist. When the bands are a known set — every room, every engineer, whether or not anything is booked — resolve them instead.

timelineViewOptionFactory<Booking>({
  // The bands, when they are not simply the distinct values of a key —
  // every room, including those with nothing booked in them.
  resolveGroups: () => rooms.map((room) => ({
    id: room["@id"],
    label: room.name,
    sublabel: `${room.capacity} seats`,
  })),
  // Which band a record belongs in.
  resolveGroupForRow: (booking) => ({
    id: booking.room,
    label: roomsById[booking.room]?.name ?? "Unknown",
  }),
})

An empty band is information

A scheduling screen is read to find the gap. Derived bands hide exactly the rows a planner is looking for.

Locale#

Month names, day headers and the day a week starts on all come from the dateLocale port — Sunday in en-US, Monday in French — so one call settles both layouts.

import { fr } from "date-fns/locale"
import { configurePorts } from "react-resource-view"

// Month names, day headers, and the day a week starts on.
configurePorts({ dateLocale: fr })

Which one, when#

  • Calendar when the question is about a moment: what is on today, is that week busy.
  • Timeline when the question is about a resource: is this room free, is that person double-booked.
  • Declare both. They are two entries in viewVariants and the reader switches between them — as with any other layout.