sovereign 1.0.0b125__py3-none-any.whl → 1.0.0b127__py3-none-any.whl

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.

Potentially problematic release.


This version of sovereign might be problematic. Click here for more details.

@@ -0,0 +1,642 @@
1
+ /* Resources page JavaScript functionality */
2
+
3
+ // Global variables
4
+ let resourceNames = [];
5
+ let resources = [];
6
+ let filteredResources = [];
7
+ let currentResourceData = null;
8
+ let currentChunkSize = 1000; // lines
9
+ let currentChunkIndex = 0;
10
+ let totalChunks = 0;
11
+ let resourceType = '';
12
+
13
+ const perPage = 10;
14
+ let currentPage = 1;
15
+
16
+ // Initialize the resources functionality
17
+ function initializeResources(resourceNamesArray, resourceTypeString) {
18
+ resourceNames = resourceNamesArray;
19
+ resourceType = resourceTypeString;
20
+ resources = resourceNames.map((name, index) => ({ name: name, index: index }));
21
+ filteredResources = [...resources];
22
+
23
+ // Initialize pagination
24
+ renderPage(currentPage);
25
+
26
+ // Set up event listeners
27
+ setupEventListeners();
28
+ }
29
+
30
+ // Function to set envoy_version cookie and reload page
31
+ function setEnvoyVersion(version) {
32
+ document.cookie = `envoy_version=${version}; path=/ui/resources/; max-age=31536000`;
33
+ window.location.reload();
34
+ }
35
+
36
+ // Set up event listeners
37
+ function setupEventListeners() {
38
+ // Pagination event listeners
39
+ const prevBtn = document.getElementById('prev-btn');
40
+ const nextBtn = document.getElementById('next-btn');
41
+
42
+ if (prevBtn) {
43
+ prevBtn.addEventListener('click', () => {
44
+ if (currentPage > 1) {
45
+ currentPage--;
46
+ renderPage(currentPage);
47
+ }
48
+ });
49
+ }
50
+
51
+ if (nextBtn) {
52
+ nextBtn.addEventListener('click', () => {
53
+ const totalPages = Math.ceil(filteredResources.length / perPage);
54
+ if (currentPage < totalPages) {
55
+ currentPage++;
56
+ renderPage(currentPage);
57
+ }
58
+ });
59
+ }
60
+
61
+ // Close side panel when clicking outside of it
62
+ document.addEventListener('click', function(event) {
63
+ const sidePanel = document.getElementById('sidePanel');
64
+ const backdrop = document.getElementById('sidePanelBackdrop');
65
+ const isClickInsidePanel = sidePanel && sidePanel.contains(event.target);
66
+ const isResourceLink = event.target.closest('.panel-block');
67
+ const isBackdropClick = event.target === backdrop;
68
+
69
+ if ((isBackdropClick || (!isClickInsidePanel && !isResourceLink)) && sidePanel && sidePanel.classList.contains('is-active')) {
70
+ closeSidePanel();
71
+ }
72
+ });
73
+
74
+ // Keyboard shortcuts
75
+ document.addEventListener('keydown', function(event) {
76
+ if (event.key === 'Escape') {
77
+ const sidePanel = document.getElementById('sidePanel');
78
+ if (sidePanel && sidePanel.classList.contains('is-active')) {
79
+ closeSidePanel();
80
+ }
81
+ }
82
+ });
83
+ }
84
+
85
+ // Side panel functionality
86
+ function openSidePanel() {
87
+ const sidePanel = document.getElementById('sidePanel');
88
+ const backdrop = document.getElementById('sidePanelBackdrop');
89
+
90
+ if (sidePanel) sidePanel.classList.add('is-active');
91
+ if (backdrop) backdrop.classList.add('is-active');
92
+ document.body.classList.add('side-panel-open');
93
+ }
94
+
95
+ function closeSidePanel() {
96
+ const sidePanel = document.getElementById('sidePanel');
97
+ const backdrop = document.getElementById('sidePanelBackdrop');
98
+
99
+ if (sidePanel) sidePanel.classList.remove('is-active');
100
+ if (backdrop) backdrop.classList.remove('is-active');
101
+ document.body.classList.remove('side-panel-open');
102
+ }
103
+
104
+ function showLoading() {
105
+ const content = document.getElementById('sidePanelContent');
106
+ if (content) {
107
+ content.innerHTML = `
108
+ <div style="text-align: center; padding: 2rem;">
109
+ <div class="loading-spinner"></div>
110
+ <p style="margin-top: 1rem;">Loading resource data...</p>
111
+ </div>
112
+ `;
113
+ }
114
+ }
115
+
116
+ function showError(message) {
117
+ const content = document.getElementById('sidePanelContent');
118
+ if (content) {
119
+ content.innerHTML = `
120
+ <div class="notification is-danger">
121
+ <strong>Error:</strong> ${message}
122
+ </div>
123
+ `;
124
+ }
125
+ }
126
+
127
+ function escapeHtml(text) {
128
+ const div = document.createElement('div');
129
+ div.textContent = text;
130
+ return div.innerHTML;
131
+ }
132
+
133
+ function syntaxHighlight(json) {
134
+ if (typeof json !== 'string') {
135
+ json = JSON.stringify(json, null, 2);
136
+ }
137
+
138
+ // Escape HTML first
139
+ json = escapeHtml(json);
140
+
141
+ // Apply syntax highlighting
142
+ json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
143
+ let cls = 'json-number';
144
+ if (/^"/.test(match)) {
145
+ if (/:$/.test(match)) {
146
+ cls = 'json-key';
147
+ // Remove the colon from the match for styling, we'll add it back
148
+ match = match.slice(0, -1);
149
+ return '<span class="' + cls + '">' + match + '</span><span class="json-punctuation">:</span>';
150
+ } else {
151
+ cls = 'json-string';
152
+ }
153
+ } else if (/true|false/.test(match)) {
154
+ cls = 'json-boolean';
155
+ } else if (/null/.test(match)) {
156
+ cls = 'json-null';
157
+ }
158
+ return '<span class="' + cls + '">' + match + '</span>';
159
+ });
160
+
161
+ // Highlight punctuation (brackets, braces, commas)
162
+ json = json.replace(/([{}[\],])/g, '<span class="json-punctuation">$1</span>');
163
+
164
+ return json;
165
+ }
166
+
167
+ function makeJsonCollapsible(element) {
168
+ // This function would add collapsible functionality
169
+ // For now, we'll keep it simple and just return the element
170
+ // Future enhancement: add click handlers for { } and [ ] to collapse/expand
171
+ return element;
172
+ }
173
+
174
+ function copyToClipboard(text) {
175
+ if (navigator.clipboard && window.isSecureContext) {
176
+ // Use the modern clipboard API
177
+ navigator.clipboard.writeText(text).then(() => {
178
+ showCopySuccess();
179
+ }).catch(err => {
180
+ console.error('Failed to copy: ', err);
181
+ fallbackCopyTextToClipboard(text);
182
+ });
183
+ } else {
184
+ // Fallback for older browsers
185
+ fallbackCopyTextToClipboard(text);
186
+ }
187
+ }
188
+
189
+ function fallbackCopyTextToClipboard(text) {
190
+ const textArea = document.createElement("textarea");
191
+ textArea.value = text;
192
+ textArea.style.top = "0";
193
+ textArea.style.left = "0";
194
+ textArea.style.position = "fixed";
195
+ document.body.appendChild(textArea);
196
+ textArea.focus();
197
+ textArea.select();
198
+
199
+ try {
200
+ const successful = document.execCommand('copy');
201
+ if (successful) {
202
+ showCopySuccess();
203
+ }
204
+ } catch (err) {
205
+ console.error('Fallback: Oops, unable to copy', err);
206
+ }
207
+
208
+ document.body.removeChild(textArea);
209
+ }
210
+
211
+ function showCopySuccess() {
212
+ const copyBtn = document.querySelector('.copy-button');
213
+ if (copyBtn) {
214
+ const originalText = copyBtn.textContent;
215
+ copyBtn.textContent = 'Copied!';
216
+ copyBtn.style.backgroundColor = '#48c774';
217
+ setTimeout(() => {
218
+ copyBtn.textContent = originalText;
219
+ copyBtn.style.backgroundColor = '#433fca';
220
+ }, 2000);
221
+ }
222
+ }
223
+
224
+ // Helper function to generate preferences section HTML
225
+ function getPreferencesHtml() {
226
+ const currentThreshold = localStorage.getItem('jsonSizeThreshold') || 0.5;
227
+ const autoChunk = localStorage.getItem('autoChunkLargeJson') === 'true';
228
+
229
+ return `
230
+ <details class="has-background-grey-darker">
231
+ <summary class="has-background-grey-dark has-text-white-ter p-3 is-clickable" style="cursor: pointer;">
232
+ <span class="is-size-7">Display Preferences</span>
233
+ </summary>
234
+ <div class="box has-background-grey-dark has-text-white-ter m-0" style="border-radius: 0;">
235
+ <div class="content is-small">
236
+ <div class="field">
237
+ <label class="checkbox has-text-white-ter">
238
+ <input type="checkbox" id="autoChunkPreference" ${autoChunk ? 'checked' : ''}
239
+ onchange="event.stopPropagation(); updateAutoChunkPreference(this.checked)"
240
+ class="mr-2">
241
+ Always use chunked view for large files
242
+ </label>
243
+ </div>
244
+ <div class="field is-grouped">
245
+ <div class="control">
246
+ <label class="label is-small has-text-white-ter">Size threshold:</label>
247
+ </div>
248
+ <div class="control">
249
+ <div class="select is-dark is-small">
250
+ <select id="thresholdSelector" value="${currentThreshold}"
251
+ onchange="event.stopPropagation(); updateThresholdPreference(this.value)">
252
+ <option value="0.25" ${currentThreshold == 0.25 ? 'selected' : ''}>250 KB</option>
253
+ <option value="0.5" ${currentThreshold == 0.5 ? 'selected' : ''}>500 KB</option>
254
+ <option value="1" ${currentThreshold == 1 ? 'selected' : ''}>1 MB</option>
255
+ <option value="2" ${currentThreshold == 2 ? 'selected' : ''}>2 MB</option>
256
+ <option value="5" ${currentThreshold == 5 ? 'selected' : ''}>5 MB</option>
257
+ </select>
258
+ </div>
259
+ </div>
260
+ </div>
261
+ <div class="field is-grouped">
262
+ <div class="control">
263
+ <button class="button is-link is-small" onclick="event.stopPropagation(); showChunkedJson()">
264
+ Switch to Chunked View
265
+ </button>
266
+ </div>
267
+ <div class="control">
268
+ <button class="button is-success is-small" onclick="event.stopPropagation(); showFullJson()">
269
+ Switch to Full View
270
+ </button>
271
+ </div>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ </details>
276
+ `;
277
+ }
278
+
279
+ // Helper functions to update preferences
280
+ function updateAutoChunkPreference(checked) {
281
+ localStorage.setItem('autoChunkLargeJson', checked.toString());
282
+ }
283
+
284
+ function updateThresholdPreference(threshold) {
285
+ localStorage.setItem('jsonSizeThreshold', threshold);
286
+ }
287
+
288
+ async function loadResourceInSidePanel(resourceType, resourceName) {
289
+ const titleElement = document.getElementById('sidePanelTitle');
290
+ if (titleElement) {
291
+ titleElement.textContent = `${resourceType}: ${resourceName}`;
292
+ }
293
+ openSidePanel();
294
+ showLoading();
295
+
296
+ try {
297
+ // Get current URL parameters to maintain context
298
+ const urlParams = new URLSearchParams(window.location.search);
299
+ const region = urlParams.get('region') || '';
300
+ const apiVersion = urlParams.get('api_version') || 'v2';
301
+
302
+ // Build the fetch URL with current parameters
303
+ let fetchUrl = `/ui/resources/${resourceType}/${encodeURIComponent(resourceName)}`;
304
+ const params = new URLSearchParams();
305
+ if (region) params.append('region', region);
306
+ params.append('api_version', apiVersion);
307
+
308
+ if (params.toString()) {
309
+ fetchUrl += '?' + params.toString();
310
+ }
311
+
312
+ const response = await fetch(fetchUrl, {
313
+ credentials: 'same-origin' // Include cookies in the request
314
+ });
315
+
316
+ if (!response.ok) {
317
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
318
+ }
319
+
320
+ const data = await response.json();
321
+ const jsonString = JSON.stringify(data, null, 2);
322
+ const sizeKB = (new Blob([jsonString]).size / 1024).toFixed(1);
323
+ const sizeMB = sizeKB / 1024;
324
+
325
+ // Store the data globally
326
+ currentResourceData = {
327
+ data: data,
328
+ jsonString: jsonString,
329
+ sizeKB: sizeKB,
330
+ sizeMB: sizeMB
331
+ };
332
+
333
+ // Check if the JSON is large and show warning/chunked view
334
+ // Use a lower threshold (500KB) to be more conservative about browser performance
335
+ const threshold = localStorage.getItem('jsonSizeThreshold') || 0.5; // MB
336
+ if (sizeMB > threshold) {
337
+ // Check user preference for auto-chunking
338
+ const autoChunk = localStorage.getItem('autoChunkLargeJson') === 'true';
339
+ if (autoChunk) {
340
+ showChunkedJson();
341
+ } else {
342
+ showLargeJsonWarning(resourceType, resourceName);
343
+ }
344
+ } else {
345
+ showFullJson();
346
+ }
347
+ } catch (error) {
348
+ console.error('Error loading resource:', error);
349
+ showError(error.message);
350
+ }
351
+ }
352
+
353
+ function showLargeJsonWarning(resourceType, resourceName) {
354
+ const sizeKB = currentResourceData.sizeKB;
355
+ const sizeMB = currentResourceData.sizeMB;
356
+
357
+ const content = document.getElementById('sidePanelContent');
358
+ if (content) {
359
+ content.innerHTML = `
360
+ <div class="notification is-warning">
361
+ <p>
362
+ <strong>Large JSON detected (${sizeKB} KB / ${sizeMB.toFixed(1)} MB)</strong><br>
363
+ Loading this much data may slow down your browser. <br>
364
+ </p>
365
+
366
+ Choose an option:
367
+ <button class="button is-dark is-small" onclick="event.stopPropagation(); showChunkedJson()">
368
+ View in Chunks
369
+ </button>
370
+ <button class="button is-dark is-small" onclick="event.stopPropagation(); showFullJson()">
371
+ Load Full JSON
372
+ </button>
373
+ </div>
374
+ `;
375
+ }
376
+ }
377
+
378
+ function showChunkedJson() {
379
+ const lines = currentResourceData.jsonString.split('\n');
380
+ totalChunks = Math.ceil(lines.length / currentChunkSize);
381
+ currentChunkIndex = 0;
382
+
383
+ renderChunkedJson();
384
+ }
385
+
386
+ function showFullJson() {
387
+ // Store the JSON string globally for copying
388
+ window.currentJsonData = currentResourceData.jsonString;
389
+
390
+ const content = document.getElementById('sidePanelContent');
391
+ if (content) {
392
+ content.innerHTML = `
393
+ <div class="json-container">
394
+ <div class="json-header">
395
+ <span style="color: #d4d4d4; font-size: 0.875rem;">JSON Response (${currentResourceData.sizeKB} KB) - Full View</span>
396
+ <button class="button is-primary is-small" onclick="event.stopPropagation(); copyToClipboard(window.currentJsonData)">
397
+ Copy JSON
398
+ </button>
399
+ </div>
400
+ ${getPreferencesHtml()}
401
+ <div class="json-content">${syntaxHighlight(currentResourceData.data)}</div>
402
+ </div>
403
+ `;
404
+ }
405
+ }
406
+
407
+ function renderChunkedJson() {
408
+ const lines = currentResourceData.jsonString.split('\n');
409
+ const startLine = currentChunkIndex * currentChunkSize;
410
+ const endLine = Math.min(startLine + currentChunkSize, lines.length);
411
+ const chunkLines = lines.slice(startLine, endLine);
412
+ const chunkText = chunkLines.join('\n');
413
+
414
+ // Store current chunk for copying
415
+ window.currentJsonData = currentResourceData.jsonString; // Always allow copying full JSON
416
+ window.currentChunkData = chunkText;
417
+
418
+ const content = document.getElementById('sidePanelContent');
419
+ if (content) {
420
+ content.innerHTML = `
421
+ <div class="json-container">
422
+ <div class="json-header">
423
+ <span style="color: #d4d4d4; font-size: 0.875rem;">JSON Response (${currentResourceData.sizeKB} KB) - Chunked View</span>
424
+ <button class="button is-primary is-small" onclick="event.stopPropagation(); copyToClipboard(window.currentJsonData)">
425
+ Copy Full JSON
426
+ </button>
427
+ </div>
428
+ ${getPreferencesHtml()}
429
+ <div class="json-chunk-controls p-4">
430
+ <div class="field is-grouped is-grouped-multiline">
431
+ <div class="control">
432
+ <label class="label is-small has-text-white">Lines per chunk:</label>
433
+ <div class="select is-dark is-small">
434
+ <select onchange="event.stopPropagation(); changeChunkSize(this.value)">
435
+ <option value="50" ${currentChunkSize === 50 ? 'selected' : ''}>50</option>
436
+ <option value="100" ${currentChunkSize === 100 ? 'selected' : ''}>100</option>
437
+ <option value="250" ${currentChunkSize === 250 ? 'selected' : ''}>250</option>
438
+ <option value="500" ${currentChunkSize === 500 ? 'selected' : ''}>500</option>
439
+ <option value="1000" ${currentChunkSize === 1000 ? 'selected' : ''}>1000</option>
440
+ <option value="2000" ${currentChunkSize === 2000 ? 'selected' : ''}>2000</option>
441
+ <option value="5000" ${currentChunkSize === 5000 ? 'selected' : ''}>5000</option>
442
+ </select>
443
+ </div>
444
+ </div>
445
+ <div class="control">
446
+ <div class="buttons">
447
+ <button class="button is-primary is-small" onclick="event.stopPropagation(); previousChunk()" ${currentChunkIndex === 0 ? 'disabled' : ''}>
448
+ ← Previous
449
+ </button>
450
+ <button class="button is-primary is-small" onclick="event.stopPropagation(); nextChunk()" ${currentChunkIndex >= totalChunks - 1 ? 'disabled' : ''}>
451
+ Next →
452
+ </button>
453
+ <button class="button is-info is-small" onclick="event.stopPropagation(); copyToClipboard(window.currentChunkData)">
454
+ Copy Chunk
455
+ </button>
456
+ </div>
457
+ </div>
458
+ </div>
459
+ </div>
460
+ <progress class="progress is-primary is-small" value="${endLine}" max="${lines.length}">
461
+ Chunk ${currentChunkIndex + 1} of ${totalChunks}
462
+ </progress>
463
+ <div class="json-content">${syntaxHighlight(chunkText)}</div>
464
+ </div>
465
+ `;
466
+ }
467
+ }
468
+
469
+ function changeChunkSize(newSize) {
470
+ currentChunkSize = parseInt(newSize);
471
+ const lines = currentResourceData.jsonString.split('\n');
472
+ totalChunks = Math.ceil(lines.length / currentChunkSize);
473
+
474
+ // Adjust current chunk index to stay roughly in the same area
475
+ const currentLine = currentChunkIndex * currentChunkSize;
476
+ currentChunkIndex = Math.floor(currentLine / currentChunkSize);
477
+ currentChunkIndex = Math.min(currentChunkIndex, totalChunks - 1);
478
+
479
+ renderChunkedJson();
480
+ }
481
+
482
+ function previousChunk() {
483
+ if (currentChunkIndex > 0) {
484
+ currentChunkIndex--;
485
+ renderChunkedJson();
486
+ }
487
+ }
488
+
489
+ function nextChunk() {
490
+ if (currentChunkIndex < totalChunks - 1) {
491
+ currentChunkIndex++;
492
+ renderChunkedJson();
493
+ }
494
+ }
495
+
496
+ async function loadVirtualHostInSidePanel(routeConfiguration, virtualHost) {
497
+ const titleElement = document.getElementById('sidePanelTitle');
498
+ if (titleElement) {
499
+ titleElement.textContent = `Virtual Host: ${virtualHost}`;
500
+ }
501
+ openSidePanel();
502
+ showLoading();
503
+
504
+ try {
505
+ // Get current URL parameters to maintain context
506
+ const urlParams = new URLSearchParams(window.location.search);
507
+ const region = urlParams.get('region') || '';
508
+ const apiVersion = urlParams.get('api_version') || 'v2';
509
+
510
+ // Build the fetch URL with current parameters
511
+ let fetchUrl = `/ui/resources/routes/${encodeURIComponent(routeConfiguration)}/${encodeURIComponent(virtualHost)}`;
512
+ const params = new URLSearchParams();
513
+ if (region) params.append('region', region);
514
+ params.append('api_version', apiVersion);
515
+
516
+ if (params.toString()) {
517
+ fetchUrl += '?' + params.toString();
518
+ }
519
+
520
+ const response = await fetch(fetchUrl, {
521
+ credentials: 'same-origin' // Include cookies in the request
522
+ });
523
+
524
+ if (!response.ok) {
525
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
526
+ }
527
+
528
+ const data = await response.json();
529
+ const jsonString = JSON.stringify(data, null, 2);
530
+ const sizeKB = (new Blob([jsonString]).size / 1024).toFixed(1);
531
+ const sizeMB = sizeKB / 1024;
532
+
533
+ // Store the data globally
534
+ currentResourceData = {
535
+ data: data,
536
+ jsonString: jsonString,
537
+ sizeKB: sizeKB,
538
+ sizeMB: sizeMB
539
+ };
540
+
541
+ // Check if the JSON is large and show warning/chunked view
542
+ const threshold = localStorage.getItem('jsonSizeThreshold') || 0.5; // MB
543
+ if (sizeMB > threshold) {
544
+ // Check user preference for auto-chunking
545
+ const autoChunk = localStorage.getItem('autoChunkLargeJson') === 'true';
546
+ if (autoChunk) {
547
+ showChunkedJson();
548
+ } else {
549
+ showLargeJsonWarning('Virtual Host', virtualHost);
550
+ }
551
+ } else {
552
+ showFullJson();
553
+ }
554
+ } catch (error) {
555
+ console.error('Error loading virtual host:', error);
556
+ showError(error.message);
557
+ }
558
+ }
559
+
560
+ function filter_results(id) {
561
+ const input = document.getElementById(id);
562
+ if (!input) return;
563
+
564
+ const filter = input.value.toLowerCase();
565
+
566
+ filteredResources = resources.filter((res) => {
567
+ const name = res.name || "";
568
+ return name.toLowerCase().includes(filter);
569
+ });
570
+
571
+ currentPage = 1;
572
+ renderPage(currentPage);
573
+
574
+ // Update the resource count display
575
+ const countElement = document.getElementById('resource-count');
576
+ if (countElement) {
577
+ const count = filteredResources.length;
578
+ const plural = count === 1 ? 'resource' : 'resources';
579
+ countElement.textContent = `${count} ${plural}`;
580
+ }
581
+ }
582
+
583
+ function renderPage(page) {
584
+ const container = document.getElementById('resource-container');
585
+ if (!container) return;
586
+
587
+ container.innerHTML = '';
588
+
589
+ const start = (page - 1) * perPage;
590
+ const end = start + perPage;
591
+ const pageItems = filteredResources.slice(start, end);
592
+
593
+ for (const resource of pageItems) {
594
+ const name = resource.name;
595
+ const item = document.createElement('a');
596
+ item.className = 'panel-block has-text-weight-medium';
597
+ item.href = '#';
598
+ item.onclick = (e) => {
599
+ e.preventDefault();
600
+ loadResourceInSidePanel(resourceType, name);
601
+ };
602
+ item.innerHTML = `
603
+ <span class="panel-icon">
604
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
605
+ </span>
606
+ ${name}
607
+ `;
608
+ container.appendChild(item);
609
+ }
610
+ renderPaginationControls();
611
+ }
612
+
613
+ function renderPaginationControls() {
614
+ const totalPages = Math.ceil(filteredResources.length / perPage);
615
+ const pageList = document.getElementById('page-numbers');
616
+ if (!pageList) return;
617
+
618
+ pageList.innerHTML = '';
619
+
620
+ for (let i = 1; i <= totalPages; i++) {
621
+ const li = document.createElement('li');
622
+ const a = document.createElement('a');
623
+ a.className = 'pagination-link';
624
+ if (i === currentPage) {
625
+ a.classList.add('has-background-grey-lighter');
626
+ a.classList.add('has-background-black-bis');
627
+ }
628
+ a.textContent = i;
629
+ a.addEventListener('click', () => {
630
+ currentPage = i;
631
+ renderPage(currentPage);
632
+ });
633
+ li.appendChild(a);
634
+ pageList.appendChild(li);
635
+ }
636
+
637
+ const prevBtn = document.getElementById('prev-btn');
638
+ const nextBtn = document.getElementById('next-btn');
639
+
640
+ if (prevBtn) prevBtn.disabled = currentPage === 1;
641
+ if (nextBtn) nextBtn.disabled = currentPage === totalPages;
642
+ }