@mideind/netskrafl-react 1.0.1 → 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/esm/index.js CHANGED
@@ -1,6 +1,101 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import React, { useEffect } from 'react';
3
3
 
4
+ // Key for storing auth settings in sessionStorage
5
+ const AUTH_SETTINGS_KEY = "netskrafl_auth_settings";
6
+ // Save authentication settings to sessionStorage
7
+ const saveAuthSettings = (settings) => {
8
+ if (!settings) {
9
+ clearAuthSettings();
10
+ return;
11
+ }
12
+ try {
13
+ // Filter to only include properties defined in PersistedAuthSettings interface
14
+ const filteredSettings = {
15
+ userEmail: settings.userEmail, // Required field
16
+ };
17
+ // Only add optional fields if they are defined
18
+ if (settings.account !== undefined)
19
+ filteredSettings.account = settings.account;
20
+ if (settings.userId !== undefined)
21
+ filteredSettings.userId = settings.userId;
22
+ if (settings.userNick !== undefined)
23
+ filteredSettings.userNick = settings.userNick;
24
+ if (settings.firebaseAPIKey !== undefined)
25
+ filteredSettings.firebaseAPIKey = settings.firebaseAPIKey;
26
+ if (settings.beginner !== undefined)
27
+ filteredSettings.beginner = settings.beginner;
28
+ if (settings.fairPlay !== undefined)
29
+ filteredSettings.fairPlay = settings.fairPlay;
30
+ if (settings.ready !== undefined)
31
+ filteredSettings.ready = settings.ready;
32
+ if (settings.readyTimed !== undefined)
33
+ filteredSettings.readyTimed = settings.readyTimed;
34
+ // Only save if we have actual settings to persist
35
+ if (Object.keys(filteredSettings).length > 1) {
36
+ sessionStorage.setItem(AUTH_SETTINGS_KEY, JSON.stringify(filteredSettings));
37
+ }
38
+ else {
39
+ clearAuthSettings();
40
+ }
41
+ }
42
+ catch (error) {
43
+ // SessionStorage might be unavailable or full
44
+ console.warn("Could not save auth settings to sessionStorage:", error);
45
+ }
46
+ };
47
+ // Retrieve authentication settings from sessionStorage
48
+ const loadAuthSettings = () => {
49
+ try {
50
+ const stored = sessionStorage.getItem(AUTH_SETTINGS_KEY);
51
+ if (stored) {
52
+ return JSON.parse(stored);
53
+ }
54
+ }
55
+ catch (error) {
56
+ // SessionStorage might be unavailable or data might be corrupted
57
+ console.warn("Could not load auth settings from sessionStorage:", error);
58
+ }
59
+ return null;
60
+ };
61
+ // Clear authentication settings from sessionStorage
62
+ const clearAuthSettings = () => {
63
+ try {
64
+ sessionStorage.removeItem(AUTH_SETTINGS_KEY);
65
+ }
66
+ catch (error) {
67
+ console.warn("Could not clear auth settings from sessionStorage:", error);
68
+ }
69
+ };
70
+ // Apply persisted settings to a GlobalState object
71
+ const applyPersistedSettings = (state) => {
72
+ var _a, _b, _c, _d;
73
+ const persisted = loadAuthSettings();
74
+ if (!persisted) {
75
+ return state;
76
+ }
77
+ // CRITICAL SECURITY CHECK: Only apply persisted settings if they belong to the current user
78
+ // This prevents data leakage between different users in the same browser session
79
+ if (persisted.userEmail !== state.userEmail) {
80
+ // Different user detected - clear the old user's settings
81
+ clearAuthSettings();
82
+ return state;
83
+ }
84
+ // Apply persisted settings, but don't override values explicitly passed in props
85
+ return {
86
+ ...state,
87
+ // Only apply persisted values if current values are defaults
88
+ account: state.account || persisted.account || state.userId, // Use userId as fallback
89
+ userId: state.userId || persisted.userId || state.userId,
90
+ userNick: state.userNick || persisted.userNick || state.userNick,
91
+ firebaseAPIKey: state.firebaseAPIKey || persisted.firebaseAPIKey || state.firebaseAPIKey,
92
+ beginner: (_a = persisted.beginner) !== null && _a !== void 0 ? _a : state.beginner,
93
+ fairPlay: (_b = persisted.fairPlay) !== null && _b !== void 0 ? _b : state.fairPlay,
94
+ ready: (_c = persisted.ready) !== null && _c !== void 0 ? _c : state.ready,
95
+ readyTimed: (_d = persisted.readyTimed) !== null && _d !== void 0 ? _d : state.readyTimed,
96
+ };
97
+ };
98
+
4
99
  const DEFAULT_STATE = {
5
100
  projectId: "netskrafl",
6
101
  firebaseAPIKey: "",
@@ -8,6 +103,7 @@ const DEFAULT_STATE = {
8
103
  firebaseSenderId: "",
9
104
  firebaseAppId: "",
10
105
  measurementId: "",
106
+ account: "",
11
107
  userEmail: "",
12
108
  userId: "",
13
109
  userNick: "",
@@ -21,12 +117,12 @@ const DEFAULT_STATE = {
21
117
  loginUrl: "",
22
118
  loginMethod: "",
23
119
  newUser: false,
24
- beginner: false,
25
- fairPlay: true,
120
+ beginner: true,
121
+ fairPlay: false,
26
122
  plan: "", // Not a friend
27
123
  hasPaid: false,
28
- ready: false,
29
- readyTimed: false,
124
+ ready: true,
125
+ readyTimed: true,
30
126
  uiFullscreen: true,
31
127
  uiLandscape: false,
32
128
  runningLocal: false,
@@ -50,7 +146,9 @@ const makeGlobalState = (overrides) => {
50
146
  ...DEFAULT_STATE,
51
147
  ...overrides,
52
148
  };
53
- return { ...state, ...makeServerUrls(state.serverUrl, state.movesUrl) };
149
+ const stateWithUrls = { ...state, ...makeServerUrls(state.serverUrl, state.movesUrl) };
150
+ // Apply any persisted authentication settings from sessionStorage
151
+ return applyPersistedSettings(stateWithUrls);
54
152
  };
55
153
 
56
154
  function getDefaultExportFromCjs (x) {
@@ -23435,6 +23533,9 @@ let View$1 = class View {
23435
23533
  function viewGetServerCache(view) {
23436
23534
  return view.viewCache_.serverCache.getNode();
23437
23535
  }
23536
+ function viewGetCompleteNode(view) {
23537
+ return viewCacheGetCompleteEventSnap(view.viewCache_);
23538
+ }
23438
23539
  function viewGetCompleteServerCache(view, path) {
23439
23540
  const cache = viewCacheGetCompleteServerSnap(view.viewCache_);
23440
23541
  if (cache) {
@@ -24096,6 +24197,33 @@ function syncTreeCalcCompleteEventCache(syncTree, path, writeIdsToExclude) {
24096
24197
  });
24097
24198
  return writeTreeCalcCompleteEventCache(writeTree, path, serverCache, writeIdsToExclude, includeHiddenSets);
24098
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
+ }
24099
24227
  /**
24100
24228
  * A helper method that visits all descendant and ancestor SyncPoints, applying the operation.
24101
24229
  *
@@ -25225,6 +25353,63 @@ function repoUpdateInfo(repo, pathString, value) {
25225
25353
  function repoGetNextWriteId(repo) {
25226
25354
  return repo.nextWriteId_++;
25227
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
+ }
25228
25413
  function repoSetWithPriority(repo, path, newVal, newPriority, onComplete) {
25229
25414
  repoLog(repo, 'set', {
25230
25415
  path: path.toString(),
@@ -26641,6 +26826,22 @@ function set(ref, value) {
26641
26826
  /*priority=*/ null, deferred.wrapCallback(() => { }));
26642
26827
  return deferred.promise;
26643
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
+ }
26644
26845
  /**
26645
26846
  * Represents registration for 'value' events.
26646
26847
  */
@@ -27144,6 +27345,14 @@ function logEvent(ev, params) {
27144
27345
  return;
27145
27346
  logEvent$2(analytics, ev, params);
27146
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
+ }
27147
27356
 
27148
27357
  /*
27149
27358
 
@@ -27403,29 +27612,67 @@ class AuthenticationError extends Error {
27403
27612
  }
27404
27613
  // Internal function to ensure authentication
27405
27614
  const ensureAuthenticated = async (state) => {
27615
+ var _a, _b, _c, _d, _e, _f, _g, _h;
27406
27616
  // If login is already in progress, wait for it to complete
27407
27617
  if (authPromise) {
27408
27618
  await authPromise;
27409
27619
  return;
27410
27620
  }
27411
- // Start new login attempt (either forced by 401 or needed for Firebase)
27412
- authPromise = loginUserByEmail(state);
27413
- try {
27414
- const result = await authPromise;
27415
- if (result.status === "expired") {
27416
- // Token has expired, notify the React component if callback is set
27417
- state.tokenExpired && state.tokenExpired();
27418
- throw new Error("Token expired");
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);
27419
27671
  }
27420
- else if (result.status !== "success") {
27421
- throw new Error(`Authentication failed: ${result.message || result.status}`);
27672
+ finally {
27673
+ // Reset the promise so future 401s can trigger a new login
27674
+ authPromise = null;
27422
27675
  }
27423
- // Success: Log in to Firebase with the token passed from the server
27424
- await loginFirebase(state, result.firebase_token);
27425
- }
27426
- finally {
27427
- // Reset the promise so future 401s can trigger a new login
27428
- authPromise = null;
27429
27676
  }
27430
27677
  };
27431
27678
  // Internal authenticated request function
@@ -34231,6 +34478,161 @@ class Game extends BaseGame {
34231
34478
  ;
34232
34479
  } // class Game
34233
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
+
34234
34636
  /*
34235
34637
 
34236
34638
  Riddle.ts
@@ -34245,8 +34647,8 @@ class Game extends BaseGame {
34245
34647
  For further information, see https://github.com/mideind/Netskrafl
34246
34648
 
34247
34649
  */
34248
- const HOT_WARM_BOUNDARY_RATIO = 0.6;
34249
- const WARM_COLD_BOUNDARY_RATIO = 0.3;
34650
+ const HOT_WARM_BOUNDARY_RATIO = 0.5;
34651
+ const WARM_COLD_BOUNDARY_RATIO = 0.25;
34250
34652
  class Riddle extends BaseGame {
34251
34653
  constructor(uuid, date, model) {
34252
34654
  if (!model.state) {
@@ -34318,6 +34720,22 @@ class Riddle extends BaseGame {
34318
34720
  this.hotBoundary = this.bestPossibleScore * HOT_WARM_BOUNDARY_RATIO;
34319
34721
  // Initialize word checker
34320
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
+ }
34321
34739
  }
34322
34740
  }
34323
34741
  catch (error) {
@@ -34338,6 +34756,7 @@ class Riddle extends BaseGame {
34338
34756
  locale: this.locale,
34339
34757
  userId: state.userId,
34340
34758
  groupId: state.userGroupId || null,
34759
+ userDisplayName: state.userFullname || state.userNick || state.userId,
34341
34760
  move,
34342
34761
  }
34343
34762
  });
@@ -34386,13 +34805,26 @@ class Riddle extends BaseGame {
34386
34805
  // If the move is not valid or was already played, return
34387
34806
  if (!move)
34388
34807
  return;
34389
- if (move.score > this.personalBestScore) {
34390
- // This is the best score we've seen yet
34391
- this.personalBestScore = move.score;
34392
- // This might affect the global state,
34393
- // so notify the server
34394
- this.submitRiddleWord(move);
34395
- }
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);
34396
34828
  }
34397
34829
  updateGlobalBestScore(best) {
34398
34830
  // Update the global best score, typically as a result
@@ -34619,6 +35051,9 @@ class Model {
34619
35051
  this.game = null;
34620
35052
  // The current Gáta Dagsins riddle, if any
34621
35053
  this.riddle = null;
35054
+ // Gáta Dagsins-specific properties
35055
+ this.userStats = null;
35056
+ this.leaderboard = [];
34622
35057
  // The current Netskrafl game list
34623
35058
  this.gameList = null;
34624
35059
  // Number of games where it's the player's turn, plus count of zombie games
@@ -35124,6 +35559,7 @@ class Model {
35124
35559
  state.userNick = user.nickname;
35125
35560
  state.beginner = user.beginner;
35126
35561
  state.fairPlay = user.fairplay;
35562
+ saveAuthSettings(state);
35127
35563
  }
35128
35564
  // Note that state.plan is updated via a Firebase notification
35129
35565
  // Give the game instance a chance to update its state
@@ -35158,41 +35594,22 @@ class Model {
35158
35594
  return false;
35159
35595
  }
35160
35596
  handleUserMessage(json, firstAttach) {
35161
- var _a;
35162
35597
  // Handle an incoming Firebase user message, i.e. a message
35163
35598
  // on the /user/[userid] path
35164
- if (firstAttach || !this.state)
35599
+ if (firstAttach || !this.state || !json)
35165
35600
  return;
35166
35601
  let redraw = false;
35167
- if (json.friend !== undefined) {
35168
- // Potential change of user friendship status
35169
- const newFriend = json.friend ? true : false;
35170
- if (this.user && this.user.friend != newFriend) {
35171
- this.user.friend = newFriend;
35172
- redraw = true;
35173
- }
35174
- }
35175
- if (json.plan !== undefined) {
35602
+ if (typeof json.plan === "string") {
35176
35603
  // Potential change of user subscription plan
35177
- if (this.state.plan != json.plan) {
35604
+ if (this.state.plan !== json.plan) {
35178
35605
  this.state.plan = json.plan;
35179
35606
  redraw = true;
35180
35607
  }
35181
- if (this.user && !this.user.friend && this.state.plan == "friend") {
35182
- // plan == "friend" implies that user.friend should be true
35183
- this.user.friend = true;
35184
- redraw = true;
35185
- }
35186
- if (this.state.plan == "" && ((_a = this.user) === null || _a === void 0 ? void 0 : _a.friend)) {
35187
- // Conversely, an empty plan string means that the user is not a friend
35188
- this.user.friend = false;
35189
- redraw = true;
35190
- }
35191
35608
  }
35192
35609
  if (json.hasPaid !== undefined) {
35193
35610
  // Potential change of payment status
35194
- const newHasPaid = (this.state.plan != "" && json.hasPaid) ? true : false;
35195
- if (this.state.hasPaid != newHasPaid) {
35611
+ const newHasPaid = (this.state.plan !== "" && json.hasPaid) ? true : false;
35612
+ if (this.state.hasPaid !== newHasPaid) {
35196
35613
  this.state.hasPaid = newHasPaid;
35197
35614
  redraw = true;
35198
35615
  }
@@ -35696,6 +36113,8 @@ class Actions {
35696
36113
  url: "/setuserpref",
35697
36114
  body: pref
35698
36115
  }); // No result required or expected
36116
+ // Update the persisted settings in sessionStorage
36117
+ saveAuthSettings(this.model.state);
35699
36118
  }
35700
36119
  catch (e) {
35701
36120
  // A future TODO might be to signal an error in the UI
@@ -35742,6 +36161,12 @@ class Actions {
35742
36161
  if (state === null || state === void 0 ? void 0 : state.userGroupId) {
35743
36162
  attachFirebaseListener(basePath + `group/${state.userGroupId}/best`, (json, firstAttach) => this.onRiddleGroupScoreUpdate(json, firstAttach));
35744
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
+ }
35745
36170
  }
35746
36171
  detachListenerFromRiddle(date, locale) {
35747
36172
  const { state } = this.model;
@@ -35750,6 +36175,10 @@ class Actions {
35750
36175
  if (state === null || state === void 0 ? void 0 : state.userGroupId) {
35751
36176
  detachFirebaseListener(basePath + `group/${state.userGroupId}/best`);
35752
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
+ }
35753
36182
  }
35754
36183
  onRiddleGlobalScoreUpdate(json, firstAttach) {
35755
36184
  const { riddle } = this.model;
@@ -35767,6 +36196,38 @@ class Actions {
35767
36196
  riddle.updateGroupBestScore(json);
35768
36197
  m.redraw();
35769
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
+ }
35770
36231
  async fetchRiddle(date, locale) {
35771
36232
  // Create the game via model
35772
36233
  if (!this.model)
@@ -35957,13 +36418,14 @@ const Netskrafl = React.memo(NetskraflImpl);
35957
36418
  */
35958
36419
  const RiddleScore = {
35959
36420
  view: (vnode) => {
35960
- const { riddle } = vnode.attrs;
36421
+ const { riddle, mode = "desktop" } = vnode.attrs;
35961
36422
  if (!riddle)
35962
36423
  return m("div");
35963
36424
  const score = riddle.currentScore;
35964
36425
  const hasValidMove = score !== undefined;
35965
36426
  const hasTiles = riddle.tilesPlaced().length > 0;
35966
- let classes = [".gata-dagsins-score"];
36427
+ const baseClass = (mode === "mobile" ? ".mobile-score" : ".gata-dagsins-score");
36428
+ let classes = [baseClass];
35967
36429
  let displayText = "0";
35968
36430
  if (!hasTiles) {
35969
36431
  // State 1: No tiles on board - grayed/disabled, showing zero
@@ -35992,8 +36454,13 @@ const RiddleScore = {
35992
36454
  else {
35993
36455
  classes.push(".hot");
35994
36456
  }
36457
+ // Add celebration class if the player achieved the best possible score
36458
+ if (score >= riddle.bestPossibleScore) {
36459
+ classes.push(".celebrate");
36460
+ }
35995
36461
  }
35996
- return m("div" + classes.join(""), m("span.gata-dagsins-legend", displayText));
36462
+ const legendClass = mode === "mobile" ? ".mobile-score-legend" : ".gata-dagsins-legend";
36463
+ return m("div" + classes.join(""), m("span" + legendClass, displayText));
35997
36464
  }
35998
36465
  };
35999
36466
 
@@ -36053,35 +36520,40 @@ const GataDagsinsBoardAndRack = {
36053
36520
  */
36054
36521
  const SunCorona = {
36055
36522
  view: (vnode) => {
36056
- 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;
36057
36529
  return m("div.sun-corona" + (animate ? ".rotating" : ""), [
36058
36530
  m.trust(`
36059
- <svg width="80" height="80" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
36531
+ <svg width="${size}" height="${size}" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
36060
36532
  <g transform="translate(50,50)">
36061
36533
  <!-- Ray at 0° (12 o'clock) -->
36062
- <polygon points="0,-40 -3,-20 3,-20" fill="#f1c40f" opacity="0.8" transform="rotate(0)"/>
36534
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(0)"/>
36063
36535
  <!-- Ray at 30° -->
36064
- <polygon points="0,-40 -3,-20 3,-20" fill="#f39c12" opacity="0.7" transform="rotate(30)"/>
36536
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(30)"/>
36065
36537
  <!-- Ray at 60° -->
36066
- <polygon points="0,-40 -3,-20 3,-20" fill="#f1c40f" opacity="0.8" transform="rotate(60)"/>
36538
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(60)"/>
36067
36539
  <!-- Ray at 90° (3 o'clock) -->
36068
- <polygon points="0,-40 -3,-20 3,-20" fill="#f39c12" opacity="0.7" transform="rotate(90)"/>
36540
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(90)"/>
36069
36541
  <!-- Ray at 120° -->
36070
- <polygon points="0,-40 -3,-20 3,-20" fill="#f1c40f" opacity="0.8" transform="rotate(120)"/>
36542
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(120)"/>
36071
36543
  <!-- Ray at 150° -->
36072
- <polygon points="0,-40 -3,-20 3,-20" fill="#f39c12" opacity="0.7" transform="rotate(150)"/>
36544
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(150)"/>
36073
36545
  <!-- Ray at 180° (6 o'clock) -->
36074
- <polygon points="0,-40 -3,-20 3,-20" fill="#f1c40f" opacity="0.8" transform="rotate(180)"/>
36546
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(180)"/>
36075
36547
  <!-- Ray at 210° -->
36076
- <polygon points="0,-40 -3,-20 3,-20" fill="#f39c12" opacity="0.7" transform="rotate(210)"/>
36548
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(210)"/>
36077
36549
  <!-- Ray at 240° -->
36078
- <polygon points="0,-40 -3,-20 3,-20" fill="#f1c40f" opacity="0.8" transform="rotate(240)"/>
36550
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(240)"/>
36079
36551
  <!-- Ray at 270° (9 o'clock) -->
36080
- <polygon points="0,-40 -3,-20 3,-20" fill="#f39c12" opacity="0.7" transform="rotate(270)"/>
36552
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(270)"/>
36081
36553
  <!-- Ray at 300° -->
36082
- <polygon points="0,-40 -3,-20 3,-20" fill="#f1c40f" opacity="0.8" transform="rotate(300)"/>
36554
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f1c40f" opacity="0.8" transform="rotate(300)"/>
36083
36555
  <!-- Ray at 330° -->
36084
- <polygon points="0,-40 -3,-20 3,-20" fill="#f39c12" opacity="0.7" transform="rotate(330)"/>
36556
+ <polygon points="0,-${outerRadius} -3,-${innerRadius} 3,-${innerRadius}" fill="#f39c12" opacity="0.7" transform="rotate(330)"/>
36085
36557
  </g>
36086
36558
  </svg>
36087
36559
  `)
@@ -36089,6 +36561,101 @@ const SunCorona = {
36089
36561
  }
36090
36562
  };
36091
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
+
36092
36659
  /*
36093
36660
 
36094
36661
  Thermometer.ts
@@ -36163,12 +36730,27 @@ const BestPossibleScore = () => {
36163
36730
  return {
36164
36731
  view: (vnode) => {
36165
36732
  const { score, bestMove, onMoveClick } = vnode.attrs;
36166
- const topLabel = bestMove
36167
- ? removeBlankMarkers(bestMove.word)
36168
- : ts("Besta mögulega lögn");
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;
36169
36750
  const celebrate = bestMove && bestMove.word !== "";
36170
36751
  return m(".thermometer-best-score"
36171
- + (celebrate ? ".celebrate" : ""), m(".thermometer-best-score-container", {
36752
+ + (celebrate ? ".celebrate" : "")
36753
+ + (achieved ? ".achieved" : ""), m(".thermometer-best-score-container", {
36172
36754
  onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
36173
36755
  }, [
36174
36756
  // Sun corona behind the circle when celebrating
@@ -36296,6 +36878,196 @@ const Thermometer = () => {
36296
36878
  };
36297
36879
  };
36298
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
+
36299
37071
  /*
36300
37072
 
36301
37073
  GataDagsins-Right-Side.ts
@@ -36311,7 +37083,7 @@ const Thermometer = () => {
36311
37083
 
36312
37084
  */
36313
37085
  const GataDagsinsRightSide = {
36314
- // Component containing a single vertical thermometer-style performance display
37086
+ // Component containing both mobile status bar and desktop tabbed view
36315
37087
  view: (vnode) => {
36316
37088
  const { view, selectedMoves, bestMove } = vnode.attrs;
36317
37089
  const { riddle } = view.model;
@@ -36322,17 +37094,198 @@ const GataDagsinsRightSide = {
36322
37094
  }
36323
37095
  };
36324
37096
  return m(".gatadagsins-right-side-wrapper", riddle ? [
36325
- // Single thermometer column containing all performance levels
36326
- m(".gatadagsins-thermometer-column", m(Thermometer, {
37097
+ // Mobile-only status bar (visible on mobile, hidden on desktop)
37098
+ m(".gatadagsins-mobile-status", m(MobileStatus, {
36327
37099
  riddle,
36328
37100
  selectedMoves,
36329
37101
  bestMove,
36330
37102
  onMoveClick: handleMoveClick
36331
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
+ })),
36332
37111
  ] : null);
36333
37112
  }
36334
37113
  };
36335
37114
 
37115
+ /*
37116
+
37117
+ gatadagsins-help.ts
37118
+
37119
+ Help dialog for Gáta Dagsins
37120
+
37121
+ Copyright (C) 2025 Miðeind ehf.
37122
+ Author: Vilhjálmur Þorsteinsson
37123
+
37124
+ The Creative Commons Attribution-NonCommercial 4.0
37125
+ International Public License (CC-BY-NC 4.0) applies to this software.
37126
+ For further information, see https://github.com/mideind/Netskrafl
37127
+
37128
+ */
37129
+ const GataDagsinsHelp = {
37130
+ view: (vnode) => {
37131
+ const closeHelp = vnode.attrs.onClose;
37132
+ return m(".modal-dialog.gatadagsins-help", m(".modal-content", [
37133
+ // Header with close button
37134
+ m(".modal-header", [
37135
+ m("h2", "Um Gátu dagsins"),
37136
+ m("button.close", {
37137
+ onclick: closeHelp,
37138
+ "aria-label": "Loka"
37139
+ }, m("span", { "aria-hidden": "true" }, "×"))
37140
+ ]),
37141
+ // Body with help content
37142
+ m(".modal-body", [
37143
+ m("p", "Gáta dagsins er dagleg krossgátuþraut, svipuð skrafli, þar sem þú reynir að finna " +
37144
+ "stigahæsta orðið sem hægt er að mynda með gefnum stöfum."),
37145
+ m("h3", "Hvernig á að spila"),
37146
+ m("ul", [
37147
+ m("li", "Þú færð borð með allmörgum stöfum sem þegar hafa verið lagðir."),
37148
+ m("li", "Neðst á skjánum eru stafaflísar sem þú getur notað til að mynda orð."),
37149
+ m("li", "Dragðu flísar á borðið til að mynda orð, annaðhvort lárétt eða lóðrétt."),
37150
+ m("li", "Orðin verða að tengjast við stafi sem fyrir eru á borðinu."),
37151
+ m("li", "Þú sérð jafnóðum hvort lögnin á borðinu er gild og hversu mörg stig hún gefur."),
37152
+ m("li", "Þú getur prófað eins mörg orð og þú vilt - besta skorið þitt er vistað."),
37153
+ ]),
37154
+ m("h3", "Stigagjöf"),
37155
+ m("p", "Þú færð stig fyrir hvern staf í orðinu, auk bónusstiga fyrir lengri orð:"),
37156
+ m("ul", [
37157
+ m("li", "Hver stafur gefur 1-10 stig eftir gildi hans"),
37158
+ m("li", "Orð sem nota allar 7 stafaflísarnar gefa 50 stiga bónus"),
37159
+ m("li", "Sumir reitir á borðinu tvöfalda eða þrefalda stafagildið"),
37160
+ m("li", "Sumir reitir tvöfalda eða þrefalda heildarorðagildið"),
37161
+ ]),
37162
+ m("h3", "Hitamælir"),
37163
+ m("p", "Hitamælirinn hægra megin (eða efst á farsímum) sýnir:"),
37164
+ m("ul", [
37165
+ m("li", m("strong", "Besta mögulega skor:"), " Hæstu stig sem hægt er að ná á þessu borði."),
37166
+ m("li", m("strong", "Besta skor dagsins:"), " Hæstu stig sem einhver leikmaður hefur náð í dag."),
37167
+ m("li", m("strong", "Þín bestu orð:"), " Orðin sem þú hefur lagt og stigin fyrir þau."),
37168
+ m("li", "Þú getur smellt á orð á hitamælinum til að fá þá lögn aftur á borðið."),
37169
+ ]),
37170
+ m("h3", "Ábendingar"),
37171
+ m("ul", [
37172
+ m("li", "Reyndu að nota dýra stafi (eins og X, Ý, Þ) á tvöföldunar- eða þreföldunarreitum."),
37173
+ m("li", "Lengri orð gefa mun fleiri stig vegna bónussins."),
37174
+ m("li", "Þú getur dregið allar flísar til baka með bláa endurkalls-hnappnum."),
37175
+ m("li", "Ný gáta birtist á hverjum nýjum degi - klukkan 00:00!"),
37176
+ ]),
37177
+ m("h3", "Um leikinn"),
37178
+ m("p", [
37179
+ "Gáta dagsins er systkini ",
37180
+ m("a", { href: "https://netskrafl.is", target: "_blank" }, "Netskrafls"),
37181
+ ", hins sívinsæla íslenska krossgátuleiks á netinu. ",
37182
+ "Leikurinn er þróaður af Miðeind ehf."
37183
+ ]),
37184
+ ]),
37185
+ // Footer with close button
37186
+ m(".modal-footer", m("button.btn.btn-primary", {
37187
+ onclick: closeHelp
37188
+ }, "Loka"))
37189
+ ]));
37190
+ }
37191
+ };
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
+
36336
37289
  /*
36337
37290
 
36338
37291
  GataDagsins.ts
@@ -36348,11 +37301,19 @@ const GataDagsinsRightSide = {
36348
37301
 
36349
37302
  */
36350
37303
  const MAX_MOVES_TO_DISPLAY = 10;
36351
- const selectTopMoves = (thisPlayer, moves, globalBestScore, groupBestScore) => {
36352
- 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;
36353
37314
  // Sort moves by score in descending order and
36354
37315
  // cut the tail off the list to only include the top moves
36355
- const movesToDisplay = moves
37316
+ const selectedMoves = playerMoves
36356
37317
  .sort((a, b) => b.score - a.score)
36357
37318
  .slice(0, MAX_MOVES_TO_DISPLAY)
36358
37319
  .map(move => ({
@@ -36360,88 +37321,102 @@ const selectTopMoves = (thisPlayer, moves, globalBestScore, groupBestScore) => {
36360
37321
  word: move.word,
36361
37322
  coord: move.coord,
36362
37323
  }));
36363
- // If there is a global best score, and it is different from
36364
- // 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
36365
37325
  if (globalBestScore && globalBestScore.score > 0) {
36366
37326
  const { score, word, coord } = globalBestScore;
36367
- if (((_a = movesToDisplay[0]) === null || _a === void 0 ? void 0 : _a.score) >= score) {
37327
+ if (((_c = (_b = selectedMoves[0]) === null || _b === void 0 ? void 0 : _b.score) !== null && _c !== void 0 ? _c : 0) >= score) {
36368
37328
  // This player has made a move that scores the same
36369
37329
  // or better as the top score: mark the move
36370
- movesToDisplay[0].isGlobalBestScore = true;
37330
+ selectedMoves[0].isGlobalBestScore = true;
36371
37331
  }
36372
37332
  else if (globalBestScore.player === thisPlayer) {
36373
37333
  // This player holds the global best score, probably
36374
- // from a previous session: add it as a move
36375
- movesToDisplay.unshift({ score, isGlobalBestScore: true, word, coord });
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 });
36376
37337
  }
36377
37338
  else {
36378
- // This is a global best score from another player: add it as a special move
36379
- movesToDisplay.unshift({ score, isGlobalBestScore: true, word: "", coord: "" });
37339
+ // This is a global best score from another player
37340
+ selectedMoves.unshift({ score, isGlobalBestScore: true, word: "", coord: "" });
36380
37341
  }
36381
37342
  }
36382
- // TODO: Add handling for group best score
36383
- return movesToDisplay;
36384
- };
36385
- const currentMoveState = (riddle) => {
36386
- var _a;
36387
- const thisPlayer = ((_a = riddle.model.state) === null || _a === void 0 ? void 0 : _a.userId) || "";
36388
- const { bestPossibleScore, globalBestScore, groupBestScore, playerMoves, } = riddle;
36389
- // If the player has equaled the best possible score,
36390
- // the winning word is stored here and displayed at the top
36391
- let bestMove = undefined;
36392
- // Apply the move selection and allocation algorithm
36393
- const selectedMoves = selectTopMoves(thisPlayer, playerMoves, globalBestScore);
36394
- // If the top-scoring move has the bestPossibleScore,
36395
- // 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.
36396
37346
  if (selectedMoves.length > 0 && selectedMoves[0].score === bestPossibleScore) {
36397
- if (selectedMoves[0].word) {
36398
- // The word was played by this player
36399
- bestMove = selectedMoves.shift();
36400
- }
37347
+ bestMove = selectedMoves.shift();
36401
37348
  }
36402
37349
  return { selectedMoves, bestMove };
36403
37350
  };
36404
- const GataDagsins$1 = {
37351
+ const GataDagsins$1 = () => {
36405
37352
  // A view of the Gáta Dagsins page
36406
- oninit: (vnode) => {
36407
- const { model, actions } = vnode.attrs.view;
36408
- const { riddle } = model;
36409
- if (!riddle) {
36410
- const { date, locale } = vnode.attrs;
36411
- // Initialize a fresh riddle object if it doesn't exist
36412
- actions.fetchRiddle(date, locale);
37353
+ let showHelp = false;
37354
+ let showStatsModal = false;
37355
+ return {
37356
+ oninit: async (vnode) => {
37357
+ const { model, actions } = vnode.attrs.view;
37358
+ const { riddle } = model;
37359
+ if (!riddle) {
37360
+ const { date, locale } = vnode.attrs;
37361
+ // Initialize a fresh riddle object if it doesn't exist
37362
+ await actions.fetchRiddle(date, locale);
37363
+ }
37364
+ // Initialize dialog states
37365
+ showHelp = false;
37366
+ showStatsModal = false;
37367
+ },
37368
+ view: (vnode) => {
37369
+ var _a;
37370
+ const { view } = vnode.attrs;
37371
+ const { model } = view;
37372
+ const { riddle } = model;
37373
+ const { selectedMoves, bestMove } = (riddle
37374
+ ? currentMoveState(riddle)
37375
+ : { selectedMoves: [], bestMove: undefined });
37376
+ const toggleHelp = () => {
37377
+ showHelp = !showHelp;
37378
+ m.redraw();
37379
+ };
37380
+ const toggleStatsModal = () => {
37381
+ showStatsModal = !showStatsModal;
37382
+ m.redraw();
37383
+ };
37384
+ return m("div.drop-target", {
37385
+ id: "gatadagsins-background",
37386
+ }, [
37387
+ // The main content area
37388
+ riddle ? m(".gatadagsins-container", [
37389
+ // Main display area with flex layout
37390
+ m(".gatadagsins-main", [
37391
+ // Board and rack component (left side)
37392
+ m(GataDagsinsBoardAndRack, { view }),
37393
+ // Right-side component with scores and comparisons
37394
+ m(GataDagsinsRightSide, { view, selectedMoves, bestMove }),
37395
+ // Blank dialog
37396
+ riddle.askingForBlank
37397
+ ? m(BlankDialog, { game: riddle })
37398
+ : "",
37399
+ ])
37400
+ ]) : "",
37401
+ // The left margin elements: back button and info/help button
37402
+ // These elements appear after the main container for proper z-order
37403
+ // m(LeftLogo), // Currently no need for the logo for Gáta Dagsins
37404
+ // Show the Beginner component if the user is a beginner
37405
+ ((_a = model.state) === null || _a === void 0 ? void 0 : _a.beginner) ? m(Beginner, { view }) : "",
37406
+ // Custom Info button for GataDagsins that shows help dialog
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 }),
37410
+ // Help dialog and backdrop
37411
+ showHelp ? [
37412
+ m(".modal-backdrop", { onclick: (e) => { e.preventDefault(); } }),
37413
+ m(GataDagsinsHelp, { onClose: toggleHelp })
37414
+ ] : "",
37415
+ // Stats modal and backdrop (mobile only)
37416
+ showStatsModal ? m(StatsModal, { view, onClose: toggleStatsModal }) : "",
37417
+ ]);
36413
37418
  }
36414
- },
36415
- view: (vnode) => {
36416
- const { view } = vnode.attrs;
36417
- const { model } = view;
36418
- const { riddle } = model;
36419
- const { selectedMoves, bestMove } = (riddle
36420
- ? currentMoveState(riddle)
36421
- : { selectedMoves: [], bestMove: undefined });
36422
- return m("div.drop-target", {
36423
- id: "gatadagsins-background",
36424
- }, [
36425
- // The main content area
36426
- riddle ? m(".gatadagsins-container", [
36427
- // Main display area with flex layout
36428
- m(".gatadagsins-main", [
36429
- // Board and rack component (left side)
36430
- m(GataDagsinsBoardAndRack, { view }),
36431
- // Right-side component with scores and comparisons
36432
- m(GataDagsinsRightSide, { view, selectedMoves, bestMove }),
36433
- // Blank dialog
36434
- riddle.askingForBlank
36435
- ? m(BlankDialog, { game: riddle })
36436
- : "",
36437
- ])
36438
- ]) : "",
36439
- // The left margin elements: back button and info/help button
36440
- // These elements appear after the main container for proper z-order
36441
- m(LeftLogo),
36442
- m(Info),
36443
- ]);
36444
- }
37419
+ };
36445
37420
  };
36446
37421
 
36447
37422
  /*
@@ -36477,11 +37452,22 @@ async function main(state, container) {
36477
37452
  const model = new Model(settings, state);
36478
37453
  const actions = new Actions(model);
36479
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');
36480
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;
36481
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
+ }
36482
37468
  // Mount the Gáta Dagsins UI using an anonymous closure component
36483
37469
  m.mount(container, {
36484
- view: () => m(GataDagsins$1, { view, date: today, locale }),
37470
+ view: () => m(GataDagsins$1, { view, date: validDate, locale }),
36485
37471
  });
36486
37472
  }
36487
37473
  catch (e) {
@@ -36491,6 +37477,8 @@ async function main(state, container) {
36491
37477
  return "success";
36492
37478
  }
36493
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
36494
37482
  const mountForUser = async (state) => {
36495
37483
  // Return a DOM tree containing a mounted Gáta Dagsins UI
36496
37484
  // for the user specified in the state object