@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,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Gameview.ts
|
|
4
|
+
|
|
5
|
+
Game view 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 { IView } from "./types";
|
|
17
|
+
import { t, ts } from "./i18n";
|
|
18
|
+
import { glyph } from "./util";
|
|
19
|
+
import { ComponentFunc, m } from "./mithril";
|
|
20
|
+
import { Info, LeftLogo } from "./components";
|
|
21
|
+
import { BlankDialog } from "./blankdialog";
|
|
22
|
+
import { Bag } from "./bag";
|
|
23
|
+
import { BoardArea } from "./board";
|
|
24
|
+
import { RightColumn } from "./rightcolumn";
|
|
25
|
+
|
|
26
|
+
const Beginner: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
27
|
+
// Show the board color guide
|
|
28
|
+
const view = initialVnode.attrs.view;
|
|
29
|
+
const model = view.model;
|
|
30
|
+
const state = model.state;
|
|
31
|
+
return {
|
|
32
|
+
view: () => m(".board-help",
|
|
33
|
+
{ title: ts("Hvernig reitirnir margfalda stigin") },
|
|
34
|
+
[
|
|
35
|
+
m(".board-help-close",
|
|
36
|
+
{
|
|
37
|
+
title: ts("Loka þessari hjálp"),
|
|
38
|
+
onclick: (ev: Event) => {
|
|
39
|
+
// Close the guide and set a preference not to see it again
|
|
40
|
+
if (state) {
|
|
41
|
+
state.beginner = false;
|
|
42
|
+
model.setUserPref({ beginner: false });
|
|
43
|
+
}
|
|
44
|
+
ev.preventDefault();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
glyph("remove")
|
|
48
|
+
),
|
|
49
|
+
m(".board-colors",
|
|
50
|
+
[
|
|
51
|
+
m(".board-color[id='triple-word']", ["3 x", m("br"), t("orð")]),
|
|
52
|
+
m(".board-color[id='double-word']", ["2 x", m("br"), t("orð")]),
|
|
53
|
+
m(".board-color[id='triple-letter']", ["3 x", m("br"), t("stafur")]),
|
|
54
|
+
m(".board-color[id='double-letter']", ["2 x", m("br"), t("stafur")]),
|
|
55
|
+
m(".board-color[id='single-letter']", ["1 x", m("br"), t("stafur")])
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
interface IAttributes {
|
|
64
|
+
view: IView;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const GameView: ComponentFunc<IAttributes> = () => {
|
|
68
|
+
// A view of a game, in-progress or finished
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
view: (vnode) => {
|
|
72
|
+
const view = vnode.attrs.view;
|
|
73
|
+
const model = view.model;
|
|
74
|
+
const game = model.game;
|
|
75
|
+
if (!game) {
|
|
76
|
+
// No associated game
|
|
77
|
+
return m("div", [m(".game-container"), m(LeftLogo)]);
|
|
78
|
+
}
|
|
79
|
+
const bag = game.bag;
|
|
80
|
+
const newbag = game.newbag;
|
|
81
|
+
const state = model.state;
|
|
82
|
+
return m(
|
|
83
|
+
// Allow tiles to be dropped on the background
|
|
84
|
+
// by marking the div with the drop-target class.
|
|
85
|
+
// Such a drop sends the tile back into the rack.
|
|
86
|
+
"div.drop-target",
|
|
87
|
+
{
|
|
88
|
+
id: "board-background",
|
|
89
|
+
},
|
|
90
|
+
[
|
|
91
|
+
// The main game area
|
|
92
|
+
m(".game-container",
|
|
93
|
+
[
|
|
94
|
+
m(RightColumn, { view }),
|
|
95
|
+
m(BoardArea, { view }),
|
|
96
|
+
// The bag is visible in fullscreen
|
|
97
|
+
state?.uiFullscreen ? m(Bag, { bag: bag, newbag: newbag }) : "",
|
|
98
|
+
game.askingForBlank ? m(BlankDialog, { view }) : ""
|
|
99
|
+
]
|
|
100
|
+
),
|
|
101
|
+
// The left margin stuff: back button, square color help, info/help button
|
|
102
|
+
// These elements appear after the game container, since we want
|
|
103
|
+
// them to be above it in the z-order
|
|
104
|
+
m(LeftLogo),
|
|
105
|
+
state?.beginner ? m(Beginner, { view }) : "",
|
|
106
|
+
m(Info),
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
// Global state, for the most part obtained from the server
|
|
3
|
+
// when the single page UI is initialized (i.e. from page.html)
|
|
4
|
+
export interface GlobalState {
|
|
5
|
+
projectId: string;
|
|
6
|
+
databaseURL: string;
|
|
7
|
+
firebaseSenderId: string;
|
|
8
|
+
firebaseAppId: string;
|
|
9
|
+
firebaseAPIKey: string;
|
|
10
|
+
measurementId: string;
|
|
11
|
+
userEmail: string;
|
|
12
|
+
userId: string;
|
|
13
|
+
userNick: string;
|
|
14
|
+
userFullname: string;
|
|
15
|
+
locale: string;
|
|
16
|
+
isExplo: boolean;
|
|
17
|
+
serverUrl: string;
|
|
18
|
+
movesUrl: string;
|
|
19
|
+
movesAccessKey: string;
|
|
20
|
+
token: string;
|
|
21
|
+
loginUrl: string;
|
|
22
|
+
loginMethod: string;
|
|
23
|
+
newUser: boolean;
|
|
24
|
+
beginner: boolean;
|
|
25
|
+
fairPlay: boolean;
|
|
26
|
+
plan: string;
|
|
27
|
+
hasPaid: boolean;
|
|
28
|
+
ready: boolean;
|
|
29
|
+
readyTimed: boolean;
|
|
30
|
+
uiFullscreen: boolean;
|
|
31
|
+
uiLandscape: boolean;
|
|
32
|
+
runningLocal: boolean;
|
|
33
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
i8n.ts
|
|
4
|
+
|
|
5
|
+
Single page UI for Explo using the Mithril library
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2023 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
|
+
|
|
15
|
+
This module contains internationalization (i18n) utility functions,
|
|
16
|
+
allowing for translation of displayed text between languages.
|
|
17
|
+
|
|
18
|
+
Text messages for individual locales are loaded from the
|
|
19
|
+
/static/assets/messages.json file, which is fetched from the server.
|
|
20
|
+
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export { t, ts, mt, loadMessages };
|
|
24
|
+
|
|
25
|
+
import { m, VnodeChildren } from "./mithril";
|
|
26
|
+
import { request } from "./request";
|
|
27
|
+
|
|
28
|
+
// Type declarations
|
|
29
|
+
type Messages = { [key: string]: { [locale: string]: string | string[] }};
|
|
30
|
+
type FlattenedMessages = { [key: string]: { [locale: string]: VnodeChildren }};
|
|
31
|
+
type Interpolations = { [key: string]: string };
|
|
32
|
+
|
|
33
|
+
// Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
|
|
34
|
+
// This is overwritten in setLocale()
|
|
35
|
+
let currentLocale = "is_IS";
|
|
36
|
+
let currentFallback = "is";
|
|
37
|
+
|
|
38
|
+
// Regex that matches embedded interpolations such as "Welcome, {username}!"
|
|
39
|
+
// Interpolation identifiers should only contain ASCII characters, digits and '_'
|
|
40
|
+
const rex = /{\s*(\w+)\s*}/g;
|
|
41
|
+
|
|
42
|
+
let messages: FlattenedMessages = {};
|
|
43
|
+
let messagesLoaded = false;
|
|
44
|
+
|
|
45
|
+
function hasAnyTranslation(msgs: Messages, locale: string): boolean {
|
|
46
|
+
// Return true if any translation is available for the given locale
|
|
47
|
+
for (let key in msgs) {
|
|
48
|
+
if (msgs[key][locale] !== undefined)
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setLocale(locale: string, msgs: Messages): void {
|
|
55
|
+
// Set the current i18n locale and fallback
|
|
56
|
+
currentLocale = locale;
|
|
57
|
+
currentFallback = locale.split("_")[0];
|
|
58
|
+
// For unsupported locales, i.e. locales that have no
|
|
59
|
+
// translations available for them, fall back to English (U.S.).
|
|
60
|
+
if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
|
|
61
|
+
currentLocale = "en_US";
|
|
62
|
+
currentFallback = "en";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Flatten the Messages structure, enabling long strings
|
|
66
|
+
// to be represented as string arrays in the messages.json file
|
|
67
|
+
messages = {};
|
|
68
|
+
for (let key in msgs) {
|
|
69
|
+
for (let lc in msgs[key]) {
|
|
70
|
+
let s: VnodeChildren = msgs[key][lc];
|
|
71
|
+
if (Array.isArray(s))
|
|
72
|
+
s = s.join("");
|
|
73
|
+
if (messages[key] === undefined)
|
|
74
|
+
messages[key] = {};
|
|
75
|
+
// If the string s contains HTML markup of the form <tag>...</tag>,
|
|
76
|
+
// convert it into a list of Mithril Vnode children corresponding to
|
|
77
|
+
// the text and the tags
|
|
78
|
+
if (s.match(/<[a-z]+>/)) {
|
|
79
|
+
// Looks like the string contains HTML markup
|
|
80
|
+
const vnodes: VnodeChildren[] = [];
|
|
81
|
+
let i = 0;
|
|
82
|
+
let tagMatch: RegExpMatchArray | null = null;
|
|
83
|
+
while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
|
|
84
|
+
// Found what looks like an HTML tag
|
|
85
|
+
// Calculate the index of the enclosed text within s
|
|
86
|
+
const tag = tagMatch[0];
|
|
87
|
+
let j = i + tagMatch.index + tag.length;
|
|
88
|
+
// Find the end tag
|
|
89
|
+
let end = s.indexOf("</" + tag.slice(1), j);
|
|
90
|
+
if (end < 0) {
|
|
91
|
+
// No end tag - skip past this weirdness
|
|
92
|
+
i = j;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// Add the text preceding the tag
|
|
96
|
+
if (tagMatch.index > 0)
|
|
97
|
+
vnodes.push(s.slice(i, i + tagMatch.index));
|
|
98
|
+
// Create the Mithril node corresponding to the tag and the enclosed text
|
|
99
|
+
// and add it to the list
|
|
100
|
+
vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
|
|
101
|
+
// Advance the index past the end of the tag
|
|
102
|
+
i = end + tag.length + 1;
|
|
103
|
+
}
|
|
104
|
+
// Push the final text part, if any
|
|
105
|
+
if (i < s.length)
|
|
106
|
+
vnodes.push(s.slice(i));
|
|
107
|
+
// Reassign s to the list of vnodes
|
|
108
|
+
s = vnodes;
|
|
109
|
+
}
|
|
110
|
+
messages[key][lc] = s;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
messagesLoaded = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function loadMessages(locale: string) {
|
|
117
|
+
// Load the internationalization message JSON file from the server
|
|
118
|
+
// and set the user's locale
|
|
119
|
+
try {
|
|
120
|
+
const messages = await request<Messages>({
|
|
121
|
+
method: "GET",
|
|
122
|
+
url: "/static/assets/messages.json",
|
|
123
|
+
});
|
|
124
|
+
setLocale(locale, messages);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
setLocale(locale, {});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function t(key: string, ips: Interpolations = {}): VnodeChildren {
|
|
132
|
+
// Main text translation function, supporting interpolation
|
|
133
|
+
// and HTML tag substitution
|
|
134
|
+
const msgDict = messages[key];
|
|
135
|
+
if (msgDict === undefined)
|
|
136
|
+
// No dictionary for this key - may actually be a missing entry
|
|
137
|
+
return messagesLoaded ? key : "";
|
|
138
|
+
// Lookup exact locale, then fallback, then resort to returning the key
|
|
139
|
+
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
140
|
+
// If we have an interpolation object, do the interpolation first
|
|
141
|
+
return Object.keys(ips).length ? interpolate(message, ips) : message;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ts(key: string, ips: Interpolations = {}): string {
|
|
145
|
+
// String translation function, supporting interpolation
|
|
146
|
+
// but not HTML tag substitution
|
|
147
|
+
const msgDict = messages[key];
|
|
148
|
+
if (msgDict === undefined)
|
|
149
|
+
// No dictionary for this key - may actually be a missing entry
|
|
150
|
+
return messagesLoaded ? key : "";
|
|
151
|
+
// Lookup exact locale, then fallback, then resort to returning the key
|
|
152
|
+
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
153
|
+
if (typeof message != "string")
|
|
154
|
+
// This is actually an error - the client should be calling t() instead
|
|
155
|
+
return "";
|
|
156
|
+
// If we have an interpolation object, do the interpolation first
|
|
157
|
+
return Object.keys(ips).length ? interpolate_string(message, ips) : message;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function mt(cls: string, children: VnodeChildren): VnodeChildren {
|
|
161
|
+
// Wrapper for the Mithril m() function that auto-translates
|
|
162
|
+
// string and array arguments
|
|
163
|
+
if (typeof children == "string") {
|
|
164
|
+
return m(cls, t(children));
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(children)) {
|
|
167
|
+
return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
|
|
168
|
+
}
|
|
169
|
+
return m(cls, children);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function interpolate(message: VnodeChildren, ips: Interpolations): VnodeChildren {
|
|
173
|
+
// Replace interpolation placeholders with their corresponding values
|
|
174
|
+
if (typeof message == "string") {
|
|
175
|
+
return message.replace(rex, (match, key) => ips[key] || match);
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(message)) {
|
|
178
|
+
return message.map((item) => interpolate(item, ips));
|
|
179
|
+
}
|
|
180
|
+
return message;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function interpolate_string(message: string, ips: Interpolations): string {
|
|
184
|
+
// Replace interpolation placeholders with their corresponding values
|
|
185
|
+
return message.replace(rex, (match, key) => ips[key] || match);
|
|
186
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Localstorage.ts
|
|
4
|
+
|
|
5
|
+
An interface around HTML5 local storage functionality, if available
|
|
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
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { RACK_SIZE, SavedTile } from "./types";
|
|
17
|
+
|
|
18
|
+
export interface LocalStorage {
|
|
19
|
+
getLocalTile: (ix: number) => string;
|
|
20
|
+
getLocalTileSq: (ix: number) => string;
|
|
21
|
+
setLocalTile: (ix: number, t: string) => void;
|
|
22
|
+
setLocalTileSq: (ix: number, sq: string) => void;
|
|
23
|
+
clearTiles: () => void;
|
|
24
|
+
saveTiles: (tilesPlaced: SavedTile[]) => void;
|
|
25
|
+
loadTiles: () => SavedTile[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let _hasLocal: boolean | null = null; // Is HTML5 local storage supported by the browser?
|
|
29
|
+
|
|
30
|
+
function hasLocalStorage(): boolean {
|
|
31
|
+
// Return true if HTML5 local storage is supported by the browser
|
|
32
|
+
if (_hasLocal === null)
|
|
33
|
+
try {
|
|
34
|
+
_hasLocal = ('localStorage' in window) &&
|
|
35
|
+
(window.localStorage !== null) &&
|
|
36
|
+
(window.localStorage !== undefined);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
_hasLocal = false;
|
|
39
|
+
}
|
|
40
|
+
return _hasLocal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class LocalStorageImpl implements LocalStorage {
|
|
44
|
+
|
|
45
|
+
_prefix: string;
|
|
46
|
+
|
|
47
|
+
constructor(uuid: string) {
|
|
48
|
+
// Constructor for local storage associated with a particular game
|
|
49
|
+
this._prefix = `game.${uuid}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getLocalTile(ix: number) {
|
|
53
|
+
return window.localStorage[`${this._prefix}.tile.${ix}.t`];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getLocalTileSq(ix: number) {
|
|
57
|
+
return window.localStorage[`${this._prefix}.tile.${ix}.sq`];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setLocalTile(ix: number, t: string) {
|
|
61
|
+
window.localStorage[`${this._prefix}.tile.${ix}.t`] = t;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setLocalTileSq(ix: number, sq: string) {
|
|
65
|
+
window.localStorage[`${this._prefix}.tile.${ix}.sq`] = sq;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clearTiles() {
|
|
69
|
+
// Clean up local storage when game is over
|
|
70
|
+
try {
|
|
71
|
+
const ls = window.localStorage;
|
|
72
|
+
for (let i = 1; i <= RACK_SIZE; i++) {
|
|
73
|
+
ls.removeItem(`${this._prefix}.tile.${i}.sq`);
|
|
74
|
+
ls.removeItem(`${this._prefix}.tile.${i}.t`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
saveTiles(tilesPlaced: SavedTile[]) {
|
|
82
|
+
// Save tile locations in local storage
|
|
83
|
+
let i: number;
|
|
84
|
+
for (i = 0; i < tilesPlaced.length; i++) {
|
|
85
|
+
// Store this placed tile in local storage
|
|
86
|
+
const sq = tilesPlaced[i].sq;
|
|
87
|
+
const tile = tilesPlaced[i].tile;
|
|
88
|
+
// Set the placed tile's square
|
|
89
|
+
this.setLocalTileSq(i + 1, sq);
|
|
90
|
+
// Set the letter (or ?+letter if undefined)
|
|
91
|
+
this.setLocalTile(i + 1, tile);
|
|
92
|
+
}
|
|
93
|
+
// Erase all remaining positions in local storage
|
|
94
|
+
for (; i < RACK_SIZE; i++) {
|
|
95
|
+
this.setLocalTileSq(i + 1, "");
|
|
96
|
+
this.setLocalTile(i + 1, "");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
loadTiles() {
|
|
101
|
+
// Return the saved tile locations
|
|
102
|
+
let sq: string, tile: string;
|
|
103
|
+
let tp: SavedTile[] = [];
|
|
104
|
+
for (let i = 0; i < RACK_SIZE; i++) {
|
|
105
|
+
sq = this.getLocalTileSq(i + 1);
|
|
106
|
+
tile = this.getLocalTile(i + 1);
|
|
107
|
+
if (sq && tile)
|
|
108
|
+
tp.push({sq: sq, tile: tile});
|
|
109
|
+
}
|
|
110
|
+
return tp;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
} // class LocalStorageImpl
|
|
114
|
+
|
|
115
|
+
class NoLocalStorageImpl implements LocalStorage {
|
|
116
|
+
|
|
117
|
+
// This class is used if the browser does not implement local storage
|
|
118
|
+
|
|
119
|
+
constructor() { }
|
|
120
|
+
|
|
121
|
+
getLocalTile(ix: number) { return ""; }
|
|
122
|
+
getLocalTileSq(ix: number) { return ""; }
|
|
123
|
+
setLocalTile(ix: number, t: string) { }
|
|
124
|
+
setLocalTileSq(ix: number, sq: string) { }
|
|
125
|
+
clearTiles() { }
|
|
126
|
+
saveTiles(tilesPlaced: SavedTile[]) { }
|
|
127
|
+
loadTiles(): SavedTile[] { return []; }
|
|
128
|
+
|
|
129
|
+
} // class NoLocalStorageImpl
|
|
130
|
+
|
|
131
|
+
export const getLocalStorage = (uuid: string): LocalStorage => {
|
|
132
|
+
return hasLocalStorage() ? new LocalStorageImpl(uuid) : new NoLocalStorageImpl();
|
|
133
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Login.ts
|
|
4
|
+
|
|
5
|
+
Login UI for Explo using the Mithril library
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2025 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
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { m, ComponentFunc, Component } from "./mithril";
|
|
20
|
+
|
|
21
|
+
import { mt, t } from "./i18n";
|
|
22
|
+
import { AnimatedExploLogo, NetskraflLegend } from "./logo";
|
|
23
|
+
import { request } from "./request";
|
|
24
|
+
|
|
25
|
+
export interface LoginData {
|
|
26
|
+
status: string;
|
|
27
|
+
message?: string;
|
|
28
|
+
account: string;
|
|
29
|
+
user_id: string;
|
|
30
|
+
token: string;
|
|
31
|
+
firebase_token: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const loginUserByEmail = async (
|
|
35
|
+
email: string,
|
|
36
|
+
nickname: string,
|
|
37
|
+
fullname: string,
|
|
38
|
+
token: string,
|
|
39
|
+
) => {
|
|
40
|
+
// Call the /login_malstadur endpoint on the server
|
|
41
|
+
// to log in the user with the given email and token.
|
|
42
|
+
// The token is a standard HS256-encoded JWT with aud "netskrafl"
|
|
43
|
+
// and iss typically "malstadur".
|
|
44
|
+
return request<LoginData>({
|
|
45
|
+
method: "POST",
|
|
46
|
+
url: "/login_malstadur",
|
|
47
|
+
body: { email, nickname, fullname, token }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const LoginError: Component<{ message: string}> = {
|
|
52
|
+
view: (vnode) => {
|
|
53
|
+
return m(
|
|
54
|
+
"div.error",
|
|
55
|
+
{ style: { visibility: "visible" }},
|
|
56
|
+
vnode.attrs?.message || "Error logging in",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const LoginForm: ComponentFunc<{ loginUrl: string }> = (initialVnode) => {
|
|
62
|
+
|
|
63
|
+
const loginUrl = initialVnode.attrs.loginUrl;
|
|
64
|
+
let loginInProgress = false;
|
|
65
|
+
|
|
66
|
+
function doLogin(ev: MouseEvent) {
|
|
67
|
+
loginInProgress = true;
|
|
68
|
+
ev.preventDefault();
|
|
69
|
+
window.location.href = loginUrl;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
view: () => {
|
|
74
|
+
return m.fragment({}, [
|
|
75
|
+
// This is visible on large screens
|
|
76
|
+
m("div.loginform-large", [
|
|
77
|
+
m(AnimatedExploLogo, {
|
|
78
|
+
className: "login-logo",
|
|
79
|
+
width: 200,
|
|
80
|
+
withCircle: false,
|
|
81
|
+
msStepTime: 150,
|
|
82
|
+
once: true
|
|
83
|
+
}),
|
|
84
|
+
m(NetskraflLegend, {
|
|
85
|
+
className: "login-legend",
|
|
86
|
+
width: 600,
|
|
87
|
+
msStepTime: 0
|
|
88
|
+
}),
|
|
89
|
+
mt("div.welcome", "welcome_0"),
|
|
90
|
+
mt("div.welcome", "welcome_1"),
|
|
91
|
+
mt("div.welcome", "welcome_2"),
|
|
92
|
+
m("div.login-btn-large",
|
|
93
|
+
{ onclick: doLogin },
|
|
94
|
+
loginInProgress ? t("Skrái þig inn...") : [
|
|
95
|
+
t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
]),
|
|
99
|
+
// This is visible on small screens
|
|
100
|
+
m("div.loginform-small", [
|
|
101
|
+
m(AnimatedExploLogo, {
|
|
102
|
+
className: "login-logo",
|
|
103
|
+
width: 160,
|
|
104
|
+
withCircle: false,
|
|
105
|
+
msStepTime: 150,
|
|
106
|
+
once: true
|
|
107
|
+
}),
|
|
108
|
+
m(NetskraflLegend, {
|
|
109
|
+
className: "login-legend",
|
|
110
|
+
width: 650,
|
|
111
|
+
msStepTime: 0
|
|
112
|
+
}),
|
|
113
|
+
m("div.login-btn-small",
|
|
114
|
+
{ onclick: doLogin },
|
|
115
|
+
loginInProgress ? t("Skrái þig inn...") : t("Innskrá")
|
|
116
|
+
)
|
|
117
|
+
])
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
};
|