From 451e6a2e8e35cc619210e430a1d8d2e7e387d7c0 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Sun, 7 Jun 2020 10:23:02 -0700 Subject: [PATCH 01/16] Drop unused scribe/game.html The server actually has a template file that serves this prupose. So this static HTML file is entirely unused, (perhaps obviously so since it has "Tic-Tac-Toe" in its title. --- scribe/game.html | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 scribe/game.html diff --git a/scribe/game.html b/scribe/game.html deleted file mode 100644 index b9136b7..0000000 --- a/scribe/game.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - Tic-tac-toe - - - - - - - - - - - -
- -

Tic Tac Toe

- -

- Just the classic game. -

- -
-
- -
- -
- - - -- 2.43.0 From 53c8c6ba09301563293750e1010ca1ac8dd05590 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Sun, 7 Jun 2020 11:56:35 -0700 Subject: [PATCH 02/16] Initial framework for an Empathy game This starts with the generic code, (grabbed from Scribe), for displaying the game INFO, (with a button for copying the link), and the list of players that are in the game. It also provides a form for submitting a category, but that is still inert for now. --- empathy/.gitignore | 1 + empathy/Makefile | 12 +++ empathy/empathy.css | 1 + empathy/empathy.jsx | 217 ++++++++++++++++++++++++++++++++++++++++++++ empathy/index.html | 36 ++++++++ 5 files changed, 267 insertions(+) create mode 100644 empathy/.gitignore create mode 100644 empathy/Makefile create mode 100644 empathy/empathy.css create mode 100644 empathy/empathy.jsx create mode 100644 empathy/index.html diff --git a/empathy/.gitignore b/empathy/.gitignore new file mode 100644 index 0000000..053da87 --- /dev/null +++ b/empathy/.gitignore @@ -0,0 +1 @@ +empathy.js diff --git a/empathy/Makefile b/empathy/Makefile new file mode 100644 index 0000000..9f07401 --- /dev/null +++ b/empathy/Makefile @@ -0,0 +1,12 @@ +# Defer all targets up to the upper-level +# +# This requires two recipes. The first to cover the case of no +# explicit target specifed (so when invoked as "make" we call "make" +# at the upper-level) and then a .DEFAULT recipe to pass any explicit +# target up as well, (so that an invocation of "make foo" results in a +# call to "make foo" above. +all: + $(MAKE) -C .. + +.DEFAULT: + $(MAKE) -C .. $@ diff --git a/empathy/empathy.css b/empathy/empathy.css new file mode 100644 index 0000000..e8a317e --- /dev/null +++ b/empathy/empathy.css @@ -0,0 +1 @@ +/* Nothing to see here yet. */ diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx new file mode 100644 index 0000000..9ebe9f3 --- /dev/null +++ b/empathy/empathy.jsx @@ -0,0 +1,217 @@ +function undisplay(element) { + element.style.display="none"; +} + +function add_message(severity, message) { + message = `
+× +${message} +
`; + const message_area = document.getElementById('message-area'); + message_area.insertAdjacentHTML('beforeend', message); +} + +/********************************************************* + * Handling server-sent event stream * + *********************************************************/ + +const events = new EventSource("events"); + +events.onerror = function(event) { + if (event.target.readyState === EventSource.CLOSED) { + setTimeout(() => { + add_message("danger", "Connection to server lost."); + }, 1000); + } +}; + +events.addEventListener("game-info", event => { + const info = JSON.parse(event.data); + + window.game.set_game_info(info); +}); + +events.addEventListener("player-info", event => { + const info = JSON.parse(event.data); + + window.game.set_player_info(info); +}); + +events.addEventListener("player-enter", event => { + const info = JSON.parse(event.data); + + window.game.set_other_player_info(info); +}); + +events.addEventListener("player-update", event => { + const info = JSON.parse(event.data); + + if (info.id === window.game.state.player_info.id) + window.game.set_player_info(info); + else + window.game.set_other_player_info(info); +}); + +/********************************************************* + * Game and supporting classes * + *********************************************************/ + +function copy_to_clipboard(id) +{ + const tmp = document.createElement("input"); + tmp.setAttribute("value", document.getElementById(id).innerHTML); + document.body.appendChild(tmp); + tmp.select(); + document.execCommand("copy"); + document.body.removeChild(tmp); +} + +function GameInfo(props) { + if (! props.id) + return null; + + return ( +
+ {props.id} + {" "} + Share this link to invite friends:{" "} + {props.url} + {" "} + +
+ ); +} + +function PlayerInfo(props) { + if (! props.player.id) + return null; + + return ( +
+ Players: + {props.player.name} + {props.other_players.map(other => ( + + {", "} + {other.name} + + ))} +
+ ); +} + +function fetch_method_json(method, api = '', data = {}) { + const response = fetch(api, { + method: method, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + return response; +} + +function fetch_post_json(api = '', data = {}) { + return fetch_method_json('POST', api, data); +} + +async function fetch_put_json(api = '', data = {}) { + return fetch_method_json('PUT', api, data); +} + +function CategoryRequest(props) { + return ( +
+

Submit a Category

+

+ Suggest a category to play with your friends. Don't forget to + include the number of items for each person to submit. +

+ +
+
+ + +
+ +
+ +
+ +
+
+ ); +} + +class Game extends React.Component { + constructor(props) { + super(props); + this.state = { + game_info: {}, + player_info: {}, + other_players: [], + }; + } + + set_game_info(info) { + this.setState({ + game_info: info + }); + } + + set_player_info(info) { + this.setState({ + player_info: info + }); + } + + set_other_player_info(info) { + const other_players_copy = [...this.state.other_players]; + const idx = other_players_copy.findIndex(o => o.id === info.id); + if (idx >= 0) { + other_players_copy[idx] = info; + } else { + other_players_copy.push(info); + } + this.setState({ + other_players: other_players_copy + }); + } + + render() { + const state = this.state; + + return [ + , + , +

, + + ]; + } +} + +ReactDOM.render( window.game = me} + />, document.getElementById("empathy")); diff --git a/empathy/index.html b/empathy/index.html new file mode 100644 index 0000000..d250dc8 --- /dev/null +++ b/empathy/index.html @@ -0,0 +1,36 @@ + + + + + + + The Game of Empires + + + + + + + + +
+ +

The Game of Empathy

+ +

+ You don't need to be right, you just need to agree with your + friends. +

+ +
+
+ +
+ +
+ +
+ + -- 2.43.0 From 68a66356ecb9d184e44b8d75e1f7db25926d4af3 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Sun, 7 Jun 2020 15:14:10 -0700 Subject: [PATCH 03/16] Convert CategoryRequest from a function to a class This seems like it will be more convenient for defining additional methods to get access to from the JSX code (such as a submit handler function). --- empathy/empathy.jsx | 62 +++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx index 9ebe9f3..cd80629 100644 --- a/empathy/empathy.jsx +++ b/empathy/empathy.jsx @@ -122,36 +122,42 @@ async function fetch_put_json(api = '', data = {}) { return fetch_method_json('PUT', api, data); } -function CategoryRequest(props) { - return ( -
-

Submit a Category

-

+class CategoryRequest extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( +

+

Submit a Category

+

Suggest a category to play with your friends. Don't forget to include the number of items for each person to submit. -

- -
-
- - -
- -
- -
- -
-
- ); +

+ +
+
+ + +
+ +
+ +
+ +
+
+ ); + } } class Game extends React.Component { -- 2.43.0 From a1ed0fc881f3bfe6cc55041fda3b54f7c0baa94b Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Sun, 7 Jun 2020 15:22:28 -0700 Subject: [PATCH 04/16] Add a submit handler function for our form This is still a stub that doesn't actually _do_ aything yet. Note that we're using the "uncontrolled components" React pattern for the input field here as described here: https://reactjs.org/docs/uncontrolled-components.html The alternative, forcing a new call to React's setState on every keypress seems really silly to me, (I don't care to have React getting involved on every keypress). --- empathy/empathy.jsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx index cd80629..84a35a5 100644 --- a/empathy/empathy.jsx +++ b/empathy/empathy.jsx @@ -125,6 +125,19 @@ async function fetch_put_json(api = '', data = {}) { class CategoryRequest extends React.Component { constructor(props) { super(props); + this.category = React.createRef(); + + this.handle_submit = this.handle_submit.bind(this); + } + + handle_submit(event) { + const category_input = this.category.current; + const category = category_input.value; + + /* Prevent the default page-changing form-submission behavior. */ + event.preventDefault(); + + console.log("Do something here with category: " + category); } render() { @@ -136,7 +149,7 @@ class CategoryRequest extends React.Component { include the number of items for each person to submit.

-
+
- + ref={this.category} + />
-- 2.43.0 From b0b2d2d5dd5c3bac29537e2f5b206208f0a43898 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Sun, 7 Jun 2020 15:47:36 -0700 Subject: [PATCH 05/16] Take control over the wording of the validation message for category The "pattern" attribute on the text field was really handy for triggering HTML5 validation that the input matched a regular expression. The only real downside is that it gives a truly generic error message: Please match the requested format. That doesn't actually tell the user waht to fix, (in this case, to add a number). I really wish there existed another attribute to simply provide the error message that should be provided. If there is, I couldn't find one. I did have the title set here, which is close, but with two problems: 1. The resulting error message isn't _just_ my text, it is: Please match the requested format: so I don't quite get as much control as I want. 2. The title text shows up immediately when hovering over the input field, (even before the user has ever typed anything). But this is an error message that I only want the user to see if they've committed an error. It took me quite a lot of struggling to come up with a solution for this minor issue that works the way I want. Here it is: 1. I remove the pattern attribute, (so I don't get any default validation of the input format). This is essential or else the "Please match the requested format" text will appear at least somewhere. 2. In the subit handler, I validate the input against my regular expression and call setCustomValidity on the input field to set the text I want to appear. 3. But _also_ immediately after doing this I _also_ call reportVisibility on the form element. Otherwise, the input field will still get styled as invalid as I want, but the error message won't actually get reported, (unless the user tries submitting _again_ while the input is still invalid). 4. Next I also have to arrange to _clear_ this invalid state. So for this I add a new onChange handler (so it will be called for every keystroke). For this, a lot of tutorials just call setCustomValidity with an empty string here unconditionally. The downside of that is that the field will get styled as valid as soon as the user makes any change. Instead, I check the regular expression here so that the field is only styled as valid once the mistake is corrected. (This is consistent with the behavior of the default HTML5 validation with the "pattern" attribute.) So in the end, this gives the behavior that I want. It's a little wordy, especially here in my explanation!, but also in the code: Particularly that two different handlers are required: One to set the error state and one to clear it. As I implemented things here the regular expression is even duplicated in those two cases, (but that's a defect that could be addressed---the pattern could be stashed in a common place if I cared to do it). Note that it would be possible to set the error state in an 'else' clause within my onChange handler, (and that would eliminate the duplication of the regular expression pattern). The reason I don't do that here is that it would cause the field to be styled with the angry-red "invalid" styling as soon as the user started typing, rather than waiting for form submission before validation happens. So that could be annoying to users (it would drive me crazy) and it would also be inconsistent with how HTML5 validation happens with the "pattern" attribute. --- empathy/empathy.jsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx index 84a35a5..1ee8000 100644 --- a/empathy/empathy.jsx +++ b/empathy/empathy.jsx @@ -127,9 +127,18 @@ class CategoryRequest extends React.Component { super(props); this.category = React.createRef(); + this.handle_change = this.handle_change.bind(this); this.handle_submit = this.handle_submit.bind(this); } + handle_change(event) { + const category_input = this.category.current; + const category = category_input.value; + + if (/[0-9]/.test(category)) + category_input.setCustomValidity(""); + } + handle_submit(event) { const category_input = this.category.current; const category = category_input.value; @@ -137,6 +146,12 @@ class CategoryRequest extends React.Component { /* Prevent the default page-changing form-submission behavior. */ event.preventDefault(); + if (! /[0-9]/.test(category)) { + category_input.setCustomValidity("Category must include a number"); + event.currentTarget.reportValidity(); + return; + } + console.log("Do something here with category: " + category); } @@ -155,8 +170,8 @@ class CategoryRequest extends React.Component { type="text" id="category" placeholder="6 things at the beach" - required pattern=".*[0-9]+.*" - title="Category must contain a number" + required + onChange={this.handle_change} ref={this.category} />
-- 2.43.0 From 9be7b5eabaa074f23c410861dd8b99d61711a6f6 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Mon, 8 Jun 2020 06:48:56 -0700 Subject: [PATCH 06/16] style: Apply "box-sizing: border-box" globally This is really the only sane way to work. Without this, the behavior of padding is really hard to use. For example, if I set an element to "width: 100%" it fills the space like I want. But then, its child elements might be right up against the left edge, so I had "padding-left" of some amount. This moves the children from the edge like I want, but _also_ makes the element grow so that its now larger than the 100% of its container like I had specified. That looks to me entirely like a broken default and is something to just set once, globally to avoid this class of frustration. --- style.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/style.css b/style.css index baf35bf..b770986 100644 --- a/style.css +++ b/style.css @@ -1,3 +1,8 @@ +/* Because this is the only sizing that is sane, make it global. */ +* { + box-sizing: border-box; +} + /*\ |*| |*| Properties for the page: colors, etc. @@ -124,7 +129,6 @@ body { * of the page content. */ #page { - box-sizing: border-box; max-width: var(--page-max-width-padded); margin-left: auto; margin-right: auto; -- 2.43.0 From bcc27fd33ea930e8f22d9d8689b7c95cf04b3bd7 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Mon, 8 Jun 2020 06:51:56 -0700 Subject: [PATCH 07/16] style: Add a new variable: --text-fg-on-accent This removes a hard-coded "white" from the styling of the button. Instead, the theme designer can now choose an appropriate color for text that is displayed against a background of --accent-color. --- style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/style.css b/style.css index b770986..877ef59 100644 --- a/style.css +++ b/style.css @@ -22,6 +22,7 @@ /* A little color to avoid a fully monochromatic theme. */ --accent-color: #287789; --accent-color-bright: #44c7ef; + --text-fg-on-accent: white; /* Some colors intended to convey semnatics. */ --warning-color: #ffa92a; @@ -261,7 +262,7 @@ button { border-radius: 4px; background-color: var(--accent-color); border: none; - color: white; + color: var(--text-fg-on-accent); text-align: center; font-size: 125%; margin-top: .25em; -- 2.43.0 From fcdecf554d0bbad80b96b3c34fb5ba8cefef2144 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Mon, 8 Jun 2020 06:54:16 -0700 Subject: [PATCH 08/16] Empathy: Send prompt suggestions to server and display received prompts The received prompts can come in an initial list as part of the "game-state" event sent when first connecting, and they can also come in subsequent "prompt" events when a player submits a category suggestion. These are all displayed as large buttons. Clicking a button also sends a vote to the server, but received votes are not yet displayed in any way. --- empathy/empathy.css | 11 +++++- empathy/empathy.jsx | 81 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/empathy/empathy.css b/empathy/empathy.css index e8a317e..f3a4c4b 100644 --- a/empathy/empathy.css +++ b/empathy/empathy.css @@ -1 +1,10 @@ -/* Nothing to see here yet. */ +.vote-button { + width: 100%; + background-color: var(--accent-color); + color: var(--text-fg-on-accent); + text-align: left; + border-radius: 25px; + font-size: 200%; + padding: 1em; + margin-top: 0.25em; +} diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx index 1ee8000..1867381 100644 --- a/empathy/empathy.jsx +++ b/empathy/empathy.jsx @@ -52,6 +52,20 @@ events.addEventListener("player-update", event => { window.game.set_other_player_info(info); }); +events.addEventListener("game-state", event => { + const state = JSON.parse(event.data); + + for (let prompt of state.prompts) { + window.game.add_or_update_prompt(prompt); + } +}); + +events.addEventListener("prompt", event => { + const prompt = JSON.parse(event.data); + + window.game.add_or_update_prompt(prompt); +}); + /********************************************************* * Game and supporting classes * *********************************************************/ @@ -140,19 +154,26 @@ class CategoryRequest extends React.Component { } handle_submit(event) { + const form = event.currentTarget; const category_input = this.category.current; const category = category_input.value; /* Prevent the default page-changing form-submission behavior. */ event.preventDefault(); - if (! /[0-9]/.test(category)) { + const match = category.match(/[0-9]+/); + if (match === null) { category_input.setCustomValidity("Category must include a number"); - event.currentTarget.reportValidity(); + form.reportValidity(); return; } - console.log("Do something here with category: " + category); + fetch_post_json("prompts", { + items: parseInt(match[0], 10), + prompt: category + }); + + form.reset(); } render() { @@ -160,8 +181,8 @@ class CategoryRequest extends React.Component {

Submit a Category

- Suggest a category to play with your friends. Don't forget to - include the number of items for each person to submit. + Suggest a category to play. Don't forget to include the + number of items for each person to submit.

@@ -171,6 +192,7 @@ class CategoryRequest extends React.Component { id="category" placeholder="6 things at the beach" required + autoComplete="off" onChange={this.handle_change} ref={this.category} /> @@ -188,6 +210,37 @@ class CategoryRequest extends React.Component { } } +function PromptOptions(props) { + + function handle_click(id) { + fetch_post_json(`vote/${id}`); + } + + if (props.prompts.length === 0) + return null; + + return ( +
+

Vote on Categories

+

+ Select any categories below that you'd like to play. + You can choose as many as you'd like. +

+ {props.prompts.map(p => { + return ( + + ); + })} +
+ ); +} + class Game extends React.Component { constructor(props) { super(props); @@ -195,6 +248,7 @@ class Game extends React.Component { game_info: {}, player_info: {}, other_players: [], + prompts: [] }; } @@ -223,6 +277,19 @@ class Game extends React.Component { }); } + add_or_update_prompt(prompt) { + const prompts_copy = [...this.state.prompts]; + const idx = prompts_copy.findIndex(p => p.id === prompt.id); + if (idx >= 0) { + prompts_copy[idx] = prompt; + } else { + prompts_copy.push(prompt); + } + this.setState({ + prompts: prompts_copy + }); + } + render() { const state = this.state; @@ -241,6 +308,10 @@ class Game extends React.Component {

, , + ]; } -- 2.43.0 From c4b6ba0b36f47f9985614bc3842273fec1746f16 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Mon, 8 Jun 2020 09:11:03 -0700 Subject: [PATCH 09/16] Add a new color --text-fg-on-accent-bright For allowing the designer to select what color text should be when on a background of --accent-color-bright. Note: The value for this color happens to match the color of previously-defined values currently, but that's just me throwing programmer-art colors in place. That's why we don't reference an existing variable in the value here, even though the color is currently identical to a previous one, (we wouldn't want a designer to think they are necessarily linked). --- style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/style.css b/style.css index 877ef59..240014a 100644 --- a/style.css +++ b/style.css @@ -23,6 +23,7 @@ --accent-color: #287789; --accent-color-bright: #44c7ef; --text-fg-on-accent: white; + --text-fg-on-accent-bright: #333738; /* Some colors intended to convey semnatics. */ --warning-color: #ffa92a; -- 2.43.0 From 7f2adf5117f25ce666ec4fa8d628cd3123b32aa6 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Mon, 8 Jun 2020 09:13:43 -0700 Subject: [PATCH 10/16] Restrict :hover styling to when a media query says hover is fully supported Otherwise, on my phone I was seeing "sticky" hover effects, (after I clicked a button it stayed looking like it was still being pressed after I let go which was very confusing). --- style.css | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/style.css b/style.css index 240014a..b4d5b1b 100644 --- a/style.css +++ b/style.css @@ -82,8 +82,10 @@ a:link { a:visited { color: var(--accent-color); } -a:hover { - color: var(--accent-color-bright); +@media (hover:hover) { + a:hover { + color: var(--accent-color-bright); + } } a:active { color: var(--accent-color-bright); @@ -279,9 +281,11 @@ button.inline { width: auto; } -button:hover { - transform: translateY(-1px); - background-color: var(--accent-color-bright); +@media (hover:hover) { + button:hover { + transform: translateY(-1px); + background-color: var(--accent-color-bright); + } } :focus { @@ -332,8 +336,10 @@ button:hover { top: 0; } -.hide-button:hover { - color: var(--danger-color-dark); +@media (hover:hover) { + .hide-button:hover { + color: var(--danger-color-dark); + } } /*\ -- 2.43.0 From dee335da95357a8993a22aab01809d5bbd3e9b57 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Mon, 8 Jun 2020 09:15:00 -0700 Subject: [PATCH 11/16] Add display of votes on top of each prompt's vote button This is a simple bit of feedback so players can see which options are being voted on by other players. I won't claim that there's anything inspired about the stlying or color selection here. Maybe Karen will have some suggestions to improve this later. --- empathy/empathy.css | 21 +++++++++++++++++++++ empathy/empathy.jsx | 12 ++++++++++++ 2 files changed, 33 insertions(+) diff --git a/empathy/empathy.css b/empathy/empathy.css index f3a4c4b..d553597 100644 --- a/empathy/empathy.css +++ b/empathy/empathy.css @@ -8,3 +8,24 @@ padding: 1em; margin-top: 0.25em; } + +.vote-choices { + display: flex; +} + +.vote-choice { + font-size: 40%; + background-color: var(--accent-color-bright); + color: var(--text-fg-on-accent-bright); + border-radius: 4px; + padding-left: 4px; + padding-right: 4px; + margin-right: 0.5em; +} + +@media (hover:hover) { + button:hover .vote-choice { + background-color: var(--accent-color); + color: var(--text-fg-on-accent); + } +} diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx index 1867381..b69ce6f 100644 --- a/empathy/empathy.jsx +++ b/empathy/empathy.jsx @@ -234,6 +234,18 @@ function PromptOptions(props) { onClick={() => handle_click(p.id)} > {p.prompt} +
+ {p.votes.map(v => { + return ( +
+ {v} +
+ ); + })} +
); })} -- 2.43.0 From b28c923a50b2fc9001fdff69e18506ea02470fe5 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Mon, 8 Jun 2020 19:10:52 -0700 Subject: [PATCH 12/16] Empathy: Let React now I'm a good boy and I won't mutate state By using React.memo() and PureComponent, React can avoid doing any re-rendering of components that haven't changed at all. --- empathy/empathy.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx index b69ce6f..959c3fe 100644 --- a/empathy/empathy.jsx +++ b/empathy/empathy.jsx @@ -80,7 +80,7 @@ function copy_to_clipboard(id) document.body.removeChild(tmp); } -function GameInfo(props) { +const GameInfo = React.memo(props => { if (! props.id) return null; @@ -97,9 +97,9 @@ function GameInfo(props) { >Copy Link
); -} +}); -function PlayerInfo(props) { +const PlayerInfo = React.memo(props => { if (! props.player.id) return null; @@ -115,7 +115,7 @@ function PlayerInfo(props) { ))} ); -} +}); function fetch_method_json(method, api = '', data = {}) { const response = fetch(api, { @@ -136,7 +136,7 @@ async function fetch_put_json(api = '', data = {}) { return fetch_method_json('PUT', api, data); } -class CategoryRequest extends React.Component { +class CategoryRequest extends React.PureComponent { constructor(props) { super(props); this.category = React.createRef(); @@ -210,7 +210,7 @@ class CategoryRequest extends React.Component { } } -function PromptOptions(props) { +const PromptOptions = React.memo(props => { function handle_click(id) { fetch_post_json(`vote/${id}`); @@ -251,9 +251,9 @@ function PromptOptions(props) { })} ); -} +}); -class Game extends React.Component { +class Game extends React.PureComponent { constructor(props) { super(props); this.state = { -- 2.43.0 From 67bface07504b1e5809c860f81c2cdc1d3a720a3 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Mon, 8 Jun 2020 21:34:00 -0700 Subject: [PATCH 13/16] Drop an unneeded function It was only one line, and only called once, so better just to inline it. --- empathy/empathy.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx index 959c3fe..110b330 100644 --- a/empathy/empathy.jsx +++ b/empathy/empathy.jsx @@ -212,10 +212,6 @@ class CategoryRequest extends React.PureComponent { const PromptOptions = React.memo(props => { - function handle_click(id) { - fetch_post_json(`vote/${id}`); - } - if (props.prompts.length === 0) return null; @@ -231,7 +227,7 @@ const PromptOptions = React.memo(props => { + + ); +}); + +const ActivePrompt = React.memo(props => { + + function handle_submit(event) { + + /* Prevent the default page-changing form-submission behavior. */ + event.preventDefault(); + } + + return ( +
+

The Game of Empathy

+

+ Remember, you're trying to match your answers with + what the other players submit. + Give {props.prompt.items} responses for the following prompt: +

+

{props.prompt.prompt}

+ + {Array(props.prompt.items).fill(null).map((whocares,i) => { + return ( +
+ +
+ ); + })} + +
+ +
+ + +
+ ); +}); + class Game extends React.PureComponent { constructor(props) { super(props); @@ -298,9 +386,21 @@ class Game extends React.PureComponent { }); } + set_active_prompt(prompt) { + this.setState({ + active_prompt: prompt + }); + } + render() { const state = this.state; + if (state.active_prompt) { + return ; + } + return [ , + ]; } diff --git a/style.css b/style.css index b4d5b1b..c8d6c86 100644 --- a/style.css +++ b/style.css @@ -51,6 +51,7 @@ body { h1,h2 { color: var(--text-fg-color); font-weight: bold; + margin-top: 0.5em; } h1 { @@ -215,6 +216,11 @@ form { grid-column-gap: 1em; } +.form-field { + margin-top: 0.25em; + margin-bottom: 0.25em; +} + .form-field.small.left,.form-field.medium.left { grid-column-start: 1; } -- 2.43.0 From ff43730d806faf03b488ee59c49d576686696d6c Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Tue, 9 Jun 2020 19:46:28 -0700 Subject: [PATCH 15/16] Empathy: Add support for submitting answers to a prompy For this we convert ActivePrompt from a function to a class-based React component. Then we use the "uncontrolled components" technique again to arrange for an array to hold the form values. Finally, when answers are submitted successfully we display a simple, static message. Eventually, we'll want to display some dynamic updates as other players vote, (but for that, we'll also need the server to give us that information). --- empathy/empathy.jsx | 120 ++++++++++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 37 deletions(-) diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx index 0771bbe..3946189 100644 --- a/empathy/empathy.jsx +++ b/empathy/empathy.jsx @@ -291,51 +291,97 @@ const LetsPlay = React.memo(props => { ); }); -const ActivePrompt = React.memo(props => { +class ActivePrompt extends React.PureComponent { - function handle_submit(event) { + constructor(props) { + super(props); + const items = props.prompt.items; + + this.state = { + submitted: false + }; + + this.answers = [...Array(items)].map(() => React.createRef()); + this.handle_submit = this.handle_submit.bind(this); + } + + async handle_submit(event) { + const form = event.currentTarget; /* Prevent the default page-changing form-submission behavior. */ event.preventDefault(); + + const response = await fetch_post_json(`answer/${this.props.prompt.id}`, { + answers: this.answers.map(r => r.current.value) + }); + if (response.status == 200) { + const result = await response.json(); + if (! result.valid) { + add_message("danger", result.message); + return; + } + } else { + add_message("danger", "An error occurred submitting your answers"); + return; + } + + /* Everything worked. Server is happy with our answers. */ + form.reset(); + this.setState({ + submitted: true + }); } - return ( -
-

The Game of Empathy

-

- Remember, you're trying to match your answers with - what the other players submit. - Give {props.prompt.items} responses for the following prompt: -

-

{props.prompt.prompt}

-
- {Array(props.prompt.items).fill(null).map((whocares,i) => { - return ( -
- -
- ); - })} - -
- + render() { + if (this.state.submitted) + return ( +
+

Answers submitted

+

+ Please wait for the rest of the players to submit their answers. +

+ ); - -
- ); -}); + return ( +
+

The Game of Empathy

+

+ Remember, you're trying to match your answers with + what the other players submit. + Give {this.props.prompt.items} answers for the following prompt: +

+

{this.props.prompt.prompt}

+
+ {Array(this.props.prompt.items).fill(null).map((whocares,i) => { + return ( +
+ +
+ ); + })} + +
+ +
+ +
+
+ ); + } +} class Game extends React.PureComponent { constructor(props) { -- 2.43.0 From a3d68ced6e2f65187e4fc076a488715a777b8a62 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Tue, 9 Jun 2020 20:03:02 -0700 Subject: [PATCH 16/16] Display the number of players who have already submitted answers With a message that could look like this: 2/5 players have responded And of course, that dynamically updates. This lets players at least know that something is happening, or whether they are still waiting for somebody. --- empathy/empathy.jsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx index 3946189..2aec434 100644 --- a/empathy/empathy.jsx +++ b/empathy/empathy.jsx @@ -74,6 +74,12 @@ events.addEventListener("start", event => { window.game.set_active_prompt(prompt); }); +events.addEventListener("answered", event => { + const players_answered = JSON.parse(event.data); + + window.game.set_players_answered(players_answered); +}); + /********************************************************* * Game and supporting classes * *********************************************************/ @@ -336,7 +342,8 @@ class ActivePrompt extends React.PureComponent { if (this.state.submitted) return (
-

Answers submitted

+

{this.props.players_answered}/ + {this.props.players_total} players have responded

Please wait for the rest of the players to submit their answers.

@@ -390,7 +397,9 @@ class Game extends React.PureComponent { game_info: {}, player_info: {}, other_players: [], - prompts: [] + prompts: [], + active_prompt: null, + players_answered: 0 }; } @@ -438,12 +447,21 @@ class Game extends React.PureComponent { }); } + set_players_answered(players_answered) { + this.setState({ + players_answered: players_answered + }); + } + render() { const state = this.state; + const players_total = 1 + state.other_players.length; if (state.active_prompt) { return ; } -- 2.43.0