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.
Files changed (2) hide show
  1. package/index.html +515 -113
  2. 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 1 / Roboto Typography & Icons -->
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
- /* Portal Dashboard Grid */
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: 900px;
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(320px, 1fr));
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: 140px;
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
- /* Modal / Window Overlay */
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
- <i class="material-icons">school</i>
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. Please check with your teacher or instructor.</div>
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
- <!-- Card 1: Sandboxels -->
402
- <div class="portal-card">
403
- <div class="card-media" style="background-color: #00897b;">
404
- <i class="material-icons">science</i>
405
- </div>
406
- <div class="card-content">
407
- <h2 class="portal-card-title">Sandboxels</h2>
408
- <p class="card-text">A falling sand and physics sandbox game where you can mix elements, trigger chemical reactions, and experiment on a grid. Great for life science.</p>
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')">&larr; Back to Portal</button>
413
452
  </div>
414
453
 
415
- <!-- Card 2: HexGL -->
416
- <div class="portal-card">
417
- <div class="card-media" style="background-color: #e53935;">
418
- <i class="material-icons">speed</i>
419
- </div>
420
- <div class="card-content">
421
- <h2 class="portal-card-title">HexGL</h2>
422
- <p class="card-text">A futuristic 3D WebGL racing game built with HTML5 where you pilot a high-speed craft through complex tracks. Great for testing your shape naming skills.</p>
423
- </div>
424
- <div class="card-actions">
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
- <!-- Card 3: ClassiCube -->
430
- <div class="portal-card">
431
- <div class="card-media" style="background-color: #00acc1;">
432
- <i class="material-icons">terrain</i>
433
- </div>
434
- <div class="card-content">
435
- <h2 class="portal-card-title">ClassiCube</h2>
436
- <p class="card-text">A lightweight multiplayer-compatible classic block building sandbox game. Great for architecture and spatial visualization exercises.</p>
437
- </div>
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
- <!-- Card 4: 2048 -->
444
- <div class="portal-card">
445
- <div class="card-media" style="background-color: #ffb300;">
446
- <i class="material-icons">grid_on</i>
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 &bull; Student Resource Portal &bull; 2026
481
518
  </footer>
482
519
 
483
520
  <script>
484
- // Allow pressing 'Enter' to submit code
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('login-view').classList.remove('active');
498
- document.getElementById('portal-view').classList.add('active');
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
- alert(`Error attempting to enable fullscreen: ${err.message}`);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "studyup",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "StudyUp Game Portal",
5
5
  "main": "index.html",
6
6
  "scripts": {