feat(journal): beautiful entrance animation for entries — cascade fade-up on load, yellow glow pop for newly submitted entries

This commit is contained in:
dev
2026-09-16 20:14:09 +03:00
parent 2784084c68
commit cc9a9e6c2d
2 changed files with 26 additions and 2 deletions
+9
View File
@@ -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.skipped b{color:var(--muted)}
.worker-counts .chip.reverted b{color:var(--accent)} .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}}
+17 -2
View File
@@ -1,4 +1,7 @@
let currentEntries = []; let currentEntries = [];
let seenEntryIds = new Set();
let highlightNewIds = new Set();
let highlightClearTimer = null;
let editId = null; let editId = null;
let editGroups = []; let editGroups = [];
let groupsLoaded = false; let groupsLoaded = false;
@@ -91,7 +94,7 @@ async function detachFile(id) {
function gridEntryHTML(e) { function gridEntryHTML(e) {
const mp = mainPhotoPath(e); const mp = mainPhotoPath(e);
return ` return `
<div class="card"> <div class="card${highlightNewIds.has(e.id) ? ' entry-appear' : ' entry-enter'}">
${mp ? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" loading="lazy" decoding="async" alt="">` : ''} ${mp ? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" loading="lazy" decoding="async" alt="">` : ''}
<div class="info"> <div class="info">
<div class="name">${esc(e.student_name)}</div> <div class="name">${esc(e.student_name)}</div>
@@ -109,7 +112,7 @@ function gridEntryHTML(e) {
function cardEntryHTML(e) { function cardEntryHTML(e) {
const mp = mainPhotoPath(e); const mp = mainPhotoPath(e);
return ` return `
<div class="journal-card"> <div class="journal-card${highlightNewIds.has(e.id) ? ' entry-appear' : ' entry-enter'}">
${mp ${mp
? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" loading="lazy" decoding="async" alt="">` ? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" loading="lazy" decoding="async" alt="">`
: `<div class="no-photo"><svg viewBox="0 0 240 160" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><circle cx="105" cy="48" r="15"/><path d="M74 140 C74 106 88 92 105 92 C122 92 136 106 136 140 Z"/><circle cx="145" cy="58" r="18"/><path d="M108 144 C108 104 124 88 145 88 C166 88 182 104 182 144 Z"/></svg></div>`} : `<div class="no-photo"><svg viewBox="0 0 240 160" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><circle cx="105" cy="48" r="15"/><path d="M74 140 C74 106 88 92 105 92 C122 92 136 106 136 140 Z"/><circle cx="145" cy="58" r="18"/><path d="M108 144 C108 104 124 88 145 88 C166 88 182 104 182 144 Z"/></svg></div>`}
@@ -151,6 +154,10 @@ function renderGrid() {
grid.className = 'grid'; grid.className = 'grid';
grid.innerHTML = currentEntries.map(gridEntryHTML).join(''); 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(); renderIcons();
} }
@@ -208,6 +215,14 @@ async function loadEntries() {
p.set('offset', (page - 1) * PAGE_SIZE); p.set('offset', (page - 1) * PAGE_SIZE);
const res = await fetch(`${API}/api/entries?${p}`, { headers: hdr() }); const res = await fetch(`${API}/api/entries?${p}`, { headers: hdr() });
const data = await res.json(); 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; currentEntries = data.entries;
totalEntries = data.total; totalEntries = data.total;
renderGrid(); renderGrid();