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') @push('head')
<style> <style>
#grid { #grid {
--card-min: 104px; --card-min: 54px;
--emoji-size: 2rem; --emoji-size: 1.9rem;
grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr)); grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
} }
#grid[data-hide-labels="1"] .emoji-card { #grid[data-hide-labels="1"] .emoji-card {
@@ -25,8 +25,7 @@
} }
@media (max-width: 640px) { @media (max-width: 640px) {
#grid { #grid {
--card-min: 0px; --card-min: 44px;
grid-template-columns: repeat(3, minmax(0, 1fr));
} }
} }
</style> </style>
@@ -144,7 +143,7 @@
</div> </div>
</header> </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 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"> <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"> <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" class="grid gap-2 pb-8"></div>
<div id="grid-sentinel" class="h-2 w-full"></div>
<div class="py-8 flex justify-center"> <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> <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> </div>
@@ -248,7 +248,7 @@
@push('scripts') @push('scripts')
<script> <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 userTier = @json($userTier ?? null);
const isPersonal = userTier === 'personal'; const isPersonal = userTier === 'personal';
const isSignedIn = @json(auth()->check()); const isSignedIn = @json(auth()->check());
@@ -266,8 +266,10 @@
const themeMobileBtn = document.getElementById('theme-toggle-mobile'); const themeMobileBtn = document.getElementById('theme-toggle-mobile');
const themeDesktopBtn = document.getElementById('theme-toggle'); const themeDesktopBtn = document.getElementById('theme-toggle');
const grid = document.getElementById('grid'); const grid = document.getElementById('grid');
const catalogScrollEl = document.getElementById('catalog-scroll');
const count = document.getElementById('count'); const count = document.getElementById('count');
const more = document.getElementById('more'); const more = document.getElementById('more');
const gridSentinel = document.getElementById('grid-sentinel');
const resultCount = document.getElementById('result-count'); const resultCount = document.getElementById('result-count');
const datasetCount = document.getElementById('dataset-count'); const datasetCount = document.getElementById('dataset-count');
const trendingList = document.getElementById('trending-list'); const trendingList = document.getElementById('trending-list');
@@ -284,6 +286,19 @@
const labelsToggleEl = document.getElementById('labels-toggle'); const labelsToggleEl = document.getElementById('labels-toggle');
const densityStorageKey = 'dewemoji_grid_density'; const densityStorageKey = 'dewemoji_grid_density';
const labelsStorageKey = 'dewemoji_grid_show_labels'; 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 csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const keywordEditModal = document.getElementById('keyword-edit-modal'); const keywordEditModal = document.getElementById('keyword-edit-modal');
const keywordEditClose = document.getElementById('keyword-edit-close'); const keywordEditClose = document.getElementById('keyword-edit-close');
@@ -420,15 +435,11 @@
} }
function applyGridDensity(level) { function applyGridDensity(level) {
const sizes = [ const safe = Math.max(0, Math.min(6, Number(level) || 0));
{ min: 90, emoji: '2.2rem' }, const min = 34 + (safe * 10);
{ min: 108, emoji: '2.45rem' }, const emoji = 1.45 + (safe * 0.17);
{ min: 132, emoji: '2.8rem' }, grid.style.setProperty('--card-min', `${min}px`);
]; grid.style.setProperty('--emoji-size', `${emoji.toFixed(2)}rem`);
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);
if (gridSizeEl) gridSizeEl.value = String(safe); if (gridSizeEl) gridSizeEl.value = String(safe);
localStorage.setItem(densityStorageKey, String(safe)); localStorage.setItem(densityStorageKey, String(safe));
} }
@@ -553,6 +564,8 @@
} }
async function fetchEmojis(reset = false) { async function fetchEmojis(reset = false) {
if (isFetching) return;
isFetching = true;
if (reset) { if (reset) {
state.page = 1; state.page = 1;
state.items = []; state.items = [];
@@ -567,6 +580,7 @@
if (subEl.value) params.set('subcategory', subEl.value); if (subEl.value) params.set('subcategory', subEl.value);
const endpoint = isSignedIn ? '/dashboard/keywords/search' : '/v1/emojis'; const endpoint = isSignedIn ? '/dashboard/keywords/search' : '/v1/emojis';
try {
const res = await fetch(endpoint + '?' + params.toString()); const res = await fetch(endpoint + '?' + params.toString());
const data = await res.json(); const data = await res.json();
if (!res.ok) { if (!res.ok) {
@@ -584,6 +598,16 @@
renderGrid(incoming, reset); renderGrid(incoming, reset);
updateStats(); updateStats();
syncUrl(); 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) { function renderGrid(items, reset) {
@@ -678,8 +702,7 @@
}); });
more.addEventListener('click', async () => { more.addEventListener('click', async () => {
state.page += 1; await maybeAutoLoadNextPage();
await fetchEmojis(false);
}); });
document.querySelectorAll('.quick-tag').forEach((btn) => { document.querySelectorAll('.quick-tag').forEach((btn) => {
@@ -739,7 +762,8 @@
}); });
if (gridSizeEl && gridSmallerEl && gridBiggerEl) { if (gridSizeEl && gridSmallerEl && gridBiggerEl) {
const initialDensity = localStorage.getItem(densityStorageKey) ?? '1'; gridSizeEl.max = '6';
const initialDensity = localStorage.getItem(densityStorageKey) ?? '2';
applyGridDensity(Number(initialDensity)); applyGridDensity(Number(initialDensity));
gridSizeEl.addEventListener('input', () => applyGridDensity(Number(gridSizeEl.value))); gridSizeEl.addEventListener('input', () => applyGridDensity(Number(gridSizeEl.value)));
@@ -747,11 +771,11 @@
gridBiggerEl.addEventListener('click', () => applyGridDensity(Number(gridSizeEl.value) + 1)); gridBiggerEl.addEventListener('click', () => applyGridDensity(Number(gridSizeEl.value) + 1));
} }
gridSmallerMobileEl?.addEventListener('click', () => { gridSmallerMobileEl?.addEventListener('click', () => {
const base = Number(localStorage.getItem(densityStorageKey) ?? gridSizeEl?.value ?? 1); const base = Number(localStorage.getItem(densityStorageKey) ?? gridSizeEl?.value ?? 2);
applyGridDensity(base - 1); applyGridDensity(base - 1);
}); });
gridBiggerMobileEl?.addEventListener('click', () => { gridBiggerMobileEl?.addEventListener('click', () => {
const base = Number(localStorage.getItem(densityStorageKey) ?? gridSizeEl?.value ?? 1); const base = Number(localStorage.getItem(densityStorageKey) ?? gridSizeEl?.value ?? 2);
applyGridDensity(base + 1); applyGridDensity(base + 1);
}); });
labelsToggleEl?.addEventListener('click', () => { labelsToggleEl?.addEventListener('click', () => {
@@ -776,6 +800,23 @@
renderTrendingFromItems(state.items); renderTrendingFromItems(state.items);
renderRecent(); renderRecent();
updateHeroMode(); 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(); lucide.createIcons();
})(); })();
})(); })();