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