chore: folder structure

This commit is contained in:
Carlos Valente
2025-02-03 20:01:34 +01:00
committed by Carlos Valente
parent 665496907e
commit f99ad83049
10 changed files with 24 additions and 24 deletions
@@ -1,164 +0,0 @@
@use '../../../theme/viewerDefs' as *;
.backstage {
margin: 0;
box-sizing: border-box; /* reset */
overflow: hidden;
width: 100%; /* restrict the page width to viewport */
height: 100vh;
font-family: var(--font-family-override, $viewer-font-family);
background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
gap: min(2vh, 16px);
padding: min(2vh, 16px) clamp(16px, 10vw, 64px);
display: grid;
grid-template-columns: 1fr 1fr 40vw;
grid-template-rows: auto 12px 1fr auto;
grid-template-areas:
' header header header'
' progress progress schedule-nav'
' now now schedule'
' info info schedule';
/* =================== HEADER + EXTRAS ===================*/
.project-header {
grid-area: header;
font-size: clamp(32px, 4.5vw, 64px);
font-weight: 600;
display: flex;
gap: 1rem;
}
.logo {
max-width: min(200px, 20vw);
max-height: min(100px, 20vh);
}
.clock-container {
margin-left: auto;
.label {
font-size: clamp(16px, 1.5vw, 24px);
font-weight: 600;
color: var(--label-color-override, $viewer-label-color);
text-transform: uppercase;
}
.time {
font-size: clamp(32px, 3.5vw, 50px);
font-weight: 600;
color: var(--secondary-color-override, $viewer-secondary-color);
letter-spacing: 0.05em;
line-height: 0.95em;
}
}
/* =================== MAIN - NOW ===================*/
.progress-container {
grid-area: progress;
width: 100%;
margin: 0 auto -8px;
}
.now-container {
grid-area: now;
display: flex;
flex-direction: column;
gap: min(2vh, 16px);
}
.event {
background-color: var(--card-background-color-override, $viewer-card-bg-color);
padding: 16px 24px;
border-radius: 8px;
&.blink {
animation-name: blink;
animation-timing-function: ease-in-out;
animation-iteration-count: 3;
animation-duration: 1s;
}
}
.timer-group {
grid-area: timer;
border-top: 2px solid var(--background-color-override, $viewer-background-color);
margin-top: 2em;
padding-top: 1em;
display: flex;
}
.timer-gap {
flex: 1;
max-width: 7.5em;
}
.aux-timers {
font-size: max(1vw, 16px);
&__label {
color: var(--label-color-override, $viewer-label-color);
font-weight: 600;
text-transform: uppercase;
}
&__value {
color: var(--secondary-color-override, $viewer-secondary-color);
font-size: clamp(24px, 2vw, 32px);
letter-spacing: 0.05em;
}
}
/* =================== MAIN - SCHEDULE ===================*/
.schedule-container {
grid-area: schedule;
overflow: hidden;
height: 100%;
margin-left: clamp(16px, 5vw, 64px);
}
.schedule-nav-container {
grid-area: schedule-nav;
align-self: center;
}
.info {
grid-area: info;
display: flex;
gap: max(1vw, 16px);
align-self: flex-end;
overflow: hidden;
&__message {
font-size: clamp(16px, 1.5vw, 24px);
line-height: 1.3em;
white-space: pre-line;
overflow: hidden;
flex: 1;
}
.qr {
padding: 4px;
background-color: white;
}
}
}
/* =================== AMIMATION ===================*/
@keyframes blink {
0% {
background-color: var(--card-background-color-override, $viewer-card-bg-color);
}
50% {
background-color: var(--card-background-color-blink-override, $playback-start);
}
100% {
background-color: var(--card-background-color-override, $viewer-card-bg-color);
}
}
@@ -1,164 +0,0 @@
import { useEffect, useState } from 'react';
import QRCode from 'react-qr-code';
import { useSearchParams } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { CustomFields, OntimeEvent, ProjectData, Settings, SupportedEvent } from 'ontime-types';
import { millisToString, removeLeadingZero } from 'ontime-utils';
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
import Schedule from '../../../common/components/schedule/Schedule';
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
import TitleCard from '../../../common/components/title-card/TitleCard';
import ViewLogo from '../../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { timerPlaceholderMin } from '../../../common/utils/styleUtils';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
import { titleVariants } from '../common/animation';
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
import { getPropertyValue } from '../common/viewUtils';
import { getBackstageOptions } from './backstage.options';
import './Backstage.scss';
export const MotionTitleCard = motion(TitleCard);
interface BackstageProps {
backstageEvents: OntimeEvent[];
customFields: CustomFields;
eventNext: OntimeEvent | null;
eventNow: OntimeEvent | null;
general: ProjectData;
isMirrored: boolean;
time: ViewExtendedTimer;
selectedId: string | null;
settings: Settings | undefined;
}
export default function Backstage(props: BackstageProps) {
const { backstageEvents, customFields, eventNext, eventNow, general, time, isMirrored, selectedId, settings } = props;
const { getLocalizedString } = useTranslation();
const [blinkClass, setBlinkClass] = useState(false);
const [searchParams] = useSearchParams();
useWindowTitle('Backstage');
// blink on change
useEffect(() => {
setBlinkClass(false);
const timer = setTimeout(() => {
setBlinkClass(true);
}, 10);
return () => clearTimeout(timer);
}, [selectedId]);
const clock = formatTime(time.clock);
const startedAt = formatTime(time.startedAt);
const isNegative = (time.current ?? 0) < 0;
const expectedFinish = isNegative ? getLocalizedString('countdown.overtime') : formatTime(time.expectedFinish);
const qrSize = Math.max(window.innerWidth / 15, 128);
const filteredEvents = backstageEvents.filter((event) => event.type === SupportedEvent.Event);
const showProgress = time.playback !== 'stop';
const secondarySource = searchParams.get('secondary-src');
const secondaryTextNext = getPropertyValue(eventNext, secondarySource);
const secondaryTextNow = getPropertyValue(eventNow, secondarySource);
let stageTimer = millisToString(time.current, { fallback: timerPlaceholderMin });
stageTimer = removeLeadingZero(stageTimer);
const defaultFormat = getDefaultFormat(settings?.timeFormat);
const backstageOptions = getBackstageOptions(defaultFormat, customFields);
return (
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
<ViewParamsEditor viewOptions={backstageOptions} />
<div className='project-header'>
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />}
{general.title}
<div className='clock-container'>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<SuperscriptTime time={clock} className='time' />
</div>
</div>
<ProgressBar
className='progress-container'
current={time.current}
duration={time.duration}
hidden={!showProgress}
/>
<div className='now-container'>
<AnimatePresence>
{eventNow && (
<motion.div
className={`event now ${blinkClass ? 'blink' : ''}`}
key='now'
variants={titleVariants}
initial='hidden'
animate='visible'
exit='exit'
>
<TitleCard title={eventNow.title} secondary={secondaryTextNow} />
<div className='timer-group'>
<div className='aux-timers'>
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
<SuperscriptTime time={startedAt} className='aux-timers__value' />
</div>
<div className='timer-gap' />
<div className='aux-timers'>
<div className='aux-timers__label'>{getLocalizedString('common.expected_finish')}</div>
{isNegative ? (
<div className='aux-timers__value'>{expectedFinish}</div>
) : (
<SuperscriptTime time={expectedFinish} className='aux-timers__value' />
)}
</div>
<div className='timer-gap' />
<div className='aux-timers'>
<div className='aux-timers__label'>{getLocalizedString('common.stage_timer')}</div>
<div className='aux-timers__value'>{stageTimer}</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{eventNext && (
<MotionTitleCard
className='event next'
key='next'
variants={titleVariants}
initial='hidden'
animate='visible'
exit='exit'
label='next'
title={eventNext.title}
secondary={secondaryTextNext}
/>
)}
</AnimatePresence>
</div>
<ScheduleProvider events={filteredEvents} selectedEventId={selectedId} isBackstage>
<ScheduleNav className='schedule-nav-container' />
<Schedule isProduction className='schedule-container' />
</ScheduleProvider>
<div className='info'>
{general.backstageUrl && <QRCode value={general.backstageUrl} size={qrSize} level='L' className='qr' />}
{general.backstageInfo && <div className='info__message'>{general.backstageInfo}</div>}
</div>
</div>
);
}
@@ -1,58 +0,0 @@
import { CustomFields } from 'ontime-types';
import {
getTimeOption,
makeOptionsFromCustomFields,
OptionTitle,
} from '../../../common/components/view-params-editor/constants';
import { ViewOption } from '../../../common/components/view-params-editor/types';
export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' });
return [
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
{
title: OptionTitle.DataSources,
collapsible: true,
options: [
{
id: 'secondary-src',
title: 'Event secondary text',
description: 'Select the data source for auxiliary text shown in now and next cards',
type: 'option',
values: secondaryOptions,
defaultValue: '',
},
],
},
{
title: OptionTitle.Schedule,
collapsible: true,
options: [
{
id: 'eventsPerPage',
title: 'Events per page',
description: 'Sets the number of events on the page, can cause overflow',
type: 'number',
placeholder: '8 (default)',
},
{
id: 'hidePast',
title: 'Hide past events',
description: 'Scheduler will only show upcoming events',
type: 'boolean',
defaultValue: false,
},
{
id: 'stopCycle',
title: 'Stop cycling through event pages',
description: 'Schedule will not auto-cycle through events',
type: 'boolean',
defaultValue: false,
},
],
},
];
};
@@ -3,9 +3,6 @@ import { useSearchParams } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { CustomFields, OntimeEvent, ProjectData, Settings } from 'ontime-types';
import Schedule from '../../../common/components/schedule/Schedule';
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
import TitleCard from '../../../common/components/title-card/TitleCard';
import ViewLogo from '../../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
@@ -13,6 +10,9 @@ import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
import Schedule from '../../../views/common/schedule/Schedule';
import { ScheduleProvider } from '../../../views/common/schedule/ScheduleContext';
import ScheduleNav from '../../../views/common/schedule/ScheduleNav';
import { titleVariants } from '../common/animation';
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
import { getPropertyValue } from '../common/viewUtils';