Take control over the wording of the validation message for category
[lmno.games] / empathy / empathy.jsx
1 function undisplay(element) {
2   element.style.display="none";
3 }
4
5 function add_message(severity, message) {
6   message = `<div class="message ${severity}" onclick="undisplay(this)">
7 <span class="hide-button" onclick="undisplay(this.parentElement)">&times;</span>
8 ${message}
9 </div>`;
10   const message_area = document.getElementById('message-area');
11   message_area.insertAdjacentHTML('beforeend', message);
12 }
13
14 /*********************************************************
15  * Handling server-sent event stream                     *
16  *********************************************************/
17
18 const events = new EventSource("events");
19
20 events.onerror = function(event) {
21   if (event.target.readyState === EventSource.CLOSED) {
22     setTimeout(() => {
23       add_message("danger", "Connection to server lost.");
24     }, 1000);
25   }
26 };
27
28 events.addEventListener("game-info", event => {
29   const info = JSON.parse(event.data);
30
31   window.game.set_game_info(info);
32 });
33
34 events.addEventListener("player-info", event => {
35   const info = JSON.parse(event.data);
36
37   window.game.set_player_info(info);
38 });
39
40 events.addEventListener("player-enter", event => {
41   const info = JSON.parse(event.data);
42
43   window.game.set_other_player_info(info);
44 });
45
46 events.addEventListener("player-update", event => {
47   const info = JSON.parse(event.data);
48
49   if (info.id === window.game.state.player_info.id)
50     window.game.set_player_info(info);
51   else
52     window.game.set_other_player_info(info);
53 });
54
55 /*********************************************************
56  * Game and supporting classes                           *
57  *********************************************************/
58
59 function copy_to_clipboard(id)
60 {
61   const tmp = document.createElement("input");
62   tmp.setAttribute("value", document.getElementById(id).innerHTML);
63   document.body.appendChild(tmp);
64   tmp.select();
65   document.execCommand("copy");
66   document.body.removeChild(tmp);
67 }
68
69 function GameInfo(props) {
70   if (! props.id)
71     return null;
72
73   return (
74     <div className="game-info">
75       <span className="game-id">{props.id}</span>
76       {" "}
77       Share this link to invite friends:{" "}
78       <span id="game-share-url">{props.url}</span>
79       {" "}
80       <button
81         className="inline"
82         onClick={() => copy_to_clipboard('game-share-url')}
83       >Copy Link</button>
84     </div>
85   );
86 }
87
88 function PlayerInfo(props) {
89   if (! props.player.id)
90     return null;
91
92   return (
93     <div className="player-info">
94       <span className="players-header">Players: </span>
95       {props.player.name}
96       {props.other_players.map(other => (
97         <span key={other.id}>
98           {", "}
99           {other.name}
100         </span>
101       ))}
102     </div>
103   );
104 }
105
106 function fetch_method_json(method, api = '', data = {}) {
107   const response = fetch(api, {
108     method: method,
109     headers: {
110       'Content-Type': 'application/json'
111     },
112     body: JSON.stringify(data)
113   });
114   return response;
115 }
116
117 function fetch_post_json(api = '', data = {}) {
118   return fetch_method_json('POST', api, data);
119 }
120
121 async function fetch_put_json(api = '', data = {}) {
122   return fetch_method_json('PUT', api, data);
123 }
124
125 class CategoryRequest extends React.Component {
126   constructor(props) {
127     super(props);
128     this.category = React.createRef();
129
130     this.handle_change = this.handle_change.bind(this);
131     this.handle_submit = this.handle_submit.bind(this);
132   }
133
134   handle_change(event) {
135     const category_input = this.category.current;
136     const category = category_input.value;
137
138     if (/[0-9]/.test(category))
139       category_input.setCustomValidity("");
140   }
141
142   handle_submit(event) {
143     const category_input = this.category.current;
144     const category = category_input.value;
145
146     /* Prevent the default page-changing form-submission behavior. */
147     event.preventDefault();
148
149     if (! /[0-9]/.test(category)) {
150       category_input.setCustomValidity("Category must include a number");
151       event.currentTarget.reportValidity();
152       return;
153     }
154
155     console.log("Do something here with category: " + category);
156   }
157
158   render() {
159     return (
160       <div className="category-request">
161         <h2>Submit a Category</h2>
162         <p>
163           Suggest a category to play with your friends. Don't forget to
164           include the number of items for each person to submit.
165         </p>
166
167         <form onSubmit={this.handle_submit} >
168           <div className="form-field large">
169             <input
170               type="text"
171               id="category"
172               placeholder="6 things at the beach"
173               required
174               onChange={this.handle_change}
175               ref={this.category}
176             />
177           </div>
178
179           <div className="form-field large">
180             <button type="submit">
181               Send
182             </button>
183           </div>
184
185         </form>
186       </div>
187     );
188   }
189 }
190
191 class Game extends React.Component {
192   constructor(props) {
193     super(props);
194     this.state = {
195       game_info: {},
196       player_info: {},
197       other_players: [],
198     };
199   }
200
201   set_game_info(info) {
202     this.setState({
203       game_info: info
204     });
205   }
206
207   set_player_info(info) {
208     this.setState({
209       player_info: info
210     });
211   }
212
213   set_other_player_info(info) {
214     const other_players_copy = [...this.state.other_players];
215     const idx = other_players_copy.findIndex(o => o.id === info.id);
216     if (idx >= 0) {
217       other_players_copy[idx] = info;
218     } else {
219       other_players_copy.push(info);
220     }
221     this.setState({
222       other_players: other_players_copy
223     });
224   }
225
226   render() {
227     const state = this.state;
228
229     return [
230       <GameInfo
231         key="game-info"
232         id={state.game_info.id}
233         url={state.game_info.url}
234       />,
235       <PlayerInfo
236         key="player-info"
237         game={this}
238         player={state.player_info}
239         other_players={state.other_players}
240       />,
241       <p key="spacer"></p>,
242       <CategoryRequest
243         key="category-request"
244       />
245     ];
246   }
247 }
248
249 ReactDOM.render(<Game
250                   ref={(me) => window.game = me}
251                 />, document.getElementById("empathy"));