Improve frontend
This commit is contained in:
parent
570ffd4dff
commit
3d51368825
@ -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"
|
||||
|
@ -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 />);
|
||||
|
@ -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;
|
@ -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
66
frontend/src/Client.tsx
Normal 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
16
frontend/src/TVMode.css
Normal 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
58
frontend/src/TVMode.tsx
Normal 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;
|
@ -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
25
frontend/src/types.ts
Normal 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);
|
Loading…
Reference in New Issue
Block a user