studyup 1.0.2 → 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 +515 -113
- package/package.json +1 -1
package/index.html
CHANGED
|
@@ -4,12 +4,10 @@
|
|
|
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
|
-
|
|
11
10
|
<style>
|
|
12
|
-
/* Material Design 1 Core Variables & Palette */
|
|
13
11
|
:root {
|
|
14
12
|
--primary-color: #3f51b5; /* Indigo */
|
|
15
13
|
--primary-dark: #303f9f;
|
|
@@ -19,10 +17,11 @@
|
|
|
19
17
|
--text-primary: rgba(0, 0, 0, 0.87);
|
|
20
18
|
--text-secondary: rgba(0, 0, 0, 0.54);
|
|
21
19
|
--error-color: #d32f2f;
|
|
20
|
+
--success-color: #388e3c;
|
|
21
|
+
--warning-color: #f57c00;
|
|
22
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);
|
|
23
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);
|
|
24
24
|
}
|
|
25
|
-
|
|
26
25
|
body {
|
|
27
26
|
font-family: 'Roboto', sans-serif;
|
|
28
27
|
background-color: var(--background-color);
|
|
@@ -34,8 +33,6 @@
|
|
|
34
33
|
min-height: 100vh;
|
|
35
34
|
justify-content: space-between;
|
|
36
35
|
}
|
|
37
|
-
|
|
38
|
-
/* Material Toolbar (Header) */
|
|
39
36
|
header {
|
|
40
37
|
background-color: var(--primary-color);
|
|
41
38
|
color: white;
|
|
@@ -45,15 +42,18 @@
|
|
|
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;
|
|
52
48
|
font-weight: 500;
|
|
53
49
|
letter-spacing: 0.5px;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
}
|
|
52
|
+
.header-actions {
|
|
53
|
+
display: flex;
|
|
54
|
+
gap: 12px;
|
|
55
|
+
align-items: center;
|
|
54
56
|
}
|
|
55
|
-
|
|
56
|
-
/* View Switching (Login vs Portal Dashboard) */
|
|
57
57
|
.view {
|
|
58
58
|
display: none;
|
|
59
59
|
flex: 1;
|
|
@@ -62,8 +62,6 @@
|
|
|
62
62
|
.view.active {
|
|
63
63
|
display: flex;
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
/* Main Container & Login Card */
|
|
67
65
|
.container {
|
|
68
66
|
max-width: 420px;
|
|
69
67
|
width: 100%;
|
|
@@ -75,7 +73,6 @@
|
|
|
75
73
|
align-items: center;
|
|
76
74
|
justify-content: center;
|
|
77
75
|
}
|
|
78
|
-
|
|
79
76
|
.card {
|
|
80
77
|
background-color: var(--card-background);
|
|
81
78
|
border-radius: 2px;
|
|
@@ -88,7 +85,6 @@
|
|
|
88
85
|
width: 100%;
|
|
89
86
|
box-sizing: border-box;
|
|
90
87
|
}
|
|
91
|
-
|
|
92
88
|
.card-icon-wrapper {
|
|
93
89
|
background-color: rgba(63, 81, 181, 0.1);
|
|
94
90
|
color: var(--primary-color);
|
|
@@ -100,33 +96,27 @@
|
|
|
100
96
|
justify-content: center;
|
|
101
97
|
margin-bottom: 20px;
|
|
102
98
|
}
|
|
103
|
-
|
|
104
99
|
.card-icon-wrapper i {
|
|
105
100
|
font-size: 32px;
|
|
106
101
|
}
|
|
107
|
-
|
|
108
102
|
.card-title {
|
|
109
103
|
font-size: 24px;
|
|
110
104
|
font-weight: 500;
|
|
111
105
|
margin: 0 0 8px 0;
|
|
112
106
|
color: var(--text-primary);
|
|
113
107
|
}
|
|
114
|
-
|
|
115
108
|
.card-subtitle {
|
|
116
109
|
font-size: 14px;
|
|
117
110
|
color: var(--text-secondary);
|
|
118
111
|
margin: 0 0 24px 0;
|
|
119
112
|
line-height: 1.4;
|
|
120
113
|
}
|
|
121
|
-
|
|
122
|
-
/* Material Floating Label Input Field */
|
|
123
114
|
.input-container {
|
|
124
115
|
position: relative;
|
|
125
116
|
margin-bottom: 24px;
|
|
126
117
|
width: 100%;
|
|
127
118
|
text-align: left;
|
|
128
119
|
}
|
|
129
|
-
|
|
130
120
|
.input-container input {
|
|
131
121
|
width: 100%;
|
|
132
122
|
padding: 12px;
|
|
@@ -139,13 +129,11 @@
|
|
|
139
129
|
background: transparent;
|
|
140
130
|
transition: border-color 0.2s;
|
|
141
131
|
}
|
|
142
|
-
|
|
143
132
|
.input-container input:focus {
|
|
144
133
|
border-color: var(--primary-color);
|
|
145
134
|
border-width: 2px;
|
|
146
135
|
padding: 11px;
|
|
147
136
|
}
|
|
148
|
-
|
|
149
137
|
.input-container label {
|
|
150
138
|
position: absolute;
|
|
151
139
|
left: 12px;
|
|
@@ -157,7 +145,6 @@
|
|
|
157
145
|
background: white;
|
|
158
146
|
padding: 0 4px;
|
|
159
147
|
}
|
|
160
|
-
|
|
161
148
|
.input-container input:focus ~ label,
|
|
162
149
|
.input-container input:not(:placeholder-shown) ~ label {
|
|
163
150
|
top: -10px;
|
|
@@ -165,8 +152,6 @@
|
|
|
165
152
|
color: var(--primary-color);
|
|
166
153
|
font-weight: 500;
|
|
167
154
|
}
|
|
168
|
-
|
|
169
|
-
/* Error Message */
|
|
170
155
|
#error-msg {
|
|
171
156
|
color: var(--error-color);
|
|
172
157
|
font-size: 13px;
|
|
@@ -174,8 +159,6 @@
|
|
|
174
159
|
display: none;
|
|
175
160
|
font-weight: 500;
|
|
176
161
|
}
|
|
177
|
-
|
|
178
|
-
/* Material Raised Button */
|
|
179
162
|
.btn-raised {
|
|
180
163
|
background-color: var(--primary-color);
|
|
181
164
|
color: white;
|
|
@@ -192,25 +175,33 @@
|
|
|
192
175
|
transition: background-color 0.2s, box-shadow 0.2s;
|
|
193
176
|
letter-spacing: 0.5px;
|
|
194
177
|
}
|
|
195
|
-
|
|
196
178
|
.btn-raised:hover {
|
|
197
179
|
background-color: var(--primary-dark);
|
|
198
180
|
box-shadow: var(--shadow-elevation-4);
|
|
199
181
|
}
|
|
200
|
-
|
|
201
|
-
|
|
182
|
+
.btn-danger {
|
|
183
|
+
background-color: var(--error-color);
|
|
184
|
+
}
|
|
185
|
+
.btn-danger:hover {
|
|
186
|
+
background-color: #b71c1c;
|
|
187
|
+
}
|
|
188
|
+
.btn-warning {
|
|
189
|
+
background-color: var(--warning-color);
|
|
190
|
+
}
|
|
191
|
+
.btn-warning:hover {
|
|
192
|
+
background-color: #e65100;
|
|
193
|
+
}
|
|
202
194
|
.portal-container {
|
|
203
|
-
max-width:
|
|
195
|
+
max-width: 1000px;
|
|
204
196
|
width: 100%;
|
|
205
197
|
margin: 40px auto;
|
|
206
198
|
padding: 0 16px;
|
|
207
199
|
box-sizing: border-box;
|
|
208
200
|
display: grid;
|
|
209
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
201
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
210
202
|
gap: 24px;
|
|
211
203
|
flex: 1;
|
|
212
204
|
}
|
|
213
|
-
|
|
214
205
|
.portal-card {
|
|
215
206
|
background-color: var(--card-background);
|
|
216
207
|
border-radius: 2px;
|
|
@@ -220,44 +211,38 @@
|
|
|
220
211
|
flex-direction: column;
|
|
221
212
|
overflow: hidden;
|
|
222
213
|
}
|
|
223
|
-
|
|
224
214
|
.portal-card:hover {
|
|
225
215
|
box-shadow: var(--shadow-elevation-4);
|
|
226
216
|
}
|
|
227
|
-
|
|
228
217
|
.card-media {
|
|
229
|
-
height:
|
|
218
|
+
height: 120px;
|
|
230
219
|
display: flex;
|
|
231
220
|
align-items: center;
|
|
232
221
|
justify-content: center;
|
|
233
222
|
color: white;
|
|
234
223
|
}
|
|
235
|
-
|
|
236
224
|
.card-content {
|
|
237
225
|
padding: 24px;
|
|
238
226
|
flex: 1;
|
|
239
227
|
}
|
|
240
|
-
|
|
241
228
|
.portal-card-title {
|
|
242
229
|
font-size: 20px;
|
|
243
230
|
font-weight: 500;
|
|
244
231
|
margin: 0 0 8px 0;
|
|
245
232
|
color: var(--text-primary);
|
|
246
233
|
}
|
|
247
|
-
|
|
248
234
|
.card-text {
|
|
249
235
|
font-size: 14px;
|
|
250
236
|
color: var(--text-secondary);
|
|
251
237
|
line-height: 1.4;
|
|
252
238
|
margin: 0;
|
|
253
239
|
}
|
|
254
|
-
|
|
255
240
|
.card-actions {
|
|
256
241
|
padding: 8px 16px 16px 16px;
|
|
257
242
|
display: flex;
|
|
258
243
|
justify-content: flex-end;
|
|
244
|
+
gap: 8px;
|
|
259
245
|
}
|
|
260
|
-
|
|
261
246
|
.btn-flat {
|
|
262
247
|
background: transparent;
|
|
263
248
|
border: none;
|
|
@@ -273,12 +258,53 @@
|
|
|
273
258
|
transition: background-color 0.2s;
|
|
274
259
|
letter-spacing: 0.5px;
|
|
275
260
|
}
|
|
276
|
-
|
|
277
261
|
.btn-flat:hover {
|
|
278
262
|
background-color: rgba(63, 81, 181, 0.08);
|
|
279
263
|
}
|
|
280
|
-
|
|
281
|
-
|
|
264
|
+
.admin-section {
|
|
265
|
+
background: white;
|
|
266
|
+
padding: 24px;
|
|
267
|
+
border-radius: 2px;
|
|
268
|
+
box-shadow: var(--shadow-elevation-2);
|
|
269
|
+
margin-bottom: 24px;
|
|
270
|
+
}
|
|
271
|
+
.admin-section h2 {
|
|
272
|
+
margin-top: 0;
|
|
273
|
+
font-size: 18px;
|
|
274
|
+
color: var(--primary-color);
|
|
275
|
+
border-bottom: 2px solid var(--background-color);
|
|
276
|
+
padding-bottom: 8px;
|
|
277
|
+
}
|
|
278
|
+
.code-list-item {
|
|
279
|
+
display: flex;
|
|
280
|
+
flex-direction: column;
|
|
281
|
+
background: #fafafa;
|
|
282
|
+
padding: 16px;
|
|
283
|
+
margin-bottom: 12px;
|
|
284
|
+
border-radius: 4px;
|
|
285
|
+
border: 1px solid #e0e0e0;
|
|
286
|
+
gap: 12px;
|
|
287
|
+
}
|
|
288
|
+
@media(min-width: 600px) {
|
|
289
|
+
.code-list-item {
|
|
290
|
+
flex-direction: row;
|
|
291
|
+
align-items: center;
|
|
292
|
+
justify-content: space-between;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
.checkbox-group {
|
|
296
|
+
display: flex;
|
|
297
|
+
flex-wrap: wrap;
|
|
298
|
+
gap: 12px;
|
|
299
|
+
font-size: 14px;
|
|
300
|
+
align-items: center;
|
|
301
|
+
}
|
|
302
|
+
.checkbox-group label {
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
gap: 4px;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
}
|
|
282
308
|
#game-modal {
|
|
283
309
|
display: none;
|
|
284
310
|
position: fixed;
|
|
@@ -292,7 +318,6 @@
|
|
|
292
318
|
align-items: center;
|
|
293
319
|
justify-content: center;
|
|
294
320
|
}
|
|
295
|
-
|
|
296
321
|
.modal-content {
|
|
297
322
|
width: 90vw;
|
|
298
323
|
height: 85vh;
|
|
@@ -303,7 +328,6 @@
|
|
|
303
328
|
flex-direction: column;
|
|
304
329
|
overflow: hidden;
|
|
305
330
|
}
|
|
306
|
-
|
|
307
331
|
.modal-header {
|
|
308
332
|
background-color: #212121;
|
|
309
333
|
color: white;
|
|
@@ -311,21 +335,17 @@
|
|
|
311
335
|
display: flex;
|
|
312
336
|
align-items: center;
|
|
313
337
|
justify-content: space-between;
|
|
314
|
-
font-family: 'Roboto', sans-serif;
|
|
315
338
|
}
|
|
316
|
-
|
|
317
339
|
.modal-title {
|
|
318
340
|
margin: 0;
|
|
319
341
|
font-size: 16px;
|
|
320
342
|
font-weight: 500;
|
|
321
343
|
}
|
|
322
|
-
|
|
323
344
|
.modal-header-actions {
|
|
324
345
|
display: flex;
|
|
325
346
|
align-items: center;
|
|
326
347
|
gap: 8px;
|
|
327
348
|
}
|
|
328
|
-
|
|
329
349
|
.modal-btn {
|
|
330
350
|
background: transparent;
|
|
331
351
|
border: none;
|
|
@@ -339,38 +359,62 @@
|
|
|
339
359
|
border-radius: 50%;
|
|
340
360
|
transition: background-color 0.2s;
|
|
341
361
|
}
|
|
342
|
-
|
|
343
362
|
.modal-btn:hover {
|
|
344
363
|
background-color: rgba(255, 255, 255, 0.1);
|
|
345
364
|
}
|
|
346
|
-
|
|
347
365
|
.modal-body {
|
|
348
366
|
flex: 1;
|
|
349
367
|
width: 100%;
|
|
350
368
|
background: #fff;
|
|
351
369
|
}
|
|
352
|
-
|
|
353
370
|
.modal-body iframe {
|
|
354
371
|
width: 100%;
|
|
355
372
|
height: 100%;
|
|
356
373
|
border: none;
|
|
357
374
|
}
|
|
358
|
-
|
|
359
|
-
/* Footer */
|
|
360
375
|
footer {
|
|
361
376
|
text-align: center;
|
|
362
377
|
padding: 16px;
|
|
363
378
|
color: var(--text-secondary);
|
|
364
379
|
font-size: 12px;
|
|
365
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
|
+
}
|
|
366
409
|
</style>
|
|
367
410
|
</head>
|
|
368
411
|
<body>
|
|
369
|
-
|
|
370
|
-
<!-- Material Design Toolbar -->
|
|
371
412
|
<header>
|
|
372
|
-
<h1>StudyUp</h1>
|
|
373
|
-
<
|
|
413
|
+
<h1 onclick="logout()">StudyUp</h1>
|
|
414
|
+
<div class="header-actions">
|
|
415
|
+
<span id="current-user-badge" style="font-size: 13px; font-weight: 500;"></span>
|
|
416
|
+
<i class="material-icons" style="cursor:pointer;" onclick="logout()">exit_to_app</i>
|
|
417
|
+
</div>
|
|
374
418
|
</header>
|
|
375
419
|
|
|
376
420
|
<!-- VIEW 1: Login Screen -->
|
|
@@ -388,8 +432,7 @@
|
|
|
388
432
|
<label for="class-code-input">Class Code</label>
|
|
389
433
|
</div>
|
|
390
434
|
|
|
391
|
-
<div id="error-msg">Invalid class code
|
|
392
|
-
|
|
435
|
+
<div id="error-msg">Invalid class code or system currently locked.</div>
|
|
393
436
|
<button class="btn-raised" onclick="verifyCode()">Enter Code</button>
|
|
394
437
|
</div>
|
|
395
438
|
</main>
|
|
@@ -397,61 +440,47 @@
|
|
|
397
440
|
|
|
398
441
|
<!-- VIEW 2: Portal Dashboard -->
|
|
399
442
|
<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>
|
|
443
|
+
<div class="portal-container" id="student-games-grid"></div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<!-- VIEW 3: Admin Management Panel View -->
|
|
447
|
+
<div id="admin-view" class="view">
|
|
448
|
+
<div style="max-width: 1000px; width: 100%; margin: 30px auto; padding: 0 16px; box-sizing: border-box; flex:1;">
|
|
449
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
450
|
+
<h2 style="margin: 0; color: var(--primary-color);">Admin Dashboard</h2>
|
|
451
|
+
<button class="btn-raised" style="width: auto; padding: 8px 16px;" onclick="switchView('portal-view')">← Back to Portal</button>
|
|
413
452
|
</div>
|
|
414
453
|
|
|
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>
|
|
454
|
+
<!-- Global Control Section -->
|
|
455
|
+
<div class="admin-section">
|
|
456
|
+
<h2>Global System Controls</h2>
|
|
457
|
+
<p style="font-size: 14px; color: var(--text-secondary);">Manage global availability for all student class codes.</p>
|
|
458
|
+
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;">
|
|
459
|
+
<div>
|
|
460
|
+
<strong>Maintenance / Lockdown Mode:</strong>
|
|
461
|
+
<div id="global-status-text" style="font-size: 13px; color: var(--success-color); font-weight: 500;">All student codes are currently ACTIVE.</div>
|
|
462
|
+
</div>
|
|
463
|
+
<button id="global-toggle-btn" class="btn-raised btn-warning" onclick="toggleGlobalBlock()">Temporarily Block All Codes</button>
|
|
426
464
|
</div>
|
|
427
465
|
</div>
|
|
428
466
|
|
|
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>
|
|
467
|
+
<!-- Create Code Section -->
|
|
468
|
+
<div class="admin-section">
|
|
469
|
+
<h2>Create New Class Code</h2>
|
|
470
|
+
<div style="display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end;">
|
|
471
|
+
<div class="input-container" style="flex: 1; min-width: 200px; margin-bottom: 0;">
|
|
472
|
+
<input type="text" id="new-code-input" placeholder="e.g. 654321" maxlength="6">
|
|
473
|
+
<label for="new-code-input">New Code (Digits)</label>
|
|
474
|
+
</div>
|
|
475
|
+
<button class="btn-raised" onclick="createNewCode()" style="height: 48px; width: auto; padding: 0 24px;">Create Code</button>
|
|
440
476
|
</div>
|
|
477
|
+
<div id="admin-error-msg" style="color: var(--error-color); font-size: 13px; margin-top: 8px; display: none;"></div>
|
|
441
478
|
</div>
|
|
442
479
|
|
|
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>
|
|
480
|
+
<!-- Manage Codes Section -->
|
|
481
|
+
<div class="admin-section">
|
|
482
|
+
<h2>Manage Existing Codes & Game Permissions</h2>
|
|
483
|
+
<div id="codes-management-list"></div>
|
|
455
484
|
</div>
|
|
456
485
|
</div>
|
|
457
486
|
</div>
|
|
@@ -476,12 +505,174 @@
|
|
|
476
505
|
</div>
|
|
477
506
|
</div>
|
|
478
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
|
+
|
|
479
516
|
<footer>
|
|
480
517
|
StudyUp • Student Resource Portal • 2026
|
|
481
518
|
</footer>
|
|
482
519
|
|
|
483
520
|
<script>
|
|
484
|
-
|
|
521
|
+
const AVAILABLE_GAMES = [
|
|
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' }
|
|
526
|
+
];
|
|
527
|
+
|
|
528
|
+
const ADMIN_APP = {
|
|
529
|
+
id: 'AdminPanel',
|
|
530
|
+
name: 'Admin Panel',
|
|
531
|
+
desc: 'Manage global locks, create or delete class codes, and control individual student app permissions.',
|
|
532
|
+
color: '#3f51b5',
|
|
533
|
+
icon: 'admin_panel_settings',
|
|
534
|
+
isCustomAction: true,
|
|
535
|
+
action: function() {
|
|
536
|
+
switchView('admin-view');
|
|
537
|
+
loadAdminDashboard();
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
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() {
|
|
587
|
+
try {
|
|
588
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
589
|
+
if (stored) {
|
|
590
|
+
return JSON.parse(stored);
|
|
591
|
+
}
|
|
592
|
+
} catch (err) {
|
|
593
|
+
console.error("Failed to read from localStorage:", err);
|
|
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;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function saveConfig(configObj, broadcast = true) {
|
|
608
|
+
try {
|
|
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
|
+
}
|
|
616
|
+
} catch (err) {
|
|
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);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
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
|
+
|
|
485
676
|
document.getElementById('class-code-input').addEventListener('keypress', function (e) {
|
|
486
677
|
if (e.key === 'Enter') {
|
|
487
678
|
verifyCode();
|
|
@@ -491,17 +682,230 @@
|
|
|
491
682
|
function verifyCode() {
|
|
492
683
|
const inputVal = document.getElementById('class-code-input').value.trim();
|
|
493
684
|
const errorMsg = document.getElementById('error-msg');
|
|
685
|
+
const config = getConfig();
|
|
686
|
+
|
|
687
|
+
if (config.globalBlock && inputVal !== '814212') {
|
|
688
|
+
errorMsg.innerText = "System is temporarily locked by administrator.";
|
|
689
|
+
errorMsg.style.display = 'block';
|
|
690
|
+
document.getElementById('class-code-input').focus();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
494
693
|
|
|
495
694
|
if (inputVal === '814212') {
|
|
496
695
|
errorMsg.style.display = 'none';
|
|
497
|
-
document.getElementById('
|
|
498
|
-
|
|
696
|
+
document.getElementById('class-code-input').value = '';
|
|
697
|
+
currentSession = { isLoggedIn: true, code: '814212', isAdmin: true };
|
|
698
|
+
switchView('portal-view');
|
|
699
|
+
document.getElementById('current-user-badge').innerText = 'Master Admin Mode';
|
|
700
|
+
|
|
701
|
+
const allGameIds = AVAILABLE_GAMES.map(g => g.id);
|
|
702
|
+
loadStudentDashboard(allGameIds, true);
|
|
703
|
+
} else if (config.codes && config.codes[inputVal]) {
|
|
704
|
+
errorMsg.style.display = 'none';
|
|
705
|
+
document.getElementById('class-code-input').value = '';
|
|
706
|
+
currentSession = { isLoggedIn: true, code: inputVal, isAdmin: false };
|
|
707
|
+
switchView('portal-view');
|
|
708
|
+
document.getElementById('current-user-badge').innerText = `Class Code: ${inputVal}`;
|
|
709
|
+
loadStudentDashboard(config.codes[inputVal].games || [], false);
|
|
499
710
|
} else {
|
|
711
|
+
errorMsg.innerText = "Invalid class code. Please check with your teacher or instructor.";
|
|
500
712
|
errorMsg.style.display = 'block';
|
|
501
713
|
document.getElementById('class-code-input').focus();
|
|
502
714
|
}
|
|
503
715
|
}
|
|
504
716
|
|
|
717
|
+
function switchView(viewId) {
|
|
718
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
719
|
+
document.getElementById(viewId).classList.add('active');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function logout() {
|
|
723
|
+
currentSession = { isLoggedIn: false, code: null, isAdmin: false };
|
|
724
|
+
closeGame();
|
|
725
|
+
switchView('login-view');
|
|
726
|
+
document.getElementById('current-user-badge').innerText = '';
|
|
727
|
+
document.getElementById('class-code-input').focus();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function loadStudentDashboard(allowedGameIds, isAdminUser) {
|
|
731
|
+
const grid = document.getElementById('student-games-grid');
|
|
732
|
+
grid.innerHTML = '';
|
|
733
|
+
|
|
734
|
+
const permittedGames = AVAILABLE_GAMES.filter(g => allowedGameIds.includes(g.id));
|
|
735
|
+
const displayCards = [];
|
|
736
|
+
if (isAdminUser) displayCards.push(ADMIN_APP);
|
|
737
|
+
permittedGames.forEach(g => displayCards.push(g));
|
|
738
|
+
|
|
739
|
+
if (displayCards.length === 0) {
|
|
740
|
+
grid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; padding: 40px; background: white; border-radius: 4px; box-shadow: var(--shadow-elevation-2);">
|
|
741
|
+
<i class="material-icons" style="font-size: 48px; color: var(--text-secondary);">block</i>
|
|
742
|
+
<h3 style="margin: 12px 0 4px 0;">No Apps Available</h3>
|
|
743
|
+
<p style="color: var(--text-secondary); margin: 0;">There are currently no apps assigned or permitted for this class code.</p>
|
|
744
|
+
</div>`;
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
displayCards.forEach(item => {
|
|
749
|
+
const card = document.createElement('div');
|
|
750
|
+
card.className = 'portal-card';
|
|
751
|
+
|
|
752
|
+
if (item.isCustomAction) {
|
|
753
|
+
card.innerHTML = `
|
|
754
|
+
<div class="card-media" style="background-color: ${item.color};">
|
|
755
|
+
<i class="material-icons">${item.icon}</i>
|
|
756
|
+
</div>
|
|
757
|
+
<div class="card-content">
|
|
758
|
+
<h2 class="portal-card-title">${item.name}</h2>
|
|
759
|
+
<p class="card-text">${item.desc}</p>
|
|
760
|
+
</div>
|
|
761
|
+
<div class="card-actions">
|
|
762
|
+
<button class="btn-flat" id="admin-panel-launch-btn">Open Panel</button>
|
|
763
|
+
</div>
|
|
764
|
+
`;
|
|
765
|
+
card.querySelector('#admin-panel-launch-btn').addEventListener('click', item.action);
|
|
766
|
+
} else {
|
|
767
|
+
card.innerHTML = `
|
|
768
|
+
<div class="card-media" style="background-color: ${item.color};">
|
|
769
|
+
<i class="material-icons">${item.icon}</i>
|
|
770
|
+
</div>
|
|
771
|
+
<div class="card-content">
|
|
772
|
+
<h2 class="portal-card-title">${item.name}</h2>
|
|
773
|
+
<p class="card-text">${item.desc}</p>
|
|
774
|
+
</div>
|
|
775
|
+
<div class="card-actions">
|
|
776
|
+
<button class="btn-flat" onclick="openGame('${item.name}', '${item.url}')">Launch App</button>
|
|
777
|
+
</div>
|
|
778
|
+
`;
|
|
779
|
+
}
|
|
780
|
+
grid.appendChild(card);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function loadAdminDashboard() {
|
|
785
|
+
const config = getConfig();
|
|
786
|
+
const isBlocked = config.globalBlock;
|
|
787
|
+
|
|
788
|
+
const statusText = document.getElementById('global-status-text');
|
|
789
|
+
const toggleBtn = document.getElementById('global-toggle-btn');
|
|
790
|
+
if (isBlocked) {
|
|
791
|
+
statusText.innerText = "System is LOCKED. All student codes are temporarily blocked.";
|
|
792
|
+
statusText.style.color = "var(--error-color)";
|
|
793
|
+
toggleBtn.innerText = "Unblock All Codes";
|
|
794
|
+
toggleBtn.className = "btn-raised";
|
|
795
|
+
} else {
|
|
796
|
+
statusText.innerText = "All student codes are currently ACTIVE.";
|
|
797
|
+
statusText.style.color = "var(--success-color)";
|
|
798
|
+
toggleBtn.innerText = "Temporarily Block All Codes";
|
|
799
|
+
toggleBtn.className = "btn-raised btn-warning";
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const listContainer = document.getElementById('codes-management-list');
|
|
803
|
+
listContainer.innerHTML = '';
|
|
804
|
+
|
|
805
|
+
const studentCodes = Object.keys(config.codes || {});
|
|
806
|
+
|
|
807
|
+
if (studentCodes.length === 0) {
|
|
808
|
+
listContainer.innerHTML = `<p style="color: var(--text-secondary); font-style: italic;">No active student codes found. Create one above.</p>`;
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
studentCodes.forEach(code => {
|
|
813
|
+
const codeObj = config.codes[code];
|
|
814
|
+
const assignedGames = codeObj.games || [];
|
|
815
|
+
|
|
816
|
+
const item = document.createElement('div');
|
|
817
|
+
item.className = 'code-list-item';
|
|
818
|
+
|
|
819
|
+
let checkboxesHtml = AVAILABLE_GAMES.map(g => {
|
|
820
|
+
const checked = assignedGames.includes(g.id) ? 'checked' : '';
|
|
821
|
+
return `<label><input type="checkbox" value="${g.id}" ${checked} onchange="updateCodePermission('${code}', '${g.id}', this.checked)"> ${g.name}</label>`;
|
|
822
|
+
}).join('');
|
|
823
|
+
|
|
824
|
+
item.innerHTML = `
|
|
825
|
+
<div>
|
|
826
|
+
<strong style="font-size: 16px; color: var(--primary-color);">Code: ${code}</strong>
|
|
827
|
+
</div>
|
|
828
|
+
<div class="checkbox-group">
|
|
829
|
+
${checkboxesHtml}
|
|
830
|
+
</div>
|
|
831
|
+
<div style="display: flex; gap: 8px;">
|
|
832
|
+
<button class="btn-raised" style="padding: 6px 12px; font-size: 12px;" onclick="allowAllForCode('${code}')">Allow All</button>
|
|
833
|
+
<button class="btn-raised btn-danger" style="padding: 6px 12px; font-size: 12px;" onclick="deleteCode('${code}')">Delete</button>
|
|
834
|
+
</div>
|
|
835
|
+
`;
|
|
836
|
+
listContainer.appendChild(item);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function toggleGlobalBlock() {
|
|
841
|
+
const config = getConfig();
|
|
842
|
+
config.globalBlock = !config.globalBlock;
|
|
843
|
+
saveConfig(config);
|
|
844
|
+
loadAdminDashboard();
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function createNewCode() {
|
|
848
|
+
const input = document.getElementById('new-code-input');
|
|
849
|
+
const err = document.getElementById('admin-error-msg');
|
|
850
|
+
const val = input.value.trim();
|
|
851
|
+
|
|
852
|
+
if (!/^\d{6}$/.test(val)) {
|
|
853
|
+
err.innerText = "Code must be exactly 6 digits.";
|
|
854
|
+
err.style.display = 'block';
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (val === '814212') {
|
|
858
|
+
err.innerText = "814212 is reserved as the Master Admin code.";
|
|
859
|
+
err.style.display = 'block';
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const config = getConfig();
|
|
864
|
+
if (!config.codes) config.codes = {};
|
|
865
|
+
|
|
866
|
+
if (config.codes[val]) {
|
|
867
|
+
err.innerText = "This code already exists.";
|
|
868
|
+
err.style.display = 'block';
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
err.style.display = 'none';
|
|
873
|
+
config.codes[val] = { games: AVAILABLE_GAMES.map(g => g.id) };
|
|
874
|
+
saveConfig(config);
|
|
875
|
+
input.value = '';
|
|
876
|
+
loadAdminDashboard();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function deleteCode(code) {
|
|
880
|
+
if (confirm(`Are you sure you want to delete code ${code}?`)) {
|
|
881
|
+
const config = getConfig();
|
|
882
|
+
delete config.codes[code];
|
|
883
|
+
saveConfig(config);
|
|
884
|
+
loadAdminDashboard();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function updateCodePermission(code, gameId, isChecked) {
|
|
889
|
+
const config = getConfig();
|
|
890
|
+
if (!config.codes[code].games) config.codes[code].games = [];
|
|
891
|
+
|
|
892
|
+
if (isChecked) {
|
|
893
|
+
if (!config.codes[code].games.includes(gameId)) {
|
|
894
|
+
config.codes[code].games.push(gameId);
|
|
895
|
+
}
|
|
896
|
+
} else {
|
|
897
|
+
config.codes[code].games = config.codes[code].games.filter(id => id !== gameId);
|
|
898
|
+
}
|
|
899
|
+
saveConfig(config);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function allowAllForCode(code) {
|
|
903
|
+
const config = getConfig();
|
|
904
|
+
config.codes[code].games = AVAILABLE_GAMES.map(g => g.id);
|
|
905
|
+
saveConfig(config);
|
|
906
|
+
loadAdminDashboard();
|
|
907
|
+
}
|
|
908
|
+
|
|
505
909
|
function openGame(gameName, gameUrl) {
|
|
506
910
|
document.getElementById('modal-game-title').innerText = gameName;
|
|
507
911
|
document.getElementById('game-iframe').src = gameUrl;
|
|
@@ -509,7 +913,6 @@
|
|
|
509
913
|
}
|
|
510
914
|
|
|
511
915
|
function closeGame() {
|
|
512
|
-
// Exit fullscreen if active when closing
|
|
513
916
|
if (document.fullscreenElement) {
|
|
514
917
|
document.exitFullscreen();
|
|
515
918
|
}
|
|
@@ -523,7 +926,7 @@
|
|
|
523
926
|
|
|
524
927
|
if (!document.fullscreenElement) {
|
|
525
928
|
modalBox.requestFullscreen().catch(err => {
|
|
526
|
-
|
|
929
|
+
showNotification(`Error attempting to enable fullscreen: ${err.message}`);
|
|
527
930
|
});
|
|
528
931
|
icon.innerText = 'fullscreen_exit';
|
|
529
932
|
} else {
|
|
@@ -532,7 +935,6 @@
|
|
|
532
935
|
}
|
|
533
936
|
}
|
|
534
937
|
|
|
535
|
-
// Keep fullscreen icon updated if user exits via keyboard (Esc)
|
|
536
938
|
document.addEventListener('fullscreenchange', () => {
|
|
537
939
|
const icon = document.getElementById('fullscreen-icon');
|
|
538
940
|
if (!document.fullscreenElement) {
|