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