OSDN Git Service

e6aa25767761f0f477bf0bea5b20240237ba99c6
[hengband/web.git] / score / src / popurality_ranking.tsx
1 import React from "react";
2 import ReactDOM from "react-dom";
3
4 const name2j: { [key: string]: string } = {
5     class: "職業",
6     personality: "性格",
7     race: "種族",
8     realm1: "魔法領域1",
9     realm2: "魔法領域2",
10 };
11
12 const column2j: { [col: string]: string } = {
13     average_score: "平均スコア",
14     female_count: "女性",
15     male_count: "男性",
16     max_score: "最大スコア",
17     total_count: "計",
18     winner_count: "勝利",
19 };
20
21 const columnOrder = [
22     "total_count",
23     "male_count",
24     "female_count",
25     "winner_count",
26     "average_score",
27     "max_score",
28 ];
29
30 /**
31  * 表示テーブル選択ボタンコンポーネントプロパティ
32  */
33 interface ITableSelectButtonProps {
34     /** ボタンが選択状態かどうか */
35     selected: boolean;
36     /** ボタンの銘板 */
37     name: string;
38     /** ボタンがクリックされた時に呼び出されるコールバック関数 */
39     onClick: () => void;
40 }
41
42 /**
43  * 表示テーブル選択ボタンコンポーネント
44  */
45 // tslint:disable-next-line:max-classes-per-file
46 class TableSelectButton extends React.Component<ITableSelectButtonProps> {
47     public shouldComponentUpdate(nextProps: ITableSelectButtonProps) {
48         return this.props.selected !== nextProps.selected;
49     }
50
51     public render() {
52         if (this.props.selected) {
53             return (
54                 <b>
55                     {name2j[this.props.name]}
56                 </b>
57             );
58         } else {
59             return (
60                 <a href="javascript:void(0)" onClick={() => this.props.onClick()}>
61                     {name2j[this.props.name]}
62                 </a>
63             );
64         }
65     }
66 }
67
68 /**
69  * 表示テーブル選択コンポーネントプロパティ
70  */
71 interface ITableSelectProps {
72     /** 選択中のテーブル名称 */
73     selectedTableName: string;
74     /**
75      * 選択テーブル切替時に呼び出されるコールバック関数
76      * @param name 新たに選択されたテーブル名称
77      */
78     onSelectChange: (name: string) => void;
79 }
80
81 /**
82  * 表示テーブル選択コンポーネント
83  */
84 // tslint:disable-next-line:max-classes-per-file
85 class TableSelector extends React.Component<ITableSelectProps> {
86     public render() {
87         return (
88             <div>
89                 [ {this.renderSelectButton("race")} | {this.renderSelectButton("class")} |
90                  {this.renderSelectButton("personality")} ]
91                 [ {this.renderSelectButton("realm1")} | {this.renderSelectButton("realm2")} ]
92             </div>
93         );
94     }
95
96     private renderSelectButton(name: string) {
97         return <TableSelectButton
98             selected={this.props.selectedTableName === name}
99             name={name}
100             onClick={() => this.props.onSelectChange(name)} />;
101     }
102 }
103
104 /**
105  * テーブルヘッダコンポーネントプロパティ
106  */
107 interface ITableHeaderProps {
108     /** テーブル名称 */
109     tableName: string;
110     /** ソートキーカラム */
111     sortKeyColumn: string;
112     /** ソート順 */
113     sortOrder: SortOrder;
114     /**
115      * ヘッダをクリックした時に呼ばれるコールバック関数
116      * @param clickColumn クリックしたカラムの名称
117      */
118     onClick: (clickColumn: string) => void;
119 }
120
121 /**
122  * テーブルヘッダコンポーネント
123  */
124 function TableHeader(props: ITableHeaderProps) {
125     return (
126         <thead>
127             <tr>
128                 <th>{props.tableName}</th>
129                 {columnOrder.map((col) => {
130                     let className = "sort";
131                     if (props.sortKeyColumn === col) {
132                         className += " ";
133                         className += (props.sortOrder === SortOrder.Ascend) ? "ascend" : "descend";
134                     }
135                     return (
136                         <th className={className} onClick={() => props.onClick(col)}>
137                             {column2j[col]}
138                         </th>
139                     );
140                 })}
141             </tr>
142         </thead>
143     );
144 }
145
146 /**
147  * テーブル行データコンポーネントプロパティ
148  */
149 interface ITableDataRowProps {
150     /** スコアランキングのURLのリンクに渡すパラメータ */
151     linkParam: string;
152     /** 行データの名称(第1カラムに表示される文字列) */
153     rowName: string;
154     /** 行データ */
155     row_data: { [key: string]: string | number };
156 }
157
158 /**
159  * テーブル行データコンポーネント
160  */
161 function TableDataRow(props: ITableDataRowProps) {
162     return (
163         <tr key={props.row_data.id}>
164             <td>
165                 <a href={`score_ranking.php?${props.linkParam}`}>
166                     {props.rowName}
167                 </a>
168             </td>
169             {columnOrder.map((col) =>
170                 <td className="number">{props.row_data[col]}</td>)}
171         </tr>
172     );
173 }
174
175 /**
176  * ソート順序列挙体
177  */
178 enum SortOrder {
179     /** 昇順 */
180     Ascend,
181     /** 降順 */
182     Descend,
183 }
184
185 /**
186  * 人気データテーブルコンポーネントプロパティ
187  */
188 interface IPopuralityTableProps {
189     /** データ内容 */
190     data: Array<{ [col: string]: any }>;
191     /** データ名称 */
192     name: string;
193     /** テーブルの表示/非表示 */
194     visible: boolean;
195 }
196
197 /**
198  * 人気データテーブルコンポーネントステータス
199  */
200 interface IPopuralityTableState {
201     /** データソートのキーとするカラムの名称 */
202     sortKeyColumn: string;
203     /** データソートの順序 */
204     sortOrder: SortOrder;
205 }
206
207 /**
208  * 人気データテーブルコンポーネント
209  */
210 // tslint:disable-next-line:max-classes-per-file
211 class PopuralityTable extends React.Component<IPopuralityTableProps, IPopuralityTableState> {
212     constructor(props: IPopuralityTableProps) {
213         super(props);
214         this.state = {
215             sortKeyColumn: columnOrder[0],
216             sortOrder: SortOrder.Descend,
217         };
218     }
219
220     public render() {
221         if (!this.props.visible) {
222             return "";
223         }
224
225         const data = this.get_sorted_data();
226         const tableName = this.props.name;
227         return (
228             <table className="score statistics_table one_row">
229                 <TableHeader
230                     tableName={name2j[tableName]}
231                     sortKeyColumn={this.state.sortKeyColumn}
232                     sortOrder={this.state.sortOrder}
233                     onClick={this.selectSortColumn.bind(this)}
234                 />
235                 <tbody>
236                     {data.map((row) =>
237                         <TableDataRow
238                             linkParam={`${tableName}_id=${row.id}`}
239                             rowName={row.name}
240                             row_data={row}
241                         />)}
242                 </tbody>
243             </table>
244         );
245     }
246
247     /**
248      * ソートされたデータを得る
249      * @return ソートされたデータ
250      */
251     protected get_sorted_data() {
252         const sortedData = this.props.data.slice();
253
254         const col = this.state.sortKeyColumn;
255         const order = this.state.sortOrder;
256
257         sortedData.sort((a, b) =>
258             (order === (SortOrder.Ascend) ?
259                 a[col] - b[col] : b[col] - a[col]));
260
261         return sortedData;
262     }
263
264     /**
265      * ソートするカラムを選択する
266      * @param column ソートするカラムの名称
267      */
268     protected selectSortColumn(column: string) {
269         let newSortOrder = SortOrder.Descend;
270         if (this.state.sortKeyColumn === column) {
271             newSortOrder =
272                 (this.state.sortOrder === SortOrder.Ascend) ?
273                     SortOrder.Descend : SortOrder.Ascend;
274         }
275         this.setState({
276             sortKeyColumn: column,
277             sortOrder: newSortOrder,
278         });
279     }
280 }
281
282 /**
283  * 人気データテーブルコンポーネント(魔法領域)
284  */
285 // tslint:disable-next-line:max-classes-per-file
286 class PopuralityRealmTable extends PopuralityTable {
287     public render() {
288         if (!this.props.visible) {
289             return "";
290         }
291
292         const data = this.get_sorted_data();
293         const realm = this.props.name;
294         return (
295             <table className="score statistics_table one_row">
296                 <TableHeader
297                     tableName={this.props.data[0].class_name}
298                     sortKeyColumn={this.state.sortKeyColumn}
299                     sortOrder={this.state.sortOrder}
300                     onClick={this.selectSortColumn.bind(this)}
301                 />
302                 <tbody>
303                     {data.map((row) =>
304                         <TableDataRow
305                             linkParam={`class_id=${row.class_id}&${realm}_id=${row.realm_id}`}
306                             rowName={row.realm_name}
307                             row_data={row}
308                         />)}
309                 </tbody>
310             </table>
311         );
312     }
313 }
314
315 /**
316  * 人気データテーブル全体表示コンポーネントプロパティ
317  */
318 interface IRankingTablesProps {
319     /** 表示選択中のテーブル名称 */
320     selectedTableName: string;
321     /** テーブルデータ */
322     data: { [col: string]: any[] };
323 }
324
325 /**
326  * 人気データテーブル全体表示コンポーネント
327  */
328 // tslint:disable-next-line:max-classes-per-file
329 class RankingTables extends React.Component<IRankingTablesProps> {
330     public shouldComponentUpdate(nextProps: IRankingTablesProps) {
331         return (this.props.data === null && nextProps.data !== null) ||
332             (this.props.selectedTableName !== nextProps.selectedTableName);
333     }
334
335     public render() {
336         const tables = Object.keys(name2j).map((name) => {
337             if (!name.startsWith("realm")) {
338                 return (
339                     <div id={name} key={name}>
340                         <PopuralityTable
341                             data={this.props.data[name]}
342                             visible={this.props.selectedTableName === name}
343                             name={name} />
344                     </div>
345                 );
346             } else {
347                 const realmTables = this.props.data[name].map((i) => {
348                     return <PopuralityRealmTable
349                         data={i}
350                         name={name}
351                         visible={this.props.selectedTableName === name}
352                         key={i[0].class_id} />;
353                 });
354                 return <div id={name} key={name}>{realmTables}</div>;
355             }
356         });
357         return <div>{tables}</div>;
358     }
359 }
360
361 /**
362  * 人気データ表示コンポーネントプロパティ
363  */
364 interface IPopuralityRankingProps {
365     /** 人気データ取得URL */
366     dataUrl: string;
367 }
368
369 /**
370  * 人気データ表示コンポーネントステータス
371  */
372 interface IPopuralityRankingState {
373     /** 表示選択中テーブル名称 */
374     selectedTableName: string;
375     /** データ内容 */
376     data: { [col: string]: any[] };
377     /** データ表示状態を示すメッセージ */
378     stateMessage: string;
379 }
380
381 /**
382  * 人気データ表示コンポーネント
383  */
384 // tslint:disable-next-line:max-classes-per-file
385 class PopuralityRanking extends React.Component<IPopuralityRankingProps, IPopuralityRankingState> {
386     constructor(props: IPopuralityRankingProps) {
387         super(props);
388         const selectedTableName = this.storageAvailable("sessionStorage") ?
389             sessionStorage.getItem("selectedTableName") : null;
390
391         this.state = {
392             data: {},
393             selectedTableName: selectedTableName !== null ? selectedTableName : "race",
394             stateMessage: "Loading...",
395         };
396     }
397
398     public componentDidMount() {
399         fetch(this.props.dataUrl).then((response) => {
400             if (!response.ok) {
401                 throw Error(`${response.status} ${response.statusText}`);
402             }
403             return response.json();
404         }).then((data) => {
405             data.realm1 = this.group_by_class(data.realm1);
406             data.realm2 = this.group_by_class(data.realm2);
407             this.setState({
408                 data,
409                 stateMessage: "Success",
410             });
411         }).catch((err) => {
412             this.setState({ stateMessage: err.message });
413         });
414     }
415
416     public componentDidUpdate() {
417         if (this.storageAvailable("sessionStorage")) {
418             sessionStorage.setItem("selectedTableName", this.state.selectedTableName);
419         }
420     }
421
422     public render() {
423         if (Object.keys(this.state.data).length === 0) {
424             return <div>{this.state.stateMessage}</div>;
425         }
426
427         return (
428             <div>
429                 <TableSelector
430                     onSelectChange={this.selectTable.bind(this)}
431                     selectedTableName={this.state.selectedTableName}
432                 />
433                 <RankingTables
434                     data={this.state.data}
435                     selectedTableName={this.state.selectedTableName}
436                 />
437             </div>
438         );
439     }
440
441     private storageAvailable(type: string) {
442         const storage = (window as any)[type];
443         try {
444             const x = "__storage_test__";
445             storage.setItem(x, x);
446             storage.removeItem(x);
447             return true;
448         } catch (e) {
449             return e instanceof DOMException && (
450                 // everything except Firefox
451                 e.code === 22 ||
452                 // Firefox
453                 e.code === 1014 ||
454                 // test name field too, because code might not be present
455                 // everything except Firefox
456                 e.name === "QuotaExceededError" ||
457                 // Firefox
458                 e.name === "NS_ERROR_DOM_QUOTA_REACHED") &&
459                 // acknowledge QuotaExceededError only if there's something already stored
460                 storage.length !== 0;
461         }
462     }
463
464     private selectTable(tableName: string) {
465         this.setState({ selectedTableName: tableName });
466     }
467
468     private group_by_class(data: any[]) {
469         const result: any[] = [];
470         data.forEach((i) => {
471             if (result[i.class_id] === undefined) {
472                 result[i.class_id] = [];
473             }
474             result[i.class_id].push(i);
475         });
476         return result.filter((e) => e.length > 1);
477     }
478 }
479
480 ReactDOM.render(
481     <PopuralityRanking dataUrl="get_popularity_ranking.php" />,
482     document.getElementById("content"),
483 );