mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 22:49:18 +00:00
refactor: extract navigation elements
This commit is contained in:
committed by
Carlos Valente
parent
7fdda1c969
commit
269091986a
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+38
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+23
@@ -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;
|
||||||
|
}
|
||||||
+12
-10
@@ -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'
|
||||||
+37
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+18
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+22
@@ -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;
|
||||||
|
}
|
||||||
+3
-3
@@ -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();
|
||||||
@@ -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;
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user