@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.
- package/.env.example +541 -468
- package/.github/workflows/codeql.yml +101 -0
- package/README.md +9 -9
- package/ml/clip_classifier/requirements.txt +2 -2
- package/package.json +1 -1
- package/public/js/apps/stickersApp.js +155 -322
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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 ({
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2420
|
+
setPacks([]);
|
|
2421
|
+
setPackHasMore(false);
|
|
2645
2422
|
} finally {
|
|
2646
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
3451
|
-
|
|
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({
|
|
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}
|
|
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
|
-
<
|
|
3830
|
-
<
|
|
3831
|
-
<
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
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"
|
|
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
|
|
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
|
`}
|