@mideind/netskrafl-react 1.0.2 → 1.1.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 +622 -49
- package/dist/cjs/index.js +892 -109
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/css/netskrafl.css +622 -49
- package/dist/esm/index.js +892 -109
- package/dist/esm/index.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -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
|
|
@@ -34352,6 +34478,161 @@ class Game extends BaseGame {
|
|
|
34352
34478
|
;
|
|
34353
34479
|
} // class Game
|
|
34354
34480
|
|
|
34481
|
+
/*
|
|
34482
|
+
|
|
34483
|
+
riddlePersistence.ts
|
|
34484
|
+
|
|
34485
|
+
Local persistence for Gáta Dagsins using localStorage
|
|
34486
|
+
|
|
34487
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
34488
|
+
|
|
34489
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
34490
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
34491
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
34492
|
+
|
|
34493
|
+
*/
|
|
34494
|
+
class RiddlePersistence {
|
|
34495
|
+
// Generate storage key for a specific user and date
|
|
34496
|
+
static getStorageKey(userId, date) {
|
|
34497
|
+
return `${this.STORAGE_KEY_PREFIX}${date}_${userId}`;
|
|
34498
|
+
}
|
|
34499
|
+
// Save complete move list to localStorage
|
|
34500
|
+
static saveLocalMoves(userId, date, moves) {
|
|
34501
|
+
if (!userId || !date) {
|
|
34502
|
+
return;
|
|
34503
|
+
}
|
|
34504
|
+
const data = {
|
|
34505
|
+
date,
|
|
34506
|
+
moves,
|
|
34507
|
+
timestamp: new Date().toISOString(),
|
|
34508
|
+
userId,
|
|
34509
|
+
};
|
|
34510
|
+
try {
|
|
34511
|
+
const key = this.getStorageKey(userId, date);
|
|
34512
|
+
localStorage.setItem(key, JSON.stringify(data));
|
|
34513
|
+
// Clean up old entries while we're here
|
|
34514
|
+
this.cleanupOldEntries();
|
|
34515
|
+
}
|
|
34516
|
+
catch (e) {
|
|
34517
|
+
// Handle localStorage quota errors silently
|
|
34518
|
+
console.error('Failed to save moves to localStorage:', e);
|
|
34519
|
+
}
|
|
34520
|
+
}
|
|
34521
|
+
// Load move history from localStorage
|
|
34522
|
+
static loadLocalMoves(userId, date) {
|
|
34523
|
+
if (!userId || !date) {
|
|
34524
|
+
return [];
|
|
34525
|
+
}
|
|
34526
|
+
try {
|
|
34527
|
+
const key = this.getStorageKey(userId, date);
|
|
34528
|
+
const stored = localStorage.getItem(key);
|
|
34529
|
+
if (!stored) {
|
|
34530
|
+
return [];
|
|
34531
|
+
}
|
|
34532
|
+
const data = JSON.parse(stored);
|
|
34533
|
+
// Verify that this data belongs to the correct (current) user
|
|
34534
|
+
if (!data.userId || data.userId !== userId) {
|
|
34535
|
+
return [];
|
|
34536
|
+
}
|
|
34537
|
+
return data.moves || [];
|
|
34538
|
+
}
|
|
34539
|
+
catch (e) {
|
|
34540
|
+
console.error('Failed to load moves from localStorage:', e);
|
|
34541
|
+
return [];
|
|
34542
|
+
}
|
|
34543
|
+
}
|
|
34544
|
+
// Check if user has achieved top score (local check)
|
|
34545
|
+
static hasAchievedTopScore(userId, date, topScore) {
|
|
34546
|
+
const moves = this.loadLocalMoves(userId, date);
|
|
34547
|
+
return moves.some(move => move.score >= topScore);
|
|
34548
|
+
}
|
|
34549
|
+
// Get the best move from localStorage
|
|
34550
|
+
static getBestLocalMove(userId, date) {
|
|
34551
|
+
const moves = this.loadLocalMoves(userId, date);
|
|
34552
|
+
if (moves.length === 0) {
|
|
34553
|
+
return null;
|
|
34554
|
+
}
|
|
34555
|
+
// Find the move with the highest score
|
|
34556
|
+
return moves.reduce((best, current) => current.score > best.score ? current : best);
|
|
34557
|
+
}
|
|
34558
|
+
// Clean up entries older than MAX_AGE_DAYS
|
|
34559
|
+
static cleanupOldEntries() {
|
|
34560
|
+
try {
|
|
34561
|
+
const now = new Date();
|
|
34562
|
+
const cutoffDate = new Date(now);
|
|
34563
|
+
cutoffDate.setDate(cutoffDate.getDate() - this.MAX_AGE_DAYS);
|
|
34564
|
+
const keysToRemove = [];
|
|
34565
|
+
// Iterate through localStorage keys
|
|
34566
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
34567
|
+
const key = localStorage.key(i);
|
|
34568
|
+
if (key && key.startsWith(this.STORAGE_KEY_PREFIX)) {
|
|
34569
|
+
// Extract date from key: "gata_YYYY-MM-DD_userId"
|
|
34570
|
+
const parts = key.split('_');
|
|
34571
|
+
if (parts.length >= 2) {
|
|
34572
|
+
const dateStr = parts[1];
|
|
34573
|
+
const entryDate = new Date(dateStr);
|
|
34574
|
+
if (!isNaN(entryDate.getTime()) && entryDate < cutoffDate) {
|
|
34575
|
+
keysToRemove.push(key);
|
|
34576
|
+
}
|
|
34577
|
+
}
|
|
34578
|
+
}
|
|
34579
|
+
}
|
|
34580
|
+
// Remove old entries
|
|
34581
|
+
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
34582
|
+
}
|
|
34583
|
+
catch (e) {
|
|
34584
|
+
console.error('Failed to cleanup old entries:', e);
|
|
34585
|
+
}
|
|
34586
|
+
}
|
|
34587
|
+
// Clear all persistence for a specific user
|
|
34588
|
+
static clearUserData(userId) {
|
|
34589
|
+
const keysToRemove = [];
|
|
34590
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
34591
|
+
const key = localStorage.key(i);
|
|
34592
|
+
if (key && key.startsWith(this.STORAGE_KEY_PREFIX) && key.endsWith(`_${userId}`)) {
|
|
34593
|
+
keysToRemove.push(key);
|
|
34594
|
+
}
|
|
34595
|
+
}
|
|
34596
|
+
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
34597
|
+
}
|
|
34598
|
+
// === Firebase Read Methods ===
|
|
34599
|
+
// Note: All Firebase write operations (achievements, stats, global best, leaderboard)
|
|
34600
|
+
// are now handled by the backend server in the /gatadagsins/submit endpoint.
|
|
34601
|
+
// The client only handles localStorage persistence and Firebase reads for display.
|
|
34602
|
+
// Get leaderboard for a specific date
|
|
34603
|
+
static async getLeaderboard(date, locale, limit = 10) {
|
|
34604
|
+
try {
|
|
34605
|
+
const leadersPath = `gatadagsins/${date}/${locale}/leaders`;
|
|
34606
|
+
const leaders = await getFirebaseData(leadersPath);
|
|
34607
|
+
if (!leaders) {
|
|
34608
|
+
return [];
|
|
34609
|
+
}
|
|
34610
|
+
// Convert object to array and sort by score
|
|
34611
|
+
const entries = Object.values(leaders);
|
|
34612
|
+
entries.sort((a, b) => b.score - a.score);
|
|
34613
|
+
return entries.slice(0, limit);
|
|
34614
|
+
}
|
|
34615
|
+
catch (error) {
|
|
34616
|
+
console.error('Failed to get leaderboard:', error);
|
|
34617
|
+
return [];
|
|
34618
|
+
}
|
|
34619
|
+
}
|
|
34620
|
+
// Get user's streak statistics
|
|
34621
|
+
static async getUserStats(userId, locale) {
|
|
34622
|
+
try {
|
|
34623
|
+
const statsPath = `gatadagsins/users/${locale}/${userId}/stats`;
|
|
34624
|
+
const stats = await getFirebaseData(statsPath);
|
|
34625
|
+
return stats;
|
|
34626
|
+
}
|
|
34627
|
+
catch (error) {
|
|
34628
|
+
console.error('Failed to get user stats:', error);
|
|
34629
|
+
return null;
|
|
34630
|
+
}
|
|
34631
|
+
}
|
|
34632
|
+
}
|
|
34633
|
+
RiddlePersistence.STORAGE_KEY_PREFIX = 'gata_';
|
|
34634
|
+
RiddlePersistence.MAX_AGE_DAYS = 30;
|
|
34635
|
+
|
|
34355
34636
|
/*
|
|
34356
34637
|
|
|
34357
34638
|
Riddle.ts
|
|
@@ -34366,8 +34647,8 @@ class Game extends BaseGame {
|
|
|
34366
34647
|
For further information, see https://github.com/mideind/Netskrafl
|
|
34367
34648
|
|
|
34368
34649
|
*/
|
|
34369
|
-
const HOT_WARM_BOUNDARY_RATIO = 0.
|
|
34370
|
-
const WARM_COLD_BOUNDARY_RATIO = 0.
|
|
34650
|
+
const HOT_WARM_BOUNDARY_RATIO = 0.5;
|
|
34651
|
+
const WARM_COLD_BOUNDARY_RATIO = 0.25;
|
|
34371
34652
|
class Riddle extends BaseGame {
|
|
34372
34653
|
constructor(uuid, date, model) {
|
|
34373
34654
|
if (!model.state) {
|
|
@@ -34439,6 +34720,22 @@ class Riddle extends BaseGame {
|
|
|
34439
34720
|
this.hotBoundary = this.bestPossibleScore * HOT_WARM_BOUNDARY_RATIO;
|
|
34440
34721
|
// Initialize word checker
|
|
34441
34722
|
wordChecker.ingestTwoLetterWords(this.locale, this.two_letter_words[0]);
|
|
34723
|
+
// Load persisted player moves from localStorage
|
|
34724
|
+
if (state.userId) {
|
|
34725
|
+
const persistedMoves = RiddlePersistence.loadLocalMoves(state.userId, date);
|
|
34726
|
+
if (persistedMoves.length > 0) {
|
|
34727
|
+
// Convert from IPlayerMove to RiddleWord format, preserving timestamps
|
|
34728
|
+
this.playerMoves = persistedMoves.map(move => ({
|
|
34729
|
+
word: move.word,
|
|
34730
|
+
score: move.score,
|
|
34731
|
+
coord: move.coord,
|
|
34732
|
+
timestamp: move.timestamp || new Date().toISOString() // Use stored timestamp or fallback
|
|
34733
|
+
}));
|
|
34734
|
+
// Update personal best score from persisted moves
|
|
34735
|
+
const bestMove = persistedMoves.reduce((best, current) => current.score > best.score ? current : best);
|
|
34736
|
+
this.personalBestScore = bestMove.score;
|
|
34737
|
+
}
|
|
34738
|
+
}
|
|
34442
34739
|
}
|
|
34443
34740
|
}
|
|
34444
34741
|
catch (error) {
|
|
@@ -34459,6 +34756,7 @@ class Riddle extends BaseGame {
|
|
|
34459
34756
|
locale: this.locale,
|
|
34460
34757
|
userId: state.userId,
|
|
34461
34758
|
groupId: state.userGroupId || null,
|
|
34759
|
+
userDisplayName: state.userFullname || state.userNick || state.userId,
|
|
34462
34760
|
move,
|
|
34463
34761
|
}
|
|
34464
34762
|
});
|
|
@@ -34507,13 +34805,26 @@ class Riddle extends BaseGame {
|
|
|
34507
34805
|
// If the move is not valid or was already played, return
|
|
34508
34806
|
if (!move)
|
|
34509
34807
|
return;
|
|
34510
|
-
|
|
34511
|
-
|
|
34512
|
-
|
|
34513
|
-
|
|
34514
|
-
|
|
34515
|
-
|
|
34516
|
-
|
|
34808
|
+
const { state } = this;
|
|
34809
|
+
if (!state || !state.userId)
|
|
34810
|
+
return;
|
|
34811
|
+
// Save all moves to localStorage (local backup/cache)
|
|
34812
|
+
// Convert RiddleWord[] to IPlayerMove[] for persistence
|
|
34813
|
+
const movesToSave = this.playerMoves.map(m => ({
|
|
34814
|
+
score: m.score,
|
|
34815
|
+
word: m.word,
|
|
34816
|
+
coord: m.coord,
|
|
34817
|
+
timestamp: m.timestamp
|
|
34818
|
+
}));
|
|
34819
|
+
RiddlePersistence.saveLocalMoves(state.userId, this.date, movesToSave);
|
|
34820
|
+
// If the move does not improve the personal best, we're done
|
|
34821
|
+
if (move.score <= this.personalBestScore)
|
|
34822
|
+
return;
|
|
34823
|
+
// This is the best score we've seen yet
|
|
34824
|
+
this.personalBestScore = move.score;
|
|
34825
|
+
// Submit to server; the server handles all Firebase updates
|
|
34826
|
+
// (achievements, stats, global best, leaderboard)
|
|
34827
|
+
this.submitRiddleWord(move);
|
|
34517
34828
|
}
|
|
34518
34829
|
updateGlobalBestScore(best) {
|
|
34519
34830
|
// Update the global best score, typically as a result
|
|
@@ -34740,6 +35051,9 @@ class Model {
|
|
|
34740
35051
|
this.game = null;
|
|
34741
35052
|
// The current Gáta Dagsins riddle, if any
|
|
34742
35053
|
this.riddle = null;
|
|
35054
|
+
// Gáta Dagsins-specific properties
|
|
35055
|
+
this.userStats = null;
|
|
35056
|
+
this.leaderboard = [];
|
|
34743
35057
|
// The current Netskrafl game list
|
|
34744
35058
|
this.gameList = null;
|
|
34745
35059
|
// Number of games where it's the player's turn, plus count of zombie games
|
|
@@ -35847,6 +36161,12 @@ class Actions {
|
|
|
35847
36161
|
if (state === null || state === void 0 ? void 0 : state.userGroupId) {
|
|
35848
36162
|
attachFirebaseListener(basePath + `group/${state.userGroupId}/best`, (json, firstAttach) => this.onRiddleGroupScoreUpdate(json, firstAttach));
|
|
35849
36163
|
}
|
|
36164
|
+
// Listen to global leaderboard
|
|
36165
|
+
attachFirebaseListener(basePath + "leaders", (json, firstAttach) => this.onLeaderboardUpdate(json, firstAttach));
|
|
36166
|
+
// Listen to user stats (if user is logged in)
|
|
36167
|
+
if (state === null || state === void 0 ? void 0 : state.userId) {
|
|
36168
|
+
attachFirebaseListener(`gatadagsins/users/${locale}/${state.userId}/stats`, (json, firstAttach) => this.onUserStatsUpdate(json, firstAttach));
|
|
36169
|
+
}
|
|
35850
36170
|
}
|
|
35851
36171
|
detachListenerFromRiddle(date, locale) {
|
|
35852
36172
|
const { state } = this.model;
|
|
@@ -35855,6 +36175,10 @@ class Actions {
|
|
|
35855
36175
|
if (state === null || state === void 0 ? void 0 : state.userGroupId) {
|
|
35856
36176
|
detachFirebaseListener(basePath + `group/${state.userGroupId}/best`);
|
|
35857
36177
|
}
|
|
36178
|
+
detachFirebaseListener(basePath + "leaders");
|
|
36179
|
+
if (state === null || state === void 0 ? void 0 : state.userId) {
|
|
36180
|
+
detachFirebaseListener(`gatadagsins/users/${locale}/${state.userId}/stats`);
|
|
36181
|
+
}
|
|
35858
36182
|
}
|
|
35859
36183
|
onRiddleGlobalScoreUpdate(json, firstAttach) {
|
|
35860
36184
|
const { riddle } = this.model;
|
|
@@ -35872,6 +36196,38 @@ class Actions {
|
|
|
35872
36196
|
riddle.updateGroupBestScore(json);
|
|
35873
36197
|
m.redraw();
|
|
35874
36198
|
}
|
|
36199
|
+
onLeaderboardUpdate(json, firstAttach) {
|
|
36200
|
+
if (!json || typeof json !== 'object') {
|
|
36201
|
+
this.model.leaderboard = [];
|
|
36202
|
+
}
|
|
36203
|
+
else {
|
|
36204
|
+
// Convert dictionary to array and sort by score (desc), then timestamp (desc)
|
|
36205
|
+
const entries = Object.keys(json).map(userId => ({
|
|
36206
|
+
userId: json[userId].userId || userId,
|
|
36207
|
+
displayName: json[userId].displayName || '',
|
|
36208
|
+
score: json[userId].score || 0,
|
|
36209
|
+
timestamp: json[userId].timestamp || ''
|
|
36210
|
+
}));
|
|
36211
|
+
// Sort by score descending, then by timestamp descending (newer first)
|
|
36212
|
+
entries.sort((a, b) => {
|
|
36213
|
+
if (b.score !== a.score) {
|
|
36214
|
+
return b.score - a.score;
|
|
36215
|
+
}
|
|
36216
|
+
return b.timestamp.localeCompare(a.timestamp);
|
|
36217
|
+
});
|
|
36218
|
+
this.model.leaderboard = entries;
|
|
36219
|
+
}
|
|
36220
|
+
m.redraw();
|
|
36221
|
+
}
|
|
36222
|
+
onUserStatsUpdate(json, firstAttach) {
|
|
36223
|
+
if (!json) {
|
|
36224
|
+
this.model.userStats = null;
|
|
36225
|
+
}
|
|
36226
|
+
else {
|
|
36227
|
+
this.model.userStats = json;
|
|
36228
|
+
}
|
|
36229
|
+
m.redraw();
|
|
36230
|
+
}
|
|
35875
36231
|
async fetchRiddle(date, locale) {
|
|
35876
36232
|
// Create the game via model
|
|
35877
36233
|
if (!this.model)
|
|
@@ -36062,13 +36418,14 @@ const Netskrafl = React.memo(NetskraflImpl);
|
|
|
36062
36418
|
*/
|
|
36063
36419
|
const RiddleScore = {
|
|
36064
36420
|
view: (vnode) => {
|
|
36065
|
-
const { riddle } = vnode.attrs;
|
|
36421
|
+
const { riddle, mode = "desktop" } = vnode.attrs;
|
|
36066
36422
|
if (!riddle)
|
|
36067
36423
|
return m("div");
|
|
36068
36424
|
const score = riddle.currentScore;
|
|
36069
36425
|
const hasValidMove = score !== undefined;
|
|
36070
36426
|
const hasTiles = riddle.tilesPlaced().length > 0;
|
|
36071
|
-
|
|
36427
|
+
const baseClass = (mode === "mobile" ? ".mobile-score" : ".gata-dagsins-score");
|
|
36428
|
+
let classes = [baseClass];
|
|
36072
36429
|
let displayText = "0";
|
|
36073
36430
|
if (!hasTiles) {
|
|
36074
36431
|
// State 1: No tiles on board - grayed/disabled, showing zero
|
|
@@ -36102,7 +36459,8 @@ const RiddleScore = {
|
|
|
36102
36459
|
classes.push(".celebrate");
|
|
36103
36460
|
}
|
|
36104
36461
|
}
|
|
36105
|
-
|
|
36462
|
+
const legendClass = mode === "mobile" ? ".mobile-score-legend" : ".gata-dagsins-legend";
|
|
36463
|
+
return m("div" + classes.join(""), m("span" + legendClass, displayText));
|
|
36106
36464
|
}
|
|
36107
36465
|
};
|
|
36108
36466
|
|
|
@@ -36162,35 +36520,40 @@ const GataDagsinsBoardAndRack = {
|
|
|
36162
36520
|
*/
|
|
36163
36521
|
const SunCorona = {
|
|
36164
36522
|
view: (vnode) => {
|
|
36165
|
-
const { animate = false } = vnode.attrs;
|
|
36523
|
+
const { animate = false, size = 80 } = vnode.attrs;
|
|
36524
|
+
// Calculate ray positions based on size
|
|
36525
|
+
// For 80px: rays from -40 to -20 (inner radius 20px, outer radius 40px)
|
|
36526
|
+
// For 90px: rays from -45 to -30 (inner radius 30px, outer radius 45px) to fit 60px circle
|
|
36527
|
+
const outerRadius = size / 2;
|
|
36528
|
+
const innerRadius = size / 3;
|
|
36166
36529
|
return m("div.sun-corona" + (animate ? ".rotating" : ""), [
|
|
36167
36530
|
m.trust(`
|
|
36168
|
-
<svg width="
|
|
36531
|
+
<svg width="${size}" height="${size}" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
36169
36532
|
<g transform="translate(50,50)">
|
|
36170
36533
|
<!-- Ray at 0° (12 o'clock) -->
|
|
36171
|
-
<polygon points="0
|
|
36534
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(0)"/>
|
|
36172
36535
|
<!-- Ray at 30° -->
|
|
36173
|
-
<polygon points="0
|
|
36536
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(30)"/>
|
|
36174
36537
|
<!-- Ray at 60° -->
|
|
36175
|
-
<polygon points="0
|
|
36538
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(60)"/>
|
|
36176
36539
|
<!-- Ray at 90° (3 o'clock) -->
|
|
36177
|
-
<polygon points="0
|
|
36540
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(90)"/>
|
|
36178
36541
|
<!-- Ray at 120° -->
|
|
36179
|
-
<polygon points="0
|
|
36542
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(120)"/>
|
|
36180
36543
|
<!-- Ray at 150° -->
|
|
36181
|
-
<polygon points="0
|
|
36544
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(150)"/>
|
|
36182
36545
|
<!-- Ray at 180° (6 o'clock) -->
|
|
36183
|
-
<polygon points="0
|
|
36546
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(180)"/>
|
|
36184
36547
|
<!-- Ray at 210° -->
|
|
36185
|
-
<polygon points="0
|
|
36548
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(210)"/>
|
|
36186
36549
|
<!-- Ray at 240° -->
|
|
36187
|
-
<polygon points="0
|
|
36550
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(240)"/>
|
|
36188
36551
|
<!-- Ray at 270° (9 o'clock) -->
|
|
36189
|
-
<polygon points="0
|
|
36552
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(270)"/>
|
|
36190
36553
|
<!-- Ray at 300° -->
|
|
36191
|
-
<polygon points="0
|
|
36554
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(300)"/>
|
|
36192
36555
|
<!-- Ray at 330° -->
|
|
36193
|
-
<polygon points="0
|
|
36556
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(330)"/>
|
|
36194
36557
|
</g>
|
|
36195
36558
|
</svg>
|
|
36196
36559
|
`)
|
|
@@ -36198,6 +36561,101 @@ const SunCorona = {
|
|
|
36198
36561
|
}
|
|
36199
36562
|
};
|
|
36200
36563
|
|
|
36564
|
+
/*
|
|
36565
|
+
|
|
36566
|
+
Mobile-Status.ts
|
|
36567
|
+
|
|
36568
|
+
Mobile-only horizontal status component for Gáta Dagsins
|
|
36569
|
+
|
|
36570
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36571
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36572
|
+
|
|
36573
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36574
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36575
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36576
|
+
|
|
36577
|
+
*/
|
|
36578
|
+
// Mobile-only horizontal status display
|
|
36579
|
+
const MobileStatus = () => {
|
|
36580
|
+
return {
|
|
36581
|
+
view: (vnode) => {
|
|
36582
|
+
const { riddle, selectedMoves, bestMove, onMoveClick } = vnode.attrs;
|
|
36583
|
+
const { bestPossibleScore, globalBestScore } = riddle;
|
|
36584
|
+
// Determine if player achieved best possible score
|
|
36585
|
+
const achieved = bestMove !== undefined;
|
|
36586
|
+
const celebrate = bestMove && bestMove.word !== "";
|
|
36587
|
+
// Get player's current best score
|
|
36588
|
+
const playerBestScore = selectedMoves.length > 0 ? selectedMoves[0].score : 0;
|
|
36589
|
+
// Determine current leader score (may be this player or another)
|
|
36590
|
+
let leaderScore = 0;
|
|
36591
|
+
let isPlayerLeading = false;
|
|
36592
|
+
if (globalBestScore && globalBestScore.score > 0) {
|
|
36593
|
+
leaderScore = globalBestScore.score;
|
|
36594
|
+
// Check if player is leading
|
|
36595
|
+
isPlayerLeading = playerBestScore >= globalBestScore.score;
|
|
36596
|
+
}
|
|
36597
|
+
else {
|
|
36598
|
+
leaderScore = playerBestScore;
|
|
36599
|
+
isPlayerLeading = playerBestScore > 0;
|
|
36600
|
+
}
|
|
36601
|
+
return m(".mobile-status-container", [
|
|
36602
|
+
// Current word score (leftmost) - uses RiddleScore component in mobile mode
|
|
36603
|
+
m(".mobile-status-item", m(RiddleScore, { riddle, mode: "mobile" })),
|
|
36604
|
+
// Player's best score
|
|
36605
|
+
m(".mobile-status-item.player-best", [
|
|
36606
|
+
m(".mobile-status-label", ts("Þín besta:")),
|
|
36607
|
+
m(".mobile-status-score", playerBestScore.toString())
|
|
36608
|
+
]),
|
|
36609
|
+
// Current leader score
|
|
36610
|
+
m(".mobile-status-item.leader" + (isPlayerLeading ? ".is-player" : ""), [
|
|
36611
|
+
m(".mobile-status-label", isPlayerLeading ? ts("Þú leiðir!") : ts("Leiðandi:")),
|
|
36612
|
+
m(".mobile-status-score", leaderScore.toString())
|
|
36613
|
+
]),
|
|
36614
|
+
// Best possible score
|
|
36615
|
+
m(".mobile-status-item.best-possible"
|
|
36616
|
+
+ (celebrate ? ".celebrate" : "")
|
|
36617
|
+
+ (achieved ? ".achieved" : ""), {
|
|
36618
|
+
onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
|
|
36619
|
+
}, [
|
|
36620
|
+
// Wrapper for score and corona to position them together
|
|
36621
|
+
m(".mobile-best-score-wrapper", [
|
|
36622
|
+
celebrate ? m(SunCorona, { animate: true, size: 100 }) : null,
|
|
36623
|
+
m(".mobile-status-score", bestPossibleScore.toString())
|
|
36624
|
+
])
|
|
36625
|
+
])
|
|
36626
|
+
]);
|
|
36627
|
+
}
|
|
36628
|
+
};
|
|
36629
|
+
};
|
|
36630
|
+
|
|
36631
|
+
/*
|
|
36632
|
+
|
|
36633
|
+
TabBar.ts
|
|
36634
|
+
|
|
36635
|
+
Reusable tab navigation component
|
|
36636
|
+
|
|
36637
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36638
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36639
|
+
|
|
36640
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36641
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36642
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36643
|
+
|
|
36644
|
+
*/
|
|
36645
|
+
const TabBar = {
|
|
36646
|
+
view: (vnode) => {
|
|
36647
|
+
const { tabs, activeTab, onTabChange } = vnode.attrs;
|
|
36648
|
+
return m(".tab-bar", tabs.map(tab => m(".tab-item" + (activeTab === tab.id ? ".active" : ""), {
|
|
36649
|
+
key: tab.id,
|
|
36650
|
+
onclick: () => onTabChange(tab.id)
|
|
36651
|
+
}, [
|
|
36652
|
+
tab.iconGlyph ? m("span.tab-icon", glyph(tab.iconGlyph)) :
|
|
36653
|
+
tab.icon ? m("span.tab-icon", tab.icon) : null,
|
|
36654
|
+
m("span.tab-label", tab.label)
|
|
36655
|
+
])));
|
|
36656
|
+
}
|
|
36657
|
+
};
|
|
36658
|
+
|
|
36201
36659
|
/*
|
|
36202
36660
|
|
|
36203
36661
|
Thermometer.ts
|
|
@@ -36272,12 +36730,27 @@ const BestPossibleScore = () => {
|
|
|
36272
36730
|
return {
|
|
36273
36731
|
view: (vnode) => {
|
|
36274
36732
|
const { score, bestMove, onMoveClick } = vnode.attrs;
|
|
36275
|
-
|
|
36276
|
-
|
|
36277
|
-
|
|
36733
|
+
// Determine the label based on achievement status
|
|
36734
|
+
let topLabel;
|
|
36735
|
+
if (bestMove !== undefined) {
|
|
36736
|
+
if (bestMove.word) {
|
|
36737
|
+
// Current player achieved it - show their word
|
|
36738
|
+
topLabel = removeBlankMarkers(bestMove.word);
|
|
36739
|
+
}
|
|
36740
|
+
else {
|
|
36741
|
+
// Someone else achieved it - indicate this
|
|
36742
|
+
topLabel = ts("Bestu lögn náð!");
|
|
36743
|
+
}
|
|
36744
|
+
}
|
|
36745
|
+
else {
|
|
36746
|
+
// Not achieved yet - show default label
|
|
36747
|
+
topLabel = ts("Besta mögulega lögn");
|
|
36748
|
+
}
|
|
36749
|
+
const achieved = bestMove !== undefined;
|
|
36278
36750
|
const celebrate = bestMove && bestMove.word !== "";
|
|
36279
36751
|
return m(".thermometer-best-score"
|
|
36280
|
-
+ (celebrate ? ".celebrate" : "")
|
|
36752
|
+
+ (celebrate ? ".celebrate" : "")
|
|
36753
|
+
+ (achieved ? ".achieved" : ""), m(".thermometer-best-score-container", {
|
|
36281
36754
|
onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
|
|
36282
36755
|
}, [
|
|
36283
36756
|
// Sun corona behind the circle when celebrating
|
|
@@ -36405,6 +36878,196 @@ const Thermometer = () => {
|
|
|
36405
36878
|
};
|
|
36406
36879
|
};
|
|
36407
36880
|
|
|
36881
|
+
/*
|
|
36882
|
+
|
|
36883
|
+
StatsView.ts
|
|
36884
|
+
|
|
36885
|
+
User statistics display component for Gáta Dagsins
|
|
36886
|
+
|
|
36887
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36888
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36889
|
+
|
|
36890
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36891
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36892
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36893
|
+
|
|
36894
|
+
*/
|
|
36895
|
+
const StatsView = {
|
|
36896
|
+
view: (vnode) => {
|
|
36897
|
+
const { stats, loading = false } = vnode.attrs;
|
|
36898
|
+
if (loading) {
|
|
36899
|
+
return m(".stats-view.loading", m(".loading-message", ts("Sæki tölfræði...")));
|
|
36900
|
+
}
|
|
36901
|
+
if (!stats) {
|
|
36902
|
+
return m(".stats-view.empty", m(".empty-message", ts("Engin tölfræði til að sýna")));
|
|
36903
|
+
}
|
|
36904
|
+
const statItems = [
|
|
36905
|
+
{
|
|
36906
|
+
iconGlyph: "fire",
|
|
36907
|
+
label: ts("Núverandi striklota"),
|
|
36908
|
+
value: stats.currentStreak,
|
|
36909
|
+
highlight: stats.currentStreak > 0
|
|
36910
|
+
},
|
|
36911
|
+
{
|
|
36912
|
+
iconGlyph: "star",
|
|
36913
|
+
label: ts("Lengsta striklota"),
|
|
36914
|
+
value: stats.longestStreak
|
|
36915
|
+
},
|
|
36916
|
+
{
|
|
36917
|
+
iconGlyph: "tower",
|
|
36918
|
+
label: ts("Hæsta skori náð"),
|
|
36919
|
+
value: stats.totalTopScores
|
|
36920
|
+
},
|
|
36921
|
+
{
|
|
36922
|
+
iconGlyph: "certificate",
|
|
36923
|
+
label: ts("Striklota hæsta skors"),
|
|
36924
|
+
value: stats.topScoreStreak,
|
|
36925
|
+
highlight: stats.topScoreStreak > 0
|
|
36926
|
+
},
|
|
36927
|
+
{
|
|
36928
|
+
iconGlyph: "calendar",
|
|
36929
|
+
label: ts("Heildarfjöldi daga"),
|
|
36930
|
+
value: stats.totalDaysPlayed
|
|
36931
|
+
},
|
|
36932
|
+
];
|
|
36933
|
+
return m(".stats-view", m(".stats-grid", statItems.map((item, index) => m(".stat-item" + (item.highlight ? ".highlight" : ""), { key: index }, [
|
|
36934
|
+
m(".stat-icon", glyph(item.iconGlyph)),
|
|
36935
|
+
m(".stat-info", [
|
|
36936
|
+
m(".stat-label", item.label),
|
|
36937
|
+
m(".stat-value", item.value.toString())
|
|
36938
|
+
])
|
|
36939
|
+
]))));
|
|
36940
|
+
}
|
|
36941
|
+
};
|
|
36942
|
+
|
|
36943
|
+
/*
|
|
36944
|
+
|
|
36945
|
+
LeaderboardView.ts
|
|
36946
|
+
|
|
36947
|
+
Daily leaderboard display component for Gáta Dagsins
|
|
36948
|
+
|
|
36949
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36950
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36951
|
+
|
|
36952
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36953
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36954
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36955
|
+
|
|
36956
|
+
*/
|
|
36957
|
+
function getMedalIcon(rank) {
|
|
36958
|
+
switch (rank) {
|
|
36959
|
+
case 1: return "🥇";
|
|
36960
|
+
case 2: return "🥈";
|
|
36961
|
+
case 3: return "🥉";
|
|
36962
|
+
default: return null;
|
|
36963
|
+
}
|
|
36964
|
+
}
|
|
36965
|
+
function formatDate(dateStr) {
|
|
36966
|
+
// Format YYYY-MM-DD to Icelandic date (e.g., "2. okt.")
|
|
36967
|
+
const date = new Date(dateStr + "T00:00:00");
|
|
36968
|
+
const day = date.getDate();
|
|
36969
|
+
const months = [
|
|
36970
|
+
"janúar", "febrúar", "mars", "apríl", "maí", "júní",
|
|
36971
|
+
"júlí", "ágúst", "september", "október", "nóvember", "desember",
|
|
36972
|
+
];
|
|
36973
|
+
const month = months[date.getMonth()];
|
|
36974
|
+
return `${day}. ${month}`;
|
|
36975
|
+
}
|
|
36976
|
+
const LeaderboardView = {
|
|
36977
|
+
view: (vnode) => {
|
|
36978
|
+
const { leaderboard, currentUserId, date, loading = false } = vnode.attrs;
|
|
36979
|
+
if (loading) {
|
|
36980
|
+
return m(".leaderboard-view.loading", m(".loading-message", ts("Hleð stigatöflu...")));
|
|
36981
|
+
}
|
|
36982
|
+
if (!leaderboard || leaderboard.length === 0) {
|
|
36983
|
+
return m(".leaderboard-view.empty", m(".empty-message", ts("Engin stig skráð enn")));
|
|
36984
|
+
}
|
|
36985
|
+
return m(".leaderboard-view", [
|
|
36986
|
+
m(".leaderboard-header", [
|
|
36987
|
+
m(".leaderboard-title", formatDate(date)),
|
|
36988
|
+
]),
|
|
36989
|
+
m(".leaderboard-list", leaderboard.map((entry, index) => {
|
|
36990
|
+
const rank = index + 1;
|
|
36991
|
+
const isCurrentUser = entry.userId === currentUserId;
|
|
36992
|
+
const medal = getMedalIcon(rank);
|
|
36993
|
+
return m(".leaderboard-entry" + (isCurrentUser ? ".current-user" : ""), { key: entry.userId }, [
|
|
36994
|
+
m(".entry-rank", [
|
|
36995
|
+
medal ? m("span.medal", medal) : m("span.rank-number", rank.toString())
|
|
36996
|
+
]),
|
|
36997
|
+
m(".entry-name", isCurrentUser ? ts("Þú") : entry.displayName),
|
|
36998
|
+
m(".entry-score", entry.score.toString())
|
|
36999
|
+
]);
|
|
37000
|
+
}))
|
|
37001
|
+
]);
|
|
37002
|
+
}
|
|
37003
|
+
};
|
|
37004
|
+
|
|
37005
|
+
/*
|
|
37006
|
+
|
|
37007
|
+
RightSideTabs.ts
|
|
37008
|
+
|
|
37009
|
+
Desktop tabbed container for Performance/Stats/Leaderboard in Gáta Dagsins
|
|
37010
|
+
|
|
37011
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
37012
|
+
Author: Vilhjálmur Þorsteinsson
|
|
37013
|
+
|
|
37014
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
37015
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
37016
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
37017
|
+
|
|
37018
|
+
*/
|
|
37019
|
+
const RightSideTabs = () => {
|
|
37020
|
+
// Component-local state for active tab (defaults to performance)
|
|
37021
|
+
let activeTab = "performance";
|
|
37022
|
+
const tabs = [
|
|
37023
|
+
{ id: "performance", label: ts("Frammistaða"), iconGlyph: "dashboard" },
|
|
37024
|
+
{ id: "stats", label: ts("Tölfræði"), iconGlyph: "stats" },
|
|
37025
|
+
{ id: "leaderboard", label: ts("Stigatafla"), iconGlyph: "tower" }
|
|
37026
|
+
];
|
|
37027
|
+
return {
|
|
37028
|
+
view: (vnode) => {
|
|
37029
|
+
const { view, selectedMoves, bestMove, onMoveClick } = vnode.attrs;
|
|
37030
|
+
const { riddle, state } = view.model;
|
|
37031
|
+
if (!riddle) {
|
|
37032
|
+
return m(".gatadagsins-right-side-tabs", "");
|
|
37033
|
+
}
|
|
37034
|
+
const handleTabChange = (tabId) => {
|
|
37035
|
+
activeTab = tabId;
|
|
37036
|
+
};
|
|
37037
|
+
return m(".gatadagsins-right-side-tabs", [
|
|
37038
|
+
// Tab navigation
|
|
37039
|
+
m(TabBar, {
|
|
37040
|
+
tabs,
|
|
37041
|
+
activeTab,
|
|
37042
|
+
onTabChange: handleTabChange
|
|
37043
|
+
}),
|
|
37044
|
+
// Tab content
|
|
37045
|
+
m(".tab-content", [
|
|
37046
|
+
// Performance tab (thermometer)
|
|
37047
|
+
activeTab === "performance" ? m(Thermometer, {
|
|
37048
|
+
riddle,
|
|
37049
|
+
selectedMoves,
|
|
37050
|
+
bestMove,
|
|
37051
|
+
onMoveClick
|
|
37052
|
+
}) : null,
|
|
37053
|
+
// Stats tab
|
|
37054
|
+
activeTab === "stats" ? m(StatsView, {
|
|
37055
|
+
stats: view.model.userStats || null,
|
|
37056
|
+
loading: false
|
|
37057
|
+
}) : null,
|
|
37058
|
+
// Leaderboard tab
|
|
37059
|
+
activeTab === "leaderboard" ? m(LeaderboardView, {
|
|
37060
|
+
leaderboard: view.model.leaderboard || [],
|
|
37061
|
+
currentUserId: (state === null || state === void 0 ? void 0 : state.userId) || "",
|
|
37062
|
+
date: riddle.date,
|
|
37063
|
+
loading: false
|
|
37064
|
+
}) : null
|
|
37065
|
+
])
|
|
37066
|
+
]);
|
|
37067
|
+
}
|
|
37068
|
+
};
|
|
37069
|
+
};
|
|
37070
|
+
|
|
36408
37071
|
/*
|
|
36409
37072
|
|
|
36410
37073
|
GataDagsins-Right-Side.ts
|
|
@@ -36420,7 +37083,7 @@ const Thermometer = () => {
|
|
|
36420
37083
|
|
|
36421
37084
|
*/
|
|
36422
37085
|
const GataDagsinsRightSide = {
|
|
36423
|
-
// Component containing
|
|
37086
|
+
// Component containing both mobile status bar and desktop tabbed view
|
|
36424
37087
|
view: (vnode) => {
|
|
36425
37088
|
const { view, selectedMoves, bestMove } = vnode.attrs;
|
|
36426
37089
|
const { riddle } = view.model;
|
|
@@ -36431,13 +37094,20 @@ const GataDagsinsRightSide = {
|
|
|
36431
37094
|
}
|
|
36432
37095
|
};
|
|
36433
37096
|
return m(".gatadagsins-right-side-wrapper", riddle ? [
|
|
36434
|
-
//
|
|
36435
|
-
m(".gatadagsins-
|
|
37097
|
+
// Mobile-only status bar (visible on mobile, hidden on desktop)
|
|
37098
|
+
m(".gatadagsins-mobile-status", m(MobileStatus, {
|
|
36436
37099
|
riddle,
|
|
36437
37100
|
selectedMoves,
|
|
36438
37101
|
bestMove,
|
|
36439
37102
|
onMoveClick: handleMoveClick
|
|
36440
37103
|
})),
|
|
37104
|
+
// Desktop-only tabbed view (hidden on mobile, visible on desktop)
|
|
37105
|
+
m(".gatadagsins-thermometer-column", m(RightSideTabs, {
|
|
37106
|
+
view,
|
|
37107
|
+
selectedMoves,
|
|
37108
|
+
bestMove,
|
|
37109
|
+
onMoveClick: handleMoveClick
|
|
37110
|
+
})),
|
|
36441
37111
|
] : null);
|
|
36442
37112
|
}
|
|
36443
37113
|
};
|
|
@@ -36520,6 +37190,102 @@ const GataDagsinsHelp = {
|
|
|
36520
37190
|
}
|
|
36521
37191
|
};
|
|
36522
37192
|
|
|
37193
|
+
/*
|
|
37194
|
+
|
|
37195
|
+
StatsModal.ts
|
|
37196
|
+
|
|
37197
|
+
Mobile modal for stats and leaderboard
|
|
37198
|
+
|
|
37199
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
37200
|
+
Author: Vilhjálmur Þorsteinsson
|
|
37201
|
+
|
|
37202
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
37203
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
37204
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
37205
|
+
|
|
37206
|
+
*/
|
|
37207
|
+
const StatsModal = () => {
|
|
37208
|
+
// Component-local state for active tab (defaults to stats)
|
|
37209
|
+
let activeTab = "stats";
|
|
37210
|
+
const tabs = [
|
|
37211
|
+
{ id: "stats", label: ts("Tölfræði"), iconGlyph: "stats" },
|
|
37212
|
+
{ id: "leaderboard", label: ts("Stigatafla"), iconGlyph: "tower" }
|
|
37213
|
+
];
|
|
37214
|
+
return {
|
|
37215
|
+
view: (vnode) => {
|
|
37216
|
+
const { view, onClose } = vnode.attrs;
|
|
37217
|
+
const { riddle, state } = view.model;
|
|
37218
|
+
if (!riddle) {
|
|
37219
|
+
return null;
|
|
37220
|
+
}
|
|
37221
|
+
const handleTabChange = (tabId) => {
|
|
37222
|
+
activeTab = tabId;
|
|
37223
|
+
};
|
|
37224
|
+
return [
|
|
37225
|
+
// Backdrop
|
|
37226
|
+
m(".modal-backdrop", {
|
|
37227
|
+
onclick: onClose
|
|
37228
|
+
}),
|
|
37229
|
+
// Modal dialog
|
|
37230
|
+
m(".modal-dialog.stats-modal", [
|
|
37231
|
+
m(".modal-content", [
|
|
37232
|
+
// Header with close button
|
|
37233
|
+
m(".modal-header", [
|
|
37234
|
+
m("h2", ts("Tölfræði")),
|
|
37235
|
+
m("button.close", {
|
|
37236
|
+
onclick: onClose
|
|
37237
|
+
}, "×")
|
|
37238
|
+
]),
|
|
37239
|
+
// Tab navigation
|
|
37240
|
+
m(TabBar, {
|
|
37241
|
+
tabs,
|
|
37242
|
+
activeTab,
|
|
37243
|
+
onTabChange: handleTabChange
|
|
37244
|
+
}),
|
|
37245
|
+
// Modal body with tab content
|
|
37246
|
+
m(".modal-body", [
|
|
37247
|
+
activeTab === "stats" ? m(StatsView, {
|
|
37248
|
+
stats: view.model.userStats || null,
|
|
37249
|
+
loading: false
|
|
37250
|
+
}) : null,
|
|
37251
|
+
activeTab === "leaderboard" ? m(LeaderboardView, {
|
|
37252
|
+
leaderboard: view.model.leaderboard || [],
|
|
37253
|
+
currentUserId: (state === null || state === void 0 ? void 0 : state.userId) || "",
|
|
37254
|
+
date: riddle.date,
|
|
37255
|
+
loading: false
|
|
37256
|
+
}) : null
|
|
37257
|
+
])
|
|
37258
|
+
])
|
|
37259
|
+
])
|
|
37260
|
+
];
|
|
37261
|
+
}
|
|
37262
|
+
};
|
|
37263
|
+
};
|
|
37264
|
+
|
|
37265
|
+
/*
|
|
37266
|
+
|
|
37267
|
+
MobileStatsButton.ts
|
|
37268
|
+
|
|
37269
|
+
Button to open stats modal on mobile
|
|
37270
|
+
|
|
37271
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
37272
|
+
Author: Vilhjálmur Þorsteinsson
|
|
37273
|
+
|
|
37274
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
37275
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
37276
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
37277
|
+
|
|
37278
|
+
*/
|
|
37279
|
+
const MobileStatsButton = {
|
|
37280
|
+
view: (vnode) => {
|
|
37281
|
+
const { onClick } = vnode.attrs;
|
|
37282
|
+
return m(".mobile-stats-button", {
|
|
37283
|
+
onclick: onClick,
|
|
37284
|
+
title: "Tölfræði og stigatafla"
|
|
37285
|
+
}, m(".stats-icon", glyph("stats")));
|
|
37286
|
+
}
|
|
37287
|
+
};
|
|
37288
|
+
|
|
36523
37289
|
/*
|
|
36524
37290
|
|
|
36525
37291
|
GataDagsins.ts
|
|
@@ -36535,11 +37301,19 @@ const GataDagsinsHelp = {
|
|
|
36535
37301
|
|
|
36536
37302
|
*/
|
|
36537
37303
|
const MAX_MOVES_TO_DISPLAY = 10;
|
|
36538
|
-
const
|
|
36539
|
-
var _a;
|
|
37304
|
+
const currentMoveState = (riddle) => {
|
|
37305
|
+
var _a, _b, _c;
|
|
37306
|
+
const thisPlayer = ((_a = riddle.model.state) === null || _a === void 0 ? void 0 : _a.userId) || "";
|
|
37307
|
+
const { bestPossibleScore, // The highest score achievable for this riddle
|
|
37308
|
+
globalBestScore, // The best score achieved by any player
|
|
37309
|
+
groupBestScore, // The best score achieved within the player's group
|
|
37310
|
+
playerMoves, } = riddle;
|
|
37311
|
+
// If the player has equaled the best possible score,
|
|
37312
|
+
// the winning word is stored here and displayed at the top
|
|
37313
|
+
let bestMove = undefined;
|
|
36540
37314
|
// Sort moves by score in descending order and
|
|
36541
37315
|
// cut the tail off the list to only include the top moves
|
|
36542
|
-
const
|
|
37316
|
+
const selectedMoves = playerMoves
|
|
36543
37317
|
.sort((a, b) => b.score - a.score)
|
|
36544
37318
|
.slice(0, MAX_MOVES_TO_DISPLAY)
|
|
36545
37319
|
.map(move => ({
|
|
@@ -36547,61 +37321,49 @@ const selectTopMoves = (thisPlayer, moves, globalBestScore, groupBestScore) => {
|
|
|
36547
37321
|
word: move.word,
|
|
36548
37322
|
coord: move.coord,
|
|
36549
37323
|
}));
|
|
36550
|
-
//
|
|
36551
|
-
// the player's own top score, we include it as the first move
|
|
37324
|
+
// Check whether we need to add or annotate the global best score
|
|
36552
37325
|
if (globalBestScore && globalBestScore.score > 0) {
|
|
36553
37326
|
const { score, word, coord } = globalBestScore;
|
|
36554
|
-
if (((
|
|
37327
|
+
if (((_c = (_b = selectedMoves[0]) === null || _b === void 0 ? void 0 : _b.score) !== null && _c !== void 0 ? _c : 0) >= score) {
|
|
36555
37328
|
// This player has made a move that scores the same
|
|
36556
37329
|
// or better as the top score: mark the move
|
|
36557
|
-
|
|
37330
|
+
selectedMoves[0].isGlobalBestScore = true;
|
|
36558
37331
|
}
|
|
36559
37332
|
else if (globalBestScore.player === thisPlayer) {
|
|
36560
37333
|
// This player holds the global best score, probably
|
|
36561
|
-
// from a previous session
|
|
36562
|
-
|
|
37334
|
+
// from a previous session, so it's not already
|
|
37335
|
+
// in the selectedMoves list: add it as a move
|
|
37336
|
+
selectedMoves.unshift({ score, isGlobalBestScore: true, word, coord });
|
|
36563
37337
|
}
|
|
36564
37338
|
else {
|
|
36565
|
-
// This is a global best score from another player
|
|
36566
|
-
|
|
37339
|
+
// This is a global best score from another player
|
|
37340
|
+
selectedMoves.unshift({ score, isGlobalBestScore: true, word: "", coord: "" });
|
|
36567
37341
|
}
|
|
36568
37342
|
}
|
|
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
|
|
37343
|
+
// Check if the best possible score has been achieved, by this player
|
|
37344
|
+
// or another player. If so, we remove it from the move list, since we
|
|
37345
|
+
// only display it at the top of the thermometer.
|
|
36583
37346
|
if (selectedMoves.length > 0 && selectedMoves[0].score === bestPossibleScore) {
|
|
36584
|
-
|
|
36585
|
-
// The word was played by this player
|
|
36586
|
-
bestMove = selectedMoves.shift();
|
|
36587
|
-
}
|
|
37347
|
+
bestMove = selectedMoves.shift();
|
|
36588
37348
|
}
|
|
36589
37349
|
return { selectedMoves, bestMove };
|
|
36590
37350
|
};
|
|
36591
37351
|
const GataDagsins$1 = () => {
|
|
36592
37352
|
// A view of the Gáta Dagsins page
|
|
36593
37353
|
let showHelp = false;
|
|
37354
|
+
let showStatsModal = false;
|
|
36594
37355
|
return {
|
|
36595
|
-
oninit: (vnode) => {
|
|
37356
|
+
oninit: async (vnode) => {
|
|
36596
37357
|
const { model, actions } = vnode.attrs.view;
|
|
36597
37358
|
const { riddle } = model;
|
|
36598
37359
|
if (!riddle) {
|
|
36599
37360
|
const { date, locale } = vnode.attrs;
|
|
36600
37361
|
// Initialize a fresh riddle object if it doesn't exist
|
|
36601
|
-
actions.fetchRiddle(date, locale);
|
|
37362
|
+
await actions.fetchRiddle(date, locale);
|
|
36602
37363
|
}
|
|
36603
|
-
// Initialize
|
|
37364
|
+
// Initialize dialog states
|
|
36604
37365
|
showHelp = false;
|
|
37366
|
+
showStatsModal = false;
|
|
36605
37367
|
},
|
|
36606
37368
|
view: (vnode) => {
|
|
36607
37369
|
var _a;
|
|
@@ -36615,6 +37377,10 @@ const GataDagsins$1 = () => {
|
|
|
36615
37377
|
showHelp = !showHelp;
|
|
36616
37378
|
m.redraw();
|
|
36617
37379
|
};
|
|
37380
|
+
const toggleStatsModal = () => {
|
|
37381
|
+
showStatsModal = !showStatsModal;
|
|
37382
|
+
m.redraw();
|
|
37383
|
+
};
|
|
36618
37384
|
return m("div.drop-target", {
|
|
36619
37385
|
id: "gatadagsins-background",
|
|
36620
37386
|
}, [
|
|
@@ -36639,11 +37405,15 @@ const GataDagsins$1 = () => {
|
|
|
36639
37405
|
((_a = model.state) === null || _a === void 0 ? void 0 : _a.beginner) ? m(Beginner, { view }) : "",
|
|
36640
37406
|
// Custom Info button for GataDagsins that shows help dialog
|
|
36641
37407
|
m(".info", { title: ts("Upplýsingar og hjálp") }, m("a.iconlink", { href: "#", onclick: (e) => { e.preventDefault(); toggleHelp(); } }, glyph("info-sign"))),
|
|
37408
|
+
// Mobile stats button (hidden on desktop)
|
|
37409
|
+
m(MobileStatsButton, { onClick: toggleStatsModal }),
|
|
36642
37410
|
// Help dialog and backdrop
|
|
36643
37411
|
showHelp ? [
|
|
36644
37412
|
m(".modal-backdrop", { onclick: (e) => { e.preventDefault(); } }),
|
|
36645
37413
|
m(GataDagsinsHelp, { onClose: toggleHelp })
|
|
36646
37414
|
] : "",
|
|
37415
|
+
// Stats modal and backdrop (mobile only)
|
|
37416
|
+
showStatsModal ? m(StatsModal, { view, onClose: toggleStatsModal }) : "",
|
|
36647
37417
|
]);
|
|
36648
37418
|
}
|
|
36649
37419
|
};
|
|
@@ -36682,11 +37452,22 @@ async function main(state, container) {
|
|
|
36682
37452
|
const model = new Model(settings, state);
|
|
36683
37453
|
const actions = new Actions(model);
|
|
36684
37454
|
const view = new View(actions);
|
|
37455
|
+
// Get date from URL parameter, fallback to today
|
|
37456
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
37457
|
+
const dateParam = urlParams.get('date');
|
|
36685
37458
|
const today = new Date().toISOString().split("T")[0];
|
|
37459
|
+
const riddleDate = dateParam || today;
|
|
37460
|
+
// Validate date format (YYYY-MM-DD)
|
|
37461
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
37462
|
+
const validDate = dateRegex.test(riddleDate) ? riddleDate : today;
|
|
36686
37463
|
const locale = state.locale || "is_IS";
|
|
37464
|
+
// Log the date being used (helpful for debugging)
|
|
37465
|
+
if (dateParam) {
|
|
37466
|
+
console.log(`Loading Gáta Dagsins for date: ${validDate} (from URL parameter)`);
|
|
37467
|
+
}
|
|
36687
37468
|
// Mount the Gáta Dagsins UI using an anonymous closure component
|
|
36688
37469
|
m.mount(container, {
|
|
36689
|
-
view: () => m(GataDagsins$1, { view, date:
|
|
37470
|
+
view: () => m(GataDagsins$1, { view, date: validDate, locale }),
|
|
36690
37471
|
});
|
|
36691
37472
|
}
|
|
36692
37473
|
catch (e) {
|
|
@@ -36696,6 +37477,8 @@ async function main(state, container) {
|
|
|
36696
37477
|
return "success";
|
|
36697
37478
|
}
|
|
36698
37479
|
|
|
37480
|
+
// Note: To load a specific date for debugging, use URL parameter: ?date=YYYY-MM-DD
|
|
37481
|
+
// Example: http://localhost:6006/?date=2025-01-25
|
|
36699
37482
|
const mountForUser = async (state) => {
|
|
36700
37483
|
// Return a DOM tree containing a mounted Gáta Dagsins UI
|
|
36701
37484
|
// for the user specified in the state object
|