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
| Name | Type | Default | Description |
|---|---|---|---|
dateField | string | "dueDate" | The field the event is placed by. Almost always worth setting. |
endDateField | string | — | Makes the event a block rather than a point. |
titleField | string | — | What is written on the event. |
colorField | string | — | One colour per distinct value of this field. |
mode | "day" | "week" | "month" | "month" | The span on screen. |
hourStart / hourEnd | number | 7 / 21 | The 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
| Name | Type | Default | Description |
|---|---|---|---|
startField / endField | string | "startDate" / "endDate" | The span each band segment covers. |
groupBy | string | — | The field whose distinct values become the bands. |
groupsLabel | string | — | Header of the left-hand column — “Rooms”, “Staff”. |
statusField / colorByStatus | string / Record<string, string> | "state" | Colours a segment by a status field. |
daysToShow | number | 14 | How wide the window is. |
showUnassigned | boolean | true | A band collecting records with no group. unassignedLabel names it. |
resolveGroups / resolveGroupForRow | functions | — | 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.layoutsand the reader switches between them — as with any other layout.