mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
Feat/205 (#207)
* feat(text-background): add discrete text background to <Minimal> * refactor: convert <Minimal> to typescript * feat(clock): create <Clock> view * feat(clock): version bump
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "1.8.0",
|
"version": "1.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.3.2",
|
"@chakra-ui/react": "^2.3.2",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const Table = lazy(() => import('features/table/ProtectedTable'));
|
|||||||
|
|
||||||
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
|
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
|
||||||
const MinimalTimerView = lazy(() => import('features/viewers/minimal-timer/MinimalTimer'));
|
const MinimalTimerView = lazy(() => import('features/viewers/minimal-timer/MinimalTimer'));
|
||||||
|
const ClockView = lazy(() => import('features/viewers/clock/Clock'));
|
||||||
const Countdown = lazy(() => import('features/viewers/countdown/Countdown'));
|
const Countdown = lazy(() => import('features/viewers/countdown/Countdown'));
|
||||||
|
|
||||||
const Backstage = lazy(() => import('features/viewers/backstage/Backstage'));
|
const Backstage = lazy(() => import('features/viewers/backstage/Backstage'));
|
||||||
@@ -22,6 +23,7 @@ const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
|
|||||||
|
|
||||||
const STimer = withSocket(TimerView);
|
const STimer = withSocket(TimerView);
|
||||||
const SMinimalTimer = withSocket(MinimalTimerView);
|
const SMinimalTimer = withSocket(MinimalTimerView);
|
||||||
|
const SClock = withSocket(ClockView);
|
||||||
const SCountdown = withSocket(Countdown);
|
const SCountdown = withSocket(Countdown);
|
||||||
const SBackstage = withSocket(Backstage);
|
const SBackstage = withSocket(Backstage);
|
||||||
const SPublic = withSocket(Public);
|
const SPublic = withSocket(Public);
|
||||||
@@ -64,6 +66,8 @@ export default function AppRouter() {
|
|||||||
<Route path='/minimalTimer' element={<SMinimalTimer />} />
|
<Route path='/minimalTimer' element={<SMinimalTimer />} />
|
||||||
<Route path='/simpleTimer' element={<SMinimalTimer />} />
|
<Route path='/simpleTimer' element={<SMinimalTimer />} />
|
||||||
|
|
||||||
|
<Route path='/clock' element={<SClock />} />
|
||||||
|
|
||||||
<Route path='/countdown' element={<SCountdown />} />
|
<Route path='/countdown' element={<SCountdown />} />
|
||||||
|
|
||||||
<Route path='/sm' element={<SBackstage />} />
|
<Route path='/sm' element={<SBackstage />} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const navigatorConstants = [
|
const navigatorConstants = [
|
||||||
{ url: '/timer', label: 'Timer' },
|
{ url: '/timer', label: 'Timer' },
|
||||||
|
{ url: '/clock', label: 'Clock' },
|
||||||
{ url: '/minimal', label: 'Minimal Timer' },
|
{ url: '/minimal', label: 'Minimal Timer' },
|
||||||
{ url: '/backstage', label: 'Backstage' },
|
{ url: '/backstage', label: 'Backstage' },
|
||||||
{ url: '/public', label: 'Public' },
|
{ url: '/public', label: 'Public' },
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
type Playstate = 'roll' | 'start' | 'pause' | 'stop';
|
||||||
|
|
||||||
|
export type TimeManager = {
|
||||||
|
clock: number,
|
||||||
|
running: number,
|
||||||
|
isNegative: boolean;
|
||||||
|
startedAt: null | number;
|
||||||
|
expectedFinish: null | number;
|
||||||
|
finished: boolean;
|
||||||
|
playstate: Playstate
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PresenterMessageData = {
|
||||||
|
text: string;
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ViewSettings = {
|
||||||
|
overrideStyles: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export type OverridableOptions = {
|
||||||
|
keyColour?: string;
|
||||||
|
textColour?: string;
|
||||||
|
textBackground?: string;
|
||||||
|
font?: string;
|
||||||
|
size?: number;
|
||||||
|
justifyContent?: 'start' | 'center' | 'end';
|
||||||
|
alignItems?: 'start' | 'center' | 'end';
|
||||||
|
left?: string;
|
||||||
|
top?: string;
|
||||||
|
hideNav?: boolean;
|
||||||
|
hideOvertime?: boolean;
|
||||||
|
hideMessagesOverlay?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
.clock-view {
|
||||||
|
background: var(--background-color-override, $viewer-background-color);
|
||||||
|
height: 100vh;
|
||||||
|
color: var(--color-override, $viewer-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1vw;
|
||||||
|
border: 1vw solid transparent;
|
||||||
|
|
||||||
|
.clock {
|
||||||
|
font-family: "Arial Black", sans-serif;
|
||||||
|
font-size: 20vw;
|
||||||
|
position: relative;
|
||||||
|
color: var(--timer-color-override, $viewer-color);
|
||||||
|
opacity: 1;
|
||||||
|
transition: 0.5s;
|
||||||
|
transition-property: opacity;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
|
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||||
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
|
import {
|
||||||
|
TimeManager,
|
||||||
|
ViewSettings,
|
||||||
|
} from '../../../common/models/OntimeTypes';
|
||||||
|
import { OverridableOptions } from '../../../common/models/ViewTypes';
|
||||||
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
|
||||||
|
import './Clock.scss';
|
||||||
|
|
||||||
|
interface ClockProps {
|
||||||
|
time: TimeManager;
|
||||||
|
viewSettings: ViewSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatOptions = {
|
||||||
|
showSeconds: true,
|
||||||
|
format: 'hh:mm:ss a',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Clock(props: ClockProps) {
|
||||||
|
const { time, viewSettings } = props;
|
||||||
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = 'ontime - Clock';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// defer rendering until we load stylesheets
|
||||||
|
if (!shouldRender) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get config from url: key, text, font, size, hidenav, hideovertime
|
||||||
|
// eg. http://localhost:3000/minimal?key=f00&text=fff
|
||||||
|
// Check for user options
|
||||||
|
const userOptions: OverridableOptions = {
|
||||||
|
size: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// key: string
|
||||||
|
// Should be a hex string '#00FF00' with key colour
|
||||||
|
const key = searchParams.get('key');
|
||||||
|
if (key) {
|
||||||
|
userOptions.keyColour = `#${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// textColour: string
|
||||||
|
// Should be a hex string '#ffffff'
|
||||||
|
const textColour = searchParams.get('text');
|
||||||
|
if (textColour) {
|
||||||
|
userOptions.textColour = `#${textColour}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// textBackground: string
|
||||||
|
// Should be a hex string '#ffffff'
|
||||||
|
const textBackground = searchParams.get('textbg');
|
||||||
|
if (textBackground) {
|
||||||
|
userOptions.textBackground = `#${textBackground}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// font: string
|
||||||
|
// Should be a string with a font name 'arial'
|
||||||
|
const font = searchParams.get('font');
|
||||||
|
if (font) {
|
||||||
|
userOptions.font = font;
|
||||||
|
}
|
||||||
|
|
||||||
|
// size: multiplier
|
||||||
|
// Should be a number 0.0-n
|
||||||
|
const size = searchParams.get('size');
|
||||||
|
if (size !== null && typeof size !== 'undefined') {
|
||||||
|
if (!Number.isNaN(Number(size))) {
|
||||||
|
userOptions.size = Number(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// alignX: flex justification
|
||||||
|
// start | center | end
|
||||||
|
const alignX = searchParams.get('alignx');
|
||||||
|
if (alignX) {
|
||||||
|
if (alignX === 'start' || alignX === 'center' || alignX === 'end') {
|
||||||
|
userOptions.justifyContent = alignX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// alignX: flex alignment
|
||||||
|
// start | center | end
|
||||||
|
const alignY = searchParams.get('aligny');
|
||||||
|
if (alignY) {
|
||||||
|
if (alignY === 'start' || alignY === 'center' || alignY === 'end') {
|
||||||
|
userOptions.alignItems = alignY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// offsetX: position in pixels
|
||||||
|
// Should be a number 0 - 1920
|
||||||
|
const offsetX = searchParams.get('offsetx');
|
||||||
|
if (offsetX) {
|
||||||
|
const pixels = Number(offsetX);
|
||||||
|
if (!isNaN(pixels)) {
|
||||||
|
userOptions.left = `${pixels}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// offsetX: position in pixels
|
||||||
|
// Should be a number 0 - 1920
|
||||||
|
const offsetY = searchParams.get('offsety');
|
||||||
|
if (offsetY) {
|
||||||
|
const pixels = Number(offsetY);
|
||||||
|
if (!isNaN(pixels)) {
|
||||||
|
userOptions.top = `${pixels}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideNav = searchParams.get('hidenav');
|
||||||
|
userOptions.hideNav = Boolean(hideNav);
|
||||||
|
|
||||||
|
const clock = formatTime(time.clock, formatOptions);
|
||||||
|
const clean = clock.replace('/:/g', '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='clock-view'
|
||||||
|
style={{
|
||||||
|
backgroundColor: userOptions.keyColour,
|
||||||
|
color: userOptions.textColour,
|
||||||
|
justifyContent: userOptions.justifyContent,
|
||||||
|
alignItems: userOptions.alignItems,
|
||||||
|
}}
|
||||||
|
data-testid='clock-view'
|
||||||
|
>
|
||||||
|
{!userOptions?.hideNav && <NavLogo />}
|
||||||
|
<div
|
||||||
|
className='clock'
|
||||||
|
style={{
|
||||||
|
fontSize: `${(89 / (clean.length - 1)) * (userOptions.size || 1)}vw`,
|
||||||
|
fontFamily: userOptions.font,
|
||||||
|
top: userOptions.top,
|
||||||
|
left: userOptions.left,
|
||||||
|
backgroundColor: userOptions.textBackground,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clock}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: 0.5s;
|
transition: 0.5s;
|
||||||
transition-property: opacity;
|
transition-property: opacity;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
&--paused {
|
&--paused {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
|||||||
+32
-21
@@ -1,15 +1,26 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
|
import {
|
||||||
|
PresenterMessageData,
|
||||||
|
TimeManager,
|
||||||
|
ViewSettings,
|
||||||
|
} from '../../../common/models/OntimeTypes';
|
||||||
|
import { OverridableOptions } from '../../../common/models/ViewTypes';
|
||||||
import { formatDisplay } from '../../../common/utils/dateConfig';
|
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||||
|
|
||||||
import './MinimalTimer.scss';
|
import './MinimalTimer.scss';
|
||||||
|
|
||||||
export default function MinimalTimer(props) {
|
interface MinimalTimerProps {
|
||||||
|
pres: PresenterMessageData;
|
||||||
|
time: TimeManager;
|
||||||
|
viewSettings: ViewSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MinimalTimer(props: MinimalTimerProps) {
|
||||||
const { pres, time, viewSettings } = props;
|
const { pres, time, viewSettings } = props;
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@@ -26,7 +37,9 @@ export default function MinimalTimer(props) {
|
|||||||
// get config from url: key, text, font, size, hidenav, hideovertime
|
// get config from url: key, text, font, size, hidenav, hideovertime
|
||||||
// eg. http://localhost:3000/minimal?key=f00&text=fff
|
// eg. http://localhost:3000/minimal?key=f00&text=fff
|
||||||
// Check for user options
|
// Check for user options
|
||||||
const userOptions = {};
|
const userOptions: OverridableOptions = {
|
||||||
|
size: 1,
|
||||||
|
};
|
||||||
|
|
||||||
// key: string
|
// key: string
|
||||||
// Should be a hex string '#00FF00' with key colour
|
// Should be a hex string '#00FF00' with key colour
|
||||||
@@ -42,6 +55,13 @@ export default function MinimalTimer(props) {
|
|||||||
userOptions.textColour = `#${textColour}`;
|
userOptions.textColour = `#${textColour}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// textBackground: string
|
||||||
|
// Should be a hex string '#ffffff'
|
||||||
|
const textBackground = searchParams.get('textbg');
|
||||||
|
if (textBackground) {
|
||||||
|
userOptions.textBackground = `#${textBackground}`;
|
||||||
|
}
|
||||||
|
|
||||||
// font: string
|
// font: string
|
||||||
// Should be a string with a font name 'arial'
|
// Should be a string with a font name 'arial'
|
||||||
const font = searchParams.get('font');
|
const font = searchParams.get('font');
|
||||||
@@ -52,8 +72,10 @@ export default function MinimalTimer(props) {
|
|||||||
// size: multiplier
|
// size: multiplier
|
||||||
// Should be a number 0.0-n
|
// Should be a number 0.0-n
|
||||||
const size = searchParams.get('size');
|
const size = searchParams.get('size');
|
||||||
if (size) {
|
if (size !== null && typeof size !== 'undefined') {
|
||||||
userOptions.size = size;
|
if (!Number.isNaN(Number(size))) {
|
||||||
|
userOptions.size = Number(size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// alignX: flex justification
|
// alignX: flex justification
|
||||||
@@ -95,19 +117,13 @@ export default function MinimalTimer(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hideNav = searchParams.get('hidenav');
|
const hideNav = searchParams.get('hidenav');
|
||||||
if (hideNav) {
|
userOptions.hideNav = Boolean(hideNav);
|
||||||
userOptions.hideNav = hideNav;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideOvertime = searchParams.get('hideovertime');
|
const hideOvertime = searchParams.get('hideovertime');
|
||||||
if (hideOvertime) {
|
userOptions.hideOvertime = Boolean(hideOvertime);
|
||||||
userOptions.hideOvertime = hideOvertime;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideMessagesOverlay = searchParams.get('hidemessages');
|
const hideMessagesOverlay = searchParams.get('hidemessages');
|
||||||
if (hideMessagesOverlay) {
|
userOptions.hideMessagesOverlay = Boolean(hideMessagesOverlay);
|
||||||
userOptions.hideMessagesOverlay = hideMessagesOverlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
const showOverlay = pres.text !== '' && pres.visible;
|
const showOverlay = pres.text !== '' && pres.visible;
|
||||||
const isPlaying = time.playstate !== 'pause';
|
const isPlaying = time.playstate !== 'pause';
|
||||||
@@ -139,10 +155,11 @@ export default function MinimalTimer(props) {
|
|||||||
showFinished ? 'timer--finished' : ''
|
showFinished ? 'timer--finished' : ''
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${(89 / (clean.length - 1)) * userOptions.size}vw`,
|
fontSize: `${(89 / (clean.length - 1)) * (userOptions.size || 1)}vw`,
|
||||||
fontFamily: userOptions.font,
|
fontFamily: userOptions.font,
|
||||||
top: userOptions.top,
|
top: userOptions.top,
|
||||||
left: userOptions.left,
|
left: userOptions.left,
|
||||||
|
backgroundColor: userOptions.textBackground,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{time.isNegative ? `-${timer}` : timer}
|
{time.isNegative ? `-${timer}` : timer}
|
||||||
@@ -150,9 +167,3 @@ export default function MinimalTimer(props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
MinimalTimer.propTypes = {
|
|
||||||
pres: PropTypes.object,
|
|
||||||
time: PropTypes.object,
|
|
||||||
viewSettings: PropTypes.object,
|
|
||||||
};
|
|
||||||
@@ -92,6 +92,16 @@ describe('validate routes', () => {
|
|||||||
cy.contains('Running Timer');
|
cy.contains('Running Timer');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('clock view timer', () => {
|
||||||
|
it('renders base route', () => {
|
||||||
|
// base route
|
||||||
|
cy.visit('http://localhost:4001/clock');
|
||||||
|
cy.get('[data-testid="clock-view"]').should('exist');
|
||||||
|
// ontime fallsback to stage timer on errors, so we check for that
|
||||||
|
cy.get('.App').should('not.contain', 'Time Now');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('minimal timer and options', () => {
|
describe('minimal timer and options', () => {
|
||||||
it('renders base route', () => {
|
it('renders base route', () => {
|
||||||
// base route
|
// base route
|
||||||
|
|||||||
Reference in New Issue
Block a user