diff --git a/package.json b/package.json index 4167e9c0..410cc164 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ "typecheck": "tsc --noEmit -p tsconfig.json" }, "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.0", + "@types/react-router-dom": "^5.3.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.0" + "react-router-dom": "^6.22.3" }, "devDependencies": { "@commitlint/cli": "^19.2.0", diff --git a/src/App.tsx b/src/App.tsx index b6de5dd1..dad596f4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,37 @@ +import { createBrowserRouter, RouterProvider } from "react-router-dom"; import "./App.css"; -import { Header } from "./components/header.tsx"; import { Production } from "./components/production.tsx"; -import { DeviceSelector } from "./components/device-select.tsx"; +import { ErrorPage } from "./components/router-error.tsx"; +import { useDevicePermissions } from "./hooks/device-permission.ts"; +import { LandingPage } from "./components/landing-page/landing-page.tsx"; + +const router = createBrowserRouter([ + { + path: "/", + element: , + errorElement: , + }, + { + path: "production/:productionId", + // loader: productionLoader, + element: , + }, +]); const App = () => { - return ( - <> -
- - - - ); + const { denied, permission } = useDevicePermissions(); + + if (denied) { + return ( +
+ Permission denied, reload browser and/or reset permissions to try again. +
+ ); + } + + if (!permission) return
Waiting for device permissions
; + + return ; }; export default App; diff --git a/src/components/landing-page/create-production.tsx b/src/components/landing-page/create-production.tsx new file mode 100644 index 00000000..30a97b25 --- /dev/null +++ b/src/components/landing-page/create-production.tsx @@ -0,0 +1,29 @@ +import { DisplayContainerHeader } from "./display-container-header.tsx"; +import { + DecorativeLabel, + FormContainer, + FormInput, + FormLabel, + SubmitButton, +} from "./form-elements.tsx"; + +export const CreateProduction = () => { + return ( + + Create Production + + Production Name + + + + Line + + + + Line + + + Create Production + + ); +}; diff --git a/src/components/landing-page/display-container-header.tsx b/src/components/landing-page/display-container-header.tsx new file mode 100644 index 00000000..83813b80 --- /dev/null +++ b/src/components/landing-page/display-container-header.tsx @@ -0,0 +1,8 @@ +import styled from "@emotion/styled"; + +export const DisplayContainerHeader = styled.h2` + font-size: 3rem; + font-weight: bold; + line-height: 1; + margin: 0 0 1rem; +`; diff --git a/src/components/landing-page/form-elements.tsx b/src/components/landing-page/form-elements.tsx new file mode 100644 index 00000000..9a474ad9 --- /dev/null +++ b/src/components/landing-page/form-elements.tsx @@ -0,0 +1,31 @@ +import styled from "@emotion/styled"; + +export const FormContainer = styled.div``; + +export const FormInput = styled.input` + width: 100%; +`; + +export const FormSelect = styled.select` + width: 100%; +`; + +export const FormLabel = styled.label` + display: block; + padding: 0 0 1rem; + input, + select { + font-size: 1.6rem; + display: inline-block; + } +`; + +export const DecorativeLabel = styled.span` + display: block; + white-space: nowrap; + padding: 0 1rem 1rem 0; +`; + +export const SubmitButton = styled.button` + font-size: 1.6rem; +`; diff --git a/src/components/header.tsx b/src/components/landing-page/header.tsx similarity index 82% rename from src/components/header.tsx rename to src/components/landing-page/header.tsx index c38f6c2b..a404712c 100644 --- a/src/components/header.tsx +++ b/src/components/landing-page/header.tsx @@ -1,6 +1,6 @@ import styled from "@emotion/styled"; import { FC } from "react"; -import { backgroundColour } from "../css-helpers/defaults"; +import { backgroundColour } from "../../css-helpers/defaults.ts"; const HeaderWrapper = styled.div` width: 100%; diff --git a/src/components/landing-page/join-production.tsx b/src/components/landing-page/join-production.tsx new file mode 100644 index 00000000..2b5b956a --- /dev/null +++ b/src/components/landing-page/join-production.tsx @@ -0,0 +1,49 @@ +import { DisplayContainerHeader } from "./display-container-header.tsx"; +import { + DecorativeLabel, + FormLabel, + FormContainer, + FormInput, + FormSelect, + SubmitButton, +} from "./form-elements.tsx"; + +export const JoinProduction = () => { + return ( + + Join Production + + Production ID + + + + Username + + + + Input + + + + + + + Output + + + + + + + + Line + + + + + + + Join + + ); +}; diff --git a/src/components/landing-page/landing-page.tsx b/src/components/landing-page/landing-page.tsx new file mode 100644 index 00000000..b75b8ed5 --- /dev/null +++ b/src/components/landing-page/landing-page.tsx @@ -0,0 +1,38 @@ +import styled from "@emotion/styled"; +import { Header } from "./header.tsx"; +import { JoinProduction } from "./join-production.tsx"; +import { CreateProduction } from "./create-production.tsx"; +import { ProductionsList } from "./productions-list.tsx"; + +const FlexContainer = styled.div` + display: flex; +`; + +const DisplayContainer = styled.div` + flex-basis: 100%; + display: flex; + + &:first-of-type { + border-right: 1px solid white; + padding: 5rem 2rem 5rem 5rem; + justify-content: flex-end; + } + &:last-of-type { + padding: 5rem 5rem 5rem 2rem; + } +`; + +export const LandingPage = () => ( + <> +
+ + + + + + + + + + +); diff --git a/src/components/landing-page/productions-list.tsx b/src/components/landing-page/productions-list.tsx new file mode 100644 index 00000000..c7ed8d42 --- /dev/null +++ b/src/components/landing-page/productions-list.tsx @@ -0,0 +1,69 @@ +import styled from "@emotion/styled"; + +const ProductionListContainer = styled.div` + display: flex; + padding: 2rem 0 2rem 2rem; + flex-wrap: wrap; +`; + +const ProductionItem = styled.div` + flex: 1 0 calc(25% - 2rem); + min-width: 20rem; + border: 1px solid #424242; + border-radius: 0.5rem; + padding: 2rem; + margin: 0 2rem 2rem 0; +`; + +const ProductionName = styled.div` + font-size: 1.4rem; + font-weight: bold; + margin: 0 0 1rem; + word-break: break-word; +`; + +const ProductionId = styled.div` + font-size: 2rem; + color: #9e9e9e; +`; + +export const ProductionsList = () => { + return ( + + + Mello + 123 + + + Bolibompa + 4 + + + Nyheterna + 928 + + + Sikta mot Stjärnorna + 38974 + + + Idol + 5 + + + + IdolIdol Idol Idol Idol Idol Idol Idol Idol Idol Idol Idol Idol Idol + Idol Idol Idol Idol Idol Idol Idol Idol Idol Idol + + 5 + + + + IdolIdolIdolIdolIdolIdolIdolIdolIdolIdolIdolIdolIdolIdol + IdolIdolIdolIdolIdolIdolIdolIdolIdolIdol + + 5 + + + ); +}; diff --git a/src/components/production.tsx b/src/components/production.tsx index e6193143..8525d107 100644 --- a/src/components/production.tsx +++ b/src/components/production.tsx @@ -8,5 +8,9 @@ const UserList = styled.div` `; export const Production: FC = () => { + // const { participants } = useLoaderData(); + // Mute/Unmute mic + // Mute/Unmute speaker + // Show active sink and mic return A User; }; diff --git a/src/components/router-error.tsx b/src/components/router-error.tsx new file mode 100644 index 00000000..8aee6e0d --- /dev/null +++ b/src/components/router-error.tsx @@ -0,0 +1,19 @@ +import { useRouteError } from "react-router-dom"; + +export const ErrorPage = () => { + const error = useRouteError(); + console.error(error); + + if (error instanceof Error) { + return ( +
+

Oops!

+

Sorry, an unexpected error has occurred.

+

+ {`${error.name} ${error.message}`} +

+
+ ); + } + return
{`Oops ${JSON.stringify(error)}`}
; +}; diff --git a/src/hooks/device-permission.ts b/src/hooks/device-permission.ts new file mode 100644 index 00000000..c1453a41 --- /dev/null +++ b/src/hooks/device-permission.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react"; + +export const useDevicePermissions = () => { + const [permission, setPermission] = useState(false); + const [denied, setDenied] = useState(false); + + useEffect(() => { + navigator.mediaDevices + .getUserMedia({ audio: true, video: false }) + .then(() => { + setDenied(false); + setPermission(true); + }) + .catch(() => { + setDenied(true); + setPermission(false); + }); + }, []); + + return { permission, denied }; +}; diff --git a/src/index.css b/src/index.css index 5aecfa1b..12a0f620 100644 --- a/src/index.css +++ b/src/index.css @@ -91,6 +91,7 @@ video { font: inherit; vertical-align: baseline; } + /* HTML5 display-role reset for older browsers */ article, aside, @@ -105,17 +106,21 @@ nav, section { display: block; } + body { line-height: 1; } + ol, ul { list-style: none; } + blockquote, q { quotes: none; } + blockquote:before, blockquote:after, q:before, @@ -123,6 +128,7 @@ q:after { content: ""; content: none; } + table { border-collapse: collapse; border-spacing: 0; @@ -148,7 +154,9 @@ html { box-sizing: border-box; /* easy REM sizing (e.g. 1.2rem = 12px) */ font-size: 62.5%; + height: 100%; } + *, *:before, *:after { @@ -160,6 +168,7 @@ html { body { font-size: 1.6rem; + height: 100%; } /* image size contstrained to container size */ @@ -169,6 +178,10 @@ img { height: auto; } +#root { + height: 100%; +} + /* * media queries here, or as part of layout component @media only screen and (min-width: 768px) { diff --git a/yarn.lock b/yarn.lock index b846697b..b7191fcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -512,6 +512,11 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@remix-run/router@1.15.3": + version "1.15.3" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.3.tgz#d2509048d69dbb72d5389a14945339f1430b2d3c" + integrity sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w== + "@rollup/rollup-android-arm-eabi@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz#b98786c1304b4ff8db3a873180b778649b5dff2b" @@ -670,6 +675,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/history@^4.7.11": + version "4.7.11" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== + "@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -704,6 +714,23 @@ dependencies: "@types/react" "*" +"@types/react-router-dom@^5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" + integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.20" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" + integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react@*", "@types/react@^18.2.64": version "18.2.66" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.66.tgz#d2eafc8c4e70939c5432221adb23d32d76bfe451" @@ -2996,6 +3023,21 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-router-dom@^6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.3.tgz#9781415667fd1361a475146c5826d9f16752a691" + integrity sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw== + dependencies: + "@remix-run/router" "1.15.3" + react-router "6.22.3" + +react-router@6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.3.tgz#9d9142f35e08be08c736a2082db5f0c9540a885e" + integrity sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ== + dependencies: + "@remix-run/router" "1.15.3" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"