Browse Source

Split index.tsx up into separate modules

Joeri Exelmans 2 years ago
parent
commit
0b42ed95f9
7 changed files with 591 additions and 328 deletions
  1. 2 36
      dist/index.html
  2. 1 0
      dist/logoFM.svg
  3. 264 0
      dist/logoUA.svg
  4. 138 0
      src/frontend/connectiontab.tsx
  5. 20 292
      src/frontend/index.tsx
  6. 53 0
      src/frontend/loginform.tsx
  7. 113 0
      src/frontend/videowallcontrol.tsx

+ 2 - 36
dist/index.html

@@ -2,44 +2,10 @@
 <html>
   <head>
     <meta charset="UTF-8"/>
-<link rel="icon" href="/themes/custom/dropsolid-theme-flex-8/favicon.ico" type="image/vnd.microsoft.icon" />
-
-          <link rel="apple-touch-icon" sizes="57x57" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/apple-touch-icon-57x57.png">
-      <link rel="apple-touch-icon" sizes="60x60" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/apple-touch-icon-60x60.png">
-      <link rel="apple-touch-icon" sizes="72x72" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/apple-touch-icon-72x72.png">
-      <link rel="apple-touch-icon" sizes="76x76" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/apple-touch-icon-76x76.png">
-      <link rel="apple-touch-icon" sizes="114x114" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/apple-touch-icon-114x114.png">
-      <link rel="apple-touch-icon" sizes="120x120" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/apple-touch-icon-120x120.png">
-      <link rel="apple-touch-icon" sizes="144x144" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/apple-touch-icon-144x144.png">
-      <link rel="apple-touch-icon" sizes="152x152" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/apple-touch-icon-152x152.png">
-      <link rel="apple-touch-icon" sizes="180x180" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/apple-touch-icon-180x180.png">
-
-      <link rel='mask-icon' color='#0096A9' href='/themes/custom/dropsolid-theme-flex-8/favicons/generated/safari-pinned-tab.svg'>
-
-      <link rel="icon" type="image/png" sizes="36x36"  href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/android-chrome-36x36.png">
-      <link rel="icon" type="image/png" sizes="48x48"  href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/android-chrome-48x48.png">
-      <link rel="icon" type="image/png" sizes="72x72"  href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/android-chrome-72x72.png">
-      <link rel="icon" type="image/png" sizes="96x96"  href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/android-chrome-96x96.png">
-      <link rel="icon" type="image/png" sizes="144x144"  href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/android-chrome-144x144.png">
-      <link rel="icon" type="image/png" sizes="192x192"  href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/android-chrome-192x192.png">
-      <link rel="icon" type="image/png" sizes="256x256"  href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/android-chrome-256x256.png">
-
-      <link rel="icon" type="image/png" sizes="32x32" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/favicon-32x32.png">
-      <link rel="icon" type="image/png" sizes="16x16" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/favicon-16x16.png">
-      <link rel="icon" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/favicon.ico">
-
-      <link rel="manifest" href="/themes/custom/dropsolid-theme-flex-8/favicons/generated/manifest.json">
-      <meta name="msapplication-TileColor" content="#0096A9">
-      <meta name="msapplication-TileImage" content="/themes/custom/dropsolid-theme-flex-8/favicons/generated/ms-icon-70x70.png">
-      <meta name="msapplication-TileImage" content="/themes/custom/dropsolid-theme-flex-8/favicons/generated/ms-icon-144x144.png">
-      <meta name="msapplication-TileImage" content="/themes/custom/dropsolid-theme-flex-8/favicons/generated/ms-icon-150x150.png">
-      <meta name="msapplication-TileImage" content="/themes/custom/dropsolid-theme-flex-8/favicons/generated/ms-icon-310x310.png">
-      <meta name="msapplication-TileImage" content="/themes/custom/dropsolid-theme-flex-8/favicons/generated/ms-icon-310x150.png">
-
-    <title>CDF Mod Tool</title>
+    <title>CDF Moderator</title>
   </head>
   <body>
     <div id="root"></div>
-    <script type="module" src="./bundle.js"></script>
+    <script src="./bundle.js"></script>
   </body>
 </html>

File diff suppressed because it is too large
+ 1 - 0
dist/logoFM.svg


File diff suppressed because it is too large
+ 264 - 0
dist/logoUA.svg


+ 138 - 0
src/frontend/connectiontab.tsx

@@ -0,0 +1,138 @@
+import * as React from 'react';
+import * as api from "./api";
+
+import {
+  Text,
+  Button,
+  Space,
+  Title,
+  NumberInput,
+  List,
+} from '@mantine/core';
+
+import {IconAlertCircle} from "@tabler/icons";
+
+import {VideoWallControl, Items} from "./videowallcontrol";
+
+export interface ConnectionProps {
+  server: string;
+  user: string;
+  pass: string;
+}
+
+type ConnectingState = {
+  state: "connecting";
+};
+
+type ErrorState = {
+  state: "error";
+  msg: string;
+};
+
+type SuccessState = {
+  state: "success";
+  accessToken: string;
+  updating: boolean; // did we just send an uncompleted request to the server to change the layout?
+};
+
+type ConnectionState =
+  | ConnectingState
+  | ErrorState
+  | SuccessState;
+
+export function ConnectionTab(props: ConnectionProps) {
+  const [state, setState] = React.useState<ConnectionState>({state: "connecting"});
+  const [items, setItems] = React.useState<Items>({videowall: [], hidden: []});
+
+  function refreshScreenShares(accessToken) {
+    return api.getScreenShares({
+      server: props.server,
+      userEmail: props.user,
+      accessToken,
+    });
+  }
+
+  function attemptConnect() {
+    api.login({
+      server: props.server,
+      userEmail: props.user,
+      password: props.pass,
+    })
+    .then(accessToken => {
+      return refreshScreenShares(accessToken)
+      .then(setItems)
+      .then(() => {
+        setState({state: 'success', accessToken, updating: false});
+      });
+    })
+    .catch(err => {
+      setState({state: 'error', msg: err.message});
+    });    
+  }
+
+  React.useEffect(() => {
+    attemptConnect();
+  }, []);
+
+  const [pollInterval, setPollInterval] = React.useState<number>(1000);
+
+  React.useEffect(() => {
+    let canceled = false;
+    if (state.state === "success" && !state.updating) {
+      const accessToken = state.accessToken;
+      setTimeout(() => {
+        refreshScreenShares(accessToken)
+        .then(items => {
+          console.log("setItems due to POLL");
+          if (!canceled) setItems(items);
+        })
+        .catch(err => {
+          if (!canceled) setState({state: 'error', msg: err.message});
+        });
+      }, pollInterval);
+    }
+    return function cleanup() {
+      canceled = true;
+    }
+  }, [state, items, pollInterval]);
+
+  switch (state.state) {
+    case "connecting":
+      return <Text>Connecting...</Text>;
+    case "success":
+      return <>
+        <VideoWallControl {...{items, setItems}} onChange={newItems => {
+          console.log("SEND UPDATE");
+          setItems(newItems);
+          setState(oldState => ({...oldState, updating: true}));
+          api.setScreenShares({
+            server: props.server,
+            userEmail: props.user,
+            accessToken: state.accessToken,
+            visibleShares: newItems.videowall,
+          })
+          .then(result => {
+            console.log("UPDATE RESULT:", result);
+            setState(oldState => ({...oldState, updating: false}));
+          })
+          .catch(err => {
+            setState({state: 'error', msg: err.message});
+          });
+        }}/>
+        <Space h="xl"/>
+        <Title order={5}>Advanced</Title>
+        <Space h="md"/>
+        <NumberInput label="Polling interval (ms)" value={pollInterval} onChange={setPollInterval} step={100}/>
+      </>;
+    case "error":
+      return <>
+        <Title order={5}>Something went wrong</Title>
+        <Space h="xl"/>
+        <List style={{color:"red"}} icon={<IconAlertCircle/>}>
+          <List.Item>{state.msg}</List.Item>
+        </List>
+        <Space h="xl"/>
+        <Button onClick={attemptConnect}>Retry</Button>
+      </>;
+  }
+}

+ 20 - 292
src/frontend/index.tsx

@@ -4,305 +4,21 @@ import * as React from 'react';
 import {createRoot} from 'react-dom/client';
 
 import {
-  TextInput,
-  Text,
-  PasswordInput,
-  Button,
   Group,
-  Space,
   Tabs,
   Stack,
-  CloseButton,
-  Card,
   Title,
-  Center,
-  Accordion,
-  SimpleGrid,
   Anchor,
-  ActionIcon,
-  LoadingOverlay,
-  NumberInput,
-  List,
   MantineProvider,
+  Space,
 } from '@mantine/core';
-import {useColorScheme} from '@mantine/hooks';
-
-import {IconAlertCircle} from "@tabler/icons";
-
-import {
-  DragDropContext,
-  Droppable,
-  Draggable,
-} from "react-beautiful-dnd";
-
-import * as api from "./api";
-
-const container = document.getElementById('root');
-const root = createRoot(container!);
-
-interface ConnectionProps {
-  server: string;
-  user: string;
-  pass: string;
-}
-
-interface LoginProps {
-  onLogin: (c: ConnectionProps) => void;
-}
-
-function LoginForm(props: LoginProps) {
-  const [server, setServer] = React.useState<string>("https://msdl-testing.uantwerpen.be");
-  const [user, setUser] = React.useState<string>("joeri+1@uantwerpen.be");
-  const [pass, setPass] = React.useState<string>("drovio123");
-
-  return <form>
-    <SimpleGrid cols={2} style={{maxWidth: 1000}}>
-    <div>
-      <Title order={5}>Credentials</Title>
-      <Space h="md"/>
-      <TextInput
-        label="Drovio User (of CDF room)"
-        value={user}
-        onChange={e => setUser(e.currentTarget.value)}
-        error={/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(user) ? "Invalid email" : undefined}
-      />
-      <Space h="md"/>
-      <PasswordInput
-        label="Drovio Password"
-        value={pass}
-        onChange={e => setPass(e.currentTarget.value)}
-      />
-    </div>
-    <div>
-      <Title order={5}>Advanced</Title>
-      <Space h="md"/>
-      <TextInput
-        label="Server"
-        value={server}
-        onChange={e => setServer(e.currentTarget.value)}
-      />
-    </div>
-    </SimpleGrid>
-    <Space h="xl"/>
-    <Button disabled={user.length === 0} onClick={() => props.onLogin({server, user, pass})}>Login</Button>
-  </form>;
-}
-
-type ConnectingState = {
-  state: "connecting";
-};
-
-type ErrorState = {
-  state: "error";
-  msg: string;
-};
-
-type SuccessState = {
-  state: "success";
-  accessToken: string;
-  updating: boolean; // did we just send an uncompleted request to the server to change the layout?
-};
-
-type ConnectionState =
-  | ConnectingState
-  | ErrorState
-  | SuccessState;
-
-interface ItemProps {
-  children?: React.ReactNode,
-  id: string,
-  style?: object,
-  dragging?: boolean,
-}
-
-function Item(props: ItemProps) {
-  const style: React.CSSProperties = {
-    cursor: props.dragging ? 'grabbing' : 'grab',
-    // height: '100%',
-    boxShadow: props.dragging ? "0px 0px 30px rgba(0,0,0,0.7)" : "0px 0px 5px rgba(0,0,0,0.5)",
-    backgroundColor: "white",
-    userSelect: 'none',
-    margin: 20,
-    minWidth: 200,
-    height: 120,
-
-    ...props.style,
-  };
 
-  return <Center {...props} style={style}>{props.id}</Center>
-}
-
-type LayoutType = "row" | "grid";
-
-type Items = {videowall: Array<string>, hidden: Array<string>}
-
-interface VideoWallControlProps {
-  items: Items;
-  setItems: (callback: (Items) => Items) => void;
-  freeze: boolean;
-
-  onChange: (Items) => void;
-}
-
-function VideoWallControl(props: VideoWallControlProps) {
-  function onDragEnd(result) {
-    if (!result.destination) {
-      return;
-    }
-    props.setItems(items => {
-      const newItems = {
-        videowall: items.videowall.slice(),
-        hidden: items.hidden.slice(),
-      }
-      const removed = newItems[result.source.droppableId].splice(result.source.index, 1);
-      newItems[result.destination.droppableId].splice(result.destination.index, 0, ...removed);
-
-      props.onChange(newItems);
-      return newItems;
-    });
-  }
-
-  return <>
-    <DragDropContext onDragEnd={onDragEnd}>
-      <div style={{position:"relative"}}>
-      <LoadingOverlay visible={false} overlayBlur={2}/>
-      <Title order={5}>Video Wall</Title>
-      <Space h="md"/>
-      <Droppable droppableId="videowall" direction="horizontal">
-        {(provided, snapshot) => (
-          <div ref={provided.innerRef} {...provided.droppableProps} style={{height: 160, backgroundColor: '#0096a9', position:"relative"}}>
-            <Group spacing={0} style={{height: '100%'}}>
-              {props.items.videowall.map((item, index) => (
-                <Draggable key={item} draggableId={item} index={index} style={{height: '100%'}}>
-                  {(provided, snapshot) =>
-                    <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
-                      <Item id={item} dragging={snapshot.isDragging}/>
-                    </div>}
-                </Draggable>
-              ))}
-              {props.items.videowall.length === 0 ? <Center style={{width:"100%",height:"100%"}}><Text color="white">No active screenshares.</Text></Center> : <></>}
-              <div/>
-            </Group>
-            {provided.placeholder}
-          </div>
-        )}
-      </Droppable>
-      <Space h="xl"/>
-      <Title order={5}>Hidden Screenshares</Title>
-      <Space h="md"/>
-      <Droppable droppableId="hidden" direction="horizontal">
-        {(provided, snapshot) => (
-          <div ref={provided.innerRef} {...provided.droppableProps} style={{height: 160, backgroundColor: '#555059', position:"relative"}}>
-            <Group spacing={0} style={{height: '100%'}}>
-              {props.items.hidden.map((item, index) => (
-                <Draggable key={item} draggableId={item} index={index}>
-                  {(provided, snapshot) =>
-                    <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
-                      <Item id={item} dragging={snapshot.isDragging}/>
-                    </div>}
-                </Draggable>
-              ))}
-              {props.items.hidden.length === 0 ? <Center style={{width:"100%",height:"100%"}}><Text color="white">No hidden screenshares.</Text></Center> : <></>}
-              <div/>
-            </Group>
-            {provided.placeholder}
-          </div>
-        )}
-      </Droppable>
-      </div>
-    </DragDropContext>
-  </>;
-}
-
-
-function ConnectionTab(props: ConnectionProps) {
-  const [state, setState] = React.useState<ConnectionState>({state: "connecting"});
-  const [items, setItems] = React.useState<Items>({videowall: [], hidden: []});
-
-  function refreshScreenShares(accessToken) {
-    return api.getScreenShares({
-      server: props.server,
-      userEmail: props.user,
-      accessToken,
-    });
-  }
-
-  function attemptConnect() {
-    api.login({
-      server: props.server,
-      userEmail: props.user,
-      password: props.pass,
-    })
-    .then(accessToken => {
-      return refreshScreenShares(accessToken)
-      .then(setItems)
-      .then(() => {
-        setState({state: 'success', accessToken, updating: false});
-      });
-    })
-    .catch(err => {
-      setState({state: 'error', msg: err.message});
-    });    
-  }
-
-  React.useEffect(() => {
-    attemptConnect();
-  }, []);
-
-  const [pollInterval, setPollInterval] = React.useState<number>(1000);
+import {useColorScheme} from '@mantine/hooks';
 
-  React.useEffect(() => {
-    let canceled = false;
-    if (state.state === "success" && !state.updating) {
-      const accessToken = state.accessToken;
-      setTimeout(() => {
-        refreshScreenShares(accessToken)
-        .then(items => {
-          console.log("setItems due to POLL");
-          if (!canceled) setItems(items);
-        });
-      }, pollInterval);
-    }
-    return function cleanup() {
-      canceled = true;
-    }
-  }, [state, items, pollInterval]);
+import {IconExternalLink} from "@tabler/icons";
 
-  switch (state.state) {
-    case "connecting":
-      return <Text>Connecting...</Text>;
-    case "success":
-      return <>
-        <VideoWallControl {...{items, setItems}} freeze={state.updating} onChange={items => {
-          // setItems(items);
-          console.log("SEND UPDATE");
-          setState(oldState => ({...oldState, updating: true}));
-          api.setScreenShares({
-            server: props.server,
-            userEmail: props.user,
-            accessToken: state.accessToken,
-            visibleShares: items.videowall,
-          })
-          .then(result => {
-            console.log("UPDATE RESULT:", result);
-            setState(oldState => ({...oldState, updating: false}));
-          })
-        }}/>
-        <Space h="xl"/>
-        <Title order={5}>Advanced</Title>
-        <Space h="md"/>
-        <NumberInput label="Polling interval (ms)" value={pollInterval} onChange={setPollInterval} step={100}/>
-      </>;
-    case "error":
-      return <>
-        <List style={{color:"red"}} icon={<IconAlertCircle/>}>
-          <List.Item>Error {state.msg}</List.Item>
-        </List>
-        <Space h="xl"/>
-        <Button onClick={attemptConnect}>Retry</Button>
-      </>;
-  }
-}
+import {ConnectionTab, ConnectionProps} from "./connectiontab";
+import {LoginForm} from "./loginform";
 
 function App() {
   const [connections, setConnections] = React.useState<Array<ConnectionProps>>([]);
@@ -318,10 +34,12 @@ function App() {
   return (
     <MantineProvider theme={{colorScheme: preferredColorScheme}} withGlobalStyles withNormalizeCSS>
       <Stack style={{userSelect: 'none'}}>
-        {/*<Group position="left"></Group>*/}
         <Group position="left" style={{padding:10}}>
-        <Title order={3}>CDF Mod Tool</Title>
-        <img src="https://www.flandersmake.be/themes/custom/dropsolid-theme-flex-8/logo.svg" style={{width:100, position: "absolute", right: 10, top:10}}/>
+          <Title order={3}>CDF Moderator</Title>
+          <Group style={{position: "absolute", right:8, top:8}}>
+          <img src="logoFM.svg" style={{height:40}}/>
+          {/*<img src="logoUA.svg" style={{height:45}}/>*/}
+          </Group>
         </Group>
         <Tabs value={currentTab} onTabChange={setCurrentTab}>
           <Tabs.List>
@@ -348,10 +66,20 @@ function App() {
               <ConnectionTab server={c.server} user={c.user} pass={c.pass}/>
             </Tabs.Panel>)}
         </Tabs>
+        {/* Make sure we can always scroll down far enough, such that nothing is hidden behind the viewport-fixed source code link: */}
+        <Space h="lg"/>
       </Stack>
+      <Anchor href="https://msdl.uantwerpen.be/git/jexelmans/cdf-modtool"
+              target="_blank"
+              style={{position: "fixed", bottom: 8, right: 8}}>
+          <IconExternalLink size={14} style={{marginRight: 4}}/>Source Code
+      </Anchor>
+
     </MantineProvider>
   );
 }
 
+const container = document.getElementById('root');
+const root = createRoot(container!);
 
 root.render( <App/> );

+ 53 - 0
src/frontend/loginform.tsx

@@ -0,0 +1,53 @@
+import * as React from 'react';
+
+import {
+  TextInput,
+  PasswordInput,
+  Button,
+  Space,
+  Title,
+  SimpleGrid,
+} from '@mantine/core';
+
+import {ConnectionProps} from "./connectiontab";
+
+export interface LoginProps {
+  onLogin: (c: ConnectionProps) => void;
+}
+
+export function LoginForm(props: LoginProps) {
+  const [server, setServer] = React.useState<string>("https://msdl-testing.uantwerpen.be");
+  const [user, setUser] = React.useState<string>("");
+  const [pass, setPass] = React.useState<string>("");
+
+  return <form>
+    <SimpleGrid cols={2} style={{maxWidth: 1000}}>
+    <div>
+      <Title order={5}>Credentials</Title>
+      <Space h="md"/>
+      <TextInput
+        label="Drovio User (of CDF room)"
+        value={user}
+        onChange={e => setUser(e.currentTarget.value)}
+      />
+      <Space h="md"/>
+      <PasswordInput
+        label="Drovio Password"
+        value={pass}
+        onChange={e => setPass(e.currentTarget.value)}
+      />
+    </div>
+    <div>
+      <Title order={5}>Advanced</Title>
+      <Space h="md"/>
+      <TextInput
+        label="Server"
+        value={server}
+        onChange={e => setServer(e.currentTarget.value)}
+      />
+    </div>
+    </SimpleGrid>
+    <Space h="xl"/>
+    <Button disabled={user.length === 0} onClick={() => props.onLogin({server, user, pass})}>Login</Button>
+  </form>;
+}

+ 113 - 0
src/frontend/videowallcontrol.tsx

@@ -0,0 +1,113 @@
+import * as React from 'react';
+
+import {
+  Text,
+  Group,
+  Space,
+  Title,
+  Center,
+} from '@mantine/core';
+
+import {
+  DragDropContext,
+  Droppable,
+  Draggable,
+} from "react-beautiful-dnd";
+
+interface ItemProps {
+  children?: React.ReactNode,
+  id: string,
+  style?: object,
+  dragging?: boolean,
+}
+
+function Item(props: ItemProps) {
+  const style: React.CSSProperties = {
+    cursor: props.dragging ? 'grabbing' : 'grab',
+    // height: '100%',
+    boxShadow: props.dragging ? "0px 0px 30px rgba(0,0,0,0.7)" : "0px 0px 5px rgba(0,0,0,0.5)",
+    backgroundColor: "white",
+    userSelect: 'none',
+    margin: 20,
+    minWidth: 200,
+    height: 120,
+
+    ...props.style,
+  };
+
+  return <Center {...props} style={style}>{props.id}</Center>
+}
+
+type LayoutType = "row" | "grid";
+
+export type Items = {videowall: Array<string>, hidden: Array<string>}
+
+interface VideoWallControlProps {
+  items: Items;
+  onChange: (Items) => void;
+}
+
+export function VideoWallControl(props: VideoWallControlProps) {
+  function onDragEnd(result) {
+    if (!result.destination) {
+      return;
+    }
+    const newItems = {
+      videowall: props.items.videowall.slice(),
+      hidden: props.items.hidden.slice(),
+    }
+    const removed = newItems[result.source.droppableId].splice(result.source.index, 1);
+    newItems[result.destination.droppableId].splice(result.destination.index, 0, ...removed);
+    props.onChange(newItems);
+  }
+
+  return <>
+    <DragDropContext onDragEnd={onDragEnd}>
+      <div style={{position:"relative"}}>
+      <Title order={5}>Video Wall</Title>
+      <Space h="md"/>
+      <Droppable droppableId="videowall" direction="horizontal">
+        {(provided, snapshot) => (
+          <div ref={provided.innerRef} {...provided.droppableProps} style={{height: 160, backgroundColor: '#0096a9', position:"relative"}}>
+            <Group spacing={0} style={{height: '100%'}}>
+              {props.items.videowall.map((item, index) => (
+                <Draggable key={item} draggableId={item} index={index} style={{height: '100%'}}>
+                  {(provided, snapshot) =>
+                    <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
+                      <Item id={item} dragging={snapshot.isDragging}/>
+                    </div>}
+                </Draggable>
+              ))}
+              {props.items.videowall.length === 0 ? <Center style={{width:"100%",height:"100%"}}><Text color="white">No active screenshares.</Text></Center> : <></>}
+              <div/>
+            </Group>
+            {provided.placeholder}
+          </div>
+        )}
+      </Droppable>
+      <Space h="xl"/>
+      <Title order={5}>Hidden Screenshares</Title>
+      <Space h="md"/>
+      <Droppable droppableId="hidden" direction="horizontal">
+        {(provided, snapshot) => (
+          <div ref={provided.innerRef} {...provided.droppableProps} style={{height: 160, backgroundColor: '#555059', position:"relative"}}>
+            <Group spacing={0} style={{height: '100%'}}>
+              {props.items.hidden.map((item, index) => (
+                <Draggable key={item} draggableId={item} index={index}>
+                  {(provided, snapshot) =>
+                    <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
+                      <Item id={item} dragging={snapshot.isDragging}/>
+                    </div>}
+                </Draggable>
+              ))}
+              {props.items.hidden.length === 0 ? <Center style={{width:"100%",height:"100%"}}><Text color="white">No hidden screenshares.</Text></Center> : <></>}
+              <div/>
+            </Group>
+            {provided.placeholder}
+          </div>
+        )}
+      </Droppable>
+      </div>
+    </DragDropContext>
+  </>;
+}