From 3d51368825b203549ce5ecc8c397a2f37a60d883 Mon Sep 17 00:00:00 2001 From: Dominic Zimmer Date: Fri, 28 Oct 2022 15:21:50 +0200 Subject: [PATCH] Improve frontend --- frontend/public/index.html | 3 +- frontend/src/App.test.tsx | 2 +- frontend/src/App.tsx | 52 ---------------------- frontend/src/{App.css => Client.css} | 48 ++++++++++++++------ frontend/src/Client.tsx | 66 ++++++++++++++++++++++++++++ frontend/src/TVMode.css | 16 +++++++ frontend/src/TVMode.tsx | 58 ++++++++++++++++++++++++ frontend/src/index.tsx | 8 +++- frontend/src/types.ts | 25 +++++++++++ 9 files changed, 208 insertions(+), 70 deletions(-) delete mode 100644 frontend/src/App.tsx rename frontend/src/{App.css => Client.css} (74%) create mode 100644 frontend/src/Client.tsx create mode 100644 frontend/src/TVMode.css create mode 100644 frontend/src/TVMode.tsx create mode 100644 frontend/src/types.ts diff --git a/frontend/public/index.html b/frontend/public/index.html index aa069f2..f9a560d 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -3,7 +3,8 @@ - + { render(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 891d1b8..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import './App.css'; - -type State = { - -}; - -function App() { - - const [state, setState] = useState({}); - const [socket, setSocket] = useState(); - - useEffect(() => { - const url = new URL(`api/client`, window.location.href); - url.protocol = url.protocol.replace("http", "ws"); - const sock = new WebSocket(url.href); - - sock.onmessage = (e) => { - const newState = JSON.parse(e.data) as State; - setState(newState); - } - setSocket(sock); - - return () => { - sock.close(); - setSocket(undefined); - }; - }, []) - - const clickButton = (button: string) => (e: React.MouseEvent) => { - if (!socket) return; - socket.send(JSON.stringify({ "button": button })); - }; - - return ( -
-
- State:
{JSON.stringify(state)}
-
-
-
- {"up down left right a b l r start select".split(" ").map((b) => -
{b}
- )} -
-
-
-
- ); -} - -export default App; diff --git a/frontend/src/App.css b/frontend/src/Client.css similarity index 74% rename from frontend/src/App.css rename to frontend/src/Client.css index 8fe7241..d99f46f 100644 --- a/frontend/src/App.css +++ b/frontend/src/Client.css @@ -1,12 +1,25 @@ @media (orientation: landscape) { - body { - flex-direction: row; + .button-start { + margin-bottom: 10px; + grid-column: 9; + grid-row: 7; + } + .button-select { + grid-column: 7; + grid-row: 7; } } @media (orientation: portrait) { - body { - flex-direction: column; + .button-start { + margin-bottom: 10px; + justify-self: end; + grid-column: 13; + grid-row: 7; + } + .button-select { + grid-column: 2; + grid-row: 7; } } @@ -25,6 +38,9 @@ user-select: none; } +.button.pressed { + filter: drop-shadow(2px 2px 6px black); +} .button-a, .button-b { --button-height: 50px; --button-width: 50px; @@ -81,9 +97,8 @@ width: 100%; padding: 10px; display: grid; - /*grid-template-columns: auto auto auto 1fr auto auto;*/ - grid-template-columns: auto 10px auto auto auto auto 1fr auto auto 10px auto; - grid-template-rows: auto 10px auto auto auto 1fr auto auto; + grid-template-columns: 10px 10px auto auto auto 1fr auto 10px auto 1fr auto auto 10px 10px; + grid-template-rows: auto 1fr auto auto auto 1fr auto auto; } .button-up { grid-column: 4; @@ -102,16 +117,17 @@ grid-row: 5; } .button-dummy { + pointer-events: none; grid-column: 4; grid-row: 4; } .button-a { - grid-column: 9; + grid-column: 12; grid-row: 4; transform: scale(1.5); } .button-b { - grid-column: 8; + grid-column: 11; grid-row: 4; transform: scale(1.5) translate(-45%, 45%); } @@ -120,15 +136,19 @@ grid-column: 1; } .button-r { + justify-self: end; grid-row: 1; - grid-column: 11; + grid-column: 14; } + +/* .button-start { margin-bottom: 10px; - grid-column: 6; + grid-column: 9; grid-row: 7; } .button-select { - grid-column: 6; - grid-row: 8; -} \ No newline at end of file + grid-column: 7; + grid-row: 7; +} +*/ \ No newline at end of file diff --git a/frontend/src/Client.tsx b/frontend/src/Client.tsx new file mode 100644 index 0000000..61b12d6 --- /dev/null +++ b/frontend/src/Client.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react'; +import './Client.css'; +import { ButtonMap, ButtonType, buttonTypeList, defaultButtonMap, mapToBitvector, WGPPState } from './types'; + + + +const Client = () => { + + const [state, setState] = useState>({}); + const [socket, setSocket] = useState(); + const [buttonMap, setButtonMap] = useState(defaultButtonMap); + + const updateButtonEvent = (button: ButtonType, state: "down" | "up") => + () => setButtonMap((m) => ({ ...m, [button]: (state === "down") })); + + + // Send data to server + useEffect(() => { + if (!socket) return; + //socket.send(JSON.stringify(buttonMap)); + }, [buttonMap, socket]); + + useEffect(() => { + const url = new URL(`api/client`, window.location.href); + url.protocol = url.protocol.replace("http", "ws"); + const sock = new WebSocket(url.href); + + sock.onmessage = (e) => { + const newState = JSON.parse(e.data) as Partial; + // Merge old and new state + setState(oldState => ({ ...oldState, ...newState })); + } + setSocket(sock); + + return () => { + sock.close(); + setSocket(undefined); + }; + }, []) + + return ( +
+
+ State:
{JSON.stringify(state)}
+ ButtonMap:
{mapToBitvector(buttonMap).map(s => s.toString()).join(" ")}
+
+
+
+ {buttonTypeList.map((b, index) => +
+ {b} +
+ )} +
+
+
+
+ ); +} + +export default Client; diff --git a/frontend/src/TVMode.css b/frontend/src/TVMode.css new file mode 100644 index 0000000..bda63ad --- /dev/null +++ b/frontend/src/TVMode.css @@ -0,0 +1,16 @@ +body > div { + width: 250px; + height: 50vh; + background-color: #00ff00; + padding: 5px; + display: flex; + flex-direction: column; +} +.heading { + display: flex; + flex-direction: row; + justify-content: space-between; +} +span { + font-weight: bold; +} \ No newline at end of file diff --git a/frontend/src/TVMode.tsx b/frontend/src/TVMode.tsx new file mode 100644 index 0000000..66ed12e --- /dev/null +++ b/frontend/src/TVMode.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from 'react'; +import './TVMode.css'; +import { WGPPState } from './types'; + +const TVMode = () => { + + const [state, setState] = useState>({}); + const [socket, setSocket] = useState(); + const [endTime, setEndTime] = useState(0); + const [secondsRemaining, setSecondsRemaining] =useState(0); + + useEffect(() => { + setEndTime(Date.now() + (state?.timeUntilNextMode??15) * 1000); + const interval = setInterval(() => { + const remaining = Math.round((endTime - Date.now())/1000); + setSecondsRemaining(remaining < 0 ? 0 : remaining); + }, 500); + return () => clearInterval(interval); + }, [endTime, state?.timeUntilNextMode]); + + useEffect(() => { + const url = new URL(`api/client`, window.location.href); + url.protocol = url.protocol.replace("http", "ws"); + const sock = new WebSocket(url.href); + + sock.onmessage = (e) => { + const newState = JSON.parse(e.data) as Partial; + // Merge old and new state + setState(oldState => ({ ...oldState , ...newState })); + } + setSocket(sock); + + return () => { + sock.close(); + setSocket(undefined); + }; + }, []) + + return (state === undefined ? + Loading... : +
+
+ Mode: {state.mode} +
+ {secondsRemaining}s ⏱️ +
+
+
+ Current state:
+          {JSON.stringify(state)}
+        
+
+ +
+ ); +} + +export default TVMode; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 032464f..7835648 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,15 +1,19 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import Client from './Client'; import './index.css'; -import App from './App'; import reportWebVitals from './reportWebVitals'; +import TVMode from './TVMode'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); +const tvUi = document.location.href.endsWith("stream"); root.render( - + {tvUi ? + : + } ); diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..e2930fb --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,25 @@ + + +export const defaultButtonMap : ButtonMap = {up: false, down: false, left: false, right: false, a: false, b: false, l: false, r: false, start: false, select: false}; + +export type GameMode = "democracy" | "anarchy" | "random"; + +export type WGPPState = { + // current game mode + mode: GameMode; + allowMultitouch?: boolean; // derived from mode + + // stuff for TV + nextMode: GameMode; + timeUntilNextMode: number; + votes: {[button in ButtonType]?: number}; + + playerIdle: boolean; // default true, removed on first message +}; + +export const buttonTypeList: ButtonType[] = "up down left right a b l r start select".split(" ") as ButtonType[]; + +export type ButtonType = "up" | "down" | "left" | "right" | "a" | "b" | "l" | "r" | "start" | "select"; +export type ButtonMap = { [key in ButtonType]: boolean }; + +export const mapToBitvector = (map: ButtonMap): number[] => buttonTypeList.map(b => map[b] ? 1: 0); \ No newline at end of file