mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-05 07:28:01 +00:00
feat: V2 ontime menu (#259)
This commit is contained in:
@@ -1,116 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { IconButton, Image } from '@chakra-ui/react';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import { IoSync } from '@react-icons/all-files/io5/IoSync';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { mirrorViewersAtom } from '../../atoms/ViewerSettings';
|
||||
import useFullscreen from '../../hooks/useFullscreen';
|
||||
|
||||
import navigatorConstants from './navigatorConstants';
|
||||
|
||||
import style from './NavLogo.module.scss';
|
||||
|
||||
/* Styling for action buttons */
|
||||
const navButtonStyle = {
|
||||
size: 'md',
|
||||
variant: 'outline',
|
||||
colorScheme: 'whiteAlpha',
|
||||
_hover: { bg: '#ebedf0', color: '#333' },
|
||||
};
|
||||
|
||||
export default function NavLogo(props) {
|
||||
const { isHidden } = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
const [isMirrored, setMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setShowNav((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback((e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Space bar
|
||||
if (e.keyCode === 32) {
|
||||
setShowNav((s) => !s);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
const baseOpacity = isHidden ? 0 : 0.5;
|
||||
const tabProps = {
|
||||
className: style.navItem,
|
||||
tabIndex: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
className={style.navContainer}
|
||||
data-testid='nav-logo'
|
||||
>
|
||||
<Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
|
||||
<AnimatePresence>
|
||||
{showNav && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scaleX: 0 }}
|
||||
animate={{ opacity: 1, scaleX: 1 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, scaleX: 0 }}
|
||||
className={style.actions}
|
||||
>
|
||||
<IconButton
|
||||
aria-label='Mirror screen'
|
||||
icon={<IoSync />}
|
||||
onClick={() => setMirrored((prev) => !prev)}
|
||||
{...navButtonStyle}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label='Toggle Fullscreen'
|
||||
icon={isFullScreen ? <IoContract /> : <IoExpand />}
|
||||
onClick={toggleFullScreen}
|
||||
{...navButtonStyle}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0, y: -50 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, scaleY: 0, y: -50 }}
|
||||
className={style.nav}
|
||||
>
|
||||
{navigatorConstants.map((route) => (
|
||||
<Link to={route.url} key={route.url} {...tabProps}>
|
||||
{route.label}
|
||||
</Link>
|
||||
))}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
NavLogo.propTypes = {
|
||||
isHidden: PropTypes.bool,
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
.navContainer {
|
||||
position: absolute;
|
||||
right: 2vw;
|
||||
top: 1vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-size: max(1vw, 16px);
|
||||
z-index: 10;
|
||||
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav,
|
||||
.showNav {
|
||||
margin-top: 2vh;
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.navItem {
|
||||
background-color: $bg-overlay;
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
position: absolute;
|
||||
top: 0.5vw;
|
||||
right: 75px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
$action-text-color: #87A3EF;
|
||||
$border-color: rgba(white, 0.1);
|
||||
$menu-bg: #202020;
|
||||
$menu-hover-bg: #101010;
|
||||
$menu-focus-bg: #181818;
|
||||
$menu-box-shadow: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
|
||||
|
||||
$icon-color: #f6f6f6;
|
||||
$button-bg: #303030;
|
||||
$button-size: 48px;
|
||||
|
||||
$ontime-pink: #ff7597;
|
||||
|
||||
// Todo: remove important once style is retired
|
||||
a {
|
||||
&::after {
|
||||
content: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mirror {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.navButton {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
left: 0.5em;
|
||||
top: 0.5em;
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.3s;
|
||||
opacity: 1;
|
||||
font-size: 24px;
|
||||
color: $icon-color;
|
||||
background-color: $button-bg;
|
||||
width: $button-size;
|
||||
height: $button-size;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
border-radius: 3px;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menuContainer {
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
height: fit-content;
|
||||
position: absolute;
|
||||
background-color: $menu-bg;
|
||||
min-width: 200px;
|
||||
border-radius: 0 0 24px 0;
|
||||
border-right: 1px solid $border-color;
|
||||
|
||||
box-shadow: $menu-box-shadow;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.buttonsContainer {
|
||||
margin-top: calc(56px + 1rem);
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $action-text-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background-color: $menu-hover-bg;
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $border-color;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: $menu-focus-bg;
|
||||
border-left: 2px solid $action-text-color;
|
||||
}
|
||||
|
||||
&.current {
|
||||
background-color: $menu-hover-bg;
|
||||
border-left: 4px solid $action-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.linkIcon {
|
||||
display: inline-block;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-color: $border-color;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { KeyboardEvent, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { mirrorViewersAtom } from '../../atoms/ViewerSettings';
|
||||
import useFullscreen from '../../hooks/useFullscreen';
|
||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
|
||||
// Todo: restyle _main.scss links
|
||||
export default function NavigationMenu() {
|
||||
const location = useLocation();
|
||||
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
const [isMirrored, setMirrored] = useAtom(mirrorViewersAtom);
|
||||
const [showButton, setShowButton] = useState(false);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
|
||||
const toggleMenu = () => setShowMenu((prev) => !prev);
|
||||
useKeyDown(toggleMenu, ' ');
|
||||
|
||||
useEffect(() => {
|
||||
let fadeOut: NodeJS.Timeout | null = null;
|
||||
const setShowMenuTrue = () => {
|
||||
setShowButton(true);
|
||||
if (fadeOut) {
|
||||
clearTimeout(fadeOut);
|
||||
}
|
||||
fadeOut = setTimeout(() => setShowButton(false), 3000);
|
||||
};
|
||||
document.addEventListener('mousemove', setShowMenuTrue);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', setShowMenuTrue);
|
||||
if (fadeOut) {
|
||||
clearTimeout(fadeOut);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
|
||||
const handleFullscreen = () => toggleFullScreen();
|
||||
const handleMirror = () => setMirrored((prev) => !prev);
|
||||
|
||||
return createPortal(
|
||||
<div id='navigation-menu-portal' className={isMirrored ? style.mirror : ''}>
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
aria-label='toggle menu'
|
||||
className={`${style.navButton} ${!showButton && !showMenu ? style.hidden : ''}`}
|
||||
>
|
||||
<IoApps />
|
||||
</button>
|
||||
|
||||
{showMenu && (
|
||||
<div className={style.menuContainer} data-testid='navigation-menu'>
|
||||
<div className={style.buttonsContainer}>
|
||||
<div
|
||||
className={style.link}
|
||||
tabIndex={0}
|
||||
onClick={handleFullscreen}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && handleFullscreen();
|
||||
}}
|
||||
>
|
||||
Toggle Fullscreen
|
||||
{isFullScreen ? <IoContract /> : <IoExpand />}
|
||||
</div>
|
||||
<div
|
||||
className={style.link}
|
||||
tabIndex={0}
|
||||
onClick={handleMirror}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && handleMirror();
|
||||
}}>
|
||||
Mirror Screen
|
||||
<IoSwapVertical />
|
||||
</div>
|
||||
{/*<div className={style.link} tabIndex={0}>*/}
|
||||
{/* Rename Client*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
<hr className={style.separator} />
|
||||
{navigatorConstants.map((route) => (
|
||||
<Link
|
||||
key={route.url}
|
||||
to={route.url}
|
||||
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
||||
tabIndex={0}>
|
||||
{route.label}
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>, document.body);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useKeyDown = (callback: () => void, targetKey: string) => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const targetKeyPressed = event.key === targetKey && !event.repeat;
|
||||
if (targetKeyPressed) {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [onKeyDown]);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
// Vitest Snapshot v1
|
||||
|
||||
exports[`cx() > ignores falsy values 1`] = `""`;
|
||||
|
||||
exports[`cx() > merges styles 1`] = `"_test_98a1e0 _another_98a1e0"`;
|
||||
@@ -0,0 +1,2 @@
|
||||
.test {}
|
||||
.another {}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { cx } from '../styleUtils';
|
||||
|
||||
import style from './styleUtils.module.scss';
|
||||
|
||||
describe('cx()', () => {
|
||||
test('merges styles', () => {
|
||||
const merged = cx([style.test, style.another]);
|
||||
expect(merged).toMatchSnapshot();
|
||||
});
|
||||
test('ignores falsy values', () => {
|
||||
const falsyStuff = false;
|
||||
const merged = cx([undefined, false, 0, null, falsyStuff ? style.test : null]);
|
||||
console.log(merged)
|
||||
expect(merged).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -26,4 +26,4 @@ export const getAccessibleColour = (bgColour: string): ColourCombination => {
|
||||
* @description Creates a list of classnames from array of css module conditions
|
||||
* @param classNames - css modules objects
|
||||
*/
|
||||
export const cx = (...classNames: any[]) => classNames.filter(Boolean).join(" ")
|
||||
export const cx = (classNames: any[]) => classNames.filter(Boolean).join(" ");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import TitleSide from 'common/components/title-side/TitleSide';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
@@ -9,6 +8,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import Paginator from '../../../common/components/paginator/Paginator';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||
@@ -62,7 +62,7 @@ export default function Backstage(props) {
|
||||
|
||||
return (
|
||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
||||
<NavLogo />
|
||||
<NavigationMenu />
|
||||
|
||||
<div className='event-title'>{general.title}</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAtom } from 'jotai';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManaget.type';
|
||||
import { ViewSettingsType } from '../../../common/models/ViewSettings.type';
|
||||
@@ -120,9 +120,6 @@ export default function Clock(props: ClockProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const hideNav = searchParams.get('hidenav');
|
||||
userOptions.hideNav = Boolean(hideNav);
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const clean = clock.replace('/:/g', '');
|
||||
|
||||
@@ -137,7 +134,7 @@ export default function Clock(props: ClockProps) {
|
||||
}}
|
||||
data-testid='clock-view'
|
||||
>
|
||||
{!userOptions?.hideNav && <NavLogo />}
|
||||
<NavigationMenu />
|
||||
<div
|
||||
className='clock'
|
||||
style={{
|
||||
|
||||
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import Empty from '../../../common/components/state/Empty';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
||||
@@ -94,7 +94,7 @@ export default function Countdown(props) {
|
||||
|
||||
return (
|
||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||
<NavLogo />
|
||||
<NavigationMenu />
|
||||
{follow === null ? (
|
||||
<div className='event-select' data-testid='countdown-select'>
|
||||
<span className='event-select__title'>Select an event to follow</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import NavLogo from "../../../common/components/nav/NavLogo";
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
|
||||
import './LowerClean.scss';
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function LowerClean(props) {
|
||||
}}
|
||||
>
|
||||
|
||||
<NavLogo isHidden />
|
||||
<NavigationMenu />
|
||||
|
||||
<AnimatePresence>
|
||||
{showLower && (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import NavLogo from "../../../common/components/nav/NavLogo";
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
|
||||
import './LowerLines.scss';
|
||||
|
||||
@@ -130,7 +130,7 @@ export default function LowerLines(props) {
|
||||
}}
|
||||
>
|
||||
|
||||
<NavLogo isHidden />
|
||||
<NavigationMenu />
|
||||
|
||||
<AnimatePresence>
|
||||
{showLower && (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAtom } from 'jotai';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { PresenterMessageType } from '../../../common/models/PresenterMessage.type';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManaget.type';
|
||||
@@ -117,9 +117,6 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const hideNav = searchParams.get('hidenav');
|
||||
userOptions.hideNav = Boolean(hideNav);
|
||||
|
||||
const hideOvertime = searchParams.get('hideovertime');
|
||||
userOptions.hideOvertime = Boolean(hideOvertime);
|
||||
|
||||
@@ -144,6 +141,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
}}
|
||||
data-testid='minimal-timer'
|
||||
>
|
||||
<NavigationMenu />
|
||||
{!hideMessagesOverlay && (
|
||||
<div
|
||||
className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}
|
||||
@@ -151,7 +149,6 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
<div className='message'>{pres.text}</div>
|
||||
</div>
|
||||
)}
|
||||
{!userOptions?.hideNav && <NavLogo />}
|
||||
<div
|
||||
className={`timer ${!isPlaying ? 'timer--paused' : ''} ${
|
||||
showFinished ? 'timer--finished' : ''
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
@@ -9,6 +8,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import Paginator from '../../../common/components/paginator/Paginator';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
@@ -69,7 +69,7 @@ export default function Pip(props) {
|
||||
|
||||
return (
|
||||
<div className={`pip ${isMirrored ? 'mirror' : ''}`} data-testid='pip-view'>
|
||||
<NavLogo />
|
||||
<NavigationMenu />
|
||||
|
||||
<div className='event-title'>{general.title}</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import TitleSide from 'common/components/title-side/TitleSide';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
@@ -8,6 +7,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import Paginator from '../../../common/components/paginator/Paginator';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
@@ -42,7 +42,7 @@ export default function Public(props) {
|
||||
|
||||
return (
|
||||
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
||||
<NavLogo />
|
||||
<NavigationMenu />
|
||||
|
||||
<div className='event-title'>{general.title}</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import useFitText from '../../../common/hooks/useFitText';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||
@@ -58,7 +58,7 @@ export default function StudioClock(props) {
|
||||
|
||||
return (
|
||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||
<NavLogo />
|
||||
<NavigationMenu />
|
||||
<div className='clock-container'>
|
||||
<div className='studio-timer'>{clock}</div>
|
||||
<div
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import TimerDisplay from 'common/components/countdown/TimerDisplay';
|
||||
import MyProgressBar from 'common/components/myProgressBar/MyProgressBar';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import TitleCard from 'common/components/title-card/TitleCard';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
@@ -10,6 +9,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
@@ -72,12 +72,12 @@ export default function Timer(props) {
|
||||
return (
|
||||
<div className={time.finished ? `${baseClasses} stage-timer--finished` : baseClasses}
|
||||
data-testid='timer-view'>
|
||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||
<NavigationMenu />
|
||||
<div
|
||||
className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||
<div className='message'>{pres.text}</div>
|
||||
</div>
|
||||
|
||||
<NavLogo />
|
||||
|
||||
<div className='clock-container'>
|
||||
<div className='label'>Time Now</div>
|
||||
<div className='clock'>{clock}</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const navigatorConstants = [
|
||||
export const navigatorConstants = [
|
||||
{ url: '/timer', label: 'Timer' },
|
||||
{ url: '/clock', label: 'Clock' },
|
||||
{ url: '/minimal', label: 'Minimal Timer' },
|
||||
@@ -9,5 +9,3 @@ const navigatorConstants = [
|
||||
{ url: '/studio', label: 'Studio Clock' },
|
||||
{ url: '/countdown', label: 'Countdown' },
|
||||
];
|
||||
|
||||
export default navigatorConstants;
|
||||
@@ -12,39 +12,23 @@ import { devices } from '@playwright/test';
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './tests',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
timeout: 60 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
screenshot: 'only-on-failure', // Capture screenshot after each test failure
|
||||
screenshot: 'only-on-failure',
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
actionTimeout: 5000,
|
||||
baseURL: 'http://localhost:4001',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
@@ -55,7 +39,7 @@ const config: PlaywrightTestConfig = {
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
testMatch: /.*.spec.ts/,
|
||||
testMatch: /.*.spec.all.ts/,
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
},
|
||||
@@ -70,14 +54,13 @@ const config: PlaywrightTestConfig = {
|
||||
},
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'yarn start:server',
|
||||
timeout: 120 * 1000,
|
||||
port: 4001,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ test('test', async ({ context }) => {
|
||||
|
||||
// stage timer message
|
||||
await editorPage.getByPlaceholder('Shown in stage timer').click();
|
||||
await editorPage.getByPlaceholder('Shown in stage timer').fill('testing');
|
||||
await editorPage.getByPlaceholder('Shown in stage timer').fill('testing stage');
|
||||
await editorPage.getByRole('button', { name: 'toggle timer screen message' }).click();
|
||||
|
||||
await featurePage.goto('http://localhost:4001/timer');
|
||||
await featurePage.getByText('testing').click();
|
||||
await featurePage.getByText('testing stage').click();
|
||||
|
||||
// public screen message
|
||||
await editorPage.getByPlaceholder('Shown in public screens').click();
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('test view navigation feature', () => {
|
||||
test('user flow through links', async ({ page }) => {
|
||||
// default view is timer view
|
||||
await page.goto('http://localhost:4001/');
|
||||
await page.locator('data-test-id=timer-view');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation-menu');
|
||||
await page.getByRole('link', { name: 'Clock' }).click();
|
||||
await page.locator('data-test-id=clock-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/clock');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation-menu');
|
||||
await page.getByRole('link', { name: 'Minimal Timer' }).click();
|
||||
await page.locator('data-test-id=minimal-timer');
|
||||
await expect(page).toHaveURL('http://localhost:4001/minimal');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation-menu');
|
||||
await page.getByRole('link', { name: 'Backstage' }).click();
|
||||
await page.locator('data-test-id=backstage-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/backstage');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation-menu');
|
||||
await page.getByRole('link', { name: 'Public' }).click();
|
||||
await page.locator('data-test-id=public-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/public');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation-menu');
|
||||
await page.getByRole('link', { name: 'Lower Thirds' }).click();
|
||||
await expect(page).toHaveURL('http://localhost:4001/lower');
|
||||
const errorBoundary = await page.locator('data-test-id=error-container');
|
||||
await expect(errorBoundary).toHaveCount(0);
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation-menu');
|
||||
await page.getByRole('link', { name: 'PiP' }).click();
|
||||
await page.locator('data-test-id=pip-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/pip');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation-menu');
|
||||
await page.getByRole('link', { name: 'Studio Clock' }).click();
|
||||
await page.locator('data-test-id=studio-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/studio');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation-menu');
|
||||
await page.getByRole('link', { name: 'Countdown' }).click();
|
||||
await page.locator('data-test-id=countdown-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/countdown');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation-menu');
|
||||
await page.getByRole('link', { name: 'Timer' }).click();
|
||||
await page.locator('data-test-id=timer-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/timer');
|
||||
});
|
||||
});
|
||||
@@ -39,40 +39,4 @@ test.describe('pages routes are available', () => {
|
||||
await page.getByTestId('panel-info').click();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('client routes', () => {
|
||||
test('stage timer', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/timer');
|
||||
await page.getByTestId('timer-view').click();
|
||||
});
|
||||
test('clock', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/clock');
|
||||
await page.getByTestId('clock-view').click();
|
||||
});
|
||||
test('minimal timer', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/minimal');
|
||||
await page.getByTestId('minimal-timer').click();
|
||||
});
|
||||
test('backstage', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/backstage');
|
||||
await page.getByTestId('backstage-view').click();
|
||||
});
|
||||
test('public', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/public');
|
||||
await page.getByTestId('public-view').click();
|
||||
});
|
||||
test('pip', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/pip');
|
||||
await page.getByTestId('pip-view').click();
|
||||
});
|
||||
test('studio clock', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/studio');
|
||||
await page.getByTestId('studio-view').click();
|
||||
});
|
||||
test('lower', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/lower');
|
||||
const errorBoundary = await page.locator('data-test-id=error-container');
|
||||
await expect(errorBoundary).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user