@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,319 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Actions.ts
|
|
4
|
+
|
|
5
|
+
Single page UI for Explo using the Mithril library
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2024 Miðeind ehf.
|
|
8
|
+
Author: Vilhjálmur Þorsteinsson
|
|
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
|
+
This file implements the Actions class.
|
|
15
|
+
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { ServerGame } from "./types";
|
|
19
|
+
import { Params, scrollMovelistToBottom } from "./util";
|
|
20
|
+
import { m } from "./mithril";
|
|
21
|
+
import {
|
|
22
|
+
attachFirebaseListener, detachFirebaseListener, logEvent
|
|
23
|
+
} from "./channel";
|
|
24
|
+
import { Model } from "./model";
|
|
25
|
+
import { View } from "./page";
|
|
26
|
+
|
|
27
|
+
export class Actions {
|
|
28
|
+
|
|
29
|
+
model: Model;
|
|
30
|
+
view: View;
|
|
31
|
+
|
|
32
|
+
constructor(model: Model, view: View) {
|
|
33
|
+
this.model = model;
|
|
34
|
+
this.view = view;
|
|
35
|
+
this.initMediaListener();
|
|
36
|
+
// this.attachListenerToUser();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onNavigateTo(routeName: string, params: Params) {
|
|
40
|
+
// We have navigated to a new route
|
|
41
|
+
// If navigating to something other than help,
|
|
42
|
+
// we need to have a logged-in user
|
|
43
|
+
const model = this.model;
|
|
44
|
+
this.view.boardScale = 1.0;
|
|
45
|
+
model.routeName = routeName;
|
|
46
|
+
model.params = params;
|
|
47
|
+
const uuid = params.uuid ?? "";
|
|
48
|
+
if (routeName == "game") {
|
|
49
|
+
// New game route: initiate loading of the game into the model
|
|
50
|
+
if (model.game !== null) {
|
|
51
|
+
this.detachListenerFromGame(model.game.uuid);
|
|
52
|
+
}
|
|
53
|
+
// If opening this game as a zombie, remove zombie status
|
|
54
|
+
const deleteZombie = params.zombie === "1";
|
|
55
|
+
// Load the game, and attach it to the Firebase listener once it's loaded
|
|
56
|
+
model.loadGame(
|
|
57
|
+
uuid,
|
|
58
|
+
() => {
|
|
59
|
+
this.attachListenerToGame(uuid);
|
|
60
|
+
setTimeout(scrollMovelistToBottom);
|
|
61
|
+
},
|
|
62
|
+
deleteZombie
|
|
63
|
+
);
|
|
64
|
+
if (model.game !== null && model.game !== undefined) {
|
|
65
|
+
logEvent("game_open",
|
|
66
|
+
{
|
|
67
|
+
locale: model.game.locale,
|
|
68
|
+
uuid: params.uuid,
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
} else if (routeName == "review") {
|
|
73
|
+
// A game review: detach listener, if any, and load
|
|
74
|
+
// new game if necessary
|
|
75
|
+
if (model.game !== null) {
|
|
76
|
+
// !!! This may cause an extra detach - we assume that's OK
|
|
77
|
+
this.detachListenerFromGame(model.game.uuid);
|
|
78
|
+
}
|
|
79
|
+
// Find out which move we should show in the review
|
|
80
|
+
let moveParam: string = params.move || "0";
|
|
81
|
+
// Start with move number 0 by default
|
|
82
|
+
let move = parseInt(moveParam);
|
|
83
|
+
if (isNaN(move) || !move || move < 0)
|
|
84
|
+
move = 0;
|
|
85
|
+
if (model.game === null || model.game.uuid != params.uuid) {
|
|
86
|
+
// Different game than we had before: load it, and then
|
|
87
|
+
// fetch the best moves
|
|
88
|
+
model.loadGame(uuid, () => {
|
|
89
|
+
model.loadBestMoves(move);
|
|
90
|
+
setTimeout(scrollMovelistToBottom);
|
|
91
|
+
});
|
|
92
|
+
} else if (model.game !== null) {
|
|
93
|
+
// Already have the right game loaded:
|
|
94
|
+
// Fetch the best moves and show them once they're available
|
|
95
|
+
model.loadBestMoves(move);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Not a game route: delete the previously loaded game, if any
|
|
99
|
+
if (model.game !== null) {
|
|
100
|
+
this.detachListenerFromGame(model.game.uuid);
|
|
101
|
+
model.game.cleanup();
|
|
102
|
+
model.game = null;
|
|
103
|
+
}
|
|
104
|
+
const locale = model.state?.locale || "is_IS";
|
|
105
|
+
if (routeName == "help") {
|
|
106
|
+
// Make sure that the help HTML is loaded upon first use
|
|
107
|
+
model.loadHelp();
|
|
108
|
+
logEvent("help", { locale });
|
|
109
|
+
} else if (routeName == "thanks") {
|
|
110
|
+
// Log a conversion event
|
|
111
|
+
if (model.state?.userId) {
|
|
112
|
+
logEvent("init_plan",
|
|
113
|
+
{
|
|
114
|
+
userid: model.state.userId,
|
|
115
|
+
locale,
|
|
116
|
+
// TODO: Add plan identifiers here
|
|
117
|
+
plan: "friend"
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
} else if (routeName == "main") {
|
|
122
|
+
// Force reload of lists
|
|
123
|
+
// TODO: This may not be necessary,
|
|
124
|
+
// if all Firebase notifications are acted upon
|
|
125
|
+
model.gameList = null;
|
|
126
|
+
model.userListCriteria = null;
|
|
127
|
+
model.userList = null;
|
|
128
|
+
model.challengeList = null;
|
|
129
|
+
model.recentList = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onMoveMessage(json: ServerGame, firstAttach: boolean) {
|
|
135
|
+
// Handle a move message from Firebase
|
|
136
|
+
console.log("Move message received: " + JSON.stringify(json));
|
|
137
|
+
this.model.handleMoveMessage(json, firstAttach);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onUserMessage(json: any, firstAttach: boolean) {
|
|
141
|
+
// Handle a user message from Firebase
|
|
142
|
+
console.log("User message received: " + JSON.stringify(json));
|
|
143
|
+
this.model.handleUserMessage(json, firstAttach);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onChatMessage(
|
|
147
|
+
json: { from_userid: string; game: string; msg: string; ts: string; },
|
|
148
|
+
firstAttach: boolean
|
|
149
|
+
) {
|
|
150
|
+
// Handle an incoming chat message
|
|
151
|
+
if (firstAttach)
|
|
152
|
+
console.log("First attach of chat: " + JSON.stringify(json));
|
|
153
|
+
else {
|
|
154
|
+
console.log("Chat message received: " + JSON.stringify(json));
|
|
155
|
+
if (this.model.addChatMessage(json.game, json.from_userid, json.msg, json.ts)) {
|
|
156
|
+
// A chat message was successfully added
|
|
157
|
+
this.view.notifyChatMessage();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onFullScreen() {
|
|
163
|
+
// Take action when min-width exceeds 768
|
|
164
|
+
const state = this.model.state;
|
|
165
|
+
if (state && !state.uiFullscreen) {
|
|
166
|
+
state.uiFullscreen = true;
|
|
167
|
+
this.view.notifyMediaChange();
|
|
168
|
+
m.redraw();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
onMobileScreen() {
|
|
173
|
+
const state = this.model.state;
|
|
174
|
+
if (state && state.uiFullscreen !== false) {
|
|
175
|
+
state.uiFullscreen = false;
|
|
176
|
+
this.view.notifyMediaChange();
|
|
177
|
+
m.redraw();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
onLandscapeScreen() {
|
|
182
|
+
const state = this.model.state;
|
|
183
|
+
if (state && !state.uiLandscape) {
|
|
184
|
+
state.uiLandscape = true;
|
|
185
|
+
this.view.notifyMediaChange();
|
|
186
|
+
m.redraw();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
onPortraitScreen() {
|
|
191
|
+
const state = this.model.state;
|
|
192
|
+
if (state && state.uiLandscape !== false) {
|
|
193
|
+
state.uiLandscape = false;
|
|
194
|
+
this.view.notifyMediaChange();
|
|
195
|
+
m.redraw();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
mediaMinWidth667(mql: MediaQueryList) {
|
|
200
|
+
if (mql.matches) {
|
|
201
|
+
// Take action when min-width exceeds 667
|
|
202
|
+
// (usually because of rotation from portrait to landscape)
|
|
203
|
+
// The board tab is not visible, so the movelist is default
|
|
204
|
+
this.onLandscapeScreen();
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// min-width is below 667
|
|
208
|
+
// (usually because of rotation from landscape to portrait)
|
|
209
|
+
// Make sure the board tab is selected
|
|
210
|
+
this.onPortraitScreen();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
mediaMinWidth768(mql: MediaQueryList) {
|
|
215
|
+
if (mql.matches) {
|
|
216
|
+
this.onFullScreen();
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.onMobileScreen();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
initMediaListener() {
|
|
224
|
+
// Install listener functions for media changes
|
|
225
|
+
|
|
226
|
+
function addEventListener(mql: MediaQueryList, func: (ev: MediaQueryListEvent) => void) {
|
|
227
|
+
// Hack to make addEventListener work on older Safari platforms
|
|
228
|
+
try {
|
|
229
|
+
// Chrome & Firefox
|
|
230
|
+
mql.addEventListener('change', func, { passive: true });
|
|
231
|
+
} catch (e1) {
|
|
232
|
+
try {
|
|
233
|
+
// Safari
|
|
234
|
+
mql.addListener(func);
|
|
235
|
+
} catch (e2) {
|
|
236
|
+
console.error(e2);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let mql: MediaQueryList = window.matchMedia("(min-width: 667px)");
|
|
242
|
+
let view = this;
|
|
243
|
+
if (mql) {
|
|
244
|
+
this.mediaMinWidth667(mql);
|
|
245
|
+
addEventListener(mql, () => view.mediaMinWidth667(mql));
|
|
246
|
+
}
|
|
247
|
+
mql = window.matchMedia("(min-width: 768px)");
|
|
248
|
+
if (mql) {
|
|
249
|
+
this.mediaMinWidth768(mql);
|
|
250
|
+
addEventListener(mql, () => view.mediaMinWidth768(mql));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
attachListenerToUser() {
|
|
255
|
+
const state = this.model.state;
|
|
256
|
+
if (state && state.userId)
|
|
257
|
+
attachFirebaseListener('user/' + state.userId,
|
|
258
|
+
(json, firstAttach) => this.onUserMessage(json, firstAttach)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
detachListenerFromUser() {
|
|
263
|
+
// Stop listening to Firebase notifications for the current user
|
|
264
|
+
const state = this.model.state;
|
|
265
|
+
if (state && state.userId)
|
|
266
|
+
detachFirebaseListener('user/' + state.userId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
attachListenerToGame(uuid: string) {
|
|
270
|
+
// Listen to Firebase events on the /game/[gameId]/[userId] path
|
|
271
|
+
const state = this.model.state;
|
|
272
|
+
if (!uuid || !state) return;
|
|
273
|
+
const basepath = 'game/' + uuid + "/" + state.userId + "/";
|
|
274
|
+
// New moves
|
|
275
|
+
attachFirebaseListener(basepath + "move",
|
|
276
|
+
(json, firstAttach) => this.onMoveMessage(json, firstAttach)
|
|
277
|
+
);
|
|
278
|
+
// New chat messages
|
|
279
|
+
attachFirebaseListener(basepath + "chat",
|
|
280
|
+
(json, firstAttach) => this.onChatMessage(json, firstAttach)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
detachListenerFromGame(uuid: string) {
|
|
285
|
+
// Stop listening to Firebase events on the /game/[gameId]/[userId] path
|
|
286
|
+
const state = this.model.state;
|
|
287
|
+
if (!uuid || !state) return;
|
|
288
|
+
const basepath = 'game/' + uuid + "/" + state.userId + "/";
|
|
289
|
+
detachFirebaseListener(basepath + "move");
|
|
290
|
+
detachFirebaseListener(basepath + "chat");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
} // class Actions
|
|
294
|
+
|
|
295
|
+
export function createRouteResolver(actions: Actions): m.RouteDefs {
|
|
296
|
+
|
|
297
|
+
// Return a map of routes to onmatch and render functions
|
|
298
|
+
|
|
299
|
+
let model = actions.model;
|
|
300
|
+
let view = actions.view;
|
|
301
|
+
// let state = model.state;
|
|
302
|
+
|
|
303
|
+
return model.paths.reduce((acc, item) => {
|
|
304
|
+
acc[item.route] = {
|
|
305
|
+
|
|
306
|
+
// Navigating to a new route (passed in the second parameter)
|
|
307
|
+
onmatch: (args: Params) => {
|
|
308
|
+
// Automatically close all dialogs
|
|
309
|
+
view.popAllDialogs();
|
|
310
|
+
actions.onNavigateTo(item.name, args);
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// Render a view on a model
|
|
314
|
+
render: () => { return view.appView(item.name); }
|
|
315
|
+
|
|
316
|
+
};
|
|
317
|
+
return acc;
|
|
318
|
+
}, {} as m.RouteDefs);
|
|
319
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Bag.ts
|
|
4
|
+
|
|
5
|
+
Bag of tiles 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 { ts } from "./i18n";
|
|
17
|
+
import { ComponentFunc, m, VnodeChildren } from "./mithril";
|
|
18
|
+
import { RACK_SIZE } from "./types";
|
|
19
|
+
|
|
20
|
+
const BAG_TILES_PER_LINE = 19;
|
|
21
|
+
|
|
22
|
+
interface IAttributes {
|
|
23
|
+
bag: string;
|
|
24
|
+
newbag: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Bag: ComponentFunc<IAttributes> = () => {
|
|
28
|
+
// The bag of tiles
|
|
29
|
+
|
|
30
|
+
function tiles(bag: string): VnodeChildren {
|
|
31
|
+
let r: VnodeChildren = [];
|
|
32
|
+
let ix = 0;
|
|
33
|
+
let count = bag.length;
|
|
34
|
+
while (count > 0) {
|
|
35
|
+
// Rows
|
|
36
|
+
let cols: VnodeChildren = [];
|
|
37
|
+
// Columns: max BAG_TILES_PER_LINE tiles per row
|
|
38
|
+
for (let i = 0; i < BAG_TILES_PER_LINE && count > 0; i++) {
|
|
39
|
+
let tile = bag[ix++];
|
|
40
|
+
if (tile == "?")
|
|
41
|
+
// Show wildcard tiles '?' as blanks
|
|
42
|
+
tile = " ";
|
|
43
|
+
cols.push(m("td", m.trust(tile)));
|
|
44
|
+
count--;
|
|
45
|
+
}
|
|
46
|
+
r.push(m("tr", cols));
|
|
47
|
+
}
|
|
48
|
+
return r;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
view: (vnode) => {
|
|
53
|
+
const { bag, newbag } = vnode.attrs;
|
|
54
|
+
let cls = "";
|
|
55
|
+
if (bag.length <= RACK_SIZE)
|
|
56
|
+
cls += ".empty";
|
|
57
|
+
else if (newbag)
|
|
58
|
+
cls += ".new";
|
|
59
|
+
return m(".bag",
|
|
60
|
+
{ title: ts("Flísar sem eftir eru") },
|
|
61
|
+
m("table.bag-content" + cls, tiles(bag))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Bestdisplay.ts
|
|
4
|
+
|
|
5
|
+
Best words and games 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 { ts } from "./i18n";
|
|
17
|
+
import { ComponentFunc, VnodeChildren, m } from "./mithril";
|
|
18
|
+
|
|
19
|
+
interface IAttributes {
|
|
20
|
+
ownStats: Record<string, any>;
|
|
21
|
+
myself: boolean;
|
|
22
|
+
id: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const BestDisplay: ComponentFunc<IAttributes> = () => {
|
|
26
|
+
// Display the best words and best games played for a given user
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
|
|
30
|
+
view: (vnode) => {
|
|
31
|
+
// Populate the highest score/best word field
|
|
32
|
+
const json = vnode.attrs.ownStats || {};
|
|
33
|
+
const best: VnodeChildren = [];
|
|
34
|
+
if (json.highest_score) {
|
|
35
|
+
best.push(ts("Hæsta skor "));
|
|
36
|
+
best.push(m("b",
|
|
37
|
+
m(m.route.Link,
|
|
38
|
+
{ href: "/game/" + json.highest_score_game },
|
|
39
|
+
json.highest_score
|
|
40
|
+
)
|
|
41
|
+
));
|
|
42
|
+
}
|
|
43
|
+
if (json.best_word) {
|
|
44
|
+
if (best.length)
|
|
45
|
+
if (vnode.attrs.myself)
|
|
46
|
+
best.push(m("br")); // Own stats: Line break between parts
|
|
47
|
+
else
|
|
48
|
+
best.push(" | "); // Opponent stats: Divider bar between parts
|
|
49
|
+
let bw = json.best_word;
|
|
50
|
+
let s = [];
|
|
51
|
+
// Make sure blank tiles get a different color
|
|
52
|
+
for (let i = 0; i < bw.length; i++)
|
|
53
|
+
if (bw[i] == '?') {
|
|
54
|
+
s.push(m("span.blanktile", bw[i + 1]));
|
|
55
|
+
i += 1;
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
s.push(bw[i]);
|
|
59
|
+
best.push(ts("Besta orð "));
|
|
60
|
+
best.push(m("span.best-word", s));
|
|
61
|
+
best.push(", ");
|
|
62
|
+
best.push(m("b",
|
|
63
|
+
m(m.route.Link,
|
|
64
|
+
{ href: "/game/" + json.best_word_game },
|
|
65
|
+
json.best_word_score
|
|
66
|
+
)
|
|
67
|
+
));
|
|
68
|
+
best.push(ts(" stig"));
|
|
69
|
+
}
|
|
70
|
+
return m("p", { id: vnode.attrs.id }, best);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
BlankDialog.ts
|
|
4
|
+
|
|
5
|
+
Blank tile dialog 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, IGame } from "./types";
|
|
17
|
+
import { ComponentFunc, m, VnodeChildren } from "./mithril";
|
|
18
|
+
import { buttonOut, buttonOver, glyph } from "./util";
|
|
19
|
+
import { mt, ts } from "./i18n";
|
|
20
|
+
import { DialogButton } from "./components";
|
|
21
|
+
|
|
22
|
+
const BLANK_TILES_PER_LINE = 6;
|
|
23
|
+
|
|
24
|
+
interface IAttributes {
|
|
25
|
+
view: IView;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const BlankDialog: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
29
|
+
// A dialog for choosing the meaning of a blank tile
|
|
30
|
+
|
|
31
|
+
const view = initialVnode.attrs.view;
|
|
32
|
+
const model = view.model;
|
|
33
|
+
|
|
34
|
+
function blankLetters(game: IGame): VnodeChildren {
|
|
35
|
+
let legalLetters = game.alphabet;
|
|
36
|
+
let len = legalLetters.length;
|
|
37
|
+
let ix = 0;
|
|
38
|
+
let r: VnodeChildren = [];
|
|
39
|
+
|
|
40
|
+
while (len > 0) {
|
|
41
|
+
/* Rows */
|
|
42
|
+
let c = [];
|
|
43
|
+
/* Columns: max BLANK_TILES_PER_LINE tiles per row */
|
|
44
|
+
for (let i = 0; i < BLANK_TILES_PER_LINE && len > 0; i++) {
|
|
45
|
+
let letter = legalLetters[ix++];
|
|
46
|
+
c.push(
|
|
47
|
+
m("td",
|
|
48
|
+
{
|
|
49
|
+
onclick: (ev: Event) => { game.placeBlank(letter); ev.preventDefault(); },
|
|
50
|
+
onmouseover: buttonOver,
|
|
51
|
+
onmouseout: buttonOut
|
|
52
|
+
},
|
|
53
|
+
m(".blank-choice.tile.racktile", letter)
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
len--;
|
|
57
|
+
}
|
|
58
|
+
r.push(m("tr", c));
|
|
59
|
+
}
|
|
60
|
+
return r;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
view: () => {
|
|
65
|
+
const game = model.game;
|
|
66
|
+
if (!game) return undefined;
|
|
67
|
+
return m(".modal-dialog",
|
|
68
|
+
{
|
|
69
|
+
id: 'blank-dialog',
|
|
70
|
+
style: { visibility: "visible" }
|
|
71
|
+
},
|
|
72
|
+
m(".ui-widget.ui-widget-content.ui-corner-all", { id: 'blank-form' },
|
|
73
|
+
[
|
|
74
|
+
mt("p", "Hvaða staf táknar auða flísin?"),
|
|
75
|
+
m(".rack.blank-rack",
|
|
76
|
+
m("table.board", { id: 'blank-meaning' }, blankLetters(game))
|
|
77
|
+
),
|
|
78
|
+
m(DialogButton,
|
|
79
|
+
{
|
|
80
|
+
id: 'blank-close',
|
|
81
|
+
title: ts("Hætta við"),
|
|
82
|
+
onclick: (ev: Event) => {
|
|
83
|
+
ev.preventDefault();
|
|
84
|
+
game.cancelBlankDialog();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
glyph("remove")
|
|
88
|
+
)
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
};
|