studyup 1.0.3 → 1.0.4
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 +192 -97
- package/package.json +1 -1
- package/server.js +0 -50
package/index.html
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
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
|
-
|
|
11
10
|
<style>
|
|
12
11
|
:root {
|
|
13
12
|
--primary-color: #3f51b5; /* Indigo */
|
|
@@ -23,7 +22,6 @@
|
|
|
23
22
|
--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);
|
|
24
23
|
--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);
|
|
25
24
|
}
|
|
26
|
-
|
|
27
25
|
body {
|
|
28
26
|
font-family: 'Roboto', sans-serif;
|
|
29
27
|
background-color: var(--background-color);
|
|
@@ -35,7 +33,6 @@
|
|
|
35
33
|
min-height: 100vh;
|
|
36
34
|
justify-content: space-between;
|
|
37
35
|
}
|
|
38
|
-
|
|
39
36
|
header {
|
|
40
37
|
background-color: var(--primary-color);
|
|
41
38
|
color: white;
|
|
@@ -45,7 +42,6 @@
|
|
|
45
42
|
align-items: center;
|
|
46
43
|
justify-content: space-between;
|
|
47
44
|
}
|
|
48
|
-
|
|
49
45
|
header h1 {
|
|
50
46
|
margin: 0;
|
|
51
47
|
font-size: 20px;
|
|
@@ -53,13 +49,11 @@
|
|
|
53
49
|
letter-spacing: 0.5px;
|
|
54
50
|
cursor: pointer;
|
|
55
51
|
}
|
|
56
|
-
|
|
57
52
|
.header-actions {
|
|
58
53
|
display: flex;
|
|
59
54
|
gap: 12px;
|
|
60
55
|
align-items: center;
|
|
61
56
|
}
|
|
62
|
-
|
|
63
57
|
.view {
|
|
64
58
|
display: none;
|
|
65
59
|
flex: 1;
|
|
@@ -68,7 +62,6 @@
|
|
|
68
62
|
.view.active {
|
|
69
63
|
display: flex;
|
|
70
64
|
}
|
|
71
|
-
|
|
72
65
|
.container {
|
|
73
66
|
max-width: 420px;
|
|
74
67
|
width: 100%;
|
|
@@ -80,7 +73,6 @@
|
|
|
80
73
|
align-items: center;
|
|
81
74
|
justify-content: center;
|
|
82
75
|
}
|
|
83
|
-
|
|
84
76
|
.card {
|
|
85
77
|
background-color: var(--card-background);
|
|
86
78
|
border-radius: 2px;
|
|
@@ -93,7 +85,6 @@
|
|
|
93
85
|
width: 100%;
|
|
94
86
|
box-sizing: border-box;
|
|
95
87
|
}
|
|
96
|
-
|
|
97
88
|
.card-icon-wrapper {
|
|
98
89
|
background-color: rgba(63, 81, 181, 0.1);
|
|
99
90
|
color: var(--primary-color);
|
|
@@ -105,32 +96,27 @@
|
|
|
105
96
|
justify-content: center;
|
|
106
97
|
margin-bottom: 20px;
|
|
107
98
|
}
|
|
108
|
-
|
|
109
99
|
.card-icon-wrapper i {
|
|
110
100
|
font-size: 32px;
|
|
111
101
|
}
|
|
112
|
-
|
|
113
102
|
.card-title {
|
|
114
103
|
font-size: 24px;
|
|
115
104
|
font-weight: 500;
|
|
116
105
|
margin: 0 0 8px 0;
|
|
117
106
|
color: var(--text-primary);
|
|
118
107
|
}
|
|
119
|
-
|
|
120
108
|
.card-subtitle {
|
|
121
109
|
font-size: 14px;
|
|
122
110
|
color: var(--text-secondary);
|
|
123
111
|
margin: 0 0 24px 0;
|
|
124
112
|
line-height: 1.4;
|
|
125
113
|
}
|
|
126
|
-
|
|
127
114
|
.input-container {
|
|
128
115
|
position: relative;
|
|
129
116
|
margin-bottom: 24px;
|
|
130
117
|
width: 100%;
|
|
131
118
|
text-align: left;
|
|
132
119
|
}
|
|
133
|
-
|
|
134
120
|
.input-container input {
|
|
135
121
|
width: 100%;
|
|
136
122
|
padding: 12px;
|
|
@@ -143,13 +129,11 @@
|
|
|
143
129
|
background: transparent;
|
|
144
130
|
transition: border-color 0.2s;
|
|
145
131
|
}
|
|
146
|
-
|
|
147
132
|
.input-container input:focus {
|
|
148
133
|
border-color: var(--primary-color);
|
|
149
134
|
border-width: 2px;
|
|
150
135
|
padding: 11px;
|
|
151
136
|
}
|
|
152
|
-
|
|
153
137
|
.input-container label {
|
|
154
138
|
position: absolute;
|
|
155
139
|
left: 12px;
|
|
@@ -161,7 +145,6 @@
|
|
|
161
145
|
background: white;
|
|
162
146
|
padding: 0 4px;
|
|
163
147
|
}
|
|
164
|
-
|
|
165
148
|
.input-container input:focus ~ label,
|
|
166
149
|
.input-container input:not(:placeholder-shown) ~ label {
|
|
167
150
|
top: -10px;
|
|
@@ -169,7 +152,6 @@
|
|
|
169
152
|
color: var(--primary-color);
|
|
170
153
|
font-weight: 500;
|
|
171
154
|
}
|
|
172
|
-
|
|
173
155
|
#error-msg {
|
|
174
156
|
color: var(--error-color);
|
|
175
157
|
font-size: 13px;
|
|
@@ -177,7 +159,6 @@
|
|
|
177
159
|
display: none;
|
|
178
160
|
font-weight: 500;
|
|
179
161
|
}
|
|
180
|
-
|
|
181
162
|
.btn-raised {
|
|
182
163
|
background-color: var(--primary-color);
|
|
183
164
|
color: white;
|
|
@@ -194,26 +175,22 @@
|
|
|
194
175
|
transition: background-color 0.2s, box-shadow 0.2s;
|
|
195
176
|
letter-spacing: 0.5px;
|
|
196
177
|
}
|
|
197
|
-
|
|
198
178
|
.btn-raised:hover {
|
|
199
179
|
background-color: var(--primary-dark);
|
|
200
180
|
box-shadow: var(--shadow-elevation-4);
|
|
201
181
|
}
|
|
202
|
-
|
|
203
182
|
.btn-danger {
|
|
204
183
|
background-color: var(--error-color);
|
|
205
184
|
}
|
|
206
185
|
.btn-danger:hover {
|
|
207
186
|
background-color: #b71c1c;
|
|
208
187
|
}
|
|
209
|
-
|
|
210
188
|
.btn-warning {
|
|
211
189
|
background-color: var(--warning-color);
|
|
212
190
|
}
|
|
213
191
|
.btn-warning:hover {
|
|
214
192
|
background-color: #e65100;
|
|
215
193
|
}
|
|
216
|
-
|
|
217
194
|
.portal-container {
|
|
218
195
|
max-width: 1000px;
|
|
219
196
|
width: 100%;
|
|
@@ -225,7 +202,6 @@
|
|
|
225
202
|
gap: 24px;
|
|
226
203
|
flex: 1;
|
|
227
204
|
}
|
|
228
|
-
|
|
229
205
|
.portal-card {
|
|
230
206
|
background-color: var(--card-background);
|
|
231
207
|
border-radius: 2px;
|
|
@@ -235,11 +211,9 @@
|
|
|
235
211
|
flex-direction: column;
|
|
236
212
|
overflow: hidden;
|
|
237
213
|
}
|
|
238
|
-
|
|
239
214
|
.portal-card:hover {
|
|
240
215
|
box-shadow: var(--shadow-elevation-4);
|
|
241
216
|
}
|
|
242
|
-
|
|
243
217
|
.card-media {
|
|
244
218
|
height: 120px;
|
|
245
219
|
display: flex;
|
|
@@ -247,33 +221,28 @@
|
|
|
247
221
|
justify-content: center;
|
|
248
222
|
color: white;
|
|
249
223
|
}
|
|
250
|
-
|
|
251
224
|
.card-content {
|
|
252
225
|
padding: 24px;
|
|
253
226
|
flex: 1;
|
|
254
227
|
}
|
|
255
|
-
|
|
256
228
|
.portal-card-title {
|
|
257
229
|
font-size: 20px;
|
|
258
230
|
font-weight: 500;
|
|
259
231
|
margin: 0 0 8px 0;
|
|
260
232
|
color: var(--text-primary);
|
|
261
233
|
}
|
|
262
|
-
|
|
263
234
|
.card-text {
|
|
264
235
|
font-size: 14px;
|
|
265
236
|
color: var(--text-secondary);
|
|
266
237
|
line-height: 1.4;
|
|
267
238
|
margin: 0;
|
|
268
239
|
}
|
|
269
|
-
|
|
270
240
|
.card-actions {
|
|
271
241
|
padding: 8px 16px 16px 16px;
|
|
272
242
|
display: flex;
|
|
273
243
|
justify-content: flex-end;
|
|
274
244
|
gap: 8px;
|
|
275
245
|
}
|
|
276
|
-
|
|
277
246
|
.btn-flat {
|
|
278
247
|
background: transparent;
|
|
279
248
|
border: none;
|
|
@@ -289,11 +258,9 @@
|
|
|
289
258
|
transition: background-color 0.2s;
|
|
290
259
|
letter-spacing: 0.5px;
|
|
291
260
|
}
|
|
292
|
-
|
|
293
261
|
.btn-flat:hover {
|
|
294
262
|
background-color: rgba(63, 81, 181, 0.08);
|
|
295
263
|
}
|
|
296
|
-
|
|
297
264
|
.admin-section {
|
|
298
265
|
background: white;
|
|
299
266
|
padding: 24px;
|
|
@@ -301,7 +268,6 @@
|
|
|
301
268
|
box-shadow: var(--shadow-elevation-2);
|
|
302
269
|
margin-bottom: 24px;
|
|
303
270
|
}
|
|
304
|
-
|
|
305
271
|
.admin-section h2 {
|
|
306
272
|
margin-top: 0;
|
|
307
273
|
font-size: 18px;
|
|
@@ -309,7 +275,6 @@
|
|
|
309
275
|
border-bottom: 2px solid var(--background-color);
|
|
310
276
|
padding-bottom: 8px;
|
|
311
277
|
}
|
|
312
|
-
|
|
313
278
|
.code-list-item {
|
|
314
279
|
display: flex;
|
|
315
280
|
flex-direction: column;
|
|
@@ -320,7 +285,6 @@
|
|
|
320
285
|
border: 1px solid #e0e0e0;
|
|
321
286
|
gap: 12px;
|
|
322
287
|
}
|
|
323
|
-
|
|
324
288
|
@media(min-width: 600px) {
|
|
325
289
|
.code-list-item {
|
|
326
290
|
flex-direction: row;
|
|
@@ -328,7 +292,6 @@
|
|
|
328
292
|
justify-content: space-between;
|
|
329
293
|
}
|
|
330
294
|
}
|
|
331
|
-
|
|
332
295
|
.checkbox-group {
|
|
333
296
|
display: flex;
|
|
334
297
|
flex-wrap: wrap;
|
|
@@ -336,14 +299,12 @@
|
|
|
336
299
|
font-size: 14px;
|
|
337
300
|
align-items: center;
|
|
338
301
|
}
|
|
339
|
-
|
|
340
302
|
.checkbox-group label {
|
|
341
303
|
display: flex;
|
|
342
304
|
align-items: center;
|
|
343
305
|
gap: 4px;
|
|
344
306
|
cursor: pointer;
|
|
345
307
|
}
|
|
346
|
-
|
|
347
308
|
#game-modal {
|
|
348
309
|
display: none;
|
|
349
310
|
position: fixed;
|
|
@@ -357,7 +318,6 @@
|
|
|
357
318
|
align-items: center;
|
|
358
319
|
justify-content: center;
|
|
359
320
|
}
|
|
360
|
-
|
|
361
321
|
.modal-content {
|
|
362
322
|
width: 90vw;
|
|
363
323
|
height: 85vh;
|
|
@@ -368,7 +328,6 @@
|
|
|
368
328
|
flex-direction: column;
|
|
369
329
|
overflow: hidden;
|
|
370
330
|
}
|
|
371
|
-
|
|
372
331
|
.modal-header {
|
|
373
332
|
background-color: #212121;
|
|
374
333
|
color: white;
|
|
@@ -377,19 +336,16 @@
|
|
|
377
336
|
align-items: center;
|
|
378
337
|
justify-content: space-between;
|
|
379
338
|
}
|
|
380
|
-
|
|
381
339
|
.modal-title {
|
|
382
340
|
margin: 0;
|
|
383
341
|
font-size: 16px;
|
|
384
342
|
font-weight: 500;
|
|
385
343
|
}
|
|
386
|
-
|
|
387
344
|
.modal-header-actions {
|
|
388
345
|
display: flex;
|
|
389
346
|
align-items: center;
|
|
390
347
|
gap: 8px;
|
|
391
348
|
}
|
|
392
|
-
|
|
393
349
|
.modal-btn {
|
|
394
350
|
background: transparent;
|
|
395
351
|
border: none;
|
|
@@ -403,33 +359,56 @@
|
|
|
403
359
|
border-radius: 50%;
|
|
404
360
|
transition: background-color 0.2s;
|
|
405
361
|
}
|
|
406
|
-
|
|
407
362
|
.modal-btn:hover {
|
|
408
363
|
background-color: rgba(255, 255, 255, 0.1);
|
|
409
364
|
}
|
|
410
|
-
|
|
411
365
|
.modal-body {
|
|
412
366
|
flex: 1;
|
|
413
367
|
width: 100%;
|
|
414
368
|
background: #fff;
|
|
415
369
|
}
|
|
416
|
-
|
|
417
370
|
.modal-body iframe {
|
|
418
371
|
width: 100%;
|
|
419
372
|
height: 100%;
|
|
420
373
|
border: none;
|
|
421
374
|
}
|
|
422
|
-
|
|
423
375
|
footer {
|
|
424
376
|
text-align: center;
|
|
425
377
|
padding: 16px;
|
|
426
378
|
color: var(--text-secondary);
|
|
427
379
|
font-size: 12px;
|
|
428
380
|
}
|
|
381
|
+
|
|
382
|
+
/* Custom modal notification box */
|
|
383
|
+
#notification-modal {
|
|
384
|
+
display: none;
|
|
385
|
+
position: fixed;
|
|
386
|
+
top: 0;
|
|
387
|
+
left: 0;
|
|
388
|
+
width: 100vw;
|
|
389
|
+
height: 100vh;
|
|
390
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
391
|
+
z-index: 2000;
|
|
392
|
+
align-items: center;
|
|
393
|
+
justify-content: center;
|
|
394
|
+
}
|
|
395
|
+
.notification-box {
|
|
396
|
+
background: white;
|
|
397
|
+
padding: 24px;
|
|
398
|
+
border-radius: 4px;
|
|
399
|
+
box-shadow: var(--shadow-elevation-4);
|
|
400
|
+
max-width: 350px;
|
|
401
|
+
width: 90%;
|
|
402
|
+
text-align: center;
|
|
403
|
+
}
|
|
404
|
+
.notification-message {
|
|
405
|
+
margin-bottom: 20px;
|
|
406
|
+
font-size: 15px;
|
|
407
|
+
color: var(--text-primary);
|
|
408
|
+
}
|
|
429
409
|
</style>
|
|
430
410
|
</head>
|
|
431
411
|
<body>
|
|
432
|
-
|
|
433
412
|
<header>
|
|
434
413
|
<h1 onclick="logout()">StudyUp</h1>
|
|
435
414
|
<div class="header-actions">
|
|
@@ -454,7 +433,6 @@
|
|
|
454
433
|
</div>
|
|
455
434
|
|
|
456
435
|
<div id="error-msg">Invalid class code or system currently locked.</div>
|
|
457
|
-
|
|
458
436
|
<button class="btn-raised" onclick="verifyCode()">Enter Code</button>
|
|
459
437
|
</div>
|
|
460
438
|
</main>
|
|
@@ -527,16 +505,24 @@
|
|
|
527
505
|
</div>
|
|
528
506
|
</div>
|
|
529
507
|
|
|
508
|
+
<!-- Custom Notification Modal -->
|
|
509
|
+
<div id="notification-modal">
|
|
510
|
+
<div class="notification-box">
|
|
511
|
+
<div id="notification-text" class="notification-message">Message here</div>
|
|
512
|
+
<button class="btn-raised" onclick="closeNotification()">OK</button>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
530
516
|
<footer>
|
|
531
517
|
StudyUp • Student Resource Portal • 2026
|
|
532
518
|
</footer>
|
|
533
519
|
|
|
534
520
|
<script>
|
|
535
521
|
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: '
|
|
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
|
|
538
|
-
{ id: 'ClassiCube', name: 'ClassiCube', desc: 'A lightweight multiplayer-compatible classic block building sandbox game for spatial visualization.', url: 'classicube
|
|
539
|
-
{ id: '2048', name: '2048', desc: 'A classic sliding tile puzzle game where you combine matching numbers to reach the 2048 tile.', url: '
|
|
522
|
+
{ id: 'Sandboxels', name: 'Sandboxels', desc: 'A falling sand and physics sandbox game where you can mix elements and trigger chemical reactions.', url: 'https://cdn.jsdelivr.net/gh/RuneScape/sandboxels@master/index.html', color: '#00897b', icon: 'science' },
|
|
523
|
+
{ 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: 'https://hexgl.bkcore.com/play/', color: '#e53935', icon: 'speed' },
|
|
524
|
+
{ id: 'ClassiCube', name: 'ClassiCube', desc: 'A lightweight multiplayer-compatible classic block building sandbox game for spatial visualization.', url: 'https://www.classicube.net/play/', color: '#00acc1', icon: 'terrain' },
|
|
525
|
+
{ id: '2048', name: '2048', desc: 'A classic sliding tile puzzle game where you combine matching numbers to reach the 2048 tile.', url: 'https://play2048.co/', color: '#ffb300', icon: 'grid_on' }
|
|
540
526
|
];
|
|
541
527
|
|
|
542
528
|
const ADMIN_APP = {
|
|
@@ -552,45 +538,151 @@
|
|
|
552
538
|
}
|
|
553
539
|
};
|
|
554
540
|
|
|
555
|
-
|
|
556
|
-
|
|
541
|
+
const STORAGE_KEY = 'studyup_portal_config_v2';
|
|
542
|
+
const BROADCAST_CHANNEL_NAME = 'studyup_sync_channel';
|
|
543
|
+
|
|
544
|
+
// Session state tracking for current browser tab/window
|
|
545
|
+
let currentSession = {
|
|
546
|
+
isLoggedIn: false,
|
|
547
|
+
code: null, // null, '814212', or student class code
|
|
548
|
+
isAdmin: false
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
let broadcastChannel = null;
|
|
552
|
+
try {
|
|
553
|
+
if (window.BroadcastChannel) {
|
|
554
|
+
broadcastChannel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
|
|
555
|
+
broadcastChannel.onmessage = (event) => {
|
|
556
|
+
handleSyncEvent(event.data);
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
} catch (e) {
|
|
560
|
+
console.warn("BroadcastChannel not supported in this environment.", e);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Listen to storage events across tabs/devices
|
|
564
|
+
window.addEventListener('storage', (event) => {
|
|
565
|
+
if (event.key === STORAGE_KEY || event.key === STORAGE_KEY + '_timestamp') {
|
|
566
|
+
const config = getConfig();
|
|
567
|
+
evaluateCurrentSession(config);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Periodic polling interval to guarantee instant updates across all open tabs/devices
|
|
572
|
+
setInterval(() => {
|
|
573
|
+
const config = getConfig();
|
|
574
|
+
evaluateCurrentSession(config);
|
|
575
|
+
}, 1000);
|
|
576
|
+
|
|
577
|
+
function showNotification(msg) {
|
|
578
|
+
document.getElementById('notification-text').innerText = msg;
|
|
579
|
+
document.getElementById('notification-modal').style.display = 'flex';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function closeNotification() {
|
|
583
|
+
document.getElementById('notification-modal').style.display = 'none';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function getConfig() {
|
|
557
587
|
try {
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
588
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
589
|
+
if (stored) {
|
|
590
|
+
return JSON.parse(stored);
|
|
591
|
+
}
|
|
561
592
|
} catch (err) {
|
|
562
|
-
console.error("Failed to
|
|
563
|
-
return {
|
|
564
|
-
globalBlock: false,
|
|
565
|
-
codes: { "123456": { games: ["Sandboxels", "HexGL", "ClassiCube", "2048"] } }
|
|
566
|
-
};
|
|
593
|
+
console.error("Failed to read from localStorage:", err);
|
|
567
594
|
}
|
|
595
|
+
|
|
596
|
+
const defaultConfig = {
|
|
597
|
+
globalBlock: false,
|
|
598
|
+
timestamp: Date.now(),
|
|
599
|
+
codes: {
|
|
600
|
+
"123456": { games: ["Sandboxels", "HexGL", "ClassiCube", "2048"] }
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
saveConfig(defaultConfig, false);
|
|
604
|
+
return defaultConfig;
|
|
568
605
|
}
|
|
569
606
|
|
|
570
|
-
|
|
571
|
-
async function saveConfig(configObj) {
|
|
607
|
+
function saveConfig(configObj, broadcast = true) {
|
|
572
608
|
try {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
609
|
+
configObj.timestamp = Date.now();
|
|
610
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(configObj));
|
|
611
|
+
localStorage.setItem(STORAGE_KEY + '_timestamp', configObj.timestamp.toString());
|
|
612
|
+
|
|
613
|
+
if (broadcast && broadcastChannel) {
|
|
614
|
+
broadcastChannel.postMessage({ type: 'CONFIG_UPDATED', config: configObj });
|
|
615
|
+
}
|
|
578
616
|
} catch (err) {
|
|
579
|
-
console.error("Failed to save
|
|
580
|
-
|
|
617
|
+
console.error("Failed to save to localStorage:", err);
|
|
618
|
+
showNotification("Error saving update to local storage.");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function handleSyncEvent(data) {
|
|
623
|
+
if (data && data.type === 'CONFIG_UPDATED') {
|
|
624
|
+
evaluateCurrentSession(data.config);
|
|
581
625
|
}
|
|
582
626
|
}
|
|
583
627
|
|
|
628
|
+
// Evaluate whether the logged-in user is still valid based on latest config
|
|
629
|
+
function evaluateCurrentSession(config) {
|
|
630
|
+
const loginViewActive = document.getElementById('login-view').classList.contains('active');
|
|
631
|
+
if (!currentSession.isLoggedIn || loginViewActive) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Master Admin is never automatically kicked out unless global lock happens and they choose to log out,
|
|
636
|
+
// but let's check if regular student code is valid or globally blocked.
|
|
637
|
+
if (config.globalBlock && !currentSession.isAdmin) {
|
|
638
|
+
forceLogout("System is temporarily locked by administrator.");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (currentSession.isAdmin) {
|
|
643
|
+
// Admin dashboard might be open, update dashboard if view is active
|
|
644
|
+
if (document.getElementById('admin-view').classList.contains('active')) {
|
|
645
|
+
loadAdminDashboard();
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Student session validation
|
|
651
|
+
const code = currentSession.code;
|
|
652
|
+
if (!config.codes || !config.codes[code]) {
|
|
653
|
+
forceLogout("Your class code has been modified or revoked.");
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Update student dashboard games dynamically in real time
|
|
658
|
+
const allowedGames = config.codes[code].games || [];
|
|
659
|
+
if (document.getElementById('portal-view').classList.contains('active')) {
|
|
660
|
+
loadStudentDashboard(allowedGames, false);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function forceLogout(reason) {
|
|
665
|
+
currentSession = { isLoggedIn: false, code: null, isAdmin: false };
|
|
666
|
+
closeGame();
|
|
667
|
+
switchView('login-view');
|
|
668
|
+
document.getElementById('current-user-badge').innerText = '';
|
|
669
|
+
const errorMsg = document.getElementById('error-msg');
|
|
670
|
+
errorMsg.innerText = reason;
|
|
671
|
+
errorMsg.style.display = 'block';
|
|
672
|
+
document.getElementById('class-code-input').value = '';
|
|
673
|
+
document.getElementById('class-code-input').focus();
|
|
674
|
+
}
|
|
675
|
+
|
|
584
676
|
document.getElementById('class-code-input').addEventListener('keypress', function (e) {
|
|
585
677
|
if (e.key === 'Enter') {
|
|
586
678
|
verifyCode();
|
|
587
679
|
}
|
|
588
680
|
});
|
|
589
681
|
|
|
590
|
-
|
|
682
|
+
function verifyCode() {
|
|
591
683
|
const inputVal = document.getElementById('class-code-input').value.trim();
|
|
592
684
|
const errorMsg = document.getElementById('error-msg');
|
|
593
|
-
const config =
|
|
685
|
+
const config = getConfig();
|
|
594
686
|
|
|
595
687
|
if (config.globalBlock && inputVal !== '814212') {
|
|
596
688
|
errorMsg.innerText = "System is temporarily locked by administrator.";
|
|
@@ -602,14 +694,16 @@
|
|
|
602
694
|
if (inputVal === '814212') {
|
|
603
695
|
errorMsg.style.display = 'none';
|
|
604
696
|
document.getElementById('class-code-input').value = '';
|
|
697
|
+
currentSession = { isLoggedIn: true, code: '814212', isAdmin: true };
|
|
605
698
|
switchView('portal-view');
|
|
606
699
|
document.getElementById('current-user-badge').innerText = 'Master Admin Mode';
|
|
607
|
-
|
|
700
|
+
|
|
608
701
|
const allGameIds = AVAILABLE_GAMES.map(g => g.id);
|
|
609
702
|
loadStudentDashboard(allGameIds, true);
|
|
610
703
|
} else if (config.codes && config.codes[inputVal]) {
|
|
611
704
|
errorMsg.style.display = 'none';
|
|
612
705
|
document.getElementById('class-code-input').value = '';
|
|
706
|
+
currentSession = { isLoggedIn: true, code: inputVal, isAdmin: false };
|
|
613
707
|
switchView('portal-view');
|
|
614
708
|
document.getElementById('current-user-badge').innerText = `Class Code: ${inputVal}`;
|
|
615
709
|
loadStudentDashboard(config.codes[inputVal].games || [], false);
|
|
@@ -626,6 +720,7 @@
|
|
|
626
720
|
}
|
|
627
721
|
|
|
628
722
|
function logout() {
|
|
723
|
+
currentSession = { isLoggedIn: false, code: null, isAdmin: false };
|
|
629
724
|
closeGame();
|
|
630
725
|
switchView('login-view');
|
|
631
726
|
document.getElementById('current-user-badge').innerText = '';
|
|
@@ -653,7 +748,7 @@
|
|
|
653
748
|
displayCards.forEach(item => {
|
|
654
749
|
const card = document.createElement('div');
|
|
655
750
|
card.className = 'portal-card';
|
|
656
|
-
|
|
751
|
+
|
|
657
752
|
if (item.isCustomAction) {
|
|
658
753
|
card.innerHTML = `
|
|
659
754
|
<div class="card-media" style="background-color: ${item.color};">
|
|
@@ -686,10 +781,10 @@
|
|
|
686
781
|
});
|
|
687
782
|
}
|
|
688
783
|
|
|
689
|
-
|
|
690
|
-
const config =
|
|
784
|
+
function loadAdminDashboard() {
|
|
785
|
+
const config = getConfig();
|
|
691
786
|
const isBlocked = config.globalBlock;
|
|
692
|
-
|
|
787
|
+
|
|
693
788
|
const statusText = document.getElementById('global-status-text');
|
|
694
789
|
const toggleBtn = document.getElementById('global-toggle-btn');
|
|
695
790
|
if (isBlocked) {
|
|
@@ -720,7 +815,7 @@
|
|
|
720
815
|
|
|
721
816
|
const item = document.createElement('div');
|
|
722
817
|
item.className = 'code-list-item';
|
|
723
|
-
|
|
818
|
+
|
|
724
819
|
let checkboxesHtml = AVAILABLE_GAMES.map(g => {
|
|
725
820
|
const checked = assignedGames.includes(g.id) ? 'checked' : '';
|
|
726
821
|
return `<label><input type="checkbox" value="${g.id}" ${checked} onchange="updateCodePermission('${code}', '${g.id}', this.checked)"> ${g.name}</label>`;
|
|
@@ -742,14 +837,14 @@
|
|
|
742
837
|
});
|
|
743
838
|
}
|
|
744
839
|
|
|
745
|
-
|
|
746
|
-
const config =
|
|
840
|
+
function toggleGlobalBlock() {
|
|
841
|
+
const config = getConfig();
|
|
747
842
|
config.globalBlock = !config.globalBlock;
|
|
748
|
-
|
|
843
|
+
saveConfig(config);
|
|
749
844
|
loadAdminDashboard();
|
|
750
845
|
}
|
|
751
846
|
|
|
752
|
-
|
|
847
|
+
function createNewCode() {
|
|
753
848
|
const input = document.getElementById('new-code-input');
|
|
754
849
|
const err = document.getElementById('admin-error-msg');
|
|
755
850
|
const val = input.value.trim();
|
|
@@ -765,7 +860,7 @@
|
|
|
765
860
|
return;
|
|
766
861
|
}
|
|
767
862
|
|
|
768
|
-
const config =
|
|
863
|
+
const config = getConfig();
|
|
769
864
|
if (!config.codes) config.codes = {};
|
|
770
865
|
|
|
771
866
|
if (config.codes[val]) {
|
|
@@ -776,22 +871,22 @@
|
|
|
776
871
|
|
|
777
872
|
err.style.display = 'none';
|
|
778
873
|
config.codes[val] = { games: AVAILABLE_GAMES.map(g => g.id) };
|
|
779
|
-
|
|
874
|
+
saveConfig(config);
|
|
780
875
|
input.value = '';
|
|
781
876
|
loadAdminDashboard();
|
|
782
877
|
}
|
|
783
878
|
|
|
784
|
-
|
|
879
|
+
function deleteCode(code) {
|
|
785
880
|
if (confirm(`Are you sure you want to delete code ${code}?`)) {
|
|
786
|
-
const config =
|
|
881
|
+
const config = getConfig();
|
|
787
882
|
delete config.codes[code];
|
|
788
|
-
|
|
883
|
+
saveConfig(config);
|
|
789
884
|
loadAdminDashboard();
|
|
790
885
|
}
|
|
791
886
|
}
|
|
792
887
|
|
|
793
|
-
|
|
794
|
-
const config =
|
|
888
|
+
function updateCodePermission(code, gameId, isChecked) {
|
|
889
|
+
const config = getConfig();
|
|
795
890
|
if (!config.codes[code].games) config.codes[code].games = [];
|
|
796
891
|
|
|
797
892
|
if (isChecked) {
|
|
@@ -801,13 +896,13 @@
|
|
|
801
896
|
} else {
|
|
802
897
|
config.codes[code].games = config.codes[code].games.filter(id => id !== gameId);
|
|
803
898
|
}
|
|
804
|
-
|
|
899
|
+
saveConfig(config);
|
|
805
900
|
}
|
|
806
901
|
|
|
807
|
-
|
|
808
|
-
const config =
|
|
902
|
+
function allowAllForCode(code) {
|
|
903
|
+
const config = getConfig();
|
|
809
904
|
config.codes[code].games = AVAILABLE_GAMES.map(g => g.id);
|
|
810
|
-
|
|
905
|
+
saveConfig(config);
|
|
811
906
|
loadAdminDashboard();
|
|
812
907
|
}
|
|
813
908
|
|
|
@@ -831,7 +926,7 @@
|
|
|
831
926
|
|
|
832
927
|
if (!document.fullscreenElement) {
|
|
833
928
|
modalBox.requestFullscreen().catch(err => {
|
|
834
|
-
|
|
929
|
+
showNotification(`Error attempting to enable fullscreen: ${err.message}`);
|
|
835
930
|
});
|
|
836
931
|
icon.innerText = 'fullscreen_exit';
|
|
837
932
|
} else {
|
package/package.json
CHANGED
package/server.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
});
|