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/css/style.css
CHANGED
|
@@ -50,8 +50,9 @@ a { color: inherit; text-decoration: none; }
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
.logo-img {
|
|
53
|
-
height:
|
|
53
|
+
height: 40px;
|
|
54
54
|
width: auto;
|
|
55
|
+
border-radius: 8px;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
.site-nav {
|
|
@@ -611,3 +612,4 @@ a { color: inherit; text-decoration: none; }
|
|
|
611
612
|
transition: background .15s, color .15s;
|
|
612
613
|
}
|
|
613
614
|
.nav-calc-btn:hover { background: var(--surface); color: var(--text); }
|
|
615
|
+
|
package/index.html
CHANGED
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
user-select: none;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/* Header */
|
|
29
28
|
.calc-header {
|
|
30
29
|
display: flex;
|
|
31
30
|
align-items: center;
|
|
@@ -64,7 +63,6 @@
|
|
|
64
63
|
letter-spacing: .05em;
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
/* Display */
|
|
68
66
|
.calc-display {
|
|
69
67
|
padding: 12px 24px 20px;
|
|
70
68
|
text-align: right;
|
|
@@ -95,7 +93,6 @@
|
|
|
95
93
|
transition: font-size .1s;
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
/* Scientific row */
|
|
99
96
|
.sci-rows {
|
|
100
97
|
background: #111;
|
|
101
98
|
border-bottom: 1px solid rgba(255,255,255,.06);
|
|
@@ -127,7 +124,6 @@
|
|
|
127
124
|
.btn-sci:active { background: rgba(255,255,255,.1); }
|
|
128
125
|
.btn-sci.inv-active { color: #ff9f0a; }
|
|
129
126
|
|
|
130
|
-
/* Main buttons */
|
|
131
127
|
.calc-buttons {
|
|
132
128
|
display: grid;
|
|
133
129
|
grid-template-columns: repeat(4, 1fr);
|
|
@@ -156,8 +152,8 @@
|
|
|
156
152
|
.btn-zero { grid-column: span 2; justify-content: flex-start; padding-left: 30px; }
|
|
157
153
|
.btn-eq { background: #ff9f0a; color: #fff; }
|
|
158
154
|
|
|
159
|
-
/*
|
|
160
|
-
.
|
|
155
|
+
/* Login overlay */
|
|
156
|
+
.login-overlay {
|
|
161
157
|
position: fixed;
|
|
162
158
|
inset: 0;
|
|
163
159
|
background: rgba(0,0,0,.75);
|
|
@@ -167,97 +163,84 @@
|
|
|
167
163
|
z-index: 999;
|
|
168
164
|
backdrop-filter: blur(8px);
|
|
169
165
|
}
|
|
166
|
+
.login-overlay.hidden { display: none; }
|
|
170
167
|
|
|
171
|
-
.
|
|
172
|
-
|
|
173
|
-
.pin-box {
|
|
168
|
+
.login-box {
|
|
174
169
|
background: #1c1c1e;
|
|
175
170
|
border-radius: 20px;
|
|
176
|
-
padding: 32px 28px
|
|
171
|
+
padding: 32px 28px 28px;
|
|
177
172
|
width: 300px;
|
|
178
173
|
box-shadow: 0 32px 80px rgba(0,0,0,.6);
|
|
179
174
|
border: 1px solid rgba(255,255,255,.08);
|
|
180
175
|
text-align: center;
|
|
181
176
|
}
|
|
182
177
|
|
|
183
|
-
.
|
|
178
|
+
.login-box h2 {
|
|
184
179
|
font-size: 1rem;
|
|
185
180
|
font-weight: 600;
|
|
186
181
|
color: #fff;
|
|
187
182
|
margin-bottom: 4px;
|
|
188
183
|
}
|
|
189
184
|
|
|
190
|
-
.
|
|
185
|
+
.login-box p {
|
|
191
186
|
font-size: 0.8rem;
|
|
192
187
|
color: rgba(255,255,255,.4);
|
|
193
188
|
margin-bottom: 24px;
|
|
194
189
|
}
|
|
195
190
|
|
|
196
|
-
.
|
|
197
|
-
|
|
198
|
-
justify-content: center;
|
|
199
|
-
gap: 14px;
|
|
200
|
-
margin-bottom: 28px;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.pin-dot {
|
|
204
|
-
width: 13px;
|
|
205
|
-
height: 13px;
|
|
206
|
-
border-radius: 50%;
|
|
207
|
-
border: 2px solid rgba(255,255,255,.2);
|
|
208
|
-
background: transparent;
|
|
209
|
-
transition: all .15s;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.pin-dot.filled { background: #ff9f0a; border-color: #ff9f0a; }
|
|
213
|
-
.pin-dot.error { background: #ff3b30; border-color: #ff3b30; }
|
|
214
|
-
|
|
215
|
-
.pin-keypad {
|
|
216
|
-
display: grid;
|
|
217
|
-
grid-template-columns: repeat(3, 1fr);
|
|
218
|
-
gap: 10px;
|
|
219
|
-
margin-bottom: 16px;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
.pin-key {
|
|
223
|
-
padding: 15px;
|
|
224
|
-
border: none;
|
|
225
|
-
border-radius: 12px;
|
|
191
|
+
.login-field {
|
|
192
|
+
width: 100%;
|
|
226
193
|
background: rgba(255,255,255,.08);
|
|
227
|
-
|
|
228
|
-
|
|
194
|
+
border: 1px solid rgba(255,255,255,.12);
|
|
195
|
+
border-radius: 10px;
|
|
229
196
|
color: #fff;
|
|
230
|
-
|
|
231
|
-
|
|
197
|
+
font-size: 0.9rem;
|
|
198
|
+
padding: 11px 14px;
|
|
199
|
+
margin-bottom: 10px;
|
|
200
|
+
outline: none;
|
|
201
|
+
font-family: inherit;
|
|
202
|
+
transition: border-color .15s;
|
|
232
203
|
}
|
|
204
|
+
.login-field:focus { border-color: #ff9f0a; }
|
|
205
|
+
.login-field::placeholder { color: rgba(255,255,255,.3); }
|
|
233
206
|
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
207
|
+
.login-submit {
|
|
208
|
+
width: 100%;
|
|
209
|
+
background: #ff9f0a;
|
|
210
|
+
color: #fff;
|
|
211
|
+
border: none;
|
|
212
|
+
border-radius: 10px;
|
|
213
|
+
font-size: 0.9rem;
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
padding: 12px;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
margin-top: 4px;
|
|
218
|
+
font-family: inherit;
|
|
219
|
+
transition: filter .1s;
|
|
241
220
|
}
|
|
221
|
+
.login-submit:hover { filter: brightness(1.1); }
|
|
222
|
+
.login-submit:active { filter: brightness(0.9); }
|
|
223
|
+
.login-submit:disabled { opacity: .5; cursor: not-allowed; filter: none; }
|
|
242
224
|
|
|
243
|
-
.
|
|
244
|
-
font-size: 0.
|
|
225
|
+
.login-error {
|
|
226
|
+
font-size: 0.78rem;
|
|
245
227
|
color: #ff3b30;
|
|
246
228
|
min-height: 1.2em;
|
|
247
|
-
margin
|
|
229
|
+
margin: 8px 0 0;
|
|
248
230
|
}
|
|
249
231
|
|
|
250
|
-
.
|
|
232
|
+
.login-cancel {
|
|
251
233
|
background: none;
|
|
252
234
|
border: none;
|
|
253
|
-
color: rgba(255,255,255,.
|
|
254
|
-
font-size: 0.
|
|
235
|
+
color: rgba(255,255,255,.35);
|
|
236
|
+
font-size: 0.82rem;
|
|
255
237
|
cursor: pointer;
|
|
256
|
-
padding: 6px;
|
|
238
|
+
padding: 10px 6px 0;
|
|
239
|
+
font-family: inherit;
|
|
257
240
|
}
|
|
258
|
-
|
|
259
|
-
.pin-cancel:hover { color: rgba(255,255,255,.7); }
|
|
241
|
+
.login-cancel:hover { color: rgba(255,255,255,.6); }
|
|
260
242
|
</style>
|
|
243
|
+
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
|
261
244
|
</head>
|
|
262
245
|
<body>
|
|
263
246
|
|
|
@@ -265,10 +248,10 @@
|
|
|
265
248
|
|
|
266
249
|
<div class="calc-header">
|
|
267
250
|
<div class="calc-logo-wrap" id="calc-logo" style="cursor:pointer;">
|
|
268
|
-
<div class="calc-logo-icon"
|
|
251
|
+
<div class="calc-logo-icon">∑</div>
|
|
269
252
|
<span class="calc-logo-text">SciCalc Pro</span>
|
|
270
253
|
</div>
|
|
271
|
-
<span class="calc-mode-indicator" id="mode-indicator">DEG</span><span style="font-size:0.6rem;color:rgba(255,255,255,.2);margin-left:8px;">v1.0.
|
|
254
|
+
<span class="calc-mode-indicator" id="mode-indicator">DEG</span><span style="font-size:0.6rem;color:rgba(255,255,255,.2);margin-left:8px;">v1.0.5</span>
|
|
272
255
|
</div>
|
|
273
256
|
|
|
274
257
|
<div class="calc-display">
|
|
@@ -283,12 +266,12 @@
|
|
|
283
266
|
<button class="btn-sci" data-sci="sin">sin</button>
|
|
284
267
|
<button class="btn-sci" data-sci="cos">cos</button>
|
|
285
268
|
<button class="btn-sci" data-sci="tan">tan</button>
|
|
286
|
-
<button class="btn-sci" data-sci="pi"
|
|
269
|
+
<button class="btn-sci" data-sci="pi">π</button>
|
|
287
270
|
<button class="btn-sci" data-sci="e">e</button>
|
|
288
271
|
</div>
|
|
289
272
|
<div class="sci-row">
|
|
290
|
-
<button class="btn-sci" data-sci="sq">x
|
|
291
|
-
<button class="btn-sci" data-sci="sqrt"
|
|
273
|
+
<button class="btn-sci" data-sci="sq">x²</button>
|
|
274
|
+
<button class="btn-sci" data-sci="sqrt">√x</button>
|
|
292
275
|
<button class="btn-sci" data-sci="log">log</button>
|
|
293
276
|
<button class="btn-sci" data-sci="ln">ln</button>
|
|
294
277
|
<button class="btn-sci" data-sci="open">(</button>
|
|
@@ -299,19 +282,19 @@
|
|
|
299
282
|
<!-- Standard buttons -->
|
|
300
283
|
<div class="calc-buttons">
|
|
301
284
|
<button class="btn btn-func" data-action="clear">AC</button>
|
|
302
|
-
<button class="btn btn-func" data-action="sign"
|
|
285
|
+
<button class="btn btn-func" data-action="sign">+/−</button>
|
|
303
286
|
<button class="btn btn-func" data-action="percent">%</button>
|
|
304
|
-
<button class="btn btn-op" data-action="op" data-op="
|
|
287
|
+
<button class="btn btn-op" data-action="op" data-op="÷">÷</button>
|
|
305
288
|
|
|
306
289
|
<button class="btn btn-num" data-action="digit" data-digit="7">7</button>
|
|
307
290
|
<button class="btn btn-num" data-action="digit" data-digit="8">8</button>
|
|
308
291
|
<button class="btn btn-num" data-action="digit" data-digit="9">9</button>
|
|
309
|
-
<button class="btn btn-op" data-action="op" data-op="
|
|
292
|
+
<button class="btn btn-op" data-action="op" data-op="×">×</button>
|
|
310
293
|
|
|
311
294
|
<button class="btn btn-num" data-action="digit" data-digit="4">4</button>
|
|
312
295
|
<button class="btn btn-num" data-action="digit" data-digit="5">5</button>
|
|
313
296
|
<button class="btn btn-num" data-action="digit" data-digit="6">6</button>
|
|
314
|
-
<button class="btn btn-op" data-action="op" data-op="
|
|
297
|
+
<button class="btn btn-op" data-action="op" data-op="−">−</button>
|
|
315
298
|
|
|
316
299
|
<button class="btn btn-num" data-action="digit" data-digit="1">1</button>
|
|
317
300
|
<button class="btn btn-num" data-action="digit" data-digit="2">2</button>
|
|
@@ -324,35 +307,194 @@
|
|
|
324
307
|
</div>
|
|
325
308
|
</div>
|
|
326
309
|
|
|
327
|
-
<!--
|
|
328
|
-
<div class="
|
|
329
|
-
<div class="
|
|
330
|
-
<h2>
|
|
331
|
-
<p>Enter your
|
|
332
|
-
<
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
</
|
|
338
|
-
<div class="pin-error" id="pin-error"></div>
|
|
339
|
-
<div class="pin-keypad">
|
|
340
|
-
<button class="pin-key" data-k="1">1</button>
|
|
341
|
-
<button class="pin-key" data-k="2">2</button>
|
|
342
|
-
<button class="pin-key" data-k="3">3</button>
|
|
343
|
-
<button class="pin-key" data-k="4">4</button>
|
|
344
|
-
<button class="pin-key" data-k="5">5</button>
|
|
345
|
-
<button class="pin-key" data-k="6">6</button>
|
|
346
|
-
<button class="pin-key" data-k="7">7</button>
|
|
347
|
-
<button class="pin-key" data-k="8">8</button>
|
|
348
|
-
<button class="pin-key" data-k="9">9</button>
|
|
349
|
-
<button class="pin-key del" data-k="del">⌫</button>
|
|
350
|
-
<button class="pin-key" data-k="0">0</button>
|
|
351
|
-
<button class="pin-key del" data-k="cancel" style="font-size:.75rem;">cancel</button>
|
|
352
|
-
</div>
|
|
310
|
+
<!-- Login overlay — revealed by triple-clicking the logo -->
|
|
311
|
+
<div class="login-overlay hidden" id="login-overlay">
|
|
312
|
+
<div class="login-box">
|
|
313
|
+
<h2>Sign In</h2>
|
|
314
|
+
<p>Enter your credentials to continue</p>
|
|
315
|
+
<input class="login-field" id="login-user" type="text" placeholder="Username" autocomplete="username" spellcheck="false">
|
|
316
|
+
<input class="login-field" id="login-pass" type="password" placeholder="Password" autocomplete="current-password">
|
|
317
|
+
<div class="login-error" id="login-error"></div>
|
|
318
|
+
<button class="login-submit" id="login-btn">Sign In</button>
|
|
319
|
+
<br>
|
|
320
|
+
<button class="login-cancel" id="login-cancel">Cancel</button>
|
|
353
321
|
</div>
|
|
354
322
|
</div>
|
|
355
323
|
|
|
356
|
-
<script src="./js/main.js?v=
|
|
324
|
+
<script src="./js/main.js?v=1.0.5"></script>
|
|
325
|
+
|
|
326
|
+
<!-- Matrix Easter egg -->
|
|
327
|
+
<div id="matrix-overlay" style="display:none;position:fixed;inset:0;z-index:9999;background:#000;overflow:hidden;">
|
|
328
|
+
<canvas id="matrix-canvas" style="display:block;width:100%;height:100%;"></canvas>
|
|
329
|
+
<div id="matrix-msg" style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;pointer-events:none;"></div>
|
|
330
|
+
</div>
|
|
331
|
+
<script>
|
|
332
|
+
function triggerMatrixEgg() {
|
|
333
|
+
pressClear();
|
|
334
|
+
var overlay = document.getElementById('matrix-overlay');
|
|
335
|
+
var canvas = document.getElementById('matrix-canvas');
|
|
336
|
+
var msgBox = document.getElementById('matrix-msg');
|
|
337
|
+
var ctx = canvas.getContext('2d');
|
|
338
|
+
overlay.style.display = 'block';
|
|
339
|
+
overlay.style.opacity = '1';
|
|
340
|
+
|
|
341
|
+
canvas.width = window.innerWidth;
|
|
342
|
+
canvas.height = window.innerHeight;
|
|
343
|
+
|
|
344
|
+
var cols = Math.floor(canvas.width / 16);
|
|
345
|
+
var drops = Array.from({length: cols}, function() { return Math.random() * -50 | 0; });
|
|
346
|
+
var chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%';
|
|
347
|
+
|
|
348
|
+
function drawMatrix() {
|
|
349
|
+
ctx.fillStyle = 'rgba(0,0,0,0.05)';
|
|
350
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
351
|
+
ctx.font = '15px monospace';
|
|
352
|
+
for (var i = 0; i < drops.length; i++) {
|
|
353
|
+
var ch = chars[Math.random() * chars.length | 0];
|
|
354
|
+
ctx.fillStyle = drops[i] < 2 ? '#afffaf' : '#00ff41';
|
|
355
|
+
ctx.fillText(ch, i * 16, drops[i] * 16);
|
|
356
|
+
if (drops[i] * 16 > canvas.height && Math.random() > 0.975) drops[i] = 0;
|
|
357
|
+
drops[i]++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
var messages = ['ACCESS GRANTED', 'WELCOME HACKER', 'MM GAMES UNLOCKED'];
|
|
362
|
+
msgBox.innerHTML = messages.map(function(m, i) {
|
|
363
|
+
return '<div style="font-family:monospace;font-size:clamp(20px,4vw,40px);font-weight:bold;color:#00ff41;'
|
|
364
|
+
+ 'text-shadow:0 0 20px #00ff41,0 0 40px #00ff41;letter-spacing:4px;opacity:0;'
|
|
365
|
+
+ 'animation:mfade .4s ease ' + (i * 0.35) + 's forwards;">' + m + '</div>';
|
|
366
|
+
}).join('');
|
|
367
|
+
|
|
368
|
+
var style = document.createElement('style');
|
|
369
|
+
style.textContent = '@keyframes mfade{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}';
|
|
370
|
+
document.head.appendChild(style);
|
|
371
|
+
|
|
372
|
+
var raf = setInterval(drawMatrix, 40);
|
|
373
|
+
|
|
374
|
+
setTimeout(function() {
|
|
375
|
+
var fade = 1;
|
|
376
|
+
var fi = setInterval(function() {
|
|
377
|
+
fade -= 0.05;
|
|
378
|
+
overlay.style.opacity = Math.max(0, fade);
|
|
379
|
+
if (fade <= 0) {
|
|
380
|
+
clearInterval(fi);
|
|
381
|
+
clearInterval(raf);
|
|
382
|
+
overlay.style.display = 'none';
|
|
383
|
+
overlay.style.opacity = '1';
|
|
384
|
+
msgBox.innerHTML = '';
|
|
385
|
+
style.remove();
|
|
386
|
+
}
|
|
387
|
+
}, 40);
|
|
388
|
+
}, 3000);
|
|
389
|
+
}
|
|
390
|
+
</script>
|
|
391
|
+
|
|
392
|
+
<script>
|
|
393
|
+
(function () {
|
|
394
|
+
var logo = document.getElementById('calc-logo');
|
|
395
|
+
var overlay = document.getElementById('login-overlay');
|
|
396
|
+
var userEl = document.getElementById('login-user');
|
|
397
|
+
var passEl = document.getElementById('login-pass');
|
|
398
|
+
var btn = document.getElementById('login-btn');
|
|
399
|
+
var errEl = document.getElementById('login-error');
|
|
400
|
+
var cancelBtn = document.getElementById('login-cancel');
|
|
401
|
+
|
|
402
|
+
var failCount = 0;
|
|
403
|
+
var lockedUntil = 0;
|
|
404
|
+
|
|
405
|
+
// Triple-click detection
|
|
406
|
+
var clicks = 0;
|
|
407
|
+
var clickTimer = null;
|
|
408
|
+
logo.addEventListener('click', function () {
|
|
409
|
+
clicks++;
|
|
410
|
+
if (clicks === 1) {
|
|
411
|
+
clickTimer = setTimeout(function () { clicks = 0; }, 500);
|
|
412
|
+
}
|
|
413
|
+
if (clicks >= 3) {
|
|
414
|
+
clearTimeout(clickTimer);
|
|
415
|
+
clicks = 0;
|
|
416
|
+
openLogin();
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
function openLogin() {
|
|
421
|
+
errEl.textContent = '';
|
|
422
|
+
userEl.value = '';
|
|
423
|
+
passEl.value = '';
|
|
424
|
+
btn.disabled = false;
|
|
425
|
+
overlay.classList.remove('hidden');
|
|
426
|
+
setTimeout(function () { userEl.focus(); }, 50);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function closeLogin() {
|
|
430
|
+
overlay.classList.add('hidden');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
cancelBtn.addEventListener('click', closeLogin);
|
|
434
|
+
|
|
435
|
+
overlay.addEventListener('click', function (e) {
|
|
436
|
+
if (e.target === overlay) closeLogin();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
document.addEventListener('keydown', function (e) {
|
|
440
|
+
if (!overlay.classList.contains('hidden') && e.key === 'Escape') closeLogin();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
passEl.addEventListener('keydown', function (e) {
|
|
444
|
+
if (e.key === 'Enter') doLogin();
|
|
445
|
+
});
|
|
446
|
+
userEl.addEventListener('keydown', function (e) {
|
|
447
|
+
if (e.key === 'Enter') passEl.focus();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
btn.addEventListener('click', doLogin);
|
|
451
|
+
|
|
452
|
+
async function doLogin() {
|
|
453
|
+
var now = Date.now();
|
|
454
|
+
if (now < lockedUntil) {
|
|
455
|
+
var secs = Math.ceil((lockedUntil - now) / 1000);
|
|
456
|
+
errEl.textContent = 'Too many attempts. Wait ' + secs + 's.';
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
var username = userEl.value.trim();
|
|
460
|
+
var password = passEl.value;
|
|
461
|
+
if (!username || !password) { errEl.textContent = 'Enter username and password.'; return; }
|
|
462
|
+
btn.disabled = true;
|
|
463
|
+
errEl.textContent = '';
|
|
464
|
+
try {
|
|
465
|
+
var res = await fetch('./api/login', {
|
|
466
|
+
method: 'POST',
|
|
467
|
+
headers: { 'Content-Type': 'application/json' },
|
|
468
|
+
body: JSON.stringify({ username: username, password: password }),
|
|
469
|
+
});
|
|
470
|
+
var data = await res.json();
|
|
471
|
+
if (data.success) {
|
|
472
|
+
sessionStorage.setItem('mm_auth', 'true');
|
|
473
|
+
sessionStorage.setItem('mm_rank', data.rank);
|
|
474
|
+
sessionStorage.setItem('mm_token', data.token);
|
|
475
|
+
if (data.rank === 'administrator') window.location.href = './admin.html';
|
|
476
|
+
else if (data.rank === 'admin') window.location.href = './control-panel.html';
|
|
477
|
+
else window.location.href = './browse.html';
|
|
478
|
+
} else {
|
|
479
|
+
failCount++;
|
|
480
|
+
if (failCount >= 5) {
|
|
481
|
+
lockedUntil = Date.now() + 60000;
|
|
482
|
+
failCount = 0;
|
|
483
|
+
errEl.textContent = 'Too many attempts. Locked for 60 seconds.';
|
|
484
|
+
} else {
|
|
485
|
+
errEl.textContent = 'Invalid credentials.';
|
|
486
|
+
}
|
|
487
|
+
btn.disabled = false;
|
|
488
|
+
passEl.value = '';
|
|
489
|
+
passEl.focus();
|
|
490
|
+
}
|
|
491
|
+
} catch (err) {
|
|
492
|
+
errEl.textContent = 'Connection error. Try again.';
|
|
493
|
+
btn.disabled = false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
})();
|
|
497
|
+
</script>
|
|
498
|
+
|
|
357
499
|
</body>
|
|
358
500
|
</html>
|
package/js/admin.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
if (!localStorage.getItem('mm_admin_session')) {
|
|
2
|
-
window.location.replace('./browse.html');
|
|
3
|
-
}
|
|
4
|
-
|
|
5
1
|
let games = [];
|
|
6
2
|
|
|
7
|
-
function
|
|
8
|
-
return sessionStorage.getItem('
|
|
3
|
+
function getToken() {
|
|
4
|
+
return sessionStorage.getItem('mm_token') || '';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function authHeaders() {
|
|
8
|
+
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() };
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
async function loadGames() {
|
|
12
12
|
try {
|
|
13
|
-
const res = await fetch('./api/games/all');
|
|
13
|
+
const res = await fetch('./api/games/all', { headers: authHeaders() });
|
|
14
14
|
if (!res.ok) throw new Error();
|
|
15
15
|
games = await res.json();
|
|
16
16
|
} catch {
|
|
@@ -27,8 +27,8 @@ async function saveGames() {
|
|
|
27
27
|
try {
|
|
28
28
|
const res = await fetch('./api/games', {
|
|
29
29
|
method: 'POST',
|
|
30
|
-
headers:
|
|
31
|
-
body: JSON.stringify({
|
|
30
|
+
headers: authHeaders(),
|
|
31
|
+
body: JSON.stringify({ games }),
|
|
32
32
|
});
|
|
33
33
|
if (!res.ok) throw new Error();
|
|
34
34
|
return true;
|
|
@@ -135,34 +135,125 @@ document.getElementById('table-body').addEventListener('change', async e => {
|
|
|
135
135
|
showFeedback('table-feedback', ok ? 'Visibility updated.' : 'Updated locally (API unavailable).', ok ? 'ok' : 'error');
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
// ── User Management ───────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
async function loadUsers() {
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch('./api/users', { headers: authHeaders() });
|
|
143
|
+
if (!res.ok) throw new Error();
|
|
144
|
+
const users = await res.json();
|
|
145
|
+
renderUsers(users);
|
|
146
|
+
} catch {
|
|
147
|
+
document.getElementById('users-tbody').innerHTML =
|
|
148
|
+
'<tr><td colspan="3" style="color:var(--muted);text-align:center;padding:24px;">Unable to load users.</td></tr>';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const RANK_CLASS = { administrator: 'rank-administrator', admin: 'rank-admin', user: 'rank-user' };
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
function renderUsers(users) {
|
|
155
|
+
const tbody = document.getElementById('users-tbody');
|
|
156
|
+
if (!users.length) {
|
|
157
|
+
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;padding:24px;color:var(--muted);">No users.</td></tr>';
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
tbody.innerHTML = users.map(u => `
|
|
161
|
+
<tr>
|
|
162
|
+
<td style="font-weight:500;">${u.username}</td>
|
|
163
|
+
<td>
|
|
164
|
+
<select class="rank-select" data-user="${u.username}" data-orig="${u.rank}">
|
|
165
|
+
<option value="user" ${u.rank==='user'?'selected':''}>user</option>
|
|
166
|
+
<option value="admin" ${u.rank==='admin'?'selected':''}>admin</option>
|
|
167
|
+
<option value="administrator" ${u.rank==='administrator'?'selected':''}>administrator</option>
|
|
168
|
+
</select>
|
|
169
|
+
</td>
|
|
170
|
+
<td style="display:flex;gap:6px;flex-wrap:wrap;padding:6px 8px;">
|
|
171
|
+
<button class="btn btn-sm btn-primary" data-action="update-rank" data-user="${u.username}">Save Rank</button>
|
|
172
|
+
<button class="btn btn-sm btn-ghost" data-action="reset-pass" data-user="${u.username}">Reset Password</button>
|
|
173
|
+
<button class="btn btn-sm btn-danger" data-action="del-user" data-user="${u.username}">Delete</button>
|
|
174
|
+
</td>
|
|
175
|
+
</tr>
|
|
176
|
+
`).join('');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
document.getElementById('users-tbody').addEventListener('click', async e => {
|
|
180
|
+
const btn = e.target.closest('[data-action]');
|
|
181
|
+
if (!btn) return;
|
|
182
|
+
const action = btn.dataset.action;
|
|
183
|
+
const username = btn.dataset.user;
|
|
184
|
+
|
|
185
|
+
if (action === 'update-rank') {
|
|
186
|
+
const row = btn.closest('tr');
|
|
187
|
+
const sel = row.querySelector('select.rank-select');
|
|
188
|
+
const rank = sel.value;
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch('./api/users/update', {
|
|
191
|
+
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username, rank }),
|
|
192
|
+
});
|
|
193
|
+
const data = await res.json();
|
|
194
|
+
if (!res.ok) throw new Error(data.error);
|
|
195
|
+
showFeedback('users-feedback', `${username} rank updated to ${rank}.`, 'ok');
|
|
196
|
+
loadUsers();
|
|
197
|
+
} catch (err) {
|
|
198
|
+
showFeedback('users-feedback', err.message || 'Failed.', 'error');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (action === 'reset-pass') {
|
|
203
|
+
const newPass = prompt(`New password for "${username}":`);
|
|
204
|
+
if (!newPass) return;
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch('./api/users/reset-password', {
|
|
207
|
+
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username, password: newPass }),
|
|
208
|
+
});
|
|
209
|
+
const data = await res.json();
|
|
210
|
+
if (!res.ok) throw new Error(data.error);
|
|
211
|
+
showFeedback('users-feedback', `Password reset for ${username}.`, 'ok');
|
|
212
|
+
} catch (err) {
|
|
213
|
+
showFeedback('users-feedback', err.message || 'Failed.', 'error');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (action === 'del-user') {
|
|
218
|
+
if (!confirm(`Delete user "${username}"?`)) return;
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch('./api/users/delete', {
|
|
221
|
+
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username }),
|
|
222
|
+
});
|
|
223
|
+
const data = await res.json();
|
|
224
|
+
if (!res.ok) throw new Error(data.error);
|
|
225
|
+
showFeedback('users-feedback', `${username} deleted.`, 'ok');
|
|
226
|
+
loadUsers();
|
|
227
|
+
} catch (err) {
|
|
228
|
+
showFeedback('users-feedback', err.message || 'Failed.', 'error');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
document.getElementById('add-user-btn').addEventListener('click', async () => {
|
|
234
|
+
const username = document.getElementById('new-username').value.trim();
|
|
235
|
+
const password = document.getElementById('new-password').value;
|
|
236
|
+
const rank = document.getElementById('new-rank').value;
|
|
237
|
+
|
|
238
|
+
if (!username || !password) {
|
|
239
|
+
showFeedback('add-user-feedback', 'Username and password are required.', 'error');
|
|
145
240
|
return;
|
|
146
241
|
}
|
|
147
242
|
|
|
148
243
|
try {
|
|
149
|
-
const res = await fetch('./api/
|
|
150
|
-
method: 'POST',
|
|
151
|
-
headers: { 'Content-Type': 'application/json' },
|
|
152
|
-
body: JSON.stringify({ adminPin: getAdminPin(), newAdmin, newUser }),
|
|
244
|
+
const res = await fetch('./api/users/add', {
|
|
245
|
+
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username, password, rank }),
|
|
153
246
|
});
|
|
154
247
|
const data = await res.json();
|
|
155
|
-
if (!res.ok)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
showFeedback('pin-feedback', 'PINs updated.', 'ok');
|
|
163
|
-
} catch {
|
|
164
|
-
showFeedback('pin-feedback', 'API unavailable.', 'error');
|
|
248
|
+
if (!res.ok) throw new Error(data.error);
|
|
249
|
+
document.getElementById('new-username').value = '';
|
|
250
|
+
document.getElementById('new-password').value = '';
|
|
251
|
+
showFeedback('add-user-feedback', `User "${username}" added.`, 'ok');
|
|
252
|
+
loadUsers();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
showFeedback('add-user-feedback', err.message || 'Failed.', 'error');
|
|
165
255
|
}
|
|
166
256
|
});
|
|
167
257
|
|
|
168
258
|
loadGames();
|
|
259
|
+
loadUsers();
|
package/js/games.js
CHANGED
|
@@ -131,7 +131,7 @@ function renderGotd() {
|
|
|
131
131
|
|
|
132
132
|
section.innerHTML = `
|
|
133
133
|
<div class="section-title">Game of the Day</div>
|
|
134
|
-
<a href="./
|
|
134
|
+
<a href="./tool.html?slug=${gotd.slug}" class="gotd-card">
|
|
135
135
|
${thumbHtml}
|
|
136
136
|
<div>
|
|
137
137
|
<div class="gotd-label">⭐ Featured Today</div>
|
|
@@ -159,7 +159,7 @@ function renderGrid() {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
grid.innerHTML = games.map(g => `
|
|
162
|
-
<a href="./
|
|
162
|
+
<a href="./tool.html?slug=${g.slug}" class="game-card">
|
|
163
163
|
${cardThumb(g)}
|
|
164
164
|
<div class="game-card-body">
|
|
165
165
|
<div class="game-card-name">${g.name}</div>
|