Improve frontend

This commit is contained in:
Dominic Zimmer 2022-10-28 15:21:50 +02:00
parent 570ffd4dff
commit 3d51368825
9 changed files with 208 additions and 70 deletions

View File

@ -3,7 +3,8 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name="theme-color" content="#000000" />
<meta
name="description"

View File

@ -1,6 +1,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
import App from './Client';
test('renders learn react link', () => {
render(<App />);

View File

@ -1,52 +0,0 @@
import React, { useEffect, useState } from 'react';
import './App.css';
type State = {
};
function App() {
const [state, setState] = useState<State>({});
const [socket, setSocket] = useState<WebSocket>();
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 (
<div>
<div style={{ position: "absolute", top: 10, left: "50%" }}>
State: <pre>{JSON.stringify(state)}</pre>
</div>
<div className="buttonwrap">
<div className="buttons">
{"up down left right a b l r start select".split(" ").map((b) =>
<div className={`button button-${b}`} onClick={clickButton(b)}>{b}</div>
)}
<div className="button button-dummy" />
</div>
</div>
</div>
);
}
export default App;

View File

@ -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;
}
grid-column: 7;
grid-row: 7;
}
*/

66
frontend/src/Client.tsx Normal file
View File

@ -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<Partial<WGPPState>>({});
const [socket, setSocket] = useState<WebSocket>();
const [buttonMap, setButtonMap] = useState<ButtonMap>(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<WGPPState>;
// Merge old and new state
setState(oldState => ({ ...oldState, ...newState }));
}
setSocket(sock);
return () => {
sock.close();
setSocket(undefined);
};
}, [])
return (
<div>
<div style={{ transform: "translate(-50%, 0)",position: "absolute", top: 10, left: "50%" }}>
State: <pre>{JSON.stringify(state)}</pre>
ButtonMap: <pre>{mapToBitvector(buttonMap).map(s => s.toString()).join(" ")}</pre>
</div>
<div className="buttonwrap">
<div className="buttons">
{buttonTypeList.map((b, index) =>
<div key={index} className={`button button-${b}${buttonMap[b] ? " pressed" : ""}`}
onTouchStart={updateButtonEvent(b, "down")}
onTouchEnd={updateButtonEvent(b, "up")}
onMouseDown={updateButtonEvent(b, "down")}
onMouseUp={updateButtonEvent(b, "up")}
>
{b}
</div>
)}
<div className="button button-dummy" />
</div>
</div>
</div >
);
}
export default Client;

16
frontend/src/TVMode.css Normal file
View File

@ -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;
}

58
frontend/src/TVMode.tsx Normal file
View File

@ -0,0 +1,58 @@
import { useEffect, useState } from 'react';
import './TVMode.css';
import { WGPPState } from './types';
const TVMode = () => {
const [state, setState] = useState<Partial<WGPPState>>({});
const [socket, setSocket] = useState<WebSocket>();
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<WGPPState>;
// Merge old and new state
setState(oldState => ({ ...oldState , ...newState }));
}
setSocket(sock);
return () => {
sock.close();
setSocket(undefined);
};
}, [])
return (state === undefined ?
<span>Loading...</span> :
<div>
<div className="heading">
<span>Mode: {state.mode}</span>
<div>
<span>{secondsRemaining}s </span>
</div>
</div>
<div>
Current state: <pre>
{JSON.stringify(state)}
</pre>
</div>
</div>
);
}
export default TVMode;

View File

@ -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(
<React.StrictMode>
<App />
{tvUi ?
<TVMode /> :
<Client />}
</React.StrictMode>
);

25
frontend/src/types.ts Normal file
View File

@ -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);