From cc9a9e6c2d9815d604a836b553d6f62d0aad1129 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 16 Sep 2026 20:14:09 +0300 Subject: [PATCH] =?UTF-8?q?feat(journal):=20beautiful=20entrance=20animati?= =?UTF-8?q?on=20for=20entries=20=E2=80=94=20cascade=20fade-up=20on=20load,?= =?UTF-8?q?=20yellow=20glow=20pop=20for=20newly=20submitted=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/admin.css | 9 +++++++++ public/js/journal.js | 19 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/public/admin.css b/public/admin.css index 0bbf891..a09fd5e 100644 --- a/public/admin.css +++ b/public/admin.css @@ -418,4 +418,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b .worker-counts .chip.skipped b{color:var(--muted)} .worker-counts .chip.reverted b{color:var(--accent)} +@keyframes entryEnter{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}} +@keyframes entryPopIn{0%{opacity:0;transform:translateY(26px) scale(.95)}55%{opacity:1;transform:translateY(-5px) scale(1.015)}100%{opacity:1;transform:none}} +@keyframes entryFadeOut{to{opacity:0}} +.entry-enter{animation:entryEnter .4s cubic-bezier(.22,1,.36,1) both} +.entry-appear{animation:entryPopIn .6s cubic-bezier(.22,1,.36,1) both;position:relative} +.entry-appear::before{content:'';position:absolute;inset:-1px;border-radius:inherit;box-shadow:0 0 0 3px rgba(247,201,14,.9),0 10px 30px rgba(247,201,14,.4);animation:entryFadeOut 2.4s ease .35s both;pointer-events:none} +.entry-appear::after{content:'';position:absolute;inset:0;border-radius:inherit;background:linear-gradient(180deg,rgba(247,201,14,.16),transparent 55%);animation:entryFadeOut 2.4s ease .35s both;pointer-events:none} +@media (prefers-reduced-motion: reduce){.entry-enter,.entry-appear,.entry-appear::before,.entry-appear::after{animation:none}} + diff --git a/public/js/journal.js b/public/js/journal.js index 56999e3..ed19784 100644 --- a/public/js/journal.js +++ b/public/js/journal.js @@ -1,4 +1,7 @@ let currentEntries = []; +let seenEntryIds = new Set(); +let highlightNewIds = new Set(); +let highlightClearTimer = null; let editId = null; let editGroups = []; let groupsLoaded = false; @@ -91,7 +94,7 @@ async function detachFile(id) { function gridEntryHTML(e) { const mp = mainPhotoPath(e); return ` -
+
${mp ? `` : ''}
${esc(e.student_name)}
@@ -109,7 +112,7 @@ function gridEntryHTML(e) { function cardEntryHTML(e) { const mp = mainPhotoPath(e); return ` -
+
${mp ? `` : `
`} @@ -151,6 +154,10 @@ function renderGrid() { grid.className = 'grid'; grid.innerHTML = currentEntries.map(gridEntryHTML).join(''); } + const items = Array.from(grid.children); + items.forEach((el, i) => { + el.style.animationDelay = `${Math.min(i * 45, 500)}ms`; + }); renderIcons(); } @@ -208,6 +215,14 @@ async function loadEntries() { p.set('offset', (page - 1) * PAGE_SIZE); const res = await fetch(`${API}/api/entries?${p}`, { headers: hdr() }); const data = await res.json(); + const firstLoad = seenEntryIds.size === 0; + const newIds = firstLoad ? [] : data.entries.filter(e => !seenEntryIds.has(e.id)).map(e => e.id); + if (newIds.length) { + highlightNewIds = new Set([...highlightNewIds, ...newIds]); + if (highlightClearTimer) clearTimeout(highlightClearTimer); + highlightClearTimer = setTimeout(() => { highlightNewIds = new Set(); }, 8000); + } + data.entries.forEach(e => seenEntryIds.add(e.id)); currentEntries = data.entries; totalEntries = data.total; renderGrid();