stem-lab-toolkit 1.0.0

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/game.html ADDED
@@ -0,0 +1,208 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>MM Games</title>
7
+ <link rel="stylesheet" href="./css/style.css">
8
+ <style>
9
+ body { background: #0a0a0a; color: #fff; }
10
+
11
+ .site-header { background: #111; border-bottom-color: #222; }
12
+ .site-header .logo-link { color: #fff; }
13
+ .site-nav a { color: rgba(255,255,255,.5); }
14
+ .site-nav a:hover { background: #222; color: #fff; }
15
+
16
+ .game-wrap {
17
+ display: flex;
18
+ flex-direction: column;
19
+ align-items: center;
20
+ padding: 20px 16px 40px;
21
+ }
22
+
23
+ .game-title-bar {
24
+ width: 100%;
25
+ max-width: 960px;
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+ margin-bottom: 12px;
30
+ }
31
+
32
+ .game-name {
33
+ font-size: 1.1rem;
34
+ font-weight: 600;
35
+ }
36
+
37
+ .game-frame-wrap {
38
+ width: 100%;
39
+ max-width: 960px;
40
+ position: relative;
41
+ background: #000;
42
+ border-radius: 10px;
43
+ overflow: hidden;
44
+ box-shadow: 0 8px 40px rgba(0,0,0,.6);
45
+ }
46
+
47
+ .game-frame-ratio {
48
+ padding-bottom: 56.25%;
49
+ position: relative;
50
+ }
51
+
52
+ .game-iframe {
53
+ position: absolute;
54
+ inset: 0;
55
+ width: 100%;
56
+ height: 100%;
57
+ border: none;
58
+ }
59
+
60
+ .game-actions {
61
+ width: 100%;
62
+ max-width: 960px;
63
+ display: flex;
64
+ gap: 10px;
65
+ margin-top: 14px;
66
+ }
67
+
68
+ .fullscreen-btn {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 8px;
72
+ padding: 10px 22px;
73
+ background: #ff9f0a;
74
+ color: #fff;
75
+ border: none;
76
+ border-radius: 8px;
77
+ font-size: 0.9rem;
78
+ font-weight: 600;
79
+ cursor: pointer;
80
+ transition: background .15s;
81
+ font-family: inherit;
82
+ }
83
+
84
+ .fullscreen-btn:hover { background: #e68e00; }
85
+
86
+ .back-btn {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 6px;
90
+ padding: 10px 18px;
91
+ background: rgba(255,255,255,.08);
92
+ color: rgba(255,255,255,.7);
93
+ border: none;
94
+ border-radius: 8px;
95
+ font-size: 0.9rem;
96
+ font-weight: 500;
97
+ cursor: pointer;
98
+ font-family: inherit;
99
+ transition: background .15s;
100
+ }
101
+
102
+ .back-btn:hover { background: rgba(255,255,255,.14); }
103
+
104
+ .error-state {
105
+ text-align: center;
106
+ padding: 80px 20px;
107
+ color: rgba(255,255,255,.5);
108
+ }
109
+
110
+ .error-state h2 { font-size: 1.2rem; margin-bottom: 8px; color: #fff; }
111
+ </style>
112
+ </head>
113
+ <body>
114
+
115
+ <header class="site-header">
116
+ <div class="header-inner">
117
+ <a href="./browse.html" class="logo-link">
118
+ <img src="./assets/logo.png" alt="" class="logo-img" onerror="this.style.display='none'">
119
+ <span>MM Games</span>
120
+ </a>
121
+ <nav class="site-nav">
122
+ <a href="./browse.html">Browse</a>
123
+ </nav>
124
+ </div>
125
+ </header>
126
+
127
+ <div class="game-wrap" id="game-wrap">
128
+ <div class="error-state"><h2>Loading…</h2></div>
129
+ </div>
130
+
131
+ <script>
132
+ (async () => {
133
+ const params = new URLSearchParams(window.location.search);
134
+ const slug = params.get('slug');
135
+ const wrap = document.getElementById('game-wrap');
136
+
137
+ function error(msg) {
138
+ wrap.innerHTML = `<div class="error-state"><h2>Game not found</h2><p>${msg}</p><br><a href="./browse.html" style="color:#ff9f0a;">← Back to games</a></div>`;
139
+ }
140
+
141
+ if (!slug) return error('No game specified.');
142
+
143
+ let games;
144
+ try {
145
+ const res = await fetch('./api/games');
146
+ games = res.ok ? await res.json() : null;
147
+ } catch { games = null; }
148
+
149
+ if (!games) {
150
+ try {
151
+ const res = await fetch('./games.json');
152
+ const all = await res.json();
153
+ games = all.filter(g => g.visible);
154
+ } catch { return error('Could not load game data.'); }
155
+ }
156
+
157
+ const game = games.find(g => g.slug === slug);
158
+ if (!game) return error('Game not found.');
159
+
160
+ document.title = game.name + ' — MM Games';
161
+
162
+ const SITE = 'https://calc.moshelab.com';
163
+ let src;
164
+ if (game.embedType === 'gamepix') {
165
+ src = game.embedUrl;
166
+ } else {
167
+ src = `https://html5.gamedistribution.com/${game.gameId}/?gd_sdk_referrer_url=${SITE}/games/${game.slug}`;
168
+ }
169
+
170
+ wrap.innerHTML = `
171
+ <div class="game-title-bar">
172
+ <div class="game-name">${game.name}</div>
173
+ <span class="cat-badge ${game.category}">${game.category}</span>
174
+ </div>
175
+ <div class="game-frame-wrap" id="frame-wrap">
176
+ <div class="game-frame-ratio">
177
+ <iframe
178
+ class="game-iframe"
179
+ id="game-iframe"
180
+ src="${src}"
181
+ allowfullscreen
182
+ allow="fullscreen; autoplay; encrypted-media"
183
+ scrolling="no"
184
+ ></iframe>
185
+ </div>
186
+ </div>
187
+ <div class="game-actions">
188
+ <button class="fullscreen-btn" id="fs-btn">
189
+ <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
190
+ <path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/>
191
+ </svg>
192
+ Fullscreen
193
+ </button>
194
+ <button class="back-btn" onclick="history.length > 1 ? history.back() : window.location='./browse.html'">
195
+ ← Back
196
+ </button>
197
+ </div>
198
+ `;
199
+
200
+ document.getElementById('fs-btn').addEventListener('click', () => {
201
+ const el = document.getElementById('frame-wrap');
202
+ if (el.requestFullscreen) el.requestFullscreen();
203
+ else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
204
+ });
205
+ })();
206
+ </script>
207
+ </body>
208
+ </html>