feat: ui polish, docs, api hardening, and common pages

This commit is contained in:
Dwindi Ramadhana
2026-02-06 14:04:41 +07:00
parent 0f602c12bc
commit 844ad4901b
18 changed files with 1106 additions and 128 deletions

View File

@@ -10,6 +10,11 @@
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
#grid {
--card-min: 104px;
--emoji-size: 2rem;
grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
}
</style>
@endpush
@@ -136,10 +141,15 @@
<div class="flex items-center gap-2 mb-4 px-1">
<h4 class="font-bold text-gray-200">All Emojis</h4>
<div class="h-px bg-white/10 flex-1"></div>
<div class="hidden sm:flex items-center gap-2 text-[11px] text-gray-400">
<button id="grid-smaller" class="w-7 h-7 rounded-md bg-white/5 hover:bg-white/10 border border-white/10">-</button>
<input id="grid-size" type="range" min="0" max="2" step="1" value="1" class="w-20 accent-[#2053ff]">
<button id="grid-bigger" class="w-7 h-7 rounded-md bg-white/5 hover:bg-white/10 border border-white/10">+</button>
</div>
<span id="count" class="text-xs text-gray-500">0 / 0</span>
</div>
<div id="grid" class="grid grid-cols-5 sm:grid-cols-6 md:grid-cols-8 lg:grid-cols-10 xl:grid-cols-14 2xl:grid-cols-16 gap-2 pb-8"></div>
<div id="grid" class="grid gap-2 pb-8"></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>
@@ -202,6 +212,10 @@
const heroMain = document.getElementById('hero-main');
const heroOptional1 = document.getElementById('hero-optional-1');
const heroOptional2 = document.getElementById('hero-optional-2');
const gridSizeEl = document.getElementById('grid-size');
const gridSmallerEl = document.getElementById('grid-smaller');
const gridBiggerEl = document.getElementById('grid-bigger');
const densityStorageKey = 'dewemoji_grid_density';
if (initialQuery) qEl.value = initialQuery;
@@ -262,6 +276,21 @@
return String(s || '').replace(/[&<>"']/g, (c) => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
}
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);
if (gridSizeEl) gridSizeEl.value = String(safe);
localStorage.setItem(densityStorageKey, String(safe));
}
function hasActiveFilters() {
return qEl.value.trim() !== '' || catEl.value !== '' || subEl.value !== '';
}
@@ -396,6 +425,14 @@
const res = await fetch('/v1/emojis?' + 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 || [];
@@ -412,13 +449,28 @@
}
items.forEach((item) => {
const card = document.createElement('a');
card.href = '/emoji/' + encodeURIComponent(item.slug);
card.className = 'aspect-square rounded-lg bg-white/5 hover:bg-white/10 flex flex-col items-center justify-center gap-1 text-center transition-transform hover:scale-105 border border-transparent hover:border-white/20';
const card = document.createElement('div');
card.className = 'relative aspect-square rounded-lg bg-white/5 hover:bg-white/10 transition-transform hover:scale-[1.02] border border-transparent hover:border-white/20 overflow-hidden group';
card.innerHTML = `
<span class="text-2xl leading-none">${esc(item.emoji)}</span>
<span class="emoji-name-clamp text-[10px] text-gray-400 px-1 w-full">${esc(item.name)}</span>
<a href="/emoji/${encodeURIComponent(item.slug)}" class="absolute inset-0 flex items-center justify-center pb-10">
<span class="leading-none" style="font-size:var(--emoji-size)">${esc(item.emoji)}</span>
</a>
<div class="absolute bottom-0 left-0 right-0 border-t border-white/10 bg-black/20 px-2 py-1.5 flex items-end gap-1">
<span class="emoji-name-clamp text-[10px] text-gray-300 text-left flex-1">${esc(item.name)}</span>
<button type="button" class="copy-btn shrink-0 w-6 h-6 rounded bg-white/10 hover:bg-brand-ocean/30 text-[11px] text-gray-200 hover:text-white transition-colors" title="Copy emoji"></button>
</div>
`;
const copyBtn = card.querySelector('.copy-btn');
if (copyBtn) {
copyBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
navigator.clipboard.writeText(item.emoji).then(() => {
showToast('Copied ' + item.emoji);
addRecent(item.emoji);
});
});
}
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
navigator.clipboard.writeText(item.emoji).then(() => {
@@ -462,6 +514,15 @@
});
});
if (gridSizeEl && gridSmallerEl && gridBiggerEl) {
const initialDensity = localStorage.getItem(densityStorageKey) ?? '1';
applyGridDensity(Number(initialDensity));
gridSizeEl.addEventListener('input', () => applyGridDensity(Number(gridSizeEl.value)));
gridSmallerEl.addEventListener('click', () => applyGridDensity(Number(gridSizeEl.value) - 1));
gridBiggerEl.addEventListener('click', () => applyGridDensity(Number(gridSizeEl.value) + 1));
}
(async () => {
await loadCategories();
if (initialCategory && state.categories[initialCategory]) {