diff --git a/client/src/Actions.css b/client/src/Actions.css
new file mode 100644
index 0000000..b4014da
--- /dev/null
+++ b/client/src/Actions.css
@@ -0,0 +1,28 @@
+.Actions {
+ display: flex;
+ flex-wrap: wrap;
+ flex-shrink: 1;
+ align-items: center;
+ flex-direction: column;
+ justify-content: space-evenly;
+ background-color: rgba(16, 16, 16, 0.25);
+ margin: 0.25rem 0.25rem 0.25rem 0;
+ padding: 0.25em;
+}
+
+.Actions > div {
+ display: flex;
+ flex-direction: row;
+}
+
+.Actions .PlayerName {
+ flex-grow: 1;
+ align-self: stretch;
+}
+
+.Actions button {
+ margin: 0.25em;
+ background-color: white;
+ border: 1px solid black !important;
+}
+
diff --git a/client/src/Actions.js b/client/src/Actions.js
new file mode 100644
index 0000000..3a963c9
--- /dev/null
+++ b/client/src/Actions.js
@@ -0,0 +1,169 @@
+import React, { useState, useEffect, useContext, useRef, useMemo } from "react";
+import "./Actions.css";
+import Paper from '@material-ui/core/Paper';
+import Button from '@material-ui/core/Button';
+import { PlayerName } from './PlayerName.js';
+
+import { GlobalContext } from "./GlobalContext.js";
+
+const Actions = () => {
+ const { ws, gameId, name } = useContext(GlobalContext);
+ const [state, setState] = useState('lobby');
+ const [color, setColor] = useState(undefined);
+ const [player, setPlayer] = useState(undefined);
+ const [turn, setTurn] = useState({});
+ const [active, setActive] = useState(0);
+ const [edit, setEdit] = useState(name);
+
+ const fields = useMemo(() => [
+ 'state', 'turn', 'player', 'active', 'color'
+ ], []);
+
+ const onWsMessage = (event) => {
+ const data = JSON.parse(event.data);
+ switch (data.type) {
+ case 'game-update':
+ if ('state' in data.update && data.update.state !== state) {
+ setState(data.update.state);
+ }
+ if ('color' in data.update && data.update.color !== color) {
+ setColor(data.update.color);
+ }
+ if ('name' in data.update && data.update.name !== edit) {
+ setEdit(data.update.name);
+ }
+ if ('player' in data.update) {
+ setPlayer(data.update.player);
+ }
+ if ('turn' in data.update) {
+ setTurn(data.update.turn);
+ }
+ if ('active' in data.update && data.update.active !== active) {
+ setActive(data.update.active);
+ }
+ break;
+ default:
+ break;
+ }
+ };
+ const refWsMessage = useRef(onWsMessage);
+
+ useEffect(() => { refWsMessage.current = onWsMessage; });
+
+ useEffect(() => {
+ if (!ws) {
+ return;
+ }
+ const cbMessage = e => refWsMessage.current(e);
+ ws.addEventListener('message', cbMessage);
+ return () => {
+ ws.removeEventListener('message', cbMessage);
+ }
+ }, [ws, refWsMessage]);
+
+ useEffect(() => {
+ if (!ws) {
+ return;
+ }
+ ws.send(JSON.stringify({
+ type: 'get',
+ fields
+ }));
+ }, [ws, fields]);
+
+ const sendMessage = (data) => {
+ ws.send(JSON.stringify(data));
+ }
+ const buildClicked = (event) => {
+ alert(`This is a no-op currently; you can click the Build cost card.`);
+ };
+
+ const setName = (update) => {
+ if (!update) {
+ setEdit(name);
+ } else if (update !== name) {
+ sendMessage({ type: 'player-name', name: update });
+ }
+ }
+
+ const changeNameClick = (event) => {
+ setEdit("");
+ }
+
+ const discardClick = (event) => {
+ const nodes = document.querySelectorAll('.Hand .Resource.Selected'),
+ discarding = { wheat: 0, brick: 0, sheep: 0, stone: 0, wood: 0 };
+
+ for (let i = 0; i < nodes.length; i++) {
+ discarding[nodes[i].getAttribute("data-type")]++;
+ nodes[i].classList.remove('Selected');
+ }
+
+ sendMessage({ type: 'discard', discarding });
+ }
+
+ const newTableClick = (event) => {
+ sendMessage({ type: 'shuffle' });
+ };
+
+ const tradeClick = (event) => {
+ sendMessage({ type: 'trade' });
+ }
+
+ const rollClick = (event) => {
+ sendMessage({ type: 'roll' });
+ }
+
+ const passClick = (event) => {
+ sendMessage({ type: 'pass' });
+ }
+
+ const startClick = (event) => {
+ sendMessage({
+ type: 'set',
+ field: 'state',
+ value: 'game-order'
+ });
+ };
+
+ if (!gameId) {
+ return (