From d86afee47713ebb95966161e5bf959b636d1e200 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Tue, 20 Sep 2022 21:19:17 +0200 Subject: [PATCH] Feat/205 (#207) * feat(text-background): add discrete text background to * refactor: convert to typescript * feat(clock): create view * feat(clock): version bump --- client/package.json | 2 +- client/src/AppRouter.jsx | 4 + .../components/nav/navigatorConstants.js | 1 + client/src/common/models/OntimeTypes.ts | 20 +++ client/src/common/models/ViewTypes.ts | 14 ++ client/src/features/viewers/clock/Clock.scss | 23 +++ client/src/features/viewers/clock/Clock.tsx | 154 ++++++++++++++++++ .../viewers/minimal-timer/MinimalTimer.scss | 1 + .../{MinimalTimer.jsx => MinimalTimer.tsx} | 53 +++--- server/cypress/integration/navigation.spec.js | 10 ++ 10 files changed, 260 insertions(+), 22 deletions(-) create mode 100644 client/src/common/models/OntimeTypes.ts create mode 100644 client/src/common/models/ViewTypes.ts create mode 100644 client/src/features/viewers/clock/Clock.scss create mode 100644 client/src/features/viewers/clock/Clock.tsx rename client/src/features/viewers/minimal-timer/{MinimalTimer.jsx => MinimalTimer.tsx} (78%) diff --git a/client/package.json b/client/package.json index f808f26fc..e2b338fcc 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "ontime-ui", - "version": "1.8.0", + "version": "1.9.0", "private": true, "dependencies": { "@chakra-ui/react": "^2.3.2", diff --git a/client/src/AppRouter.jsx b/client/src/AppRouter.jsx index bd61c8e55..c44168e4b 100644 --- a/client/src/AppRouter.jsx +++ b/client/src/AppRouter.jsx @@ -12,6 +12,7 @@ const Table = lazy(() => import('features/table/ProtectedTable')); const TimerView = lazy(() => import('features/viewers/timer/Timer')); 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 Backstage = lazy(() => import('features/viewers/backstage/Backstage')); @@ -22,6 +23,7 @@ const StudioClock = lazy(() => import('features/viewers/studio/StudioClock')); const STimer = withSocket(TimerView); const SMinimalTimer = withSocket(MinimalTimerView); +const SClock = withSocket(ClockView); const SCountdown = withSocket(Countdown); const SBackstage = withSocket(Backstage); const SPublic = withSocket(Public); @@ -64,6 +66,8 @@ export default function AppRouter() { } /> } /> + } /> + } /> } /> diff --git a/client/src/common/components/nav/navigatorConstants.js b/client/src/common/components/nav/navigatorConstants.js index 93ea057d8..b9d37b44e 100644 --- a/client/src/common/components/nav/navigatorConstants.js +++ b/client/src/common/components/nav/navigatorConstants.js @@ -1,5 +1,6 @@ const navigatorConstants = [ { url: '/timer', label: 'Timer' }, + { url: '/clock', label: 'Clock' }, { url: '/minimal', label: 'Minimal Timer' }, { url: '/backstage', label: 'Backstage' }, { url: '/public', label: 'Public' }, diff --git a/client/src/common/models/OntimeTypes.ts b/client/src/common/models/OntimeTypes.ts new file mode 100644 index 000000000..0bf03c655 --- /dev/null +++ b/client/src/common/models/OntimeTypes.ts @@ -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; +} \ No newline at end of file diff --git a/client/src/common/models/ViewTypes.ts b/client/src/common/models/ViewTypes.ts new file mode 100644 index 000000000..105a4f72d --- /dev/null +++ b/client/src/common/models/ViewTypes.ts @@ -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; +} diff --git a/client/src/features/viewers/clock/Clock.scss b/client/src/features/viewers/clock/Clock.scss new file mode 100644 index 000000000..ebc4eb5c3 --- /dev/null +++ b/client/src/features/viewers/clock/Clock.scss @@ -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; + } +} diff --git a/client/src/features/viewers/clock/Clock.tsx b/client/src/features/viewers/clock/Clock.tsx new file mode 100644 index 000000000..17d6b39c3 --- /dev/null +++ b/client/src/features/viewers/clock/Clock.tsx @@ -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 ( +
+ {!userOptions?.hideNav && } +
+ {clock} +
+
+ ); +} diff --git a/client/src/features/viewers/minimal-timer/MinimalTimer.scss b/client/src/features/viewers/minimal-timer/MinimalTimer.scss index f8fa2574f..a143cd953 100644 --- a/client/src/features/viewers/minimal-timer/MinimalTimer.scss +++ b/client/src/features/viewers/minimal-timer/MinimalTimer.scss @@ -23,6 +23,7 @@ opacity: 1; transition: 0.5s; transition-property: opacity; + background-color: transparent; &--paused { opacity: 0.6; diff --git a/client/src/features/viewers/minimal-timer/MinimalTimer.jsx b/client/src/features/viewers/minimal-timer/MinimalTimer.tsx similarity index 78% rename from client/src/features/viewers/minimal-timer/MinimalTimer.jsx rename to client/src/features/viewers/minimal-timer/MinimalTimer.tsx index 3db3c3896..ea723c158 100644 --- a/client/src/features/viewers/minimal-timer/MinimalTimer.jsx +++ b/client/src/features/viewers/minimal-timer/MinimalTimer.tsx @@ -1,15 +1,26 @@ import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; -import PropTypes from 'prop-types'; import { overrideStylesURL } from '../../../common/api/apiConstants'; import NavLogo from '../../../common/components/nav/NavLogo'; 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 './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 { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const [searchParams] = useSearchParams(); @@ -26,7 +37,9 @@ export default function MinimalTimer(props) { // 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 = {}; + const userOptions: OverridableOptions = { + size: 1, + }; // key: string // Should be a hex string '#00FF00' with key colour @@ -42,6 +55,13 @@ export default function MinimalTimer(props) { 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'); @@ -52,8 +72,10 @@ export default function MinimalTimer(props) { // size: multiplier // Should be a number 0.0-n const size = searchParams.get('size'); - if (size) { - userOptions.size = size; + if (size !== null && typeof size !== 'undefined') { + if (!Number.isNaN(Number(size))) { + userOptions.size = Number(size); + } } // alignX: flex justification @@ -95,19 +117,13 @@ export default function MinimalTimer(props) { } const hideNav = searchParams.get('hidenav'); - if (hideNav) { - userOptions.hideNav = hideNav; - } + userOptions.hideNav = Boolean(hideNav); const hideOvertime = searchParams.get('hideovertime'); - if (hideOvertime) { - userOptions.hideOvertime = hideOvertime; - } + userOptions.hideOvertime = Boolean(hideOvertime); const hideMessagesOverlay = searchParams.get('hidemessages'); - if (hideMessagesOverlay) { - userOptions.hideMessagesOverlay = hideMessagesOverlay; - } + userOptions.hideMessagesOverlay = Boolean(hideMessagesOverlay); const showOverlay = pres.text !== '' && pres.visible; const isPlaying = time.playstate !== 'pause'; @@ -139,10 +155,11 @@ export default function MinimalTimer(props) { showFinished ? 'timer--finished' : '' }`} style={{ - fontSize: `${(89 / (clean.length - 1)) * userOptions.size}vw`, + fontSize: `${(89 / (clean.length - 1)) * (userOptions.size || 1)}vw`, fontFamily: userOptions.font, top: userOptions.top, left: userOptions.left, + backgroundColor: userOptions.textBackground, }} > {time.isNegative ? `-${timer}` : timer} @@ -150,9 +167,3 @@ export default function MinimalTimer(props) { ); } - -MinimalTimer.propTypes = { - pres: PropTypes.object, - time: PropTypes.object, - viewSettings: PropTypes.object, -}; diff --git a/server/cypress/integration/navigation.spec.js b/server/cypress/integration/navigation.spec.js index f23e1305b..3eb9f8a8c 100644 --- a/server/cypress/integration/navigation.spec.js +++ b/server/cypress/integration/navigation.spec.js @@ -92,6 +92,16 @@ describe('validate routes', () => { 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', () => { it('renders base route', () => { // base route