studyup 1.0.2 → 1.0.3
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/index.html +379 -72
- package/package.json +1 -1
- package/server.js +50 -0
package/index.html
CHANGED
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>StudyUp - Student Portal</title>
|
|
7
|
-
<!-- Material Design
|
|
7
|
+
<!-- Material Design / Roboto Typography & Icons -->
|
|
8
8
|
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap" rel="stylesheet">
|
|
9
9
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
10
10
|
|
|
11
11
|
<style>
|
|
12
|
-
/* Material Design 1 Core Variables & Palette */
|
|
13
12
|
:root {
|
|
14
13
|
--primary-color: #3f51b5; /* Indigo */
|
|
15
14
|
--primary-dark: #303f9f;
|
|
@@ -19,6 +18,8 @@
|
|
|
19
18
|
--text-primary: rgba(0, 0, 0, 0.87);
|
|
20
19
|
--text-secondary: rgba(0, 0, 0, 0.54);
|
|
21
20
|
--error-color: #d32f2f;
|
|
21
|
+
--success-color: #388e3c;
|
|
22
|
+
--warning-color: #f57c00;
|
|
22
23
|
--shadow-elevation-2: 0 3px 1px -2px rgba(0,0,0,0.2), 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12);
|
|
23
24
|
--shadow-elevation-4: 0 4px 5px -2px rgba(0,0,0,0.2), 0 7px 10px 1px rgba(0,0,0,0.14), 0 2px 16px 1px rgba(0,0,0,0.12);
|
|
24
25
|
}
|
|
@@ -35,7 +36,6 @@
|
|
|
35
36
|
justify-content: space-between;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/* Material Toolbar (Header) */
|
|
39
39
|
header {
|
|
40
40
|
background-color: var(--primary-color);
|
|
41
41
|
color: white;
|
|
@@ -51,9 +51,15 @@
|
|
|
51
51
|
font-size: 20px;
|
|
52
52
|
font-weight: 500;
|
|
53
53
|
letter-spacing: 0.5px;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.header-actions {
|
|
58
|
+
display: flex;
|
|
59
|
+
gap: 12px;
|
|
60
|
+
align-items: center;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
/* View Switching (Login vs Portal Dashboard) */
|
|
57
63
|
.view {
|
|
58
64
|
display: none;
|
|
59
65
|
flex: 1;
|
|
@@ -63,7 +69,6 @@
|
|
|
63
69
|
display: flex;
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
/* Main Container & Login Card */
|
|
67
72
|
.container {
|
|
68
73
|
max-width: 420px;
|
|
69
74
|
width: 100%;
|
|
@@ -119,7 +124,6 @@
|
|
|
119
124
|
line-height: 1.4;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
/* Material Floating Label Input Field */
|
|
123
127
|
.input-container {
|
|
124
128
|
position: relative;
|
|
125
129
|
margin-bottom: 24px;
|
|
@@ -166,7 +170,6 @@
|
|
|
166
170
|
font-weight: 500;
|
|
167
171
|
}
|
|
168
172
|
|
|
169
|
-
/* Error Message */
|
|
170
173
|
#error-msg {
|
|
171
174
|
color: var(--error-color);
|
|
172
175
|
font-size: 13px;
|
|
@@ -175,7 +178,6 @@
|
|
|
175
178
|
font-weight: 500;
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
/* Material Raised Button */
|
|
179
181
|
.btn-raised {
|
|
180
182
|
background-color: var(--primary-color);
|
|
181
183
|
color: white;
|
|
@@ -198,15 +200,28 @@
|
|
|
198
200
|
box-shadow: var(--shadow-elevation-4);
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
|
|
203
|
+
.btn-danger {
|
|
204
|
+
background-color: var(--error-color);
|
|
205
|
+
}
|
|
206
|
+
.btn-danger:hover {
|
|
207
|
+
background-color: #b71c1c;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.btn-warning {
|
|
211
|
+
background-color: var(--warning-color);
|
|
212
|
+
}
|
|
213
|
+
.btn-warning:hover {
|
|
214
|
+
background-color: #e65100;
|
|
215
|
+
}
|
|
216
|
+
|
|
202
217
|
.portal-container {
|
|
203
|
-
max-width:
|
|
218
|
+
max-width: 1000px;
|
|
204
219
|
width: 100%;
|
|
205
220
|
margin: 40px auto;
|
|
206
221
|
padding: 0 16px;
|
|
207
222
|
box-sizing: border-box;
|
|
208
223
|
display: grid;
|
|
209
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
224
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
210
225
|
gap: 24px;
|
|
211
226
|
flex: 1;
|
|
212
227
|
}
|
|
@@ -226,7 +241,7 @@
|
|
|
226
241
|
}
|
|
227
242
|
|
|
228
243
|
.card-media {
|
|
229
|
-
height:
|
|
244
|
+
height: 120px;
|
|
230
245
|
display: flex;
|
|
231
246
|
align-items: center;
|
|
232
247
|
justify-content: center;
|
|
@@ -256,6 +271,7 @@
|
|
|
256
271
|
padding: 8px 16px 16px 16px;
|
|
257
272
|
display: flex;
|
|
258
273
|
justify-content: flex-end;
|
|
274
|
+
gap: 8px;
|
|
259
275
|
}
|
|
260
276
|
|
|
261
277
|
.btn-flat {
|
|
@@ -278,7 +294,56 @@
|
|
|
278
294
|
background-color: rgba(63, 81, 181, 0.08);
|
|
279
295
|
}
|
|
280
296
|
|
|
281
|
-
|
|
297
|
+
.admin-section {
|
|
298
|
+
background: white;
|
|
299
|
+
padding: 24px;
|
|
300
|
+
border-radius: 2px;
|
|
301
|
+
box-shadow: var(--shadow-elevation-2);
|
|
302
|
+
margin-bottom: 24px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.admin-section h2 {
|
|
306
|
+
margin-top: 0;
|
|
307
|
+
font-size: 18px;
|
|
308
|
+
color: var(--primary-color);
|
|
309
|
+
border-bottom: 2px solid var(--background-color);
|
|
310
|
+
padding-bottom: 8px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.code-list-item {
|
|
314
|
+
display: flex;
|
|
315
|
+
flex-direction: column;
|
|
316
|
+
background: #fafafa;
|
|
317
|
+
padding: 16px;
|
|
318
|
+
margin-bottom: 12px;
|
|
319
|
+
border-radius: 4px;
|
|
320
|
+
border: 1px solid #e0e0e0;
|
|
321
|
+
gap: 12px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@media(min-width: 600px) {
|
|
325
|
+
.code-list-item {
|
|
326
|
+
flex-direction: row;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: space-between;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.checkbox-group {
|
|
333
|
+
display: flex;
|
|
334
|
+
flex-wrap: wrap;
|
|
335
|
+
gap: 12px;
|
|
336
|
+
font-size: 14px;
|
|
337
|
+
align-items: center;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.checkbox-group label {
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 4px;
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
}
|
|
346
|
+
|
|
282
347
|
#game-modal {
|
|
283
348
|
display: none;
|
|
284
349
|
position: fixed;
|
|
@@ -311,7 +376,6 @@
|
|
|
311
376
|
display: flex;
|
|
312
377
|
align-items: center;
|
|
313
378
|
justify-content: space-between;
|
|
314
|
-
font-family: 'Roboto', sans-serif;
|
|
315
379
|
}
|
|
316
380
|
|
|
317
381
|
.modal-title {
|
|
@@ -356,7 +420,6 @@
|
|
|
356
420
|
border: none;
|
|
357
421
|
}
|
|
358
422
|
|
|
359
|
-
/* Footer */
|
|
360
423
|
footer {
|
|
361
424
|
text-align: center;
|
|
362
425
|
padding: 16px;
|
|
@@ -367,10 +430,12 @@
|
|
|
367
430
|
</head>
|
|
368
431
|
<body>
|
|
369
432
|
|
|
370
|
-
<!-- Material Design Toolbar -->
|
|
371
433
|
<header>
|
|
372
|
-
<h1>StudyUp</h1>
|
|
373
|
-
<
|
|
434
|
+
<h1 onclick="logout()">StudyUp</h1>
|
|
435
|
+
<div class="header-actions">
|
|
436
|
+
<span id="current-user-badge" style="font-size: 13px; font-weight: 500;"></span>
|
|
437
|
+
<i class="material-icons" style="cursor:pointer;" onclick="logout()">exit_to_app</i>
|
|
438
|
+
</div>
|
|
374
439
|
</header>
|
|
375
440
|
|
|
376
441
|
<!-- VIEW 1: Login Screen -->
|
|
@@ -388,7 +453,7 @@
|
|
|
388
453
|
<label for="class-code-input">Class Code</label>
|
|
389
454
|
</div>
|
|
390
455
|
|
|
391
|
-
<div id="error-msg">Invalid class code
|
|
456
|
+
<div id="error-msg">Invalid class code or system currently locked.</div>
|
|
392
457
|
|
|
393
458
|
<button class="btn-raised" onclick="verifyCode()">Enter Code</button>
|
|
394
459
|
</div>
|
|
@@ -397,61 +462,47 @@
|
|
|
397
462
|
|
|
398
463
|
<!-- VIEW 2: Portal Dashboard -->
|
|
399
464
|
<div id="portal-view" class="view">
|
|
400
|
-
<div class="portal-container">
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
</div>
|
|
410
|
-
<div class="card-actions">
|
|
411
|
-
<button class="btn-flat" onclick="openGame('Sandboxels', 'sandboxels-package/sandboxels-1.13.2/index.html')">Launch App</button>
|
|
412
|
-
</div>
|
|
465
|
+
<div class="portal-container" id="student-games-grid"></div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- VIEW 3: Admin Management Panel View -->
|
|
469
|
+
<div id="admin-view" class="view">
|
|
470
|
+
<div style="max-width: 1000px; width: 100%; margin: 30px auto; padding: 0 16px; box-sizing: border-box; flex:1;">
|
|
471
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
472
|
+
<h2 style="margin: 0; color: var(--primary-color);">Admin Dashboard</h2>
|
|
473
|
+
<button class="btn-raised" style="width: auto; padding: 8px 16px;" onclick="switchView('portal-view')">← Back to Portal</button>
|
|
413
474
|
</div>
|
|
414
475
|
|
|
415
|
-
<!--
|
|
416
|
-
<div class="
|
|
417
|
-
<
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
<button class="btn-flat" onclick="openGame('HexGL', 'hexgl-package/HexGL-master/index.html')">Launch App</button>
|
|
476
|
+
<!-- Global Control Section -->
|
|
477
|
+
<div class="admin-section">
|
|
478
|
+
<h2>Global System Controls</h2>
|
|
479
|
+
<p style="font-size: 14px; color: var(--text-secondary);">Manage global availability for all student class codes.</p>
|
|
480
|
+
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;">
|
|
481
|
+
<div>
|
|
482
|
+
<strong>Maintenance / Lockdown Mode:</strong>
|
|
483
|
+
<div id="global-status-text" style="font-size: 13px; color: var(--success-color); font-weight: 500;">All student codes are currently ACTIVE.</div>
|
|
484
|
+
</div>
|
|
485
|
+
<button id="global-toggle-btn" class="btn-raised btn-warning" onclick="toggleGlobalBlock()">Temporarily Block All Codes</button>
|
|
426
486
|
</div>
|
|
427
487
|
</div>
|
|
428
488
|
|
|
429
|
-
<!--
|
|
430
|
-
<div class="
|
|
431
|
-
<
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
<div class="card-actions">
|
|
439
|
-
<button class="btn-flat" onclick="openGame('ClassiCube', 'classicube-package/index.html')">Launch App</button>
|
|
489
|
+
<!-- Create Code Section -->
|
|
490
|
+
<div class="admin-section">
|
|
491
|
+
<h2>Create New Class Code</h2>
|
|
492
|
+
<div style="display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end;">
|
|
493
|
+
<div class="input-container" style="flex: 1; min-width: 200px; margin-bottom: 0;">
|
|
494
|
+
<input type="text" id="new-code-input" placeholder="e.g. 654321" maxlength="6">
|
|
495
|
+
<label for="new-code-input">New Code (Digits)</label>
|
|
496
|
+
</div>
|
|
497
|
+
<button class="btn-raised" onclick="createNewCode()" style="height: 48px; width: auto; padding: 0 24px;">Create Code</button>
|
|
440
498
|
</div>
|
|
499
|
+
<div id="admin-error-msg" style="color: var(--error-color); font-size: 13px; margin-top: 8px; display: none;"></div>
|
|
441
500
|
</div>
|
|
442
501
|
|
|
443
|
-
<!--
|
|
444
|
-
<div class="
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
</div>
|
|
448
|
-
<div class="card-content">
|
|
449
|
-
<h2 class="portal-card-title">2048</h2>
|
|
450
|
-
<p class="card-text">A classic sliding tile puzzle game where you combine matching numbers to reach the 2048 tile. Great for math logic and strategic thinking.</p>
|
|
451
|
-
</div>
|
|
452
|
-
<div class="card-actions">
|
|
453
|
-
<button class="btn-flat" onclick="openGame('2048', '2048-package/index.html')">Launch App</button>
|
|
454
|
-
</div>
|
|
502
|
+
<!-- Manage Codes Section -->
|
|
503
|
+
<div class="admin-section">
|
|
504
|
+
<h2>Manage Existing Codes & Game Permissions</h2>
|
|
505
|
+
<div id="codes-management-list"></div>
|
|
455
506
|
</div>
|
|
456
507
|
</div>
|
|
457
508
|
</div>
|
|
@@ -481,27 +532,285 @@
|
|
|
481
532
|
</footer>
|
|
482
533
|
|
|
483
534
|
<script>
|
|
484
|
-
|
|
535
|
+
const AVAILABLE_GAMES = [
|
|
536
|
+
{ id: 'Sandboxels', name: 'Sandboxels', desc: 'A falling sand and physics sandbox game where you can mix elements and trigger chemical reactions.', url: 'sandboxels-package/sandboxels-1.13.2/index.html', color: '#00897b', icon: 'science' },
|
|
537
|
+
{ id: 'HexGL', name: 'HexGL', desc: 'A futuristic 3D WebGL racing game built with HTML5 where you pilot a high-speed craft through complex tracks.', url: 'hexgl-package/HexGL-master/index.html', color: '#e53935', icon: 'speed' },
|
|
538
|
+
{ id: 'ClassiCube', name: 'ClassiCube', desc: 'A lightweight multiplayer-compatible classic block building sandbox game for spatial visualization.', url: 'classicube-package/index.html', color: '#00acc1', icon: 'terrain' },
|
|
539
|
+
{ id: '2048', name: '2048', desc: 'A classic sliding tile puzzle game where you combine matching numbers to reach the 2048 tile.', url: '2048-package/index.html', color: '#ffb300', icon: 'grid_on' }
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
const ADMIN_APP = {
|
|
543
|
+
id: 'AdminPanel',
|
|
544
|
+
name: 'Admin Panel',
|
|
545
|
+
desc: 'Manage global locks, create or delete class codes, and control individual student app permissions.',
|
|
546
|
+
color: '#3f51b5',
|
|
547
|
+
icon: 'admin_panel_settings',
|
|
548
|
+
isCustomAction: true,
|
|
549
|
+
action: function() {
|
|
550
|
+
switchView('admin-view');
|
|
551
|
+
loadAdminDashboard();
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Fetch config from Express server backend (/api/config)
|
|
556
|
+
async function getConfig() {
|
|
557
|
+
try {
|
|
558
|
+
const response = await fetch('/api/config');
|
|
559
|
+
const data = await response.json();
|
|
560
|
+
return data;
|
|
561
|
+
} catch (err) {
|
|
562
|
+
console.error("Failed to fetch configuration from server:", err);
|
|
563
|
+
return {
|
|
564
|
+
globalBlock: false,
|
|
565
|
+
codes: { "123456": { games: ["Sandboxels", "HexGL", "ClassiCube", "2048"] } }
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Save config changes to Express server backend (/api/config)
|
|
571
|
+
async function saveConfig(configObj) {
|
|
572
|
+
try {
|
|
573
|
+
await fetch('/api/config', {
|
|
574
|
+
method: 'POST',
|
|
575
|
+
headers: { 'Content-Type': 'application/json' },
|
|
576
|
+
body: JSON.stringify(configObj)
|
|
577
|
+
});
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.error("Failed to save changes to server:", err);
|
|
580
|
+
alert("Error saving update to server.");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
485
584
|
document.getElementById('class-code-input').addEventListener('keypress', function (e) {
|
|
486
585
|
if (e.key === 'Enter') {
|
|
487
586
|
verifyCode();
|
|
488
587
|
}
|
|
489
588
|
});
|
|
490
589
|
|
|
491
|
-
function verifyCode() {
|
|
590
|
+
async function verifyCode() {
|
|
492
591
|
const inputVal = document.getElementById('class-code-input').value.trim();
|
|
493
592
|
const errorMsg = document.getElementById('error-msg');
|
|
593
|
+
const config = await getConfig();
|
|
594
|
+
|
|
595
|
+
if (config.globalBlock && inputVal !== '814212') {
|
|
596
|
+
errorMsg.innerText = "System is temporarily locked by administrator.";
|
|
597
|
+
errorMsg.style.display = 'block';
|
|
598
|
+
document.getElementById('class-code-input').focus();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
494
601
|
|
|
495
602
|
if (inputVal === '814212') {
|
|
496
603
|
errorMsg.style.display = 'none';
|
|
497
|
-
document.getElementById('
|
|
498
|
-
|
|
604
|
+
document.getElementById('class-code-input').value = '';
|
|
605
|
+
switchView('portal-view');
|
|
606
|
+
document.getElementById('current-user-badge').innerText = 'Master Admin Mode';
|
|
607
|
+
|
|
608
|
+
const allGameIds = AVAILABLE_GAMES.map(g => g.id);
|
|
609
|
+
loadStudentDashboard(allGameIds, true);
|
|
610
|
+
} else if (config.codes && config.codes[inputVal]) {
|
|
611
|
+
errorMsg.style.display = 'none';
|
|
612
|
+
document.getElementById('class-code-input').value = '';
|
|
613
|
+
switchView('portal-view');
|
|
614
|
+
document.getElementById('current-user-badge').innerText = `Class Code: ${inputVal}`;
|
|
615
|
+
loadStudentDashboard(config.codes[inputVal].games || [], false);
|
|
499
616
|
} else {
|
|
617
|
+
errorMsg.innerText = "Invalid class code. Please check with your teacher or instructor.";
|
|
500
618
|
errorMsg.style.display = 'block';
|
|
501
619
|
document.getElementById('class-code-input').focus();
|
|
502
620
|
}
|
|
503
621
|
}
|
|
504
622
|
|
|
623
|
+
function switchView(viewId) {
|
|
624
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
625
|
+
document.getElementById(viewId).classList.add('active');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function logout() {
|
|
629
|
+
closeGame();
|
|
630
|
+
switchView('login-view');
|
|
631
|
+
document.getElementById('current-user-badge').innerText = '';
|
|
632
|
+
document.getElementById('class-code-input').focus();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function loadStudentDashboard(allowedGameIds, isAdminUser) {
|
|
636
|
+
const grid = document.getElementById('student-games-grid');
|
|
637
|
+
grid.innerHTML = '';
|
|
638
|
+
|
|
639
|
+
const permittedGames = AVAILABLE_GAMES.filter(g => allowedGameIds.includes(g.id));
|
|
640
|
+
const displayCards = [];
|
|
641
|
+
if (isAdminUser) displayCards.push(ADMIN_APP);
|
|
642
|
+
permittedGames.forEach(g => displayCards.push(g));
|
|
643
|
+
|
|
644
|
+
if (displayCards.length === 0) {
|
|
645
|
+
grid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; padding: 40px; background: white; border-radius: 4px; box-shadow: var(--shadow-elevation-2);">
|
|
646
|
+
<i class="material-icons" style="font-size: 48px; color: var(--text-secondary);">block</i>
|
|
647
|
+
<h3 style="margin: 12px 0 4px 0;">No Apps Available</h3>
|
|
648
|
+
<p style="color: var(--text-secondary); margin: 0;">There are currently no apps assigned or permitted for this class code.</p>
|
|
649
|
+
</div>`;
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
displayCards.forEach(item => {
|
|
654
|
+
const card = document.createElement('div');
|
|
655
|
+
card.className = 'portal-card';
|
|
656
|
+
|
|
657
|
+
if (item.isCustomAction) {
|
|
658
|
+
card.innerHTML = `
|
|
659
|
+
<div class="card-media" style="background-color: ${item.color};">
|
|
660
|
+
<i class="material-icons">${item.icon}</i>
|
|
661
|
+
</div>
|
|
662
|
+
<div class="card-content">
|
|
663
|
+
<h2 class="portal-card-title">${item.name}</h2>
|
|
664
|
+
<p class="card-text">${item.desc}</p>
|
|
665
|
+
</div>
|
|
666
|
+
<div class="card-actions">
|
|
667
|
+
<button class="btn-flat" id="admin-panel-launch-btn">Open Panel</button>
|
|
668
|
+
</div>
|
|
669
|
+
`;
|
|
670
|
+
card.querySelector('#admin-panel-launch-btn').addEventListener('click', item.action);
|
|
671
|
+
} else {
|
|
672
|
+
card.innerHTML = `
|
|
673
|
+
<div class="card-media" style="background-color: ${item.color};">
|
|
674
|
+
<i class="material-icons">${item.icon}</i>
|
|
675
|
+
</div>
|
|
676
|
+
<div class="card-content">
|
|
677
|
+
<h2 class="portal-card-title">${item.name}</h2>
|
|
678
|
+
<p class="card-text">${item.desc}</p>
|
|
679
|
+
</div>
|
|
680
|
+
<div class="card-actions">
|
|
681
|
+
<button class="btn-flat" onclick="openGame('${item.name}', '${item.url}')">Launch App</button>
|
|
682
|
+
</div>
|
|
683
|
+
`;
|
|
684
|
+
}
|
|
685
|
+
grid.appendChild(card);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function loadAdminDashboard() {
|
|
690
|
+
const config = await getConfig();
|
|
691
|
+
const isBlocked = config.globalBlock;
|
|
692
|
+
|
|
693
|
+
const statusText = document.getElementById('global-status-text');
|
|
694
|
+
const toggleBtn = document.getElementById('global-toggle-btn');
|
|
695
|
+
if (isBlocked) {
|
|
696
|
+
statusText.innerText = "System is LOCKED. All student codes are temporarily blocked.";
|
|
697
|
+
statusText.style.color = "var(--error-color)";
|
|
698
|
+
toggleBtn.innerText = "Unblock All Codes";
|
|
699
|
+
toggleBtn.className = "btn-raised";
|
|
700
|
+
} else {
|
|
701
|
+
statusText.innerText = "All student codes are currently ACTIVE.";
|
|
702
|
+
statusText.style.color = "var(--success-color)";
|
|
703
|
+
toggleBtn.innerText = "Temporarily Block All Codes";
|
|
704
|
+
toggleBtn.className = "btn-raised btn-warning";
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const listContainer = document.getElementById('codes-management-list');
|
|
708
|
+
listContainer.innerHTML = '';
|
|
709
|
+
|
|
710
|
+
const studentCodes = Object.keys(config.codes || {});
|
|
711
|
+
|
|
712
|
+
if (studentCodes.length === 0) {
|
|
713
|
+
listContainer.innerHTML = `<p style="color: var(--text-secondary); font-style: italic;">No active student codes found. Create one above.</p>`;
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
studentCodes.forEach(code => {
|
|
718
|
+
const codeObj = config.codes[code];
|
|
719
|
+
const assignedGames = codeObj.games || [];
|
|
720
|
+
|
|
721
|
+
const item = document.createElement('div');
|
|
722
|
+
item.className = 'code-list-item';
|
|
723
|
+
|
|
724
|
+
let checkboxesHtml = AVAILABLE_GAMES.map(g => {
|
|
725
|
+
const checked = assignedGames.includes(g.id) ? 'checked' : '';
|
|
726
|
+
return `<label><input type="checkbox" value="${g.id}" ${checked} onchange="updateCodePermission('${code}', '${g.id}', this.checked)"> ${g.name}</label>`;
|
|
727
|
+
}).join('');
|
|
728
|
+
|
|
729
|
+
item.innerHTML = `
|
|
730
|
+
<div>
|
|
731
|
+
<strong style="font-size: 16px; color: var(--primary-color);">Code: ${code}</strong>
|
|
732
|
+
</div>
|
|
733
|
+
<div class="checkbox-group">
|
|
734
|
+
${checkboxesHtml}
|
|
735
|
+
</div>
|
|
736
|
+
<div style="display: flex; gap: 8px;">
|
|
737
|
+
<button class="btn-raised" style="padding: 6px 12px; font-size: 12px;" onclick="allowAllForCode('${code}')">Allow All</button>
|
|
738
|
+
<button class="btn-raised btn-danger" style="padding: 6px 12px; font-size: 12px;" onclick="deleteCode('${code}')">Delete</button>
|
|
739
|
+
</div>
|
|
740
|
+
`;
|
|
741
|
+
listContainer.appendChild(item);
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function toggleGlobalBlock() {
|
|
746
|
+
const config = await getConfig();
|
|
747
|
+
config.globalBlock = !config.globalBlock;
|
|
748
|
+
await saveConfig(config);
|
|
749
|
+
loadAdminDashboard();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function createNewCode() {
|
|
753
|
+
const input = document.getElementById('new-code-input');
|
|
754
|
+
const err = document.getElementById('admin-error-msg');
|
|
755
|
+
const val = input.value.trim();
|
|
756
|
+
|
|
757
|
+
if (!/^\d{6}$/.test(val)) {
|
|
758
|
+
err.innerText = "Code must be exactly 6 digits.";
|
|
759
|
+
err.style.display = 'block';
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (val === '814212') {
|
|
763
|
+
err.innerText = "814212 is reserved as the Master Admin code.";
|
|
764
|
+
err.style.display = 'block';
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const config = await getConfig();
|
|
769
|
+
if (!config.codes) config.codes = {};
|
|
770
|
+
|
|
771
|
+
if (config.codes[val]) {
|
|
772
|
+
err.innerText = "This code already exists.";
|
|
773
|
+
err.style.display = 'block';
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
err.style.display = 'none';
|
|
778
|
+
config.codes[val] = { games: AVAILABLE_GAMES.map(g => g.id) };
|
|
779
|
+
await saveConfig(config);
|
|
780
|
+
input.value = '';
|
|
781
|
+
loadAdminDashboard();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async function deleteCode(code) {
|
|
785
|
+
if (confirm(`Are you sure you want to delete code ${code}?`)) {
|
|
786
|
+
const config = await getConfig();
|
|
787
|
+
delete config.codes[code];
|
|
788
|
+
await saveConfig(config);
|
|
789
|
+
loadAdminDashboard();
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function updateCodePermission(code, gameId, isChecked) {
|
|
794
|
+
const config = await getConfig();
|
|
795
|
+
if (!config.codes[code].games) config.codes[code].games = [];
|
|
796
|
+
|
|
797
|
+
if (isChecked) {
|
|
798
|
+
if (!config.codes[code].games.includes(gameId)) {
|
|
799
|
+
config.codes[code].games.push(gameId);
|
|
800
|
+
}
|
|
801
|
+
} else {
|
|
802
|
+
config.codes[code].games = config.codes[code].games.filter(id => id !== gameId);
|
|
803
|
+
}
|
|
804
|
+
await saveConfig(config);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async function allowAllForCode(code) {
|
|
808
|
+
const config = await getConfig();
|
|
809
|
+
config.codes[code].games = AVAILABLE_GAMES.map(g => g.id);
|
|
810
|
+
await saveConfig(config);
|
|
811
|
+
loadAdminDashboard();
|
|
812
|
+
}
|
|
813
|
+
|
|
505
814
|
function openGame(gameName, gameUrl) {
|
|
506
815
|
document.getElementById('modal-game-title').innerText = gameName;
|
|
507
816
|
document.getElementById('game-iframe').src = gameUrl;
|
|
@@ -509,7 +818,6 @@
|
|
|
509
818
|
}
|
|
510
819
|
|
|
511
820
|
function closeGame() {
|
|
512
|
-
// Exit fullscreen if active when closing
|
|
513
821
|
if (document.fullscreenElement) {
|
|
514
822
|
document.exitFullscreen();
|
|
515
823
|
}
|
|
@@ -532,7 +840,6 @@
|
|
|
532
840
|
}
|
|
533
841
|
}
|
|
534
842
|
|
|
535
|
-
// Keep fullscreen icon updated if user exits via keyboard (Esc)
|
|
536
843
|
document.addEventListener('fullscreenchange', () => {
|
|
537
844
|
const icon = document.getElementById('fullscreen-icon');
|
|
538
845
|
if (!document.fullscreenElement) {
|
package/package.json
CHANGED
package/server.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const app = express();
|
|
6
|
+
const PORT = process.env.PORT || 3000;
|
|
7
|
+
const DATA_FILE = path.join(__dirname, 'data.json');
|
|
8
|
+
|
|
9
|
+
app.use(express.json());
|
|
10
|
+
// Serve static files from the same root folder where server.js lives
|
|
11
|
+
app.use(express.static(__dirname));
|
|
12
|
+
|
|
13
|
+
// Helper to read data
|
|
14
|
+
function getData() {
|
|
15
|
+
if (!fs.existsSync(DATA_FILE)) {
|
|
16
|
+
const defaultData = {
|
|
17
|
+
globalBlock: false,
|
|
18
|
+
codes: {
|
|
19
|
+
"123456": { games: ["Sandboxels", "HexGL", "ClassiCube", "2048"] }
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
fs.writeFileSync(DATA_FILE, JSON.stringify(defaultData, null, 2));
|
|
23
|
+
return defaultData;
|
|
24
|
+
}
|
|
25
|
+
return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Helper to write data
|
|
29
|
+
function saveData(data) {
|
|
30
|
+
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// API: Get current config
|
|
34
|
+
app.get('/api/config', (req, res) => {
|
|
35
|
+
res.json(getData());
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// API: Save/Update config (Admin actions)
|
|
39
|
+
app.post('/api/config', (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
saveData(req.body);
|
|
42
|
+
res.json({ success: true });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
res.status(500).json({ success: false, error: err.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
app.listen(PORT, () => {
|
|
49
|
+
console.log(`StudyUp server running on port ${PORT}`);
|
|
50
|
+
});
|