@kaikybrofc/omnizap-system 2.1.8

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 (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. package/scripts/release.sh +129 -0
@@ -0,0 +1,1019 @@
1
+ (() => {
2
+ const root = document.getElementById('catalog-root');
3
+ if (!root) return;
4
+
5
+ const parseIntSafe = (value, fallback) => {
6
+ const parsed = Number.parseInt(String(value || ''), 10);
7
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
8
+ return parsed;
9
+ };
10
+
11
+ const CONFIG = {
12
+ apiBasePath: root.dataset.apiBasePath || '/api/sticker-packs',
13
+ orphanApiPath: root.dataset.orphanApiPath || '/api/sticker-packs/orphan-stickers',
14
+ webPath: root.dataset.webPath || '/stickers',
15
+ dataPublicPath: root.dataset.dataPublicPath || '/data',
16
+ initialPackKey: String(root.dataset.initialPackKey || '').trim() || null,
17
+ defaultLimit: parseIntSafe(root.dataset.defaultLimit, 24),
18
+ defaultOrphanLimit: parseIntSafe(root.dataset.defaultOrphanLimit, 120),
19
+ };
20
+
21
+ const state = {
22
+ q: '',
23
+ catalogLoaded: false,
24
+ visibility: 'public',
25
+ categories: [],
26
+ packs: {
27
+ offset: 0,
28
+ limit: CONFIG.defaultLimit,
29
+ hasMore: true,
30
+ loading: false,
31
+ items: [],
32
+ },
33
+ orphan: {
34
+ page: 1,
35
+ limit: CONFIG.defaultOrphanLimit,
36
+ totalPages: 1,
37
+ totalItems: 0,
38
+ loading: false,
39
+ items: [],
40
+ },
41
+ panelPagination: {
42
+ page: 1,
43
+ perPage: 100,
44
+ totalPages: 1,
45
+ },
46
+ selectedPack: null,
47
+ };
48
+
49
+ const els = {
50
+ hero: document.getElementById('catalog-hero'),
51
+ form: document.getElementById('search-form'),
52
+ search: document.getElementById('search-input'),
53
+ visibility: document.getElementById('visibility-input'),
54
+ categories: document.getElementById('categories-input'),
55
+ categoriesPicker: document.getElementById('categories-picker'),
56
+ categoriesSearch: document.getElementById('categories-search'),
57
+ categoriesChips: document.getElementById('categories-chips'),
58
+ categoriesOptions: document.getElementById('categories-options'),
59
+ status: document.getElementById('status'),
60
+ grid: document.getElementById('grid'),
61
+ more: document.getElementById('load-more'),
62
+ orphanStatus: document.getElementById('orphan-status'),
63
+ orphanGrid: document.getElementById('orphan-grid'),
64
+ orphanPagination: document.getElementById('orphan-pagination'),
65
+ orphanPrev: document.getElementById('orphan-prev'),
66
+ orphanNext: document.getElementById('orphan-next'),
67
+ orphanPageInfo: document.getElementById('orphan-page-info'),
68
+ orphanMore: document.getElementById('orphan-load-more'),
69
+ panelPagination: document.getElementById('panel-pagination'),
70
+ panelPrev: document.getElementById('panel-prev'),
71
+ panelNext: document.getElementById('panel-next'),
72
+ panelPageInfo: document.getElementById('panel-page-info'),
73
+ panel: document.getElementById('panel'),
74
+ panelTitle: document.getElementById('panel-title'),
75
+ panelSub: document.getElementById('panel-subtitle'),
76
+ panelChip: document.getElementById('panel-chip'),
77
+ panelError: document.getElementById('panel-error'),
78
+ panelClose: document.getElementById('panel-close'),
79
+ copy: document.getElementById('copy-link'),
80
+ useWhatsAppLink: document.getElementById('use-whatsapp-link'),
81
+ stickers: document.getElementById('stickers'),
82
+ packPage: document.getElementById('pack-page'),
83
+ packPageTitle: document.getElementById('pack-page-title'),
84
+ packPageSub: document.getElementById('pack-page-subtitle'),
85
+ packPageChip: document.getElementById('pack-page-chip'),
86
+ packPageStatus: document.getElementById('pack-page-status'),
87
+ packPageGrid: document.getElementById('pack-page-stickers'),
88
+ packPageBack: document.getElementById('pack-page-back'),
89
+ packPageCopy: document.getElementById('pack-page-copy'),
90
+ packPageWhatsApp: document.getElementById('pack-page-whatsapp'),
91
+ };
92
+
93
+ if (
94
+ !els.hero ||
95
+ !els.form ||
96
+ !els.search ||
97
+ !els.visibility ||
98
+ !els.categories ||
99
+ !els.categoriesPicker ||
100
+ !els.categoriesSearch ||
101
+ !els.categoriesChips ||
102
+ !els.categoriesOptions ||
103
+ !els.status ||
104
+ !els.grid ||
105
+ !els.more ||
106
+ !els.orphanStatus ||
107
+ !els.orphanGrid ||
108
+ !els.orphanPrev ||
109
+ !els.orphanNext ||
110
+ !els.orphanPageInfo ||
111
+ !els.panel ||
112
+ !els.panelTitle ||
113
+ !els.panelSub ||
114
+ !els.panelChip ||
115
+ !els.panelError ||
116
+ !els.panelPrev ||
117
+ !els.panelNext ||
118
+ !els.panelPageInfo ||
119
+ !els.copy ||
120
+ !els.useWhatsAppLink ||
121
+ !els.stickers ||
122
+ !els.packPage ||
123
+ !els.packPageTitle ||
124
+ !els.packPageSub ||
125
+ !els.packPageChip ||
126
+ !els.packPageStatus ||
127
+ !els.packPageGrid ||
128
+ !els.packPageBack ||
129
+ !els.packPageCopy ||
130
+ !els.packPageWhatsApp
131
+ ) {
132
+ return;
133
+ }
134
+
135
+ const panelModal = window.bootstrap?.Modal
136
+ ? window.bootstrap.Modal.getOrCreateInstance(els.panel)
137
+ : null;
138
+
139
+ let shouldReplaceStateOnHide = false;
140
+
141
+ els.panelPrev.disabled = true;
142
+ els.panelNext.disabled = true;
143
+ els.orphanPrev.disabled = true;
144
+ els.orphanNext.disabled = true;
145
+ if (els.orphanMore) els.orphanMore.hidden = true;
146
+
147
+ const toApi = (path, searchParams) => {
148
+ const url = new URL(path, window.location.origin);
149
+ if (searchParams) {
150
+ Object.entries(searchParams).forEach(([key, value]) => {
151
+ if (value !== undefined && value !== null && value !== '') {
152
+ url.searchParams.set(key, String(value));
153
+ }
154
+ });
155
+ }
156
+ return url.toString();
157
+ };
158
+
159
+ const extractPackKeyFromPath = () => {
160
+ const path = window.location.pathname;
161
+ if (!path.startsWith(CONFIG.webPath + '/')) return '';
162
+ try {
163
+ return decodeURIComponent(path.slice((CONFIG.webPath + '/').length).split('/')[0] || '');
164
+ } catch {
165
+ return '';
166
+ }
167
+ };
168
+
169
+ const getSelectedCategories = () =>
170
+ Array.from(els.categories.selectedOptions || [])
171
+ .map((option) => String(option.value || '').trim())
172
+ .filter(Boolean);
173
+
174
+ const normalizeCategorySearch = (value) =>
175
+ String(value || '')
176
+ .toLowerCase()
177
+ .normalize('NFD')
178
+ .replace(/[\u0300-\u036f]/g, '')
179
+ .trim();
180
+
181
+ const categoryCatalog = Array.from(els.categories.options || []).map((option) => ({
182
+ value: String(option.value || '').trim(),
183
+ label: String(option.textContent || option.value || '').trim(),
184
+ }));
185
+
186
+ const syncCategorySelect = (selectedValues) => {
187
+ const selected = new Set((selectedValues || []).map((value) => String(value || '').trim()).filter(Boolean));
188
+ Array.from(els.categories.options || []).forEach((option) => {
189
+ option.selected = selected.has(String(option.value || '').trim());
190
+ });
191
+ state.categories = getSelectedCategories();
192
+ };
193
+
194
+ const renderCategoryChips = () => {
195
+ els.categoriesChips.innerHTML = '';
196
+ if (!state.categories.length) {
197
+ const empty = document.createElement('span');
198
+ empty.className = 'categories-empty';
199
+ empty.textContent = 'Nenhuma categoria selecionada';
200
+ els.categoriesChips.appendChild(empty);
201
+ return;
202
+ }
203
+
204
+ state.categories.forEach((value) => {
205
+ const entry = categoryCatalog.find((item) => item.value === value);
206
+ const chip = document.createElement('button');
207
+ chip.type = 'button';
208
+ chip.className = 'category-chip';
209
+ chip.dataset.value = value;
210
+ chip.setAttribute('aria-label', 'Remover categoria ' + (entry?.label || value));
211
+ chip.innerHTML =
212
+ '<span class="category-chip-label">' +
213
+ (entry?.label || value) +
214
+ '</span><i class="fa-solid fa-xmark category-chip-remove" aria-hidden="true"></i>';
215
+ els.categoriesChips.appendChild(chip);
216
+ });
217
+ };
218
+
219
+ const renderCategoryOptions = () => {
220
+ const query = normalizeCategorySearch(els.categoriesSearch.value);
221
+ const selected = new Set(state.categories);
222
+ const filtered = categoryCatalog.filter((entry) => {
223
+ if (!entry.value) return false;
224
+ if (!query) return true;
225
+ return normalizeCategorySearch(entry.label).includes(query) || normalizeCategorySearch(entry.value).includes(query);
226
+ });
227
+
228
+ els.categoriesOptions.innerHTML = '';
229
+
230
+ if (!filtered.length) {
231
+ const empty = document.createElement('div');
232
+ empty.className = 'categories-options-empty';
233
+ empty.textContent = 'Nenhuma categoria encontrada';
234
+ els.categoriesOptions.appendChild(empty);
235
+ return;
236
+ }
237
+
238
+ filtered.forEach((entry) => {
239
+ const button = document.createElement('button');
240
+ button.type = 'button';
241
+ button.className = 'category-option' + (selected.has(entry.value) ? ' selected' : '');
242
+ button.dataset.value = entry.value;
243
+ button.innerHTML =
244
+ '<span class="category-option-label">' +
245
+ entry.label +
246
+ '</span><i class="fa-solid fa-check category-option-check" aria-hidden="true"></i>';
247
+ els.categoriesOptions.appendChild(button);
248
+ });
249
+ };
250
+
251
+ const applyFilters = async () => {
252
+ state.q = els.search.value.trim();
253
+ state.visibility = els.visibility.value;
254
+ state.categories = getSelectedCategories();
255
+ await Promise.all([listPacks({ reset: true }), listOrphanStickers({ reset: true })]);
256
+ };
257
+
258
+ let filterRefreshTimer = null;
259
+ const scheduleFilterRefresh = () => {
260
+ if (filterRefreshTimer) {
261
+ clearTimeout(filterRefreshTimer);
262
+ }
263
+ filterRefreshTimer = setTimeout(() => {
264
+ filterRefreshTimer = null;
265
+ void applyFilters();
266
+ }, 120);
267
+ };
268
+
269
+ const toggleCategory = (value) => {
270
+ const normalized = String(value || '').trim();
271
+ if (!normalized) return;
272
+ const selected = new Set(state.categories);
273
+ if (selected.has(normalized)) {
274
+ selected.delete(normalized);
275
+ } else {
276
+ selected.add(normalized);
277
+ }
278
+ syncCategorySelect(Array.from(selected));
279
+ renderCategoryChips();
280
+ renderCategoryOptions();
281
+ scheduleFilterRefresh();
282
+ };
283
+
284
+ const initCategoriesPicker = () => {
285
+ syncCategorySelect(getSelectedCategories());
286
+ renderCategoryChips();
287
+ renderCategoryOptions();
288
+
289
+ els.categoriesSearch.addEventListener('input', () => {
290
+ renderCategoryOptions();
291
+ });
292
+
293
+ els.categoriesOptions.addEventListener('click', (event) => {
294
+ const option = event.target.closest('.category-option');
295
+ if (!option) return;
296
+ toggleCategory(option.dataset.value);
297
+ });
298
+
299
+ els.categoriesChips.addEventListener('click', (event) => {
300
+ const chip = event.target.closest('.category-chip');
301
+ if (!chip) return;
302
+ toggleCategory(chip.dataset.value);
303
+ });
304
+ };
305
+
306
+ initCategoriesPicker();
307
+
308
+ const showCatalogView = () => {
309
+ els.packPage.hidden = true;
310
+ els.hero.hidden = false;
311
+ document.getElementById('packs-section').hidden = false;
312
+ document.getElementById('orphan-section').hidden = false;
313
+ };
314
+
315
+ const showPackPageView = () => {
316
+ els.packPage.hidden = false;
317
+ els.hero.hidden = true;
318
+ document.getElementById('packs-section').hidden = true;
319
+ document.getElementById('orphan-section').hidden = true;
320
+ };
321
+
322
+ const setStatus = (text) => {
323
+ els.status.textContent = text || '';
324
+ };
325
+
326
+ const setOrphanStatus = (text) => {
327
+ els.orphanStatus.textContent = text || '';
328
+ };
329
+
330
+ const applyWhatsAppLink = (element, url) => {
331
+ if (!element) return;
332
+ const value = String(url || '').trim();
333
+ if (!value) {
334
+ element.hidden = true;
335
+ element.removeAttribute('href');
336
+ return;
337
+ }
338
+ element.hidden = false;
339
+ element.setAttribute('href', value);
340
+ };
341
+
342
+ const fetchJson = async (url) => {
343
+ const response = await fetch(url);
344
+ const data = await response.json().catch(() => ({}));
345
+ if (!response.ok) {
346
+ const message = (data && data.error) || 'Falha ao carregar dados.';
347
+ throw new Error(message);
348
+ }
349
+ return data;
350
+ };
351
+
352
+ const clearPanelError = () => {
353
+ els.panelError.hidden = true;
354
+ els.panelError.textContent = '';
355
+ };
356
+
357
+ const setThumbFallback = (thumbWrap) => {
358
+ thumbWrap.textContent = '';
359
+ const fallback = document.createElement('div');
360
+ fallback.className = 'pack-thumb-fallback';
361
+ fallback.textContent = 'Sem capa disponível';
362
+ thumbWrap.appendChild(fallback);
363
+ };
364
+
365
+ const shortStickerId = (value) => {
366
+ const normalized = String(value || '').replace(/[^a-zA-Z0-9]/g, '');
367
+ return normalized.slice(0, 5) || '-----';
368
+ };
369
+
370
+ const toTagToken = (value) => {
371
+ const normalized = String(value || '')
372
+ .trim()
373
+ .toLowerCase();
374
+ if (!normalized) return '';
375
+
376
+ const mapped = {
377
+ 'video game screenshot': 'game',
378
+ 'real life photo': 'foto-real',
379
+ 'anime illustration': 'anime',
380
+ 'nsfw content': 'nsfw',
381
+ };
382
+
383
+ if (mapped[normalized]) return mapped[normalized];
384
+
385
+ return normalized
386
+ .normalize('NFD')
387
+ .replace(/[\u0300-\u036f]/g, '')
388
+ .replace(/[^a-z0-9]+/g, '-')
389
+ .replace(/^-+|-+$/g, '')
390
+ .slice(0, 18);
391
+ };
392
+
393
+ const resolveTopStickerTags = (entity) => {
394
+ const classification = entity?.asset?.classification || entity?.classification || null;
395
+ const explicitTags = Array.isArray(entity?.tags) ? entity.tags : [];
396
+ const classificationTags = Array.isArray(classification?.tags) ? classification.tags : [];
397
+
398
+ const rankedFromScores = Object.entries(classification?.all_scores || {})
399
+ .filter(([, score]) => Number.isFinite(Number(score)))
400
+ .sort((left, right) => Number(right[1]) - Number(left[1]))
401
+ .map(([label]) => toTagToken(label))
402
+ .filter(Boolean);
403
+
404
+ const merged = [...rankedFromScores, ...classificationTags, ...explicitTags]
405
+ .map((tag) => toTagToken(tag))
406
+ .filter(Boolean);
407
+
408
+ return Array.from(new Set(merged)).slice(0, 3);
409
+ };
410
+
411
+ const buildStickerTagsOverlay = (entity) => {
412
+ const tags = resolveTopStickerTags(entity);
413
+ if (!tags.length) return null;
414
+
415
+ const overlay = document.createElement('div');
416
+ overlay.className = 'sticker-tags';
417
+ overlay.setAttribute('aria-label', 'Tags da figurinha: ' + tags.join(', '));
418
+
419
+ tags.forEach((tag) => {
420
+ const chip = document.createElement('span');
421
+ chip.className = 'sticker-tag';
422
+ chip.textContent = tag;
423
+ overlay.appendChild(chip);
424
+ });
425
+
426
+ return overlay;
427
+ };
428
+
429
+ const resolveTopPackTags = (pack) => {
430
+ const explicitTags = Array.isArray(pack?.tags) ? pack.tags : [];
431
+ const classificationTags = Array.isArray(pack?.classification?.tags) ? pack.classification.tags : [];
432
+
433
+ const merged = [...classificationTags, ...explicitTags]
434
+ .map((tag) => toTagToken(tag))
435
+ .filter(Boolean);
436
+
437
+ return Array.from(new Set(merged)).slice(0, 3);
438
+ };
439
+
440
+ const buildPackTagsRow = (pack) => {
441
+ const tags = resolveTopPackTags(pack);
442
+ if (!tags.length) return null;
443
+
444
+ const row = document.createElement('div');
445
+ row.className = 'pack-tags';
446
+ row.setAttribute('aria-label', 'Categorias do pack: ' + tags.join(', '));
447
+
448
+ tags.forEach((tag) => {
449
+ const chip = document.createElement('span');
450
+ chip.className = 'pack-tag';
451
+ chip.textContent = tag;
452
+ row.appendChild(chip);
453
+ });
454
+
455
+ return row;
456
+ };
457
+
458
+ const renderCard = (pack) => {
459
+ const col = document.createElement('div');
460
+ col.className = 'col-4 col-sm-6 col-md-4 col-lg-3';
461
+
462
+ const card = document.createElement('button');
463
+ card.type = 'button';
464
+ card.className = 'card h-100 text-start bg-dark text-light shadow-sm pack-card';
465
+ card.setAttribute('aria-label', 'Abrir pack ' + pack.name);
466
+
467
+ const cardBody = document.createElement('div');
468
+ cardBody.className = 'card-body d-flex flex-column gap-2';
469
+
470
+ const thumbWrap = document.createElement('div');
471
+ thumbWrap.className = 'pack-thumb';
472
+
473
+ const visibilityBadge = document.createElement('span');
474
+ visibilityBadge.className = 'pack-visibility-badge';
475
+ const visibility = String(pack.visibility || '').toLowerCase();
476
+ visibilityBadge.textContent =
477
+ visibility === 'public' ? 'Público' : visibility === 'unlisted' ? 'Não listado' : 'Privado';
478
+
479
+ const countBadge = document.createElement('span');
480
+ countBadge.className = 'pack-count-badge';
481
+ countBadge.textContent = String(Number(pack.sticker_count || 0)) + ' itens';
482
+
483
+ if (pack.cover_url) {
484
+ const image = document.createElement('img');
485
+ image.loading = 'lazy';
486
+ image.alt = 'Capa do pack ' + pack.name;
487
+ image.src = pack.cover_url;
488
+ image.addEventListener('error', () => setThumbFallback(thumbWrap));
489
+ thumbWrap.appendChild(image);
490
+ } else {
491
+ setThumbFallback(thumbWrap);
492
+ }
493
+ thumbWrap.append(visibilityBadge, countBadge);
494
+
495
+ const title = document.createElement('h3');
496
+ title.className = 'card-title h6 mb-0';
497
+ title.textContent = pack.name;
498
+
499
+ const quantity = document.createElement('p');
500
+ quantity.className = 'pack-meta pack-count mb-0';
501
+ quantity.textContent = String(Number(pack.sticker_count || 0)) + ' itens';
502
+
503
+ const author = document.createElement('p');
504
+ author.className = 'pack-meta pack-author mb-0';
505
+ author.textContent = pack.publisher || 'Autor não informado';
506
+
507
+ const tagsRow = buildPackTagsRow(pack);
508
+ cardBody.append(thumbWrap, title, quantity, author);
509
+ if (tagsRow) {
510
+ cardBody.appendChild(tagsRow);
511
+ }
512
+ card.appendChild(cardBody);
513
+ card.addEventListener('click', () => openPack(pack.pack_key, { pushState: true }));
514
+
515
+ col.appendChild(card);
516
+ return col;
517
+ };
518
+
519
+ const renderGrid = () => {
520
+ els.grid.innerHTML = '';
521
+ state.packs.items.forEach((pack) => {
522
+ els.grid.appendChild(renderCard(pack));
523
+ });
524
+ };
525
+
526
+ const rankPacksByCompleteness = (packs) => {
527
+ if (!Array.isArray(packs)) return [];
528
+ return [...packs].sort((left, right) => {
529
+ const leftCount = Number(left?.sticker_count) || 0;
530
+ const rightCount = Number(right?.sticker_count) || 0;
531
+ const leftHasCover = left?.cover_url ? 1 : 0;
532
+ const rightHasCover = right?.cover_url ? 1 : 0;
533
+ const leftIsComplete = leftCount >= 30 ? 1 : 0;
534
+ const rightIsComplete = rightCount >= 30 ? 1 : 0;
535
+
536
+ if (rightIsComplete !== leftIsComplete) return rightIsComplete - leftIsComplete;
537
+
538
+ if (rightCount !== leftCount) return rightCount - leftCount;
539
+ if (rightHasCover !== leftHasCover) return rightHasCover - leftHasCover;
540
+ return String(left?.name || '').localeCompare(String(right?.name || ''), 'pt-BR');
541
+ });
542
+ };
543
+
544
+ const renderPackSkeletons = (count) => {
545
+ els.grid.innerHTML = '';
546
+ for (let index = 0; index < count; index += 1) {
547
+ const col = document.createElement('div');
548
+ col.className = 'col-4 col-sm-6 col-md-4 col-lg-3';
549
+ col.innerHTML =
550
+ '<div class="card h-100 bg-dark text-light shadow-sm pack-card">' +
551
+ '<div class="card-body d-flex flex-column gap-2">' +
552
+ '<div class="skeleton skeleton-thumb"></div>' +
553
+ '<div class="skeleton skeleton-line"></div>' +
554
+ '<div class="skeleton skeleton-line short"></div>' +
555
+ '</div></div>';
556
+ els.grid.appendChild(col);
557
+ }
558
+ };
559
+
560
+ const renderOrphanSticker = (sticker) => {
561
+ const col = document.createElement('div');
562
+ col.className = 'col-6 col-md-3 col-lg-2';
563
+
564
+ const wrapper = document.createElement('article');
565
+ wrapper.className = 'orphan-card card h-100';
566
+
567
+ const body = document.createElement('div');
568
+ body.className = 'card-body p-2';
569
+
570
+ if (sticker.url) {
571
+ const image = document.createElement('img');
572
+ image.loading = 'lazy';
573
+ image.alt = 'Sticker sem pack ' + sticker.id;
574
+ image.src = sticker.url;
575
+ body.appendChild(image);
576
+ } else {
577
+ const fallback = document.createElement('div');
578
+ fallback.className = 'pack-thumb-fallback';
579
+ fallback.textContent = 'Arquivo não acessível';
580
+ body.appendChild(fallback);
581
+ }
582
+
583
+ const tagsOverlay = buildStickerTagsOverlay(sticker);
584
+ if (tagsOverlay) body.appendChild(tagsOverlay);
585
+
586
+ const meta = document.createElement('p');
587
+ meta.className = 'orphan-meta mb-0 mt-2';
588
+ meta.textContent = 'ID: ' + shortStickerId(sticker.id);
589
+ meta.title = sticker.id || '';
590
+ body.appendChild(meta);
591
+
592
+ wrapper.appendChild(body);
593
+ col.appendChild(wrapper);
594
+ return col;
595
+ };
596
+
597
+ const renderOrphanGrid = () => {
598
+ els.orphanGrid.innerHTML = '';
599
+ state.orphan.items.forEach((sticker) => {
600
+ els.orphanGrid.appendChild(renderOrphanSticker(sticker));
601
+ });
602
+ };
603
+
604
+ const renderPackPage = (pack) => {
605
+ const items = Array.isArray(pack?.items) ? pack.items : [];
606
+ state.selectedPack = pack || null;
607
+
608
+ els.packPageTitle.textContent = pack?.name || 'Pack';
609
+ els.packPageSub.textContent = (pack?.publisher || '-') + ' | ' + (pack?.description || 'Sem descrição');
610
+ els.packPageChip.textContent =
611
+ String(pack?.sticker_count || items.length || 0) + ' itens | ' + (pack?.visibility || '-') + ' | ' + (pack?.pack_key || '-');
612
+ applyWhatsAppLink(els.packPageWhatsApp, pack?.whatsapp?.url);
613
+
614
+ els.packPageGrid.innerHTML = '';
615
+ if (!items.length) {
616
+ els.packPageStatus.textContent = 'Este pack não possui stickers disponíveis.';
617
+ return;
618
+ }
619
+
620
+ els.packPageStatus.textContent = 'Exibindo ' + items.length + ' sticker(s).';
621
+ items.forEach((item) => {
622
+ const col = document.createElement('div');
623
+ col.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
624
+
625
+ const wrapper = document.createElement('div');
626
+ wrapper.className = 'sticker-tile';
627
+
628
+ const image = document.createElement('img');
629
+ image.loading = 'lazy';
630
+ image.alt = item.accessibility_label || 'Sticker #' + item.position;
631
+ image.src = item.asset_url;
632
+
633
+ wrapper.appendChild(image);
634
+ const tagsOverlay = buildStickerTagsOverlay(item);
635
+ if (tagsOverlay) wrapper.appendChild(tagsOverlay);
636
+ col.appendChild(wrapper);
637
+ els.packPageGrid.appendChild(col);
638
+ });
639
+ };
640
+
641
+ const renderOrphanSkeletons = (count) => {
642
+ els.orphanGrid.innerHTML = '';
643
+ for (let index = 0; index < count; index += 1) {
644
+ const col = document.createElement('div');
645
+ col.className = 'col-6 col-md-3 col-lg-2';
646
+ col.innerHTML =
647
+ '<article class="orphan-card card h-100"><div class="card-body p-2">' +
648
+ '<div class="skeleton skeleton-orphan"></div>' +
649
+ '<div class="skeleton skeleton-line short mt-2"></div>' +
650
+ '</div></article>';
651
+ els.orphanGrid.appendChild(col);
652
+ }
653
+ };
654
+
655
+ const updateMoreButton = () => {
656
+ els.more.hidden = !state.packs.hasMore;
657
+ els.more.disabled = state.packs.loading;
658
+ els.more.textContent = state.packs.loading ? 'Carregando...' : 'Carregar mais';
659
+ };
660
+
661
+ const updateOrphanPaginationControls = () => {
662
+ const totalPages = Math.max(1, Number(state.orphan.totalPages) || 1);
663
+ const totalItems = Math.max(0, Number(state.orphan.totalItems) || 0);
664
+ const page = Math.max(1, Math.min(totalPages, Number(state.orphan.page) || 1));
665
+ state.orphan.page = page;
666
+
667
+ els.orphanPrev.disabled = page <= 1 || state.orphan.loading;
668
+ els.orphanNext.disabled = page >= totalPages || state.orphan.loading;
669
+ els.orphanPageInfo.textContent = 'Página ' + page + ' de ' + totalPages + ' - ' + totalItems + ' figurinhas';
670
+ };
671
+
672
+ const listPacks = async ({ reset = false } = {}) => {
673
+ if (state.packs.loading) return;
674
+ state.packs.loading = true;
675
+ updateMoreButton();
676
+ setStatus(reset ? 'Buscando packs...' : 'Carregando mais packs...');
677
+ if (reset) renderPackSkeletons(Math.min(state.packs.limit, 12));
678
+
679
+ if (reset) {
680
+ state.packs.offset = 0;
681
+ state.packs.items = [];
682
+ }
683
+
684
+ try {
685
+ const payload = await fetchJson(
686
+ toApi(CONFIG.apiBasePath, {
687
+ q: state.q,
688
+ visibility: state.visibility,
689
+ categories: state.categories.join(','),
690
+ limit: state.packs.limit,
691
+ offset: state.packs.offset,
692
+ }),
693
+ );
694
+
695
+ const packs = Array.isArray(payload.data) ? payload.data : [];
696
+ state.packs.items = reset ? packs : state.packs.items.concat(packs);
697
+ state.packs.items = rankPacksByCompleteness(state.packs.items);
698
+ state.packs.offset = (payload.pagination && payload.pagination.next_offset) || state.packs.items.length;
699
+ state.packs.hasMore = Boolean(payload.pagination && payload.pagination.has_more);
700
+
701
+ renderGrid();
702
+
703
+ if (!state.packs.items.length) {
704
+ setStatus('Nenhum pack encontrado com os filtros atuais.');
705
+ } else {
706
+ setStatus(state.packs.items.length + ' pack(s) carregado(s).');
707
+ }
708
+ } catch (error) {
709
+ setStatus(error.message || 'Não foi possível listar os packs agora.');
710
+ } finally {
711
+ state.packs.loading = false;
712
+ updateMoreButton();
713
+ }
714
+ };
715
+
716
+ const listOrphanStickers = async ({ reset = false } = {}) => {
717
+ if (state.orphan.loading) return;
718
+ state.orphan.loading = true;
719
+ updateOrphanPaginationControls();
720
+ setOrphanStatus('Buscando figurinhas sem pack...');
721
+ if (reset) renderOrphanSkeletons(Math.min(state.orphan.limit, 12));
722
+
723
+ if (reset) {
724
+ state.orphan.page = 1;
725
+ state.orphan.items = [];
726
+ }
727
+
728
+ try {
729
+ const currentPage = Math.max(1, Number(state.orphan.page) || 1);
730
+ const currentLimit = Math.max(1, Number(state.orphan.limit) || 1);
731
+ const offset = (currentPage - 1) * currentLimit;
732
+
733
+ const payload = await fetchJson(
734
+ toApi(CONFIG.orphanApiPath, {
735
+ q: state.q,
736
+ categories: state.categories.join(','),
737
+ limit: currentLimit,
738
+ offset,
739
+ }),
740
+ );
741
+
742
+ const stickers = Array.isArray(payload.data) ? payload.data : [];
743
+ const totalItems = Math.max(0, Number(payload?.pagination?.total || 0));
744
+ const totalPages = Math.max(1, Number(payload?.pagination?.total_pages || Math.ceil(totalItems / currentLimit) || 1));
745
+
746
+ state.orphan.items = stickers;
747
+ state.orphan.totalItems = totalItems;
748
+ state.orphan.totalPages = totalPages;
749
+ state.orphan.page = Math.max(1, Math.min(totalPages, currentPage));
750
+
751
+ renderOrphanGrid();
752
+
753
+ if (!state.orphan.items.length) {
754
+ setOrphanStatus('Nenhuma figurinha sem pack encontrada.');
755
+ } else {
756
+ const from = offset + 1;
757
+ const to = offset + state.orphan.items.length;
758
+ setOrphanStatus('Mostrando ' + from + '-' + to + ' de ' + state.orphan.totalItems + ' figurinha(s) sem pack.');
759
+ }
760
+ } catch (error) {
761
+ setOrphanStatus(error.message || 'Não foi possível listar figurinhas sem pack.');
762
+ } finally {
763
+ state.orphan.loading = false;
764
+ updateOrphanPaginationControls();
765
+ }
766
+ };
767
+
768
+ const updatePanelPaginationControls = (totalItems) => {
769
+ const safeTotal = Math.max(0, Number(totalItems) || 0);
770
+ const totalPages = Math.max(1, Math.ceil(safeTotal / state.panelPagination.perPage));
771
+ state.panelPagination.totalPages = totalPages;
772
+
773
+ if (state.panelPagination.page > totalPages) {
774
+ state.panelPagination.page = totalPages;
775
+ }
776
+ if (state.panelPagination.page < 1) {
777
+ state.panelPagination.page = 1;
778
+ }
779
+
780
+ els.panelPrev.disabled = state.panelPagination.page <= 1;
781
+ els.panelNext.disabled = state.panelPagination.page >= totalPages;
782
+ els.panelPageInfo.textContent =
783
+ 'Página ' + state.panelPagination.page + ' de ' + totalPages + ' - ' + safeTotal + ' stickers';
784
+ };
785
+
786
+ const renderPanelStickersPage = () => {
787
+ const pack = state.selectedPack;
788
+ const items = Array.isArray(pack?.items) ? pack.items : [];
789
+ const perPage = state.panelPagination.perPage;
790
+
791
+ updatePanelPaginationControls(items.length);
792
+
793
+ const start = (state.panelPagination.page - 1) * perPage;
794
+ const end = start + perPage;
795
+ const pageItems = items.slice(start, end);
796
+
797
+ els.stickers.innerHTML = '';
798
+
799
+ if (!items.length) {
800
+ const empty = document.createElement('p');
801
+ empty.className = 'text-secondary mb-0';
802
+ empty.textContent = 'Este pack não possui stickers disponíveis.';
803
+ els.stickers.appendChild(empty);
804
+ return;
805
+ }
806
+
807
+ pageItems.forEach((item) => {
808
+ const col = document.createElement('div');
809
+ col.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
810
+
811
+ const wrapper = document.createElement('div');
812
+ wrapper.className = 'sticker-tile';
813
+
814
+ const image = document.createElement('img');
815
+ image.loading = 'lazy';
816
+ image.alt = item.accessibility_label || 'Sticker #' + item.position;
817
+ image.src = item.asset_url;
818
+
819
+ wrapper.appendChild(image);
820
+ const tagsOverlay = buildStickerTagsOverlay(item);
821
+ if (tagsOverlay) wrapper.appendChild(tagsOverlay);
822
+ col.appendChild(wrapper);
823
+ els.stickers.appendChild(col);
824
+ });
825
+ };
826
+
827
+ const resetPanel = () => {
828
+ state.selectedPack = null;
829
+ state.panelPagination.page = 1;
830
+ state.panelPagination.totalPages = 1;
831
+ els.panelTitle.textContent = 'Pack';
832
+ els.panelSub.textContent = '';
833
+ els.panelChip.textContent = '';
834
+ els.stickers.innerHTML = '';
835
+ els.panelPageInfo.textContent = 'Página 1 de 1 - 0 stickers';
836
+ els.panelPrev.disabled = true;
837
+ els.panelNext.disabled = true;
838
+ clearPanelError();
839
+ };
840
+
841
+ const showPanel = () => {
842
+ if (panelModal) {
843
+ panelModal.show();
844
+ return;
845
+ }
846
+ els.panel.classList.add('show');
847
+ els.panel.style.display = 'block';
848
+ els.panel.removeAttribute('aria-hidden');
849
+ };
850
+
851
+ const closePanel = ({ replaceState = false } = {}) => {
852
+ shouldReplaceStateOnHide = replaceState;
853
+ if (panelModal) {
854
+ panelModal.hide();
855
+ return;
856
+ }
857
+
858
+ els.panel.classList.remove('show');
859
+ els.panel.style.display = 'none';
860
+ els.panel.setAttribute('aria-hidden', 'true');
861
+ resetPanel();
862
+ if (replaceState) {
863
+ history.replaceState({}, '', CONFIG.webPath);
864
+ }
865
+ };
866
+
867
+ const renderPack = (pack) => {
868
+ state.selectedPack = pack || null;
869
+ state.panelPagination.page = 1;
870
+
871
+ els.panelTitle.textContent = pack.name || 'Pack';
872
+ els.panelSub.textContent = (pack.publisher || '-') + ' | ' + (pack.description || 'Sem descrição');
873
+ els.panelChip.textContent = pack.sticker_count + ' itens | ' + pack.visibility + ' | ' + pack.pack_key;
874
+ applyWhatsAppLink(els.useWhatsAppLink, pack?.whatsapp?.url);
875
+ renderPanelStickersPage();
876
+
877
+ showPanel();
878
+ };
879
+
880
+ const openPack = async (packKey, { pushState = false } = {}) => {
881
+ const sanitizedKey = String(packKey || '').trim();
882
+ if (!sanitizedKey) return;
883
+ showPackPageView();
884
+ els.packPageTitle.textContent = 'Carregando...';
885
+ els.packPageSub.textContent = '';
886
+ els.packPageChip.textContent = '';
887
+ els.packPageGrid.innerHTML = '';
888
+ els.packPageStatus.textContent = 'Buscando informações do pack...';
889
+ applyWhatsAppLink(els.packPageWhatsApp, '');
890
+ applyWhatsAppLink(els.useWhatsAppLink, '');
891
+
892
+ try {
893
+ const payload = await fetchJson(
894
+ toApi(CONFIG.apiBasePath + '/' + encodeURIComponent(sanitizedKey), {
895
+ categories: state.categories.join(','),
896
+ }),
897
+ );
898
+ state.selectedPack = payload.data || null;
899
+ if (!state.selectedPack) {
900
+ throw new Error('Pack não encontrado.');
901
+ }
902
+
903
+ renderPackPage(state.selectedPack);
904
+ if (pushState) {
905
+ history.pushState({}, '', CONFIG.webPath + '/' + encodeURIComponent(sanitizedKey));
906
+ }
907
+ } catch (error) {
908
+ els.packPageStatus.textContent = error.message || 'Não foi possível abrir este pack.';
909
+ }
910
+ };
911
+
912
+ els.form.addEventListener('submit', async (event) => {
913
+ event.preventDefault();
914
+ await applyFilters();
915
+ });
916
+
917
+ els.more.addEventListener('click', async () => {
918
+ await listPacks({ reset: false });
919
+ });
920
+
921
+ els.orphanPrev.addEventListener('click', async () => {
922
+ if (state.orphan.page <= 1) return;
923
+ state.orphan.page -= 1;
924
+ await listOrphanStickers();
925
+ });
926
+
927
+ els.orphanNext.addEventListener('click', async () => {
928
+ if (state.orphan.page >= state.orphan.totalPages) return;
929
+ state.orphan.page += 1;
930
+ await listOrphanStickers();
931
+ });
932
+
933
+ els.panelPrev.addEventListener('click', () => {
934
+ if (state.panelPagination.page <= 1) return;
935
+ state.panelPagination.page -= 1;
936
+ renderPanelStickersPage();
937
+ });
938
+
939
+ els.panelNext.addEventListener('click', () => {
940
+ if (state.panelPagination.page >= state.panelPagination.totalPages) return;
941
+ state.panelPagination.page += 1;
942
+ renderPanelStickersPage();
943
+ });
944
+
945
+ els.panelClose.addEventListener('click', () => {
946
+ closePanel({ replaceState: true });
947
+ });
948
+
949
+ els.copy.addEventListener('click', async () => {
950
+ if (!state.selectedPack) return;
951
+
952
+ const url = window.location.origin + CONFIG.webPath + '/' + encodeURIComponent(state.selectedPack.pack_key);
953
+ try {
954
+ await navigator.clipboard.writeText(url);
955
+ els.copy.textContent = 'Link copiado';
956
+ setTimeout(() => {
957
+ els.copy.textContent = 'Copiar link do pack';
958
+ }, 1800);
959
+ } catch {
960
+ els.copy.textContent = 'Falha ao copiar';
961
+ }
962
+ });
963
+
964
+ els.packPageCopy.addEventListener('click', async () => {
965
+ if (!state.selectedPack) return;
966
+ const url = window.location.origin + CONFIG.webPath + '/' + encodeURIComponent(state.selectedPack.pack_key);
967
+ try {
968
+ await navigator.clipboard.writeText(url);
969
+ els.packPageCopy.textContent = 'Link copiado';
970
+ setTimeout(() => {
971
+ els.packPageCopy.textContent = 'Copiar link do pack';
972
+ }, 1800);
973
+ } catch {
974
+ els.packPageCopy.textContent = 'Falha ao copiar';
975
+ }
976
+ });
977
+
978
+ els.packPageBack.addEventListener('click', async () => {
979
+ history.pushState({}, '', CONFIG.webPath);
980
+ showCatalogView();
981
+ if (!state.catalogLoaded) {
982
+ await Promise.all([listPacks({ reset: true }), listOrphanStickers({ reset: true })]);
983
+ state.catalogLoaded = true;
984
+ }
985
+ });
986
+
987
+ if (panelModal) {
988
+ els.panel.addEventListener('hidden.bs.modal', () => {
989
+ resetPanel();
990
+
991
+ if (shouldReplaceStateOnHide || window.location.pathname.startsWith(CONFIG.webPath + '/')) {
992
+ history.replaceState({}, '', CONFIG.webPath);
993
+ }
994
+
995
+ shouldReplaceStateOnHide = false;
996
+ });
997
+ }
998
+
999
+ window.addEventListener('popstate', () => {
1000
+ const key = extractPackKeyFromPath();
1001
+ if (!key) {
1002
+ showCatalogView();
1003
+ return;
1004
+ }
1005
+ openPack(key, { pushState: false });
1006
+ });
1007
+
1008
+ (async () => {
1009
+ const pathPackKey = extractPackKeyFromPath();
1010
+ const initialPackKey = pathPackKey || CONFIG.initialPackKey;
1011
+ if (initialPackKey) {
1012
+ await openPack(initialPackKey, { pushState: false });
1013
+ return;
1014
+ }
1015
+ showCatalogView();
1016
+ await Promise.all([listPacks({ reset: true }), listOrphanStickers({ reset: true })]);
1017
+ state.catalogLoaded = true;
1018
+ })();
1019
+ })();