feat(journal): beautiful entrance animation for entries — cascade fade-up on load, yellow glow pop for newly submitted entries
This commit is contained in:
@@ -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}}
|
||||
|
||||
|
||||
|
||||
+17
-2
@@ -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 `
|
||||
<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="">` : ''}
|
||||
<div class="info">
|
||||
<div class="name">${esc(e.student_name)}</div>
|
||||
@@ -109,7 +112,7 @@ function gridEntryHTML(e) {
|
||||
function cardEntryHTML(e) {
|
||||
const mp = mainPhotoPath(e);
|
||||
return `
|
||||
<div class="journal-card">
|
||||
<div class="journal-card${highlightNewIds.has(e.id) ? ' entry-appear' : ' entry-enter'}">
|
||||
${mp
|
||||
? `<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>`}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user