Media productionFramehouse (Fictional business)
Video production planning
A shoot runs long. Move the edit that depended on it, see why, and take it back.
- Time scale
- Demo with synthetic data
- hours / days
- Pro capabilities
- Resource treeDrag and resizeDependenciesDetail panelUndo and redo
Loads SuperScheduler Pro only when you ask for it.
The situation
Framehouse is a fictional production company with two stages, two edit suites, a grading room, a mix stage and two camera crews. Producers juggle three clients in the same week, and every phase waits for the one before it: no offline edit before the shoot wraps, no grade before the offline locks.
In a spreadsheet the week looks fine until a shoot overruns. The planning board has to show what the overrun breaks, refuse the moves that make no sense (a shoot in the mix stage) and make every change easy to reverse.
What you will doYou will extend an overrunning shoot, watch its dependent edit turn red, move that edit to a free suite after the shoot, and undo a change with one keystroke.
Where the library ends and your application begins
SuperScheduler provides
- Grouped resource tree with stages, post-production rooms and crews.
- Drag, resize, snapping to four-hour blocks and overlap prevention per room.
- Curved finish-to-start links drawn between sessions.
- Undo/redo history with Ctrl/⌘+Z, and controlled events through onEventsChange.
- Zoom levels from hours to days with a hidden night, keyboard navigation and announcements.
This example’s code decides
- Which phase fits which room (a shoot needs a stage, a grade needs the grading room).
- Which sessions depend on which, and when a dependency is broken.
- Locked maintenance blocks and the messages shown when a move is refused.
- The detail panel, the mission and every label in this page.
The code behind this demo
These are the files this page runs, not a simplified copy. They compile against the public SuperScheduler Pro exports.
The integration: zoom levels, hidden night hours, rendering hooks, and the callbacks where the rules decide whether a move is allowed.
/**
* Framehouse planning board: stages, edit suites and crews over one production week.
*
* Library: timeline, tree rows, drag/resize, dependency links, undo history, keyboard, zoom.
* Application: which rooms accept which phases, which sessions are locked, and what counts as a
* dependency problem. Those rules live in ./rules and run inside the scheduler callbacks below.
*/
import { useEffect, useMemo, type RefObject } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps } from 'super-scheduler'
import type { SchedulerHistory } from 'super-scheduler/history'
import { createHeaderHook } from '../../scheduler/live-headers'
import { toneClass } from '../../scheduler/tones'
import {
PROJECT_TONES,
readSessionFields,
roomKind,
toResources,
type SessionEvent,
} from './adapter'
import { BUSINESS_HOURS, INITIAL_VIEW_START, WEEK } from './model'
import { acceptsPhase, type DependencyIssue } from './rules'
import type { FramehouseText, ZoomId } from './text'
export interface FramehouseSchedulerProps {
readonly events: SessionEvent[]
readonly issues: readonly DependencyIssue[]
readonly text: FramehouseText
/** BCP 47 tag for labels, and the scheduler locale id. */
readonly tag: string
readonly schedulerLocale: string
/** Level on mount. Later changes go through control.zoom so the view animates from where it is. */
readonly initialZoom: ZoomId
readonly showLinks: boolean
readonly history: SchedulerHistory
readonly controlRef: RefObject<SuperScheduler.Scheduler | null>
readonly onEventsChange: (args: SchedulerEventsChangeArgs) => void
readonly onSelect: (id: string) => void
readonly onMoved: (id: string) => void
readonly onResized: (id: string, end: SuperScheduler.Date) => void
readonly onRefused: (message: string) => void
readonly onZoomLevel: (level: ZoomId) => void
}
const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
{
id: 'hours',
properties: {
scale: 'Hour',
cellWidth: 46,
timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Hour' }],
},
},
{
id: 'shifts',
properties: {
scale: 'CellDuration',
cellDuration: 240,
cellWidth: 104,
timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Cell' }],
},
},
{
id: 'days',
properties: {
scale: 'Day',
cellWidth: 220,
timeHeaders: [{ groupBy: 'Week' }, { groupBy: 'Day' }],
},
},
]
const ZOOM_IDS: readonly ZoomId[] = ['hours', 'shifts', 'days']
export function FramehouseScheduler(props: FramehouseSchedulerProps) {
const {
events,
issues,
text,
tag,
schedulerLocale,
initialZoom,
showLinks,
history,
controlRef,
} = props
const { onEventsChange, onSelect, onMoved, onResized, onRefused, onZoomLevel } = props
const resources = useMemo(() => toResources(text), [text])
// Finish-to-start links come from the business model, not from the scheduler.
const links = useMemo<SuperScheduler.LinkData[]>(() => {
if (!showLinks) return []
const late = new Set(issues.map((issue) => issue.session))
return events.flatMap((event) =>
event.after === undefined
? []
: [
{
id: `${event.after}->${String(event.id)}`,
from: event.after,
to: event.id,
type: 'FinishToStart',
...(late.has(String(event.id)) ? { cssClass: 'ss-link--late' } : {}),
},
],
)
}, [events, issues, showLinks])
const config = useMemo<SchedulerProps>(() => {
const late = new Set(issues.map((issue) => issue.session))
return {
locale: schedulerLocale,
startDate: WEEK.start,
days: 5,
zoomLevels: ZOOM_LEVELS,
zoom: initialZoom,
zoomPosition: 'left',
// Nights stay hidden: the board shows the studio's working day only.
businessBeginsHour: BUSINESS_HOURS.begin,
businessEndsHour: BUSINESS_HOURS.end,
showNonBusiness: false,
useEventBoxes: 'Never',
height: '100%',
treeEnabled: true,
rowHeaderWidth: 172,
rowHeaderWidthAutoFit: false,
eventHeight: 36,
rowMarginTop: 3,
rowMarginBottom: 3,
allowEventOverlap: false,
dragCard: { duration: false },
durationBarVisible: false,
showToolTip: false,
eventHoverHandling: 'Disabled',
keyboardEnabled: true,
keyboardTarget: 'component',
keyboardMode: 'Full',
history,
controlRef,
onEventsChange,
onBeforeTimeHeaderRender: createHeaderHook(tag),
onBeforeRowHeaderRender: (args) => {
const isGroup = args.row.data.children !== undefined
args.row.cssClass = isGroup ? 'ss-rh--group' : ''
},
onBeforeEventRender: (args) => {
const fields = readSessionFields(args.data)
if (fields === null) return
const { project, phase, locked } = fields
const name = SuperScheduler.Util.escapeHtml(text.projects[project])
const detail = SuperScheduler.Util.escapeHtml(text.phases[phase])
args.data.html = `<span class="ss-evt"><span class="ss-evt__title">${name}</span><span class="ss-evt__meta">${detail}</span></span>`
const classes = ['ss-ev', toneClass(PROJECT_TONES[project])]
if (locked === true) classes.push('ss-ev--hatched')
if (phase === 'crew') classes.push('ss-ev--ghost')
if (late.has(String(args.data.id))) classes.push('ss-ev--late')
args.data.cssClass = classes.join(' ')
},
// Room compatibility and locks are application rules; overlap is enforced by the library.
onEventMoving: (args) => {
const fields = readSessionFields(args.e.data)
if (fields === null) return
const { phase, locked } = fields
if (locked === true) {
args.allowed = false
args.message = text.rules.locked
return
}
const kind = roomKind(args.resource)
if (kind === null || !acceptsPhase(kind, phase)) {
args.allowed = false
args.message = text.rules.wrongRoom[phase]
}
},
// Final check before the commit (also covers keyboard moves).
onEventMove: (args) => {
const fields = readSessionFields(args.e.data)
const kind = roomKind(args.newResource)
if (fields !== null && (kind === null || !acceptsPhase(kind, fields.phase))) {
args.preventDefault()
onRefused(text.rules.wrongRoom[fields.phase])
}
},
onEventMoved: (args) => onMoved(String(args.e.id())),
onEventResized: (args) => onResized(String(args.e.id()), args.newEnd),
onEventClick: (args) => onSelect(String(args.e.id())),
onZoom: (args) => {
if (args.phase !== 'end') return
const level = ZOOM_IDS[args.level]
if (level !== undefined) onZoomLevel(level)
},
}
}, [
history,
controlRef,
issues,
onEventsChange,
onMoved,
onRefused,
onResized,
onSelect,
onZoomLevel,
schedulerLocale,
tag,
text,
initialZoom,
])
// Imperative and once: a scroll prop would be re-applied on every update.
useEffect(() => {
controlRef.current?.scrollTo(INITIAL_VIEW_START, false, 'left')
}, [controlRef])
return <SuperSchedulerComponent {...config} resources={resources} events={events} links={links} />
}Share this scenario
The link opens this example with the current view (time scale and options). It never contains your edits or personal data.