studyup 1.0.2 → 1.0.3

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