Improve emoji grid density and infinite scroll behavior
This commit is contained in:
@@ -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();
|
||||
})();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user