@workadventure/map-starter-kit-core 0.0.1
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/.github/workflows/release.yml +47 -0
- package/LICENSE +21 -0
- package/package.json +43 -0
- package/public/assets/index.js +80 -0
- package/public/images/badumtss.svg +12 -0
- package/public/images/brand-discord.svg +22 -0
- package/public/images/brand-github.svg +19 -0
- package/public/images/brand-linkedin.svg +23 -0
- package/public/images/brand-x.svg +20 -0
- package/public/images/brand-youtube.svg +20 -0
- package/public/images/favicon.svg +50 -0
- package/public/images/logo.svg +72 -0
- package/public/images/unknown-room-image copy.png +0 -0
- package/public/images/unknown-room-image.png +0 -0
- package/public/styles/styles.css +431 -0
- package/public/styles/styles.css.map +1 -0
- package/semantic-release.config.js +8 -0
- package/src/controllers/FrontController.ts +95 -0
- package/src/controllers/MapController.ts +104 -0
- package/src/controllers/UploaderController.ts +333 -0
- package/src/getCoreRoot.ts +40 -0
- package/src/server.ts +124 -0
- package/src/views/index.html +169 -0
- package/src/views/step1-git.html +154 -0
- package/src/views/step2-hosting.html +153 -0
- package/src/views/step3-steps-selfhosted.html +502 -0
- package/src/views/step3-steps.html +549 -0
- package/src/views/step4-validated-selfhosted.html +188 -0
- package/src/views/step4-validated.html +80 -0
- package/tsconfig.json +35 -0
- package/vite.config.ts +53 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<meta name="robots" content="noindex">
|
|
9
|
+
<meta name="title" content="WorkAdventure Starter Kit">
|
|
10
|
+
|
|
11
|
+
<link href="public/styles/styles.css" rel="stylesheet">
|
|
12
|
+
|
|
13
|
+
<title>WorkAdventure test map</title>
|
|
14
|
+
<link rel="icon" href="/images/favicon.svg" type="image/svg+xml">
|
|
15
|
+
<script type="module">
|
|
16
|
+
document.addEventListener("DOMContentLoaded", (event) => {
|
|
17
|
+
// Load index.js to have access to getMapsList
|
|
18
|
+
import('/public/assets/index.js').then(() => {
|
|
19
|
+
window.createBackgroundImageFade();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
</head>
|
|
24
|
+
|
|
25
|
+
<body>
|
|
26
|
+
<div class="content">
|
|
27
|
+
<header>
|
|
28
|
+
<div class="logo">
|
|
29
|
+
<a href="https://workadventu.re/" target="_blank" title="Workadventure">
|
|
30
|
+
<img src="public/images/logo.svg" alt="Workadventure logo" height="36" />
|
|
31
|
+
</a>
|
|
32
|
+
</div>
|
|
33
|
+
<div style="flex-grow: 1;"></div>
|
|
34
|
+
<div class="socials">
|
|
35
|
+
<a href="https://discord.gg/G6Xh9ZM9aR" target="_blank" title="discord">
|
|
36
|
+
<img src="/public/images/brand-discord.svg" alt="discord">
|
|
37
|
+
</a>
|
|
38
|
+
<a href="https://github.com/thecodingmachine/workadventure" target="_blank" title="github">
|
|
39
|
+
<img src="/public/images/brand-github.svg" alt="github">
|
|
40
|
+
</a>
|
|
41
|
+
<a href="https://www.youtube.com/channel/UCXJ9igV-kb9gw1ftR33y5tA" target="_blank" title="youtube">
|
|
42
|
+
<img src="/public/images/brand-youtube.svg" alt="youtube">
|
|
43
|
+
</a>
|
|
44
|
+
<a href="https://twitter.com/Workadventure_" target="_blank" title="twitter">
|
|
45
|
+
<img src="/public/images/brand-x.svg" alt="X">
|
|
46
|
+
</a>
|
|
47
|
+
<a href="https://www.linkedin.com/company/workadventu-re" target="_blank" title="linkedin">
|
|
48
|
+
<img src="/public/images/brand-linkedin.svg" alt="linkedin">
|
|
49
|
+
</a>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="btn-header-wrapper">
|
|
52
|
+
<a href="https://discord.gg/G6Xh9ZM9aR" target="_blank" class="btn btn-light">Talk to the community</a>
|
|
53
|
+
<a href="https://docs.workadventu.re/map-building/" target="_blank" class="btn">Documentation</a>
|
|
54
|
+
</div>
|
|
55
|
+
</header>
|
|
56
|
+
<main class="container">
|
|
57
|
+
<section id="configureSteps" class="form-center steps">
|
|
58
|
+
<input type="hidden" id="stepProgress" value="1" />
|
|
59
|
+
<div id="step1" class="step">
|
|
60
|
+
<div class="step-title">
|
|
61
|
+
<div class="step-number">1</div>
|
|
62
|
+
<h2>Connect to your back-office </h2>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<button id="bo-connect" class="btn">Log-in to your back-office</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div id="step2" class="step inactive">
|
|
70
|
+
<div class="step-title">
|
|
71
|
+
<div class="step-number">2</div>
|
|
72
|
+
<h2>Complete the URL of your map storage</h2>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="alert">
|
|
75
|
+
<div class="alert-text">
|
|
76
|
+
Be careful to select the proper world just above before copying your map storage url and create your key
|
|
77
|
+
</div>
|
|
78
|
+
<img src="public/images/world-select.png" alt="World selection" />
|
|
79
|
+
</div>
|
|
80
|
+
<div class="step-text">
|
|
81
|
+
You can find it in here. Log in. In the left panel, click on "Developers" tab then "API keys / Zapier". There are 3 links, be careful to take the Map-storage API endpoint, it is the url for uploading files to the map storage service of WorkAdventure.
|
|
82
|
+
</div>
|
|
83
|
+
<div style="margin-top: 8px;">
|
|
84
|
+
<input id="mapStorageURL" type="url" placeholder="Paste here your URL..." />
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div id="step3" class="step inactive">
|
|
89
|
+
<div class="step-title">
|
|
90
|
+
<div class="step-number">3</div>
|
|
91
|
+
<h2>Add your map storage API Key</h2>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="step-text">
|
|
94
|
+
In the same section than the step before:
|
|
95
|
+
<ol>
|
|
96
|
+
<li>Create a new token on back-office</li>
|
|
97
|
+
<li>Refresh the page</li>
|
|
98
|
+
<li>Click on the button “Copy” of your token</li>
|
|
99
|
+
</ol>
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<input id="apiKey"
|
|
103
|
+
type="password"
|
|
104
|
+
placeholder="Paste here your API Key..."
|
|
105
|
+
autocomplete="off"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div id="step4" class="step inactive">
|
|
111
|
+
<div class="step-title">
|
|
112
|
+
<div class="step-number">4</div>
|
|
113
|
+
<h2>Choose a name / directory for your world</h2>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="step-text">
|
|
116
|
+
You can use an existing directory or create a new one.
|
|
117
|
+
</div>
|
|
118
|
+
<div>
|
|
119
|
+
<input id="uploadDirectory" type="text" placeholder="Name of your world" />
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
</main>
|
|
124
|
+
<!-- Loading overlay -->
|
|
125
|
+
<div id="loadingOverlay" class="loading-overlay" style="display: none;">
|
|
126
|
+
<div class="loading-content">
|
|
127
|
+
<div class="loading-spinner"></div>
|
|
128
|
+
<p id="loadingText">Verifying your data...</p>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<!-- Error popup -->
|
|
132
|
+
<div id="errorPopup" class="error-popup" style="display: none;">
|
|
133
|
+
<div class="error-popup-content">
|
|
134
|
+
<div class="error-icon">
|
|
135
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
136
|
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
|
137
|
+
<path d="M12 8v4M12 16h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
138
|
+
</svg>
|
|
139
|
+
</div>
|
|
140
|
+
<h3 class="error-title">Error</h3>
|
|
141
|
+
<p id="errorMessage" class="error-message"></p>
|
|
142
|
+
<button id="errorCloseButton" class="btn btn-secondary error-close-btn">Close</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="button-wrapper">
|
|
146
|
+
<div>
|
|
147
|
+
<a href="step2-hosting" class="btn btn-ghost">
|
|
148
|
+
Previous
|
|
149
|
+
</a>
|
|
150
|
+
</div>
|
|
151
|
+
<div style="flex-grow: 1;">
|
|
152
|
+
</div>
|
|
153
|
+
<div>
|
|
154
|
+
<button id="saveButton" class="btn btn-secondary">
|
|
155
|
+
Upload
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="bg"></div>
|
|
161
|
+
<script>
|
|
162
|
+
let stepProgress = document.getElementById('stepProgress');
|
|
163
|
+
let step1 = document.getElementById('step1');
|
|
164
|
+
let step2 = document.getElementById('step2');
|
|
165
|
+
let step3 = document.getElementById('step3');
|
|
166
|
+
let step4 = document.getElementById('step4');
|
|
167
|
+
let mapStorageURL = document.getElementById('mapStorageURL');
|
|
168
|
+
let apiKey = document.getElementById('apiKey');
|
|
169
|
+
let uploadDirectory = document.getElementById('uploadDirectory');
|
|
170
|
+
const button = document.getElementById("bo-connect");
|
|
171
|
+
const saveButton = document.getElementById("saveButton");
|
|
172
|
+
let formIsValid = false;
|
|
173
|
+
|
|
174
|
+
// Function to validate map storage URL
|
|
175
|
+
function isValidMapStorageUrl(value) {
|
|
176
|
+
const regex = /^https:\/\/[a-zA-Z0-9.-]+\.map-storage\.workadventu\.re\/?$/;
|
|
177
|
+
return regex.test(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Function to load existing configuration and auto-advance steps
|
|
181
|
+
async function loadConfiguration() {
|
|
182
|
+
try {
|
|
183
|
+
// Show loading overlay
|
|
184
|
+
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
185
|
+
loadingOverlay.style.display = 'flex';
|
|
186
|
+
|
|
187
|
+
const loadingText = document.getElementById('loadingText');
|
|
188
|
+
loadingText.innerHTML = 'Verifying your data...';
|
|
189
|
+
|
|
190
|
+
const response = await fetch("/uploader/status");
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
throw new Error('Failed to load configuration status');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
loadingText.innerHTML = 'Loading your configuration...';
|
|
197
|
+
|
|
198
|
+
const data = await response.json();
|
|
199
|
+
|
|
200
|
+
// If we have secret config, pre-fill the form fields
|
|
201
|
+
if (!data.secretConfig) {
|
|
202
|
+
throw new Error('No configuration found');
|
|
203
|
+
}
|
|
204
|
+
const config = data.secretConfig;
|
|
205
|
+
let currentStep = 1;
|
|
206
|
+
|
|
207
|
+
// Pre-fill mapStorageURL if available
|
|
208
|
+
if (config.mapStorageUrl) {
|
|
209
|
+
mapStorageURL.value = config.mapStorageUrl;
|
|
210
|
+
// Trigger validation and step advancement
|
|
211
|
+
if (isValidMapStorageUrl(config.mapStorageUrl)) {
|
|
212
|
+
mapStorageURL.classList.remove("error");
|
|
213
|
+
mapStorageURL.classList.add("success");
|
|
214
|
+
// Advance to step 2 and 3
|
|
215
|
+
currentStep = 3;
|
|
216
|
+
step2.style.opacity = 1;
|
|
217
|
+
step2.classList.remove("inactive");
|
|
218
|
+
step3.style.opacity = 1;
|
|
219
|
+
step3.classList.remove("inactive");
|
|
220
|
+
formIsValid = 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Pre-fill apiKey if available
|
|
225
|
+
if (config.mapStorageApiKey) {
|
|
226
|
+
apiKey.value = config.mapStorageApiKey;
|
|
227
|
+
// Trigger validation and step advancement
|
|
228
|
+
if (config.mapStorageApiKey) {
|
|
229
|
+
apiKey.classList.remove("error");
|
|
230
|
+
apiKey.classList.add("success");
|
|
231
|
+
// Advance to step 4
|
|
232
|
+
currentStep = 4;
|
|
233
|
+
totalHeight = step1.offsetHeight + step2.offsetHeight + (step3.offsetHeight + 100) + step4.offsetHeight;
|
|
234
|
+
step2.style.opacity = 1;
|
|
235
|
+
step2.classList.remove("inactive");
|
|
236
|
+
step3.style.opacity = 1;
|
|
237
|
+
step3.classList.remove("inactive");
|
|
238
|
+
step4.style.opacity = 1;
|
|
239
|
+
step4.classList.remove("inactive");
|
|
240
|
+
formIsValid = 2;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Pre-fill uploadDirectory if available
|
|
245
|
+
if (config.uploadDirectory) {
|
|
246
|
+
uploadDirectory.value = config.uploadDirectory;
|
|
247
|
+
// Trigger validation
|
|
248
|
+
if (config.uploadDirectory) {
|
|
249
|
+
uploadDirectory.classList.remove("error");
|
|
250
|
+
uploadDirectory.classList.add("success");
|
|
251
|
+
formIsValid = 3;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Update step progress and height if we advanced
|
|
256
|
+
if (currentStep > 1) {
|
|
257
|
+
stepProgress.value = currentStep;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Hide loading overlay
|
|
261
|
+
loadingOverlay.style.display = 'none';
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('Error loading configuration:', error);
|
|
264
|
+
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
265
|
+
loadingOverlay.style.display = 'none';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Load configuration on page load
|
|
270
|
+
loadConfiguration();
|
|
271
|
+
|
|
272
|
+
button.addEventListener("click", (event) => {
|
|
273
|
+
stepProgress.value = 2;
|
|
274
|
+
step2.style.opacity = 1;
|
|
275
|
+
step2.classList.remove("inactive");
|
|
276
|
+
// Scroll to step 2
|
|
277
|
+
step2.scrollIntoView({ behavior: "smooth" });
|
|
278
|
+
// Open new window to "https://admin.workadventu.re/login"
|
|
279
|
+
window.open("https://admin.workadventu.re/login", "_blank");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
mapStorageURL.addEventListener('input', function (evt) {
|
|
283
|
+
if(isValidMapStorageUrl(this.value)) {
|
|
284
|
+
mapStorageURL.classList.remove("error");
|
|
285
|
+
mapStorageURL.classList.add("success");
|
|
286
|
+
setTimeout(function() {
|
|
287
|
+
step3.style.opacity = 1;
|
|
288
|
+
step3.classList.remove("inactive");
|
|
289
|
+
// Scroll to step 3
|
|
290
|
+
step3.scrollIntoView({ behavior: "smooth" });
|
|
291
|
+
}, 500);
|
|
292
|
+
formIsValid++;
|
|
293
|
+
} else {
|
|
294
|
+
mapStorageURL.classList.remove("success");
|
|
295
|
+
mapStorageURL.classList.add("error");
|
|
296
|
+
formIsValid = false;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
apiKey.addEventListener('input', function (evt) {
|
|
301
|
+
if(this.value) {
|
|
302
|
+
apiKey.classList.remove("error");
|
|
303
|
+
apiKey.classList.add("success");
|
|
304
|
+
setTimeout(function() {
|
|
305
|
+
step4.style.opacity = 1;
|
|
306
|
+
step4.classList.remove("inactive");
|
|
307
|
+
// Scroll to step 4
|
|
308
|
+
console.log('Scroll to step 4');
|
|
309
|
+
step4.scrollIntoView({ behavior: "smooth" });
|
|
310
|
+
formIsValid++;
|
|
311
|
+
}, 500);
|
|
312
|
+
} else {
|
|
313
|
+
apiKey.classList.remove("success");
|
|
314
|
+
apiKey.classList.add("error");
|
|
315
|
+
formIsValid = false;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
uploadDirectory.addEventListener('input', function (evt) {
|
|
320
|
+
if(this.value) {
|
|
321
|
+
uploadDirectory.classList.remove("error");
|
|
322
|
+
uploadDirectory.classList.add("success");
|
|
323
|
+
formIsValid++;
|
|
324
|
+
} else {
|
|
325
|
+
uploadDirectory.classList.remove("success");
|
|
326
|
+
uploadDirectory.classList.add("error");
|
|
327
|
+
formIsValid = false;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Function to show error popup
|
|
332
|
+
function showErrorPopup(message) {
|
|
333
|
+
const errorPopup = document.getElementById('errorPopup');
|
|
334
|
+
const errorMessage = document.getElementById('errorMessage');
|
|
335
|
+
errorMessage.textContent = message;
|
|
336
|
+
errorPopup.style.display = 'flex';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Function to hide error popup
|
|
340
|
+
function hideErrorPopup() {
|
|
341
|
+
const errorPopup = document.getElementById('errorPopup');
|
|
342
|
+
errorPopup.style.display = 'none';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Close error popup when clicking the button
|
|
346
|
+
document.getElementById('errorCloseButton').addEventListener('click', hideErrorPopup);
|
|
347
|
+
|
|
348
|
+
// Close error popup when clicking outside
|
|
349
|
+
document.getElementById('errorPopup').addEventListener('click', function(e) {
|
|
350
|
+
if (e.target === this) {
|
|
351
|
+
hideErrorPopup();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
saveButton.addEventListener("click", async (event) => {
|
|
356
|
+
// Validate form before proceeding
|
|
357
|
+
if (!mapStorageURL.value || !isValidMapStorageUrl(mapStorageURL.value) ||
|
|
358
|
+
!apiKey.value || !uploadDirectory.value) {
|
|
359
|
+
showErrorPopup('Please fill in all required fields correctly.');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Show loading overlay
|
|
364
|
+
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
365
|
+
loadingOverlay.style.display = 'flex';
|
|
366
|
+
|
|
367
|
+
// Start verification process with 2 second minimum
|
|
368
|
+
const startTime = Date.now();
|
|
369
|
+
const minDuration = 2000; // 2 seconds
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
loadingText.innerHTML = 'Saving your configuration...';
|
|
373
|
+
// Send the form data to the server to configure
|
|
374
|
+
const configureResponse = await fetch("/uploader/configure", {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: {
|
|
377
|
+
"Content-Type": "application/json"
|
|
378
|
+
},
|
|
379
|
+
body: JSON.stringify({
|
|
380
|
+
mapStorageUrl: mapStorageURL.value,
|
|
381
|
+
mapStorageApiKey: apiKey.value,
|
|
382
|
+
uploadDirectory: uploadDirectory.value
|
|
383
|
+
})
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (!configureResponse.ok) {
|
|
387
|
+
const errorData = await configureResponse.json().catch(() => ({}));
|
|
388
|
+
throw new Error(errorData.message || errorData.error || 'Failed to save configuration. Please check your inputs and try again.');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const configureData = await configureResponse.json();
|
|
392
|
+
|
|
393
|
+
// Update loading message
|
|
394
|
+
loadingText.innerHTML = 'Uploading your map...';
|
|
395
|
+
|
|
396
|
+
// Upload the map
|
|
397
|
+
const uploadResponse = await fetch("/uploader/upload", {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: {
|
|
400
|
+
"Content-Type": "application/json"
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (!uploadResponse.ok) {
|
|
405
|
+
const errorData = await uploadResponse.json().catch(() => ({}));
|
|
406
|
+
const errorMsg = errorData.message || errorData.error || 'Failed to upload map. Please verify your configuration and try again.';
|
|
407
|
+
throw new Error(errorMsg);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Ensure minimum 2 seconds have passed
|
|
411
|
+
const elapsed = Date.now() - startTime;
|
|
412
|
+
if (elapsed < minDuration) {
|
|
413
|
+
await new Promise(resolve => setTimeout(resolve, minDuration - elapsed));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Redirect to success page
|
|
417
|
+
window.location.href = "/step4-validated";
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.error('Error saving configuration:', error);
|
|
420
|
+
// Hide loading overlay on error
|
|
421
|
+
loadingOverlay.style.display = 'none';
|
|
422
|
+
|
|
423
|
+
// Show error popup with a user-friendly message
|
|
424
|
+
const errorMessage = error instanceof Error ? error.message : 'An error occurred while saving your configuration. Please try again.';
|
|
425
|
+
showErrorPopup(errorMessage);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
</script>
|
|
429
|
+
<style>
|
|
430
|
+
.loading-overlay {
|
|
431
|
+
position: fixed;
|
|
432
|
+
top: 0;
|
|
433
|
+
left: 0;
|
|
434
|
+
width: 100%;
|
|
435
|
+
height: 100%;
|
|
436
|
+
background-color: rgba(27, 42, 65, 0.9);
|
|
437
|
+
display: flex;
|
|
438
|
+
justify-content: center;
|
|
439
|
+
align-items: center;
|
|
440
|
+
z-index: 9999;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.loading-content {
|
|
444
|
+
text-align: center;
|
|
445
|
+
color: white;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.loading-spinner {
|
|
449
|
+
border: 4px solid rgba(255, 255, 255, 0.3);
|
|
450
|
+
border-top: 4px solid #66E979;
|
|
451
|
+
border-radius: 50%;
|
|
452
|
+
width: 50px;
|
|
453
|
+
height: 50px;
|
|
454
|
+
animation: spin 1s linear infinite;
|
|
455
|
+
margin: 0 auto 20px;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
@keyframes spin {
|
|
459
|
+
0% { transform: rotate(0deg); }
|
|
460
|
+
100% { transform: rotate(360deg); }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.loading-content p {
|
|
464
|
+
margin: 0;
|
|
465
|
+
font-size: 18px;
|
|
466
|
+
font-weight: 500;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* Error popup styles */
|
|
470
|
+
.error-popup {
|
|
471
|
+
position: fixed;
|
|
472
|
+
top: 0;
|
|
473
|
+
left: 0;
|
|
474
|
+
width: 100%;
|
|
475
|
+
height: 100%;
|
|
476
|
+
background-color: rgba(27, 42, 65, 0.85);
|
|
477
|
+
display: flex;
|
|
478
|
+
justify-content: center;
|
|
479
|
+
align-items: center;
|
|
480
|
+
z-index: 10000;
|
|
481
|
+
animation: fadeIn 0.3s ease-in-out;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@keyframes fadeIn {
|
|
485
|
+
from {
|
|
486
|
+
opacity: 0;
|
|
487
|
+
}
|
|
488
|
+
to {
|
|
489
|
+
opacity: 1;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.error-popup-content {
|
|
494
|
+
background: rgba(27, 42, 65, 0.95);
|
|
495
|
+
border: 2px solid #ff4444;
|
|
496
|
+
border-radius: 16px;
|
|
497
|
+
padding: 32px;
|
|
498
|
+
max-width: 500px;
|
|
499
|
+
width: 90%;
|
|
500
|
+
text-align: center;
|
|
501
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
502
|
+
animation: slideUp 0.3s ease-out;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@keyframes slideUp {
|
|
506
|
+
from {
|
|
507
|
+
transform: translateY(20px);
|
|
508
|
+
opacity: 0;
|
|
509
|
+
}
|
|
510
|
+
to {
|
|
511
|
+
transform: translateY(0);
|
|
512
|
+
opacity: 1;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.error-icon {
|
|
517
|
+
color: #ff4444;
|
|
518
|
+
margin-bottom: 16px;
|
|
519
|
+
display: flex;
|
|
520
|
+
justify-content: center;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.error-icon svg {
|
|
524
|
+
width: 64px;
|
|
525
|
+
height: 64px;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.error-title {
|
|
529
|
+
color: #ff4444;
|
|
530
|
+
font-size: 24px;
|
|
531
|
+
font-weight: bold;
|
|
532
|
+
margin: 0 0 16px 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.error-message {
|
|
536
|
+
color: #ffffff;
|
|
537
|
+
font-size: 16px;
|
|
538
|
+
line-height: 1.5;
|
|
539
|
+
margin: 0 0 24px 0;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.error-close-btn {
|
|
543
|
+
margin-top: 8px;
|
|
544
|
+
min-width: 120px;
|
|
545
|
+
}
|
|
546
|
+
</style>
|
|
547
|
+
</body>
|
|
548
|
+
|
|
549
|
+
</html>
|