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 { layout } from "react-resource-view"

layouts: [
  layout.calendar({
    dateField: "startAt",     // when it starts
    endDateField: "endAt",    // when it ends — omit for point-in-time events
    titleField: "title",      // what is written on the event
    colorField: "track",      // one colour per distinct value
    mode: "week",             // "day" | "week" | "month"
    hourStart: 8,
    hourEnd: 18,
  }),
]

A week of conference sessions

NameTypeDefaultDescription
dateFieldstring"dueDate"The field the event is placed by. Almost always worth setting.
endDateFieldstring—Makes the event a block rather than a point.
titleFieldstring—What is written on the event.
colorFieldstring—One 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 | undefined—An 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 { layout } from "react-resource-view"

layouts: [
  layout.timeline<Session>({
    startField: "startAt",
    endField: "endAt",
    titleField: "title",
    groupBy: "room",           // one band per room
    groupsLabel: "Rooms",      // header of the left-hand column
    statusField: "status",
    colorByStatus: { confirmed: "#3b82f6", hold: "#f59e0b" },
    daysToShow: 5,
    showUnassigned: false,
  }),
]

The same sessions, banded by room

NameTypeDefaultDescription
startField / endFieldstring"startDate" / "endDate"The span each band segment covers.
groupBystring—The field whose distinct values become the bands.
groupsLabelstring—Header of the left-hand column — “Rooms”, “Staff”.
statusField / colorByStatusstring / Record<string, string>"state"Colours a segment by a status field.
daysToShownumber14How wide the window is.
showUnassignedbooleantrueA band collecting records with no group. unassignedLabel names it.
resolveGroups / resolveGroupForRowfunctions—Bands computed rather than derived. See below.

Bands that are not just a field#

groupBy 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.

layout.timeline<Booking>({
  // The bands, when they are not simply the distinct values of a field —
  // 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.

Clicking an event#

An event, a timeline bar or a “+2 more” opens a small window on the record: its name, when it happens, and a handful of fields labelled the way form.inputs labels them — followed by the actions the row offers, as anywhere else. It stops at five fields on purpose: the window is a preview, and the record in full is one click away inside it.

What fills it is the layout's components.row, which both layouts default to the summary described above. A resource with something better to show declares its own, exactly as the card and list layouts do.

layout.calendar({
  dateField: "startAt",
  titleField: "title",
  // What the window opening on an event holds. Omit it for the summary
  // the calendar and the timeline draw by themselves.
  components: { row: SessionPreview },
})

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 views.list.layouts and the reader switches between them — as with any other layout.