@kaikybrofc/omnizap-system 2.3.5 → 2.3.7

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.
@@ -22,6 +22,7 @@ const CATALOG_SORT_OPTIONS = [
22
22
 
23
23
  const DEFAULT_CATALOG_SORT = 'trending';
24
24
  const DEFAULT_CREATORS_SORT = 'popular';
25
+ const FIRST_CATALOG_PAGE = 1;
25
26
 
26
27
  const safeNumber = (value, fallback = 0) => {
27
28
  const numeric = Number(value);
@@ -250,17 +251,6 @@ const shortNum = (value) => {
250
251
  return String(n);
251
252
  };
252
253
 
253
- const formatFullNum = (value) =>
254
- new Intl.NumberFormat('pt-BR', {
255
- maximumFractionDigits: 0,
256
- }).format(Math.max(0, Math.round(Number(value || 0))));
257
-
258
- const toMiniBars = (values = [], { minHeight = 3, maxHeight = 16 } = {}) => {
259
- const source = Array.isArray(values) && values.length ? values.map((value) => Math.max(0, Number(value || 0))) : [0, 0, 0, 0, 0, 0, 0];
260
- const max = Math.max(...source, 1);
261
- return source.map((value) => Math.max(minHeight, Math.min(maxHeight, Math.round((value / max) * (maxHeight - 2)) + 2)));
262
- };
263
-
264
254
  const getPackEngagement = (pack) => {
265
255
  const engagement = pack?.engagement || {};
266
256
  const likeCount = Number(engagement.like_count || 0);
@@ -280,6 +270,8 @@ const getAvatarUrl = (name) => `https://api.dicebear.com/8.x/thumbs/svg?seed=${e
280
270
 
281
271
  const parseCatalogSearchState = (search = '') => {
282
272
  const params = new URLSearchParams(String(search || ''));
273
+ const pageParam = Number.parseInt(String(params.get('page') || ''), 10);
274
+ const page = Number.isFinite(pageParam) && pageParam > 0 ? pageParam : FIRST_CATALOG_PAGE;
283
275
  const filter = String(params.get('filter') || '')
284
276
  .trim()
285
277
  .toLowerCase();
@@ -294,6 +286,7 @@ const parseCatalogSearchState = (search = '') => {
294
286
  return {
295
287
  q,
296
288
  category,
289
+ page,
297
290
  filter: hasTrendingFilter ? 'trending' : '',
298
291
  sort: hasTrendingFilter ? 'trending' : normalizeCatalogSort(params.get('sort') || ''),
299
292
  };
@@ -419,12 +412,6 @@ const isRecent = (dateString) => {
419
412
 
420
413
  const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, Math.max(0, Number(ms) || 0)));
421
414
 
422
- const primaryTag = (item) => {
423
- const tags = Array.isArray(item?.tags) ? item.tags : [];
424
- if (!tags.length) return '';
425
- return String(tags[0] || '').replace(/-/g, ' ');
426
- };
427
-
428
415
  const tagLabel = (tag) => {
429
416
  const normalized = String(tag || '').toLowerCase();
430
417
  if (normalized.includes('nsfw')) return `🔞 ${String(tag).replace(/-/g, ' ').toUpperCase()}`;
@@ -534,65 +521,6 @@ function PackCard({ pack, index, onOpen, hasNsfwAccess = true, onRequireLogin })
534
521
  `;
535
522
  }
536
523
 
537
- function CatalogMetricCard({ label, value = '', valueRaw = null, numberFormat = 'compact', icon = '📊', hint = '', bars = [], tone = 'slate' }) {
538
- const toneMap = {
539
- slate: 'border-slate-800 bg-slate-900/60',
540
- emerald: 'border-emerald-500/20 bg-emerald-500/5',
541
- cyan: 'border-cyan-500/20 bg-cyan-500/5',
542
- amber: 'border-amber-500/20 bg-amber-500/5',
543
- };
544
- const numericTarget = valueRaw === null || valueRaw === undefined ? null : Math.max(0, Number(valueRaw || 0));
545
- const [animatedValue, setAnimatedValue] = useState(numericTarget ?? 0);
546
- const animatedFromRef = useRef(numericTarget ?? 0);
547
-
548
- useEffect(() => {
549
- if (numericTarget === null || !Number.isFinite(numericTarget)) return undefined;
550
- const startValue = Number.isFinite(animatedFromRef.current) ? animatedFromRef.current : 0;
551
- const endValue = numericTarget;
552
- if (startValue === endValue) {
553
- setAnimatedValue(endValue);
554
- animatedFromRef.current = endValue;
555
- return undefined;
556
- }
557
-
558
- let frameId = 0;
559
- const startAt = performance.now();
560
- const durationMs = 520;
561
- const tick = (now) => {
562
- const progress = Math.min(1, (now - startAt) / durationMs);
563
- const eased = 1 - Math.pow(1 - progress, 3);
564
- const next = startValue + (endValue - startValue) * eased;
565
- setAnimatedValue(next);
566
- if (progress < 1) {
567
- frameId = window.requestAnimationFrame(tick);
568
- return;
569
- }
570
- animatedFromRef.current = endValue;
571
- setAnimatedValue(endValue);
572
- };
573
-
574
- frameId = window.requestAnimationFrame(tick);
575
- return () => {
576
- if (frameId) window.cancelAnimationFrame(frameId);
577
- animatedFromRef.current = endValue;
578
- };
579
- }, [numericTarget]);
580
-
581
- const resolvedValue = numericTarget === null ? String(value || '0') : numberFormat === 'full' ? formatFullNum(animatedValue) : shortNum(animatedValue);
582
-
583
- return html`
584
- <article className=${`rounded-xl border p-2.5 ${toneMap[tone] || toneMap.slate}`} title=${hint || label}>
585
- <div className="flex items-center justify-between gap-2">
586
- <span className="text-sm">${icon}</span>
587
- <div className="flex items-end gap-0.5">${(Array.isArray(bars) ? bars : []).slice(0, 7).map((bar, index) => html` <span key=${index} className="w-1 rounded-full bg-white/15" style=${{ height: `${Math.max(4, Math.min(16, Number(bar || 0)))}px` }}></span> `)}</div>
588
- </div>
589
- <p className="mt-1 text-base font-bold text-slate-100">${resolvedValue}</p>
590
- <p className="text-[11px] text-slate-400">${label}</p>
591
- ${hint ? html`<p className="mt-0.5 text-[10px] text-slate-500">${hint}</p>` : null}
592
- </article>
593
- `;
594
- }
595
-
596
524
  function DiscoverPackRowItem({ pack, onOpen, rank = 0, hasNsfwAccess = true, onRequireLogin }) {
597
525
  if (!pack?.pack_key) return null;
598
526
  const lockedByNsfw = isPackMarkedNsfw(pack) && !hasNsfwAccess;
@@ -1467,25 +1395,6 @@ function CreatorProfileDashboard({ googleAuthConfig, googleAuth, googleAuthBusy,
1467
1395
  `;
1468
1396
  }
1469
1397
 
1470
- function OrphanCard({ sticker, hasNsfwAccess = true, onRequireLogin }) {
1471
- const lockedByNsfw = isStickerMarkedNsfw(sticker) && !hasNsfwAccess;
1472
- return html`
1473
- <article className="group rounded-2xl border border-slate-700/80 bg-slate-800/70 shadow-soft overflow-hidden transition-all duration-200 hover:-translate-y-0.5">
1474
- <div className="relative aspect-square bg-slate-900 overflow-hidden">
1475
- <img src=${lockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : sticker.url || DEFAULT_STICKER_PLACEHOLDER_URL} alt="Sticker sem pack" className=${`w-full h-full object-contain transition-transform duration-300 ${lockedByNsfw ? 'blur-md scale-105' : 'group-hover:scale-110'}`} loading="lazy" />
1476
- ${lockedByNsfw
1477
- ? html`
1478
- <div className="absolute inset-0 flex items-center justify-center bg-slate-950/35 p-2">
1479
- <button type="button" onClick=${() => onRequireLogin?.()} className="inline-flex h-8 items-center rounded-lg border border-amber-400/35 bg-amber-500/15 px-2.5 text-[10px] font-semibold text-amber-100">Entrar para desbloquear</button>
1480
- </div>
1481
- `
1482
- : null}
1483
- </div>
1484
- <div className="p-2">${primaryTag(sticker) ? html`<span className="inline-flex rounded-full border border-slate-600 bg-slate-900/80 px-2 py-0.5 text-[10px] text-slate-300">${primaryTag(sticker)}</span>` : html`<span className="inline-flex rounded-full border border-slate-600 bg-slate-900/80 px-2 py-0.5 text-[10px] text-slate-400">sticker</span>`}</div>
1485
- </article>
1486
- `;
1487
- }
1488
-
1489
1398
  function SkeletonGrid({ count = 10 }) {
1490
1399
  return html`
1491
1400
  <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-3">
@@ -1853,10 +1762,8 @@ function StickersApp() {
1853
1762
  () => ({
1854
1763
  webPath: root?.dataset.webPath || '/stickers',
1855
1764
  apiBasePath: root?.dataset.apiBasePath || '/api/sticker-packs',
1856
- orphanApiPath: root?.dataset.orphanApiPath || '/api/sticker-packs/orphan-stickers',
1857
1765
  loginPath: root?.dataset.loginPath || '/login',
1858
1766
  limit: parseIntSafe(root?.dataset.defaultLimit, 24),
1859
- orphanLimit: parseIntSafe(root?.dataset.defaultOrphanLimit, 24),
1860
1767
  }),
1861
1768
  [root],
1862
1769
  );
@@ -1869,6 +1776,7 @@ function StickersApp() {
1869
1776
  const [sortBy, setSortBy] = useState(normalizeCatalogSort(initialCatalogSearch.sort || DEFAULT_CATALOG_SORT));
1870
1777
  const [activeCategory, setActiveCategory] = useState(initialCatalogSearch.category || '');
1871
1778
  const [catalogFilter, setCatalogFilter] = useState(initialCatalogSearch.filter || '');
1779
+ const [catalogPage, setCatalogPage] = useState(Math.max(FIRST_CATALOG_PAGE, Number(initialCatalogSearch.page || FIRST_CATALOG_PAGE)));
1872
1780
  const [discoverTab, setDiscoverTab] = useState('growing');
1873
1781
  const [showAutocomplete, setShowAutocomplete] = useState(false);
1874
1782
  const [recentSearches, setRecentSearches] = useState([]);
@@ -1876,16 +1784,10 @@ function StickersApp() {
1876
1784
  const [sortPickerBusy, setSortPickerBusy] = useState(false);
1877
1785
 
1878
1786
  const [packs, setPacks] = useState([]);
1879
- const [packOffset, setPackOffset] = useState(0);
1880
1787
  const [packHasMore, setPackHasMore] = useState(true);
1881
1788
  const [packsLoading, setPacksLoading] = useState(false);
1882
- const [packsLoadingMore, setPacksLoadingMore] = useState(false);
1883
-
1884
- const [orphans, setOrphans] = useState([]);
1885
- const [orphansLoading, setOrphansLoading] = useState(false);
1886
1789
 
1887
1790
  const [error, setError] = useState('');
1888
- const [sentinel, setSentinel] = useState(null);
1889
1791
 
1890
1792
  const [currentView, setCurrentView] = useState(initialRoute.view || 'catalog');
1891
1793
  const [currentPackKey, setCurrentPackKey] = useState(initialRoute.packKey || '');
@@ -1954,9 +1856,6 @@ function StickersApp() {
1954
1856
  const scoreBoost = 1 + engagement.openCount * 0.02 + engagement.likeCount * 0.08;
1955
1857
  (Array.isArray(pack?.tags) ? pack.tags : []).forEach((tag) => ensureTag(tag, scoreBoost));
1956
1858
  });
1957
- orphans.forEach((asset) => {
1958
- (Array.isArray(asset?.tags) ? asset.tags : []).forEach((tag) => ensureTag(tag, 1));
1959
- });
1960
1859
 
1961
1860
  const sortedTags = Array.from(scoreByTag.entries())
1962
1861
  .sort((a, b) => b[1] - a[1])
@@ -1974,7 +1873,7 @@ function StickersApp() {
1974
1873
  }
1975
1874
 
1976
1875
  return [{ value: '', label: '🔥 Em alta', icon: '🔥' }, ...sortedTags];
1977
- }, [packs, orphans, activeCategory]);
1876
+ }, [packs, activeCategory]);
1978
1877
 
1979
1878
  const tagSuggestions = useMemo(() => {
1980
1879
  const options = new Map();
@@ -2003,12 +1902,9 @@ function StickersApp() {
2003
1902
  packs.forEach((pack) => {
2004
1903
  (Array.isArray(pack?.tags) ? pack.tags : []).forEach(addTag);
2005
1904
  });
2006
- orphans.forEach((sticker) => {
2007
- (Array.isArray(sticker?.tags) ? sticker.tags : []).forEach(addTag);
2008
- });
2009
1905
 
2010
1906
  return Array.from(options.values());
2011
- }, [dynamicCategoryOptions, packs, orphans]);
1907
+ }, [dynamicCategoryOptions, packs]);
2012
1908
 
2013
1909
  const filteredSuggestions = useMemo(() => {
2014
1910
  const q = normalizeToken(query);
@@ -2148,24 +2044,6 @@ function StickersApp() {
2148
2044
  list.sort((a, b) => b.creatorScore - a.creatorScore);
2149
2045
  return list;
2150
2046
  }, [creatorRanking, creatorSort]);
2151
- const platformStats = useMemo(() => {
2152
- const totals = packs.reduce(
2153
- (acc, pack) => {
2154
- const engagement = getPackEngagement(pack);
2155
- acc.stickers += Number(pack?.sticker_count || 0);
2156
- acc.opens += engagement.openCount;
2157
- acc.likes += engagement.likeCount;
2158
- return acc;
2159
- },
2160
- { stickers: 0, opens: 0, likes: 0 },
2161
- );
2162
- return {
2163
- packs: packs.length,
2164
- stickers: totals.stickers + orphans.length,
2165
- opens: totals.opens,
2166
- likes: totals.likes,
2167
- };
2168
- }, [packs, orphans.length]);
2169
2047
  const recentPublishedPacks = useMemo(
2170
2048
  () =>
2171
2049
  [...packs]
@@ -2177,102 +2055,10 @@ function StickersApp() {
2177
2055
  .slice(0, 10),
2178
2056
  [packs],
2179
2057
  );
2180
- const localTrendBars = useMemo(() => {
2181
- const sample = topWeekPacks.slice(0, 7).map((pack) => {
2182
- const engagement = getPackEngagement(pack);
2183
- return Number(engagement.openCount || 0) + Number(engagement.likeCount || 0) * 2;
2184
- });
2185
- return toMiniBars(sample);
2186
- }, [topWeekPacks]);
2187
- const globalMarketplaceSeries = useMemo(() => {
2188
- const rows = Array.isArray(globalMarketplaceStats?.seriesLast7Days) ? globalMarketplaceStats.seriesLast7Days : [];
2189
- return {
2190
- packs: rows.map((entry) => safeNumber(entry?.packsPublished)),
2191
- stickers: rows.map((entry) => safeNumber(entry?.stickersCreated)),
2192
- clicks: rows.map((entry) => safeNumber(entry?.clicks)),
2193
- likes: rows.map((entry) => safeNumber(entry?.likes)),
2194
- };
2195
- }, [globalMarketplaceStats]);
2196
- const catalogMetricCards = useMemo(() => {
2197
- const localTrendingLikes = growingNowPacks.reduce((acc, pack) => acc + getPackEngagement(pack).likeCount, 0);
2198
- const hasGlobalStats = Boolean(globalMarketplaceStats);
2199
- const metrics = hasGlobalStats
2200
- ? {
2201
- packs: safeNumber(globalMarketplaceStats?.totalPacks),
2202
- stickers: safeNumber(globalMarketplaceStats?.totalStickers),
2203
- clicks: safeNumber(globalMarketplaceStats?.totalClicks),
2204
- likes: safeNumber(globalMarketplaceStats?.totalLikes),
2205
- packsLast7Days: safeNumber(globalMarketplaceStats?.packsLast7Days),
2206
- stickersWithoutPack: safeNumber(globalMarketplaceStats?.stickersWithoutPack),
2207
- likesLast7Days: safeNumber(globalMarketplaceStats?.likesLast7Days),
2208
- bars: {
2209
- packs: toMiniBars(globalMarketplaceSeries.packs),
2210
- stickers: toMiniBars(globalMarketplaceSeries.stickers),
2211
- clicks: toMiniBars(globalMarketplaceSeries.clicks),
2212
- likes: toMiniBars(globalMarketplaceSeries.likes),
2213
- },
2214
- }
2215
- : {
2216
- packs: safeNumber(platformStats.packs),
2217
- stickers: safeNumber(platformStats.stickers),
2218
- clicks: safeNumber(platformStats.opens),
2219
- likes: safeNumber(platformStats.likes),
2220
- packsLast7Days: recentPublishedPacks.slice(0, 7).length,
2221
- stickersWithoutPack: safeNumber(orphans.length),
2222
- likesLast7Days: safeNumber(localTrendingLikes),
2223
- bars: {
2224
- packs: localTrendBars,
2225
- stickers: [...localTrendBars].reverse(),
2226
- clicks: localTrendBars.map((v, i) => Math.max(3, v - (i % 3))),
2227
- likes: localTrendBars.map((v, i) => Math.max(3, Math.min(16, v - 2 + (i % 2)))),
2228
- },
2229
- };
2230
2058
 
2231
- return [
2232
- {
2233
- key: 'packs',
2234
- label: 'Packs globais',
2235
- valueRaw: metrics.packs,
2236
- numberFormat: 'full',
2237
- icon: '📦',
2238
- tone: 'slate',
2239
- hint: `+${formatFullNum(metrics.packsLast7Days)} esta semana`,
2240
- bars: metrics.bars.packs,
2241
- },
2242
- {
2243
- key: 'stickers',
2244
- label: 'Stickers globais',
2245
- valueRaw: metrics.stickers,
2246
- numberFormat: 'full',
2247
- icon: '🧩',
2248
- tone: 'cyan',
2249
- hint: `${formatFullNum(metrics.stickersWithoutPack)} sem pack`,
2250
- bars: metrics.bars.stickers,
2251
- },
2252
- {
2253
- key: 'opens',
2254
- label: 'Cliques globais',
2255
- valueRaw: metrics.clicks,
2256
- numberFormat: 'full',
2257
- icon: '👁',
2258
- tone: 'emerald',
2259
- hint: 'Engajamento total',
2260
- bars: metrics.bars.clicks,
2261
- },
2262
- {
2263
- key: 'likes',
2264
- label: 'Likes globais',
2265
- valueRaw: metrics.likes,
2266
- numberFormat: 'full',
2267
- icon: '❤️',
2268
- tone: 'amber',
2269
- hint: `+${formatFullNum(metrics.likesLast7Days)} em tendência`,
2270
- bars: metrics.bars.likes,
2271
- },
2272
- ];
2273
- }, [globalMarketplaceStats, globalMarketplaceSeries, platformStats, recentPublishedPacks, orphans.length, growingNowPacks, localTrendBars]);
2274
-
2275
- const hasAnyResult = packs.length > 0 || orphans.length > 0;
2059
+ const hasAnyResult = packs.length > 0;
2060
+ const canGoCatalogPrev = catalogPage > FIRST_CATALOG_PAGE && !packsLoading;
2061
+ const canGoCatalogNext = packHasMore && !packsLoading;
2276
2062
  const marketplaceGlobalStatsApiPath = '/api/marketplace/stats';
2277
2063
  const googleSessionApiPath = `${config.apiBasePath}/auth/google/session`;
2278
2064
  const myProfileApiPath = `${config.apiBasePath}/me`;
@@ -2608,19 +2394,12 @@ function StickersApp() {
2608
2394
  return params;
2609
2395
  };
2610
2396
 
2611
- const loadPacks = async ({ reset = false } = {}) => {
2612
- if (reset) {
2613
- setPacksLoading(true);
2614
- setPackOffset(0);
2615
- setPackHasMore(true);
2616
- } else {
2617
- if (packsLoadingMore || !packHasMore) return;
2618
- setPacksLoadingMore(true);
2619
- }
2620
-
2397
+ const loadPacks = async ({ page = catalogPage } = {}) => {
2398
+ const safePage = Math.max(FIRST_CATALOG_PAGE, Number(page || FIRST_CATALOG_PAGE));
2399
+ const nextOffset = (safePage - 1) * config.limit;
2400
+ setPacksLoading(true);
2621
2401
  setError('');
2622
2402
  try {
2623
- const nextOffset = reset ? 0 : packOffset;
2624
2403
  const effectiveSort = catalogFilter === 'trending' ? 'trending' : sortBy;
2625
2404
  const params = buildParams({
2626
2405
  q: catalogFilter === 'trending' ? '' : appliedQuery,
@@ -2634,34 +2413,14 @@ function StickersApp() {
2634
2413
  const payload = await fetchJson(`${config.apiBasePath}?${params.toString()}`);
2635
2414
  const data = Array.isArray(payload?.data) ? payload.data : [];
2636
2415
  const hasMore = Boolean(payload?.pagination?.has_more);
2637
- const next = Number(payload?.pagination?.next_offset);
2638
-
2639
- setPacks((prev) => (reset ? data : prev.concat(data)));
2416
+ setPacks(data);
2640
2417
  setPackHasMore(hasMore);
2641
- setPackOffset(Number.isFinite(next) ? next : reset ? data.length : nextOffset + data.length);
2642
2418
  } catch (err) {
2643
2419
  setError(err?.message || 'Falha ao carregar packs');
2644
- if (reset) setPacks([]);
2420
+ setPacks([]);
2421
+ setPackHasMore(false);
2645
2422
  } finally {
2646
- if (reset) setPacksLoading(false);
2647
- else setPacksLoadingMore(false);
2648
- }
2649
- };
2650
-
2651
- const loadOrphans = async () => {
2652
- setOrphansLoading(true);
2653
- try {
2654
- const params = buildParams({
2655
- q: catalogFilter === 'trending' ? '' : appliedQuery,
2656
- category: catalogFilter === 'trending' ? '' : activeCategory,
2657
- limit: config.orphanLimit,
2658
- });
2659
- const payload = await fetchJson(`${config.orphanApiPath}?${params.toString()}`);
2660
- setOrphans(Array.isArray(payload?.data) ? payload.data : []);
2661
- } catch {
2662
- setOrphans([]);
2663
- } finally {
2664
- setOrphansLoading(false);
2423
+ setPacksLoading(false);
2665
2424
  }
2666
2425
  };
2667
2426
 
@@ -2798,8 +2557,9 @@ function StickersApp() {
2798
2557
  }
2799
2558
  };
2800
2559
 
2801
- const buildCatalogWebUrl = ({ q = appliedQuery, category = activeCategory, sort = sortBy, filter = catalogFilter } = {}) => {
2560
+ const buildCatalogWebUrl = ({ q = appliedQuery, category = activeCategory, sort = sortBy, filter = catalogFilter, page = catalogPage } = {}) => {
2802
2561
  const params = new URLSearchParams();
2562
+ const safePage = Math.max(FIRST_CATALOG_PAGE, Number(page || FIRST_CATALOG_PAGE));
2803
2563
  const normalizedFilter =
2804
2564
  String(filter || '')
2805
2565
  .trim()
@@ -2814,6 +2574,7 @@ function StickersApp() {
2814
2574
  if (String(category || '').trim()) params.set('category', String(category).trim().toLowerCase());
2815
2575
  if (normalizedSort && normalizedSort !== DEFAULT_CATALOG_SORT) params.set('sort', normalizedSort);
2816
2576
  }
2577
+ if (safePage > FIRST_CATALOG_PAGE) params.set('page', String(safePage));
2817
2578
  const qs = params.toString();
2818
2579
  return `${config.webPath}/${qs ? `?${qs}` : ''}`;
2819
2580
  };
@@ -2824,7 +2585,7 @@ function StickersApp() {
2824
2585
  return `${config.webPath}/creators?${params.toString()}`;
2825
2586
  };
2826
2587
 
2827
- const applyCatalogViewState = ({ q = '', category = '', sort = DEFAULT_CATALOG_SORT, filter = '' } = {}) => {
2588
+ const applyCatalogViewState = ({ q = '', category = '', sort = DEFAULT_CATALOG_SORT, filter = '', page = FIRST_CATALOG_PAGE } = {}) => {
2828
2589
  const normalizedFilter =
2829
2590
  String(filter || '')
2830
2591
  .trim()
@@ -2838,12 +2599,22 @@ function StickersApp() {
2838
2599
  : String(category || '')
2839
2600
  .trim()
2840
2601
  .toLowerCase();
2602
+ const nextPage = Math.max(FIRST_CATALOG_PAGE, Number(page || FIRST_CATALOG_PAGE));
2841
2603
 
2842
2604
  setCatalogFilter(normalizedFilter);
2843
2605
  setSortBy(normalizedSort);
2844
2606
  setQuery(nextQ);
2845
2607
  setAppliedQuery(nextQ);
2846
2608
  setActiveCategory(nextCategory);
2609
+ setCatalogPage(nextPage);
2610
+ };
2611
+
2612
+ const scrollToTopIfMobile = ({ behavior = 'smooth' } = {}) => {
2613
+ const isMobile = window.matchMedia ? window.matchMedia('(max-width: 1023px)').matches : window.innerWidth < 1024;
2614
+ if (!isMobile) return;
2615
+ window.requestAnimationFrame(() => {
2616
+ window.scrollTo({ top: 0, behavior });
2617
+ });
2847
2618
  };
2848
2619
 
2849
2620
  const openPack = (packKey, push = true) => {
@@ -2852,6 +2623,7 @@ function StickersApp() {
2852
2623
  setCurrentView('pack');
2853
2624
  setCurrentPackKey(packKey);
2854
2625
  setSortPickerOpen(false);
2626
+ scrollToTopIfMobile({ behavior: 'smooth' });
2855
2627
  };
2856
2628
 
2857
2629
  const goCatalog = (push = true) => {
@@ -2863,11 +2635,12 @@ function StickersApp() {
2863
2635
  setSortPickerOpen(false);
2864
2636
  };
2865
2637
 
2866
- const openCatalogWithState = ({ q = '', category = '', sort = DEFAULT_CATALOG_SORT, filter = '', push = true } = {}) => {
2638
+ const openCatalogWithState = ({ q = '', category = '', sort = DEFAULT_CATALOG_SORT, filter = '', page = FIRST_CATALOG_PAGE, push = true } = {}) => {
2867
2639
  const nextState = {
2868
2640
  q,
2869
2641
  category,
2870
2642
  sort: normalizeCatalogSort(sort || DEFAULT_CATALOG_SORT),
2643
+ page: Math.max(FIRST_CATALOG_PAGE, Number(page || FIRST_CATALOG_PAGE)),
2871
2644
  filter:
2872
2645
  String(filter || '')
2873
2646
  .trim()
@@ -2885,9 +2658,42 @@ function StickersApp() {
2885
2658
  setSortPickerOpen(false);
2886
2659
  };
2887
2660
 
2661
+ const goToCatalogPage = (nextPage, { push = true } = {}) => {
2662
+ const safePage = Math.max(FIRST_CATALOG_PAGE, Number(nextPage || FIRST_CATALOG_PAGE));
2663
+ if (safePage === catalogPage && currentView === 'catalog' && !currentPackKey) return;
2664
+ openCatalogWithState({
2665
+ q: catalogFilter === 'trending' ? '' : appliedQuery,
2666
+ category: catalogFilter === 'trending' ? '' : activeCategory,
2667
+ sort: sortBy,
2668
+ filter: catalogFilter,
2669
+ page: safePage,
2670
+ push,
2671
+ });
2672
+ window.scrollTo({ top: 0, behavior: 'smooth' });
2673
+ };
2674
+
2675
+ const scrollToCatalogPacksSection = ({ behavior = 'smooth' } = {}) => {
2676
+ const tryScroll = () => {
2677
+ const section = document.getElementById('catalog-packs-section');
2678
+ if (!section) return false;
2679
+ section.scrollIntoView({ behavior, block: 'start' });
2680
+ return true;
2681
+ };
2682
+
2683
+ if (tryScroll()) return;
2684
+ let tries = 0;
2685
+ const retry = () => {
2686
+ tries += 1;
2687
+ if (tryScroll() || tries >= 8) return;
2688
+ window.setTimeout(retry, 80);
2689
+ };
2690
+ window.setTimeout(retry, 80);
2691
+ };
2692
+
2888
2693
  const openTrendingCatalog = () => {
2889
2694
  openCatalogWithState({ q: '', category: '', sort: 'trending', filter: 'trending', push: true });
2890
2695
  setDiscoverTab('growing');
2696
+ scrollToCatalogPacksSection({ behavior: 'smooth' });
2891
2697
  };
2892
2698
 
2893
2699
  const openCreatorsRanking = (sort = DEFAULT_CREATORS_SORT, push = true) => {
@@ -3218,11 +3024,11 @@ function StickersApp() {
3218
3024
  if (sortPickerBusy || sortPickerLockRef.current) return;
3219
3025
  sortPickerLockRef.current = true;
3220
3026
  setSortPickerBusy(true);
3221
- const previousScrollY = window.scrollY || 0;
3222
3027
  if (catalogFilter) setCatalogFilter('');
3223
3028
  setSortBy(nextSort);
3029
+ setCatalogPage(FIRST_CATALOG_PAGE);
3224
3030
  setSortPickerOpen(false);
3225
- window.requestAnimationFrame(() => window.scrollTo({ top: previousScrollY }));
3031
+ scrollToCatalogPacksSection({ behavior: 'smooth' });
3226
3032
  window.setTimeout(() => {
3227
3033
  sortPickerLockRef.current = false;
3228
3034
  setSortPickerBusy(false);
@@ -3336,9 +3142,7 @@ function StickersApp() {
3336
3142
  return;
3337
3143
  }
3338
3144
  if (route.view === 'pack' && route.packKey) {
3339
- setCurrentView('pack');
3340
- setCurrentPackKey(route.packKey);
3341
- setSortPickerOpen(false);
3145
+ openPack(route.packKey, false);
3342
3146
  return;
3343
3147
  }
3344
3148
  applyCatalogViewState(parseCatalogSearchState(window.location.search));
@@ -3426,7 +3230,7 @@ function StickersApp() {
3426
3230
  if (nextUrl !== currentUrl) {
3427
3231
  window.history.replaceState({}, '', nextUrl);
3428
3232
  }
3429
- }, [currentView, currentPackKey, appliedQuery, activeCategory, sortBy, catalogFilter, config.webPath]);
3233
+ }, [currentView, currentPackKey, appliedQuery, activeCategory, sortBy, catalogFilter, catalogPage, config.webPath]);
3430
3234
 
3431
3235
  useEffect(() => {
3432
3236
  if (currentView !== 'creators') return;
@@ -3447,32 +3251,16 @@ function StickersApp() {
3447
3251
  return;
3448
3252
  }
3449
3253
  if (currentView !== 'catalog') return;
3450
- void loadPacks({ reset: true });
3451
- void loadOrphans();
3452
- }, [appliedQuery, activeCategory, sortBy, catalogFilter, currentView, currentPackKey, creatorSort]);
3254
+ void loadPacks({ page: catalogPage });
3255
+ }, [appliedQuery, activeCategory, sortBy, catalogFilter, currentView, currentPackKey, creatorSort, catalogPage]);
3453
3256
 
3454
3257
  useEffect(() => {
3455
3258
  if (currentView !== 'catalog' || currentPackKey) return undefined;
3456
3259
  const timer = setInterval(() => {
3457
- void loadPacks({ reset: true });
3458
- void loadOrphans();
3260
+ void loadPacks({ page: catalogPage });
3459
3261
  }, 60 * 1000);
3460
3262
  return () => clearInterval(timer);
3461
- }, [currentView, currentPackKey, appliedQuery, activeCategory, sortBy, catalogFilter]);
3462
-
3463
- useEffect(() => {
3464
- if (currentView !== 'catalog' || !sentinel || !packHasMore || packsLoading || packsLoadingMore || currentPackKey) return;
3465
- const observer = new IntersectionObserver(
3466
- (entries) => {
3467
- if (entries.some((entry) => entry.isIntersecting)) {
3468
- void loadPacks({ reset: false });
3469
- }
3470
- },
3471
- { rootMargin: '220px 0px' },
3472
- );
3473
- observer.observe(sentinel);
3474
- return () => observer.disconnect();
3475
- }, [sentinel, packHasMore, packsLoading, packsLoadingMore, packOffset, appliedQuery, activeCategory, sortBy, catalogFilter, currentView, currentPackKey]);
3263
+ }, [currentView, currentPackKey, appliedQuery, activeCategory, sortBy, catalogFilter, catalogPage]);
3476
3264
 
3477
3265
  useEffect(() => {
3478
3266
  const sync = () => setUploadTask(readUploadTask());
@@ -3613,6 +3401,7 @@ function StickersApp() {
3613
3401
  setShowAutocomplete(false);
3614
3402
  const next = query.trim();
3615
3403
  setCatalogFilter('');
3404
+ setCatalogPage(FIRST_CATALOG_PAGE);
3616
3405
  setAppliedQuery(next);
3617
3406
  if (next) {
3618
3407
  const nextHistory = [next, ...recentSearches.filter((entry) => entry !== next)].slice(0, 8);
@@ -3628,6 +3417,7 @@ function StickersApp() {
3628
3417
  if (!value) return;
3629
3418
  setQuery(value);
3630
3419
  setCatalogFilter('');
3420
+ setCatalogPage(FIRST_CATALOG_PAGE);
3631
3421
  setAppliedQuery(value);
3632
3422
  if (dynamicCategoryOptions.some((entry) => entry.value === value)) {
3633
3423
  setActiveCategory(value);
@@ -3640,6 +3430,7 @@ function StickersApp() {
3640
3430
  setAppliedQuery('');
3641
3431
  setActiveCategory('');
3642
3432
  setCatalogFilter('');
3433
+ setCatalogPage(FIRST_CATALOG_PAGE);
3643
3434
  };
3644
3435
 
3645
3436
  return html`
@@ -3755,7 +3546,7 @@ function StickersApp() {
3755
3546
  <h3 className="text-xs font-semibold uppercase tracking-wide text-slate-300">Filtros</h3>
3756
3547
  <button type="button" onClick=${clearFilters} className="h-8 rounded-lg border border-slate-700 px-2 text-[11px] text-slate-200 hover:bg-slate-800">Limpar</button>
3757
3548
  </div>
3758
- <p className="mt-1 text-[11px] text-slate-500">${packs.length}${packHasMore ? '+' : ''} packs · ${orphans.length} sem pack</p>
3549
+ <p className="mt-1 text-[11px] text-slate-500">${packs.length} packs nesta página</p>
3759
3550
  </div>
3760
3551
 
3761
3552
  <details open className="rounded-xl border border-slate-800 bg-slate-950/40 p-2">
@@ -3784,11 +3575,9 @@ function StickersApp() {
3784
3575
  <div>
3785
3576
  <p className="text-[11px] uppercase tracking-wide text-slate-400">Descobrir</p>
3786
3577
  <h3 className="text-sm font-semibold text-slate-100">Painel oficial do marketplace</h3>
3787
- <p className="text-[11px] text-slate-500">Métricas globais reais da plataforma (cache ~${globalMarketplaceStats?.cacheSeconds || 45}s, atualização automática).</p>
3788
3578
  </div>
3789
3579
  <div className="flex items-center gap-2">
3790
3580
  ${globalMarketplaceStatsLoading && !globalMarketplaceStats ? html`<span className="inline-flex h-8 items-center rounded-lg border border-slate-700 bg-slate-900/60 px-3 text-[11px] text-slate-300">Carregando métricas...</span>` : null} ${globalMarketplaceStatsError ? html`<span className="inline-flex h-8 items-center rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 text-[11px] text-amber-100">Fallback local</span>` : null}
3791
- <a href="/stickers/create/" className="inline-flex h-8 items-center rounded-lg border border-emerald-500/35 bg-emerald-500/10 px-3 text-[11px] font-semibold text-emerald-200 hover:bg-emerald-500/20"> ✨ Criar pack agora </a>
3792
3581
  </div>
3793
3582
  </div>
3794
3583
 
@@ -3826,13 +3615,37 @@ function StickersApp() {
3826
3615
  <div className="mt-2 lg:hidden">
3827
3616
  ${discoverTab === 'growing'
3828
3617
  ? html`
3829
- <section className="space-y-1.5">
3830
- <div className="flex items-center justify-between">
3831
- <h4 className="text-xs font-semibold text-slate-200">🔥 Em alta agora</h4>
3832
- <button type="button" onClick=${openTrendingCatalog} className="text-[10px] text-cyan-300">ver lista</button>
3833
- </div>
3834
- <div className="flex gap-2 overflow-x-auto pb-1">${growingNowPacks.slice(0, 8).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-grow-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3835
- </section>
3618
+ <div className="space-y-2">
3619
+ <section className="space-y-1.5">
3620
+ <div className="flex items-center justify-between">
3621
+ <h4 className="text-xs font-semibold text-slate-200">🔥 Em alta agora</h4>
3622
+ <button
3623
+ type="button"
3624
+ onClick=${openTrendingCatalog}
3625
+ className="inline-flex h-7 items-center gap-1 rounded-full border border-cyan-400/35 bg-cyan-500/10 px-2.5 text-[10px] font-semibold text-cyan-100 transition hover:bg-cyan-500/20 active:scale-[0.98]"
3626
+ >
3627
+ <span>ver lista</span>
3628
+ <span aria-hidden="true">↗</span>
3629
+ </button>
3630
+ </div>
3631
+ <div className="flex gap-2 overflow-x-auto pb-1">${growingNowPacks.slice(0, 8).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-grow-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3632
+ </section>
3633
+ <section className="space-y-1.5">
3634
+ <div className="flex items-center justify-between">
3635
+ <h4 className="text-xs font-semibold text-slate-200">🆕 Recém publicados</h4>
3636
+ <button
3637
+ type="button"
3638
+ onClick=${openSortPicker}
3639
+ disabled=${sortPickerBusy}
3640
+ className="inline-flex h-7 items-center gap-1 rounded-full border border-emerald-400/35 bg-emerald-500/10 px-2.5 text-[10px] font-semibold text-emerald-100 transition hover:bg-emerald-500/20 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100"
3641
+ >
3642
+ <span>ordenar</span>
3643
+ <span aria-hidden="true">⇅</span>
3644
+ </button>
3645
+ </div>
3646
+ <div className="flex gap-2 overflow-x-auto pb-1">${recentPublishedPacks.slice(0, 8).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-new-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3647
+ </section>
3648
+ </div>
3836
3649
  `
3837
3650
  : discoverTab === 'top'
3838
3651
  ? html`
@@ -3856,18 +3669,6 @@ function StickersApp() {
3856
3669
  </div>
3857
3670
  </div>
3858
3671
 
3859
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">${catalogMetricCards.map((card) => html`<${CatalogMetricCard} key=${card.key} label=${card.label} value=${card.value} valueRaw=${card.valueRaw} numberFormat=${card.numberFormat} icon=${card.icon} hint=${card.hint} bars=${card.bars} tone=${card.tone} />`)}</div>
3860
-
3861
- <div className="lg:hidden space-y-2">
3862
- <section className="space-y-1.5">
3863
- <div className="flex items-center justify-between">
3864
- <h4 className="text-xs font-semibold text-slate-200">🆕 Recém publicados</h4>
3865
- <button type="button" onClick=${openSortPicker} disabled=${sortPickerBusy} className="text-[10px] text-cyan-300 disabled:opacity-50">ordenar</button>
3866
- </div>
3867
- <div className="flex gap-2 overflow-x-auto pb-1">${recentPublishedPacks.slice(0, 8).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-new-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3868
- </section>
3869
- </div>
3870
-
3871
3672
  <div className="hidden lg:block rounded-2xl border border-emerald-500/20 bg-gradient-to-r from-emerald-500/10 to-cyan-500/5 p-2.5">
3872
3673
  <div className="flex items-center justify-between gap-3">
3873
3674
  <div>
@@ -3882,11 +3683,11 @@ function StickersApp() {
3882
3683
  : null}
3883
3684
  ${packs.length
3884
3685
  ? html`
3885
- <section className="space-y-3 min-w-0">
3686
+ <section id="catalog-packs-section" className="space-y-3 min-w-0">
3886
3687
  <div className="flex items-end justify-between gap-3">
3887
3688
  <div>
3888
3689
  <h2 className="text-lg sm:text-xl font-bold">Packs</h2>
3889
- <p className="text-xs text-slate-400">${sortedPacks.length}${packHasMore ? '+' : ''} resultados · ${categoryActiveLabel}</p>
3690
+ <p className="text-xs text-slate-400">Página ${catalogPage} · ${sortedPacks.length} resultados · ${categoryActiveLabel}</p>
3890
3691
  </div>
3891
3692
  <div className="hidden md:flex items-center gap-2">
3892
3693
  <span className="text-xs text-slate-400">Ordenar por</span>
@@ -3896,21 +3697,53 @@ function StickersApp() {
3896
3697
  </button>
3897
3698
  </div>
3898
3699
  </div>
3700
+ <div className="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-slate-800 bg-slate-900/60 px-3 py-2">
3701
+ <span className="text-xs text-slate-400">Página ${catalogPage}${packHasMore ? ' · há mais resultados' : ' · fim da lista'}</span>
3702
+ <div className="flex items-center gap-2">
3703
+ <button
3704
+ type="button"
3705
+ onClick=${() => goToCatalogPage(catalogPage - 1, { push: true })}
3706
+ disabled=${!canGoCatalogPrev}
3707
+ className="inline-flex h-8 items-center rounded-lg border border-slate-700 px-3 text-xs text-slate-200 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
3708
+ >
3709
+ Anterior
3710
+ </button>
3711
+ <button
3712
+ type="button"
3713
+ onClick=${() => goToCatalogPage(catalogPage + 1, { push: true })}
3714
+ disabled=${!canGoCatalogNext}
3715
+ className="inline-flex h-8 items-center rounded-lg border border-slate-700 px-3 text-xs text-slate-200 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
3716
+ >
3717
+ Próxima
3718
+ </button>
3719
+ </div>
3720
+ </div>
3899
3721
  <div className="grid min-w-0 grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-2.5 sm:gap-3">${sortedPacks.map((pack, index) => html`<div key=${pack.pack_key || pack.id} className="fade-card"><${PackCard} pack=${pack} index=${index} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} /></div>`)}</div>
3900
- <div ref=${setSentinel} className="h-8 flex items-center justify-center text-xs text-slate-500">${packsLoadingMore ? 'Carregando mais packs...' : packHasMore ? 'Role para carregar mais' : 'Fim da lista'}</div>
3722
+ <div className="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-slate-800 bg-slate-900/60 px-3 py-2">
3723
+ <span className="text-xs text-slate-400">Página ${catalogPage}${packHasMore ? ' · há mais resultados' : ' · fim da lista'}</span>
3724
+ <div className="flex items-center gap-2">
3725
+ <button
3726
+ type="button"
3727
+ onClick=${() => goToCatalogPage(catalogPage - 1, { push: true })}
3728
+ disabled=${!canGoCatalogPrev}
3729
+ className="inline-flex h-8 items-center rounded-lg border border-slate-700 px-3 text-xs text-slate-200 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
3730
+ >
3731
+ Anterior
3732
+ </button>
3733
+ <button
3734
+ type="button"
3735
+ onClick=${() => goToCatalogPage(catalogPage + 1, { push: true })}
3736
+ disabled=${!canGoCatalogNext}
3737
+ className="inline-flex h-8 items-center rounded-lg border border-slate-700 px-3 text-xs text-slate-200 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
3738
+ >
3739
+ Próxima
3740
+ </button>
3741
+ </div>
3742
+ </div>
3901
3743
  </section>
3902
3744
  `
3903
3745
  : null}
3904
3746
  ${packsLoading ? html`<${SkeletonGrid} count=${10} />` : null} ${!packsLoading && !hasAnyResult ? html`<${EmptyState} onClear=${clearFilters} />` : null}
3905
-
3906
- <section className="space-y-2.5">
3907
- <div className="flex items-center justify-between">
3908
- <h2 className="text-base sm:text-lg font-bold">Stickers sem pack</h2>
3909
- <span className="text-xs text-slate-400">${orphans.length} resultados</span>
3910
- </div>
3911
-
3912
- ${orphansLoading ? html`<div className="grid grid-cols-3 md:grid-cols-5 lg:grid-cols-8 gap-2.5 sm:gap-3">${Array.from({ length: 16 }).map((_, i) => html`<div key=${i} className="rounded-2xl border border-slate-700 bg-slate-800 animate-pulse aspect-square"></div>`)}</div>` : html` <div className="grid grid-cols-3 md:grid-cols-5 lg:grid-cols-8 gap-2.5 sm:gap-3">${orphans.map((item) => html`<div key=${item.id} className="fade-card"><${OrphanCard} sticker=${item} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} /></div>`)}</div> `}
3913
- </section>
3914
3747
  </div>
3915
3748
  </div>
3916
3749
  `}