@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.
Files changed (68) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.json +13 -0
  3. package/README.md +63 -0
  4. package/dist/cjs/index.css +6837 -0
  5. package/dist/cjs/index.js +3046 -0
  6. package/dist/cjs/index.js.map +1 -0
  7. package/dist/esm/index.css +6837 -0
  8. package/dist/esm/index.js +3046 -0
  9. package/dist/esm/index.js.map +1 -0
  10. package/dist/types.d.ts +41 -0
  11. package/package.json +63 -0
  12. package/rollup.config.js +60 -0
  13. package/src/components/index.ts +2 -0
  14. package/src/components/netskrafl/Netskrafl.stories.tsx +66 -0
  15. package/src/components/netskrafl/Netskrafl.tsx +135 -0
  16. package/src/components/netskrafl/Netskrafl.types.ts +7 -0
  17. package/src/components/netskrafl/index.ts +2 -0
  18. package/src/css/fonts.css +4 -0
  19. package/src/css/glyphs.css +224 -0
  20. package/src/css/skrafl-explo.css +6616 -0
  21. package/src/fonts/glyphicons-regular.eot +0 -0
  22. package/src/fonts/glyphicons-regular.ttf +0 -0
  23. package/src/fonts/glyphicons-regular.woff +0 -0
  24. package/src/index.ts +2 -0
  25. package/src/messages/messages.json +1576 -0
  26. package/src/mithril/actions.ts +319 -0
  27. package/src/mithril/bag.ts +65 -0
  28. package/src/mithril/bestdisplay.ts +74 -0
  29. package/src/mithril/blankdialog.ts +94 -0
  30. package/src/mithril/board.ts +336 -0
  31. package/src/mithril/buttons.ts +303 -0
  32. package/src/mithril/challengedialog.ts +186 -0
  33. package/src/mithril/channel.ts +162 -0
  34. package/src/mithril/chat.ts +228 -0
  35. package/src/mithril/components.ts +496 -0
  36. package/src/mithril/dragdrop.ts +219 -0
  37. package/src/mithril/elopage.ts +180 -0
  38. package/src/mithril/friend.ts +227 -0
  39. package/src/mithril/game.ts +1378 -0
  40. package/src/mithril/gameview.ts +111 -0
  41. package/src/mithril/globalstate.ts +33 -0
  42. package/src/mithril/i18n.ts +186 -0
  43. package/src/mithril/localstorage.ts +133 -0
  44. package/src/mithril/login.ts +122 -0
  45. package/src/mithril/logo.ts +270 -0
  46. package/src/mithril/main.ts +737 -0
  47. package/src/mithril/mithril.ts +29 -0
  48. package/src/mithril/model.ts +817 -0
  49. package/src/mithril/movelistitem.ts +226 -0
  50. package/src/mithril/page.ts +852 -0
  51. package/src/mithril/playername.ts +91 -0
  52. package/src/mithril/promodialog.ts +82 -0
  53. package/src/mithril/recentlist.ts +148 -0
  54. package/src/mithril/request.ts +52 -0
  55. package/src/mithril/review.ts +634 -0
  56. package/src/mithril/rightcolumn.ts +398 -0
  57. package/src/mithril/searchbutton.ts +118 -0
  58. package/src/mithril/statsdisplay.ts +109 -0
  59. package/src/mithril/tabs.ts +169 -0
  60. package/src/mithril/tile.ts +145 -0
  61. package/src/mithril/twoletter.ts +76 -0
  62. package/src/mithril/types.ts +379 -0
  63. package/src/mithril/userinfodialog.ts +171 -0
  64. package/src/mithril/util.ts +304 -0
  65. package/src/mithril/wait.ts +246 -0
  66. package/src/mithril/wordcheck.ts +102 -0
  67. package/tsconfig.json +28 -0
  68. package/vite.config.ts +12 -0
@@ -0,0 +1,852 @@
1
+ /*
2
+
3
+ Page.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 UI is built on top of Mithril (https://mithril.js.org), a lightweight,
15
+ straightforward JavaScript single-page reactive UI library.
16
+
17
+ The page is structured into models, actions and views,
18
+ cf. https://github.com/pakx/the-mithril-diaries/wiki/Basic-App-Structure
19
+
20
+ */
21
+
22
+ import { mt, t, ts } from "./i18n";
23
+ import { GlobalState } from "./globalstate";
24
+ import { IView, DialogViewEnum } from "./types";
25
+ import { setServerUrl } from "./request";
26
+ import {
27
+ glyph, nbsp, getInput,
28
+ scrollMovelistToBottom, coord, toVector,
29
+ } from "./util";
30
+ import {
31
+ m, Vnode, VnodeDOM, VnodeChildren, EventHandler,
32
+ } from "./mithril";
33
+ import { logEvent, loginFirebase } from "./channel";
34
+ import { Model, getSettings } from "./model";
35
+ import { Actions, createRouteResolver } from "./actions";
36
+ import { WaitDialog, AcceptDialog } from "./wait";
37
+ import {
38
+ FriendPromoteDialog, FriendThanksDialog,
39
+ FriendCancelDialog, FriendCancelConfirmDialog
40
+ } from "./friend";
41
+ import { LoginError, LoginForm, loginUserByEmail } from "./login";
42
+ import {
43
+ DialogButton, Spinner,
44
+ UserId,
45
+ LeftLogo,
46
+ TogglerAudio,
47
+ TogglerFanfare,
48
+ TogglerBeginner,
49
+ TogglerFairplay,
50
+ TextInput
51
+ } from "./components";
52
+ import { ChallengeDialog } from "./challengedialog";
53
+ import { Main } from "./main";
54
+ import { makeTabs, selectTab, TabVnode } from "./tabs";
55
+ import { PromoDialog } from "./promodialog";
56
+ import { UserInfoDialog } from "./userinfodialog";
57
+ import { GameView } from "./gameview";
58
+ import { vwReview } from "./review";
59
+
60
+ /*
61
+ // EXPERIMENTAL
62
+ function insertStyleSheet(url: string) {
63
+ // Insert a link rel="stylesheet" element into the document head,
64
+ // if it isn't there already
65
+ const head = document.head;
66
+ if (!head) return;
67
+ const links = head.getElementsByTagName("link");
68
+ for (const link of links) {
69
+ if (link.href === url) return;
70
+ }
71
+ const link = document.createElement("link");
72
+ link.rel = "stylesheet";
73
+ link.href = url;
74
+ head.appendChild(link);
75
+ }
76
+ */
77
+
78
+ function updateFontFaceUrls(serverUrl: string) {
79
+ // Get all stylesheets in the document
80
+ const styleSheets = document.styleSheets;
81
+ // Iterate through each stylesheet
82
+ for (const styleSheet of styleSheets) {
83
+ // Iterate through each CSS rule in the stylesheet
84
+ for (const rule of styleSheet.cssRules) {
85
+ // Check if the rule is a @font-face rule
86
+ if (rule instanceof CSSFontFaceRule) {
87
+ // Update the src property with new URLs
88
+ const src = rule.style.getPropertyValue("src");
89
+ if (src.includes("glyphicons-")) {
90
+ rule.style.setProperty("src", `
91
+ url('${serverUrl}/static/glyphicons-regular.eot') format('embedded-opentype'),
92
+ url('${serverUrl}/static/glyphicons-regular.woff') format('woff'),
93
+ url('${serverUrl}/static/glyphicons-regular.ttf') format('truetype')
94
+ `);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ export type LoginResult = "success" | "expired" | "error";
102
+
103
+ export async function main(state: GlobalState, container: HTMLElement): Promise<LoginResult> {
104
+ // The main UI entry point, called from page.html
105
+
106
+ if (!container) {
107
+ console.error("No container element found");
108
+ return "error";
109
+ }
110
+ // Set up Netskrafl backend server URLs
111
+ setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
112
+
113
+ // Insert the Explo CSS stylesheet
114
+ // insertStyleSheet("./index.css");
115
+
116
+ // Update font URLs to point to the backend server
117
+ updateFontFaceUrls(state.serverUrl);
118
+
119
+ try {
120
+ const loginData = await loginUserByEmail(
121
+ state.userEmail,
122
+ state.userNick,
123
+ state.userFullname,
124
+ state.token,
125
+ );
126
+ if (loginData.status === "expired") {
127
+ // The current Málstaður JWT has expired;
128
+ // we need to obtain a new one
129
+ return "expired";
130
+ }
131
+ if (loginData.status === "success") {
132
+ state.userId = loginData.user_id;
133
+ // Log in to Firebase with the token passed from the server
134
+ await loginFirebase(state, loginData.firebase_token);
135
+ // Everything looks OK:
136
+ // Create the model, view and actions objects
137
+ const settings = getSettings();
138
+ const model = new Model(settings, state);
139
+ const view = new View(model);
140
+ const actions = new Actions(model, view);
141
+ // Run the Mithril router
142
+ const routeResolver = createRouteResolver(actions);
143
+ m.route(container, settings.defaultRoute, routeResolver);
144
+ return "success";
145
+ }
146
+ } catch(e) {
147
+ console.error("Exception during login: ", e);
148
+ }
149
+ m.mount(container, LoginError);
150
+ return "error";
151
+ }
152
+
153
+ export function unmount(container: HTMLElement) {
154
+ // Unmount the Mithril UI
155
+ m.mount(container, null);
156
+ }
157
+
158
+ type DialogFunc = (view: View, args: any) => VnodeChildren;
159
+
160
+ type DialogViews = Record<DialogViewEnum, DialogFunc>;
161
+
162
+ interface Dialog {
163
+ name: DialogViewEnum;
164
+ args: any;
165
+ }
166
+
167
+ export class View implements IView {
168
+
169
+ // The View class exposes the vwApp view function.
170
+ // Each instance maintains a current dialog window stack.
171
+
172
+ // The model that the view is attached to
173
+ model: Model;
174
+
175
+ // The currently displayed dialogs
176
+ private dialogStack: Dialog[] = [];
177
+
178
+ // Map of available dialogs
179
+ private static dialogViews: DialogViews = {
180
+ userprefs:
181
+ (view) => view.vwUserPrefs(),
182
+ userinfo:
183
+ (view, args) => view.vwUserInfo(args),
184
+ challenge:
185
+ (view, args) => m(ChallengeDialog, { view, item: args }),
186
+ promo:
187
+ (view, args) => view.vwPromo(args),
188
+ friend:
189
+ (view) => m(FriendPromoteDialog, { view }),
190
+ thanks:
191
+ (view) => m(FriendThanksDialog, { view }),
192
+ cancel:
193
+ (view) => m(FriendCancelDialog, { view }),
194
+ confirm:
195
+ (view) => m(FriendCancelConfirmDialog, { view }),
196
+ wait:
197
+ (view, args) => view.vwWait(args),
198
+ accept:
199
+ (view, args) => view.vwAccept(args)
200
+ };
201
+
202
+ // The current scaling of the board
203
+ boardScale: number = 1.0;
204
+
205
+ constructor(model: Model) {
206
+
207
+ this.model = model;
208
+
209
+ // Start a blinker interval function
210
+ window.setInterval(this.blinker, 500);
211
+
212
+ }
213
+
214
+ appView(routeName: string): VnodeChildren {
215
+ // Returns a view based on the current route.
216
+ // Displays the appropriate content for the route,
217
+ // also considering active dialogs.
218
+ const model = this.model;
219
+ let views: VnodeChildren = [];
220
+ switch (routeName) {
221
+ case "login":
222
+ // The login screen is displayed by default
223
+ views.push(this.vwLogin());
224
+ break;
225
+ case "loginerror":
226
+ views.push(m(LoginError));
227
+ break;
228
+ case "main":
229
+ views.push(m(Main, { view: this }));
230
+ break;
231
+ case "game":
232
+ views.push(m(GameView, { view: this }));
233
+ break;
234
+ case "review":
235
+ const n = vwReview(this);
236
+ n && views.push(n);
237
+ break;
238
+ case "thanks":
239
+ // Display a thank-you dialog on top of the normal main screen
240
+ views.push(m(Main, { view: this }));
241
+ // Be careful to add the Thanks dialog only once to the stack
242
+ if (!this.dialogStack.length)
243
+ this.showThanks();
244
+ break;
245
+ case "help":
246
+ // A route parameter of ?q=N goes directly to the FAQ number N
247
+ // A route parameter of ?tab=N goes directly to tab N (0-based)
248
+ views.push(
249
+ this.vwHelp(
250
+ parseInt(m.route.param("tab") || ""),
251
+ parseInt(m.route.param("faq") || "")
252
+ )
253
+ );
254
+ break;
255
+ default:
256
+ // console.log("Unknown route name: " + model.routeName);
257
+ return [ m("div", t("Þessi vefslóð er ekki rétt")) ];
258
+ }
259
+ // Push any open dialogs
260
+ for (const dialog of this.dialogStack) {
261
+ const v = View.dialogViews[dialog.name];
262
+ if (v === undefined) {
263
+ console.error("Unknown dialog name: " + dialog.name);
264
+ } else {
265
+ const n = v(this, dialog.args);
266
+ n && views.push(n);
267
+ }
268
+ }
269
+ // Overlay a spinner, if active
270
+ if (model.spinners)
271
+ views.push(m(Spinner));
272
+ return views;
273
+ }
274
+
275
+ // Dialog support
276
+
277
+ pushDialog(dialogName: DialogViewEnum, dialogArgs?: any) {
278
+ this.dialogStack.push({ name: dialogName, args: dialogArgs });
279
+ m.redraw(); // Ensure that the dialog is shown
280
+ }
281
+
282
+ popDialog() {
283
+ if (this.dialogStack.length > 0) {
284
+ this.dialogStack.pop();
285
+ m.redraw();
286
+ }
287
+ }
288
+
289
+ popAllDialogs() {
290
+ if (this.dialogStack.length > 0) {
291
+ this.dialogStack = [];
292
+ m.redraw();
293
+ }
294
+ }
295
+
296
+ isDialogShown() {
297
+ return this.dialogStack.length > 0;
298
+ }
299
+
300
+ startSpinner() {
301
+ this.model.spinners++;
302
+ }
303
+
304
+ stopSpinner() {
305
+ if (this.model.spinners) {
306
+ this.model.spinners--;
307
+ }
308
+ }
309
+
310
+ async cancelFriendship() {
311
+ // Initiate cancellation of the user's friendship
312
+ let spinner = true;
313
+ try {
314
+ this.startSpinner();
315
+ if (await this.model.cancelFriendship()) {
316
+ // Successfully cancelled the friendship
317
+ this.stopSpinner();
318
+ spinner = false;
319
+ // Show a confirmation of the cancellation
320
+ this.pushDialog("confirm", {});
321
+ }
322
+ } catch (e) {
323
+ // Simply display no confirmation in this case
324
+ }
325
+ finally {
326
+ if (spinner)
327
+ this.stopSpinner();
328
+ }
329
+ }
330
+
331
+ notifyMediaChange() {
332
+ // The view is changing, between mobile and fullscreen
333
+ // and/or between portrait and landscape: ensure that
334
+ // we don't end up with a selected game tab that is not visible
335
+ const model = this.model;
336
+ if (model.game) {
337
+ if (model.state?.uiFullscreen || model.state?.uiLandscape) {
338
+ // In this case, there is no board tab:
339
+ // show the movelist
340
+ if (model.game.setSelectedTab("movelist"))
341
+ setTimeout(scrollMovelistToBottom);
342
+ }
343
+ else {
344
+ // Mobile: we default to the board tab
345
+ model.game.setSelectedTab("board");
346
+ }
347
+ }
348
+ // When switching between landscape and portrait,
349
+ // close all current dialogs
350
+ this.popAllDialogs();
351
+ }
352
+
353
+ notifyChatMessage() {
354
+ // A fresh chat message has arrived
355
+ // and has been added to the chat message list
356
+ m.redraw();
357
+ }
358
+
359
+ resetScale() {
360
+ // Reset the board scale (zoom) to 100% and the scroll origin to (0, 0)
361
+ this.boardScale = 1.0;
362
+ const boardParent = document.getElementById("board-parent");
363
+ const board = boardParent?.children[0];
364
+ if (board)
365
+ board.setAttribute("style", "transform: scale(1.0)");
366
+ if (boardParent)
367
+ boardParent.scrollTo(0, 0);
368
+ }
369
+
370
+ updateScale() {
371
+
372
+ const model = this.model;
373
+ const game = model.game;
374
+
375
+ // Update the board scale (zoom)
376
+
377
+ function scrollIntoView(sq: string) {
378
+ // Scroll a square above and to the left of the placed tile into view
379
+ const offset = 3;
380
+ const vec = toVector(sq);
381
+ const row = Math.max(0, vec.row - offset);
382
+ const col = Math.max(0, vec.col - offset);
383
+ const c = coord(row, col);
384
+ const boardParent = document.getElementById("board-parent");
385
+ const board = boardParent?.children[0];
386
+ // The following seems to be needed to ensure that
387
+ // the transform and hence the size of the board has been
388
+ // updated in the browser, before calculating the client rects
389
+ if (board)
390
+ board.setAttribute("style", "transform: scale(1.5)");
391
+ const el = document.getElementById("sq_" + c);
392
+ const elRect = el?.getBoundingClientRect();
393
+ const boardRect = boardParent?.getBoundingClientRect();
394
+ if (boardParent && elRect && boardRect) {
395
+ boardParent.scrollTo(
396
+ {
397
+ left: elRect.left - boardRect.left,
398
+ top: elRect.top - boardRect.top,
399
+ behavior: "smooth"
400
+ }
401
+ );
402
+ }
403
+ }
404
+
405
+ if (!game || model.state?.uiFullscreen || game.moveInProgress) {
406
+ // No game or we're in full screen mode: always 100% scale
407
+ // Also, as soon as a move is being processed by the server, we zoom out
408
+ this.boardScale = 1.0; // Needs to be done before setTimeout() call
409
+ setTimeout(this.resetScale);
410
+ return;
411
+ }
412
+ const tp = game.tilesPlaced();
413
+ const numTiles = tp.length;
414
+ if (numTiles === 1 && this.boardScale === 1.0) {
415
+ // Laying down first tile: zoom in & position
416
+ this.boardScale = 1.5;
417
+ setTimeout(() => scrollIntoView(tp[0]));
418
+ }
419
+ else if (numTiles === 0 && this.boardScale > 1.0) {
420
+ // Removing only remaining tile: zoom out
421
+ this.boardScale = 1.0; // Needs to be done before setTimeout() call
422
+ setTimeout(() => this.resetScale());
423
+ }
424
+ }
425
+
426
+ showUserInfo(userid: string, nick: string, fullname: string) {
427
+ // Show a user info dialog
428
+ this.pushDialog("userinfo", { userid: userid, nick: nick, fullname: fullname });
429
+ }
430
+
431
+ showFriendPromo() {
432
+ // Show a friendship promotion
433
+ this.pushDialog("friend", { });
434
+ }
435
+
436
+ showThanks() {
437
+ // Show thanks for becoming a friend
438
+ this.pushDialog("thanks", { });
439
+ }
440
+
441
+ showFriendCancel() {
442
+ // Show a friendship cancellation dialog
443
+ this.pushDialog("cancel", { });
444
+ }
445
+
446
+ showAcceptDialog(oppId: string, oppNick: string, challengeKey: string) {
447
+ this.pushDialog("accept", { oppId, oppNick, challengeKey });
448
+ }
449
+
450
+ // Globally available view functions
451
+
452
+ vwDialogButton(
453
+ id: string, title: string, func: EventHandler,
454
+ content: VnodeChildren, tabindex: number
455
+ ): VnodeChildren {
456
+ // Create a .modal-close dialog button
457
+ const attrs = {
458
+ id: id,
459
+ onclick: func,
460
+ title,
461
+ tabindex
462
+ };
463
+ return m(DialogButton, attrs, content);
464
+ }
465
+
466
+ blinker() {
467
+ // Toggle the 'over' class on all elements having the 'blinking' class
468
+ const blinkers = document.getElementsByClassName('blinking');
469
+ for (let b of blinkers)
470
+ b.classList.toggle("over");
471
+ }
472
+
473
+ // A control that rigs up a tabbed view of raw HTML
474
+
475
+ vwTabsFromHtml(
476
+ html: string, id: string, tabNumber: number, createFunc: (vnode: TabVnode) => void
477
+ ): VnodeChildren {
478
+ // The function assumes that 'this' is the current view object
479
+ if (!html)
480
+ return "";
481
+ return m("div",
482
+ {
483
+ oninit: (vnode) => { vnode.state.selected = tabNumber || 1; },
484
+ oncreate: (vnode) => { makeTabs(this, id, createFunc, true, vnode); }
485
+ /* onupdate: updateSelection */
486
+ },
487
+ m.trust(html)
488
+ );
489
+ }
490
+
491
+ // Help screen
492
+
493
+ vwHelp(tabNumber: number, faqNumber: number): Vnode {
494
+
495
+ const model = this.model;
496
+
497
+ function wireQuestions(vnode: TabVnode) {
498
+ // Clicking on a question brings the corresponding answer into view
499
+ // This is achieved by wiring up all contained a[href="#faq-*"] links
500
+
501
+ function showAnswer(ev: Event, href: string) {
502
+ // this points to the vnode
503
+ vnode.state.selected = 1; // FAQ tab
504
+ vnode.dom.querySelector(href)?.scrollIntoView();
505
+ ev.preventDefault();
506
+ }
507
+
508
+ const anchors = vnode.dom.querySelectorAll("a");
509
+ for (const anchor of anchors) {
510
+ const href = anchor.getAttribute("href");
511
+ if (href && href.slice(0, 5) == "#faq-")
512
+ // This is a direct link to a question: wire it up
513
+ anchor.onclick = (ev) => { showAnswer(ev, href); };
514
+ }
515
+ if (faqNumber !== undefined && !isNaN(faqNumber)) {
516
+ // Go to the FAQ tab and scroll the requested question into view
517
+ selectTab(vnode, 1);
518
+ vnode.state.selected = 1; // FAQ tab
519
+ vnode.dom.querySelector("#faq-" + faqNumber.toString())?.scrollIntoView();
520
+ }
521
+ }
522
+
523
+ // Output literal HTML obtained from rawhelp.html on the server
524
+ return m.fragment({}, [
525
+ m(LeftLogo),
526
+ m(UserId, { view: this }),
527
+ this.vwTabsFromHtml(model.helpHTML || "", "tabs", tabNumber, wireQuestions),
528
+ ]);
529
+ }
530
+
531
+ // User preferences screen
532
+
533
+ vwUserPrefsDialog(): VnodeChildren {
534
+
535
+ const model = this.model;
536
+ if (!model.user) return undefined;
537
+ const user = model.user;
538
+ if (!model.state) return undefined;
539
+ const state = model.state;
540
+ const err = model.userErrors || {};
541
+ const view = this;
542
+
543
+ function vwErrMsg(propname: keyof typeof err) {
544
+ // Show a validation error message returned from the server
545
+ return err.hasOwnProperty(propname) ?
546
+ m(".errinput", [glyph("arrow-up"), nbsp(), err[propname] || ""]) : "";
547
+ }
548
+
549
+ function getToggle(elemId: string): boolean {
550
+ const cls2 = document.querySelector("#" + elemId + "-toggler #opt2")?.classList;
551
+ if (!cls2) return false;
552
+ return cls2.contains("selected");
553
+ }
554
+
555
+ function validate() {
556
+ // Move data back to the model.user object
557
+ // before sending it to the server
558
+ user.nickname = getInput("nickname");
559
+ user.full_name = getInput("full_name");
560
+ user.audio = getToggle("audio");
561
+ user.fanfare = getToggle("fanfare");
562
+ user.beginner = getToggle("beginner");
563
+ user.fairplay = getToggle("fairplay");
564
+ // When done, pop the current dialog
565
+ model.saveUser(() => { view.popDialog(); });
566
+ }
567
+
568
+ function initFocus(vnode: VnodeDOM) {
569
+ // Set the focus on the nickname field when the dialog is displayed
570
+ (vnode.dom.querySelector("#nickname") as HTMLElement).focus();
571
+ }
572
+
573
+ return m(".modal-dialog",
574
+ {
575
+ id: "user-dialog",
576
+ oncreate: initFocus
577
+ // onupdate: initFocus
578
+ },
579
+ m(".ui-widget.ui-widget-content.ui-corner-all", { id: 'user-form' },
580
+ [
581
+ m(".loginhdr", [glyph("address-book"), " " + ts("player_info")]), // "Player information"
582
+ m("div",
583
+ m("form", { action: '', id: 'frm1', method: 'post', name: 'frm1' },
584
+ [
585
+ m(".dialog-spacer",
586
+ [
587
+ m("span.caption", t("Einkenni:")),
588
+ m(TextInput,
589
+ {
590
+ initialValue: user.nickname || "",
591
+ class: "username",
592
+ maxlength: 15,
593
+ id: "nickname",
594
+ // autocomplete: "nickname", // Chrome doesn't like this
595
+ }
596
+ ),
597
+ nbsp(), m("span.asterisk", "*")
598
+ ]
599
+ ),
600
+ m(".explain", t("Verður að vera útfyllt")),
601
+ vwErrMsg("nickname"),
602
+ m(".dialog-spacer",
603
+ [
604
+ m("span.caption", t("Fullt nafn:")),
605
+ m(TextInput,
606
+ {
607
+ initialValue: user.full_name || "",
608
+ class: "fullname",
609
+ maxlength: 32,
610
+ id: "full_name",
611
+ autocomplete: "name",
612
+ }
613
+ )
614
+ ]
615
+ ),
616
+ m(".explain", t("Valfrjálst - sýnt í notendalistum Netskrafls")),
617
+ vwErrMsg("full_name"),
618
+ m(".dialog-spacer",
619
+ [
620
+ m("span.caption.sub", t("Hljóðmerki:")),
621
+ m(TogglerAudio, { view, state: user.audio, tabindex: 4 }),
622
+ m("span.subcaption", t("Lúðraþytur eftir sigur:")),
623
+ m(TogglerFanfare, { view, state: user.fanfare, tabindex: 5 }),
624
+ ]
625
+ ),
626
+ m(".explain", t("explain_sound")),
627
+ m(".dialog-spacer",
628
+ [
629
+ m("span.caption.sub", t("Sýna reitagildi:")),
630
+ m(TogglerBeginner, { view, state: user.beginner, tabindex: 6 }),
631
+ mt(".subexplain",
632
+ [
633
+ "Stillir hvort ",
634
+ mt("strong", "minnismiði"),
635
+ " um margföldunargildi reita er sýndur við borðið"
636
+ ]
637
+ )
638
+ ]
639
+ ),
640
+ m(".dialog-spacer",
641
+ [
642
+ m("span.caption.sub", t("Án hjálpartækja:")),
643
+ m(TogglerFairplay, { view, state: user.fairplay, tabindex: 7 }),
644
+ mt(".subexplain",
645
+ [
646
+ "no_helpers",
647
+ mt("strong", "án stafrænna hjálpartækja"),
648
+ " af nokkru tagi"
649
+ ]
650
+ )
651
+ ]
652
+ )
653
+ ]
654
+ )
655
+ ),
656
+ this.vwDialogButton("user-ok", ts("Vista"), validate, glyph("ok"), 8),
657
+ this.vwDialogButton("user-cancel", ts("Hætta við"),
658
+ (ev) => { this.popDialog(); ev.preventDefault(); },
659
+ glyph("remove"), 9),
660
+ user.friend ?
661
+ this.vwDialogButton("user-unfriend", ts("Hætta sem vinur"),
662
+ (ev) => {
663
+ ev.preventDefault();
664
+ view.showFriendCancel()
665
+ },
666
+ [glyph("coffee-cup"), nbsp(), nbsp(), ts("Þú ert vinur Netskrafls!")], 10
667
+ )
668
+ :
669
+ this.vwDialogButton("user-friend", ts("Gerast vinur"),
670
+ (ev) => {
671
+ // Invoke the friend promo dialog
672
+ ev.preventDefault();
673
+ logEvent("click_friend",
674
+ {
675
+ userid: state.userId, locale: state.locale
676
+ }
677
+ );
678
+ view.showFriendPromo();
679
+ },
680
+ [glyph("coffee-cup"), nbsp(), nbsp(), ts("Gerast vinur Netskrafls")], 11
681
+ )
682
+ ]
683
+ )
684
+ );
685
+ }
686
+
687
+ vwUserPrefs(): VnodeChildren {
688
+ const model = this.model;
689
+ if (model.user === null && !model.userLoadError)
690
+ model.loadUser(true); // Activate spinner while loading
691
+ if (!model.user)
692
+ // Nothing to edit (the spinner should be showing in this case)
693
+ return m.fragment({}, []);
694
+ return this.vwUserPrefsDialog();
695
+ }
696
+
697
+ vwUserInfo(args: { userid: string; nick: string; fullname: string; }): VnodeChildren {
698
+ return m(UserInfoDialog,
699
+ {
700
+ view: this,
701
+ userid: args.userid,
702
+ nick: args.nick,
703
+ fullname: args.fullname
704
+ }
705
+ );
706
+ }
707
+
708
+ vwPromo(args: { kind: string; initFunc: () => void; }): VnodeChildren {
709
+ return m(PromoDialog,
710
+ {
711
+ view: this,
712
+ kind: args.kind,
713
+ initFunc: args.initFunc
714
+ }
715
+ );
716
+ }
717
+
718
+ vwWait(args: {
719
+ oppId: string;
720
+ oppNick: string;
721
+ oppName: string;
722
+ duration: number;
723
+ challengeKey: string;
724
+ }): VnodeChildren {
725
+ return m(WaitDialog, {
726
+ view: this,
727
+ oppId: args.oppId,
728
+ oppNick: args.oppNick,
729
+ oppName: args.oppName,
730
+ duration: args.duration,
731
+ challengeKey: args.challengeKey,
732
+ });
733
+ }
734
+
735
+ vwAccept(args: { oppId: string; oppNick: string; challengeKey: string; }): VnodeChildren {
736
+ return m(AcceptDialog, {
737
+ view: this,
738
+ oppId: args.oppId,
739
+ oppNick: args.oppNick,
740
+ challengeKey: args.challengeKey,
741
+ });
742
+ }
743
+
744
+ vwLogin(): VnodeChildren {
745
+ const model = this.model;
746
+ const loginUrl = model.state?.loginUrl || "";
747
+ return m(LoginForm, { loginUrl });
748
+ }
749
+
750
+ vwDialogs(): VnodeChildren {
751
+ // Show prompt dialogs below game board, if any
752
+ const game = this.model.game;
753
+ let r: Vnode[] = [];
754
+ if (!game || game.showingDialog === null && !game.last_chall)
755
+ return r;
756
+ // The dialogs below, specifically the challenge and pass
757
+ // dialogs, have priority over the last_chall dialog - since
758
+ // they can be invoked while the last_chall dialog is being
759
+ // displayed. We therefore allow them to cover the last_chall
760
+ // dialog. On mobile, both dialogs are displayed simultaneously.
761
+ if (game.last_chall)
762
+ r.push(m(".chall-info", { style: { visibility: "visible" } },
763
+ [
764
+ glyph("info-sign"), nbsp(),
765
+ // "Your opponent emptied the rack - you can challenge or pass"
766
+ mt("span.pass-explain", "opponent_emptied_rack")
767
+ ]
768
+ ));
769
+ if (game.showingDialog == "resign")
770
+ r.push(m(".resign", { style: { visibility: "visible" } },
771
+ [
772
+ glyph("exclamation-sign"), nbsp(), ts("Viltu gefa leikinn?"), nbsp(),
773
+ m("span.mobile-break", m("br")),
774
+ m("span.yesnobutton", { onclick: () => game.confirmResign(true) },
775
+ [glyph("ok"), ts(" Já")]
776
+ ),
777
+ m("span.mobile-space"),
778
+ m("span.yesnobutton", { onclick: () => game.confirmResign(false) },
779
+ [glyph("remove"), ts(" Nei")]
780
+ )
781
+ ]
782
+ ));
783
+ if (game.showingDialog == "pass") {
784
+ if (game.last_chall)
785
+ r.push(m(".pass-last", { style: { visibility: "visible" } },
786
+ [
787
+ glyph("forward"), nbsp(), ts("Segja pass?"),
788
+ mt("span.pass-explain", "Viðureign lýkur þar með"),
789
+ nbsp(),
790
+ m("span.mobile-break", m("br")),
791
+ m("span.yesnobutton", { onclick: () => game.confirmPass(true) },
792
+ [glyph("ok"), ts(" Já")]
793
+ ),
794
+ m("span.mobile-space"),
795
+ m("span.yesnobutton", { onclick: () => game.confirmPass(false) },
796
+ [glyph("remove"), ts(" Nei")]
797
+ )
798
+ ]
799
+ ));
800
+ else
801
+ r.push(m(".pass", { style: { visibility: "visible" } },
802
+ [
803
+ glyph("forward"), nbsp(), ts("Segja pass?"),
804
+ mt("span.pass-explain", "2x3 pöss í röð ljúka viðureign"),
805
+ nbsp(), m("span.mobile-break", m("br")),
806
+ m("span.yesnobutton", { onclick: () => game.confirmPass(true) },
807
+ [glyph("ok"), ts(" Já")]
808
+ ),
809
+ m("span.mobile-space"),
810
+ m("span.yesnobutton", { onclick: () => game.confirmPass(false) },
811
+ [glyph("remove"), ts(" Nei")]
812
+ )
813
+ ]
814
+ ));
815
+ }
816
+ if (game.showingDialog == "exchange")
817
+ r.push(m(".exchange", { style: { visibility: "visible" } },
818
+ [
819
+ glyph("refresh"), nbsp(),
820
+ ts("Smelltu á flísarnar sem þú vilt skipta"), nbsp(),
821
+ m("span.mobile-break", m("br")),
822
+ m("span.yesnobutton",
823
+ { title: ts('Skipta'), onclick: () => game.confirmExchange(true) },
824
+ glyph("ok")
825
+ ),
826
+ m("span.mobile-space"),
827
+ m("span.yesnobutton",
828
+ { title: ts('Hætta við'), onclick: () => game.confirmExchange(false) },
829
+ glyph("remove"))
830
+ ]
831
+ ));
832
+ if (game.showingDialog == "chall")
833
+ r.push(m(".chall", { style: { visibility: "visible" } },
834
+ [
835
+ glyph("ban-circle"), nbsp(), ts("Véfengja lögn?"),
836
+ mt("span.pass-explain", "Röng véfenging kostar 10 stig"), nbsp(),
837
+ m("span.mobile-break", m("br")),
838
+ m("span.yesnobutton",
839
+ { onclick: () => game.confirmChallenge(true) },
840
+ [glyph("ok"), ts(" Já")]
841
+ ),
842
+ m("span.mobile-space"),
843
+ m("span.yesnobutton",
844
+ { onclick: () => game.confirmChallenge(false) },
845
+ [glyph("remove"), ts(" Nei")]
846
+ )
847
+ ]
848
+ ));
849
+ return r;
850
+ }
851
+
852
+ } // class View