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/admin.html +58 -13
- package/api.js +134 -85
- package/assets/favicon.png +0 -0
- package/assets/logo.png +0 -0
- package/browse.html +23 -5
- package/bump.sh +5 -1
- package/control-panel.html +884 -0
- package/css/style.css +3 -1
- package/index.html +240 -98
- package/js/admin.js +120 -29
- package/js/games.js +2 -2
- package/js/main.js +14 -95
- package/js/pin-modal.js +10 -95
- package/js/theme.js +20 -0
- package/package.json +7 -2
- package/{game.html → tool.html} +28 -18
- package/users.json +17 -0
- package/version.txt +1 -1
- package/pins.json +0 -1
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 === '
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
//
|
|
2
|
-
|
|
3
|
-
(()
|
|
4
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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.
|
|
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
|
}
|
package/{game.html → tool.html}
RENAMED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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="${
|
|
180
|
-
<div class="${
|
|
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="${
|
|
188
|
-
style="${
|
|
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.
|
|
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=
|
|
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.
|
|
1
|
+
v1.0.5
|
package/pins.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"admin":"1234","user":"5555"}
|