diff --git a/apps/client/src/common/components/view-params-editor/viewParams.utils.ts b/apps/client/src/common/components/view-params-editor/viewParams.utils.ts index 93789451d..746e93f9c 100644 --- a/apps/client/src/common/components/view-params-editor/viewParams.utils.ts +++ b/apps/client/src/common/components/view-params-editor/viewParams.utils.ts @@ -1,4 +1,4 @@ -import type { CustomFields } from 'ontime-types'; +import type { CustomFields, ProjectData } from 'ontime-types'; import type { SelectOption } from '../select/Select'; @@ -53,6 +53,18 @@ export function makeCustomFieldSelectOptions(customFields: CustomFields, filterI return options; } +/** + * Creates data for a select element that displays project custom data + */ +export function makeProjectDataOptions(projectData: ProjectData): SelectOption[] { + return projectData.custom.map((entry, index) => { + return { + value: `${index}-${entry.title}`, + label: entry.title, + }; + }); +} + type ViewParamsObj = { [key: string]: string | FormDataEntryValue }; /** diff --git a/apps/client/src/views/backstage/Backstage.scss b/apps/client/src/views/backstage/Backstage.scss index f47645359..1aaeb30bb 100644 --- a/apps/client/src/views/backstage/Backstage.scss +++ b/apps/client/src/views/backstage/Backstage.scss @@ -149,10 +149,27 @@ .info { grid-area: info; display: flex; - gap: max(1vw, 16px); - align-self: flex-end; + flex-direction: column; + gap: $view-element-gap; + align-self: end; overflow: hidden; - align-items: end; + } + + .info__column { + display: flex; + flex-direction: column; + } + + .info-card { + display: flex; + gap: $view-element-gap; + + &__label { + font-size: $timer-label-size; + color: var(--label-color-override, $viewer-label-color); + font-weight: 600; + text-transform: uppercase; + } &__message { font-size: $base-font-size; @@ -162,11 +179,16 @@ flex: 1; } - .qr { + &__qr { padding: 0.5rem; background-color: $ui-white; border-radius: 2px; } + + &__img { + padding: 0.5rem; + border-radius: 2px; + } } } diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index f1f7c2df6..f1cb64d1f 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -48,7 +48,7 @@ export default function Backstage({ settings, }: BackstageProps) { const { getLocalizedString } = useTranslation(); - const { secondarySource } = useBackstageOptions(); + const { secondarySource, extraInfo } = useBackstageOptions(); const [blinkClass, setBlinkClass] = useState(false); const { height: screenHeight } = useViewportSize(); @@ -104,8 +104,8 @@ export default function Backstage({ // gather option data const defaultFormat = getDefaultFormat(settings?.timeFormat); const backstageOptions = useMemo( - () => getBackstageOptions(defaultFormat, customFields), - [defaultFormat, customFields], + () => getBackstageOptions(defaultFormat, customFields, general), + [defaultFormat, customFields, general], ); return ( @@ -180,8 +180,44 @@ export default function Backstage({ {showSchedule && }
- {general.url && } - {general.info &&
{general.info}
} + {extraInfo && } +
+ {general.url && } + {general.info &&
{general.info}
} +
+
+ + ); +} + +interface ExtraInfoProps { + projectData: ProjectData; + size: number; + source: string; +} +function ExtraInfo({ projectData, size, source }: ExtraInfoProps) { + const info = projectData.custom.find((entry, index) => { + const label = `${index}-${entry.title}`; + return label === source; + }); + + if (!info) { + return null; + } + + return ( +
+ {info.url && ( + (event.currentTarget.style.display = 'none')} + /> + )} +
+ {info.title &&
{info.title}
} + {info.value &&
{info.value}
}
); diff --git a/apps/client/src/views/backstage/backstage.options.ts b/apps/client/src/views/backstage/backstage.options.ts index 735ad4aae..25570c785 100644 --- a/apps/client/src/views/backstage/backstage.options.ts +++ b/apps/client/src/views/backstage/backstage.options.ts @@ -1,15 +1,23 @@ import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { CustomFields, OntimeEvent } from 'ontime-types'; +import { CustomFields, OntimeEvent, ProjectData } from 'ontime-types'; import { getTimeOption } from '../../common/components/view-params-editor/common.options'; import { OptionTitle } from '../../common/components/view-params-editor/constants'; import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; -import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; +import { + makeOptionsFromCustomFields, + makeProjectDataOptions, +} from '../../common/components/view-params-editor/viewParams.utils'; import { scheduleOptions } from '../common/schedule/schedule.options'; -export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { +export const getBackstageOptions = ( + timeFormat: string, + customFields: CustomFields, + projectData: ProjectData, +): ViewOption[] => { const secondaryOptions = makeOptionsFromCustomFields(customFields, [{ value: 'note', label: 'Note' }]); + const projectDataOptions = makeProjectDataOptions(projectData); return [ { title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] }, @@ -28,11 +36,26 @@ export const getBackstageOptions = (timeFormat: string, customFields: CustomFiel ], }, scheduleOptions, + { + title: OptionTitle.ElementVisibility, + collapsible: true, + options: [ + { + id: 'extra-info', + title: 'Extra info', + description: 'Select a project data source to show in the view', + type: 'option', + values: projectDataOptions, + defaultValue: '', + }, + ], + }, ]; }; type BackstageOptions = { secondarySource: keyof OntimeEvent | null; + extraInfo: string | null; }; /** @@ -43,6 +66,7 @@ function getOptionsFromParams(searchParams: URLSearchParams): BackstageOptions { // we manually make an object that matches the key above return { secondarySource: searchParams.get('secondary-src') as keyof OntimeEvent | null, + extraInfo: searchParams.get('extra-info'), }; }