@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/cjs/index.js
CHANGED
|
@@ -17,6 +17,8 @@ const saveAuthSettings = (settings) => {
|
|
|
17
17
|
userEmail: settings.userEmail, // Required field
|
|
18
18
|
};
|
|
19
19
|
// Only add optional fields if they are defined
|
|
20
|
+
if (settings.account !== undefined)
|
|
21
|
+
filteredSettings.account = settings.account;
|
|
20
22
|
if (settings.userId !== undefined)
|
|
21
23
|
filteredSettings.userId = settings.userId;
|
|
22
24
|
if (settings.userNick !== undefined)
|
|
@@ -85,6 +87,7 @@ const applyPersistedSettings = (state) => {
|
|
|
85
87
|
return {
|
|
86
88
|
...state,
|
|
87
89
|
// Only apply persisted values if current values are defaults
|
|
90
|
+
account: state.account || persisted.account || state.userId, // Use userId as fallback
|
|
88
91
|
userId: state.userId || persisted.userId || state.userId,
|
|
89
92
|
userNick: state.userNick || persisted.userNick || state.userNick,
|
|
90
93
|
firebaseAPIKey: state.firebaseAPIKey || persisted.firebaseAPIKey || state.firebaseAPIKey,
|
|
@@ -102,6 +105,7 @@ const DEFAULT_STATE = {
|
|
|
102
105
|
firebaseSenderId: "",
|
|
103
106
|
firebaseAppId: "",
|
|
104
107
|
measurementId: "",
|
|
108
|
+
account: "",
|
|
105
109
|
userEmail: "",
|
|
106
110
|
userId: "",
|
|
107
111
|
userNick: "",
|
|
@@ -23531,6 +23535,9 @@ let View$1 = class View {
|
|
|
23531
23535
|
function viewGetServerCache(view) {
|
|
23532
23536
|
return view.viewCache_.serverCache.getNode();
|
|
23533
23537
|
}
|
|
23538
|
+
function viewGetCompleteNode(view) {
|
|
23539
|
+
return viewCacheGetCompleteEventSnap(view.viewCache_);
|
|
23540
|
+
}
|
|
23534
23541
|
function viewGetCompleteServerCache(view, path) {
|
|
23535
23542
|
const cache = viewCacheGetCompleteServerSnap(view.viewCache_);
|
|
23536
23543
|
if (cache) {
|
|
@@ -24192,6 +24199,33 @@ function syncTreeCalcCompleteEventCache(syncTree, path, writeIdsToExclude) {
|
|
|
24192
24199
|
});
|
|
24193
24200
|
return writeTreeCalcCompleteEventCache(writeTree, path, serverCache, writeIdsToExclude, includeHiddenSets);
|
|
24194
24201
|
}
|
|
24202
|
+
function syncTreeGetServerValue(syncTree, query) {
|
|
24203
|
+
const path = query._path;
|
|
24204
|
+
let serverCache = null;
|
|
24205
|
+
// Any covering writes will necessarily be at the root, so really all we need to find is the server cache.
|
|
24206
|
+
// Consider optimizing this once there's a better understanding of what actual behavior will be.
|
|
24207
|
+
syncTree.syncPointTree_.foreachOnPath(path, (pathToSyncPoint, sp) => {
|
|
24208
|
+
const relativePath = newRelativePath(pathToSyncPoint, path);
|
|
24209
|
+
serverCache =
|
|
24210
|
+
serverCache || syncPointGetCompleteServerCache(sp, relativePath);
|
|
24211
|
+
});
|
|
24212
|
+
let syncPoint = syncTree.syncPointTree_.get(path);
|
|
24213
|
+
if (!syncPoint) {
|
|
24214
|
+
syncPoint = new SyncPoint();
|
|
24215
|
+
syncTree.syncPointTree_ = syncTree.syncPointTree_.set(path, syncPoint);
|
|
24216
|
+
}
|
|
24217
|
+
else {
|
|
24218
|
+
serverCache =
|
|
24219
|
+
serverCache || syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
|
|
24220
|
+
}
|
|
24221
|
+
const serverCacheComplete = serverCache != null;
|
|
24222
|
+
const serverCacheNode = serverCacheComplete
|
|
24223
|
+
? new CacheNode(serverCache, true, false)
|
|
24224
|
+
: null;
|
|
24225
|
+
const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, query._path);
|
|
24226
|
+
const view = syncPointGetView(syncPoint, query, writesCache, serverCacheComplete ? serverCacheNode.getNode() : ChildrenNode.EMPTY_NODE, serverCacheComplete);
|
|
24227
|
+
return viewGetCompleteNode(view);
|
|
24228
|
+
}
|
|
24195
24229
|
/**
|
|
24196
24230
|
* A helper method that visits all descendant and ancestor SyncPoints, applying the operation.
|
|
24197
24231
|
*
|
|
@@ -25321,6 +25355,63 @@ function repoUpdateInfo(repo, pathString, value) {
|
|
|
25321
25355
|
function repoGetNextWriteId(repo) {
|
|
25322
25356
|
return repo.nextWriteId_++;
|
|
25323
25357
|
}
|
|
25358
|
+
/**
|
|
25359
|
+
* The purpose of `getValue` is to return the latest known value
|
|
25360
|
+
* satisfying `query`.
|
|
25361
|
+
*
|
|
25362
|
+
* This method will first check for in-memory cached values
|
|
25363
|
+
* belonging to active listeners. If they are found, such values
|
|
25364
|
+
* are considered to be the most up-to-date.
|
|
25365
|
+
*
|
|
25366
|
+
* If the client is not connected, this method will wait until the
|
|
25367
|
+
* repo has established a connection and then request the value for `query`.
|
|
25368
|
+
* If the client is not able to retrieve the query result for another reason,
|
|
25369
|
+
* it reports an error.
|
|
25370
|
+
*
|
|
25371
|
+
* @param query - The query to surface a value for.
|
|
25372
|
+
*/
|
|
25373
|
+
function repoGetValue(repo, query, eventRegistration) {
|
|
25374
|
+
// Only active queries are cached. There is no persisted cache.
|
|
25375
|
+
const cached = syncTreeGetServerValue(repo.serverSyncTree_, query);
|
|
25376
|
+
if (cached != null) {
|
|
25377
|
+
return Promise.resolve(cached);
|
|
25378
|
+
}
|
|
25379
|
+
return repo.server_.get(query).then(payload => {
|
|
25380
|
+
const node = nodeFromJSON(payload).withIndex(query._queryParams.getIndex());
|
|
25381
|
+
/**
|
|
25382
|
+
* Below we simulate the actions of an `onlyOnce` `onValue()` event where:
|
|
25383
|
+
* Add an event registration,
|
|
25384
|
+
* Update data at the path,
|
|
25385
|
+
* Raise any events,
|
|
25386
|
+
* Cleanup the SyncTree
|
|
25387
|
+
*/
|
|
25388
|
+
syncTreeAddEventRegistration(repo.serverSyncTree_, query, eventRegistration, true);
|
|
25389
|
+
let events;
|
|
25390
|
+
if (query._queryParams.loadsAllData()) {
|
|
25391
|
+
events = syncTreeApplyServerOverwrite(repo.serverSyncTree_, query._path, node);
|
|
25392
|
+
}
|
|
25393
|
+
else {
|
|
25394
|
+
const tag = syncTreeTagForQuery(repo.serverSyncTree_, query);
|
|
25395
|
+
events = syncTreeApplyTaggedQueryOverwrite(repo.serverSyncTree_, query._path, node, tag);
|
|
25396
|
+
}
|
|
25397
|
+
/*
|
|
25398
|
+
* We need to raise events in the scenario where `get()` is called at a parent path, and
|
|
25399
|
+
* while the `get()` is pending, `onValue` is called at a child location. While get() is waiting
|
|
25400
|
+
* for the data, `onValue` will register a new event. Then, get() will come back, and update the syncTree
|
|
25401
|
+
* and its corresponding serverCache, including the child location where `onValue` is called. Then,
|
|
25402
|
+
* `onValue` will receive the event from the server, but look at the syncTree and see that the data received
|
|
25403
|
+
* from the server is already at the SyncPoint, and so the `onValue` callback will never get fired.
|
|
25404
|
+
* Calling `eventQueueRaiseEventsForChangedPath()` is the correct way to propagate the events and
|
|
25405
|
+
* ensure the corresponding child events will get fired.
|
|
25406
|
+
*/
|
|
25407
|
+
eventQueueRaiseEventsForChangedPath(repo.eventQueue_, query._path, events);
|
|
25408
|
+
syncTreeRemoveEventRegistration(repo.serverSyncTree_, query, eventRegistration, null, true);
|
|
25409
|
+
return node;
|
|
25410
|
+
}, err => {
|
|
25411
|
+
repoLog(repo, 'get for query ' + stringify(query) + ' failed: ' + err);
|
|
25412
|
+
return Promise.reject(new Error(err));
|
|
25413
|
+
});
|
|
25414
|
+
}
|
|
25324
25415
|
function repoSetWithPriority(repo, path, newVal, newPriority, onComplete) {
|
|
25325
25416
|
repoLog(repo, 'set', {
|
|
25326
25417
|
path: path.toString(),
|
|
@@ -26737,6 +26828,22 @@ function set(ref, value) {
|
|
|
26737
26828
|
/*priority=*/ null, deferred.wrapCallback(() => { }));
|
|
26738
26829
|
return deferred.promise;
|
|
26739
26830
|
}
|
|
26831
|
+
/**
|
|
26832
|
+
* Gets the most up-to-date result for this query.
|
|
26833
|
+
*
|
|
26834
|
+
* @param query - The query to run.
|
|
26835
|
+
* @returns A `Promise` which resolves to the resulting DataSnapshot if a value is
|
|
26836
|
+
* available, or rejects if the client is unable to return a value (e.g., if the
|
|
26837
|
+
* server is unreachable and there is nothing cached).
|
|
26838
|
+
*/
|
|
26839
|
+
function get(query) {
|
|
26840
|
+
query = getModularInstance(query);
|
|
26841
|
+
const callbackContext = new CallbackContext(() => { });
|
|
26842
|
+
const container = new ValueEventRegistration(callbackContext);
|
|
26843
|
+
return repoGetValue(query._repo, query, container).then(node => {
|
|
26844
|
+
return new DataSnapshot(node, new ReferenceImpl(query._repo, query._path), query._queryParams.getIndex());
|
|
26845
|
+
});
|
|
26846
|
+
}
|
|
26740
26847
|
/**
|
|
26741
26848
|
* Represents registration for 'value' events.
|
|
26742
26849
|
*/
|
|
@@ -27240,6 +27347,14 @@ function logEvent(ev, params) {
|
|
|
27240
27347
|
return;
|
|
27241
27348
|
logEvent$2(analytics, ev, params);
|
|
27242
27349
|
}
|
|
27350
|
+
async function getFirebaseData(path) {
|
|
27351
|
+
// Get data from a Firebase path
|
|
27352
|
+
if (!database)
|
|
27353
|
+
return null;
|
|
27354
|
+
const pathRef = ref(database, path);
|
|
27355
|
+
const snapshot = await get(pathRef);
|
|
27356
|
+
return snapshot.val();
|
|
27357
|
+
}
|
|
27243
27358
|
|
|
27244
27359
|
/*
|
|
27245
27360
|
|
|
@@ -27505,50 +27620,61 @@ const ensureAuthenticated = async (state) => {
|
|
|
27505
27620
|
await authPromise;
|
|
27506
27621
|
return;
|
|
27507
27622
|
}
|
|
27508
|
-
|
|
27509
|
-
|
|
27510
|
-
|
|
27511
|
-
|
|
27512
|
-
|
|
27513
|
-
|
|
27514
|
-
|
|
27515
|
-
|
|
27516
|
-
|
|
27517
|
-
|
|
27623
|
+
let continueTrying = true;
|
|
27624
|
+
while (continueTrying) {
|
|
27625
|
+
continueTrying = false;
|
|
27626
|
+
// Start new login attempt (either forced by 401 or needed for Firebase)
|
|
27627
|
+
authPromise = loginUserByEmail(state);
|
|
27628
|
+
try {
|
|
27629
|
+
const result = await authPromise;
|
|
27630
|
+
if (result.status === "expired") {
|
|
27631
|
+
// Token has expired, notify the React component if callback is set
|
|
27632
|
+
if (state.tokenExpired) {
|
|
27633
|
+
// We have a callback to renew the token: do it and try again
|
|
27634
|
+
state.tokenExpired();
|
|
27635
|
+
continueTrying = true; // Try logging in again
|
|
27636
|
+
clearAuthSettings();
|
|
27637
|
+
continue;
|
|
27638
|
+
}
|
|
27639
|
+
// Clear any persisted settings since they're no longer valid
|
|
27640
|
+
clearAuthSettings();
|
|
27641
|
+
throw new Error("Authentication token has expired");
|
|
27642
|
+
}
|
|
27643
|
+
else if (result.status !== "success") {
|
|
27644
|
+
// Clear any persisted settings on auth failure
|
|
27645
|
+
clearAuthSettings();
|
|
27646
|
+
throw new Error(`Authentication failed: ${result.message || result.status}`);
|
|
27647
|
+
}
|
|
27648
|
+
// Update the user's ID to the internal one used by the backend and Firebase
|
|
27649
|
+
state.userId = result.user_id || state.userId;
|
|
27650
|
+
state.account = result.account || state.userId;
|
|
27651
|
+
// Update the user's nickname
|
|
27652
|
+
state.userNick = result.nickname || state.userNick;
|
|
27653
|
+
// Use the server's Firebase API key, if provided
|
|
27654
|
+
state.firebaseAPIKey = result.firebase_api_key || state.firebaseAPIKey;
|
|
27655
|
+
// Load state flags and preferences
|
|
27656
|
+
state.beginner = (_b = (_a = result.prefs) === null || _a === void 0 ? void 0 : _a.beginner) !== null && _b !== void 0 ? _b : true;
|
|
27657
|
+
state.fairPlay = (_d = (_c = result.prefs) === null || _c === void 0 ? void 0 : _c.fairplay) !== null && _d !== void 0 ? _d : false;
|
|
27658
|
+
state.ready = (_f = (_e = result.prefs) === null || _e === void 0 ? void 0 : _e.ready) !== null && _f !== void 0 ? _f : true;
|
|
27659
|
+
state.readyTimed = (_h = (_g = result.prefs) === null || _g === void 0 ? void 0 : _g.ready_timed) !== null && _h !== void 0 ? _h : true;
|
|
27660
|
+
// Save the authentication settings to sessionStorage for persistence
|
|
27661
|
+
saveAuthSettings({
|
|
27662
|
+
userEmail: state.userEmail, // CRITICAL: Include email to validate ownership
|
|
27663
|
+
userId: state.userId,
|
|
27664
|
+
userNick: state.userNick,
|
|
27665
|
+
firebaseAPIKey: state.firebaseAPIKey,
|
|
27666
|
+
beginner: state.beginner,
|
|
27667
|
+
fairPlay: state.fairPlay,
|
|
27668
|
+
ready: state.ready,
|
|
27669
|
+
readyTimed: state.readyTimed,
|
|
27670
|
+
});
|
|
27671
|
+
// Success: Log in to Firebase with the token passed from the server
|
|
27672
|
+
await loginFirebase(state, result.firebase_token);
|
|
27673
|
+
}
|
|
27674
|
+
finally {
|
|
27675
|
+
// Reset the promise so future 401s can trigger a new login
|
|
27676
|
+
authPromise = null;
|
|
27518
27677
|
}
|
|
27519
|
-
else if (result.status !== "success") {
|
|
27520
|
-
// Clear any persisted settings on auth failure
|
|
27521
|
-
clearAuthSettings();
|
|
27522
|
-
throw new Error(`Authentication failed: ${result.message || result.status}`);
|
|
27523
|
-
}
|
|
27524
|
-
// Update the user's ID to the internal one used by the backend and Firebase
|
|
27525
|
-
state.userId = result.user_id || state.userId;
|
|
27526
|
-
// Update the user's nickname
|
|
27527
|
-
state.userNick = result.nickname || state.userNick;
|
|
27528
|
-
// Use the server's Firebase API key, if provided
|
|
27529
|
-
state.firebaseAPIKey = result.firebase_api_key || state.firebaseAPIKey;
|
|
27530
|
-
// Load state flags and preferences
|
|
27531
|
-
state.beginner = (_b = (_a = result.prefs) === null || _a === void 0 ? void 0 : _a.beginner) !== null && _b !== void 0 ? _b : true;
|
|
27532
|
-
state.fairPlay = (_d = (_c = result.prefs) === null || _c === void 0 ? void 0 : _c.fairplay) !== null && _d !== void 0 ? _d : false;
|
|
27533
|
-
state.ready = (_f = (_e = result.prefs) === null || _e === void 0 ? void 0 : _e.ready) !== null && _f !== void 0 ? _f : true;
|
|
27534
|
-
state.readyTimed = (_h = (_g = result.prefs) === null || _g === void 0 ? void 0 : _g.ready_timed) !== null && _h !== void 0 ? _h : true;
|
|
27535
|
-
// Save the authentication settings to sessionStorage for persistence
|
|
27536
|
-
saveAuthSettings({
|
|
27537
|
-
userEmail: state.userEmail, // CRITICAL: Include email to validate ownership
|
|
27538
|
-
userId: state.userId,
|
|
27539
|
-
userNick: state.userNick,
|
|
27540
|
-
firebaseAPIKey: state.firebaseAPIKey,
|
|
27541
|
-
beginner: state.beginner,
|
|
27542
|
-
fairPlay: state.fairPlay,
|
|
27543
|
-
ready: state.ready,
|
|
27544
|
-
readyTimed: state.readyTimed,
|
|
27545
|
-
});
|
|
27546
|
-
// Success: Log in to Firebase with the token passed from the server
|
|
27547
|
-
await loginFirebase(state, result.firebase_token);
|
|
27548
|
-
}
|
|
27549
|
-
finally {
|
|
27550
|
-
// Reset the promise so future 401s can trigger a new login
|
|
27551
|
-
authPromise = null;
|
|
27552
27678
|
}
|
|
27553
27679
|
};
|
|
27554
27680
|
// Internal authenticated request function
|
|
@@ -34354,6 +34480,161 @@ class Game extends BaseGame {
|
|
|
34354
34480
|
;
|
|
34355
34481
|
} // class Game
|
|
34356
34482
|
|
|
34483
|
+
/*
|
|
34484
|
+
|
|
34485
|
+
riddlePersistence.ts
|
|
34486
|
+
|
|
34487
|
+
Local persistence for Gáta Dagsins using localStorage
|
|
34488
|
+
|
|
34489
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
34490
|
+
|
|
34491
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
34492
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
34493
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
34494
|
+
|
|
34495
|
+
*/
|
|
34496
|
+
class RiddlePersistence {
|
|
34497
|
+
// Generate storage key for a specific user and date
|
|
34498
|
+
static getStorageKey(userId, date) {
|
|
34499
|
+
return `${this.STORAGE_KEY_PREFIX}${date}_${userId}`;
|
|
34500
|
+
}
|
|
34501
|
+
// Save complete move list to localStorage
|
|
34502
|
+
static saveLocalMoves(userId, date, moves) {
|
|
34503
|
+
if (!userId || !date) {
|
|
34504
|
+
return;
|
|
34505
|
+
}
|
|
34506
|
+
const data = {
|
|
34507
|
+
date,
|
|
34508
|
+
moves,
|
|
34509
|
+
timestamp: new Date().toISOString(),
|
|
34510
|
+
userId,
|
|
34511
|
+
};
|
|
34512
|
+
try {
|
|
34513
|
+
const key = this.getStorageKey(userId, date);
|
|
34514
|
+
localStorage.setItem(key, JSON.stringify(data));
|
|
34515
|
+
// Clean up old entries while we're here
|
|
34516
|
+
this.cleanupOldEntries();
|
|
34517
|
+
}
|
|
34518
|
+
catch (e) {
|
|
34519
|
+
// Handle localStorage quota errors silently
|
|
34520
|
+
console.error('Failed to save moves to localStorage:', e);
|
|
34521
|
+
}
|
|
34522
|
+
}
|
|
34523
|
+
// Load move history from localStorage
|
|
34524
|
+
static loadLocalMoves(userId, date) {
|
|
34525
|
+
if (!userId || !date) {
|
|
34526
|
+
return [];
|
|
34527
|
+
}
|
|
34528
|
+
try {
|
|
34529
|
+
const key = this.getStorageKey(userId, date);
|
|
34530
|
+
const stored = localStorage.getItem(key);
|
|
34531
|
+
if (!stored) {
|
|
34532
|
+
return [];
|
|
34533
|
+
}
|
|
34534
|
+
const data = JSON.parse(stored);
|
|
34535
|
+
// Verify that this data belongs to the correct (current) user
|
|
34536
|
+
if (!data.userId || data.userId !== userId) {
|
|
34537
|
+
return [];
|
|
34538
|
+
}
|
|
34539
|
+
return data.moves || [];
|
|
34540
|
+
}
|
|
34541
|
+
catch (e) {
|
|
34542
|
+
console.error('Failed to load moves from localStorage:', e);
|
|
34543
|
+
return [];
|
|
34544
|
+
}
|
|
34545
|
+
}
|
|
34546
|
+
// Check if user has achieved top score (local check)
|
|
34547
|
+
static hasAchievedTopScore(userId, date, topScore) {
|
|
34548
|
+
const moves = this.loadLocalMoves(userId, date);
|
|
34549
|
+
return moves.some(move => move.score >= topScore);
|
|
34550
|
+
}
|
|
34551
|
+
// Get the best move from localStorage
|
|
34552
|
+
static getBestLocalMove(userId, date) {
|
|
34553
|
+
const moves = this.loadLocalMoves(userId, date);
|
|
34554
|
+
if (moves.length === 0) {
|
|
34555
|
+
return null;
|
|
34556
|
+
}
|
|
34557
|
+
// Find the move with the highest score
|
|
34558
|
+
return moves.reduce((best, current) => current.score > best.score ? current : best);
|
|
34559
|
+
}
|
|
34560
|
+
// Clean up entries older than MAX_AGE_DAYS
|
|
34561
|
+
static cleanupOldEntries() {
|
|
34562
|
+
try {
|
|
34563
|
+
const now = new Date();
|
|
34564
|
+
const cutoffDate = new Date(now);
|
|
34565
|
+
cutoffDate.setDate(cutoffDate.getDate() - this.MAX_AGE_DAYS);
|
|
34566
|
+
const keysToRemove = [];
|
|
34567
|
+
// Iterate through localStorage keys
|
|
34568
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
34569
|
+
const key = localStorage.key(i);
|
|
34570
|
+
if (key && key.startsWith(this.STORAGE_KEY_PREFIX)) {
|
|
34571
|
+
// Extract date from key: "gata_YYYY-MM-DD_userId"
|
|
34572
|
+
const parts = key.split('_');
|
|
34573
|
+
if (parts.length >= 2) {
|
|
34574
|
+
const dateStr = parts[1];
|
|
34575
|
+
const entryDate = new Date(dateStr);
|
|
34576
|
+
if (!isNaN(entryDate.getTime()) && entryDate < cutoffDate) {
|
|
34577
|
+
keysToRemove.push(key);
|
|
34578
|
+
}
|
|
34579
|
+
}
|
|
34580
|
+
}
|
|
34581
|
+
}
|
|
34582
|
+
// Remove old entries
|
|
34583
|
+
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
34584
|
+
}
|
|
34585
|
+
catch (e) {
|
|
34586
|
+
console.error('Failed to cleanup old entries:', e);
|
|
34587
|
+
}
|
|
34588
|
+
}
|
|
34589
|
+
// Clear all persistence for a specific user
|
|
34590
|
+
static clearUserData(userId) {
|
|
34591
|
+
const keysToRemove = [];
|
|
34592
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
34593
|
+
const key = localStorage.key(i);
|
|
34594
|
+
if (key && key.startsWith(this.STORAGE_KEY_PREFIX) && key.endsWith(`_${userId}`)) {
|
|
34595
|
+
keysToRemove.push(key);
|
|
34596
|
+
}
|
|
34597
|
+
}
|
|
34598
|
+
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
34599
|
+
}
|
|
34600
|
+
// === Firebase Read Methods ===
|
|
34601
|
+
// Note: All Firebase write operations (achievements, stats, global best, leaderboard)
|
|
34602
|
+
// are now handled by the backend server in the /gatadagsins/submit endpoint.
|
|
34603
|
+
// The client only handles localStorage persistence and Firebase reads for display.
|
|
34604
|
+
// Get leaderboard for a specific date
|
|
34605
|
+
static async getLeaderboard(date, locale, limit = 10) {
|
|
34606
|
+
try {
|
|
34607
|
+
const leadersPath = `gatadagsins/${date}/${locale}/leaders`;
|
|
34608
|
+
const leaders = await getFirebaseData(leadersPath);
|
|
34609
|
+
if (!leaders) {
|
|
34610
|
+
return [];
|
|
34611
|
+
}
|
|
34612
|
+
// Convert object to array and sort by score
|
|
34613
|
+
const entries = Object.values(leaders);
|
|
34614
|
+
entries.sort((a, b) => b.score - a.score);
|
|
34615
|
+
return entries.slice(0, limit);
|
|
34616
|
+
}
|
|
34617
|
+
catch (error) {
|
|
34618
|
+
console.error('Failed to get leaderboard:', error);
|
|
34619
|
+
return [];
|
|
34620
|
+
}
|
|
34621
|
+
}
|
|
34622
|
+
// Get user's streak statistics
|
|
34623
|
+
static async getUserStats(userId, locale) {
|
|
34624
|
+
try {
|
|
34625
|
+
const statsPath = `gatadagsins/users/${locale}/${userId}/stats`;
|
|
34626
|
+
const stats = await getFirebaseData(statsPath);
|
|
34627
|
+
return stats;
|
|
34628
|
+
}
|
|
34629
|
+
catch (error) {
|
|
34630
|
+
console.error('Failed to get user stats:', error);
|
|
34631
|
+
return null;
|
|
34632
|
+
}
|
|
34633
|
+
}
|
|
34634
|
+
}
|
|
34635
|
+
RiddlePersistence.STORAGE_KEY_PREFIX = 'gata_';
|
|
34636
|
+
RiddlePersistence.MAX_AGE_DAYS = 30;
|
|
34637
|
+
|
|
34357
34638
|
/*
|
|
34358
34639
|
|
|
34359
34640
|
Riddle.ts
|
|
@@ -34368,8 +34649,8 @@ class Game extends BaseGame {
|
|
|
34368
34649
|
For further information, see https://github.com/mideind/Netskrafl
|
|
34369
34650
|
|
|
34370
34651
|
*/
|
|
34371
|
-
const HOT_WARM_BOUNDARY_RATIO = 0.
|
|
34372
|
-
const WARM_COLD_BOUNDARY_RATIO = 0.
|
|
34652
|
+
const HOT_WARM_BOUNDARY_RATIO = 0.5;
|
|
34653
|
+
const WARM_COLD_BOUNDARY_RATIO = 0.25;
|
|
34373
34654
|
class Riddle extends BaseGame {
|
|
34374
34655
|
constructor(uuid, date, model) {
|
|
34375
34656
|
if (!model.state) {
|
|
@@ -34441,6 +34722,22 @@ class Riddle extends BaseGame {
|
|
|
34441
34722
|
this.hotBoundary = this.bestPossibleScore * HOT_WARM_BOUNDARY_RATIO;
|
|
34442
34723
|
// Initialize word checker
|
|
34443
34724
|
wordChecker.ingestTwoLetterWords(this.locale, this.two_letter_words[0]);
|
|
34725
|
+
// Load persisted player moves from localStorage
|
|
34726
|
+
if (state.userId) {
|
|
34727
|
+
const persistedMoves = RiddlePersistence.loadLocalMoves(state.userId, date);
|
|
34728
|
+
if (persistedMoves.length > 0) {
|
|
34729
|
+
// Convert from IPlayerMove to RiddleWord format, preserving timestamps
|
|
34730
|
+
this.playerMoves = persistedMoves.map(move => ({
|
|
34731
|
+
word: move.word,
|
|
34732
|
+
score: move.score,
|
|
34733
|
+
coord: move.coord,
|
|
34734
|
+
timestamp: move.timestamp || new Date().toISOString() // Use stored timestamp or fallback
|
|
34735
|
+
}));
|
|
34736
|
+
// Update personal best score from persisted moves
|
|
34737
|
+
const bestMove = persistedMoves.reduce((best, current) => current.score > best.score ? current : best);
|
|
34738
|
+
this.personalBestScore = bestMove.score;
|
|
34739
|
+
}
|
|
34740
|
+
}
|
|
34444
34741
|
}
|
|
34445
34742
|
}
|
|
34446
34743
|
catch (error) {
|
|
@@ -34461,6 +34758,7 @@ class Riddle extends BaseGame {
|
|
|
34461
34758
|
locale: this.locale,
|
|
34462
34759
|
userId: state.userId,
|
|
34463
34760
|
groupId: state.userGroupId || null,
|
|
34761
|
+
userDisplayName: state.userFullname || state.userNick || state.userId,
|
|
34464
34762
|
move,
|
|
34465
34763
|
}
|
|
34466
34764
|
});
|
|
@@ -34509,13 +34807,26 @@ class Riddle extends BaseGame {
|
|
|
34509
34807
|
// If the move is not valid or was already played, return
|
|
34510
34808
|
if (!move)
|
|
34511
34809
|
return;
|
|
34512
|
-
|
|
34513
|
-
|
|
34514
|
-
|
|
34515
|
-
|
|
34516
|
-
|
|
34517
|
-
|
|
34518
|
-
|
|
34810
|
+
const { state } = this;
|
|
34811
|
+
if (!state || !state.userId)
|
|
34812
|
+
return;
|
|
34813
|
+
// Save all moves to localStorage (local backup/cache)
|
|
34814
|
+
// Convert RiddleWord[] to IPlayerMove[] for persistence
|
|
34815
|
+
const movesToSave = this.playerMoves.map(m => ({
|
|
34816
|
+
score: m.score,
|
|
34817
|
+
word: m.word,
|
|
34818
|
+
coord: m.coord,
|
|
34819
|
+
timestamp: m.timestamp
|
|
34820
|
+
}));
|
|
34821
|
+
RiddlePersistence.saveLocalMoves(state.userId, this.date, movesToSave);
|
|
34822
|
+
// If the move does not improve the personal best, we're done
|
|
34823
|
+
if (move.score <= this.personalBestScore)
|
|
34824
|
+
return;
|
|
34825
|
+
// This is the best score we've seen yet
|
|
34826
|
+
this.personalBestScore = move.score;
|
|
34827
|
+
// Submit to server; the server handles all Firebase updates
|
|
34828
|
+
// (achievements, stats, global best, leaderboard)
|
|
34829
|
+
this.submitRiddleWord(move);
|
|
34519
34830
|
}
|
|
34520
34831
|
updateGlobalBestScore(best) {
|
|
34521
34832
|
// Update the global best score, typically as a result
|
|
@@ -34742,6 +35053,9 @@ class Model {
|
|
|
34742
35053
|
this.game = null;
|
|
34743
35054
|
// The current Gáta Dagsins riddle, if any
|
|
34744
35055
|
this.riddle = null;
|
|
35056
|
+
// Gáta Dagsins-specific properties
|
|
35057
|
+
this.userStats = null;
|
|
35058
|
+
this.leaderboard = [];
|
|
34745
35059
|
// The current Netskrafl game list
|
|
34746
35060
|
this.gameList = null;
|
|
34747
35061
|
// Number of games where it's the player's turn, plus count of zombie games
|
|
@@ -35849,6 +36163,12 @@ class Actions {
|
|
|
35849
36163
|
if (state === null || state === void 0 ? void 0 : state.userGroupId) {
|
|
35850
36164
|
attachFirebaseListener(basePath + `group/${state.userGroupId}/best`, (json, firstAttach) => this.onRiddleGroupScoreUpdate(json, firstAttach));
|
|
35851
36165
|
}
|
|
36166
|
+
// Listen to global leaderboard
|
|
36167
|
+
attachFirebaseListener(basePath + "leaders", (json, firstAttach) => this.onLeaderboardUpdate(json, firstAttach));
|
|
36168
|
+
// Listen to user stats (if user is logged in)
|
|
36169
|
+
if (state === null || state === void 0 ? void 0 : state.userId) {
|
|
36170
|
+
attachFirebaseListener(`gatadagsins/users/${locale}/${state.userId}/stats`, (json, firstAttach) => this.onUserStatsUpdate(json, firstAttach));
|
|
36171
|
+
}
|
|
35852
36172
|
}
|
|
35853
36173
|
detachListenerFromRiddle(date, locale) {
|
|
35854
36174
|
const { state } = this.model;
|
|
@@ -35857,6 +36177,10 @@ class Actions {
|
|
|
35857
36177
|
if (state === null || state === void 0 ? void 0 : state.userGroupId) {
|
|
35858
36178
|
detachFirebaseListener(basePath + `group/${state.userGroupId}/best`);
|
|
35859
36179
|
}
|
|
36180
|
+
detachFirebaseListener(basePath + "leaders");
|
|
36181
|
+
if (state === null || state === void 0 ? void 0 : state.userId) {
|
|
36182
|
+
detachFirebaseListener(`gatadagsins/users/${locale}/${state.userId}/stats`);
|
|
36183
|
+
}
|
|
35860
36184
|
}
|
|
35861
36185
|
onRiddleGlobalScoreUpdate(json, firstAttach) {
|
|
35862
36186
|
const { riddle } = this.model;
|
|
@@ -35874,6 +36198,38 @@ class Actions {
|
|
|
35874
36198
|
riddle.updateGroupBestScore(json);
|
|
35875
36199
|
m.redraw();
|
|
35876
36200
|
}
|
|
36201
|
+
onLeaderboardUpdate(json, firstAttach) {
|
|
36202
|
+
if (!json || typeof json !== 'object') {
|
|
36203
|
+
this.model.leaderboard = [];
|
|
36204
|
+
}
|
|
36205
|
+
else {
|
|
36206
|
+
// Convert dictionary to array and sort by score (desc), then timestamp (desc)
|
|
36207
|
+
const entries = Object.keys(json).map(userId => ({
|
|
36208
|
+
userId: json[userId].userId || userId,
|
|
36209
|
+
displayName: json[userId].displayName || '',
|
|
36210
|
+
score: json[userId].score || 0,
|
|
36211
|
+
timestamp: json[userId].timestamp || ''
|
|
36212
|
+
}));
|
|
36213
|
+
// Sort by score descending, then by timestamp descending (newer first)
|
|
36214
|
+
entries.sort((a, b) => {
|
|
36215
|
+
if (b.score !== a.score) {
|
|
36216
|
+
return b.score - a.score;
|
|
36217
|
+
}
|
|
36218
|
+
return b.timestamp.localeCompare(a.timestamp);
|
|
36219
|
+
});
|
|
36220
|
+
this.model.leaderboard = entries;
|
|
36221
|
+
}
|
|
36222
|
+
m.redraw();
|
|
36223
|
+
}
|
|
36224
|
+
onUserStatsUpdate(json, firstAttach) {
|
|
36225
|
+
if (!json) {
|
|
36226
|
+
this.model.userStats = null;
|
|
36227
|
+
}
|
|
36228
|
+
else {
|
|
36229
|
+
this.model.userStats = json;
|
|
36230
|
+
}
|
|
36231
|
+
m.redraw();
|
|
36232
|
+
}
|
|
35877
36233
|
async fetchRiddle(date, locale) {
|
|
35878
36234
|
// Create the game via model
|
|
35879
36235
|
if (!this.model)
|
|
@@ -36064,13 +36420,14 @@ const Netskrafl = React.memo(NetskraflImpl);
|
|
|
36064
36420
|
*/
|
|
36065
36421
|
const RiddleScore = {
|
|
36066
36422
|
view: (vnode) => {
|
|
36067
|
-
const { riddle } = vnode.attrs;
|
|
36423
|
+
const { riddle, mode = "desktop" } = vnode.attrs;
|
|
36068
36424
|
if (!riddle)
|
|
36069
36425
|
return m("div");
|
|
36070
36426
|
const score = riddle.currentScore;
|
|
36071
36427
|
const hasValidMove = score !== undefined;
|
|
36072
36428
|
const hasTiles = riddle.tilesPlaced().length > 0;
|
|
36073
|
-
|
|
36429
|
+
const baseClass = (mode === "mobile" ? ".mobile-score" : ".gata-dagsins-score");
|
|
36430
|
+
let classes = [baseClass];
|
|
36074
36431
|
let displayText = "0";
|
|
36075
36432
|
if (!hasTiles) {
|
|
36076
36433
|
// State 1: No tiles on board - grayed/disabled, showing zero
|
|
@@ -36104,7 +36461,8 @@ const RiddleScore = {
|
|
|
36104
36461
|
classes.push(".celebrate");
|
|
36105
36462
|
}
|
|
36106
36463
|
}
|
|
36107
|
-
|
|
36464
|
+
const legendClass = mode === "mobile" ? ".mobile-score-legend" : ".gata-dagsins-legend";
|
|
36465
|
+
return m("div" + classes.join(""), m("span" + legendClass, displayText));
|
|
36108
36466
|
}
|
|
36109
36467
|
};
|
|
36110
36468
|
|
|
@@ -36164,35 +36522,40 @@ const GataDagsinsBoardAndRack = {
|
|
|
36164
36522
|
*/
|
|
36165
36523
|
const SunCorona = {
|
|
36166
36524
|
view: (vnode) => {
|
|
36167
|
-
const { animate = false } = vnode.attrs;
|
|
36525
|
+
const { animate = false, size = 80 } = vnode.attrs;
|
|
36526
|
+
// Calculate ray positions based on size
|
|
36527
|
+
// For 80px: rays from -40 to -20 (inner radius 20px, outer radius 40px)
|
|
36528
|
+
// For 90px: rays from -45 to -30 (inner radius 30px, outer radius 45px) to fit 60px circle
|
|
36529
|
+
const outerRadius = size / 2;
|
|
36530
|
+
const innerRadius = size / 3;
|
|
36168
36531
|
return m("div.sun-corona" + (animate ? ".rotating" : ""), [
|
|
36169
36532
|
m.trust(`
|
|
36170
|
-
<svg width="
|
|
36533
|
+
<svg width="${size}" height="${size}" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
36171
36534
|
<g transform="translate(50,50)">
|
|
36172
36535
|
<!-- Ray at 0° (12 o'clock) -->
|
|
36173
|
-
<polygon points="0
|
|
36536
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(0)"/>
|
|
36174
36537
|
<!-- Ray at 30° -->
|
|
36175
|
-
<polygon points="0
|
|
36538
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(30)"/>
|
|
36176
36539
|
<!-- Ray at 60° -->
|
|
36177
|
-
<polygon points="0
|
|
36540
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(60)"/>
|
|
36178
36541
|
<!-- Ray at 90° (3 o'clock) -->
|
|
36179
|
-
<polygon points="0
|
|
36542
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(90)"/>
|
|
36180
36543
|
<!-- Ray at 120° -->
|
|
36181
|
-
<polygon points="0
|
|
36544
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(120)"/>
|
|
36182
36545
|
<!-- Ray at 150° -->
|
|
36183
|
-
<polygon points="0
|
|
36546
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(150)"/>
|
|
36184
36547
|
<!-- Ray at 180° (6 o'clock) -->
|
|
36185
|
-
<polygon points="0
|
|
36548
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(180)"/>
|
|
36186
36549
|
<!-- Ray at 210° -->
|
|
36187
|
-
<polygon points="0
|
|
36550
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(210)"/>
|
|
36188
36551
|
<!-- Ray at 240° -->
|
|
36189
|
-
<polygon points="0
|
|
36552
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(240)"/>
|
|
36190
36553
|
<!-- Ray at 270° (9 o'clock) -->
|
|
36191
|
-
<polygon points="0
|
|
36554
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(270)"/>
|
|
36192
36555
|
<!-- Ray at 300° -->
|
|
36193
|
-
<polygon points="0
|
|
36556
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(300)"/>
|
|
36194
36557
|
<!-- Ray at 330° -->
|
|
36195
|
-
<polygon points="0
|
|
36558
|
+
<polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(330)"/>
|
|
36196
36559
|
</g>
|
|
36197
36560
|
</svg>
|
|
36198
36561
|
`)
|
|
@@ -36200,6 +36563,101 @@ const SunCorona = {
|
|
|
36200
36563
|
}
|
|
36201
36564
|
};
|
|
36202
36565
|
|
|
36566
|
+
/*
|
|
36567
|
+
|
|
36568
|
+
Mobile-Status.ts
|
|
36569
|
+
|
|
36570
|
+
Mobile-only horizontal status component for Gáta Dagsins
|
|
36571
|
+
|
|
36572
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36573
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36574
|
+
|
|
36575
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36576
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36577
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36578
|
+
|
|
36579
|
+
*/
|
|
36580
|
+
// Mobile-only horizontal status display
|
|
36581
|
+
const MobileStatus = () => {
|
|
36582
|
+
return {
|
|
36583
|
+
view: (vnode) => {
|
|
36584
|
+
const { riddle, selectedMoves, bestMove, onMoveClick } = vnode.attrs;
|
|
36585
|
+
const { bestPossibleScore, globalBestScore } = riddle;
|
|
36586
|
+
// Determine if player achieved best possible score
|
|
36587
|
+
const achieved = bestMove !== undefined;
|
|
36588
|
+
const celebrate = bestMove && bestMove.word !== "";
|
|
36589
|
+
// Get player's current best score
|
|
36590
|
+
const playerBestScore = selectedMoves.length > 0 ? selectedMoves[0].score : 0;
|
|
36591
|
+
// Determine current leader score (may be this player or another)
|
|
36592
|
+
let leaderScore = 0;
|
|
36593
|
+
let isPlayerLeading = false;
|
|
36594
|
+
if (globalBestScore && globalBestScore.score > 0) {
|
|
36595
|
+
leaderScore = globalBestScore.score;
|
|
36596
|
+
// Check if player is leading
|
|
36597
|
+
isPlayerLeading = playerBestScore >= globalBestScore.score;
|
|
36598
|
+
}
|
|
36599
|
+
else {
|
|
36600
|
+
leaderScore = playerBestScore;
|
|
36601
|
+
isPlayerLeading = playerBestScore > 0;
|
|
36602
|
+
}
|
|
36603
|
+
return m(".mobile-status-container", [
|
|
36604
|
+
// Current word score (leftmost) - uses RiddleScore component in mobile mode
|
|
36605
|
+
m(".mobile-status-item", m(RiddleScore, { riddle, mode: "mobile" })),
|
|
36606
|
+
// Player's best score
|
|
36607
|
+
m(".mobile-status-item.player-best", [
|
|
36608
|
+
m(".mobile-status-label", ts("Þín besta:")),
|
|
36609
|
+
m(".mobile-status-score", playerBestScore.toString())
|
|
36610
|
+
]),
|
|
36611
|
+
// Current leader score
|
|
36612
|
+
m(".mobile-status-item.leader" + (isPlayerLeading ? ".is-player" : ""), [
|
|
36613
|
+
m(".mobile-status-label", isPlayerLeading ? ts("Þú leiðir!") : ts("Leiðandi:")),
|
|
36614
|
+
m(".mobile-status-score", leaderScore.toString())
|
|
36615
|
+
]),
|
|
36616
|
+
// Best possible score
|
|
36617
|
+
m(".mobile-status-item.best-possible"
|
|
36618
|
+
+ (celebrate ? ".celebrate" : "")
|
|
36619
|
+
+ (achieved ? ".achieved" : ""), {
|
|
36620
|
+
onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
|
|
36621
|
+
}, [
|
|
36622
|
+
// Wrapper for score and corona to position them together
|
|
36623
|
+
m(".mobile-best-score-wrapper", [
|
|
36624
|
+
celebrate ? m(SunCorona, { animate: true, size: 100 }) : null,
|
|
36625
|
+
m(".mobile-status-score", bestPossibleScore.toString())
|
|
36626
|
+
])
|
|
36627
|
+
])
|
|
36628
|
+
]);
|
|
36629
|
+
}
|
|
36630
|
+
};
|
|
36631
|
+
};
|
|
36632
|
+
|
|
36633
|
+
/*
|
|
36634
|
+
|
|
36635
|
+
TabBar.ts
|
|
36636
|
+
|
|
36637
|
+
Reusable tab navigation component
|
|
36638
|
+
|
|
36639
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36640
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36641
|
+
|
|
36642
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36643
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36644
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36645
|
+
|
|
36646
|
+
*/
|
|
36647
|
+
const TabBar = {
|
|
36648
|
+
view: (vnode) => {
|
|
36649
|
+
const { tabs, activeTab, onTabChange } = vnode.attrs;
|
|
36650
|
+
return m(".tab-bar", tabs.map(tab => m(".tab-item" + (activeTab === tab.id ? ".active" : ""), {
|
|
36651
|
+
key: tab.id,
|
|
36652
|
+
onclick: () => onTabChange(tab.id)
|
|
36653
|
+
}, [
|
|
36654
|
+
tab.iconGlyph ? m("span.tab-icon", glyph(tab.iconGlyph)) :
|
|
36655
|
+
tab.icon ? m("span.tab-icon", tab.icon) : null,
|
|
36656
|
+
m("span.tab-label", tab.label)
|
|
36657
|
+
])));
|
|
36658
|
+
}
|
|
36659
|
+
};
|
|
36660
|
+
|
|
36203
36661
|
/*
|
|
36204
36662
|
|
|
36205
36663
|
Thermometer.ts
|
|
@@ -36274,12 +36732,27 @@ const BestPossibleScore = () => {
|
|
|
36274
36732
|
return {
|
|
36275
36733
|
view: (vnode) => {
|
|
36276
36734
|
const { score, bestMove, onMoveClick } = vnode.attrs;
|
|
36277
|
-
|
|
36278
|
-
|
|
36279
|
-
|
|
36735
|
+
// Determine the label based on achievement status
|
|
36736
|
+
let topLabel;
|
|
36737
|
+
if (bestMove !== undefined) {
|
|
36738
|
+
if (bestMove.word) {
|
|
36739
|
+
// Current player achieved it - show their word
|
|
36740
|
+
topLabel = removeBlankMarkers(bestMove.word);
|
|
36741
|
+
}
|
|
36742
|
+
else {
|
|
36743
|
+
// Someone else achieved it - indicate this
|
|
36744
|
+
topLabel = ts("Bestu lögn náð!");
|
|
36745
|
+
}
|
|
36746
|
+
}
|
|
36747
|
+
else {
|
|
36748
|
+
// Not achieved yet - show default label
|
|
36749
|
+
topLabel = ts("Besta mögulega lögn");
|
|
36750
|
+
}
|
|
36751
|
+
const achieved = bestMove !== undefined;
|
|
36280
36752
|
const celebrate = bestMove && bestMove.word !== "";
|
|
36281
36753
|
return m(".thermometer-best-score"
|
|
36282
|
-
+ (celebrate ? ".celebrate" : "")
|
|
36754
|
+
+ (celebrate ? ".celebrate" : "")
|
|
36755
|
+
+ (achieved ? ".achieved" : ""), m(".thermometer-best-score-container", {
|
|
36283
36756
|
onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
|
|
36284
36757
|
}, [
|
|
36285
36758
|
// Sun corona behind the circle when celebrating
|
|
@@ -36407,6 +36880,196 @@ const Thermometer = () => {
|
|
|
36407
36880
|
};
|
|
36408
36881
|
};
|
|
36409
36882
|
|
|
36883
|
+
/*
|
|
36884
|
+
|
|
36885
|
+
StatsView.ts
|
|
36886
|
+
|
|
36887
|
+
User statistics display component for Gáta Dagsins
|
|
36888
|
+
|
|
36889
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36890
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36891
|
+
|
|
36892
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36893
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36894
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36895
|
+
|
|
36896
|
+
*/
|
|
36897
|
+
const StatsView = {
|
|
36898
|
+
view: (vnode) => {
|
|
36899
|
+
const { stats, loading = false } = vnode.attrs;
|
|
36900
|
+
if (loading) {
|
|
36901
|
+
return m(".stats-view.loading", m(".loading-message", ts("Sæki tölfræði...")));
|
|
36902
|
+
}
|
|
36903
|
+
if (!stats) {
|
|
36904
|
+
return m(".stats-view.empty", m(".empty-message", ts("Engin tölfræði til að sýna")));
|
|
36905
|
+
}
|
|
36906
|
+
const statItems = [
|
|
36907
|
+
{
|
|
36908
|
+
iconGlyph: "fire",
|
|
36909
|
+
label: ts("Núverandi striklota"),
|
|
36910
|
+
value: stats.currentStreak,
|
|
36911
|
+
highlight: stats.currentStreak > 0
|
|
36912
|
+
},
|
|
36913
|
+
{
|
|
36914
|
+
iconGlyph: "star",
|
|
36915
|
+
label: ts("Lengsta striklota"),
|
|
36916
|
+
value: stats.longestStreak
|
|
36917
|
+
},
|
|
36918
|
+
{
|
|
36919
|
+
iconGlyph: "tower",
|
|
36920
|
+
label: ts("Hæsta skori náð"),
|
|
36921
|
+
value: stats.totalTopScores
|
|
36922
|
+
},
|
|
36923
|
+
{
|
|
36924
|
+
iconGlyph: "certificate",
|
|
36925
|
+
label: ts("Striklota hæsta skors"),
|
|
36926
|
+
value: stats.topScoreStreak,
|
|
36927
|
+
highlight: stats.topScoreStreak > 0
|
|
36928
|
+
},
|
|
36929
|
+
{
|
|
36930
|
+
iconGlyph: "calendar",
|
|
36931
|
+
label: ts("Heildarfjöldi daga"),
|
|
36932
|
+
value: stats.totalDaysPlayed
|
|
36933
|
+
},
|
|
36934
|
+
];
|
|
36935
|
+
return m(".stats-view", m(".stats-grid", statItems.map((item, index) => m(".stat-item" + (item.highlight ? ".highlight" : ""), { key: index }, [
|
|
36936
|
+
m(".stat-icon", glyph(item.iconGlyph)),
|
|
36937
|
+
m(".stat-info", [
|
|
36938
|
+
m(".stat-label", item.label),
|
|
36939
|
+
m(".stat-value", item.value.toString())
|
|
36940
|
+
])
|
|
36941
|
+
]))));
|
|
36942
|
+
}
|
|
36943
|
+
};
|
|
36944
|
+
|
|
36945
|
+
/*
|
|
36946
|
+
|
|
36947
|
+
LeaderboardView.ts
|
|
36948
|
+
|
|
36949
|
+
Daily leaderboard display component for Gáta Dagsins
|
|
36950
|
+
|
|
36951
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36952
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36953
|
+
|
|
36954
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36955
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36956
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36957
|
+
|
|
36958
|
+
*/
|
|
36959
|
+
function getMedalIcon(rank) {
|
|
36960
|
+
switch (rank) {
|
|
36961
|
+
case 1: return "🥇";
|
|
36962
|
+
case 2: return "🥈";
|
|
36963
|
+
case 3: return "🥉";
|
|
36964
|
+
default: return null;
|
|
36965
|
+
}
|
|
36966
|
+
}
|
|
36967
|
+
function formatDate(dateStr) {
|
|
36968
|
+
// Format YYYY-MM-DD to Icelandic date (e.g., "2. okt.")
|
|
36969
|
+
const date = new Date(dateStr + "T00:00:00");
|
|
36970
|
+
const day = date.getDate();
|
|
36971
|
+
const months = [
|
|
36972
|
+
"janúar", "febrúar", "mars", "apríl", "maí", "júní",
|
|
36973
|
+
"júlí", "ágúst", "september", "október", "nóvember", "desember",
|
|
36974
|
+
];
|
|
36975
|
+
const month = months[date.getMonth()];
|
|
36976
|
+
return `${day}. ${month}`;
|
|
36977
|
+
}
|
|
36978
|
+
const LeaderboardView = {
|
|
36979
|
+
view: (vnode) => {
|
|
36980
|
+
const { leaderboard, currentUserId, date, loading = false } = vnode.attrs;
|
|
36981
|
+
if (loading) {
|
|
36982
|
+
return m(".leaderboard-view.loading", m(".loading-message", ts("Hleð stigatöflu...")));
|
|
36983
|
+
}
|
|
36984
|
+
if (!leaderboard || leaderboard.length === 0) {
|
|
36985
|
+
return m(".leaderboard-view.empty", m(".empty-message", ts("Engin stig skráð enn")));
|
|
36986
|
+
}
|
|
36987
|
+
return m(".leaderboard-view", [
|
|
36988
|
+
m(".leaderboard-header", [
|
|
36989
|
+
m(".leaderboard-title", formatDate(date)),
|
|
36990
|
+
]),
|
|
36991
|
+
m(".leaderboard-list", leaderboard.map((entry, index) => {
|
|
36992
|
+
const rank = index + 1;
|
|
36993
|
+
const isCurrentUser = entry.userId === currentUserId;
|
|
36994
|
+
const medal = getMedalIcon(rank);
|
|
36995
|
+
return m(".leaderboard-entry" + (isCurrentUser ? ".current-user" : ""), { key: entry.userId }, [
|
|
36996
|
+
m(".entry-rank", [
|
|
36997
|
+
medal ? m("span.medal", medal) : m("span.rank-number", rank.toString())
|
|
36998
|
+
]),
|
|
36999
|
+
m(".entry-name", isCurrentUser ? ts("Þú") : entry.displayName),
|
|
37000
|
+
m(".entry-score", entry.score.toString())
|
|
37001
|
+
]);
|
|
37002
|
+
}))
|
|
37003
|
+
]);
|
|
37004
|
+
}
|
|
37005
|
+
};
|
|
37006
|
+
|
|
37007
|
+
/*
|
|
37008
|
+
|
|
37009
|
+
RightSideTabs.ts
|
|
37010
|
+
|
|
37011
|
+
Desktop tabbed container for Performance/Stats/Leaderboard in Gáta Dagsins
|
|
37012
|
+
|
|
37013
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
37014
|
+
Author: Vilhjálmur Þorsteinsson
|
|
37015
|
+
|
|
37016
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
37017
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
37018
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
37019
|
+
|
|
37020
|
+
*/
|
|
37021
|
+
const RightSideTabs = () => {
|
|
37022
|
+
// Component-local state for active tab (defaults to performance)
|
|
37023
|
+
let activeTab = "performance";
|
|
37024
|
+
const tabs = [
|
|
37025
|
+
{ id: "performance", label: ts("Frammistaða"), iconGlyph: "dashboard" },
|
|
37026
|
+
{ id: "stats", label: ts("Tölfræði"), iconGlyph: "stats" },
|
|
37027
|
+
{ id: "leaderboard", label: ts("Stigatafla"), iconGlyph: "tower" }
|
|
37028
|
+
];
|
|
37029
|
+
return {
|
|
37030
|
+
view: (vnode) => {
|
|
37031
|
+
const { view, selectedMoves, bestMove, onMoveClick } = vnode.attrs;
|
|
37032
|
+
const { riddle, state } = view.model;
|
|
37033
|
+
if (!riddle) {
|
|
37034
|
+
return m(".gatadagsins-right-side-tabs", "");
|
|
37035
|
+
}
|
|
37036
|
+
const handleTabChange = (tabId) => {
|
|
37037
|
+
activeTab = tabId;
|
|
37038
|
+
};
|
|
37039
|
+
return m(".gatadagsins-right-side-tabs", [
|
|
37040
|
+
// Tab navigation
|
|
37041
|
+
m(TabBar, {
|
|
37042
|
+
tabs,
|
|
37043
|
+
activeTab,
|
|
37044
|
+
onTabChange: handleTabChange
|
|
37045
|
+
}),
|
|
37046
|
+
// Tab content
|
|
37047
|
+
m(".tab-content", [
|
|
37048
|
+
// Performance tab (thermometer)
|
|
37049
|
+
activeTab === "performance" ? m(Thermometer, {
|
|
37050
|
+
riddle,
|
|
37051
|
+
selectedMoves,
|
|
37052
|
+
bestMove,
|
|
37053
|
+
onMoveClick
|
|
37054
|
+
}) : null,
|
|
37055
|
+
// Stats tab
|
|
37056
|
+
activeTab === "stats" ? m(StatsView, {
|
|
37057
|
+
stats: view.model.userStats || null,
|
|
37058
|
+
loading: false
|
|
37059
|
+
}) : null,
|
|
37060
|
+
// Leaderboard tab
|
|
37061
|
+
activeTab === "leaderboard" ? m(LeaderboardView, {
|
|
37062
|
+
leaderboard: view.model.leaderboard || [],
|
|
37063
|
+
currentUserId: (state === null || state === void 0 ? void 0 : state.userId) || "",
|
|
37064
|
+
date: riddle.date,
|
|
37065
|
+
loading: false
|
|
37066
|
+
}) : null
|
|
37067
|
+
])
|
|
37068
|
+
]);
|
|
37069
|
+
}
|
|
37070
|
+
};
|
|
37071
|
+
};
|
|
37072
|
+
|
|
36410
37073
|
/*
|
|
36411
37074
|
|
|
36412
37075
|
GataDagsins-Right-Side.ts
|
|
@@ -36422,7 +37085,7 @@ const Thermometer = () => {
|
|
|
36422
37085
|
|
|
36423
37086
|
*/
|
|
36424
37087
|
const GataDagsinsRightSide = {
|
|
36425
|
-
// Component containing
|
|
37088
|
+
// Component containing both mobile status bar and desktop tabbed view
|
|
36426
37089
|
view: (vnode) => {
|
|
36427
37090
|
const { view, selectedMoves, bestMove } = vnode.attrs;
|
|
36428
37091
|
const { riddle } = view.model;
|
|
@@ -36433,13 +37096,20 @@ const GataDagsinsRightSide = {
|
|
|
36433
37096
|
}
|
|
36434
37097
|
};
|
|
36435
37098
|
return m(".gatadagsins-right-side-wrapper", riddle ? [
|
|
36436
|
-
//
|
|
36437
|
-
m(".gatadagsins-
|
|
37099
|
+
// Mobile-only status bar (visible on mobile, hidden on desktop)
|
|
37100
|
+
m(".gatadagsins-mobile-status", m(MobileStatus, {
|
|
36438
37101
|
riddle,
|
|
36439
37102
|
selectedMoves,
|
|
36440
37103
|
bestMove,
|
|
36441
37104
|
onMoveClick: handleMoveClick
|
|
36442
37105
|
})),
|
|
37106
|
+
// Desktop-only tabbed view (hidden on mobile, visible on desktop)
|
|
37107
|
+
m(".gatadagsins-thermometer-column", m(RightSideTabs, {
|
|
37108
|
+
view,
|
|
37109
|
+
selectedMoves,
|
|
37110
|
+
bestMove,
|
|
37111
|
+
onMoveClick: handleMoveClick
|
|
37112
|
+
})),
|
|
36443
37113
|
] : null);
|
|
36444
37114
|
}
|
|
36445
37115
|
};
|
|
@@ -36522,6 +37192,102 @@ const GataDagsinsHelp = {
|
|
|
36522
37192
|
}
|
|
36523
37193
|
};
|
|
36524
37194
|
|
|
37195
|
+
/*
|
|
37196
|
+
|
|
37197
|
+
StatsModal.ts
|
|
37198
|
+
|
|
37199
|
+
Mobile modal for stats and leaderboard
|
|
37200
|
+
|
|
37201
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
37202
|
+
Author: Vilhjálmur Þorsteinsson
|
|
37203
|
+
|
|
37204
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
37205
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
37206
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
37207
|
+
|
|
37208
|
+
*/
|
|
37209
|
+
const StatsModal = () => {
|
|
37210
|
+
// Component-local state for active tab (defaults to stats)
|
|
37211
|
+
let activeTab = "stats";
|
|
37212
|
+
const tabs = [
|
|
37213
|
+
{ id: "stats", label: ts("Tölfræði"), iconGlyph: "stats" },
|
|
37214
|
+
{ id: "leaderboard", label: ts("Stigatafla"), iconGlyph: "tower" }
|
|
37215
|
+
];
|
|
37216
|
+
return {
|
|
37217
|
+
view: (vnode) => {
|
|
37218
|
+
const { view, onClose } = vnode.attrs;
|
|
37219
|
+
const { riddle, state } = view.model;
|
|
37220
|
+
if (!riddle) {
|
|
37221
|
+
return null;
|
|
37222
|
+
}
|
|
37223
|
+
const handleTabChange = (tabId) => {
|
|
37224
|
+
activeTab = tabId;
|
|
37225
|
+
};
|
|
37226
|
+
return [
|
|
37227
|
+
// Backdrop
|
|
37228
|
+
m(".modal-backdrop", {
|
|
37229
|
+
onclick: onClose
|
|
37230
|
+
}),
|
|
37231
|
+
// Modal dialog
|
|
37232
|
+
m(".modal-dialog.stats-modal", [
|
|
37233
|
+
m(".modal-content", [
|
|
37234
|
+
// Header with close button
|
|
37235
|
+
m(".modal-header", [
|
|
37236
|
+
m("h2", ts("Tölfræði")),
|
|
37237
|
+
m("button.close", {
|
|
37238
|
+
onclick: onClose
|
|
37239
|
+
}, "×")
|
|
37240
|
+
]),
|
|
37241
|
+
// Tab navigation
|
|
37242
|
+
m(TabBar, {
|
|
37243
|
+
tabs,
|
|
37244
|
+
activeTab,
|
|
37245
|
+
onTabChange: handleTabChange
|
|
37246
|
+
}),
|
|
37247
|
+
// Modal body with tab content
|
|
37248
|
+
m(".modal-body", [
|
|
37249
|
+
activeTab === "stats" ? m(StatsView, {
|
|
37250
|
+
stats: view.model.userStats || null,
|
|
37251
|
+
loading: false
|
|
37252
|
+
}) : null,
|
|
37253
|
+
activeTab === "leaderboard" ? m(LeaderboardView, {
|
|
37254
|
+
leaderboard: view.model.leaderboard || [],
|
|
37255
|
+
currentUserId: (state === null || state === void 0 ? void 0 : state.userId) || "",
|
|
37256
|
+
date: riddle.date,
|
|
37257
|
+
loading: false
|
|
37258
|
+
}) : null
|
|
37259
|
+
])
|
|
37260
|
+
])
|
|
37261
|
+
])
|
|
37262
|
+
];
|
|
37263
|
+
}
|
|
37264
|
+
};
|
|
37265
|
+
};
|
|
37266
|
+
|
|
37267
|
+
/*
|
|
37268
|
+
|
|
37269
|
+
MobileStatsButton.ts
|
|
37270
|
+
|
|
37271
|
+
Button to open stats modal on mobile
|
|
37272
|
+
|
|
37273
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
37274
|
+
Author: Vilhjálmur Þorsteinsson
|
|
37275
|
+
|
|
37276
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
37277
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
37278
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
37279
|
+
|
|
37280
|
+
*/
|
|
37281
|
+
const MobileStatsButton = {
|
|
37282
|
+
view: (vnode) => {
|
|
37283
|
+
const { onClick } = vnode.attrs;
|
|
37284
|
+
return m(".mobile-stats-button", {
|
|
37285
|
+
onclick: onClick,
|
|
37286
|
+
title: "Tölfræði og stigatafla"
|
|
37287
|
+
}, m(".stats-icon", glyph("stats")));
|
|
37288
|
+
}
|
|
37289
|
+
};
|
|
37290
|
+
|
|
36525
37291
|
/*
|
|
36526
37292
|
|
|
36527
37293
|
GataDagsins.ts
|
|
@@ -36537,11 +37303,19 @@ const GataDagsinsHelp = {
|
|
|
36537
37303
|
|
|
36538
37304
|
*/
|
|
36539
37305
|
const MAX_MOVES_TO_DISPLAY = 10;
|
|
36540
|
-
const
|
|
36541
|
-
var _a;
|
|
37306
|
+
const currentMoveState = (riddle) => {
|
|
37307
|
+
var _a, _b, _c;
|
|
37308
|
+
const thisPlayer = ((_a = riddle.model.state) === null || _a === void 0 ? void 0 : _a.userId) || "";
|
|
37309
|
+
const { bestPossibleScore, // The highest score achievable for this riddle
|
|
37310
|
+
globalBestScore, // The best score achieved by any player
|
|
37311
|
+
groupBestScore, // The best score achieved within the player's group
|
|
37312
|
+
playerMoves, } = riddle;
|
|
37313
|
+
// If the player has equaled the best possible score,
|
|
37314
|
+
// the winning word is stored here and displayed at the top
|
|
37315
|
+
let bestMove = undefined;
|
|
36542
37316
|
// Sort moves by score in descending order and
|
|
36543
37317
|
// cut the tail off the list to only include the top moves
|
|
36544
|
-
const
|
|
37318
|
+
const selectedMoves = playerMoves
|
|
36545
37319
|
.sort((a, b) => b.score - a.score)
|
|
36546
37320
|
.slice(0, MAX_MOVES_TO_DISPLAY)
|
|
36547
37321
|
.map(move => ({
|
|
@@ -36549,61 +37323,49 @@ const selectTopMoves = (thisPlayer, moves, globalBestScore, groupBestScore) => {
|
|
|
36549
37323
|
word: move.word,
|
|
36550
37324
|
coord: move.coord,
|
|
36551
37325
|
}));
|
|
36552
|
-
//
|
|
36553
|
-
// the player's own top score, we include it as the first move
|
|
37326
|
+
// Check whether we need to add or annotate the global best score
|
|
36554
37327
|
if (globalBestScore && globalBestScore.score > 0) {
|
|
36555
37328
|
const { score, word, coord } = globalBestScore;
|
|
36556
|
-
if (((
|
|
37329
|
+
if (((_c = (_b = selectedMoves[0]) === null || _b === void 0 ? void 0 : _b.score) !== null && _c !== void 0 ? _c : 0) >= score) {
|
|
36557
37330
|
// This player has made a move that scores the same
|
|
36558
37331
|
// or better as the top score: mark the move
|
|
36559
|
-
|
|
37332
|
+
selectedMoves[0].isGlobalBestScore = true;
|
|
36560
37333
|
}
|
|
36561
37334
|
else if (globalBestScore.player === thisPlayer) {
|
|
36562
37335
|
// This player holds the global best score, probably
|
|
36563
|
-
// from a previous session
|
|
36564
|
-
|
|
37336
|
+
// from a previous session, so it's not already
|
|
37337
|
+
// in the selectedMoves list: add it as a move
|
|
37338
|
+
selectedMoves.unshift({ score, isGlobalBestScore: true, word, coord });
|
|
36565
37339
|
}
|
|
36566
37340
|
else {
|
|
36567
|
-
// This is a global best score from another player
|
|
36568
|
-
|
|
37341
|
+
// This is a global best score from another player
|
|
37342
|
+
selectedMoves.unshift({ score, isGlobalBestScore: true, word: "", coord: "" });
|
|
36569
37343
|
}
|
|
36570
37344
|
}
|
|
36571
|
-
//
|
|
36572
|
-
|
|
36573
|
-
|
|
36574
|
-
const currentMoveState = (riddle) => {
|
|
36575
|
-
var _a;
|
|
36576
|
-
const thisPlayer = ((_a = riddle.model.state) === null || _a === void 0 ? void 0 : _a.userId) || "";
|
|
36577
|
-
const { bestPossibleScore, globalBestScore, groupBestScore, playerMoves, } = riddle;
|
|
36578
|
-
// If the player has equaled the best possible score,
|
|
36579
|
-
// the winning word is stored here and displayed at the top
|
|
36580
|
-
let bestMove = undefined;
|
|
36581
|
-
// Apply the move selection and allocation algorithm
|
|
36582
|
-
const selectedMoves = selectTopMoves(thisPlayer, playerMoves, globalBestScore);
|
|
36583
|
-
// If the top-scoring move has the bestPossibleScore,
|
|
36584
|
-
// extract it from the move list
|
|
37345
|
+
// Check if the best possible score has been achieved, by this player
|
|
37346
|
+
// or another player. If so, we remove it from the move list, since we
|
|
37347
|
+
// only display it at the top of the thermometer.
|
|
36585
37348
|
if (selectedMoves.length > 0 && selectedMoves[0].score === bestPossibleScore) {
|
|
36586
|
-
|
|
36587
|
-
// The word was played by this player
|
|
36588
|
-
bestMove = selectedMoves.shift();
|
|
36589
|
-
}
|
|
37349
|
+
bestMove = selectedMoves.shift();
|
|
36590
37350
|
}
|
|
36591
37351
|
return { selectedMoves, bestMove };
|
|
36592
37352
|
};
|
|
36593
37353
|
const GataDagsins$1 = () => {
|
|
36594
37354
|
// A view of the Gáta Dagsins page
|
|
36595
37355
|
let showHelp = false;
|
|
37356
|
+
let showStatsModal = false;
|
|
36596
37357
|
return {
|
|
36597
|
-
oninit: (vnode) => {
|
|
37358
|
+
oninit: async (vnode) => {
|
|
36598
37359
|
const { model, actions } = vnode.attrs.view;
|
|
36599
37360
|
const { riddle } = model;
|
|
36600
37361
|
if (!riddle) {
|
|
36601
37362
|
const { date, locale } = vnode.attrs;
|
|
36602
37363
|
// Initialize a fresh riddle object if it doesn't exist
|
|
36603
|
-
actions.fetchRiddle(date, locale);
|
|
37364
|
+
await actions.fetchRiddle(date, locale);
|
|
36604
37365
|
}
|
|
36605
|
-
// Initialize
|
|
37366
|
+
// Initialize dialog states
|
|
36606
37367
|
showHelp = false;
|
|
37368
|
+
showStatsModal = false;
|
|
36607
37369
|
},
|
|
36608
37370
|
view: (vnode) => {
|
|
36609
37371
|
var _a;
|
|
@@ -36617,6 +37379,10 @@ const GataDagsins$1 = () => {
|
|
|
36617
37379
|
showHelp = !showHelp;
|
|
36618
37380
|
m.redraw();
|
|
36619
37381
|
};
|
|
37382
|
+
const toggleStatsModal = () => {
|
|
37383
|
+
showStatsModal = !showStatsModal;
|
|
37384
|
+
m.redraw();
|
|
37385
|
+
};
|
|
36620
37386
|
return m("div.drop-target", {
|
|
36621
37387
|
id: "gatadagsins-background",
|
|
36622
37388
|
}, [
|
|
@@ -36641,11 +37407,15 @@ const GataDagsins$1 = () => {
|
|
|
36641
37407
|
((_a = model.state) === null || _a === void 0 ? void 0 : _a.beginner) ? m(Beginner, { view }) : "",
|
|
36642
37408
|
// Custom Info button for GataDagsins that shows help dialog
|
|
36643
37409
|
m(".info", { title: ts("Upplýsingar og hjálp") }, m("a.iconlink", { href: "#", onclick: (e) => { e.preventDefault(); toggleHelp(); } }, glyph("info-sign"))),
|
|
37410
|
+
// Mobile stats button (hidden on desktop)
|
|
37411
|
+
m(MobileStatsButton, { onClick: toggleStatsModal }),
|
|
36644
37412
|
// Help dialog and backdrop
|
|
36645
37413
|
showHelp ? [
|
|
36646
37414
|
m(".modal-backdrop", { onclick: (e) => { e.preventDefault(); } }),
|
|
36647
37415
|
m(GataDagsinsHelp, { onClose: toggleHelp })
|
|
36648
37416
|
] : "",
|
|
37417
|
+
// Stats modal and backdrop (mobile only)
|
|
37418
|
+
showStatsModal ? m(StatsModal, { view, onClose: toggleStatsModal }) : "",
|
|
36649
37419
|
]);
|
|
36650
37420
|
}
|
|
36651
37421
|
};
|
|
@@ -36684,11 +37454,22 @@ async function main(state, container) {
|
|
|
36684
37454
|
const model = new Model(settings, state);
|
|
36685
37455
|
const actions = new Actions(model);
|
|
36686
37456
|
const view = new View(actions);
|
|
37457
|
+
// Get date from URL parameter, fallback to today
|
|
37458
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
37459
|
+
const dateParam = urlParams.get('date');
|
|
36687
37460
|
const today = new Date().toISOString().split("T")[0];
|
|
37461
|
+
const riddleDate = dateParam || today;
|
|
37462
|
+
// Validate date format (YYYY-MM-DD)
|
|
37463
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
37464
|
+
const validDate = dateRegex.test(riddleDate) ? riddleDate : today;
|
|
36688
37465
|
const locale = state.locale || "is_IS";
|
|
37466
|
+
// Log the date being used (helpful for debugging)
|
|
37467
|
+
if (dateParam) {
|
|
37468
|
+
console.log(`Loading Gáta Dagsins for date: ${validDate} (from URL parameter)`);
|
|
37469
|
+
}
|
|
36689
37470
|
// Mount the Gáta Dagsins UI using an anonymous closure component
|
|
36690
37471
|
m.mount(container, {
|
|
36691
|
-
view: () => m(GataDagsins$1, { view, date:
|
|
37472
|
+
view: () => m(GataDagsins$1, { view, date: validDate, locale }),
|
|
36692
37473
|
});
|
|
36693
37474
|
}
|
|
36694
37475
|
catch (e) {
|
|
@@ -36698,6 +37479,8 @@ async function main(state, container) {
|
|
|
36698
37479
|
return "success";
|
|
36699
37480
|
}
|
|
36700
37481
|
|
|
37482
|
+
// Note: To load a specific date for debugging, use URL parameter: ?date=YYYY-MM-DD
|
|
37483
|
+
// Example: http://localhost:6006/?date=2025-01-25
|
|
36701
37484
|
const mountForUser = async (state) => {
|
|
36702
37485
|
// Return a DOM tree containing a mounted Gáta Dagsins UI
|
|
36703
37486
|
// for the user specified in the state object
|