diff --git a/client/package.json b/client/package.json index 4216b0991..89a21abaa 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "ontime-ui", - "version": "1.2.3", + "version": "1.3.0", "private": true, "dependencies": { "@chakra-ui/react": "^2.0.0-next.3", diff --git a/client/src/app/appConstants.js b/client/src/app/appConstants.js index a901ed110..72c1578b2 100644 --- a/client/src/app/appConstants.js +++ b/client/src/app/appConstants.js @@ -1,4 +1,5 @@ // Exported viewer link location +const minimalLocation = 'minimal'; const speakerLocation = 'speaker'; const smLocation = 'sm'; const publicLocation = 'public'; @@ -7,6 +8,7 @@ const studioLocation = 'studio'; const cuesheetLocation = 'cuesheet'; export const viewerLocations = [ + { link: minimalLocation, label: 'Minimal timer' }, { link: speakerLocation, label: 'Speaker Screen' }, { link: smLocation, label: 'Backstage Screen' }, { link: publicLocation, label: 'Public Screen' }, diff --git a/client/src/common/components/nav/NavLogo.jsx b/client/src/common/components/nav/NavLogo.jsx index 1e9ed0320..7d253c63f 100644 --- a/client/src/common/components/nav/NavLogo.jsx +++ b/client/src/common/components/nav/NavLogo.jsx @@ -68,6 +68,7 @@ export default function NavLogo(props) { initial={{ opacity: showNav ? 0.5 : baseOpacity }} whileHover={{ opacity: 1 }} className={style.navContainer} + data-testid='nav-logo' > diff --git a/client/src/features/viewers/timer/MinimalTimer.jsx b/client/src/features/viewers/timer/MinimalTimer.jsx index fd143b367..7459b832a 100644 --- a/client/src/features/viewers/timer/MinimalTimer.jsx +++ b/client/src/features/viewers/timer/MinimalTimer.jsx @@ -1,4 +1,5 @@ -import React, { useEffect } from 'react'; +import React from 'react'; +import { useSearchParams } from 'react-router-dom'; import PropTypes from 'prop-types'; import NavLogo from '../../../common/components/nav/NavLogo'; @@ -8,26 +9,127 @@ import style from './MinimalTimer.module.scss'; export default function MinimalTimer(props) { const { pres, time } = props; + const [searchParams] = useSearchParams(); - // Set window title - useEffect(() => { - document.title = 'ontime - Minimal Timer'; - }, []); + document.title = 'ontime - Minimal Timer'; + + // 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 = {}; + + // 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}`; + } + + // 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) { + userOptions.size = 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'); + if (hideNav) { + userOptions.hideNav = hideNav; + } + + const hideOvertime = searchParams.get('hideovertime'); + if (hideOvertime) { + userOptions.hideOvertime = hideOvertime; + } + + const hideMessagesOverlay = searchParams.get('hidemessages'); + if (hideMessagesOverlay) { + userOptions.hideMessagesOverlay = hideMessagesOverlay; + } const showOverlay = pres.text !== '' && pres.visible; const isPlaying = time.playstate !== 'pause'; const timer = formatDisplay(time.running, true); const clean = timer.replace('/:/g', ''); + const finishedStyle = userOptions?.hideOvertime ? style.container : style.containerFinished; return ( -
-
-
{pres.text}
-
- +
+ {!hideMessagesOverlay && ( +
+
{pres.text}
+
+ )} + {!userOptions?.hideNav && }
{time.isNegative ? `-${timer}` : timer}
diff --git a/client/src/features/viewers/timer/MinimalTimer.module.scss b/client/src/features/viewers/timer/MinimalTimer.module.scss index 40a0637b0..f06f0248b 100644 --- a/client/src/features/viewers/timer/MinimalTimer.module.scss +++ b/client/src/features/viewers/timer/MinimalTimer.module.scss @@ -5,10 +5,11 @@ background: $bg-black; height: 100vh; color: $title-white; - display: grid; - place-content: center; + display: flex; + align-items: center; + justify-content: center; gap: 1vw; - border: 1vw solid $bg-black; + border: 1vw solid transparent; } .containerFinished { @@ -21,6 +22,7 @@ .timerPaused { font-family: "Arial Black", sans-serif; font-size: 20vw; + position: relative; color: inherit; opacity: 1; transition: 0.5s; diff --git a/server/cypress/integration/navigation.spec.js b/server/cypress/integration/navigation.spec.js index c37c85c94..f23e1305b 100644 --- a/server/cypress/integration/navigation.spec.js +++ b/server/cypress/integration/navigation.spec.js @@ -91,4 +91,33 @@ describe('validate routes', () => { cy.visit('http://localhost:4001/cuelist'); cy.contains('Running Timer'); }); + + describe('minimal timer and options', () => { + it('renders base route', () => { + // base route + cy.visit('http://localhost:4001/minimal'); + cy.get('[data-testid="minimal-timer"]').should('exist'); + // ontime fallsback to stage timer on errors, so we check for that + cy.get('.App').should('not.contain', 'Time Now'); + }); + + it('renders with defined options', () => { + // route with options + cy.visit( + 'http://localhost:4001/minimal?font=arial=1&size=1.5&text=0f0&key=ff0&hideovertime=true&alignx=start&aligny=end&offsetx=100&offsety=-100&hidemessages=true' + ); + cy.get('[data-testid="minimal-timer"]').should('exist'); + // ontime fallback to stage timer on errors, so we check for that + cy.get('.App').should('not.contain', 'Time Now'); + }); + + it('hides nav on given option', () => { + // route with options for no nav + cy.visit('http://localhost:4001/minimal?hidenav=true'); + cy.get('[data-testid="minimal-timer"]').should('exist'); + // ontime fallback to stage timer on errors, so we check for that + cy.get('.App').should('not.contain', 'Time Now'); + cy.get('[data-testid="nav-logo"]').should('not.exist'); + }); + }); });