Improve emoji grid density and infinite scroll behavior

This commit is contained in:
Dwindi Ramadhana
2026-02-22 00:40:30 +07:00
parent 638e1d5b20
commit d5d925d534

View File

@@ -6,8 +6,8 @@
@push('head')
<style>
#grid {
--card-min: 104px;
--emoji-size: 2rem;
--card-min: 54px;
--emoji-size: 1.9rem;
grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
}
#grid[data-hide-labels="1"] .emoji-card {
@@ -25,8 +25,7 @@
}
@media (max-width: 640px) {
#grid {
--card-min: 0px;
grid-template-columns: repeat(3, minmax(0, 1fr));
--card-min: 44px;
}
}
</style>
@@ -144,7 +143,7 @@
</div>
</header>
<div class="flex-1 overflow-y-auto p-4 sm:p-6 pb-24 lg:pb-6">
<div id="catalog-scroll" class="flex-1 overflow-y-auto p-4 sm:p-6 pb-24 lg:pb-6">
<div class="w-full">
<div id="hero-cards" class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-6 transition-all duration-300">
<section id="hero-main" class="hidden md:flex md:col-span-2 glass-card rounded-2xl p-6 relative overflow-hidden min-h-[180px] flex-col justify-end transition-all duration-300">
@@ -201,6 +200,7 @@
<div id="grid" class="grid gap-2 pb-8"></div>
<div id="grid-sentinel" class="h-2 w-full"></div>
<div class="py-8 flex justify-center">
<button id="more" class="hidden px-4 py-2 rounded-xl bg-white/10 hover:bg-white/20 border border-white/10 text-sm text-gray-200 transition-colors">Load more</button>
</div>
@@ -248,7 +248,7 @@
@push('scripts')
<script>
(() => {
const state = { page: 1, limit: 32, total: 0, items: [], categories: {} };
const state = { page: 1, limit: 48, total: 0, items: [], categories: {} };
const userTier = @json($userTier ?? null);
const isPersonal = userTier === 'personal';
const isSignedIn = @json(auth()->check());
@@ -266,8 +266,10 @@
const themeMobileBtn = document.getElementById('theme-toggle-mobile');
const themeDesktopBtn = document.getElementById('theme-toggle');
const grid = document.getElementById('grid');
const catalogScrollEl = document.getElementById('catalog-scroll');
const count = document.getElementById('count');
const more = document.getElementById('more');
const gridSentinel = document.getElementById('grid-sentinel');
const resultCount = document.getElementById('result-count');
const datasetCount = document.getElementById('dataset-count');
const trendingList = document.getElementById('trending-list');
@@ -284,6 +286,19 @@
const labelsToggleEl = document.getElementById('labels-toggle');
const densityStorageKey = 'dewemoji_grid_density';
const labelsStorageKey = 'dewemoji_grid_show_labels';
let isFetching = false;
let autoLoadObserver = null;
const AUTOLOAD_THRESHOLD_PX = 420;
function initialLimitByViewport() {
const w = window.innerWidth || 1280;
if (w >= 1536) return 120;
if (w >= 1280) return 96;
if (w >= 1024) return 80;
if (w >= 768) return 64;
return 48;
}
state.limit = initialLimitByViewport();
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const keywordEditModal = document.getElementById('keyword-edit-modal');
const keywordEditClose = document.getElementById('keyword-edit-close');
@@ -420,15 +435,11 @@
}
function applyGridDensity(level) {
const sizes = [
{ min: 90, emoji: '2.2rem' },
{ min: 108, emoji: '2.45rem' },
{ min: 132, emoji: '2.8rem' },
];
const safe = Math.max(0, Math.min(2, Number(level) || 0));
const conf = sizes[safe];
grid.style.setProperty('--card-min', `${conf.min}px`);
grid.style.setProperty('--emoji-size', conf.emoji);
const safe = Math.max(0, Math.min(6, Number(level) || 0));
const min = 34 + (safe * 10);
const emoji = 1.45 + (safe * 0.17);
grid.style.setProperty('--card-min', `${min}px`);
grid.style.setProperty('--emoji-size', `${emoji.toFixed(2)}rem`);
if (gridSizeEl) gridSizeEl.value = String(safe);
localStorage.setItem(densityStorageKey, String(safe));
}
@@ -553,6 +564,8 @@
}
async function fetchEmojis(reset = false) {
if (isFetching) return;
isFetching = true;
if (reset) {
state.page = 1;
state.items = [];
@@ -567,23 +580,34 @@
if (subEl.value) params.set('subcategory', subEl.value);
const endpoint = isSignedIn ? '/dashboard/keywords/search' : '/v1/emojis';
const res = await fetch(endpoint + '?' + params.toString());
const data = await res.json();
if (!res.ok) {
const msg = data.message || data.error || `API error (${res.status})`;
grid.innerHTML = `<p class="text-xs text-amber-300 col-span-full">${esc(msg)}</p>`;
state.total = 0;
state.items = [];
updateStats();
return;
}
state.total = data.total || 0;
try {
const res = await fetch(endpoint + '?' + params.toString());
const data = await res.json();
if (!res.ok) {
const msg = data.message || data.error || `API error (${res.status})`;
grid.innerHTML = `<p class="text-xs text-amber-300 col-span-full">${esc(msg)}</p>`;
state.total = 0;
state.items = [];
updateStats();
return;
}
state.total = data.total || 0;
const incoming = data.items || [];
incoming.forEach((item) => state.items.push(item));
renderGrid(incoming, reset);
updateStats();
syncUrl();
const incoming = data.items || [];
incoming.forEach((item) => state.items.push(item));
renderGrid(incoming, reset);
updateStats();
syncUrl();
} finally {
isFetching = false;
}
}
async function maybeAutoLoadNextPage() {
if (isFetching) return;
if (state.items.length >= state.total) return;
state.page += 1;
await fetchEmojis(false);
}
function renderGrid(items, reset) {
@@ -678,8 +702,7 @@
});
more.addEventListener('click', async () => {
state.page += 1;
await fetchEmojis(false);
await maybeAutoLoadNextPage();
});
document.querySelectorAll('.quick-tag').forEach((btn) => {
@@ -739,7 +762,8 @@
});
if (gridSizeEl && gridSmallerEl && gridBiggerEl) {
const initialDensity = localStorage.getItem(densityStorageKey) ?? '1';
gridSizeEl.max = '6';
const initialDensity = localStorage.getItem(densityStorageKey) ?? '2';
applyGridDensity(Number(initialDensity));
gridSizeEl.addEventListener('input', () => applyGridDensity(Number(gridSizeEl.value)));
@@ -747,11 +771,11 @@
gridBiggerEl.addEventListener('click', () => applyGridDensity(Number(gridSizeEl.value) + 1));
}
gridSmallerMobileEl?.addEventListener('click', () => {
const base = Number(localStorage.getItem(densityStorageKey) ?? gridSizeEl?.value ?? 1);
const base = Number(localStorage.getItem(densityStorageKey) ?? gridSizeEl?.value ?? 2);
applyGridDensity(base - 1);
});
gridBiggerMobileEl?.addEventListener('click', () => {
const base = Number(localStorage.getItem(densityStorageKey) ?? gridSizeEl?.value ?? 1);
const base = Number(localStorage.getItem(densityStorageKey) ?? gridSizeEl?.value ?? 2);
applyGridDensity(base + 1);
});
labelsToggleEl?.addEventListener('click', () => {
@@ -776,6 +800,23 @@
renderTrendingFromItems(state.items);
renderRecent();
updateHeroMode();
if (catalogScrollEl) {
catalogScrollEl.addEventListener('scroll', () => {
const remaining = catalogScrollEl.scrollHeight - catalogScrollEl.scrollTop - catalogScrollEl.clientHeight;
if (remaining <= AUTOLOAD_THRESHOLD_PX) {
maybeAutoLoadNextPage();
}
}, { passive: true });
}
if (gridSentinel && 'IntersectionObserver' in window) {
autoLoadObserver = new IntersectionObserver(async (entries) => {
const first = entries[0];
if (!first?.isIntersecting) return;
await maybeAutoLoadNextPage();
}, { root: catalogScrollEl || null, rootMargin: '500px 0px' });
autoLoadObserver.observe(gridSentinel);
}
lucide.createIcons();
})();
})();