diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index e929b66a5..6ebce9acb 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -4,16 +4,15 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import ErrorBoundary from './common/components/error-boundary/ErrorBoundary'; +import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay'; import { AppContextProvider } from './common/context/AppContext'; import { ontimeQueryClient } from './common/queryClient'; -import { socketClientName } from './common/stores/connectionName'; import { connectSocket } from './common/utils/socket'; import theme from './theme/theme'; import { TranslationProvider } from './translation/TranslationProvider'; import AppRouter from './AppRouter'; -const preferredClientName = socketClientName.getState().name; -connectSocket(preferredClientName); +connectSocket(); function App() { return ( @@ -24,11 +23,15 @@ function App() {
+
+ +
+ diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 2f34d2acf..ba146279a 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; +import { useClientPath } from './common/hooks/useClientPath'; import Log from './features/log/Log'; import withPreset from './features/PresetWrapper'; import withData from './features/viewers/ViewWrapper'; @@ -34,6 +35,9 @@ const TimerControl = lazy(() => import('./features/control/playback/TimerControl const MessageControl = lazy(() => import('./features/control/message/MessageControlExport')); export default function AppRouter() { + // handle client path changes + useClientPath(); + return ( diff --git a/apps/client/src/common/api/constants.ts b/apps/client/src/common/api/constants.ts index ea06255b5..936317936 100644 --- a/apps/client/src/common/api/constants.ts +++ b/apps/client/src/common/api/constants.ts @@ -11,6 +11,7 @@ export const RUNTIME = ['runtimeStore']; export const SHEET_STATE = ['sheetState']; export const URL_PRESETS = ['urlpresets']; export const VIEW_SETTINGS = ['viewSettings']; +export const CLIENT_LIST = ['clientList']; // resolve location const location = window.location; diff --git a/apps/client/src/common/components/client-modal/ClientModal.module.scss b/apps/client/src/common/components/client-modal/ClientModal.module.scss new file mode 100644 index 000000000..2c291157e --- /dev/null +++ b/apps/client/src/common/components/client-modal/ClientModal.module.scss @@ -0,0 +1,6 @@ +.buttonSection { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $element-spacing; + margin-top: -$element-spacing; +} diff --git a/apps/client/src/common/components/client-modal/RedirectClientModal.tsx b/apps/client/src/common/components/client-modal/RedirectClientModal.tsx new file mode 100644 index 000000000..163e481ba --- /dev/null +++ b/apps/client/src/common/components/client-modal/RedirectClientModal.tsx @@ -0,0 +1,69 @@ +import { useState } from 'react'; +import { + Button, + Input, + InputGroup, + InputLeftAddon, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, +} from '@chakra-ui/react'; + +import { setClientRemote } from '../../hooks/useSocket'; + +import style from './ClientModal.module.scss'; + +interface RedirectClientModalProps { + id: string; + name?: string; + path?: string; + isOpen: boolean; + onClose: () => void; +} + +export function RedirectClientModal(props: RedirectClientModalProps) { + const { id, isOpen, name = '', path: currentPath = '', onClose } = props; + const [path, setPath] = useState(currentPath); + + const { setRedirect } = setClientRemote; + + const handleRedirect = () => { + if (path !== currentPath && path !== '') { + setRedirect({ target: id, redirect: path }); + } + onClose(); + }; + + const host = `${window.location.origin}/`; + const canSubmit = path !== currentPath && path !== ''; + + return ( + + + + Redirect: {name} + + + + {host} + setPath(event.target.value)} /> + + + +
+ + +
+
+
+
+ ); +} diff --git a/apps/client/src/common/components/client-modal/RenameClientModal.tsx b/apps/client/src/common/components/client-modal/RenameClientModal.tsx new file mode 100644 index 000000000..a66991278 --- /dev/null +++ b/apps/client/src/common/components/client-modal/RenameClientModal.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { + Button, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, +} from '@chakra-ui/react'; + +import { setClientRemote } from '../../hooks/useSocket'; + +import style from './ClientModal.module.scss'; + +interface RenameClientModalProps { + id: string; + name?: string; + isOpen: boolean; + onClose: () => void; +} + +export function RenameClientModal(props: RenameClientModalProps) { + const { id, name: currentName = '', isOpen, onClose } = props; + const [name, setName] = useState(currentName); + + const { setClientName } = setClientRemote; + + const handleRename = () => { + if (name !== currentName && name !== '') { + setClientName({ target: id, rename: name }); + } + onClose(); + }; + + const canSubmit = name !== currentName && name !== ''; + + return ( + + + + Rename: {currentName} + + + setName(event.target.value)} + /> + + +
+ + +
+
+
+
+ ); +} diff --git a/apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss new file mode 100644 index 000000000..ac65bc290 --- /dev/null +++ b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss @@ -0,0 +1,34 @@ +.overlay { + position: absolute; + top: 0; + left: 0; + margin: 0; + width: 100vw; + height: 100vh; + padding: 0; + box-sizing: border-box; + + border: 10px solid $ui-white; + background-color: $ui-black; + + color: $ui-white; + display: grid; + place-content: center; + text-align: center; + z-index: 100; + cursor: pointer; +} + +.name { + font-weight: 600; + margin-top: -10vh; + line-height: 1em; + font-size: 12vw; + text-transform: uppercase; +} + +.message { + color: $label-gray; + line-height: 2em; + font-size: 3vw; +} diff --git a/apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx new file mode 100644 index 000000000..2f1d32c36 --- /dev/null +++ b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { MILLIS_PER_MINUTE } from 'ontime-utils'; + +import { setClientRemote } from '../../hooks/useSocket'; +import { useClientStore } from '../../stores/clientStore'; + +import style from './IdentifyOverlay.module.scss'; + +export default function IdentifyOverlay() { + const clients = useClientStore((store) => store.clients); + const id = useClientStore((store) => store.id); + const showOverlay = clients[id]?.identify; + + if (!showOverlay) { + return null; + } + + const portalRoot = document.getElementById('identify-portal'); + + if (!portalRoot) { + return null; + } + return createPortal(, portalRoot); +} + +function Overlay() { + const clients = useClientStore((store) => store.clients); + const id = useClientStore((store) => store.id); + const name = useClientStore((store) => store.name); + + const timerRef = useRef(null); + + const { setIdentify } = setClientRemote; + const showOverlay = clients[id]?.identify; + + const handleClose = useCallback(() => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + + setIdentify({ target: id, identify: false }); + }, [id, setIdentify]); + + // start a timer that will close the overlay after some time + useEffect(() => { + if (showOverlay) { + timerRef.current = setTimeout(handleClose, MILLIS_PER_MINUTE); + } + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, [showOverlay, id, setIdentify, handleClose]); + + console.log('here2'); + return ( +
+
{name}
+
Click to close
+
+ ); +} diff --git a/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx b/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx index 2e2a4ee19..59ba42937 100644 --- a/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx +++ b/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx @@ -19,10 +19,10 @@ import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical'; import { navigatorConstants } from '../../../viewerConfig'; import useClickOutside from '../../hooks/useClickOutside'; +import { useClientStore } from '../../stores/clientStore'; import { useViewOptionsStore } from '../../stores/viewOptions'; import { isKeyEnter } from '../../utils/keyEvent'; - -import RenameClientModal from './rename-client-modal/RenameClientModal'; +import { RenameClientModal } from '../client-modal/RenameClientModal'; import style from './NavigationMenu.module.scss'; @@ -34,8 +34,10 @@ interface NavigationMenuProps { function NavigationMenu(props: NavigationMenuProps) { const { isOpen, onClose } = props; - const { isOpen: isRenameOpen, onOpen: onRenameOpen, onClose: onRenameClose } = useDisclosure(); + const id = useClientStore((store) => store.id); + const name = useClientStore((store) => store.name); + const { isOpen: isOpenRename, onOpen: onRenameOpen, onClose: onCloseRename } = useDisclosure(); const { fullscreen, toggle } = useFullscreen(); const { toggleMirror } = useViewOptionsStore(); @@ -45,7 +47,7 @@ function NavigationMenu(props: NavigationMenuProps) { return createPortal(