@mideind/netskrafl-react 1.0.2 → 1.2.0
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/dist/cjs/css/netskrafl.css +2082 -1428
- package/dist/cjs/index.js +1030 -222
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/css/netskrafl.css +2082 -1428
- package/dist/esm/index.js +1030 -222
- package/dist/esm/index.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +2 -1
package/dist/cjs/index.js
CHANGED
|
@@ -17,6 +17,8 @@ const saveAuthSettings = (settings) => {
|
|
|
17
17
|
userEmail: settings.userEmail, // Required field
|
|
18
18
|
};
|
|
19
19
|
// Only add optional fields if they are defined
|
|
20
|
+
if (settings.account !== undefined)
|
|
21
|
+
filteredSettings.account = settings.account;
|
|
20
22
|
if (settings.userId !== undefined)
|
|
21
23
|
filteredSettings.userId = settings.userId;
|
|
22
24
|
if (settings.userNick !== undefined)
|
|
@@ -85,6 +87,7 @@ const applyPersistedSettings = (state) => {
|
|
|
85
87
|
return {
|
|
86
88
|
...state,
|
|
87
89
|
// Only apply persisted values if current values are defaults
|
|
90
|
+
account: state.account || persisted.account || state.userId, // Use userId as fallback
|
|
88
91
|
userId: state.userId || persisted.userId || state.userId,
|
|
89
92
|
userNick: state.userNick || persisted.userNick || state.userNick,
|
|
90
93
|
firebaseAPIKey: state.firebaseAPIKey || persisted.firebaseAPIKey || state.firebaseAPIKey,
|
|
@@ -102,6 +105,7 @@ const DEFAULT_STATE = {
|
|
|
102
105
|
firebaseSenderId: "",
|
|
103
106
|
firebaseAppId: "",
|
|
104
107
|
measurementId: "",
|
|
108
|
+
account: "",
|
|
105
109
|
userEmail: "",
|
|
106
110
|
userId: "",
|
|
107
111
|
userNick: "",
|
|
@@ -23531,6 +23535,9 @@ let View$1 = class View {
|
|
|
23531
23535
|
function viewGetServerCache(view) {
|
|
23532
23536
|
return view.viewCache_.serverCache.getNode();
|
|
23533
23537
|
}
|
|
23538
|
+
function viewGetCompleteNode(view) {
|
|
23539
|
+
return viewCacheGetCompleteEventSnap(view.viewCache_);
|
|
23540
|
+
}
|
|
23534
23541
|
function viewGetCompleteServerCache(view, path) {
|
|
23535
23542
|
const cache = viewCacheGetCompleteServerSnap(view.viewCache_);
|
|
23536
23543
|
if (cache) {
|
|
@@ -24192,6 +24199,33 @@ function syncTreeCalcCompleteEventCache(syncTree, path, writeIdsToExclude) {
|
|
|
24192
24199
|
});
|
|
24193
24200
|
return writeTreeCalcCompleteEventCache(writeTree, path, serverCache, writeIdsToExclude, includeHiddenSets);
|
|
24194
24201
|
}
|
|
24202
|
+
function syncTreeGetServerValue(syncTree, query) {
|
|
24203
|
+
const path = query._path;
|
|
24204
|
+
let serverCache = null;
|
|
24205
|
+
// Any covering writes will necessarily be at the root, so really all we need to find is the server cache.
|
|
24206
|
+
// Consider optimizing this once there's a better understanding of what actual behavior will be.
|
|
24207
|
+
syncTree.syncPointTree_.foreachOnPath(path, (pathToSyncPoint, sp) => {
|
|
24208
|
+
const relativePath = newRelativePath(pathToSyncPoint, path);
|
|
24209
|
+
serverCache =
|
|
24210
|
+
serverCache || syncPointGetCompleteServerCache(sp, relativePath);
|
|
24211
|
+
});
|
|
24212
|
+
let syncPoint = syncTree.syncPointTree_.get(path);
|
|
24213
|
+
if (!syncPoint) {
|
|
24214
|
+
syncPoint = new SyncPoint();
|
|
24215
|
+
syncTree.syncPointTree_ = syncTree.syncPointTree_.set(path, syncPoint);
|
|
24216
|
+
}
|
|
24217
|
+
else {
|
|
24218
|
+
serverCache =
|
|
24219
|
+
serverCache || syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
|
|
24220
|
+
}
|
|
24221
|
+
const serverCacheComplete = serverCache != null;
|
|
24222
|
+
const serverCacheNode = serverCacheComplete
|
|
24223
|
+
? new CacheNode(serverCache, true, false)
|
|
24224
|
+
: null;
|
|
24225
|
+
const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, query._path);
|
|
24226
|
+
const view = syncPointGetView(syncPoint, query, writesCache, serverCacheComplete ? serverCacheNode.getNode() : ChildrenNode.EMPTY_NODE, serverCacheComplete);
|
|
24227
|
+
return viewGetCompleteNode(view);
|
|
24228
|
+
}
|
|
24195
24229
|
/**
|
|
24196
24230
|
* A helper method that visits all descendant and ancestor SyncPoints, applying the operation.
|
|
24197
24231
|
*
|
|
@@ -25321,6 +25355,63 @@ function repoUpdateInfo(repo, pathString, value) {
|
|
|
25321
25355
|
function repoGetNextWriteId(repo) {
|
|
25322
25356
|
return repo.nextWriteId_++;
|
|
25323
25357
|
}
|
|
25358
|
+
/**
|
|
25359
|
+
* The purpose of `getValue` is to return the latest known value
|
|
25360
|
+
* satisfying `query`.
|
|
25361
|
+
*
|
|
25362
|
+
* This method will first check for in-memory cached values
|
|
25363
|
+
* belonging to active listeners. If they are found, such values
|
|
25364
|
+
* are considered to be the most up-to-date.
|
|
25365
|
+
*
|
|
25366
|
+
* If the client is not connected, this method will wait until the
|
|
25367
|
+
* repo has established a connection and then request the value for `query`.
|
|
25368
|
+
* If the client is not able to retrieve the query result for another reason,
|
|
25369
|
+
* it reports an error.
|
|
25370
|
+
*
|
|
25371
|
+
* @param query - The query to surface a value for.
|
|
25372
|
+
*/
|
|
25373
|
+
function repoGetValue(repo, query, eventRegistration) {
|
|
25374
|
+
// Only active queries are cached. There is no persisted cache.
|
|
25375
|
+
const cached = syncTreeGetServerValue(repo.serverSyncTree_, query);
|
|
25376
|
+
if (cached != null) {
|
|
25377
|
+
return Promise.resolve(cached);
|
|
25378
|
+
}
|
|
25379
|
+
return repo.server_.get(query).then(payload => {
|
|
25380
|
+
const node = nodeFromJSON(payload).withIndex(query._queryParams.getIndex());
|
|
25381
|
+
/**
|
|
25382
|
+
* Below we simulate the actions of an `onlyOnce` `onValue()` event where:
|
|
25383
|
+
* Add an event registration,
|
|
25384
|
+
* Update data at the path,
|
|
25385
|
+
* Raise any events,
|
|
25386
|
+
* Cleanup the SyncTree
|
|
25387
|
+
*/
|
|
25388
|
+
syncTreeAddEventRegistration(repo.serverSyncTree_, query, eventRegistration, true);
|
|
25389
|
+
let events;
|
|
25390
|
+
if (query._queryParams.loadsAllData()) {
|
|
25391
|
+
events = syncTreeApplyServerOverwrite(repo.serverSyncTree_, query._path, node);
|
|
25392
|
+
}
|
|
25393
|
+
else {
|
|
25394
|
+
const tag = syncTreeTagForQuery(repo.serverSyncTree_, query);
|
|
25395
|
+
events = syncTreeApplyTaggedQueryOverwrite(repo.serverSyncTree_, query._path, node, tag);
|
|
25396
|
+
}
|
|
25397
|
+
/*
|
|
25398
|
+
* We need to raise events in the scenario where `get()` is called at a parent path, and
|
|
25399
|
+
* while the `get()` is pending, `onValue` is called at a child location. While get() is waiting
|
|
25400
|
+
* for the data, `onValue` will register a new event. Then, get() will come back, and update the syncTree
|
|
25401
|
+
* and its corresponding serverCache, including the child location where `onValue` is called. Then,
|
|
25402
|
+
* `onValue` will receive the event from the server, but look at the syncTree and see that the data received
|
|
25403
|
+
* from the server is already at the SyncPoint, and so the `onValue` callback will never get fired.
|
|
25404
|
+
* Calling `eventQueueRaiseEventsForChangedPath()` is the correct way to propagate the events and
|
|
25405
|
+
* ensure the corresponding child events will get fired.
|
|
25406
|
+
*/
|
|
25407
|
+
eventQueueRaiseEventsForChangedPath(repo.eventQueue_, query._path, events);
|
|
25408
|
+
syncTreeRemoveEventRegistration(repo.serverSyncTree_, query, eventRegistration, null, true);
|
|
25409
|
+
return node;
|
|
25410
|
+
}, err => {
|
|
25411
|
+
repoLog(repo, 'get for query ' + stringify(query) + ' failed: ' + err);
|
|
25412
|
+
return Promise.reject(new Error(err));
|
|
25413
|
+
});
|
|
25414
|
+
}
|
|
25324
25415
|
function repoSetWithPriority(repo, path, newVal, newPriority, onComplete) {
|
|
25325
25416
|
repoLog(repo, 'set', {
|
|
25326
25417
|
path: path.toString(),
|
|
@@ -26737,6 +26828,22 @@ function set(ref, value) {
|
|
|
26737
26828
|
/*priority=*/ null, deferred.wrapCallback(() => { }));
|
|
26738
26829
|
return deferred.promise;
|
|
26739
26830
|
}
|
|
26831
|
+
/**
|
|
26832
|
+
* Gets the most up-to-date result for this query.
|
|
26833
|
+
*
|
|
26834
|
+
* @param query - The query to run.
|
|
26835
|
+
* @returns A `Promise` which resolves to the resulting DataSnapshot if a value is
|
|
26836
|
+
* available, or rejects if the client is unable to return a value (e.g., if the
|
|
26837
|
+
* server is unreachable and there is nothing cached).
|
|
26838
|
+
*/
|
|
26839
|
+
function get(query) {
|
|
26840
|
+
query = getModularInstance(query);
|
|
26841
|
+
const callbackContext = new CallbackContext(() => { });
|
|
26842
|
+
const container = new ValueEventRegistration(callbackContext);
|
|
26843
|
+
return repoGetValue(query._repo, query, container).then(node => {
|
|
26844
|
+
return new DataSnapshot(node, new ReferenceImpl(query._repo, query._path), query._queryParams.getIndex());
|
|
26845
|
+
});
|
|
26846
|
+
}
|
|
26740
26847
|
/**
|
|
26741
26848
|
* Represents registration for 'value' events.
|
|
26742
26849
|
*/
|
|
@@ -27240,6 +27347,14 @@ function logEvent(ev, params) {
|
|
|
27240
27347
|
return;
|
|
27241
27348
|
logEvent$2(analytics, ev, params);
|
|
27242
27349
|
}
|
|
27350
|
+
async function getFirebaseData(path) {
|
|
27351
|
+
// Get data from a Firebase path
|
|
27352
|
+
if (!database)
|
|
27353
|
+
return null;
|
|
27354
|
+
const pathRef = ref(database, path);
|
|
27355
|
+
const snapshot = await get(pathRef);
|
|
27356
|
+
return snapshot.val();
|
|
27357
|
+
}
|
|
27243
27358
|
|
|
27244
27359
|
/*
|
|
27245
27360
|
|
|
@@ -27505,50 +27620,61 @@ const ensureAuthenticated = async (state) => {
|
|
|
27505
27620
|
await authPromise;
|
|
27506
27621
|
return;
|
|
27507
27622
|
}
|
|
27508
|
-
|
|
27509
|
-
|
|
27510
|
-
|
|
27511
|
-
|
|
27512
|
-
|
|
27513
|
-
|
|
27514
|
-
|
|
27515
|
-
|
|
27516
|
-
|
|
27517
|
-
|
|
27623
|
+
let continueTrying = true;
|
|
27624
|
+
while (continueTrying) {
|
|
27625
|
+
continueTrying = false;
|
|
27626
|
+
// Start new login attempt (either forced by 401 or needed for Firebase)
|
|
27627
|
+
authPromise = loginUserByEmail(state);
|
|
27628
|
+
try {
|
|
27629
|
+
const result = await authPromise;
|
|
27630
|
+
if (result.status === "expired") {
|
|
27631
|
+
// Token has expired, notify the React component if callback is set
|
|
27632
|
+
if (state.tokenExpired) {
|
|
27633
|
+
// We have a callback to renew the token: do it and try again
|
|
27634
|
+
state.tokenExpired();
|
|
27635
|
+
continueTrying = true; // Try logging in again
|
|
27636
|
+
clearAuthSettings();
|
|
27637
|
+
continue;
|
|
27638
|
+
}
|
|
27639
|
+
// Clear any persisted settings since they're no longer valid
|
|
27640
|
+
clearAuthSettings();
|
|
27641
|
+
throw new Error("Authentication token has expired");
|
|
27642
|
+
}
|
|
27643
|
+
else if (result.status !== "success") {
|
|
27644
|
+
// Clear any persisted settings on auth failure
|
|
27645
|
+
clearAuthSettings();
|
|
27646
|
+
throw new Error(`Authentication failed: ${result.message || result.status}`);
|
|
27647
|
+
}
|
|
27648
|
+
// Update the user's ID to the internal one used by the backend and Firebase
|
|
27649
|
+
state.userId = result.user_id || state.userId;
|
|
27650
|
+
state.account = result.account || state.userId;
|
|
27651
|
+
// Update the user's nickname
|
|
27652
|
+
state.userNick = result.nickname || state.userNick;
|
|
27653
|
+
// Use the server's Firebase API key, if provided
|
|
27654
|
+
state.firebaseAPIKey = result.firebase_api_key || state.firebaseAPIKey;
|
|
27655
|
+
// Load state flags and preferences
|
|
27656
|
+
state.beginner = (_b = (_a = result.prefs) === null || _a === void 0 ? void 0 : _a.beginner) !== null && _b !== void 0 ? _b : true;
|
|
27657
|
+
state.fairPlay = (_d = (_c = result.prefs) === null || _c === void 0 ? void 0 : _c.fairplay) !== null && _d !== void 0 ? _d : false;
|
|
27658
|
+
state.ready = (_f = (_e = result.prefs) === null || _e === void 0 ? void 0 : _e.ready) !== null && _f !== void 0 ? _f : true;
|
|
27659
|
+
state.readyTimed = (_h = (_g = result.prefs) === null || _g === void 0 ? void 0 : _g.ready_timed) !== null && _h !== void 0 ? _h : true;
|
|
27660
|
+
// Save the authentication settings to sessionStorage for persistence
|
|
27661
|
+
saveAuthSettings({
|
|
27662
|
+
userEmail: state.userEmail, // CRITICAL: Include email to validate ownership
|
|
27663
|
+
userId: state.userId,
|
|
27664
|
+
userNick: state.userNick,
|
|
27665
|
+
firebaseAPIKey: state.firebaseAPIKey,
|
|
27666
|
+
beginner: state.beginner,
|
|
27667
|
+
fairPlay: state.fairPlay,
|
|
27668
|
+
ready: state.ready,
|
|
27669
|
+
readyTimed: state.readyTimed,
|
|
27670
|
+
});
|
|
27671
|
+
// Success: Log in to Firebase with the token passed from the server
|
|
27672
|
+
await loginFirebase(state, result.firebase_token);
|
|
27673
|
+
}
|
|
27674
|
+
finally {
|
|
27675
|
+
// Reset the promise so future 401s can trigger a new login
|
|
27676
|
+
authPromise = null;
|
|
27518
27677
|
}
|
|
27519
|
-
else if (result.status !== "success") {
|
|
27520
|
-
// Clear any persisted settings on auth failure
|
|
27521
|
-
clearAuthSettings();
|
|
27522
|
-
throw new Error(`Authentication failed: ${result.message || result.status}`);
|
|
27523
|
-
}
|
|
27524
|
-
// Update the user's ID to the internal one used by the backend and Firebase
|
|
27525
|
-
state.userId = result.user_id || state.userId;
|
|
27526
|
-
// Update the user's nickname
|
|
27527
|
-
state.userNick = result.nickname || state.userNick;
|
|
27528
|
-
// Use the server's Firebase API key, if provided
|
|
27529
|
-
state.firebaseAPIKey = result.firebase_api_key || state.firebaseAPIKey;
|
|
27530
|
-
// Load state flags and preferences
|
|
27531
|
-
state.beginner = (_b = (_a = result.prefs) === null || _a === void 0 ? void 0 : _a.beginner) !== null && _b !== void 0 ? _b : true;
|
|
27532
|
-
state.fairPlay = (_d = (_c = result.prefs) === null || _c === void 0 ? void 0 : _c.fairplay) !== null && _d !== void 0 ? _d : false;
|
|
27533
|
-
state.ready = (_f = (_e = result.prefs) === null || _e === void 0 ? void 0 : _e.ready) !== null && _f !== void 0 ? _f : true;
|
|
27534
|
-
state.readyTimed = (_h = (_g = result.prefs) === null || _g === void 0 ? void 0 : _g.ready_timed) !== null && _h !== void 0 ? _h : true;
|
|
27535
|
-
// Save the authentication settings to sessionStorage for persistence
|
|
27536
|
-
saveAuthSettings({
|
|
27537
|
-
userEmail: state.userEmail, // CRITICAL: Include email to validate ownership
|
|
27538
|
-
userId: state.userId,
|
|
27539
|
-
userNick: state.userNick,
|
|
27540
|
-
firebaseAPIKey: state.firebaseAPIKey,
|
|
27541
|
-
beginner: state.beginner,
|
|
27542
|
-
fairPlay: state.fairPlay,
|
|
27543
|
-
ready: state.ready,
|
|
27544
|
-
readyTimed: state.readyTimed,
|
|
27545
|
-
});
|
|
27546
|
-
// Success: Log in to Firebase with the token passed from the server
|
|
27547
|
-
await loginFirebase(state, result.firebase_token);
|
|
27548
|
-
}
|
|
27549
|
-
finally {
|
|
27550
|
-
// Reset the promise so future 401s can trigger a new login
|
|
27551
|
-
authPromise = null;
|
|
27552
27678
|
}
|
|
27553
27679
|
};
|
|
27554
27680
|
// Internal authenticated request function
|
|
@@ -29080,7 +29206,7 @@ const BestDisplay = () => {
|
|
|
29080
29206
|
// Make sure blank tiles get a different color
|
|
29081
29207
|
for (let i = 0; i < bw.length; i++)
|
|
29082
29208
|
if (bw[i] == '?') {
|
|
29083
|
-
s.push(m("span.blanktile", bw[i + 1]));
|
|
29209
|
+
s.push(m("span.netskrafl-blanktile", bw[i + 1]));
|
|
29084
29210
|
i += 1;
|
|
29085
29211
|
}
|
|
29086
29212
|
else
|
|
@@ -29876,7 +30002,7 @@ const BlankDialog = () => {
|
|
|
29876
30002
|
onclick: (ev) => { game.placeBlank(letter); ev.preventDefault(); },
|
|
29877
30003
|
onmouseover: buttonOver,
|
|
29878
30004
|
onmouseout: buttonOut
|
|
29879
|
-
}, m(".blank-choice.tile.racktile", letter)));
|
|
30005
|
+
}, m(".blank-choice.netskrafl-tile.netskrafl-racktile", letter)));
|
|
29880
30006
|
len--;
|
|
29881
30007
|
}
|
|
29882
30008
|
r.push(m("tr", c));
|
|
@@ -29971,25 +30097,26 @@ const Bag = () => {
|
|
|
29971
30097
|
For further information, see https://github.com/mideind/Netskrafl
|
|
29972
30098
|
|
|
29973
30099
|
*/
|
|
29974
|
-
function makeButton(cls, disabled, onclick, title, children, id) {
|
|
30100
|
+
function makeButton(cls, disabled, onclick, title, children, visible, id) {
|
|
29975
30101
|
// Create a button element, wrapping the disabling logic
|
|
29976
30102
|
// and other boilerplate
|
|
29977
|
-
const
|
|
30103
|
+
const attrs = {
|
|
29978
30104
|
onmouseout: buttonOut,
|
|
29979
30105
|
onmouseover: buttonOver,
|
|
30106
|
+
style: { visibility: visible === false ? "hidden" : "visible" },
|
|
29980
30107
|
};
|
|
29981
30108
|
if (title)
|
|
29982
|
-
|
|
30109
|
+
attrs.title = title;
|
|
29983
30110
|
if (id)
|
|
29984
|
-
|
|
30111
|
+
attrs.id = id;
|
|
29985
30112
|
if (disabled)
|
|
29986
|
-
|
|
30113
|
+
attrs.onclick = (ev) => ev.preventDefault();
|
|
29987
30114
|
else
|
|
29988
|
-
|
|
30115
|
+
attrs.onclick = (ev) => {
|
|
29989
30116
|
onclick && onclick();
|
|
29990
30117
|
ev.preventDefault();
|
|
29991
30118
|
};
|
|
29992
|
-
return m("." + cls + (disabled ? ".disabled" : ""),
|
|
30119
|
+
return m("." + cls + (disabled ? ".disabled" : ""), attrs, children // children may be omitted
|
|
29993
30120
|
);
|
|
29994
30121
|
}
|
|
29995
30122
|
const Score = {
|
|
@@ -30011,20 +30138,20 @@ const Score = {
|
|
|
30011
30138
|
const RecallButton = {
|
|
30012
30139
|
view: (vnode) => {
|
|
30013
30140
|
// Create a tile recall button
|
|
30014
|
-
const { view, game, disabled } = vnode.attrs;
|
|
30141
|
+
const { view, game, disabled, visible = true } = vnode.attrs;
|
|
30015
30142
|
if (!game)
|
|
30016
30143
|
return undefined;
|
|
30017
|
-
return makeButton("recallbtn", !!disabled, () => { game.resetRack(); view.updateScale(); }, ts("Færa stafi aftur í rekka"), glyph("down-arrow"));
|
|
30144
|
+
return makeButton("recallbtn", !!disabled, () => { game.resetRack(); view.updateScale(); }, ts("Færa stafi aftur í rekka"), glyph("down-arrow"), visible);
|
|
30018
30145
|
}
|
|
30019
30146
|
};
|
|
30020
30147
|
const ScrambleButton = {
|
|
30021
30148
|
view: (vnode) => {
|
|
30022
30149
|
// Create a tile scramble button
|
|
30023
|
-
const { game, disabled } = vnode.attrs;
|
|
30150
|
+
const { game, disabled, visible = true } = vnode.attrs;
|
|
30024
30151
|
if (!game)
|
|
30025
30152
|
return undefined;
|
|
30026
30153
|
return makeButton("scramblebtn", !!disabled, () => game.rescrambleRack(), // Note: plain game.rescrambleRack doesn't work here
|
|
30027
|
-
ts("Stokka upp rekka"), glyph("random"));
|
|
30154
|
+
ts("Stokka upp rekka"), glyph("random"), visible);
|
|
30028
30155
|
}
|
|
30029
30156
|
};
|
|
30030
30157
|
const ButtonsRecallScramble = {
|
|
@@ -30040,9 +30167,9 @@ const ButtonsRecallScramble = {
|
|
|
30040
30167
|
if (s.showRecall && !s.showingDialog)
|
|
30041
30168
|
// Show a 'Recall tiles' button
|
|
30042
30169
|
return m(RecallButton, { view, game });
|
|
30043
|
-
|
|
30044
|
-
|
|
30045
|
-
return
|
|
30170
|
+
const visible = s.showScramble && !s.tilesPlaced;
|
|
30171
|
+
const disabled = s.showingDialog;
|
|
30172
|
+
return m(ScrambleButton, { view, game, visible, disabled });
|
|
30046
30173
|
}
|
|
30047
30174
|
};
|
|
30048
30175
|
const Buttons = {
|
|
@@ -30119,7 +30246,7 @@ const Buttons = {
|
|
|
30119
30246
|
}
|
|
30120
30247
|
};
|
|
30121
30248
|
}
|
|
30122
|
-
r.push(makeButton(classes.join("."), s.showingDialog, action, text, legend, "move-mobile"));
|
|
30249
|
+
r.push(makeButton(classes.join("."), s.showingDialog, action, text, legend, true, "move-mobile"));
|
|
30123
30250
|
}
|
|
30124
30251
|
if (s.showForceResignMobile) {
|
|
30125
30252
|
// Force resignation button (only shown on mobile,
|
|
@@ -30138,7 +30265,7 @@ const Buttons = {
|
|
|
30138
30265
|
}
|
|
30139
30266
|
if (s.showExchange) {
|
|
30140
30267
|
// Exchange tiles from the rack
|
|
30141
|
-
const disabled =
|
|
30268
|
+
const disabled = !s.exchangeAllowed || s.tilesPlaced || s.showingDialog;
|
|
30142
30269
|
r.push(makeButton("submitexchange", disabled, () => game.submitExchange(), // Note: plain game.submitExchange doesn't work here
|
|
30143
30270
|
ts("Skipta stöfum"), glyph("refresh")));
|
|
30144
30271
|
}
|
|
@@ -30435,24 +30562,24 @@ const Tile = (initialVnode) => {
|
|
|
30435
30562
|
const isRackTile = coord[0] === 'R';
|
|
30436
30563
|
// A single tile, on the board or in the rack
|
|
30437
30564
|
const t = game.tiles[coord];
|
|
30438
|
-
let classes = [".tile"];
|
|
30565
|
+
let classes = [".netskrafl-tile"];
|
|
30439
30566
|
let attrs = {};
|
|
30440
30567
|
if (t.tile === '?')
|
|
30441
|
-
classes.push("blanktile");
|
|
30568
|
+
classes.push("netskrafl-blanktile");
|
|
30442
30569
|
if (EXTRA_WIDE_LETTERS.includes(t.letter))
|
|
30443
30570
|
// Extra wide letter: handle specially
|
|
30444
|
-
classes.push("extra-wide");
|
|
30571
|
+
classes.push("netskrafl-extra-wide");
|
|
30445
30572
|
else if (WIDE_LETTERS.includes(t.letter))
|
|
30446
30573
|
// Wide letter: handle specially
|
|
30447
|
-
classes.push("wide");
|
|
30574
|
+
classes.push("netskrafl-wide");
|
|
30448
30575
|
if (isRackTile || t.draggable) {
|
|
30449
30576
|
// Rack tile, or at least a draggable one
|
|
30450
|
-
classes.push(opponent ? "freshtile" : "racktile");
|
|
30577
|
+
classes.push(opponent ? "netskrafl-freshtile" : "netskrafl-racktile");
|
|
30451
30578
|
if (isRackTile && game.showingDialog === "exchange") {
|
|
30452
30579
|
// Rack tile, and we're showing the exchange dialog
|
|
30453
30580
|
if (t.xchg)
|
|
30454
30581
|
// Chosen as an exchange tile
|
|
30455
|
-
classes.push("xchgsel");
|
|
30582
|
+
classes.push("netskrafl-xchgsel");
|
|
30456
30583
|
// Exchange dialog is live: add a click handler for the
|
|
30457
30584
|
// exchange state
|
|
30458
30585
|
attrs.onclick = (ev) => {
|
|
@@ -30465,7 +30592,7 @@ const Tile = (initialVnode) => {
|
|
|
30465
30592
|
else if (t.freshtile) {
|
|
30466
30593
|
// A fresh tile on the board that has
|
|
30467
30594
|
// just been played by the opponent
|
|
30468
|
-
classes.push("freshtile");
|
|
30595
|
+
classes.push("netskrafl-freshtile");
|
|
30469
30596
|
}
|
|
30470
30597
|
if (t.index) {
|
|
30471
30598
|
// Make fresh or highlighted tiles appear sequentally by animation
|
|
@@ -32071,11 +32198,11 @@ const vwButtonsReview = (view, moveIndex) => {
|
|
|
32071
32198
|
() => {
|
|
32072
32199
|
// Navigate to previous moveIndex
|
|
32073
32200
|
model.loadBestMoves(moveIndex ? moveIndex - 1 : 0);
|
|
32074
|
-
}, "Sjá fyrri leik", m("span", { id: "nav-prev-visible" }, [glyph("chevron-left"), " Fyrri"]), "navprev"));
|
|
32201
|
+
}, "Sjá fyrri leik", m("span", { id: "nav-prev-visible" }, [glyph("chevron-left"), " Fyrri"]), true, "navprev"));
|
|
32075
32202
|
r.push(makeButton("navbtn", (!moveIndex) || (moveIndex >= numMoves), () => {
|
|
32076
32203
|
// Navigate to next moveIndex
|
|
32077
32204
|
model.loadBestMoves(moveIndex + 1);
|
|
32078
|
-
}, "Sjá næsta leik", m("span", { id: "nav-next-visible" }, ["Næsti ", glyph("chevron-right")]), "navnext"));
|
|
32205
|
+
}, "Sjá næsta leik", m("span", { id: "nav-next-visible" }, ["Næsti ", glyph("chevron-right")]), true, "navnext"));
|
|
32079
32206
|
// Show the score difference between an actual moveIndex and
|
|
32080
32207
|
// a particular moveIndex on the best moveIndex list
|
|
32081
32208
|
if (model.highlightedMove !== null) {
|
|
@@ -32285,39 +32412,68 @@ class View {
|
|
|
32285
32412
|
this.boardScale = 1.0;
|
|
32286
32413
|
const boardParent = document.getElementById("board-parent");
|
|
32287
32414
|
const board = boardParent === null || boardParent === void 0 ? void 0 : boardParent.children[0];
|
|
32288
|
-
if (board)
|
|
32289
|
-
board.
|
|
32415
|
+
if (board) {
|
|
32416
|
+
board.style.transition = 'none';
|
|
32417
|
+
board.style.transformOrigin = 'top left';
|
|
32418
|
+
board.style.transform = `translate(0, 0) scale(1.0)`;
|
|
32419
|
+
}
|
|
32290
32420
|
if (boardParent)
|
|
32291
32421
|
boardParent.scrollTo(0, 0);
|
|
32292
32422
|
}
|
|
32293
32423
|
updateScale() {
|
|
32294
32424
|
var _a;
|
|
32295
32425
|
const model = this.model;
|
|
32296
|
-
|
|
32426
|
+
// Use either the regular game or the riddle (Gáta Dagsins)
|
|
32427
|
+
const game = model.game || model.riddle;
|
|
32297
32428
|
// Update the board scale (zoom)
|
|
32298
32429
|
function scrollIntoView(sq) {
|
|
32299
|
-
// Scroll a square above and to the left of the placed tile into view
|
|
32430
|
+
// Scroll a square above and to the left of the placed tile into view,
|
|
32431
|
+
// with a smooth concurrent zoom and pan animation,
|
|
32432
|
+
// taking clamping into account to ensure that the board always fills
|
|
32433
|
+
// the viewport.
|
|
32434
|
+
const boardParent = document.getElementById("board-parent");
|
|
32435
|
+
const board = boardParent === null || boardParent === void 0 ? void 0 : boardParent.children[0];
|
|
32436
|
+
if (!board || !boardParent)
|
|
32437
|
+
return;
|
|
32300
32438
|
const offset = 3;
|
|
32301
32439
|
const vec = toVector(sq);
|
|
32302
32440
|
const row = Math.max(0, vec.row - offset);
|
|
32303
32441
|
const col = Math.max(0, vec.col - offset);
|
|
32304
32442
|
const c = coord(row, col);
|
|
32305
|
-
|
|
32306
|
-
|
|
32307
|
-
|
|
32308
|
-
// the transform and hence the size of the board has been
|
|
32309
|
-
// updated in the browser, before calculating the client rects
|
|
32310
|
-
if (board)
|
|
32311
|
-
board.setAttribute("style", `transform: scale(${ZOOM_FACTOR})`);
|
|
32443
|
+
// Temporarily set scale to calculate target scroll position
|
|
32444
|
+
board.style.transformOrigin = 'top left';
|
|
32445
|
+
board.style.transform = `translate(0, 0) scale(1.0)`;
|
|
32312
32446
|
const el = document.getElementById("sq_" + c);
|
|
32313
32447
|
const elRect = el === null || el === void 0 ? void 0 : el.getBoundingClientRect();
|
|
32314
|
-
const boardRect = boardParent
|
|
32315
|
-
if (
|
|
32316
|
-
|
|
32317
|
-
|
|
32318
|
-
|
|
32319
|
-
|
|
32320
|
-
|
|
32448
|
+
const boardRect = boardParent.getBoundingClientRect();
|
|
32449
|
+
if (elRect && boardRect) {
|
|
32450
|
+
// Get the dimensions of the scrollable area
|
|
32451
|
+
const viewportWidth = boardParent.clientWidth;
|
|
32452
|
+
const viewportHeight = boardParent.clientHeight;
|
|
32453
|
+
// The offsetWidth/Height include borders, so we use those
|
|
32454
|
+
const scaledBoardWidth = board.offsetWidth * ZOOM_FACTOR;
|
|
32455
|
+
const scaledBoardHeight = board.offsetHeight * ZOOM_FACTOR;
|
|
32456
|
+
// Calculate maximum scroll positions (board edges)
|
|
32457
|
+
const maxScrollLeft = scaledBoardWidth - viewportWidth;
|
|
32458
|
+
const maxScrollTop = scaledBoardHeight - viewportHeight;
|
|
32459
|
+
// Calculate desired scroll position to put the target into
|
|
32460
|
+
// the top left corner of the viewport, or as close as possible
|
|
32461
|
+
let targetScrollLeft = Math.min(elRect.left - boardRect.left, maxScrollLeft);
|
|
32462
|
+
let targetScrollTop = Math.min(elRect.top - boardRect.top, maxScrollTop);
|
|
32463
|
+
// Now animate both translate (for pan) and scale (for zoom) concurrently
|
|
32464
|
+
board.style.transition = 'transform 0.3s ease-in-out';
|
|
32465
|
+
// Note: transforms are applied right to left
|
|
32466
|
+
board.style.transform =
|
|
32467
|
+
`translate(${-targetScrollLeft}px, ${-targetScrollTop}px) scale(${ZOOM_FACTOR})`;
|
|
32468
|
+
// When animation completes, commit to actual scroll position and reset translate
|
|
32469
|
+
board.addEventListener('transitionend', function handler() {
|
|
32470
|
+
// First reset the transform (remove translate, keep scale)
|
|
32471
|
+
board.style.transition = 'none';
|
|
32472
|
+
board.style.transform = `translate(0, 0) scale(${ZOOM_FACTOR})`;
|
|
32473
|
+
// Now set the actual scroll position (already clamped)
|
|
32474
|
+
boardParent.scrollTo(targetScrollLeft, targetScrollTop);
|
|
32475
|
+
board.removeEventListener('transitionend', handler);
|
|
32476
|
+
}, { once: true });
|
|
32321
32477
|
}
|
|
32322
32478
|
}
|
|
32323
32479
|
if (!game || ((_a = model.state) === null || _a === void 0 ? void 0 : _a.uiFullscreen) || game.moveInProgress) {
|
|
@@ -32604,14 +32760,14 @@ class View {
|
|
|
32604
32760
|
// displayed. We therefore allow them to cover the last_chall
|
|
32605
32761
|
// dialog. On mobile, both dialogs are displayed simultaneously.
|
|
32606
32762
|
if (game.last_chall) {
|
|
32607
|
-
r.push(m(".chall-info",
|
|
32763
|
+
r.push(m(".chall-info", [
|
|
32608
32764
|
glyph("info-sign"), nbsp(),
|
|
32609
32765
|
// "Your opponent emptied the rack - you can challenge or pass"
|
|
32610
32766
|
mt("span.pass-explain", "opponent_emptied_rack")
|
|
32611
32767
|
]));
|
|
32612
32768
|
}
|
|
32613
32769
|
if (game.showingDialog == "resign") {
|
|
32614
|
-
r.push(m(".resign",
|
|
32770
|
+
r.push(m(".resign", [
|
|
32615
32771
|
glyph("exclamation-sign"), nbsp(), ts("Viltu gefa leikinn?"), nbsp(),
|
|
32616
32772
|
m("span.mobile-break", m("br")),
|
|
32617
32773
|
m("span.yesnobutton", { onclick: () => game.confirmResign(true) }, [glyph("ok"), ts(" Já")]),
|
|
@@ -32621,7 +32777,7 @@ class View {
|
|
|
32621
32777
|
}
|
|
32622
32778
|
if (game.showingDialog == "pass") {
|
|
32623
32779
|
if (game.last_chall) {
|
|
32624
|
-
r.push(m(".pass-last",
|
|
32780
|
+
r.push(m(".pass-last", [
|
|
32625
32781
|
glyph("forward"), nbsp(), ts("Segja pass?"),
|
|
32626
32782
|
mt("span.pass-explain", "Viðureign lýkur þar með"),
|
|
32627
32783
|
nbsp(),
|
|
@@ -32632,7 +32788,7 @@ class View {
|
|
|
32632
32788
|
]));
|
|
32633
32789
|
}
|
|
32634
32790
|
else {
|
|
32635
|
-
r.push(m(".pass",
|
|
32791
|
+
r.push(m(".pass", [
|
|
32636
32792
|
glyph("forward"), nbsp(), ts("Segja pass?"),
|
|
32637
32793
|
mt("span.pass-explain", "2x3 pöss í röð ljúka viðureign"),
|
|
32638
32794
|
nbsp(), m("span.mobile-break", m("br")),
|
|
@@ -32643,7 +32799,7 @@ class View {
|
|
|
32643
32799
|
}
|
|
32644
32800
|
}
|
|
32645
32801
|
if (game.showingDialog == "exchange") {
|
|
32646
|
-
r.push(m(".exchange",
|
|
32802
|
+
r.push(m(".exchange", [
|
|
32647
32803
|
glyph("refresh"), nbsp(),
|
|
32648
32804
|
ts("Smelltu á flísarnar sem þú vilt skipta"), nbsp(),
|
|
32649
32805
|
m("span.mobile-break", m("br")),
|
|
@@ -32653,7 +32809,7 @@ class View {
|
|
|
32653
32809
|
]));
|
|
32654
32810
|
}
|
|
32655
32811
|
if (game.showingDialog == "chall") {
|
|
32656
|
-
r.push(m(".chall",
|
|
32812
|
+
r.push(m(".chall", [
|
|
32657
32813
|
glyph("ban-circle"), nbsp(), ts("Véfengja lögn?"),
|
|
32658
32814
|
mt("span.pass-explain", "Röng véfenging kostar 10 stig"), nbsp(),
|
|
32659
32815
|
m("span.mobile-break", m("br")),
|
|
@@ -32992,6 +33148,7 @@ class BaseGame {
|
|
|
32992
33148
|
this.currentError = null;
|
|
32993
33149
|
this.currentMessage = null;
|
|
32994
33150
|
this.localturn = true;
|
|
33151
|
+
this.moveInProgress = false;
|
|
32995
33152
|
// UI state
|
|
32996
33153
|
this.showingDialog = null;
|
|
32997
33154
|
this.selectedSq = null;
|
|
@@ -34354,6 +34511,161 @@ class Game extends BaseGame {
|
|
|
34354
34511
|
;
|
|
34355
34512
|
} // class Game
|
|
34356
34513
|
|
|
34514
|
+
/*
|
|
34515
|
+
|
|
34516
|
+
riddlePersistence.ts
|
|
34517
|
+
|
|
34518
|
+
Local persistence for Gáta Dagsins using localStorage
|
|
34519
|
+
|
|
34520
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
34521
|
+
|
|
34522
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
34523
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
34524
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
34525
|
+
|
|
34526
|
+
*/
|
|
34527
|
+
class RiddlePersistence {
|
|
34528
|
+
// Generate storage key for a specific user and date
|
|
34529
|
+
static getStorageKey(userId, date) {
|
|
34530
|
+
return `${this.STORAGE_KEY_PREFIX}${date}_${userId}`;
|
|
34531
|
+
}
|
|
34532
|
+
// Save complete move list to localStorage
|
|
34533
|
+
static saveLocalMoves(userId, date, moves) {
|
|
34534
|
+
if (!userId || !date) {
|
|
34535
|
+
return;
|
|
34536
|
+
}
|
|
34537
|
+
const data = {
|
|
34538
|
+
date,
|
|
34539
|
+
moves,
|
|
34540
|
+
timestamp: new Date().toISOString(),
|
|
34541
|
+
userId,
|
|
34542
|
+
};
|
|
34543
|
+
try {
|
|
34544
|
+
const key = this.getStorageKey(userId, date);
|
|
34545
|
+
localStorage.setItem(key, JSON.stringify(data));
|
|
34546
|
+
// Clean up old entries while we're here
|
|
34547
|
+
this.cleanupOldEntries();
|
|
34548
|
+
}
|
|
34549
|
+
catch (e) {
|
|
34550
|
+
// Handle localStorage quota errors silently
|
|
34551
|
+
console.error('Failed to save moves to localStorage:', e);
|
|
34552
|
+
}
|
|
34553
|
+
}
|
|
34554
|
+
// Load move history from localStorage
|
|
34555
|
+
static loadLocalMoves(userId, date) {
|
|
34556
|
+
if (!userId || !date) {
|
|
34557
|
+
return [];
|
|
34558
|
+
}
|
|
34559
|
+
try {
|
|
34560
|
+
const key = this.getStorageKey(userId, date);
|
|
34561
|
+
const stored = localStorage.getItem(key);
|
|
34562
|
+
if (!stored) {
|
|
34563
|
+
return [];
|
|
34564
|
+
}
|
|
34565
|
+
const data = JSON.parse(stored);
|
|
34566
|
+
// Verify that this data belongs to the correct (current) user
|
|
34567
|
+
if (!data.userId || data.userId !== userId) {
|
|
34568
|
+
return [];
|
|
34569
|
+
}
|
|
34570
|
+
return data.moves || [];
|
|
34571
|
+
}
|
|
34572
|
+
catch (e) {
|
|
34573
|
+
console.error('Failed to load moves from localStorage:', e);
|
|
34574
|
+
return [];
|
|
34575
|
+
}
|
|
34576
|
+
}
|
|
34577
|
+
// Check if user has achieved top score (local check)
|
|
34578
|
+
static hasAchievedTopScore(userId, date, topScore) {
|
|
34579
|
+
const moves = this.loadLocalMoves(userId, date);
|
|
34580
|
+
return moves.some(move => move.score >= topScore);
|
|
34581
|
+
}
|
|
34582
|
+
// Get the best move from localStorage
|
|
34583
|
+
static getBestLocalMove(userId, date) {
|
|
34584
|
+
const moves = this.loadLocalMoves(userId, date);
|
|
34585
|
+
if (moves.length === 0) {
|
|
34586
|
+
return null;
|
|
34587
|
+
}
|
|
34588
|
+
// Find the move with the highest score
|
|
34589
|
+
return moves.reduce((best, current) => current.score > best.score ? current : best);
|
|
34590
|
+
}
|
|
34591
|
+
// Clean up entries older than MAX_AGE_DAYS
|
|
34592
|
+
static cleanupOldEntries() {
|
|
34593
|
+
try {
|
|
34594
|
+
const now = new Date();
|
|
34595
|
+
const cutoffDate = new Date(now);
|
|
34596
|
+
cutoffDate.setDate(cutoffDate.getDate() - this.MAX_AGE_DAYS);
|
|
34597
|
+
const keysToRemove = [];
|
|
34598
|
+
// Iterate through localStorage keys
|
|
34599
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
34600
|
+
const key = localStorage.key(i);
|
|
34601
|
+
if (key && key.startsWith(this.STORAGE_KEY_PREFIX)) {
|
|
34602
|
+
// Extract date from key: "gata_YYYY-MM-DD_userId"
|
|
34603
|
+
const parts = key.split('_');
|
|
34604
|
+
if (parts.length >= 2) {
|
|
34605
|
+
const dateStr = parts[1];
|
|
34606
|
+
const entryDate = new Date(dateStr);
|
|
34607
|
+
if (!isNaN(entryDate.getTime()) && entryDate < cutoffDate) {
|
|
34608
|
+
keysToRemove.push(key);
|
|
34609
|
+
}
|
|
34610
|
+
}
|
|
34611
|
+
}
|
|
34612
|
+
}
|
|
34613
|
+
// Remove old entries
|
|
34614
|
+
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
34615
|
+
}
|
|
34616
|
+
catch (e) {
|
|
34617
|
+
console.error('Failed to cleanup old entries:', e);
|
|
34618
|
+
}
|
|
34619
|
+
}
|
|
34620
|
+
// Clear all persistence for a specific user
|
|
34621
|
+
static clearUserData(userId) {
|
|
34622
|
+
const keysToRemove = [];
|
|
34623
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
34624
|
+
const key = localStorage.key(i);
|
|
34625
|
+
if (key && key.startsWith(this.STORAGE_KEY_PREFIX) && key.endsWith(`_${userId}`)) {
|
|
34626
|
+
keysToRemove.push(key);
|
|
34627
|
+
}
|
|
34628
|
+
}
|
|
34629
|
+
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
34630
|
+
}
|
|
34631
|
+
// === Firebase Read Methods ===
|
|
34632
|
+
// Note: All Firebase write operations (achievements, stats, global best, leaderboard)
|
|
34633
|
+
// are now handled by the backend server in the /gatadagsins/submit endpoint.
|
|
34634
|
+
// The client only handles localStorage persistence and Firebase reads for display.
|
|
34635
|
+
// Get leaderboard for a specific date
|
|
34636
|
+
static async getLeaderboard(date, locale, limit = 10) {
|
|
34637
|
+
try {
|
|
34638
|
+
const leadersPath = `gatadagsins/${date}/${locale}/leaders`;
|
|
34639
|
+
const leaders = await getFirebaseData(leadersPath);
|
|
34640
|
+
if (!leaders) {
|
|
34641
|
+
return [];
|
|
34642
|
+
}
|
|
34643
|
+
// Convert object to array and sort by score
|
|
34644
|
+
const entries = Object.values(leaders);
|
|
34645
|
+
entries.sort((a, b) => b.score - a.score);
|
|
34646
|
+
return entries.slice(0, limit);
|
|
34647
|
+
}
|
|
34648
|
+
catch (error) {
|
|
34649
|
+
console.error('Failed to get leaderboard:', error);
|
|
34650
|
+
return [];
|
|
34651
|
+
}
|
|
34652
|
+
}
|
|
34653
|
+
// Get user's streak statistics
|
|
34654
|
+
static async getUserStats(userId, locale) {
|
|
34655
|
+
try {
|
|
34656
|
+
const statsPath = `gatadagsins/users/${locale}/${userId}/stats`;
|
|
34657
|
+
const stats = await getFirebaseData(statsPath);
|
|
34658
|
+
return stats;
|
|
34659
|
+
}
|
|
34660
|
+
catch (error) {
|
|
34661
|
+
console.error('Failed to get user stats:', error);
|
|
34662
|
+
return null;
|
|
34663
|
+
}
|
|
34664
|
+
}
|
|
34665
|
+
}
|
|
34666
|
+
RiddlePersistence.STORAGE_KEY_PREFIX = 'gata_';
|
|
34667
|
+
RiddlePersistence.MAX_AGE_DAYS = 30;
|
|
34668
|
+
|
|
34357
34669
|
/*
|
|
34358
34670
|
|
|
34359
34671
|
Riddle.ts
|
|
@@ -34368,8 +34680,8 @@ class Game extends BaseGame {
|
|
|
34368
34680
|
For further information, see https://github.com/mideind/Netskrafl
|
|
34369
34681
|
|
|
34370
34682
|
*/
|
|
34371
|
-
const HOT_WARM_BOUNDARY_RATIO = 0.
|
|
34372
|
-
const WARM_COLD_BOUNDARY_RATIO = 0.
|
|
34683
|
+
const HOT_WARM_BOUNDARY_RATIO = 0.5;
|
|
34684
|
+
const WARM_COLD_BOUNDARY_RATIO = 0.25;
|
|
34373
34685
|
class Riddle extends BaseGame {
|
|
34374
34686
|
constructor(uuid, date, model) {
|
|
34375
34687
|
if (!model.state) {
|
|
@@ -34441,6 +34753,22 @@ class Riddle extends BaseGame {
|
|
|
34441
34753
|
this.hotBoundary = this.bestPossibleScore * HOT_WARM_BOUNDARY_RATIO;
|
|
34442
34754
|
// Initialize word checker
|
|
34443
34755
|
wordChecker.ingestTwoLetterWords(this.locale, this.two_letter_words[0]);
|
|
34756
|
+
// Load persisted player moves from localStorage
|
|
34757
|
+
if (state.userId) {
|
|
34758
|
+
const persistedMoves = RiddlePersistence.loadLocalMoves(state.userId, date);
|
|
34759
|
+
if (persistedMoves.length > 0) {
|
|
34760
|
+
// Convert from IPlayerMove to RiddleWord format, preserving timestamps
|
|
34761
|
+
this.playerMoves = persistedMoves.map(move => ({
|
|
34762
|
+
word: move.word,
|
|
34763
|
+
score: move.score,
|
|
34764
|
+
coord: move.coord,
|
|
34765
|
+
timestamp: move.timestamp || new Date().toISOString() // Use stored timestamp or fallback
|
|
34766
|
+
}));
|
|
34767
|
+
// Update personal best score from persisted moves
|
|
34768
|
+
const bestMove = persistedMoves.reduce((best, current) => current.score > best.score ? current : best);
|
|
34769
|
+
this.personalBestScore = bestMove.score;
|
|
34770
|
+
}
|
|
34771
|
+
}
|
|
34444
34772
|
}
|
|
34445
34773
|
}
|
|
34446
34774
|
catch (error) {
|
|
@@ -34461,6 +34789,7 @@ class Riddle extends BaseGame {
|
|
|
34461
34789
|
locale: this.locale,
|
|
34462
34790
|
userId: state.userId,
|
|
34463
34791
|
groupId: state.userGroupId || null,
|
|
34792
|
+
userDisplayName: state.userFullname || state.userNick || state.userId,
|
|
34464
34793
|
move,
|
|
34465
34794
|
}
|
|
34466
34795
|
});
|
|
@@ -34509,13 +34838,26 @@ class Riddle extends BaseGame {
|
|
|
34509
34838
|
// If the move is not valid or was already played, return
|
|
34510
34839
|
if (!move)
|
|
34511
34840
|
return;
|
|
34512
|
-
|
|
34513
|
-
|
|
34514
|
-
|
|
34515
|
-
|
|
34516
|
-
|
|
34517
|
-
|
|
34518
|
-
|
|
34841
|
+
const { state } = this;
|
|
34842
|
+
if (!state || !state.userId)
|
|
34843
|
+
return;
|
|
34844
|
+
// Save all moves to localStorage (local backup/cache)
|
|
34845
|
+
// Convert RiddleWord[] to IPlayerMove[] for persistence
|
|
34846
|
+
const movesToSave = this.playerMoves.map(m => ({
|
|
34847
|
+
score: m.score,
|
|
34848
|
+
word: m.word,
|
|
34849
|
+
coord: m.coord,
|
|
34850
|
+
timestamp: m.timestamp
|
|
34851
|
+
}));
|
|
34852
|
+
RiddlePersistence.saveLocalMoves(state.userId, this.date, movesToSave);
|
|
34853
|
+
// If the move does not improve the personal best, we're done
|
|
34854
|
+
if (move.score <= this.personalBestScore)
|
|
34855
|
+
return;
|
|
34856
|
+
// This is the best score we've seen yet
|
|
34857
|
+
this.personalBestScore = move.score;
|
|
34858
|
+
// Submit to server; the server handles all Firebase updates
|
|
34859
|
+
// (achievements, stats, global best, leaderboard)
|
|
34860
|
+
this.submitRiddleWord(move);
|
|
34519
34861
|
}
|
|
34520
34862
|
updateGlobalBestScore(best) {
|
|
34521
34863
|
// Update the global best score, typically as a result
|
|
@@ -34742,6 +35084,9 @@ class Model {
|
|
|
34742
35084
|
this.game = null;
|
|
34743
35085
|
// The current Gáta Dagsins riddle, if any
|
|
34744
35086
|
this.riddle = null;
|
|
35087
|
+
// Gáta Dagsins-specific properties
|
|
35088
|
+
this.userStats = null;
|
|
35089
|
+
this.leaderboard = [];
|
|
34745
35090
|
// The current Netskrafl game list
|
|
34746
35091
|
this.gameList = null;
|
|
34747
35092
|
// Number of games where it's the player's turn, plus count of zombie games
|
|
@@ -35849,6 +36194,12 @@ class Actions {
|
|
|
35849
36194
|
if (state === null || state === void 0 ? void 0 : state.userGroupId) {
|
|
35850
36195
|
attachFirebaseListener(basePath + `group/${state.userGroupId}/best`, (json, firstAttach) => this.onRiddleGroupScoreUpdate(json, firstAttach));
|
|
35851
36196
|
}
|
|
36197
|
+
// Listen to global leaderboard
|
|
36198
|
+
attachFirebaseListener(basePath + "leaders", (json, firstAttach) => this.onLeaderboardUpdate(json, firstAttach));
|
|
36199
|
+
// Listen to user stats (if user is logged in)
|
|
36200
|
+
if (state === null || state === void 0 ? void 0 : state.userId) {
|
|
36201
|
+
attachFirebaseListener(`gatadagsins/users/${locale}/${state.userId}/stats`, (json, firstAttach) => this.onUserStatsUpdate(json, firstAttach));
|
|
36202
|
+
}
|
|
35852
36203
|
}
|
|
35853
36204
|
detachListenerFromRiddle(date, locale) {
|
|
35854
36205
|
const { state } = this.model;
|
|
@@ -35857,6 +36208,10 @@ class Actions {
|
|
|
35857
36208
|
if (state === null || state === void 0 ? void 0 : state.userGroupId) {
|
|
35858
36209
|
detachFirebaseListener(basePath + `group/${state.userGroupId}/best`);
|
|
35859
36210
|
}
|
|
36211
|
+
detachFirebaseListener(basePath + "leaders");
|
|
36212
|
+
if (state === null || state === void 0 ? void 0 : state.userId) {
|
|
36213
|
+
detachFirebaseListener(`gatadagsins/users/${locale}/${state.userId}/stats`);
|
|
36214
|
+
}
|
|
35860
36215
|
}
|
|
35861
36216
|
onRiddleGlobalScoreUpdate(json, firstAttach) {
|
|
35862
36217
|
const { riddle } = this.model;
|
|
@@ -35874,6 +36229,38 @@ class Actions {
|
|
|
35874
36229
|
riddle.updateGroupBestScore(json);
|
|
35875
36230
|
m.redraw();
|
|
35876
36231
|
}
|
|
36232
|
+
onLeaderboardUpdate(json, firstAttach) {
|
|
36233
|
+
if (!json || typeof json !== 'object') {
|
|
36234
|
+
this.model.leaderboard = [];
|
|
36235
|
+
}
|
|
36236
|
+
else {
|
|
36237
|
+
// Convert dictionary to array and sort by score (desc), then timestamp (desc)
|
|
36238
|
+
const entries = Object.keys(json).map(userId => ({
|
|
36239
|
+
userId: json[userId].userId || userId,
|
|
36240
|
+
displayName: json[userId].displayName || '',
|
|
36241
|
+
score: json[userId].score || 0,
|
|
36242
|
+
timestamp: json[userId].timestamp || ''
|
|
36243
|
+
}));
|
|
36244
|
+
// Sort by score descending, then by timestamp ascending (earlier first)
|
|
36245
|
+
entries.sort((a, b) => {
|
|
36246
|
+
if (b.score !== a.score) {
|
|
36247
|
+
return b.score - a.score;
|
|
36248
|
+
}
|
|
36249
|
+
return a.timestamp.localeCompare(b.timestamp);
|
|
36250
|
+
});
|
|
36251
|
+
this.model.leaderboard = entries;
|
|
36252
|
+
}
|
|
36253
|
+
m.redraw();
|
|
36254
|
+
}
|
|
36255
|
+
onUserStatsUpdate(json, firstAttach) {
|
|
36256
|
+
if (!json) {
|
|
36257
|
+
this.model.userStats = null;
|
|
36258
|
+
}
|
|
36259
|
+
else {
|
|
36260
|
+
this.model.userStats = json;
|
|
36261
|
+
}
|
|
36262
|
+
m.redraw();
|
|
36263
|
+
}
|
|
35877
36264
|
async fetchRiddle(date, locale) {
|
|
35878
36265
|
// Create the game via model
|
|
35879
36266
|
if (!this.model)
|
|
@@ -36064,13 +36451,14 @@ const Netskrafl = React.memo(NetskraflImpl);
|
|
|
36064
36451
|
*/
|
|
36065
36452
|
const RiddleScore = {
|
|
36066
36453
|
view: (vnode) => {
|
|
36067
|
-
const { riddle } = vnode.attrs;
|
|
36454
|
+
const { riddle, mode = "desktop" } = vnode.attrs;
|
|
36068
36455
|
if (!riddle)
|
|
36069
36456
|
return m("div");
|
|
36070
36457
|
const score = riddle.currentScore;
|
|
36071
36458
|
const hasValidMove = score !== undefined;
|
|
36072
36459
|
const hasTiles = riddle.tilesPlaced().length > 0;
|
|
36073
|
-
|
|
36460
|
+
const baseClass = (mode === "mobile" ? ".mobile-score" : ".gata-dagsins-score");
|
|
36461
|
+
let classes = [baseClass];
|
|
36074
36462
|
let displayText = "0";
|
|
36075
36463
|
if (!hasTiles) {
|
|
36076
36464
|
// State 1: No tiles on board - grayed/disabled, showing zero
|
|
@@ -36104,7 +36492,8 @@ const RiddleScore = {
|
|
|
36104
36492
|
classes.push(".celebrate");
|
|
36105
36493
|
}
|
|
36106
36494
|
}
|
|
36107
|
-
|
|
36495
|
+
const legendClass = mode === "mobile" ? ".mobile-score-legend" : ".gata-dagsins-legend";
|
|
36496
|
+
return m("div" + classes.join(""), m("span" + legendClass, displayText));
|
|
36108
36497
|
}
|
|
36109
36498
|
};
|
|
36110
36499
|
|
|
@@ -36164,35 +36553,40 @@ const GataDagsinsBoardAndRack = {
|
|
|
36164
36553
|
*/
|
|
36165
36554
|
const SunCorona = {
|
|
36166
36555
|
view: (vnode) => {
|
|
36167
|
-
const { animate = false } = vnode.attrs;
|
|
36556
|
+
const { animate = false, size = 80 } = vnode.attrs;
|
|
36557
|
+
// Calculate ray positions based on size
|
|
36558
|
+
// For 80px: rays from -40 to -20 (inner radius 20px, outer radius 40px)
|
|
36559
|
+
// For 90px: rays from -45 to -30 (inner radius 30px, outer radius 45px) to fit 60px circle
|
|
36560
|
+
const outerRadius = size / 2;
|
|
36561
|
+
const innerRadius = size / 3;
|
|
36168
36562
|
return m("div.sun-corona" + (animate ? ".rotating" : ""), [
|
|
36169
36563
|
m.trust(`
|
|
36170
|
-
<svg width="
|
|
36564
|
+
<svg width="${size}" height="${size}" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
36171
36565
|
<g transform="translate(50,50)">
|
|
36172
36566
|
<!-- Ray at 0° (12 o'clock) -->
|
|
36173
|
-
<polygon points="0
|
|
36567
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(0)"/>
|
|
36174
36568
|
<!-- Ray at 30° -->
|
|
36175
|
-
<polygon points="0
|
|
36569
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(30)"/>
|
|
36176
36570
|
<!-- Ray at 60° -->
|
|
36177
|
-
<polygon points="0
|
|
36571
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(60)"/>
|
|
36178
36572
|
<!-- Ray at 90° (3 o'clock) -->
|
|
36179
|
-
<polygon points="0
|
|
36573
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(90)"/>
|
|
36180
36574
|
<!-- Ray at 120° -->
|
|
36181
|
-
<polygon points="0
|
|
36575
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(120)"/>
|
|
36182
36576
|
<!-- Ray at 150° -->
|
|
36183
|
-
<polygon points="0
|
|
36577
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(150)"/>
|
|
36184
36578
|
<!-- Ray at 180° (6 o'clock) -->
|
|
36185
|
-
<polygon points="0
|
|
36579
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(180)"/>
|
|
36186
36580
|
<!-- Ray at 210° -->
|
|
36187
|
-
<polygon points="0
|
|
36581
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(210)"/>
|
|
36188
36582
|
<!-- Ray at 240° -->
|
|
36189
|
-
<polygon points="0
|
|
36583
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(240)"/>
|
|
36190
36584
|
<!-- Ray at 270° (9 o'clock) -->
|
|
36191
|
-
<polygon points="0
|
|
36585
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(270)"/>
|
|
36192
36586
|
<!-- Ray at 300° -->
|
|
36193
|
-
<polygon points="0
|
|
36587
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(300)"/>
|
|
36194
36588
|
<!-- Ray at 330° -->
|
|
36195
|
-
<polygon points="0
|
|
36589
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(330)"/>
|
|
36196
36590
|
</g>
|
|
36197
36591
|
</svg>
|
|
36198
36592
|
`)
|
|
@@ -36200,6 +36594,112 @@ const SunCorona = {
|
|
|
36200
36594
|
}
|
|
36201
36595
|
};
|
|
36202
36596
|
|
|
36597
|
+
/*
|
|
36598
|
+
|
|
36599
|
+
Mobile-Status.ts
|
|
36600
|
+
|
|
36601
|
+
Mobile-only horizontal status component for Gáta Dagsins
|
|
36602
|
+
|
|
36603
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36604
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36605
|
+
|
|
36606
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36607
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36608
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36609
|
+
|
|
36610
|
+
*/
|
|
36611
|
+
// Mobile-only horizontal status display
|
|
36612
|
+
const MobileStatus = () => {
|
|
36613
|
+
return {
|
|
36614
|
+
view: (vnode) => {
|
|
36615
|
+
const { riddle, selectedMoves, bestMove, onMoveClick, onStatsClick } = vnode.attrs;
|
|
36616
|
+
const { bestPossibleScore, globalBestScore } = riddle;
|
|
36617
|
+
// Determine if player achieved best possible score
|
|
36618
|
+
const achieved = bestMove !== undefined;
|
|
36619
|
+
const celebrate = bestMove && bestMove.word !== "";
|
|
36620
|
+
// Get player's current best score
|
|
36621
|
+
// If the player achieved the best possible score, it's in bestMove (not selectedMoves)
|
|
36622
|
+
const playerBestScore = (bestMove && bestMove.word !== "")
|
|
36623
|
+
? bestMove.score
|
|
36624
|
+
: (selectedMoves.length > 0 ? selectedMoves[0].score : 0);
|
|
36625
|
+
// Determine current leader score (may be this player or another)
|
|
36626
|
+
let leaderScore = 0;
|
|
36627
|
+
let isPlayerLeading = false;
|
|
36628
|
+
if (globalBestScore && globalBestScore.score > 0) {
|
|
36629
|
+
leaderScore = globalBestScore.score;
|
|
36630
|
+
// Check if player is leading
|
|
36631
|
+
isPlayerLeading = playerBestScore >= globalBestScore.score;
|
|
36632
|
+
}
|
|
36633
|
+
else {
|
|
36634
|
+
leaderScore = playerBestScore;
|
|
36635
|
+
isPlayerLeading = playerBestScore > 0;
|
|
36636
|
+
}
|
|
36637
|
+
return m(".mobile-status-container", [
|
|
36638
|
+
// Current word score (leftmost) - uses RiddleScore component in mobile mode
|
|
36639
|
+
m(".mobile-status-item.left", m(RiddleScore, { riddle, mode: "mobile" })),
|
|
36640
|
+
// Interactive card containing player best and leader scores
|
|
36641
|
+
m(".mobile-status-card", {
|
|
36642
|
+
onclick: onStatsClick,
|
|
36643
|
+
title: ts("Tölfræði og stigatafla")
|
|
36644
|
+
}, [
|
|
36645
|
+
// Player's best score
|
|
36646
|
+
m(".mobile-status-card-item.player-best", [
|
|
36647
|
+
m(".mobile-status-label", ts("Þín besta:")),
|
|
36648
|
+
m(".mobile-status-score", playerBestScore.toString())
|
|
36649
|
+
]),
|
|
36650
|
+
// Current leader score
|
|
36651
|
+
m(".mobile-status-card-item.leader" + (isPlayerLeading ? ".is-player" : ""), [
|
|
36652
|
+
m(".mobile-status-label", isPlayerLeading ? ts("Þú leiðir!") : ts("Leiðandi:")),
|
|
36653
|
+
m(".mobile-status-score", leaderScore.toString())
|
|
36654
|
+
]),
|
|
36655
|
+
// Chevron indicator (overlaid at bottom center)
|
|
36656
|
+
m(".mobile-status-card-icon", glyph("chevron-down"))
|
|
36657
|
+
]),
|
|
36658
|
+
// Best possible score
|
|
36659
|
+
m(".mobile-status-item.right.best-possible"
|
|
36660
|
+
+ (celebrate ? ".celebrate" : "")
|
|
36661
|
+
+ (achieved ? ".achieved" : ""), {
|
|
36662
|
+
onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
|
|
36663
|
+
}, [
|
|
36664
|
+
// Wrapper for score and corona to position them together
|
|
36665
|
+
m(".mobile-best-score-wrapper", [
|
|
36666
|
+
celebrate ? m(SunCorona, { animate: true, size: 100 }) : null,
|
|
36667
|
+
m(".mobile-status-score", bestPossibleScore.toString())
|
|
36668
|
+
])
|
|
36669
|
+
])
|
|
36670
|
+
]);
|
|
36671
|
+
}
|
|
36672
|
+
};
|
|
36673
|
+
};
|
|
36674
|
+
|
|
36675
|
+
/*
|
|
36676
|
+
|
|
36677
|
+
TabBar.ts
|
|
36678
|
+
|
|
36679
|
+
Reusable tab navigation component
|
|
36680
|
+
|
|
36681
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36682
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36683
|
+
|
|
36684
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36685
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36686
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36687
|
+
|
|
36688
|
+
*/
|
|
36689
|
+
const TabBar = {
|
|
36690
|
+
view: (vnode) => {
|
|
36691
|
+
const { tabs, activeTab, onTabChange } = vnode.attrs;
|
|
36692
|
+
return m(".tab-bar", tabs.map(tab => m(".tab-item" + (activeTab === tab.id ? ".active" : ""), {
|
|
36693
|
+
key: tab.id,
|
|
36694
|
+
onclick: () => onTabChange(tab.id)
|
|
36695
|
+
}, [
|
|
36696
|
+
tab.iconGlyph ? m("span.tab-icon", glyph(tab.iconGlyph)) :
|
|
36697
|
+
tab.icon ? m("span.tab-icon", tab.icon) : null,
|
|
36698
|
+
m("span.tab-label", tab.label)
|
|
36699
|
+
])));
|
|
36700
|
+
}
|
|
36701
|
+
};
|
|
36702
|
+
|
|
36203
36703
|
/*
|
|
36204
36704
|
|
|
36205
36705
|
Thermometer.ts
|
|
@@ -36274,12 +36774,27 @@ const BestPossibleScore = () => {
|
|
|
36274
36774
|
return {
|
|
36275
36775
|
view: (vnode) => {
|
|
36276
36776
|
const { score, bestMove, onMoveClick } = vnode.attrs;
|
|
36277
|
-
|
|
36278
|
-
|
|
36279
|
-
|
|
36777
|
+
// Determine the label based on achievement status
|
|
36778
|
+
let topLabel;
|
|
36779
|
+
if (bestMove !== undefined) {
|
|
36780
|
+
if (bestMove.word) {
|
|
36781
|
+
// Current player achieved it - show their word
|
|
36782
|
+
topLabel = removeBlankMarkers(bestMove.word);
|
|
36783
|
+
}
|
|
36784
|
+
else {
|
|
36785
|
+
// Someone else achieved it - indicate this
|
|
36786
|
+
topLabel = ts("Bestu lögn náð!");
|
|
36787
|
+
}
|
|
36788
|
+
}
|
|
36789
|
+
else {
|
|
36790
|
+
// Not achieved yet - show default label
|
|
36791
|
+
topLabel = ts("Besta mögulega lögn");
|
|
36792
|
+
}
|
|
36793
|
+
const achieved = bestMove !== undefined;
|
|
36280
36794
|
const celebrate = bestMove && bestMove.word !== "";
|
|
36281
36795
|
return m(".thermometer-best-score"
|
|
36282
|
-
+ (celebrate ? ".celebrate" : "")
|
|
36796
|
+
+ (celebrate ? ".celebrate" : "")
|
|
36797
|
+
+ (achieved ? ".achieved" : ""), m(".thermometer-best-score-container", {
|
|
36283
36798
|
onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
|
|
36284
36799
|
}, [
|
|
36285
36800
|
// Sun corona behind the circle when celebrating
|
|
@@ -36407,6 +36922,196 @@ const Thermometer = () => {
|
|
|
36407
36922
|
};
|
|
36408
36923
|
};
|
|
36409
36924
|
|
|
36925
|
+
/*
|
|
36926
|
+
|
|
36927
|
+
StatsView.ts
|
|
36928
|
+
|
|
36929
|
+
User statistics display component for Gáta Dagsins
|
|
36930
|
+
|
|
36931
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36932
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36933
|
+
|
|
36934
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36935
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36936
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36937
|
+
|
|
36938
|
+
*/
|
|
36939
|
+
const StatsView = {
|
|
36940
|
+
view: (vnode) => {
|
|
36941
|
+
const { stats, loading = false } = vnode.attrs;
|
|
36942
|
+
if (loading) {
|
|
36943
|
+
return m(".stats-view.loading", m(".loading-message", ts("Sæki tölfræði...")));
|
|
36944
|
+
}
|
|
36945
|
+
if (!stats) {
|
|
36946
|
+
return m(".stats-view.empty", m(".empty-message", ts("Engin tölfræði til að sýna")));
|
|
36947
|
+
}
|
|
36948
|
+
const statItems = [
|
|
36949
|
+
{
|
|
36950
|
+
iconGlyph: "fire",
|
|
36951
|
+
label: ts("Núverandi striklota"),
|
|
36952
|
+
value: stats.currentStreak,
|
|
36953
|
+
highlight: stats.currentStreak > 0
|
|
36954
|
+
},
|
|
36955
|
+
{
|
|
36956
|
+
iconGlyph: "star",
|
|
36957
|
+
label: ts("Lengsta striklota"),
|
|
36958
|
+
value: stats.longestStreak
|
|
36959
|
+
},
|
|
36960
|
+
{
|
|
36961
|
+
iconGlyph: "tower",
|
|
36962
|
+
label: ts("Hæsta skori náð"),
|
|
36963
|
+
value: stats.totalTopScores
|
|
36964
|
+
},
|
|
36965
|
+
{
|
|
36966
|
+
iconGlyph: "certificate",
|
|
36967
|
+
label: ts("Striklota hæsta skors"),
|
|
36968
|
+
value: stats.topScoreStreak,
|
|
36969
|
+
highlight: stats.topScoreStreak > 0
|
|
36970
|
+
},
|
|
36971
|
+
{
|
|
36972
|
+
iconGlyph: "calendar",
|
|
36973
|
+
label: ts("Heildarfjöldi daga"),
|
|
36974
|
+
value: stats.totalDaysPlayed
|
|
36975
|
+
},
|
|
36976
|
+
];
|
|
36977
|
+
return m(".stats-view", m(".stats-grid", statItems.map((item, index) => m(".stat-item" + (item.highlight ? ".highlight" : ""), { key: index }, [
|
|
36978
|
+
m(".stat-icon", glyph(item.iconGlyph)),
|
|
36979
|
+
m(".stat-info", [
|
|
36980
|
+
m(".stat-label", item.label),
|
|
36981
|
+
m(".stat-value", item.value.toString())
|
|
36982
|
+
])
|
|
36983
|
+
]))));
|
|
36984
|
+
}
|
|
36985
|
+
};
|
|
36986
|
+
|
|
36987
|
+
/*
|
|
36988
|
+
|
|
36989
|
+
LeaderboardView.ts
|
|
36990
|
+
|
|
36991
|
+
Daily leaderboard display component for Gáta Dagsins
|
|
36992
|
+
|
|
36993
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36994
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36995
|
+
|
|
36996
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36997
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36998
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36999
|
+
|
|
37000
|
+
*/
|
|
37001
|
+
function getMedalIcon(rank) {
|
|
37002
|
+
switch (rank) {
|
|
37003
|
+
case 1: return "🥇";
|
|
37004
|
+
case 2: return "🥈";
|
|
37005
|
+
case 3: return "🥉";
|
|
37006
|
+
default: return null;
|
|
37007
|
+
}
|
|
37008
|
+
}
|
|
37009
|
+
function formatDate(dateStr) {
|
|
37010
|
+
// Format YYYY-MM-DD to Icelandic date (e.g., "2. okt.")
|
|
37011
|
+
const date = new Date(dateStr + "T00:00:00");
|
|
37012
|
+
const day = date.getDate();
|
|
37013
|
+
const months = [
|
|
37014
|
+
"janúar", "febrúar", "mars", "apríl", "maí", "júní",
|
|
37015
|
+
"júlí", "ágúst", "september", "október", "nóvember", "desember",
|
|
37016
|
+
];
|
|
37017
|
+
const month = months[date.getMonth()];
|
|
37018
|
+
return `${day}. ${month}`;
|
|
37019
|
+
}
|
|
37020
|
+
const LeaderboardView = {
|
|
37021
|
+
view: (vnode) => {
|
|
37022
|
+
const { leaderboard, currentUserId, date, loading = false } = vnode.attrs;
|
|
37023
|
+
if (loading) {
|
|
37024
|
+
return m(".leaderboard-view.loading", m(".loading-message", ts("Hleð stigatöflu...")));
|
|
37025
|
+
}
|
|
37026
|
+
if (!leaderboard || leaderboard.length === 0) {
|
|
37027
|
+
return m(".leaderboard-view.empty", m(".empty-message", ts("Engin stig skráð enn")));
|
|
37028
|
+
}
|
|
37029
|
+
return m(".leaderboard-view", [
|
|
37030
|
+
m(".leaderboard-header", [
|
|
37031
|
+
m(".leaderboard-title", formatDate(date)),
|
|
37032
|
+
]),
|
|
37033
|
+
m(".leaderboard-list", leaderboard.map((entry, index) => {
|
|
37034
|
+
const rank = index + 1;
|
|
37035
|
+
const isCurrentUser = entry.userId === currentUserId;
|
|
37036
|
+
const medal = getMedalIcon(rank);
|
|
37037
|
+
return m(".leaderboard-entry" + (isCurrentUser ? ".current-user" : ""), { key: entry.userId }, [
|
|
37038
|
+
m(".entry-rank", [
|
|
37039
|
+
medal ? m("span.medal", medal) : m("span.rank-number", rank.toString())
|
|
37040
|
+
]),
|
|
37041
|
+
m(".entry-name", isCurrentUser ? ts("Þú") : entry.displayName),
|
|
37042
|
+
m(".entry-score", entry.score.toString())
|
|
37043
|
+
]);
|
|
37044
|
+
}))
|
|
37045
|
+
]);
|
|
37046
|
+
}
|
|
37047
|
+
};
|
|
37048
|
+
|
|
37049
|
+
/*
|
|
37050
|
+
|
|
37051
|
+
RightSideTabs.ts
|
|
37052
|
+
|
|
37053
|
+
Desktop tabbed container for Performance/Stats/Leaderboard in Gáta Dagsins
|
|
37054
|
+
|
|
37055
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
37056
|
+
Author: Vilhjálmur Þorsteinsson
|
|
37057
|
+
|
|
37058
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
37059
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
37060
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
37061
|
+
|
|
37062
|
+
*/
|
|
37063
|
+
const RightSideTabs = () => {
|
|
37064
|
+
// Component-local state for active tab (defaults to performance)
|
|
37065
|
+
let activeTab = "performance";
|
|
37066
|
+
const tabs = [
|
|
37067
|
+
{ id: "performance", label: ts("Frammistaða"), iconGlyph: "dashboard" },
|
|
37068
|
+
{ id: "stats", label: ts("Tölfræði"), iconGlyph: "stats" },
|
|
37069
|
+
{ id: "leaderboard", label: ts("Stigatafla"), iconGlyph: "tower" }
|
|
37070
|
+
];
|
|
37071
|
+
return {
|
|
37072
|
+
view: (vnode) => {
|
|
37073
|
+
const { view, selectedMoves, bestMove, onMoveClick } = vnode.attrs;
|
|
37074
|
+
const { riddle, state } = view.model;
|
|
37075
|
+
if (!riddle) {
|
|
37076
|
+
return m(".gatadagsins-right-side-tabs", "");
|
|
37077
|
+
}
|
|
37078
|
+
const handleTabChange = (tabId) => {
|
|
37079
|
+
activeTab = tabId;
|
|
37080
|
+
};
|
|
37081
|
+
return m(".gatadagsins-right-side-tabs", [
|
|
37082
|
+
// Tab navigation
|
|
37083
|
+
m(TabBar, {
|
|
37084
|
+
tabs,
|
|
37085
|
+
activeTab,
|
|
37086
|
+
onTabChange: handleTabChange
|
|
37087
|
+
}),
|
|
37088
|
+
// Tab content
|
|
37089
|
+
m(".tab-content", [
|
|
37090
|
+
// Performance tab (thermometer)
|
|
37091
|
+
activeTab === "performance" ? m(Thermometer, {
|
|
37092
|
+
riddle,
|
|
37093
|
+
selectedMoves,
|
|
37094
|
+
bestMove,
|
|
37095
|
+
onMoveClick
|
|
37096
|
+
}) : null,
|
|
37097
|
+
// Stats tab
|
|
37098
|
+
activeTab === "stats" ? m(StatsView, {
|
|
37099
|
+
stats: view.model.userStats || null,
|
|
37100
|
+
loading: false
|
|
37101
|
+
}) : null,
|
|
37102
|
+
// Leaderboard tab
|
|
37103
|
+
activeTab === "leaderboard" ? m(LeaderboardView, {
|
|
37104
|
+
leaderboard: view.model.leaderboard || [],
|
|
37105
|
+
currentUserId: (state === null || state === void 0 ? void 0 : state.userId) || "",
|
|
37106
|
+
date: riddle.date,
|
|
37107
|
+
loading: false
|
|
37108
|
+
}) : null
|
|
37109
|
+
])
|
|
37110
|
+
]);
|
|
37111
|
+
}
|
|
37112
|
+
};
|
|
37113
|
+
};
|
|
37114
|
+
|
|
36410
37115
|
/*
|
|
36411
37116
|
|
|
36412
37117
|
GataDagsins-Right-Side.ts
|
|
@@ -36422,9 +37127,9 @@ const Thermometer = () => {
|
|
|
36422
37127
|
|
|
36423
37128
|
*/
|
|
36424
37129
|
const GataDagsinsRightSide = {
|
|
36425
|
-
// Component containing
|
|
37130
|
+
// Component containing both mobile status bar and desktop tabbed view
|
|
36426
37131
|
view: (vnode) => {
|
|
36427
|
-
const { view, selectedMoves, bestMove } = vnode.attrs;
|
|
37132
|
+
const { view, selectedMoves, bestMove, onStatsClick } = vnode.attrs;
|
|
36428
37133
|
const { riddle } = view.model;
|
|
36429
37134
|
const handleMoveClick = (word, coord) => {
|
|
36430
37135
|
if (riddle && word && coord) {
|
|
@@ -36433,11 +37138,19 @@ const GataDagsinsRightSide = {
|
|
|
36433
37138
|
}
|
|
36434
37139
|
};
|
|
36435
37140
|
return m(".gatadagsins-right-side-wrapper", riddle ? [
|
|
36436
|
-
//
|
|
36437
|
-
m(".gatadagsins-
|
|
37141
|
+
// Mobile-only status bar (visible on mobile, hidden on desktop)
|
|
37142
|
+
m(".gatadagsins-mobile-status", m(MobileStatus, {
|
|
36438
37143
|
riddle,
|
|
36439
37144
|
selectedMoves,
|
|
36440
37145
|
bestMove,
|
|
37146
|
+
onMoveClick: handleMoveClick,
|
|
37147
|
+
onStatsClick
|
|
37148
|
+
})),
|
|
37149
|
+
// Desktop-only tabbed view (hidden on mobile, visible on desktop)
|
|
37150
|
+
m(".gatadagsins-thermometer-column", m(RightSideTabs, {
|
|
37151
|
+
view,
|
|
37152
|
+
selectedMoves,
|
|
37153
|
+
bestMove,
|
|
36441
37154
|
onMoveClick: handleMoveClick
|
|
36442
37155
|
})),
|
|
36443
37156
|
] : null);
|
|
@@ -36461,67 +37174,150 @@ const GataDagsinsRightSide = {
|
|
|
36461
37174
|
const GataDagsinsHelp = {
|
|
36462
37175
|
view: (vnode) => {
|
|
36463
37176
|
const closeHelp = vnode.attrs.onClose;
|
|
36464
|
-
return
|
|
36465
|
-
//
|
|
36466
|
-
m(".modal-
|
|
36467
|
-
|
|
36468
|
-
|
|
36469
|
-
|
|
36470
|
-
|
|
36471
|
-
|
|
36472
|
-
|
|
36473
|
-
|
|
36474
|
-
|
|
36475
|
-
|
|
36476
|
-
|
|
36477
|
-
|
|
36478
|
-
|
|
36479
|
-
m("li", "Þú færð borð með allmörgum stöfum sem þegar hafa verið lagðir."),
|
|
36480
|
-
m("li", "Neðst á skjánum eru stafaflísar sem þú getur notað til að mynda orð."),
|
|
36481
|
-
m("li", "Dragðu flísar á borðið til að mynda orð, annaðhvort lárétt eða lóðrétt."),
|
|
36482
|
-
m("li", "Orðin verða að tengjast við stafi sem fyrir eru á borðinu."),
|
|
36483
|
-
m("li", "Þú sérð jafnóðum hvort lögnin á borðinu er gild og hversu mörg stig hún gefur."),
|
|
36484
|
-
m("li", "Þú getur prófað eins mörg orð og þú vilt - besta skorið þitt er vistað."),
|
|
36485
|
-
]),
|
|
36486
|
-
m("h3", "Stigagjöf"),
|
|
36487
|
-
m("p", "Þú færð stig fyrir hvern staf í orðinu, auk bónusstiga fyrir lengri orð:"),
|
|
36488
|
-
m("ul", [
|
|
36489
|
-
m("li", "Hver stafur gefur 1-10 stig eftir gildi hans"),
|
|
36490
|
-
m("li", "Orð sem nota allar 7 stafaflísarnar gefa 50 stiga bónus"),
|
|
36491
|
-
m("li", "Sumir reitir á borðinu tvöfalda eða þrefalda stafagildið"),
|
|
36492
|
-
m("li", "Sumir reitir tvöfalda eða þrefalda heildarorðagildið"),
|
|
36493
|
-
]),
|
|
36494
|
-
m("h3", "Hitamælir"),
|
|
36495
|
-
m("p", "Hitamælirinn hægra megin (eða efst á farsímum) sýnir:"),
|
|
36496
|
-
m("ul", [
|
|
36497
|
-
m("li", m("strong", "Besta mögulega skor:"), " Hæstu stig sem hægt er að ná á þessu borði."),
|
|
36498
|
-
m("li", m("strong", "Besta skor dagsins:"), " Hæstu stig sem einhver leikmaður hefur náð í dag."),
|
|
36499
|
-
m("li", m("strong", "Þín bestu orð:"), " Orðin sem þú hefur lagt og stigin fyrir þau."),
|
|
36500
|
-
m("li", "Þú getur smellt á orð á hitamælinum til að fá þá lögn aftur á borðið."),
|
|
36501
|
-
]),
|
|
36502
|
-
m("h3", "Ábendingar"),
|
|
36503
|
-
m("ul", [
|
|
36504
|
-
m("li", "Reyndu að nota dýra stafi (eins og X, Ý, Þ) á tvöföldunar- eða þreföldunarreitum."),
|
|
36505
|
-
m("li", "Lengri orð gefa mun fleiri stig vegna bónussins."),
|
|
36506
|
-
m("li", "Þú getur dregið allar flísar til baka með bláa endurkalls-hnappnum."),
|
|
36507
|
-
m("li", "Ný gáta birtist á hverjum nýjum degi - klukkan 00:00!"),
|
|
37177
|
+
return [
|
|
37178
|
+
// Backdrop
|
|
37179
|
+
m(".modal-backdrop-netskrafl", {
|
|
37180
|
+
onclick: (e) => { e.preventDefault(); },
|
|
37181
|
+
onwheel: (e) => { e.preventDefault(); e.stopPropagation(); },
|
|
37182
|
+
ontouchmove: (e) => { e.preventDefault(); e.stopPropagation(); }
|
|
37183
|
+
}),
|
|
37184
|
+
m(".modal-dialog.gatadagsins-help", m(".modal-content", [
|
|
37185
|
+
// Header with close button
|
|
37186
|
+
m(".modal-header", [
|
|
37187
|
+
m("h2", "Um Gátu dagsins"),
|
|
37188
|
+
m("button.close", {
|
|
37189
|
+
onclick: closeHelp,
|
|
37190
|
+
"aria-label": "Loka"
|
|
37191
|
+
}, m("span", { "aria-hidden": "true" }, "×"))
|
|
36508
37192
|
]),
|
|
36509
|
-
|
|
36510
|
-
m("
|
|
36511
|
-
"Gáta dagsins er
|
|
36512
|
-
|
|
36513
|
-
",
|
|
36514
|
-
"
|
|
37193
|
+
// Body with help content
|
|
37194
|
+
m(".modal-body", [
|
|
37195
|
+
m("p", "Gáta dagsins er dagleg krossgátuþraut, svipuð skrafli, þar sem þú reynir að finna " +
|
|
37196
|
+
"stigahæsta orðið sem hægt er að mynda með gefnum stöfum."),
|
|
37197
|
+
m("h3", "Hvernig á að spila"),
|
|
37198
|
+
m("ul", [
|
|
37199
|
+
m("li", "Þú færð borð með allmörgum stöfum sem þegar hafa verið lagðir."),
|
|
37200
|
+
m("li", "Neðst á skjánum eru stafaflísar sem þú getur notað til að mynda orð."),
|
|
37201
|
+
m("li", "Dragðu flísar á borðið til að mynda orð, annaðhvort lárétt eða lóðrétt."),
|
|
37202
|
+
m("li", "Orðin verða að tengjast við stafi sem fyrir eru á borðinu."),
|
|
37203
|
+
m("li", "Þú sérð jafnóðum hvort lögnin á borðinu er gild og hversu mörg stig hún gefur."),
|
|
37204
|
+
m("li", "Þú getur prófað eins mörg orð og þú vilt - besta skorið þitt er vistað."),
|
|
37205
|
+
]),
|
|
37206
|
+
m("h3", "Stigagjöf"),
|
|
37207
|
+
m("p", "Þú færð stig fyrir hvern staf í orðinu, auk bónusstiga fyrir lengri orð:"),
|
|
37208
|
+
m("ul", [
|
|
37209
|
+
m("li", "Hver stafur gefur 1-10 stig eftir gildi hans"),
|
|
37210
|
+
m("li", "Orð sem nota allar 7 stafaflísarnar gefa 50 stiga bónus"),
|
|
37211
|
+
m("li", "Sumir reitir á borðinu tvöfalda eða þrefalda stafagildið"),
|
|
37212
|
+
m("li", "Sumir reitir tvöfalda eða þrefalda heildarorðagildið"),
|
|
37213
|
+
]),
|
|
37214
|
+
m("h3", "Hitamælir"),
|
|
37215
|
+
m("p", "Hitamælirinn hægra megin (eða efst á farsímum) sýnir:"),
|
|
37216
|
+
m("ul", [
|
|
37217
|
+
m("li", m("strong", "Besta mögulega skor:"), " Hæstu stig sem hægt er að ná á þessu borði."),
|
|
37218
|
+
m("li", m("strong", "Besta skor dagsins:"), " Hæstu stig sem einhver leikmaður hefur náð í dag."),
|
|
37219
|
+
m("li", m("strong", "Þín bestu orð:"), " Orðin sem þú hefur lagt og stigin fyrir þau."),
|
|
37220
|
+
m("li", "Þú getur smellt á orð á hitamælinum til að fá þá lögn aftur á borðið."),
|
|
37221
|
+
]),
|
|
37222
|
+
m("h3", "Ábendingar"),
|
|
37223
|
+
m("ul", [
|
|
37224
|
+
m("li", "Reyndu að nota dýra stafi (eins og X, Ý, Þ) á tvöföldunar- eða þreföldunarreitum."),
|
|
37225
|
+
m("li", "Lengri orð gefa mun fleiri stig vegna bónussins."),
|
|
37226
|
+
m("li", "Þú getur dregið allar flísar til baka með bláa endurkalls-hnappnum."),
|
|
37227
|
+
m("li", "Ný gáta birtist á hverjum nýjum degi - klukkan 00:00!"),
|
|
37228
|
+
]),
|
|
37229
|
+
m("h3", "Um leikinn"),
|
|
37230
|
+
m("p", [
|
|
37231
|
+
"Gáta dagsins er systkini ",
|
|
37232
|
+
m("a", { href: "https://netskrafl.is", target: "_blank" }, "Netskrafls"),
|
|
37233
|
+
", hins sívinsæla íslenska krossgátuleiks á netinu. ",
|
|
37234
|
+
"Leikurinn er þróaður af Miðeind ehf."
|
|
37235
|
+
]),
|
|
36515
37236
|
]),
|
|
36516
|
-
|
|
36517
|
-
|
|
36518
|
-
|
|
36519
|
-
|
|
36520
|
-
|
|
36521
|
-
]
|
|
37237
|
+
// Footer with close button
|
|
37238
|
+
m(".modal-footer", m("button.btn.btn-primary", {
|
|
37239
|
+
onclick: closeHelp
|
|
37240
|
+
}, "Loka"))
|
|
37241
|
+
])),
|
|
37242
|
+
];
|
|
36522
37243
|
}
|
|
36523
37244
|
};
|
|
36524
37245
|
|
|
37246
|
+
/*
|
|
37247
|
+
|
|
37248
|
+
StatsModal.ts
|
|
37249
|
+
|
|
37250
|
+
Mobile modal for stats and leaderboard
|
|
37251
|
+
|
|
37252
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
37253
|
+
Author: Vilhjálmur Þorsteinsson
|
|
37254
|
+
|
|
37255
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
37256
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
37257
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
37258
|
+
|
|
37259
|
+
*/
|
|
37260
|
+
const StatsModal = () => {
|
|
37261
|
+
// Component-local state for active tab (defaults to stats)
|
|
37262
|
+
let activeTab = "stats";
|
|
37263
|
+
const tabs = [
|
|
37264
|
+
{ id: "stats", label: ts("Tölfræði"), iconGlyph: "stats" },
|
|
37265
|
+
{ id: "leaderboard", label: ts("Stigatafla"), iconGlyph: "tower" }
|
|
37266
|
+
];
|
|
37267
|
+
return {
|
|
37268
|
+
view: (vnode) => {
|
|
37269
|
+
const { view, onClose } = vnode.attrs;
|
|
37270
|
+
const { riddle, state } = view.model;
|
|
37271
|
+
if (!riddle) {
|
|
37272
|
+
return null;
|
|
37273
|
+
}
|
|
37274
|
+
const handleTabChange = (tabId) => {
|
|
37275
|
+
activeTab = tabId;
|
|
37276
|
+
};
|
|
37277
|
+
return [
|
|
37278
|
+
// Backdrop
|
|
37279
|
+
m(".modal-backdrop-netskrafl", {
|
|
37280
|
+
onclick: (e) => { e.preventDefault(); },
|
|
37281
|
+
onwheel: (e) => { e.preventDefault(); e.stopPropagation(); },
|
|
37282
|
+
ontouchmove: (e) => { e.preventDefault(); e.stopPropagation(); }
|
|
37283
|
+
}),
|
|
37284
|
+
// Modal dialog
|
|
37285
|
+
m(".modal-dialog.stats-modal", {
|
|
37286
|
+
onwheel: (e) => { e.stopPropagation(); },
|
|
37287
|
+
ontouchmove: (e) => { e.stopPropagation(); }
|
|
37288
|
+
}, [
|
|
37289
|
+
m(".modal-content", [
|
|
37290
|
+
// Close button in top right
|
|
37291
|
+
m("button.close", {
|
|
37292
|
+
onclick: onClose,
|
|
37293
|
+
"aria-label": "Loka"
|
|
37294
|
+
}, glyph("remove")),
|
|
37295
|
+
// Tab navigation
|
|
37296
|
+
m(TabBar, {
|
|
37297
|
+
tabs,
|
|
37298
|
+
activeTab,
|
|
37299
|
+
onTabChange: handleTabChange
|
|
37300
|
+
}),
|
|
37301
|
+
// Modal body with tab content
|
|
37302
|
+
m(".modal-body", [
|
|
37303
|
+
activeTab === "stats" ? m(StatsView, {
|
|
37304
|
+
stats: view.model.userStats || null,
|
|
37305
|
+
loading: false
|
|
37306
|
+
}) : null,
|
|
37307
|
+
activeTab === "leaderboard" ? m(LeaderboardView, {
|
|
37308
|
+
leaderboard: view.model.leaderboard || [],
|
|
37309
|
+
currentUserId: (state === null || state === void 0 ? void 0 : state.userId) || "",
|
|
37310
|
+
date: riddle.date,
|
|
37311
|
+
loading: false
|
|
37312
|
+
}) : null
|
|
37313
|
+
])
|
|
37314
|
+
])
|
|
37315
|
+
])
|
|
37316
|
+
];
|
|
37317
|
+
}
|
|
37318
|
+
};
|
|
37319
|
+
};
|
|
37320
|
+
|
|
36525
37321
|
/*
|
|
36526
37322
|
|
|
36527
37323
|
GataDagsins.ts
|
|
@@ -36537,11 +37333,19 @@ const GataDagsinsHelp = {
|
|
|
36537
37333
|
|
|
36538
37334
|
*/
|
|
36539
37335
|
const MAX_MOVES_TO_DISPLAY = 10;
|
|
36540
|
-
const
|
|
36541
|
-
var _a;
|
|
37336
|
+
const currentMoveState = (riddle) => {
|
|
37337
|
+
var _a, _b, _c;
|
|
37338
|
+
const thisPlayer = ((_a = riddle.model.state) === null || _a === void 0 ? void 0 : _a.userId) || "";
|
|
37339
|
+
const { bestPossibleScore, // The highest score achievable for this riddle
|
|
37340
|
+
globalBestScore, // The best score achieved by any player
|
|
37341
|
+
groupBestScore, // The best score achieved within the player's group
|
|
37342
|
+
playerMoves, } = riddle;
|
|
37343
|
+
// If the player has equaled the best possible score,
|
|
37344
|
+
// the winning word is stored here and displayed at the top
|
|
37345
|
+
let bestMove = undefined;
|
|
36542
37346
|
// Sort moves by score in descending order and
|
|
36543
37347
|
// cut the tail off the list to only include the top moves
|
|
36544
|
-
const
|
|
37348
|
+
const selectedMoves = playerMoves
|
|
36545
37349
|
.sort((a, b) => b.score - a.score)
|
|
36546
37350
|
.slice(0, MAX_MOVES_TO_DISPLAY)
|
|
36547
37351
|
.map(move => ({
|
|
@@ -36549,61 +37353,49 @@ const selectTopMoves = (thisPlayer, moves, globalBestScore, groupBestScore) => {
|
|
|
36549
37353
|
word: move.word,
|
|
36550
37354
|
coord: move.coord,
|
|
36551
37355
|
}));
|
|
36552
|
-
//
|
|
36553
|
-
// the player's own top score, we include it as the first move
|
|
37356
|
+
// Check whether we need to add or annotate the global best score
|
|
36554
37357
|
if (globalBestScore && globalBestScore.score > 0) {
|
|
36555
37358
|
const { score, word, coord } = globalBestScore;
|
|
36556
|
-
if (((
|
|
37359
|
+
if (((_c = (_b = selectedMoves[0]) === null || _b === void 0 ? void 0 : _b.score) !== null && _c !== void 0 ? _c : 0) >= score) {
|
|
36557
37360
|
// This player has made a move that scores the same
|
|
36558
37361
|
// or better as the top score: mark the move
|
|
36559
|
-
|
|
37362
|
+
selectedMoves[0].isGlobalBestScore = true;
|
|
36560
37363
|
}
|
|
36561
37364
|
else if (globalBestScore.player === thisPlayer) {
|
|
36562
37365
|
// This player holds the global best score, probably
|
|
36563
|
-
// from a previous session
|
|
36564
|
-
|
|
37366
|
+
// from a previous session, so it's not already
|
|
37367
|
+
// in the selectedMoves list: add it as a move
|
|
37368
|
+
selectedMoves.unshift({ score, isGlobalBestScore: true, word, coord });
|
|
36565
37369
|
}
|
|
36566
37370
|
else {
|
|
36567
|
-
// This is a global best score from another player
|
|
36568
|
-
|
|
37371
|
+
// This is a global best score from another player
|
|
37372
|
+
selectedMoves.unshift({ score, isGlobalBestScore: true, word: "", coord: "" });
|
|
36569
37373
|
}
|
|
36570
37374
|
}
|
|
36571
|
-
//
|
|
36572
|
-
|
|
36573
|
-
|
|
36574
|
-
const currentMoveState = (riddle) => {
|
|
36575
|
-
var _a;
|
|
36576
|
-
const thisPlayer = ((_a = riddle.model.state) === null || _a === void 0 ? void 0 : _a.userId) || "";
|
|
36577
|
-
const { bestPossibleScore, globalBestScore, groupBestScore, playerMoves, } = riddle;
|
|
36578
|
-
// If the player has equaled the best possible score,
|
|
36579
|
-
// the winning word is stored here and displayed at the top
|
|
36580
|
-
let bestMove = undefined;
|
|
36581
|
-
// Apply the move selection and allocation algorithm
|
|
36582
|
-
const selectedMoves = selectTopMoves(thisPlayer, playerMoves, globalBestScore);
|
|
36583
|
-
// If the top-scoring move has the bestPossibleScore,
|
|
36584
|
-
// extract it from the move list
|
|
37375
|
+
// Check if the best possible score has been achieved, by this player
|
|
37376
|
+
// or another player. If so, we remove it from the move list, since we
|
|
37377
|
+
// only display it at the top of the thermometer.
|
|
36585
37378
|
if (selectedMoves.length > 0 && selectedMoves[0].score === bestPossibleScore) {
|
|
36586
|
-
|
|
36587
|
-
// The word was played by this player
|
|
36588
|
-
bestMove = selectedMoves.shift();
|
|
36589
|
-
}
|
|
37379
|
+
bestMove = selectedMoves.shift();
|
|
36590
37380
|
}
|
|
36591
37381
|
return { selectedMoves, bestMove };
|
|
36592
37382
|
};
|
|
36593
37383
|
const GataDagsins$1 = () => {
|
|
36594
37384
|
// A view of the Gáta Dagsins page
|
|
36595
37385
|
let showHelp = false;
|
|
37386
|
+
let showStatsModal = false;
|
|
36596
37387
|
return {
|
|
36597
|
-
oninit: (vnode) => {
|
|
37388
|
+
oninit: async (vnode) => {
|
|
36598
37389
|
const { model, actions } = vnode.attrs.view;
|
|
36599
37390
|
const { riddle } = model;
|
|
36600
37391
|
if (!riddle) {
|
|
36601
37392
|
const { date, locale } = vnode.attrs;
|
|
36602
37393
|
// Initialize a fresh riddle object if it doesn't exist
|
|
36603
|
-
actions.fetchRiddle(date, locale);
|
|
37394
|
+
await actions.fetchRiddle(date, locale);
|
|
36604
37395
|
}
|
|
36605
|
-
// Initialize
|
|
37396
|
+
// Initialize dialog states
|
|
36606
37397
|
showHelp = false;
|
|
37398
|
+
showStatsModal = false;
|
|
36607
37399
|
},
|
|
36608
37400
|
view: (vnode) => {
|
|
36609
37401
|
var _a;
|
|
@@ -36617,6 +37409,10 @@ const GataDagsins$1 = () => {
|
|
|
36617
37409
|
showHelp = !showHelp;
|
|
36618
37410
|
m.redraw();
|
|
36619
37411
|
};
|
|
37412
|
+
const toggleStatsModal = () => {
|
|
37413
|
+
showStatsModal = !showStatsModal;
|
|
37414
|
+
m.redraw();
|
|
37415
|
+
};
|
|
36620
37416
|
return m("div.drop-target", {
|
|
36621
37417
|
id: "gatadagsins-background",
|
|
36622
37418
|
}, [
|
|
@@ -36627,7 +37423,7 @@ const GataDagsins$1 = () => {
|
|
|
36627
37423
|
// Board and rack component (left side)
|
|
36628
37424
|
m(GataDagsinsBoardAndRack, { view }),
|
|
36629
37425
|
// Right-side component with scores and comparisons
|
|
36630
|
-
m(GataDagsinsRightSide, { view, selectedMoves, bestMove }),
|
|
37426
|
+
m(GataDagsinsRightSide, { view, selectedMoves, bestMove, onStatsClick: toggleStatsModal }),
|
|
36631
37427
|
// Blank dialog
|
|
36632
37428
|
riddle.askingForBlank
|
|
36633
37429
|
? m(BlankDialog, { game: riddle })
|
|
@@ -36642,10 +37438,9 @@ const GataDagsins$1 = () => {
|
|
|
36642
37438
|
// Custom Info button for GataDagsins that shows help dialog
|
|
36643
37439
|
m(".info", { title: ts("Upplýsingar og hjálp") }, m("a.iconlink", { href: "#", onclick: (e) => { e.preventDefault(); toggleHelp(); } }, glyph("info-sign"))),
|
|
36644
37440
|
// Help dialog and backdrop
|
|
36645
|
-
showHelp ?
|
|
36646
|
-
|
|
36647
|
-
|
|
36648
|
-
] : "",
|
|
37441
|
+
showHelp ? m(GataDagsinsHelp, { onClose: toggleHelp }) : "",
|
|
37442
|
+
// Stats modal and backdrop (mobile only)
|
|
37443
|
+
showStatsModal ? m(StatsModal, { view, onClose: toggleStatsModal }) : "",
|
|
36649
37444
|
]);
|
|
36650
37445
|
}
|
|
36651
37446
|
};
|
|
@@ -36684,11 +37479,22 @@ async function main(state, container) {
|
|
|
36684
37479
|
const model = new Model(settings, state);
|
|
36685
37480
|
const actions = new Actions(model);
|
|
36686
37481
|
const view = new View(actions);
|
|
37482
|
+
// Get date from URL parameter, fallback to today
|
|
37483
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
37484
|
+
const dateParam = urlParams.get('date');
|
|
36687
37485
|
const today = new Date().toISOString().split("T")[0];
|
|
37486
|
+
const riddleDate = dateParam || today;
|
|
37487
|
+
// Validate date format (YYYY-MM-DD)
|
|
37488
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
37489
|
+
const validDate = dateRegex.test(riddleDate) ? riddleDate : today;
|
|
36688
37490
|
const locale = state.locale || "is_IS";
|
|
37491
|
+
// Log the date being used (helpful for debugging)
|
|
37492
|
+
if (dateParam) {
|
|
37493
|
+
console.log(`Loading Gáta Dagsins for date: ${validDate} (from URL parameter)`);
|
|
37494
|
+
}
|
|
36689
37495
|
// Mount the Gáta Dagsins UI using an anonymous closure component
|
|
36690
37496
|
m.mount(container, {
|
|
36691
|
-
view: () => m(GataDagsins$1, { view, date:
|
|
37497
|
+
view: () => m(GataDagsins$1, { view, date: validDate, locale }),
|
|
36692
37498
|
});
|
|
36693
37499
|
}
|
|
36694
37500
|
catch (e) {
|
|
@@ -36698,6 +37504,8 @@ async function main(state, container) {
|
|
|
36698
37504
|
return "success";
|
|
36699
37505
|
}
|
|
36700
37506
|
|
|
37507
|
+
// Note: To load a specific date for debugging, use URL parameter: ?date=YYYY-MM-DD
|
|
37508
|
+
// Example: http://localhost:6006/?date=2025-01-25
|
|
36701
37509
|
const mountForUser = async (state) => {
|
|
36702
37510
|
// Return a DOM tree containing a mounted Gáta Dagsins UI
|
|
36703
37511
|
// for the user specified in the state object
|