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.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
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user