refactor: extract navigation elements

This commit is contained in:
Carlos Valente
2025-06-22 11:34:00 +02:00
committed by Carlos Valente
parent 7fdda1c969
commit 269091986a
26 changed files with 421 additions and 356 deletions
@@ -1,17 +1,9 @@
import { useState } from 'react'; import { useState } from 'react';
import {
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';
import Button from '../../../common/components/buttons/Button';
import { setClientRemote } from '../../hooks/useSocket'; import { setClientRemote } from '../../hooks/useSocket';
import Dialog from '../dialog/Dialog';
import Input from '../input/input/Input';
interface RenameClientModalProps { interface RenameClientModalProps {
id: string; id: string;
@@ -20,8 +12,7 @@ interface RenameClientModalProps {
onClose: () => void; onClose: () => void;
} }
export function RenameClientModal(props: RenameClientModalProps) { export function RenameClientModal({ id, name: currentName = '', isOpen, onClose }: RenameClientModalProps) {
const { id, name: currentName = '', isOpen, onClose } = props;
const [name, setName] = useState(currentName); const [name, setName] = useState(currentName);
const { setClientName } = setClientRemote; const { setClientName } = setClientRemote;
@@ -36,29 +27,24 @@ export function RenameClientModal(props: RenameClientModalProps) {
const canSubmit = name !== currentName && name !== ''; const canSubmit = name !== currentName && name !== '';
return ( return (
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'> <Dialog
<ModalOverlay /> isOpen={isOpen}
<ModalContent> title={`Rename Client: ${currentName}`}
<ModalHeader>Rename: {currentName}</ModalHeader> showCloseButton
<ModalCloseButton /> onClose={onClose}
<ModalBody> bodyElements={
<Input <Input height='large' placeholder='New name' value={name} onChange={(event) => setName(event.target.value)} />
variant='ontime-filled' }
size='md' footerElements={
placeholder='new name' <>
value={name} <Button variant='subtle' size='large' onClick={onClose}>
onChange={(event) => setName(event.target.value)}
/>
</ModalBody>
<ModalFooter>
<Button size='md' variant='ontime-subtle' onClick={onClose}>
Cancel Cancel
</Button> </Button>
<Button size='md' variant='ontime-filled' onClick={handleRename} isDisabled={!canSubmit}> <Button variant='primary' size='large' onClick={handleRename} disabled={!canSubmit}>
Submit Submit
</Button> </Button>
</ModalFooter> </>
</ModalContent> }
</Modal> />
); );
} }
@@ -1,107 +1,68 @@
@use '../../../theme/mixins' as *;
$menu-hover-bg: $gray-1350;
$menu-focus-bg: $gray-1300;
$button-size: 3rem;
.fadeable {
opacity: 1;
transition-property: opacity;
transition-duration: 0.3s;
&.hidden {
opacity: 0;
pointer-events: none;
}
}
.lockIcon {
font-size: 1.5rem;
color: $active-indicator;
padding: 0.5em;
position: fixed;
left: 0;
top: 0;
z-index: 12;
}
.buttonContainer {
display: flex;
flex-direction: column;
row-gap: 1rem;
padding: 0.5em;
position: fixed;
left: 0;
top: 0;
z-index: 12;
}
.navButton {
font-size: 1.5rem;
height: $button-size;
width: $button-size;;
}
.link {
@include action-link;
padding: 0.75rem 1.5rem;
gap: 0.5rem;
width: 100%;
cursor: pointer;
&:hover {
background-color: $menu-hover-bg;
}
&:active {
background-color: $border-color-ondark;
}
&: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 {
margin-left: auto;
@include rotate-fourty-five;
}
.separator { .separator {
border-color: $border-color-ondark; border-color: $border-color-ondark;
} }
.sectionHeader { .backdrop {
font-size: calc(1rem - 2px); position: fixed;
margin-left: 1rem; inset: 0;
color: $gray-700; z-index: $zindex-backdrop;
background-color: $backdrop-color;
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
}
} }
.bottom { .drawer {
margin-top: auto; box-sizing: border-box;
margin-bottom: 1rem; position: fixed;
} top: 0;
left: 0;
bottom: 0;
z-index: $zindex-dialog;
width: 22rem;
height: 100vh;
.interfaces {
padding: 0.5rem 1rem;
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
gap: 0.5rem; padding-block: 1rem;
background-color: $gray-1250;
color: $ui-white;
border-right: 1px solid $gray-1200;
&[data-open] {
transform: translateX(0%);
transition: transform 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
}
&[data-starting-style],
&[data-ending-style] {
transform: translateX(-100%);
transition: transform 500ms cubic-bezier(0.45, 1.005, 0, 1.005);
}
// take the whole screen in mobile devices
@media (max-width: $min-tablet) {
width: 100vw;
}
} }
.goIcon { .header {
@include rotate-fourty-five; display: flex;
margin-left: 0.25rem; align-items: center;
margin-bottom: 0.25rem; justify-content: space-between;
height: 3.5rem;
padding-inline: 1.5rem;
font-weight: 600;
font-size: 1.25rem;
}
.body {
flex: 1;
overflow-y: auto;
} }
@@ -1,29 +1,20 @@
import { memo, PropsWithChildren } from 'react'; import { memo } from 'react';
import { createPortal } from 'react-dom'; import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
import { IoArrowUp, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5'; import { useLocation } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom'; import { Dialog } from '@base-ui-components/react/dialog';
import { import { useDisclosure, useFullscreen } from '@mantine/hooks';
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerHeader,
DrawerOverlay,
useDisclosure,
} from '@chakra-ui/react';
import { useFullscreen } from '@mantine/hooks';
import { isLocalhost } from '../../../externals'; import { isLocalhost } from '../../../externals';
import { navigatorConstants } from '../../../viewerConfig'; import { navigatorConstants } from '../../../viewerConfig';
import { useElectronEvent } from '../../hooks/useElectronEvent';
import useInfo from '../../hooks-query/useInfo';
import { useClientStore } from '../../stores/clientStore'; import { useClientStore } from '../../stores/clientStore';
import { useViewOptionsStore } from '../../stores/viewOptions'; import { useViewOptionsStore } from '../../stores/viewOptions';
import { isKeyEnter } from '../../utils/keyEvent'; import IconButton from '../buttons/IconButton';
import { handleLinks, linkToOtherHost, openLink } from '../../utils/linkUtils';
import { cx } from '../../utils/styleUtils';
import { RenameClientModal } from '../client-modal/RenameClientModal'; import { RenameClientModal } from '../client-modal/RenameClientModal';
import CopyTag from '../copy-tag/CopyTag';
import ClientLink from './client-link/ClientLink';
import EditorNavigation from './editor-navigation/EditorNavigation';
import NavigationMenuItem from './navigation-menu-item/NavigationMenuItem';
import OtherAddresses from './other-addresses/OtherAddresses';
import style from './NavigationMenu.module.scss'; import style from './NavigationMenu.module.scss';
@@ -32,74 +23,49 @@ interface NavigationMenuProps {
onClose: () => void; onClose: () => void;
} }
function NavigationMenu(props: NavigationMenuProps) { export default memo(NavigationMenu);
const { isOpen, onClose } = props; function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
const id = useClientStore((store) => store.id); const id = useClientStore((store) => store.id);
const name = useClientStore((store) => store.name); const name = useClientStore((store) => store.name);
const { isOpen: isOpenRename, onOpen: onRenameOpen, onClose: onCloseRename } = useDisclosure(); const [isRenameOpen, handlers] = useDisclosure(false);
const { fullscreen, toggle } = useFullscreen(); const { fullscreen, toggle } = useFullscreen();
const { mirror, toggleMirror } = useViewOptionsStore(); const { mirror, toggleMirror } = useViewOptionsStore();
const location = useLocation(); const location = useLocation();
return createPortal( return (
<div id='navigation-menu-portal'> <Dialog.Root
<RenameClientModal id={id} name={name} isOpen={isOpenRename} onClose={onCloseRename} /> open={isOpen}
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'> onOpenChange={(open) => {
<DrawerOverlay /> if (!open) {
<DrawerContent maxWidth='22rem'> onClose();
<DrawerHeader> }
<DrawerCloseButton size='lg' /> }}
Ontime >
</DrawerHeader> <Dialog.Portal>
<DrawerBody padding={0}> <Dialog.Backdrop className={style.backdrop} />
<div className={style.buttonsContainer}> <RenameClientModal id={id} name={name} isOpen={isRenameOpen} onClose={handlers.close} />
<div <Dialog.Popup className={style.drawer}>
className={cx([style.link, fullscreen && style.current])} <div className={style.header}>
tabIndex={0} <Dialog.Title>Ontime</Dialog.Title>
role='button' <IconButton variant='subtle-white' size='large' onClick={onClose}>
onClick={toggle} <IoClose />
onKeyDown={(event) => { </IconButton>
isKeyEnter(event) && toggle(); </div>
}} <div className={style.body}>
> <NavigationMenuItem active={fullscreen} onClick={toggle}>
Toggle Fullscreen Toggle Fullscreen
{fullscreen ? <IoContract /> : <IoExpand />} {fullscreen ? <IoContract /> : <IoExpand />}
</div> </NavigationMenuItem>
<div <NavigationMenuItem active={mirror} onClick={toggleMirror}>
className={cx([style.link, mirror && style.current])} Flip Screen
tabIndex={0} <IoSwapVertical />
role='button' </NavigationMenuItem>
onClick={() => toggleMirror()} <NavigationMenuItem onClick={handlers.open}>Rename Client</NavigationMenuItem>
onKeyDown={(event) => {
isKeyEnter(event) && toggleMirror();
}}
>
Flip Screen
<IoSwapVertical />
</div>
<div
className={style.link}
tabIndex={0}
role='button'
onClick={onRenameOpen}
onKeyDown={(event) => {
isKeyEnter(event) && onRenameOpen();
}}
>
Rename Client
</div>
</div>
<hr className={style.separator} /> <hr className={style.separator} />
<Link
to='/editor' <EditorNavigation />
tabIndex={0}
className={`${style.link} ${location.pathname === '/editor' && style.current}`}
>
<IoLockClosedOutline />
Editor
</Link>
<ClientLink to='cuesheet' current={location.pathname === '/cuesheet'}> <ClientLink to='cuesheet' current={location.pathname === '/cuesheet'}>
<IoLockClosedOutline /> <IoLockClosedOutline />
Cuesheet Cuesheet
@@ -108,86 +74,23 @@ function NavigationMenu(props: NavigationMenuProps) {
<IoLockClosedOutline /> <IoLockClosedOutline />
Operator Operator
</ClientLink> </ClientLink>
<hr className={style.separator} /> <hr className={style.separator} />
{navigatorConstants.map((route) => ( {navigatorConstants.map((route) => (
<ClientLink key={route.url} to={route.url} current={location.pathname === `/${route.url}`}> <ClientLink key={route.url} to={route.url} current={location.pathname === `/${route.url}`}>
{route.label} {route.label}
</ClientLink> </ClientLink>
))} ))}
{isLocalhost && <OtherAddresses currentLocation={location.pathname} />} </div>
</DrawerBody>
</DrawerContent> {isLocalhost && (
</Drawer> <div>
</div>, <OtherAddresses currentLocation={location.pathname} />
document.body, </div>
)}
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
); );
} }
interface OtherAddressesProps {
currentLocation: string;
}
function OtherAddresses(props: OtherAddressesProps) {
const { currentLocation } = props;
const { data } = useInfo();
// there is no point showing this if we only have one interface
if (data.networkInterfaces.length < 2) {
return null;
}
return (
<div className={style.bottom}>
<div className={style.sectionHeader}>Accessible on external networks</div>
<div className={style.interfaces}>
{data?.networkInterfaces?.map((nif) => {
if (nif.name === 'localhost') {
return null;
}
const address = linkToOtherHost(nif.address, currentLocation);
return (
<CopyTag
key={nif.name}
copyValue={address}
onClick={() => openLink(address)}
label='Copy IP or navigate to address'
>
{nif.address} <IoArrowUp className={style.goIcon} />
</CopyTag>
);
})}
</div>
</div>
);
}
interface ClientLinkProps {
current: boolean;
to: string;
}
function ClientLink(props: PropsWithChildren<ClientLinkProps>) {
const { current, to, children } = props;
const { isElectron } = useElectronEvent();
const classes = cx([style.link, current && style.current]);
if (isElectron) {
return (
<button className={classes} tabIndex={0} onClick={(event) => handleLinks(event, to)}>
{children}
<IoArrowUp className={style.linkIcon} />
</button>
);
}
return (
<Link to={`/${to}`} className={classes} tabIndex={0}>
{children}
</Link>
);
}
export default memo(NavigationMenu);
@@ -2,10 +2,10 @@ import { memo } from 'react';
import { useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { useHotkeys } from '@mantine/hooks'; import { useHotkeys } from '@mantine/hooks';
import FloatingNavigation from './FloatingNavigation'; import FloatingNavigation from './floating-navigation/FloatingNavigation';
import ViewLockedIcon from './view-locked-icon/ViewLockedIcon';
import NavigationMenu from './NavigationMenu'; import NavigationMenu from './NavigationMenu';
import useViewEditor from './useViewEditor'; import useViewEditor from './useViewEditor';
import ViewLockedIcon from './ViewLockedIcon';
interface ViewNavigationMenuProps { interface ViewNavigationMenuProps {
isLockable?: boolean; isLockable?: boolean;
@@ -0,0 +1,4 @@
.linkIcon {
margin-left: auto;
@include rotate-fourty-five;
}
@@ -0,0 +1,34 @@
import { PropsWithChildren } from 'react';
import { IoArrowUp } from 'react-icons/io5';
import { useNavigate } from 'react-router-dom';
import { useElectronEvent } from '../../../hooks/useElectronEvent';
import { handleLinks } from '../../../utils/linkUtils';
import NavigationMenuItem from '../navigation-menu-item/NavigationMenuItem';
import style from './ClientLink.module.scss';
interface ClientLinkProps {
current: boolean;
to: string;
}
export default function ClientLink({ current, to, children }: PropsWithChildren<ClientLinkProps>) {
const { isElectron } = useElectronEvent();
const navigate = useNavigate();
if (isElectron) {
return (
<NavigationMenuItem active={current} onClick={() => handleLinks(to)}>
{children}
<IoArrowUp className={style.linkIcon} />
</NavigationMenuItem>
);
}
return (
<NavigationMenuItem active={current} onClick={() => navigate(`/${to}`)}>
{children}
</NavigationMenuItem>
);
}
@@ -0,0 +1,38 @@
import { IoLockClosedOutline } from 'react-icons/io5';
import { useNavigate } from 'react-router-dom';
import { useViewportSize } from '@mantine/hooks';
import NavigationMenuItem from '../navigation-menu-item/NavigationMenuItem';
export default function EditorNavigation() {
const { width } = useViewportSize();
const navigate = useNavigate();
if (width > 1440) {
return (
<NavigationMenuItem active={location.pathname === '/editor'} onClick={() => navigate('/editor')}>
<IoLockClosedOutline />
Editor
</NavigationMenuItem>
);
}
return (
<>
<NavigationMenuItem active={location.pathname === '/timercontrol'} onClick={() => navigate('/timercontrol')}>
<IoLockClosedOutline />
Timer Controls
</NavigationMenuItem>
<NavigationMenuItem active={location.pathname === '/messagecontrol'} onClick={() => navigate('/messagecontrol')}>
<IoLockClosedOutline />
Message Controls
</NavigationMenuItem>
<NavigationMenuItem active={location.pathname === '/rundown'} onClick={() => navigate('/rundown')}>
<IoLockClosedOutline />
Rundown
</NavigationMenuItem>
</>
);
}
@@ -0,0 +1,23 @@
.fadeable {
opacity: 1;
transition-property: opacity;
transition-duration: 0.3s;
&.hidden {
opacity: 0;
pointer-events: none;
}
}
.buttonContainer {
display: flex;
flex-direction: column;
row-gap: 1rem;
padding: 0.5em;
position: fixed;
left: 0;
top: 0;
z-index: $zindex-nav;
}
@@ -1,26 +1,28 @@
import { IoApps } from 'react-icons/io5'; import { IoApps } from 'react-icons/io5';
import { IoSettingsOutline } from 'react-icons/io5'; import { IoSettingsOutline } from 'react-icons/io5';
import { useFadeOutOnInactivity } from '../../hooks/useFadeOutOnInactivity'; import { useFadeOutOnInactivity } from '../../../hooks/useFadeOutOnInactivity';
import { cx } from '../../utils/styleUtils'; import { cx } from '../../../utils/styleUtils';
import IconButton from '../buttons/IconButton'; import IconButton from '../../buttons/IconButton';
import style from './NavigationMenu.module.scss'; import style from './FloatingNavigation.module.scss';
interface FloatingNavigationProps { interface FloatingNavigationProps {
toggleMenu: () => void; toggleMenu: () => void;
toggleSettings: () => void; toggleSettings: () => void;
} }
export default function FloatingNavigation(props: FloatingNavigationProps) { export default function FloatingNavigation({ toggleMenu, toggleSettings }: FloatingNavigationProps) {
const { toggleMenu, toggleSettings } = props; const isButtonShown = useFadeOutOnInactivity(true);
const isButtonShown = useFadeOutOnInactivity();
return ( return (
<div className={cx([style.fadeable, style.buttonContainer, !isButtonShown && style.hidden])}> <div
id='fadeable-navigation'
className={cx([style.fadeable, style.buttonContainer, !isButtonShown && style.hidden])}
>
<IconButton <IconButton
variant='subtle-white' variant='subtle-white'
className={style.navButton} size='xlarge'
onClick={toggleMenu} onClick={toggleMenu}
aria-label='toggle menu' aria-label='toggle menu'
data-testid='navigation__toggle-menu' data-testid='navigation__toggle-menu'
@@ -29,7 +31,7 @@ export default function FloatingNavigation(props: FloatingNavigationProps) {
</IconButton> </IconButton>
<IconButton <IconButton
variant='subtle-white' variant='subtle-white'
className={style.navButton} size='xlarge'
onClick={toggleSettings} onClick={toggleSettings}
aria-label='toggle settings' aria-label='toggle settings'
data-testid='navigation__toggle-settings' data-testid='navigation__toggle-settings'
@@ -0,0 +1,37 @@
.link {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
gap: 0.5rem;
width: 100%;
border-left: 4px solid transparent;
color: $action-text-color;
white-space: nowrap;
cursor: pointer;
transition-property: color;
transition-duration: $transition-time-action;
&:hover {
color: $ontime-color;
background-color: $gray-1350;
}
&:active {
background-color: $border-color-ondark;
}
&:focus {
outline: none;
background-color: $gray-1300;
border: 2px solid $ui-white;
}
&.current {
background-color: $gray-1300;
border-left: 4px solid $action-text-color;
}
}
@@ -0,0 +1,33 @@
import { PropsWithChildren } from 'react';
import { isKeyEnter } from '../../../utils/keyEvent';
import { cx } from '../../../utils/styleUtils';
import style from './NavigationMenuItem.module.scss';
interface NavigationMenuItemProps {
active?: boolean;
className?: string;
onClick: () => void;
}
export default function NavigationMenuItem({
active,
className,
children,
onClick,
}: PropsWithChildren<NavigationMenuItemProps>) {
return (
<div
className={cx([style.link, active && style.current, className])}
tabIndex={0}
role='button'
onClick={onClick}
onKeyDown={(event) => {
isKeyEnter(event) && onClick();
}}
>
{children}
</div>
);
}
@@ -0,0 +1,18 @@
.header {
font-size: calc(1rem - 2px);
margin-left: 1rem;
color: $gray-700;
}
.interfaces {
padding: 0.5rem 1rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.goIcon {
@include rotate-fourty-five;
margin-left: 0.25rem;
margin-bottom: 0.25rem;
}
@@ -0,0 +1,46 @@
import { IoArrowUp } from 'react-icons/io5';
import useInfo from '../../../hooks-query/useInfo';
import { linkToOtherHost, openLink } from '../../../utils/linkUtils';
import CopyTag from '../../copy-tag/CopyTag';
import style from './OtherAddresses.module.scss';
interface OtherAddressesProps {
currentLocation: string;
}
export default function OtherAddresses({ currentLocation }: OtherAddressesProps) {
const { data } = useInfo();
// there is no point showing this if we only have one interface
if (data.networkInterfaces.length < 2) {
return null;
}
return (
<>
<div className={style.header}>Accessible on external networks</div>
<div className={style.interfaces}>
{data?.networkInterfaces?.map((nif) => {
if (nif.name === 'localhost') {
return null;
}
const address = linkToOtherHost(nif.address, currentLocation);
return (
<CopyTag
key={nif.name}
copyValue={address}
onClick={() => openLink(address)}
label='Copy IP or navigate to address'
>
{nif.address} <IoArrowUp className={style.goIcon} />
</CopyTag>
);
})}
</div>
</>
);
}
@@ -0,0 +1,22 @@
.fadeable {
opacity: 1;
transition-property: opacity;
transition-duration: 0.3s;
&.hidden {
opacity: 0;
pointer-events: none;
}
}
.lockIcon {
font-size: 1.5rem;
color: $active-indicator;
padding: 0.5em;
position: fixed;
left: 0;
top: 0;
z-index: $zindex-nav;
}
@@ -1,9 +1,9 @@
import { IoLockClosedOutline } from 'react-icons/io5'; import { IoLockClosedOutline } from 'react-icons/io5';
import { useFadeOutOnInactivity } from '../../hooks/useFadeOutOnInactivity'; import { useFadeOutOnInactivity } from '../../../hooks/useFadeOutOnInactivity';
import { cx } from '../../utils/styleUtils'; import { cx } from '../../../utils/styleUtils';
import style from './NavigationMenu.module.scss'; import style from './ViewLockedIcon.module.scss';
export default function ViewLockedIcon() { export default function ViewLockedIcon() {
const isLockIconShown = useFadeOutOnInactivity(); const isLockIconShown = useFadeOutOnInactivity();
+2 -2
View File
@@ -20,13 +20,13 @@ export function openLink(url: string) {
* serverUrl and baseURI are used for testing * serverUrl and baseURI are used for testing
*/ */
export function handleLinks( export function handleLinks(
event: MouseEvent,
location: string, location: string,
event?: MouseEvent,
externalServerUrl: string = serverURL, externalServerUrl: string = serverURL,
externalBaseURI: string = baseURI, externalBaseURI: string = baseURI,
) { ) {
// we handle the link manually // we handle the link manually
event.preventDefault(); event?.preventDefault();
const destination = new URL(externalServerUrl); const destination = new URL(externalServerUrl);
destination.pathname = externalBaseURI ? `${externalBaseURI}/${location}` : location; destination.pathname = externalBaseURI ? `${externalBaseURI}/${location}` : location;
@@ -198,7 +198,7 @@ export default function UrlPresetsForm() {
<TooltipActionBtn <TooltipActionBtn
size='sm' size='sm'
isDisabled={!canTest} isDisabled={!canTest}
clickHandler={(event) => handleLinks(event, preset.alias)} clickHandler={(event) => handleLinks(preset.alias, event)}
tooltip='Test preset' tooltip='Test preset'
aria-label='Test preset' aria-label='Test preset'
variant='ontime-ghosted' variant='ontime-ghosted'
@@ -10,7 +10,7 @@ import style from './NetworkLogExport.module.scss';
export default function LogExport() { export default function LogExport() {
const extract = (event: MouseEvent) => { const extract = (event: MouseEvent) => {
handleLinks(event, 'log'); handleLinks('log', event);
}; };
return ( return (
@@ -15,7 +15,7 @@ const MessageControlExport = () => {
return ( return (
<div className={style.messages} data-testid='panel-messages-control'> <div className={style.messages} data-testid='panel-messages-control'>
{!isExtracted && <Corner onClick={(event) => handleLinks(event, 'messagecontrol')} />} {!isExtracted && <Corner onClick={(event) => handleLinks('messagecontrol', event)} />}
<div className={classes}> <div className={classes}>
<ErrorBoundary> <ErrorBoundary>
<MessageControl /> <MessageControl />
@@ -49,7 +49,7 @@ export default function TimerPreview() {
return ( return (
<div className={style.preview}> <div className={style.preview}>
<Corner onClick={(event) => handleLinks(event, 'timer')} /> <Corner onClick={(event) => handleLinks('timer', event)} />
<div className={contentClasses}> <div className={contentClasses}>
<div <div
className={style.mainContent} className={style.mainContent}
@@ -12,7 +12,7 @@ const TimerControlExport = () => {
const isExtracted = window.location.pathname.includes('/timercontrol'); const isExtracted = window.location.pathname.includes('/timercontrol');
return ( return (
<div className={style.playback} data-testid='panel-timer-control'> <div className={style.playback} data-testid='panel-timer-control'>
{!isExtracted && <Corner onClick={(event) => handleLinks(event, 'timercontrol')} />} {!isExtracted && <Corner onClick={(event) => handleLinks('timercontrol', event)} />}
<div className={style.content}> <div className={style.content}>
<ErrorBoundary> <ErrorBoundary>
<PlaybackControl /> <PlaybackControl />
@@ -28,7 +28,7 @@ function RundownExport() {
<div className={style.rundown}> <div className={style.rundown}>
<div className={style.list}> <div className={style.list}>
<ErrorBoundary> <ErrorBoundary>
{!isExtracted && <Corner onClick={(event) => handleLinks(event, 'rundown')} />} {!isExtracted && <Corner onClick={(event) => handleLinks('rundown', event)} />}
<ContextMenu> <ContextMenu>
<RundownWrapper /> <RundownWrapper />
</ContextMenu> </ContextMenu>
-14
View File
@@ -2,20 +2,6 @@
//////////////////////////////////// general app elements //////////////////////////////////// general app elements
@mixin action-link {
color: $action-text-color;
display: flex;
align-items: center;
cursor: pointer;
transition-property: color;
transition-duration: $transition-time-action;
white-space: nowrap;
&:hover {
color: $ontime-color;
}
}
@mixin ellipsis-text { @mixin ellipsis-text {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
-2
View File
@@ -1,7 +1,5 @@
export const navigatorConstants = [ export const navigatorConstants = [
{ url: 'timer', label: 'Timer' }, { url: 'timer', label: 'Timer' },
{ url: 'minimal', label: 'Minimal Timer' },
{ url: 'clock', label: 'Wall Clock' },
{ url: 'backstage', label: 'Backstage' }, { url: 'backstage', label: 'Backstage' },
{ url: 'timeline', label: 'Timeline (beta)' }, { url: 'timeline', label: 'Timeline (beta)' },
{ url: 'lower', label: 'Lower Thirds' }, { url: 'lower', label: 'Lower Thirds' },
-12
View File
@@ -33,18 +33,6 @@ test.describe('pages routes are available', () => {
await expect(page).toHaveTitle(/ontime/); await expect(page).toHaveTitle(/ontime/);
}); });
test('clock', async ({ page }) => {
await page.goto('http://localhost:4001/timer');
await expect(page).toHaveTitle(/ontime/);
});
test('minimal', async ({ page }) => {
await page.goto('http://localhost:4001/minimal');
await expect(page).toHaveTitle(/ontime/);
});
test('backstage', async ({ page }) => { test('backstage', async ({ page }) => {
await page.goto('http://localhost:4001/backstage'); await page.goto('http://localhost:4001/backstage');
+7 -21
View File
@@ -6,37 +6,23 @@ test.describe('test view navigation feature', () => {
await expect(page.locator('data-testid=timer-view')).toBeVisible(); await expect(page.locator('data-testid=timer-view')).toBeVisible();
}); });
test('Minimal', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Minimal Timer' }).click();
await expect(page.locator('data-testid=minimal-timer')).toBeVisible();
await expect(page).toHaveURL('http://localhost:4001/minimal');
});
test('Wall Clock', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Wall Clock', exact: true }).click();
await expect(page.locator('data-testid=clock-view')).toBeVisible();
await expect(page).toHaveURL('http://localhost:4001/clock');
});
test('Timeline', async ({ page }) => { test('Timeline', async ({ page }) => {
await openNavigationMenu(page); await openNavigationMenu(page);
await page.getByRole('link', { name: 'Timeline' }).click(); await page.getByRole('button', { name: 'Timeline' }).click();
page.locator('data-testid=timeline-view'); page.locator('data-testid=timeline-view');
await expect(page).toHaveURL('http://localhost:4001/timeline'); await expect(page).toHaveURL('http://localhost:4001/timeline');
}); });
test('Backstage', async ({ page }) => { test('Backstage', async ({ page }) => {
await openNavigationMenu(page); await openNavigationMenu(page);
await page.getByRole('link', { name: 'Backstage' }).click(); await page.getByRole('button', { name: 'Backstage' }).click();
page.locator('data-testid=backstage-view'); page.locator('data-testid=backstage-view');
await expect(page).toHaveURL('http://localhost:4001/backstage'); await expect(page).toHaveURL('http://localhost:4001/backstage');
}); });
test('Lower Thirds', async ({ page }) => { test('Lower Thirds', async ({ page }) => {
await openNavigationMenu(page); await openNavigationMenu(page);
await page.getByRole('link', { name: 'Lower Thirds' }).click(); await page.getByRole('button', { name: 'Lower Thirds' }).click();
await expect(page).toHaveURL('http://localhost:4001/lower'); await expect(page).toHaveURL('http://localhost:4001/lower');
const errorBoundary = page.locator('data-testid=error-container'); const errorBoundary = page.locator('data-testid=error-container');
await expect(errorBoundary).toHaveCount(0); await expect(errorBoundary).toHaveCount(0);
@@ -44,28 +30,28 @@ test.describe('test view navigation feature', () => {
test('Studio Clock', async ({ page }) => { test('Studio Clock', async ({ page }) => {
await openNavigationMenu(page); await openNavigationMenu(page);
await page.getByRole('link', { name: 'Studio Clock' }).click(); await page.getByRole('button', { name: 'Studio Clock' }).click();
page.locator('data-testid=studio-view'); page.locator('data-testid=studio-view');
await expect(page).toHaveURL('http://localhost:4001/studio'); await expect(page).toHaveURL('http://localhost:4001/studio');
}); });
test('Countdown', async ({ page }) => { test('Countdown', async ({ page }) => {
await openNavigationMenu(page); await openNavigationMenu(page);
await page.getByRole('link', { name: 'Countdown' }).click(); await page.getByRole('button', { name: 'Countdown' }).click();
page.locator('data-testid=countdown-view'); page.locator('data-testid=countdown-view');
await expect(page).toHaveURL('http://localhost:4001/countdown'); await expect(page).toHaveURL('http://localhost:4001/countdown');
}); });
test('Project Info', async ({ page }) => { test('Project Info', async ({ page }) => {
await openNavigationMenu(page); await openNavigationMenu(page);
await page.getByRole('link', { name: 'Project Info' }).click(); await page.getByRole('button', { name: 'Project Info' }).click();
page.locator('data-testid=project-view'); page.locator('data-testid=project-view');
await expect(page).toHaveURL('http://localhost:4001/info'); await expect(page).toHaveURL('http://localhost:4001/info');
}); });
test('Timer', async ({ page }) => { test('Timer', async ({ page }) => {
await openNavigationMenu(page); await openNavigationMenu(page);
await page.getByRole('link', { name: 'Timer', exact: true }).click(); await page.getByRole('button', { name: 'Timer', exact: true }).click();
page.locator('data-testid=timer-view'); page.locator('data-testid=timer-view');
await expect(page).toHaveURL('http://localhost:4001/timer'); await expect(page).toHaveURL('http://localhost:4001/timer');
}); });