mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 00:13:53 +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 {
|
||||
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 Dialog from '../dialog/Dialog';
|
||||
import Input from '../input/input/Input';
|
||||
|
||||
interface RenameClientModalProps {
|
||||
id: string;
|
||||
@@ -20,8 +12,7 @@ interface RenameClientModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function RenameClientModal(props: RenameClientModalProps) {
|
||||
const { id, name: currentName = '', isOpen, onClose } = props;
|
||||
export function RenameClientModal({ id, name: currentName = '', isOpen, onClose }: RenameClientModalProps) {
|
||||
const [name, setName] = useState(currentName);
|
||||
|
||||
const { setClientName } = setClientRemote;
|
||||
@@ -36,29 +27,24 @@ export function RenameClientModal(props: RenameClientModalProps) {
|
||||
const canSubmit = name !== currentName && name !== '';
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Rename: {currentName}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='md'
|
||||
placeholder='new name'
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
||||
<Dialog
|
||||
isOpen={isOpen}
|
||||
title={`Rename Client: ${currentName}`}
|
||||
showCloseButton
|
||||
onClose={onClose}
|
||||
bodyElements={
|
||||
<Input height='large' placeholder='New name' value={name} onChange={(event) => setName(event.target.value)} />
|
||||
}
|
||||
footerElements={
|
||||
<>
|
||||
<Button variant='subtle' size='large' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size='md' variant='ontime-filled' onClick={handleRename} isDisabled={!canSubmit}>
|
||||
<Button variant='primary' size='large' onClick={handleRename} disabled={!canSubmit}>
|
||||
Submit
|
||||
</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 {
|
||||
border-color: $border-color-ondark;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
font-size: calc(1rem - 2px);
|
||||
margin-left: 1rem;
|
||||
color: $gray-700;
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
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 {
|
||||
margin-top: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.drawer {
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: $zindex-dialog;
|
||||
|
||||
width: 22rem;
|
||||
height: 100vh;
|
||||
|
||||
.interfaces {
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
@include rotate-fourty-five;
|
||||
margin-left: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 { createPortal } from 'react-dom';
|
||||
import { IoArrowUp, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useFullscreen } from '@mantine/hooks';
|
||||
import { memo } from 'react';
|
||||
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Dialog } from '@base-ui-components/react/dialog';
|
||||
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||
|
||||
import { isLocalhost } from '../../../externals';
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { useElectronEvent } from '../../hooks/useElectronEvent';
|
||||
import useInfo from '../../hooks-query/useInfo';
|
||||
import { useClientStore } from '../../stores/clientStore';
|
||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||
import { isKeyEnter } from '../../utils/keyEvent';
|
||||
import { handleLinks, linkToOtherHost, openLink } from '../../utils/linkUtils';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
import IconButton from '../buttons/IconButton';
|
||||
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';
|
||||
|
||||
@@ -32,74 +23,49 @@ interface NavigationMenuProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function NavigationMenu(props: NavigationMenuProps) {
|
||||
const { isOpen, onClose } = props;
|
||||
|
||||
export default memo(NavigationMenu);
|
||||
function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
||||
const id = useClientStore((store) => store.id);
|
||||
const name = useClientStore((store) => store.name);
|
||||
|
||||
const { isOpen: isOpenRename, onOpen: onRenameOpen, onClose: onCloseRename } = useDisclosure();
|
||||
const [isRenameOpen, handlers] = useDisclosure(false);
|
||||
const { fullscreen, toggle } = useFullscreen();
|
||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
||||
const location = useLocation();
|
||||
|
||||
return createPortal(
|
||||
<div id='navigation-menu-portal'>
|
||||
<RenameClientModal id={id} name={name} isOpen={isOpenRename} onClose={onCloseRename} />
|
||||
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent maxWidth='22rem'>
|
||||
<DrawerHeader>
|
||||
<DrawerCloseButton size='lg' />
|
||||
Ontime
|
||||
</DrawerHeader>
|
||||
<DrawerBody padding={0}>
|
||||
<div className={style.buttonsContainer}>
|
||||
<div
|
||||
className={cx([style.link, fullscreen && style.current])}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
onClick={toggle}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && toggle();
|
||||
}}
|
||||
>
|
||||
Toggle Fullscreen
|
||||
{fullscreen ? <IoContract /> : <IoExpand />}
|
||||
</div>
|
||||
<div
|
||||
className={cx([style.link, mirror && style.current])}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
onClick={() => toggleMirror()}
|
||||
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>
|
||||
return (
|
||||
<Dialog.Root
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className={style.backdrop} />
|
||||
<RenameClientModal id={id} name={name} isOpen={isRenameOpen} onClose={handlers.close} />
|
||||
<Dialog.Popup className={style.drawer}>
|
||||
<div className={style.header}>
|
||||
<Dialog.Title>Ontime</Dialog.Title>
|
||||
<IconButton variant='subtle-white' size='large' onClick={onClose}>
|
||||
<IoClose />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={style.body}>
|
||||
<NavigationMenuItem active={fullscreen} onClick={toggle}>
|
||||
Toggle Fullscreen
|
||||
{fullscreen ? <IoContract /> : <IoExpand />}
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem active={mirror} onClick={toggleMirror}>
|
||||
Flip Screen
|
||||
<IoSwapVertical />
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem onClick={handlers.open}>Rename Client</NavigationMenuItem>
|
||||
|
||||
<hr className={style.separator} />
|
||||
<Link
|
||||
to='/editor'
|
||||
tabIndex={0}
|
||||
className={`${style.link} ${location.pathname === '/editor' && style.current}`}
|
||||
>
|
||||
<IoLockClosedOutline />
|
||||
Editor
|
||||
</Link>
|
||||
|
||||
<EditorNavigation />
|
||||
<ClientLink to='cuesheet' current={location.pathname === '/cuesheet'}>
|
||||
<IoLockClosedOutline />
|
||||
Cuesheet
|
||||
@@ -108,86 +74,23 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
<IoLockClosedOutline />
|
||||
Operator
|
||||
</ClientLink>
|
||||
|
||||
<hr className={style.separator} />
|
||||
|
||||
{navigatorConstants.map((route) => (
|
||||
<ClientLink key={route.url} to={route.url} current={location.pathname === `/${route.url}`}>
|
||||
{route.label}
|
||||
</ClientLink>
|
||||
))}
|
||||
{isLocalhost && <OtherAddresses currentLocation={location.pathname} />}
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>,
|
||||
document.body,
|
||||
</div>
|
||||
|
||||
{isLocalhost && (
|
||||
<div>
|
||||
<OtherAddresses currentLocation={location.pathname} />
|
||||
</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 { 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 useViewEditor from './useViewEditor';
|
||||
import ViewLockedIcon from './ViewLockedIcon';
|
||||
|
||||
interface ViewNavigationMenuProps {
|
||||
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 { IoSettingsOutline } from 'react-icons/io5';
|
||||
|
||||
import { useFadeOutOnInactivity } from '../../hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
import IconButton from '../buttons/IconButton';
|
||||
import { useFadeOutOnInactivity } from '../../../hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
import IconButton from '../../buttons/IconButton';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
import style from './FloatingNavigation.module.scss';
|
||||
|
||||
interface FloatingNavigationProps {
|
||||
toggleMenu: () => void;
|
||||
toggleSettings: () => void;
|
||||
}
|
||||
|
||||
export default function FloatingNavigation(props: FloatingNavigationProps) {
|
||||
const { toggleMenu, toggleSettings } = props;
|
||||
const isButtonShown = useFadeOutOnInactivity();
|
||||
export default function FloatingNavigation({ toggleMenu, toggleSettings }: FloatingNavigationProps) {
|
||||
const isButtonShown = useFadeOutOnInactivity(true);
|
||||
|
||||
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
|
||||
variant='subtle-white'
|
||||
className={style.navButton}
|
||||
size='xlarge'
|
||||
onClick={toggleMenu}
|
||||
aria-label='toggle menu'
|
||||
data-testid='navigation__toggle-menu'
|
||||
@@ -29,7 +31,7 @@ export default function FloatingNavigation(props: FloatingNavigationProps) {
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant='subtle-white'
|
||||
className={style.navButton}
|
||||
size='xlarge'
|
||||
onClick={toggleSettings}
|
||||
aria-label='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 { useFadeOutOnInactivity } from '../../hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
import { useFadeOutOnInactivity } from '../../../hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
import style from './ViewLockedIcon.module.scss';
|
||||
|
||||
export default function ViewLockedIcon() {
|
||||
const isLockIconShown = useFadeOutOnInactivity();
|
||||
@@ -20,13 +20,13 @@ export function openLink(url: string) {
|
||||
* serverUrl and baseURI are used for testing
|
||||
*/
|
||||
export function handleLinks(
|
||||
event: MouseEvent,
|
||||
location: string,
|
||||
event?: MouseEvent,
|
||||
externalServerUrl: string = serverURL,
|
||||
externalBaseURI: string = baseURI,
|
||||
) {
|
||||
// we handle the link manually
|
||||
event.preventDefault();
|
||||
event?.preventDefault();
|
||||
|
||||
const destination = new URL(externalServerUrl);
|
||||
destination.pathname = externalBaseURI ? `${externalBaseURI}/${location}` : location;
|
||||
|
||||
+1
-1
@@ -198,7 +198,7 @@ export default function UrlPresetsForm() {
|
||||
<TooltipActionBtn
|
||||
size='sm'
|
||||
isDisabled={!canTest}
|
||||
clickHandler={(event) => handleLinks(event, preset.alias)}
|
||||
clickHandler={(event) => handleLinks(preset.alias, event)}
|
||||
tooltip='Test preset'
|
||||
aria-label='Test preset'
|
||||
variant='ontime-ghosted'
|
||||
|
||||
@@ -10,7 +10,7 @@ import style from './NetworkLogExport.module.scss';
|
||||
|
||||
export default function LogExport() {
|
||||
const extract = (event: MouseEvent) => {
|
||||
handleLinks(event, 'log');
|
||||
handleLinks('log', event);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,7 @@ const MessageControlExport = () => {
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<ErrorBoundary>
|
||||
<MessageControl />
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function TimerPreview() {
|
||||
|
||||
return (
|
||||
<div className={style.preview}>
|
||||
<Corner onClick={(event) => handleLinks(event, 'timer')} />
|
||||
<Corner onClick={(event) => handleLinks('timer', event)} />
|
||||
<div className={contentClasses}>
|
||||
<div
|
||||
className={style.mainContent}
|
||||
|
||||
@@ -12,7 +12,7 @@ const TimerControlExport = () => {
|
||||
const isExtracted = window.location.pathname.includes('/timercontrol');
|
||||
return (
|
||||
<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}>
|
||||
<ErrorBoundary>
|
||||
<PlaybackControl />
|
||||
|
||||
@@ -28,7 +28,7 @@ function RundownExport() {
|
||||
<div className={style.rundown}>
|
||||
<div className={style.list}>
|
||||
<ErrorBoundary>
|
||||
{!isExtracted && <Corner onClick={(event) => handleLinks(event, 'rundown')} />}
|
||||
{!isExtracted && <Corner onClick={(event) => handleLinks('rundown', event)} />}
|
||||
<ContextMenu>
|
||||
<RundownWrapper />
|
||||
</ContextMenu>
|
||||
|
||||
@@ -2,20 +2,6 @@
|
||||
|
||||
//////////////////////////////////// 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 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export const navigatorConstants = [
|
||||
{ url: 'timer', label: 'Timer' },
|
||||
{ url: 'minimal', label: 'Minimal Timer' },
|
||||
{ url: 'clock', label: 'Wall Clock' },
|
||||
{ url: 'backstage', label: 'Backstage' },
|
||||
{ url: 'timeline', label: 'Timeline (beta)' },
|
||||
{ url: 'lower', label: 'Lower Thirds' },
|
||||
|
||||
@@ -33,18 +33,6 @@ test.describe('pages routes are available', () => {
|
||||
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 }) => {
|
||||
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();
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
await openNavigationMenu(page);
|
||||
await page.getByRole('link', { name: 'Timeline' }).click();
|
||||
await page.getByRole('button', { name: 'Timeline' }).click();
|
||||
page.locator('data-testid=timeline-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/timeline');
|
||||
});
|
||||
|
||||
test('Backstage', async ({ page }) => {
|
||||
await openNavigationMenu(page);
|
||||
await page.getByRole('link', { name: 'Backstage' }).click();
|
||||
await page.getByRole('button', { name: 'Backstage' }).click();
|
||||
page.locator('data-testid=backstage-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/backstage');
|
||||
});
|
||||
|
||||
test('Lower Thirds', async ({ 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');
|
||||
const errorBoundary = page.locator('data-testid=error-container');
|
||||
await expect(errorBoundary).toHaveCount(0);
|
||||
@@ -44,28 +30,28 @@ test.describe('test view navigation feature', () => {
|
||||
|
||||
test('Studio Clock', async ({ 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');
|
||||
await expect(page).toHaveURL('http://localhost:4001/studio');
|
||||
});
|
||||
|
||||
test('Countdown', async ({ page }) => {
|
||||
await openNavigationMenu(page);
|
||||
await page.getByRole('link', { name: 'Countdown' }).click();
|
||||
await page.getByRole('button', { name: 'Countdown' }).click();
|
||||
page.locator('data-testid=countdown-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/countdown');
|
||||
});
|
||||
|
||||
test('Project Info', async ({ 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');
|
||||
await expect(page).toHaveURL('http://localhost:4001/info');
|
||||
});
|
||||
|
||||
test('Timer', async ({ 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');
|
||||
await expect(page).toHaveURL('http://localhost:4001/timer');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user