stem-lab-toolkit 1.0.1 → 1.0.2

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/js/main.js CHANGED
@@ -81,7 +81,7 @@ function pressEquals() {
81
81
  const entry = currentInput || displayVal;
82
82
 
83
83
  if (operand === null) {
84
- if (entry === '5555') { window.location.href = './browse.html'; return; }
84
+ if (entry === '8008') { triggerMatrixEgg(); return; }
85
85
  justEvaled = true;
86
86
  return;
87
87
  }
@@ -174,99 +174,18 @@ document.querySelector('.sci-rows').addEventListener('click', e => {
174
174
  });
175
175
 
176
176
  document.addEventListener('keydown', e => {
177
- if (pinOverlay.classList.contains('hidden')) {
178
- if (e.key >= '0' && e.key <= '9') pressDigit(e.key);
179
- else if (e.key === '.') pressDot();
180
- else if (e.key === '+') pressOp('+');
181
- else if (e.key === '-') pressOp('');
182
- else if (e.key === '*') pressOp('×');
183
- else if (e.key === '/') { e.preventDefault(); pressOp('÷'); }
184
- else if (e.key === 'Enter' || e.key === '=') pressEquals();
185
- else if (e.key === 'Escape') pressClear();
186
- else if (e.key === 'Backspace') {
187
- if (currentInput.length > 1) { currentInput = currentInput.slice(0, -1); setDisplay(currentInput); }
188
- else { currentInput = ''; setDisplay('0'); }
189
- }
190
- } else {
191
- if (e.key >= '0' && e.key <= '9') addPinDigit(e.key);
192
- else if (e.key === 'Backspace') delPinDigit();
193
- else if (e.key === 'Escape') closePinOverlay();
194
- }
195
- });
196
-
197
- // ── PIN overlay ──────────────────────────────────────────────────────────────
198
- const pinOverlay = document.getElementById('pin-overlay');
199
- const pinError = document.getElementById('pin-error');
200
- let pinEntry = '';
201
-
202
- function openPinOverlay() {
203
- pinEntry = '';
204
- pinError.textContent = '';
205
- updatePinDots();
206
- pinOverlay.classList.remove('hidden');
207
- }
208
-
209
- function closePinOverlay() {
210
- pinOverlay.classList.add('hidden');
211
- pinEntry = '';
212
- }
213
-
214
- function updatePinDots(state) {
215
- for (let i = 0; i < 4; i++) {
216
- const dot = document.getElementById('d' + i);
217
- dot.className = 'pin-dot';
218
- if (state === 'error') { dot.classList.add('error'); }
219
- else if (i < pinEntry.length) { dot.classList.add('filled'); }
177
+ const loginOverlay = document.getElementById('login-overlay');
178
+ if (loginOverlay && !loginOverlay.classList.contains('hidden')) return;
179
+ if (e.key >= '0' && e.key <= '9') pressDigit(e.key);
180
+ else if (e.key === '.') pressDot();
181
+ else if (e.key === '+') pressOp('+');
182
+ else if (e.key === '-') pressOp('');
183
+ else if (e.key === '*') pressOp('×');
184
+ else if (e.key === '/') { e.preventDefault(); pressOp('÷'); }
185
+ else if (e.key === 'Enter' || e.key === '=') pressEquals();
186
+ else if (e.key === 'Escape') pressClear();
187
+ else if (e.key === 'Backspace') {
188
+ if (currentInput.length > 1) { currentInput = currentInput.slice(0, -1); setDisplay(currentInput); }
189
+ else { currentInput = ''; setDisplay('0'); }
220
190
  }
221
- }
222
-
223
- function addPinDigit(d) {
224
- if (pinEntry.length >= 4) return;
225
- pinEntry += d;
226
- updatePinDots();
227
- if (pinEntry.length === 4) submitPin();
228
- }
229
-
230
- function delPinDigit() {
231
- pinEntry = pinEntry.slice(0, -1);
232
- updatePinDots();
233
- pinError.textContent = '';
234
- }
235
-
236
- async function submitPin() {
237
- const pin = pinEntry;
238
- let isUser = false;
239
- try {
240
- const res = await fetch('./api/pin', {
241
- method: 'POST',
242
- headers: { 'Content-Type': 'application/json' },
243
- body: JSON.stringify({ pin }),
244
- });
245
- const data = await res.json();
246
- isUser = data.role === 'user';
247
- } catch {
248
- isUser = pin === '5555';
249
- }
250
- if (isUser) { window.location.href = './browse.html'; }
251
- else { showPinError(); }
252
- }
253
-
254
- function showPinError() {
255
- updatePinDots('error');
256
- pinError.textContent = 'Incorrect PIN';
257
- setTimeout(() => {
258
- pinEntry = '';
259
- updatePinDots();
260
- pinError.textContent = '';
261
- }, 1200);
262
- }
263
-
264
-
265
- document.querySelector('.pin-keypad').addEventListener('click', e => {
266
- const btn = e.target.closest('button');
267
- if (!btn) return;
268
- const k = btn.dataset.k;
269
- if (k === 'del') delPinDigit();
270
- else if (k === 'cancel') closePinOverlay();
271
- else addPinDigit(k);
272
191
  });
package/js/pin-modal.js CHANGED
@@ -1,98 +1,13 @@
1
- // Shared PIN modal for light-themed pages (browse, game)
2
- // Intercepts clicks on any <a href="./admin.html"> and prompts for PIN.
3
- (() => {
4
- const el = document.createElement('div');
5
- el.innerHTML = `
6
- <div class="pin-overlay hidden" id="pm-overlay">
7
- <div class="pin-box">
8
- <h2>Admin Access</h2>
9
- <p>Enter your PIN to continue</p>
10
- <div class="pin-dots">
11
- <div class="pin-dot" id="pm-d0"></div>
12
- <div class="pin-dot" id="pm-d1"></div>
13
- <div class="pin-dot" id="pm-d2"></div>
14
- <div class="pin-dot" id="pm-d3"></div>
15
- </div>
16
- <div class="pin-error" id="pm-error"></div>
17
- <div class="pin-keypad" id="pm-keypad">
18
- <button class="pin-key" data-k="1">1</button>
19
- <button class="pin-key" data-k="2">2</button>
20
- <button class="pin-key" data-k="3">3</button>
21
- <button class="pin-key" data-k="4">4</button>
22
- <button class="pin-key" data-k="5">5</button>
23
- <button class="pin-key" data-k="6">6</button>
24
- <button class="pin-key" data-k="7">7</button>
25
- <button class="pin-key" data-k="8">8</button>
26
- <button class="pin-key" data-k="9">9</button>
27
- <button class="pin-key delete" data-k="del">⌫</button>
28
- <button class="pin-key" data-k="0">0</button>
29
- <button class="pin-cancel" data-k="cancel">Cancel</button>
30
- </div>
31
- </div>
32
- </div>`;
33
- document.body.appendChild(el.firstElementChild);
34
-
35
- const overlay = document.getElementById('pm-overlay');
36
- const errEl = document.getElementById('pm-error');
37
- let entry = '';
38
-
39
- function dots(state) {
40
- for (let i = 0; i < 4; i++) {
41
- const d = document.getElementById('pm-d' + i);
42
- d.className = 'pin-dot' + (state === 'error' ? ' error' : i < entry.length ? ' filled' : '');
43
- }
44
- }
45
-
46
- function open() { entry = ''; errEl.textContent = ''; dots(); overlay.classList.remove('hidden'); }
47
- function close() { overlay.classList.add('hidden'); entry = ''; }
48
-
49
- function addDigit(d) {
50
- if (entry.length >= 4) return;
51
- entry += d; dots();
52
- if (entry.length === 4) submit();
53
- }
54
-
55
- function del() { entry = entry.slice(0, -1); dots(); errEl.textContent = ''; }
56
-
57
- async function submit() {
58
- const pin = entry;
59
- let ok = false;
60
- try {
61
- const res = await fetch('./api/pin', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ pin }) });
62
- const data = await res.json();
63
- ok = data.role === 'admin';
64
- } catch { ok = pin === '1234'; }
65
-
66
- if (ok) {
67
- localStorage.setItem('mm_admin_session', '1');
68
- sessionStorage.setItem('mm_admin_pin', pin);
69
- window.location.href = './admin.html';
70
- } else {
71
- dots('error');
72
- errEl.textContent = 'Incorrect PIN';
73
- setTimeout(() => { entry = ''; dots(); errEl.textContent = ''; }, 1200);
74
- }
75
- }
76
-
77
- document.getElementById('pm-keypad').addEventListener('click', e => {
78
- const btn = e.target.closest('[data-k]');
79
- if (!btn) return;
80
- const k = btn.dataset.k;
81
- if (k === 'del') del(); else if (k === 'cancel') close(); else addDigit(k);
82
- });
83
-
84
- document.addEventListener('keydown', e => {
85
- if (overlay.classList.contains('hidden')) return;
86
- if (e.key >= '0' && e.key <= '9') addDigit(e.key);
87
- else if (e.key === 'Backspace') del();
88
- else if (e.key === 'Escape') close();
89
- });
90
-
91
- // Intercept every Admin nav link on this page
92
- document.addEventListener('click', e => {
93
- const a = e.target.closest('a[href="./admin.html"]');
1
+ // Redirect the Admin nav link based on the authenticated user's rank.
2
+ (function () {
3
+ document.addEventListener('click', function (e) {
4
+ var a = e.target.closest('a[href="./admin.html"]');
94
5
  if (!a) return;
95
- e.preventDefault();
96
- open();
6
+ var rank = sessionStorage.getItem('mm_rank');
7
+ if (rank === 'admin') {
8
+ e.preventDefault();
9
+ window.location.href = './control-panel.html';
10
+ }
11
+ // 'administrator' follows the href naturally; 'user' hits admin.html which redirects them
97
12
  });
98
13
  })();
package/js/theme.js ADDED
@@ -0,0 +1,20 @@
1
+ const moonSVG = `<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>`;
2
+ const sunSVG = `<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>`;
3
+
4
+ function toggleTheme() {
5
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
6
+ const next = isDark ? 'light' : 'dark';
7
+ document.documentElement.setAttribute('data-theme', next);
8
+ localStorage.setItem('mm_theme', next);
9
+ updateIcon();
10
+ }
11
+
12
+ function updateIcon() {
13
+ const btn = document.getElementById('theme-toggle');
14
+ if (!btn) return;
15
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
16
+ btn.innerHTML = isDark ? sunSVG : moonSVG;
17
+ btn.title = isDark ? 'Light mode' : 'Dark mode';
18
+ }
19
+
20
+ updateIcon();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stem-lab-toolkit",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "STEM educational toolkit",
5
5
  "main": "index.html",
6
6
  "scripts": {
@@ -13,5 +13,10 @@
13
13
  "math"
14
14
  ],
15
15
  "license": "MIT",
16
- "author": ""
16
+ "author": "",
17
+ "dependencies": {
18
+ "bcrypt": "^6.0.0",
19
+ "express": "^5.2.1",
20
+ "express-rate-limit": "^8.5.1"
21
+ }
17
22
  }
@@ -1,7 +1,9 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8">
4
+ <meta charset="UTF-8">
5
+ <script>(function(){var n=performance.getEntriesByType('navigation')[0];if(n&&n.type==='reload'){sessionStorage.clear();window.location.replace('./index.html');return;}if(sessionStorage.getItem('mm_auth')!=='true'){window.location.replace('./index.html');return;}var r=sessionStorage.getItem('mm_rank');if(!r){window.location.replace('./index.html');}})();</script>
6
+ <link rel="icon" type="image/png" href="/assets/favicon.png">
5
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
8
  <title>MM Games</title>
7
9
  <link rel="stylesheet" href="./css/style.css">
@@ -115,8 +117,9 @@
115
117
  <header class="site-header">
116
118
  <div class="header-inner">
117
119
  <a href="./browse.html" class="logo-link">
118
- <img src="./assets/logo.png" alt="" class="logo-img" onerror="this.style.display='none'">
119
- <span>MM Games</span><span style="font-size:0.65rem;color:var(--muted);margin-left:6px;">v1.0.0</span>
120
+ <img src="./assets/logo.png?v=new" alt="" class="logo-img" onerror="this.style.display='none'">
121
+ <span>MM Games</span>
122
+
120
123
  </a>
121
124
  <nav class="site-nav">
122
125
  <a href="./browse.html">Browse</a>
@@ -162,37 +165,33 @@
162
165
  document.title = game.name + ' — MM Games';
163
166
 
164
167
  const SITE = 'https://calc.moshelab.com';
165
- let src;
166
- if (game.embedType === 'local') {
167
- src = `./local-games/${game.localFile || game.slug + '.html'}`;
168
- } else if (game.embedType === 'gamepix') {
169
- src = game.embedUrl;
170
- } else {
171
- src = `https://html5.gamedistribution.com/${game.gameId}/?gd_sdk_referrer_url=${SITE}/games/${game.slug}`;
172
- }
168
+ const isLocal = game.embedType === 'local';
169
+ const src = isLocal ? '' :
170
+ game.embedType === 'gamepix' ? game.embedUrl :
171
+ `https://html5.gamedistribution.com/${game.gameId}/?gd_sdk_referrer_url=${SITE}/games/${game.slug}`;
173
172
 
174
173
  wrap.innerHTML = `
175
174
  <div class="game-title-bar">
176
175
  <div class="game-name">${game.name}</div>
177
176
  <span class="cat-badge ${game.category}">${game.category}</span>
178
177
  </div>
179
- <div class="game-frame-wrap" id="frame-wrap" style="${game.embedType === 'local' ? 'height:80vh;' : ''}">
180
- <div class="${game.embedType === 'local' ? '' : 'game-frame-ratio'}" style="${game.embedType === 'local' ? 'height:100%;' : ''}">
178
+ <div class="game-frame-wrap" id="frame-wrap" style="${isLocal ? 'height:80vh;' : ''}">
179
+ <div class="${isLocal ? '' : 'game-frame-ratio'}" style="${isLocal ? 'height:100%;' : ''}">
181
180
  <iframe
182
181
  class="game-iframe"
183
182
  id="game-iframe"
184
- src="${src}"
183
+ ${src ? `src="${src}"` : ''}
185
184
  allowfullscreen
186
185
  allow="fullscreen; autoplay; encrypted-media"
187
- scrolling="${game.embedType === 'local' ? 'yes' : 'no'}"
188
- style="${game.embedType === 'local' ? 'position:static;height:100%;' : ''}"
186
+ scrolling="${isLocal ? 'yes' : 'no'}"
187
+ style="${isLocal ? 'position:static;height:100%;' : ''}"
189
188
  ></iframe>
190
189
  </div>
191
190
  </div>
192
191
  <div class="game-actions">
193
192
  <button class="fullscreen-btn" id="fs-btn">
194
193
  <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
195
- <path d="M8 3H5a2 2 0 00-2 2v1.0.0m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v1.0.0a2 2 0 002 2h3"/>
194
+ <path d="M8 3H5a2 2 0 00-2 2v1.0.5m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v1.0.5a2 2 0 002 2h3"/>
196
195
  </svg>
197
196
  Fullscreen
198
197
  </button>
@@ -202,6 +201,17 @@
202
201
  </div>
203
202
  `;
204
203
 
204
+ if (isLocal) {
205
+ try {
206
+ const localPath = `./local-games/${game.localFile || game.slug + '.html'}`;
207
+ const res = await fetch(localPath);
208
+ const html = await res.text();
209
+ document.getElementById('game-iframe').srcdoc = html;
210
+ } catch {
211
+ error('Could not load game file.');
212
+ }
213
+ }
214
+
205
215
  document.getElementById('fs-btn').addEventListener('click', () => {
206
216
  const el = document.getElementById('frame-wrap');
207
217
  if (el.requestFullscreen) el.requestFullscreen();
@@ -209,6 +219,6 @@
209
219
  });
210
220
  })();
211
221
  </script>
212
- <script src="./js/pin-modal.js?v=2"></script>
222
+ <script src="./js/pin-modal.js?v=1.0.5c"></script>
213
223
  </body>
214
224
  </html>
package/users.json ADDED
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "username": "owner",
4
+ "password": "$2b$12$rQU1mMd1zohTMrDqv5Z3P.H8uhQZnpKo4mEibtxgQzSjR2Efdn8fy",
5
+ "rank": "administrator"
6
+ },
7
+ {
8
+ "username": "Anthony",
9
+ "password": "$2b$12$yvVmRuxDsxFsw9YDA05/COrhl.QhAV6jNYn28HxKC0DTErboyuZYO",
10
+ "rank": "admin"
11
+ },
12
+ {
13
+ "username": "user",
14
+ "password": "$2b$12$0xZ/udwEMcqjsQlfLXccMuZt6babCIRFBNGUg2l36RpU.oWzjLo5W",
15
+ "rank": "user"
16
+ }
17
+ ]
package/version.txt CHANGED
@@ -1 +1 @@
1
- v1.0.0
1
+ v1.0.5
package/pins.json DELETED
@@ -1 +0,0 @@
1
- {"admin":"1234","user":"5555"}