Live-VeranstaltungenAurora Live (Fiktives Unternehmen)
Bühnenplanung für ein Festival
Ein Linecheck ragt in den Puffer vor einem Auftritt. Auf fünf Minuten zoomen, kürzen und sehen, welche Räume und Crews am Act hängen.
- Zeitskala
- Demo mit synthetischen Daten
- Minuten / Stunden
- Pro-Funktionen
- AbhängigkeitenStufenloser ZoomMinutenrasterDetailpanelTheming
SuperScheduler Pro wird erst geladen, wenn Sie es anfordern.
Die Ausgangslage
Aurora Live ist ein fiktives Eintagesfestival mit drei Bühnen, zwei Backstage-Räumen, einem Presseraum und vier Crews. Die Running Order steht Wochen vorher fest; was die Produktion am Tag selbst noch plant, sind die Linechecks, eingeklemmt zwischen Auftritten und jeweils mit einem Puffer, bevor der Act auf die Bühne geht.
In einer Tabelle verschwinden fünf Minuten: Ein Check, der zehn Minuten vor einem Auftritt endet, sieht auf Stundenbasis gut aus. Das Board muss bis auf die Minute zoomen, jeden Check mit seinem Auftritt verbinden, unsinnige Verschiebungen ablehnen (ein Check auf einer fremden Bühne, ein veröffentlichter Auftritt) und zeigen, welche Räume und Crews ein Act betrifft.
Was Sie tun werdenSie öffnen das Ablaufblatt eines Acts im Seitenpanel, zoomen auf Fünf-Minuten-Zellen und kürzen einen Linecheck, bis seine Verknüpfung zum Auftritt wieder neutral ist.
Wo die Bibliothek endet und Ihre Anwendung beginnt
SuperScheduler liefert
- Zeitachse innerhalb eines Tages ohne die Nacht (Geschäftszeiten, Samstag eingeschlossen) und Zoomstufen von Stunden bis zu 15- und 5-Minuten-Zellen, animiert über control.zoom.
- Gruppierte Zeilen für Bühnen, Räume und Crews sowie die Zeilenauswahl-API, die sie hervorhebt.
- Geschwungene Ende-Anfang-Verknüpfungen zwischen jedem Linecheck und seinem Auftritt.
- Ziehen und Dauer ändern im Raster der aktiven Zelle, keine Überschneidung pro Bühne, Ablehnungsmeldungen auf der Drag-Karte, Rückgängig und Wiederholen sowie Tastaturbedienung.
- Theming über CSS-Custom-Properties und Regeln ohne Layer am Root des Controls (festival.css).
Der Code dieses Beispiels entscheidet
- Welche Blöcke sich bewegen (nur Linechecks), auf welcher Bühne und wie kurz ein Check sein darf.
- Der 20-Minuten-Puffer zwischen Check und Auftritt, wann seine Verknüpfung rot wird und welche Verknüpfungen gezeichnet werden (zu kurze Puffer und der gewählte Act, oder alle).
- Welche Räume und Crews zu einem Act gehören, und das Seitenpanel, das zu Check oder Auftritt springt.
- Die Kopfzeilen bei fünf Minuten, die Mission und alle Texte dieser Seite.
Der Code hinter dieser Demo
Das sind die Dateien, die diese Seite ausführt, keine vereinfachte Kopie. Sie kompilieren gegen die öffentlichen Exporte von SuperScheduler Pro.
Die Integration: drei Zoomstufen, ausgeblendete Nachtstunden, die Kopfzeilen bei fünf Minuten, das Hervorheben von Zeilen über rows.selection und die Callbacks, in denen die Regeln eine Verschiebung oder Änderung der Dauer ablehnen.
/**
* Aurora Live running order: stages, rooms and crews over one festival day, down to five minutes.
*
* Library: timeline with hidden night, zoom levels from hours to 5-minute cells, tree rows, row
* selection (used to highlight related rows), dependency links, drag/resize with overlap
* prevention, undo history, keyboard, and theming through CSS custom properties (./festival.css).
* Application: which blocks can move, the line-check buffer before each set, the shortest check,
* and which rows relate to an act. Those rules live in ./rules and run inside the callbacks below.
*/
import { useEffect, useMemo, useState, type RefObject } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type {
SchedulerBeforeTimeHeaderRenderArgs,
SchedulerEventsChangeArgs,
SchedulerProps,
} from 'super-scheduler'
import type { SchedulerHistory } from 'super-scheduler/history'
import { formatCivil } from '../../scheduler/civil'
import { headerLabel, usesTwelveHours } from '../../scheduler/header-format'
import { createHeaderHook } from '../../scheduler/live-headers'
import { toneClass } from '../../scheduler/tones'
import { KIND_TONES, readSlotFields, slotLabel, toResources, type SlotEvent } from './adapter'
import {
FESTIVAL_DAY,
INITIAL_VIEW_START,
OPEN_HOURS,
type ActId,
type RowId,
type Slot,
} from './model'
import { checkTooShort, moveProblem, type MoveProblem } from './rules'
import type { AuroraText, ZoomId } from './text'
import './festival.css'
export interface AuroraSchedulerProps {
readonly events: SlotEvent[]
readonly slots: readonly Slot[]
/** Acts whose line check ends too close to their set. */
readonly shortBuffers: readonly ActId[]
/** Rows that relate to the act in the side panel (highlighted). */
readonly relatedRows: readonly RowId[]
readonly text: AuroraText
/** 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
/** Every check-to-set link, or only those of the selected act and of short buffers. */
readonly showAllLinks: boolean
readonly selectedAct: ActId | null
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) => void
readonly onRefused: (message: string) => void
readonly onZoomLevel: (level: ZoomId) => void
}
const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
{
id: 'hours',
properties: {
scale: 'Hour',
cellWidth: 80,
timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Hour' }],
},
},
{
id: 'quarter',
properties: {
scale: 'CellDuration',
cellDuration: 15,
cellWidth: 64,
timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Cell' }],
},
},
{
id: 'five',
properties: {
scale: 'CellDuration',
cellDuration: 5,
cellWidth: 26,
timeHeaders: [{ groupBy: 'Hour' }, { groupBy: 'Cell' }],
},
},
]
const ZOOM_IDS: readonly ZoomId[] = ['hours', 'quarter', 'five']
/**
* Time headers: the shared Intl labels, except at five minutes where the hour sits above and the
* minutes (":05") below, so 26 px cells stay readable.
*/
function festivalHeaders(tag: string): (args: SchedulerBeforeTimeHeaderRenderArgs) => void {
const shared = createHeaderHook(tag)
return (args) => {
if (args.control.cellDuration !== 5) {
shared(args)
return
}
const ticks = args.header.start.getTime()
const minutes = String(args.header.start.getMinutes()).padStart(2, '0')
args.header.html =
args.header.level === 0
? SuperScheduler.Util.escapeHtml(headerLabel(ticks, 'hour', tag, 'group'))
: `:${minutes}`
}
}
export function AuroraScheduler(props: AuroraSchedulerProps) {
const {
events,
slots,
shortBuffers,
relatedRows,
selectedAct,
text,
tag,
schedulerLocale,
initialZoom,
showAllLinks,
history,
controlRef,
} = props
const { onEventsChange, onSelect, onMoved, onResized, onRefused, onZoomLevel } = props
const resources = useMemo(() => toResources(text), [text])
// Narrower row headers on phones, chosen once on mount.
const rowHeaderWidth = useMemo(
() => (window.matchMedia('(max-width: 639px)').matches ? 116 : 176),
[],
)
// Header labels follow the active level. An animated zoom renders the new level's headers before
// control.cellDuration reports it, so the hook is rebuilt (headers repainted) once a zoom lands.
const [headerHook, setHeaderHook] = useState(() => festivalHeaders(tag))
// Each act's line check leads to its set: a finish-to-start link, red when the buffer is short.
// Morning checks would cross a whole stage row, so by default only the selected act and the
// short buffers show theirs.
const links = useMemo<SuperScheduler.LinkData[]>(() => {
const short = new Set(shortBuffers)
return slots.flatMap((check) => {
const set =
check.kind === 'check'
? slots.find((slot) => slot.kind === 'set' && slot.act === check.act)
: undefined
if (set === undefined || check.act === undefined) return []
if (!showAllLinks && !short.has(check.act) && check.act !== selectedAct) return []
return [
{
id: `${check.act}-buffer`,
from: check.id,
to: set.id,
type: 'FinishToStart' as const,
...(short.has(check.act) ? { cssClass: 'ss-link--late' } : {}),
},
]
})
}, [slots, shortBuffers, showAllLinks, selectedAct])
const config = useMemo<SchedulerProps>(() => {
const short = new Set<string>(shortBuffers)
const related = new Set<string>(relatedRows)
const pattern = usesTwelveHours(tag) ? 'h:mm tt' : 'HH:mm'
const clock = (value: string) => formatCivil(value, tag, { hour: 'numeric', minute: '2-digit' })
const refusal = (problem: MoveProblem) =>
problem === 'published'
? text.rules.published
: problem === 'follows'
? text.rules.follows
: text.rules.wrongStage
return {
locale: schedulerLocale,
startDate: FESTIVAL_DAY,
days: 1,
zoomLevels: ZOOM_LEVELS,
zoom: initialZoom,
zoomPosition: 'left',
// The site opens at noon: the morning stays hidden. A Saturday is a working day here.
businessBeginsHour: OPEN_HOURS.begin,
businessEndsHour: OPEN_HOURS.end,
businessWeekends: true,
showNonBusiness: false,
useEventBoxes: 'Never',
height: '100%',
cssClass: 'al-board',
treeEnabled: true,
treePreventParentUsage: true,
rowHeaderWidth,
rowHeaderWidthAutoFit: false,
eventHeight: 32,
rowMarginTop: 4,
rowMarginBottom: 4,
allowEventOverlap: false,
linkLayer: 'Below',
timeRangeSelectedHandling: 'Disabled',
// Minute-level work: the drag card and header marker show clock times, not dates.
dragCard: {
duration: false,
dateFormat: pattern,
movingDateFormat: pattern,
markerFormat: pattern,
},
durationBarVisible: false,
showToolTip: false,
eventHoverHandling: 'Disabled',
keyboardEnabled: true,
keyboardTarget: 'component',
keyboardMode: 'Full',
history,
controlRef,
onEventsChange,
onBeforeTimeHeaderRender: headerHook,
onBeforeRowHeaderRender: (args) => {
args.row.cssClass = args.row.data.children !== undefined ? 'ss-rh--group' : ''
},
// The selected act's rows are also tinted across the grid (headers come from rows.selection).
onBeforeCellRender: (args) => {
if (related.has(String(args.cell.resource))) args.cell.properties.cssClass = 'al-related'
},
onBeforeEventRender: (args) => {
const fields = readSlotFields(args.data)
if (fields === null) return
const label = SuperScheduler.Util.escapeHtml(slotLabel(fields, text))
const time =
fields.kind === 'set'
? `<span class="ss-evt__meta">${SuperScheduler.Util.escapeHtml(clock(args.data.start.value))}</span>`
: ''
args.data.html = `<span class="ss-evt"><span class="ss-evt__title">${label}</span>${time}</span>`
const classes = ['ss-ev', toneClass(KIND_TONES[fields.kind])]
if (fields.kind === 'set') classes.push('al-set')
if (fields.kind === 'check') classes.push('ss-ev--ghost')
if (fields.kind === 'changeover') classes.push('ss-ev--hatched')
if (fields.kind === 'set' && fields.act !== undefined && short.has(fields.act))
classes.push('ss-ev--late')
args.data.cssClass = classes.join(' ')
},
// Only line checks move, and only on their act's stage; overlap is refused by the library.
onEventMoving: (args) => {
const fields = readSlotFields(args.e.data)
const problem = fields === null ? null : moveProblem(fields, String(args.resource))
if (problem !== null) {
args.allowed = false
args.message = refusal(problem)
}
},
// Final check before the commit (also covers keyboard moves).
onEventMove: (args) => {
const fields = readSlotFields(args.e.data)
const problem = fields === null ? null : moveProblem(fields, String(args.newResource))
if (problem !== null) {
args.preventDefault()
onRefused(refusal(problem))
}
},
onEventResizing: (args) => {
if (checkTooShort(args.start.value, args.end.value)) {
args.allowed = false
args.message = text.rules.tooShort
}
},
onEventResize: (args) => {
if (checkTooShort(args.newStart.value, args.newEnd.value)) {
args.preventDefault()
onRefused(text.rules.tooShort)
}
},
onEventMoved: (args) => onMoved(String(args.e.id())),
onEventResized: (args) => onResized(String(args.e.id())),
onEventClick: (args) => onSelect(String(args.e.id())),
onZoom: (args) => {
if (args.phase !== 'end') return
setHeaderHook(() => festivalHeaders(tag))
const level = ZOOM_IDS[args.level]
if (level !== undefined) onZoomLevel(level)
},
}
}, [
history,
controlRef,
shortBuffers,
relatedRows,
rowHeaderWidth,
onEventsChange,
onMoved,
onRefused,
onResized,
onSelect,
onZoomLevel,
schedulerLocale,
headerHook,
tag,
text,
initialZoom,
])
// Highlighted rows are the control's row selection, driven by the act in the side panel.
useEffect(() => {
const control = controlRef.current
if (control === null || control.disposed()) return
control.rows.selection.clear()
for (const id of relatedRows) {
const row = control.rows.find(id)
if (row !== null) control.rows.selection.add(row)
}
}, [controlRef, relatedRows])
// 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} />
}Dieses Szenario teilen
Der Link öffnet dieses Beispiel mit der aktuellen Ansicht (Zeitskala und Optionen). Er enthält nie Ihre Änderungen oder persönliche Daten.