@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,169 @@
1
+ /*
2
+ Tabs.ts
3
+
4
+ Utility functions to set up tabbed views
5
+
6
+ Copyright (C) 2024 Miðeind ehf.
7
+ Author: Vilhjalmur Thorsteinsson
8
+
9
+ The Creative Commons Attribution-NonCommercial 4.0
10
+ International Public License (CC-BY-NC 4.0) applies to this software.
11
+ For further information, see https://github.com/mideind/Netskrafl
12
+
13
+ */
14
+
15
+ import { IView } from "./types";
16
+ import { m } from "./mithril";
17
+ import { getUrlVars } from "./util";
18
+
19
+ const ROUTE_PREFIX = "/page#!";
20
+ const ROUTE_PREFIX_LEN = ROUTE_PREFIX.length;
21
+
22
+ export type TabVnode = m.VnodeDOM<
23
+ Record<string, any>,
24
+ {
25
+ selected: number;
26
+ lis: HTMLElement[];
27
+ ids: string[];
28
+ }
29
+ >;
30
+
31
+ export function makeTabs(view: IView, id: string, createFunc: ((vnode: TabVnode) => void) | undefined, wireHrefs: boolean, vnode: TabVnode) {
32
+ // When the tabs are displayed for the first time, wire'em up
33
+ let tabdiv = document.getElementById(id);
34
+ if (!tabdiv)
35
+ return;
36
+ // Add bunch of jQueryUI compatible classes
37
+ tabdiv.setAttribute("class", "ui-tabs ui-widget ui-widget-content ui-corner-all");
38
+ const tabul = document.querySelector("#" + id + " > ul");
39
+ if (tabul) {
40
+ tabul.setAttribute("class", "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");
41
+ tabul.setAttribute("role", "tablist");
42
+ }
43
+ let tablist = document.querySelectorAll("#" + id + " > ul > li > a") as NodeListOf<HTMLElement>;
44
+ let tabitems = document.querySelectorAll("#" + id + " > ul > li") as NodeListOf<HTMLElement>;
45
+ let ids: string[] = [];
46
+ let lis: HTMLElement[] = []; // The <li> elements
47
+ // Iterate over the <a> elements inside the <li> elements inside the <ul>
48
+ for (let i = 0; i < tablist.length; i++) {
49
+ const href = tablist[i].getAttribute("href");
50
+ if (!href) continue;
51
+ ids.push(href.slice(1));
52
+ // Decorate the <a> elements
53
+ tablist[i].onclick = (ev) => { selectTab(vnode, i); ev.preventDefault(); };
54
+ tablist[i].removeAttribute("href");
55
+ tablist[i].setAttribute("class", "ui-tabs-anchor sp"); // Single-page marker
56
+ tablist[i].setAttribute("role", "presentation");
57
+ // Also decorate the <li> elements
58
+ lis.push(tabitems[i]);
59
+ tabitems[i].setAttribute("class", "ui-state-default ui-corner-top");
60
+ tabitems[i].setAttribute("role", "tab");
61
+ tabitems[i].onmouseover = (ev) => {
62
+ (ev.currentTarget as HTMLElement).classList.toggle("ui-state-hover", true);
63
+ };
64
+ tabitems[i].onmouseout = (ev) => {
65
+ (ev.currentTarget as HTMLElement).classList.toggle("ui-state-hover", false);
66
+ };
67
+ // Find the tab's content <div>
68
+ const tabcontent = document.getElementById(ids[i]);
69
+ // Decorate it
70
+ if (tabcontent) {
71
+ tabcontent.setAttribute("class", "ui-tabs-panel ui-widget-content ui-corner-bottom");
72
+ tabcontent.setAttribute("role", "tabpanel");
73
+ }
74
+ }
75
+ // Save the list of tab identifiers
76
+ vnode.state.ids = ids;
77
+ // Save the list of <li> elements
78
+ vnode.state.lis = lis;
79
+ // Select the first tab by default
80
+ vnode.state.selected = 0;
81
+ if (wireHrefs) {
82
+ // Wire all hrefs that point to single-page URLs
83
+ const model = view.model;
84
+ const clickURL = (ev: Event, href: string) => {
85
+ let uri = href.slice(ROUTE_PREFIX_LEN); // Cut the /page#!/ prefix off the route
86
+ let qix = uri.indexOf("?");
87
+ let route = (qix >= 0) ? uri.slice(0, qix) : uri;
88
+ let qparams = uri.slice(route.length + 1);
89
+ let params = qparams.length ? getUrlVars(qparams) : {};
90
+ m.route.set(route, params);
91
+ if (window.history)
92
+ window.history.pushState({}, "", href); // Enable the back button
93
+ ev.preventDefault();
94
+ };
95
+ const clickUserPrefs = (ev: Event) => {
96
+ if (model?.state?.userId)
97
+ // Don't show the userprefs if no user logged in
98
+ view.pushDialog("userprefs");
99
+ ev.preventDefault();
100
+ };
101
+ const clickTwoLetter = (ev: Event) => {
102
+ selectTab(vnode, 2); // Select tab number 2
103
+ ev.preventDefault();
104
+ };
105
+ const clickNewBag = (ev: Event) => {
106
+ selectTab(vnode, 3); // Select tab number 3
107
+ ev.preventDefault();
108
+ };
109
+ let anchors = tabdiv.querySelectorAll("a");
110
+ for (let i = 0; i < anchors.length; i++) {
111
+ let a = anchors[i];
112
+ let href = a.getAttribute("href");
113
+ if (href && href.slice(0, ROUTE_PREFIX_LEN) == ROUTE_PREFIX) {
114
+ // Single-page URL: wire it up (as if it had had an m.route.Link on it)
115
+ a.onclick = (ev) => clickURL(ev, href);
116
+ }
117
+ else
118
+ if (href && href === "$$userprefs$$") {
119
+ // Special marker indicating that this link invokes
120
+ // a user preference dialog
121
+ a.onclick = clickUserPrefs;
122
+ }
123
+ else
124
+ if (href && href === "$$twoletter$$") {
125
+ // Special marker indicating that this link invokes
126
+ // the two-letter word list or the opponents tab
127
+ a.onclick = clickTwoLetter;
128
+ }
129
+ else
130
+ if (href && href === "$$newbag$$") {
131
+ // Special marker indicating that this link invokes
132
+ // the explanation of the new bag
133
+ a.onclick = clickNewBag;
134
+ }
135
+ }
136
+ }
137
+ // If a createFunc was specified, run it now
138
+ if (createFunc)
139
+ createFunc(vnode);
140
+ // Finally, make the default tab visible and hide the others
141
+ updateTabVisibility(vnode);
142
+ }
143
+
144
+ function updateTabVisibility(vnode: TabVnode) {
145
+ // Shows the tab that is currently selected,
146
+ // i.e. the one whose index is in vnode.state.selected
147
+ const selected: number = vnode.state.selected;
148
+ const lis = vnode.state.lis;
149
+ vnode.state.ids.map((id: string, i: number) => {
150
+ document.getElementById(id)?.setAttribute("style", "display: " +
151
+ (i == selected ? "block" : "none"));
152
+ lis[i].classList.toggle("ui-tabs-active", i === selected);
153
+ lis[i].classList.toggle("ui-state-active", i === selected);
154
+ }
155
+ );
156
+ }
157
+
158
+ export function selectTab(vnode: TabVnode, i: number) {
159
+ // Selects the tab with the given index under the tab control vnode
160
+ vnode.state.selected = i;
161
+ updateTabVisibility(vnode);
162
+ }
163
+
164
+ export function updateSelection(vnode: TabVnode) {
165
+ // Select a tab according to the ?tab= query parameter in the current route
166
+ var tab = m.route.param("tab");
167
+ if (tab !== undefined)
168
+ selectTab(vnode, parseInt(tab) || 0);
169
+ }
@@ -0,0 +1,145 @@
1
+ /*
2
+
3
+ Tile.ts
4
+
5
+ Tile component
6
+
7
+ Copyright (C) 2025 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 { EXTRA_WIDE_LETTERS, IView, WIDE_LETTERS } from "./types";
17
+ import { VnodeAttrs, ComponentFunc, m, MithrilMouseEvent, MithrilTouchEvent } from "./mithril";
18
+ import { nbsp } from "./util";
19
+ import { startDrag } from "./dragdrop";
20
+
21
+ interface IAttributes {
22
+ view: IView;
23
+ coord: string;
24
+ opponent: boolean;
25
+ }
26
+
27
+ export const Tile: ComponentFunc<IAttributes> = (initialVnode) => {
28
+ // Display a tile on the board or in the rack
29
+ const { view, coord, opponent } = initialVnode.attrs;
30
+ const model = view.model;
31
+
32
+ const dragHandler = (ev: MithrilMouseEvent | MithrilTouchEvent) => {
33
+ // Start a drag-and-drop process, for mouse or touch interaction
34
+ startDrag(ev, (_, target) => {
35
+ // Drop handler
36
+ const game = model.game;
37
+ if (!game) return;
38
+ const id = target.id;
39
+ if (!id) return;
40
+ let targetCoord: string = "";
41
+ if (id === "board-background") {
42
+ // Drop on the background: transfer back to the rack
43
+ targetCoord = "R1";
44
+ } else if (id.startsWith("sq_")) {
45
+ // Drop on a board square
46
+ targetCoord = id.slice(3);
47
+ }
48
+ if (targetCoord) {
49
+ try {
50
+ game.attemptMove(coord, targetCoord);
51
+ view.updateScale();
52
+ // Make sure that Mithril draws the updated state
53
+ m.redraw();
54
+ } catch (e) {
55
+ // Something went wrong: display a console error
56
+ console.error(e);
57
+ }
58
+ }
59
+ });
60
+ ev.redraw = false;
61
+ return false;
62
+ };
63
+
64
+ return {
65
+ view: () => {
66
+ const game = model.game;
67
+ if (!game) return undefined;
68
+ const isRackTile = coord[0] === 'R';
69
+ // A single tile, on the board or in the rack
70
+ const t = game.tiles[coord];
71
+ let classes = [".tile"];
72
+ let attrs: VnodeAttrs = {};
73
+ if (t.tile === '?')
74
+ classes.push("blanktile");
75
+ if (EXTRA_WIDE_LETTERS.includes(t.letter))
76
+ // Extra wide letter: handle specially
77
+ classes.push("extra-wide");
78
+ else if (WIDE_LETTERS.includes(t.letter))
79
+ // Wide letter: handle specially
80
+ classes.push("wide");
81
+ if (isRackTile || t.draggable) {
82
+ // Rack tile, or at least a draggable one
83
+ classes.push(opponent ? "freshtile" : "racktile");
84
+ if (isRackTile && game.showingDialog === "exchange") {
85
+ // Rack tile, and we're showing the exchange dialog
86
+ if (t.xchg)
87
+ // Chosen as an exchange tile
88
+ classes.push("xchgsel");
89
+ // Exchange dialog is live: add a click handler for the
90
+ // exchange state
91
+ attrs.onclick = (ev: Event) => {
92
+ // Toggle the exchange status of this tile
93
+ t.xchg = !t.xchg;
94
+ ev.preventDefault();
95
+ };
96
+ }
97
+ }
98
+ if (t.freshtile) {
99
+ // A fresh tile on the board that has
100
+ // just been played by the opponent
101
+ classes.push("freshtile");
102
+ }
103
+ if (t.index) {
104
+ // Make fresh or highlighted tiles appear sequentally by animation
105
+ const ANIMATION_STEP = 150; // Milliseconds
106
+ const delay = (t.index * ANIMATION_STEP).toString() + "ms";
107
+ attrs.style = `animation-delay: ${delay}`;
108
+ }
109
+ if (coord === game.selectedSq)
110
+ // Currently selected square
111
+ classes.push("sel"); // Blinks red
112
+ if (t.highlight !== undefined) {
113
+ // highlight0 is the local player color
114
+ // highlight1 is the remote player color
115
+ classes.push("highlight" + t.highlight);
116
+ /*
117
+ if (t.player == parseInt(t.highlight))
118
+ // This tile was originally laid down by the other player
119
+ classes.push("dim");
120
+ */
121
+ }
122
+ if (game.showingDialog === null && !game.over) {
123
+ if (t.draggable) {
124
+ attrs.onmousedown = dragHandler;
125
+ attrs.ontouchstart = dragHandler;
126
+ /*
127
+ attrs.onclick = (ev: MouseEvent) => {
128
+ // When clicking a tile, make it selected (blinking)
129
+ if (coord === game.selectedSq)
130
+ // Clicking again: deselect
131
+ game.selectedSq = null;
132
+ else
133
+ game.selectedSq = coord;
134
+ ev.stopPropagation();
135
+ return false;
136
+ };
137
+ */
138
+ }
139
+ }
140
+ return m(classes.join("."), attrs,
141
+ [t.letter === " " ? nbsp() : t.letter, m(".letterscore", t.score)]
142
+ );
143
+ }
144
+ };
145
+ }
@@ -0,0 +1,76 @@
1
+ /*
2
+
3
+ Twoletter.ts
4
+
5
+ Two Letter Word 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 { Vnode, ComponentFunc, m, VnodeChildren } from "./mithril";
18
+ import { ts } from "./i18n";
19
+
20
+ interface IAttributes {
21
+ view: IView;
22
+ }
23
+
24
+ export const TwoLetter: ComponentFunc<IAttributes> = (initialVnode) => {
25
+
26
+ // The two-letter-word list tab
27
+ const view = initialVnode.attrs.view;
28
+ const model = view.model;
29
+ let page = 0;
30
+
31
+ function renderWord(bold: boolean, w: string): Vnode {
32
+ // For the first two-letter word in each group,
33
+ // render the former letter in bold
34
+ if (!bold)
35
+ return m(".twoletter-word", w);
36
+ if (page == 0)
37
+ return m(".twoletter-word", [m("b", w[0]), w[1]]);
38
+ else
39
+ return m(".twoletter-word", [w[0], m("b", w[1])]);
40
+ }
41
+
42
+ return {
43
+ view: () => {
44
+ if (!model.game) return;
45
+ const game = model.game;
46
+ const twoLetters = game.twoLetterWords();
47
+ const twoLetterWords = twoLetters[page];
48
+ const twoLetterList: VnodeChildren = [];
49
+ for (const tw of twoLetterWords) {
50
+ const twl = tw[1];
51
+ const sublist: VnodeChildren = [];
52
+ for (let j = 0; j < twl.length; j++)
53
+ sublist.push(renderWord(j == 0, twl[j]));
54
+ twoLetterList.push(
55
+ m(".twoletter-group", sublist)
56
+ );
57
+ }
58
+ return m(".twoletter",
59
+ {
60
+ // Switch between pages when clicked
61
+ onclick: () => { page = 1 - page; },
62
+ style: "z-index: 6" // Appear on top of board on mobile
63
+ },
64
+ // Show the requested page
65
+ m(".twoletter-area" + (game.showClock() ? ".with-clock" : ""),
66
+ {
67
+ title: page == 0 ?
68
+ ts("Smelltu til að raða eftir seinni staf") :
69
+ ts("Smelltu til að raða eftir fyrri staf")
70
+ },
71
+ twoLetterList
72
+ )
73
+ );
74
+ }
75
+ };
76
+ };