@mideind/netskrafl-react 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +8 -0
- package/.eslintrc.json +13 -0
- package/README.md +63 -0
- package/dist/cjs/index.css +6837 -0
- package/dist/cjs/index.js +3046 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.css +6837 -0
- package/dist/esm/index.js +3046 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/package.json +63 -0
- package/rollup.config.js +60 -0
- package/src/components/index.ts +2 -0
- package/src/components/netskrafl/Netskrafl.stories.tsx +66 -0
- package/src/components/netskrafl/Netskrafl.tsx +135 -0
- package/src/components/netskrafl/Netskrafl.types.ts +7 -0
- package/src/components/netskrafl/index.ts +2 -0
- package/src/css/fonts.css +4 -0
- package/src/css/glyphs.css +224 -0
- package/src/css/skrafl-explo.css +6616 -0
- package/src/fonts/glyphicons-regular.eot +0 -0
- package/src/fonts/glyphicons-regular.ttf +0 -0
- package/src/fonts/glyphicons-regular.woff +0 -0
- package/src/index.ts +2 -0
- package/src/messages/messages.json +1576 -0
- package/src/mithril/actions.ts +319 -0
- package/src/mithril/bag.ts +65 -0
- package/src/mithril/bestdisplay.ts +74 -0
- package/src/mithril/blankdialog.ts +94 -0
- package/src/mithril/board.ts +336 -0
- package/src/mithril/buttons.ts +303 -0
- package/src/mithril/challengedialog.ts +186 -0
- package/src/mithril/channel.ts +162 -0
- package/src/mithril/chat.ts +228 -0
- package/src/mithril/components.ts +496 -0
- package/src/mithril/dragdrop.ts +219 -0
- package/src/mithril/elopage.ts +180 -0
- package/src/mithril/friend.ts +227 -0
- package/src/mithril/game.ts +1378 -0
- package/src/mithril/gameview.ts +111 -0
- package/src/mithril/globalstate.ts +33 -0
- package/src/mithril/i18n.ts +186 -0
- package/src/mithril/localstorage.ts +133 -0
- package/src/mithril/login.ts +122 -0
- package/src/mithril/logo.ts +270 -0
- package/src/mithril/main.ts +737 -0
- package/src/mithril/mithril.ts +29 -0
- package/src/mithril/model.ts +817 -0
- package/src/mithril/movelistitem.ts +226 -0
- package/src/mithril/page.ts +852 -0
- package/src/mithril/playername.ts +91 -0
- package/src/mithril/promodialog.ts +82 -0
- package/src/mithril/recentlist.ts +148 -0
- package/src/mithril/request.ts +52 -0
- package/src/mithril/review.ts +634 -0
- package/src/mithril/rightcolumn.ts +398 -0
- package/src/mithril/searchbutton.ts +118 -0
- package/src/mithril/statsdisplay.ts +109 -0
- package/src/mithril/tabs.ts +169 -0
- package/src/mithril/tile.ts +145 -0
- package/src/mithril/twoletter.ts +76 -0
- package/src/mithril/types.ts +379 -0
- package/src/mithril/userinfodialog.ts +171 -0
- package/src/mithril/util.ts +304 -0
- package/src/mithril/wait.ts +246 -0
- package/src/mithril/wordcheck.ts +102 -0
- package/tsconfig.json +28 -0
- package/vite.config.ts +12 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Rightcolumn.ts
|
|
4
|
+
|
|
5
|
+
Right column of board view component
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2024 Miðeind ehf.
|
|
8
|
+
Author: Vilhjalmur Thorsteinsson
|
|
9
|
+
|
|
10
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
11
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
12
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { IGame, IView } from "./types";
|
|
17
|
+
import { ts } from "./i18n";
|
|
18
|
+
import { gameUrl, glyph, nbsp, scrollMovelistToBottom } from "./util";
|
|
19
|
+
import { VnodeChildren, ComponentFunc, m } from "./mithril";
|
|
20
|
+
import { ExploLogoOnly } from "./logo";
|
|
21
|
+
import { PlayerName } from "./playername";
|
|
22
|
+
import { TwoLetter } from "./twoletter";
|
|
23
|
+
import { Chat } from "./chat";
|
|
24
|
+
import { Bag } from "./bag";
|
|
25
|
+
import { MoveListItem } from "./movelistitem";
|
|
26
|
+
|
|
27
|
+
interface IAttributes {
|
|
28
|
+
view: IView;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const Movelist: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
32
|
+
// The move list tab
|
|
33
|
+
|
|
34
|
+
const view = initialVnode.attrs.view;
|
|
35
|
+
const model = view.model;
|
|
36
|
+
const game = model.game;
|
|
37
|
+
const state = model.state;
|
|
38
|
+
|
|
39
|
+
function movelist(): VnodeChildren {
|
|
40
|
+
let mlist = game ? game.moves : []; // All moves made so far in the game
|
|
41
|
+
let r: VnodeChildren = [];
|
|
42
|
+
let leftTotal = 0;
|
|
43
|
+
let rightTotal = 0;
|
|
44
|
+
for (let i = 0; i < mlist.length; i++) {
|
|
45
|
+
let move = mlist[i];
|
|
46
|
+
let [player, [co, tiles, score]] = move;
|
|
47
|
+
if (player === 0)
|
|
48
|
+
leftTotal = Math.max(leftTotal + score, 0);
|
|
49
|
+
else
|
|
50
|
+
rightTotal = Math.max(rightTotal + score, 0);
|
|
51
|
+
const n = m(MoveListItem,
|
|
52
|
+
{
|
|
53
|
+
view,
|
|
54
|
+
move,
|
|
55
|
+
info: {
|
|
56
|
+
key: i.toString(),
|
|
57
|
+
leftTotal: leftTotal, rightTotal: rightTotal,
|
|
58
|
+
player: player, co: co, tiles: tiles, score: score
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
r.push(n);
|
|
63
|
+
}
|
|
64
|
+
return r;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const bag = game ? game.bag : "";
|
|
68
|
+
const newbag = game ? game.newbag : true;
|
|
69
|
+
return {
|
|
70
|
+
view: () => m(".movelist-container",
|
|
71
|
+
[
|
|
72
|
+
m(".movelist",
|
|
73
|
+
{
|
|
74
|
+
onupdate: () => { setTimeout(scrollMovelistToBottom); }
|
|
75
|
+
},
|
|
76
|
+
movelist()
|
|
77
|
+
),
|
|
78
|
+
// Show the bag here on mobile
|
|
79
|
+
state?.uiFullscreen ? "" : m(Bag, { bag, newbag })
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const Games: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
86
|
+
// The game list tab
|
|
87
|
+
|
|
88
|
+
const view = initialVnode.attrs.view;
|
|
89
|
+
const model = view.model;
|
|
90
|
+
|
|
91
|
+
function games(): VnodeChildren {
|
|
92
|
+
let r: VnodeChildren = [];
|
|
93
|
+
if (model.loadingGameList)
|
|
94
|
+
return r;
|
|
95
|
+
let gameList = model.gameList;
|
|
96
|
+
if (gameList === undefined)
|
|
97
|
+
// Game list is being loaded
|
|
98
|
+
return r;
|
|
99
|
+
if (gameList === null) {
|
|
100
|
+
// No games to show now, but we'll load them
|
|
101
|
+
// and they will be automatically refreshed when ready
|
|
102
|
+
model.loadGameList();
|
|
103
|
+
return r;
|
|
104
|
+
}
|
|
105
|
+
const game = model.game;
|
|
106
|
+
const gameId = game ? game.uuid : "";
|
|
107
|
+
for (let item of gameList) {
|
|
108
|
+
if (item.uuid == gameId)
|
|
109
|
+
continue; // Don't show this game
|
|
110
|
+
if (!item.my_turn && !item.zombie)
|
|
111
|
+
continue; // Only show pending games
|
|
112
|
+
let opp: VnodeChildren;
|
|
113
|
+
if (item.oppid === null)
|
|
114
|
+
// Mark robots with a cog icon
|
|
115
|
+
opp = [glyph("cog"), nbsp(), item.opp];
|
|
116
|
+
else
|
|
117
|
+
opp = [item.opp];
|
|
118
|
+
let winLose = item.sc0 < item.sc1 ? ".losing" : "";
|
|
119
|
+
let title = "Staðan er " + item.sc0 + ":" + item.sc1;
|
|
120
|
+
// Add the game-timed class if the game is a timed game.
|
|
121
|
+
// These will not be displayed in the mobile UI.
|
|
122
|
+
r.push(
|
|
123
|
+
m(".games-item" + (item.timed ? ".game-timed" : ""),
|
|
124
|
+
{ key: item.uuid, title: title },
|
|
125
|
+
m(m.route.Link,
|
|
126
|
+
{ href: gameUrl(item.url) },
|
|
127
|
+
[
|
|
128
|
+
m(".at-top-left", m(".tilecount", m(".oc", opp))),
|
|
129
|
+
m(".at-top-left",
|
|
130
|
+
m(".tilecount.trans",
|
|
131
|
+
m(".tc" + winLose, { style: { width: item.tile_count.toString() + "%" } }, opp)
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return r;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
view: () => m(".games", { style: "z-index: 6" }, games())
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
interface ITabAttributes {
|
|
148
|
+
game: IGame;
|
|
149
|
+
tabid: string;
|
|
150
|
+
title: string;
|
|
151
|
+
icon: string;
|
|
152
|
+
alert?: boolean;
|
|
153
|
+
funcSel?: () => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const Tab: ComponentFunc<ITabAttributes> = (initialVnode) => {
|
|
157
|
+
// A clickable tab for the right-side area content
|
|
158
|
+
const game = initialVnode.attrs.game;
|
|
159
|
+
return {
|
|
160
|
+
view: (vnode) => {
|
|
161
|
+
const { tabid, title, icon, alert, funcSel } = vnode.attrs;
|
|
162
|
+
const sel = game?.sel || "movelist";
|
|
163
|
+
return m(".right-tab" + (sel === tabid ? ".selected" : ""),
|
|
164
|
+
{
|
|
165
|
+
id: "tab-" + tabid,
|
|
166
|
+
className: alert ? "alert" : "",
|
|
167
|
+
title: title,
|
|
168
|
+
onclick: (ev: Event) => {
|
|
169
|
+
// Select this tab
|
|
170
|
+
if (game && game.showingDialog === null) {
|
|
171
|
+
if (game.setSelectedTab(tabid)) {
|
|
172
|
+
// A new tab was actually selected
|
|
173
|
+
funcSel && funcSel();
|
|
174
|
+
if (tabid === "movelist")
|
|
175
|
+
setTimeout(scrollMovelistToBottom);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
ev.preventDefault();
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
glyph(icon)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const TabGroup: ComponentFunc<{ game: IGame }> = () => {
|
|
188
|
+
return {
|
|
189
|
+
view: (vnode) => {
|
|
190
|
+
// A group of clickable tabs for the right-side area content
|
|
191
|
+
const game = vnode.attrs.game;
|
|
192
|
+
let showchat = !(game.autoplayer[0] || game.autoplayer[1]);
|
|
193
|
+
const r: VnodeChildren = [
|
|
194
|
+
m(Tab, { game, tabid: "board", title: ts("Borðið"), icon: "grid" }),
|
|
195
|
+
m(Tab, { game, tabid: "movelist", title: ts("Leikir"), icon: "show-lines" }),
|
|
196
|
+
m(Tab, { game, tabid: "twoletter", title: ts("Tveggja stafa orð"), icon: "life-preserver" }),
|
|
197
|
+
m(Tab, { game, tabid: "games", title: ts("Viðureignir"), icon: "flag" }),
|
|
198
|
+
];
|
|
199
|
+
if (showchat) {
|
|
200
|
+
// Add chat tab
|
|
201
|
+
r.push(
|
|
202
|
+
m(Tab, {
|
|
203
|
+
game,
|
|
204
|
+
tabid: "chat",
|
|
205
|
+
title: ts("Spjall"),
|
|
206
|
+
icon: "conversation",
|
|
207
|
+
funcSel: () => {
|
|
208
|
+
// The tab has been clicked
|
|
209
|
+
if (game.markChatShown())
|
|
210
|
+
// ...and now the user has seen all chat messages up until now
|
|
211
|
+
m.redraw();
|
|
212
|
+
},
|
|
213
|
+
// Show chat icon in red if any chat messages have not been seen
|
|
214
|
+
// and the chat tab is not already selected
|
|
215
|
+
alert: !game.chatSeen && game.sel != "chat"
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return m.fragment({}, r);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const RightColumn: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
225
|
+
// A container for the right-side header and area components
|
|
226
|
+
|
|
227
|
+
const view = initialVnode.attrs.view;
|
|
228
|
+
const model = view.model;
|
|
229
|
+
const game = model.game;
|
|
230
|
+
|
|
231
|
+
function vwClock(): VnodeChildren {
|
|
232
|
+
// Show clock data if this is a timed game
|
|
233
|
+
if (!game || !game.showClock())
|
|
234
|
+
// Not a timed game, or a game that is completed
|
|
235
|
+
return m.fragment({}, []);
|
|
236
|
+
|
|
237
|
+
function vwClockFace(cls: string, txt: string, runningOut: boolean, blinking: boolean): VnodeChildren {
|
|
238
|
+
return m("h3." + cls
|
|
239
|
+
+ (runningOut ? ".running-out" : "")
|
|
240
|
+
+ (blinking ? ".blink" : ""),
|
|
241
|
+
txt
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return m.fragment({}, [
|
|
246
|
+
vwClockFace("clockleft", game.clockText0, game.runningOut0, game.blinking0),
|
|
247
|
+
vwClockFace("clockright", game.clockText1, game.runningOut1, game.blinking1),
|
|
248
|
+
m(".clockface", glyph("time"))
|
|
249
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function vwRightHeading(): VnodeChildren {
|
|
253
|
+
// The right-side heading on the game screen
|
|
254
|
+
|
|
255
|
+
const fairplay = game ? game.fairplay : false;
|
|
256
|
+
const player = game ? game.player : null;
|
|
257
|
+
const sc0 = game ? game.displayScore(0).toString() : "";
|
|
258
|
+
const sc1 = game ? game.displayScore(1).toString() : "";
|
|
259
|
+
return m(".heading",
|
|
260
|
+
[
|
|
261
|
+
// The header-logo is not displayed in fullscreen
|
|
262
|
+
m(".logowrapper",
|
|
263
|
+
m(".header-logo",
|
|
264
|
+
m(m.route.Link,
|
|
265
|
+
{
|
|
266
|
+
href: "/page",
|
|
267
|
+
class: "backlink"
|
|
268
|
+
},
|
|
269
|
+
m(ExploLogoOnly)
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
),
|
|
273
|
+
m(".playerwrapper", [
|
|
274
|
+
m(".leftplayer" + (player === 1 ? ".autoplayercolor" : ".humancolor"), [
|
|
275
|
+
m(".player", m(PlayerName, { view, side: "left" })),
|
|
276
|
+
m(".scorewrapper", m(".scoreleft", sc0)),
|
|
277
|
+
]),
|
|
278
|
+
m(".rightplayer" + (player === 1 ? ".humancolor" : ".autoplayercolor"), [
|
|
279
|
+
m(".player", m(PlayerName, { view, side: "right" })),
|
|
280
|
+
m(".scorewrapper", m(".scoreright", sc1)),
|
|
281
|
+
]),
|
|
282
|
+
m(".fairplay",
|
|
283
|
+
{ style: { visibility: fairplay ? "visible" : "hidden" } },
|
|
284
|
+
m("span.fairplay-btn.large", { title: ts("Skraflað án hjálpartækja") })
|
|
285
|
+
)
|
|
286
|
+
]),
|
|
287
|
+
vwClock(),
|
|
288
|
+
]
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function vwRightArea(): VnodeChildren {
|
|
293
|
+
// A container for the tabbed right-side area components
|
|
294
|
+
if (!game) return undefined;
|
|
295
|
+
const sel = game.sel || "movelist";
|
|
296
|
+
// Show the chat tab unless the opponent is an autoplayer
|
|
297
|
+
let component: VnodeChildren;
|
|
298
|
+
switch (sel) {
|
|
299
|
+
case "movelist":
|
|
300
|
+
component = m(Movelist, { view });
|
|
301
|
+
break;
|
|
302
|
+
case "twoletter":
|
|
303
|
+
component = m(TwoLetter, { view });
|
|
304
|
+
break;
|
|
305
|
+
case "chat":
|
|
306
|
+
component = m(Chat, { view });
|
|
307
|
+
break;
|
|
308
|
+
case "games":
|
|
309
|
+
component = m(Games, { view });
|
|
310
|
+
break;
|
|
311
|
+
default:
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
const tabgrp: VnodeChildren = m(TabGroup, { game });
|
|
315
|
+
return m(".right-area" + (game?.showClock() ? ".with-clock" : ""),
|
|
316
|
+
component ? [tabgrp, component] : [tabgrp]
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function vwRightMessage(): VnodeChildren {
|
|
321
|
+
// Display a status message in the mobile UI
|
|
322
|
+
if (!game) return undefined;
|
|
323
|
+
const s = game.buttonState();
|
|
324
|
+
let msg: string | any[] = "";
|
|
325
|
+
const player = game.player;
|
|
326
|
+
const opp = player === null ? "" : game.nickname[1 - player];
|
|
327
|
+
const move = game.moves.length ? game.moves[game.moves.length - 1] : undefined;
|
|
328
|
+
const mtype = move ? move[1][1] : undefined;
|
|
329
|
+
if (s.congratulate) {
|
|
330
|
+
// This player won
|
|
331
|
+
if (mtype == "RSGN")
|
|
332
|
+
msg = [m("strong", [opp, " resigned!"]), " Congratulations."];
|
|
333
|
+
else
|
|
334
|
+
msg = [m("strong", ["You beat ", opp, "!"]), " Congratulations."];
|
|
335
|
+
} else if (s.gameOver) {
|
|
336
|
+
// This player lost
|
|
337
|
+
msg = "Game over!";
|
|
338
|
+
} else if (!s.localTurn) {
|
|
339
|
+
// It's the opponent's turn
|
|
340
|
+
msg = ["It's ", opp, "'s turn. Plan your next move!"];
|
|
341
|
+
} else if (s.tilesPlaced) {
|
|
342
|
+
if (game.currentScore === undefined) {
|
|
343
|
+
if (move === undefined)
|
|
344
|
+
msg = ["Your first move must cover the ", glyph("target"), " start square."];
|
|
345
|
+
else
|
|
346
|
+
msg = "Tiles must be consecutive.";
|
|
347
|
+
} else if (game.wordGood === false) {
|
|
348
|
+
msg = ["Move is not valid, but would score ", m("strong", game.currentScore.toString()), " points."];
|
|
349
|
+
} else {
|
|
350
|
+
msg = ["Valid move, score ", m("strong", game.currentScore.toString()), " points."];
|
|
351
|
+
}
|
|
352
|
+
} else if (move === undefined) {
|
|
353
|
+
// Initial move
|
|
354
|
+
msg = [m("strong", "You start!"), " Cover the ", glyph("star"), " asterisk with your move."];
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
const co = move[1][0];
|
|
358
|
+
let tiles = mtype || "";
|
|
359
|
+
const score = move[1][2];
|
|
360
|
+
if (co === "") {
|
|
361
|
+
// Not a regular tile move
|
|
362
|
+
if (tiles === "PASS")
|
|
363
|
+
msg = [opp, " passed."];
|
|
364
|
+
else if (tiles.indexOf("EXCH") === 0) {
|
|
365
|
+
const numtiles = tiles.slice(5).length;
|
|
366
|
+
msg = [
|
|
367
|
+
opp, " exchanged ",
|
|
368
|
+
numtiles.toString(),
|
|
369
|
+
(numtiles === 1 ? " tile" : " tiles"),
|
|
370
|
+
"."
|
|
371
|
+
];
|
|
372
|
+
} else if (tiles === "CHALL") {
|
|
373
|
+
msg = [opp, " challenged your move."];
|
|
374
|
+
} else if (tiles === "RESP") {
|
|
375
|
+
if (score < 0)
|
|
376
|
+
msg = [opp, " successfully challenged your move."];
|
|
377
|
+
else
|
|
378
|
+
msg = [opp, " unsuccessfully challenged your move and lost 10 points."];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// Regular tile move
|
|
383
|
+
tiles = tiles.split("?").join(""); /* TBD: Display wildcard characters differently? */
|
|
384
|
+
msg = [opp, " played ", m("strong", tiles),
|
|
385
|
+
" for ", m("strong", score.toString()), " points"];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return m(".message", msg);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
view: () => m(".rightcol", [
|
|
393
|
+
vwRightHeading(),
|
|
394
|
+
vwRightArea(),
|
|
395
|
+
/* vwRightMessage(), */
|
|
396
|
+
])
|
|
397
|
+
};
|
|
398
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Searchbutton.ts
|
|
4
|
+
|
|
5
|
+
Search button component
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2024 Miðeind ehf.
|
|
8
|
+
Author: Vilhjalmur Thorsteinsson
|
|
9
|
+
|
|
10
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
11
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
12
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { IView } from "./types";
|
|
17
|
+
import { ts } from "./i18n";
|
|
18
|
+
import { ComponentFunc, m } from "./mithril";
|
|
19
|
+
import { glyph, nbsp } from "./util";
|
|
20
|
+
|
|
21
|
+
interface IAttributes {
|
|
22
|
+
view: IView;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const SearchButton: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
26
|
+
|
|
27
|
+
// A combination of a button and pattern entry field
|
|
28
|
+
// for user search
|
|
29
|
+
|
|
30
|
+
const view = initialVnode.attrs.view;
|
|
31
|
+
const model = view.model;
|
|
32
|
+
let spec = ""; // The current search pattern
|
|
33
|
+
let promise: { result: boolean; p: Promise<boolean>; } | undefined = undefined;
|
|
34
|
+
|
|
35
|
+
function newSearch() {
|
|
36
|
+
// There may have been a change of search parameters: react
|
|
37
|
+
if (promise !== undefined) {
|
|
38
|
+
// There was a previous promise, now obsolete: make it
|
|
39
|
+
// resolve without action
|
|
40
|
+
promise.result = false;
|
|
41
|
+
promise = undefined;
|
|
42
|
+
}
|
|
43
|
+
let sel = model.userListCriteria?.query || "robots";
|
|
44
|
+
if (sel !== "search") {
|
|
45
|
+
// Not already in a search: load the user list immediately
|
|
46
|
+
model.loadUserList({ query: "search", spec: spec }, true);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (spec === model.userListCriteria?.spec)
|
|
50
|
+
// We're already looking at the same search spec: done
|
|
51
|
+
return;
|
|
52
|
+
// We're changing the search spec.
|
|
53
|
+
// In order to limit the number of search queries sent to
|
|
54
|
+
// the server while typing a new criteria, we keep an
|
|
55
|
+
// outstanding promise that resolves in 0.8 seconds,
|
|
56
|
+
// unless cancelled by a new keystroke/promise.
|
|
57
|
+
// Note: since a promise can't be directly cancelled, we use a
|
|
58
|
+
// convoluted route to associate a boolean result with it.
|
|
59
|
+
let newP = {
|
|
60
|
+
result: true,
|
|
61
|
+
p: new Promise<boolean>((resolve) => {
|
|
62
|
+
// After 800 milliseconds, resolve to whatever value the
|
|
63
|
+
// result property has at that time. It will be true
|
|
64
|
+
// unless the promise has been "cancelled" by setting
|
|
65
|
+
// its result property to false.
|
|
66
|
+
setTimeout(() => { resolve(newP.result); }, 800);
|
|
67
|
+
})
|
|
68
|
+
};
|
|
69
|
+
promise = newP;
|
|
70
|
+
promise.p.then((value: boolean) => {
|
|
71
|
+
if (value) {
|
|
72
|
+
// Successfully resolved, without cancellation:
|
|
73
|
+
// issue the search query to the server as it now stands
|
|
74
|
+
model.loadUserList({ query: "search", spec: spec }, true);
|
|
75
|
+
promise = undefined;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
|
|
82
|
+
view: () => {
|
|
83
|
+
const sel = model.userListCriteria?.query || "robots";
|
|
84
|
+
return m(".user-cat[id='user-search']",
|
|
85
|
+
[
|
|
86
|
+
glyph("search",
|
|
87
|
+
{
|
|
88
|
+
id: 'search',
|
|
89
|
+
className: (sel == "search" ? "shown" : ""),
|
|
90
|
+
onclick: () => {
|
|
91
|
+
// Reset the search pattern when clicking the search icon
|
|
92
|
+
spec = "";
|
|
93
|
+
newSearch();
|
|
94
|
+
document.getElementById("search-id")?.focus();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
nbsp(),
|
|
99
|
+
m("input.text.userid",
|
|
100
|
+
{
|
|
101
|
+
type: 'text',
|
|
102
|
+
id: 'search-id',
|
|
103
|
+
name: 'search-id',
|
|
104
|
+
maxlength: 16,
|
|
105
|
+
placeholder: ts('Einkenni eða nafn'),
|
|
106
|
+
value: spec,
|
|
107
|
+
onfocus: () => newSearch(),
|
|
108
|
+
oninput: (ev: Event) => {
|
|
109
|
+
spec = (ev.target as HTMLInputElement).value + "";
|
|
110
|
+
newSearch();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Statsdisplay.ts
|
|
4
|
+
|
|
5
|
+
User status display component
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2024 Miðeind ehf.
|
|
8
|
+
Author: Vilhjalmur Thorsteinsson
|
|
9
|
+
|
|
10
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
11
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
12
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { IView } from "./types";
|
|
17
|
+
import { ComponentFunc, m, VnodeChildren } from "./mithril";
|
|
18
|
+
import { ts } from "./i18n";
|
|
19
|
+
import { glyph, nbsp, valueOrK } from "./util";
|
|
20
|
+
|
|
21
|
+
interface IAttributes {
|
|
22
|
+
view: IView;
|
|
23
|
+
id: string;
|
|
24
|
+
ownStats: Record<string, any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const StatsDisplay: ComponentFunc<IAttributes> = () => {
|
|
28
|
+
// Display key statistics, provided via the ownStats attribute
|
|
29
|
+
|
|
30
|
+
let sel = 1;
|
|
31
|
+
|
|
32
|
+
function vwStat(val?: number, icon?: string, suffix?: string): VnodeChildren {
|
|
33
|
+
// Display a user statistics figure, eventually with an icon
|
|
34
|
+
var txt = (val === undefined) ? "" : valueOrK(val);
|
|
35
|
+
if (suffix !== undefined)
|
|
36
|
+
txt += suffix;
|
|
37
|
+
return icon ? [glyph(icon), nbsp(), txt] : txt;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
|
|
42
|
+
view: (vnode) => {
|
|
43
|
+
|
|
44
|
+
// Display statistics about this user
|
|
45
|
+
var s = vnode.attrs.ownStats;
|
|
46
|
+
var winRatio = 0, winRatioHuman = 0;
|
|
47
|
+
var avgScore = 0, avgScoreHuman = 0;
|
|
48
|
+
if (s !== undefined && s.games !== undefined && s.human_games !== undefined) {
|
|
49
|
+
if (s.games > 0) {
|
|
50
|
+
winRatio = Math.round(100.0 * s.wins / s.games);
|
|
51
|
+
avgScore = Math.round(s.score / s.games);
|
|
52
|
+
}
|
|
53
|
+
if (s.human_games > 0) {
|
|
54
|
+
winRatioHuman = Math.round(100.0 * s.human_wins / s.human_games);
|
|
55
|
+
avgScoreHuman = Math.round(s.human_score / s.human_games);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return m("div", { id: vnode.attrs.id },
|
|
60
|
+
[
|
|
61
|
+
m(".toggler",
|
|
62
|
+
{
|
|
63
|
+
id: 'own-toggler',
|
|
64
|
+
title: ts("stats_choice"), // "With or without robot games"
|
|
65
|
+
onclick: (ev: Event) => { sel = 3 - sel; ev.preventDefault(); },
|
|
66
|
+
},
|
|
67
|
+
[
|
|
68
|
+
m(".option.small" + (sel === 1 ? ".selected" : ""),
|
|
69
|
+
{ id: 'opt1' },
|
|
70
|
+
glyph("user")
|
|
71
|
+
),
|
|
72
|
+
m(".option.small" + (sel === 2 ? ".selected" : ""),
|
|
73
|
+
{ id: 'opt2' },
|
|
74
|
+
glyph("cog")
|
|
75
|
+
)
|
|
76
|
+
]
|
|
77
|
+
),
|
|
78
|
+
sel === 1 ? m("div",
|
|
79
|
+
{ id: 'own-stats-human', className: 'stats-box', style: { display: "inline-block" } },
|
|
80
|
+
[
|
|
81
|
+
m(".stats-fig", { title: ts('Elo-stig') },
|
|
82
|
+
s ? vwStat(s.locale_elo?.human_elo, "crown") : ""),
|
|
83
|
+
m(".stats-fig.stats-games", { title: ts('Fjöldi viðureigna') },
|
|
84
|
+
s ? vwStat(s.human_games, "th") : ""),
|
|
85
|
+
m(".stats-fig.stats-win-ratio", { title: ts('Vinningshlutfall') },
|
|
86
|
+
vwStat(winRatioHuman, "bookmark", "%")),
|
|
87
|
+
m(".stats-fig.stats-avg-score", { title: ts('Meðalstigafjöldi') },
|
|
88
|
+
vwStat(avgScoreHuman, "dashboard"))
|
|
89
|
+
]
|
|
90
|
+
) : "",
|
|
91
|
+
sel === 2 ? m("div",
|
|
92
|
+
{ id: 'own-stats-all', className: 'stats-box', style: { display: "inline-block" } },
|
|
93
|
+
[
|
|
94
|
+
m(".stats-fig", { title: ts("Elo-stig") },
|
|
95
|
+
s ? vwStat(s.locale_elo?.elo, "crown") : ""),
|
|
96
|
+
m(".stats-fig.stats-games", { title: ts('Fjöldi viðureigna') },
|
|
97
|
+
s ? vwStat(s.games, "th") : ""),
|
|
98
|
+
m(".stats-fig.stats-win-ratio", { title: ts('Vinningshlutfall') },
|
|
99
|
+
vwStat(winRatio, "bookmark", "%")),
|
|
100
|
+
m(".stats-fig.stats-avg-score", { title: ts('Meðalstigafjöldi') },
|
|
101
|
+
vwStat(avgScore, "dashboard"))
|
|
102
|
+
]
|
|
103
|
+
) : ""
|
|
104
|
+
]
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
};
|
|
109
|
+
};
|