feat: live updates + animations

This commit is contained in:
2026-04-21 13:45:33 +00:00
parent e9037604f8
commit a17535abda
+131 -7
View File
@@ -3,7 +3,10 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
<meta name="googlebot" content="noindex, nofollow">
<title>24mycloud</title> <title>24mycloud</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0%25' stop-color='%235856d6'/%3E%3Cstop offset='100%25' stop-color='%23818cf8'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='14' fill='url(%23g)'/%3E%3Cpath d='M44 28h-1.1A10.5 10.5 0 0 0 23 24a10.5 10.5 0 0 0 .5 3.2A7.5 7.5 0 0 0 16.5 35 7.5 7.5 0 0 0 24 42.5h20a6.5 6.5 0 0 0 0-13z' fill='none' stroke='white' stroke-width='2.5' stroke-linejoin='round'/%3E%3C/svg%3E">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style> <style>
:root { :root {
@@ -493,6 +496,75 @@
.empty { text-align: center; padding: 36px; color: var(--text-3); font-size: 13px; } .empty { text-align: center; padding: 36px; color: var(--text-3); font-size: 13px; }
/* Animations */
.st-val {
transition: opacity 0.3s;
}
.st-val.updating {
opacity: 0.4;
}
.cm, .pj, .iss {
animation: fadeSlideIn 0.35s ease-out both;
}
@keyframes fadeSlideIn {
0% { opacity: 0; transform: translateY(8px); }
100% { opacity: 1; transform: translateY(0); }
}
.cm:nth-child(1) { animation-delay: 0s; }
.cm:nth-child(2) { animation-delay: 0.03s; }
.cm:nth-child(3) { animation-delay: 0.06s; }
.cm:nth-child(4) { animation-delay: 0.09s; }
.cm:nth-child(5) { animation-delay: 0.12s; }
.cm:nth-child(6) { animation-delay: 0.15s; }
.cm:nth-child(7) { animation-delay: 0.18s; }
.cm:nth-child(8) { animation-delay: 0.21s; }
.cm:nth-child(9) { animation-delay: 0.24s; }
.cm:nth-child(10) { animation-delay: 0.27s; }
.pj:nth-child(1) { animation-delay: 0s; }
.pj:nth-child(2) { animation-delay: 0.05s; }
.pj:nth-child(3) { animation-delay: 0.1s; }
.pj:nth-child(4) { animation-delay: 0.15s; }
.pj:nth-child(5) { animation-delay: 0.2s; }
.pj:nth-child(6) { animation-delay: 0.25s; }
.iss:nth-child(1) { animation-delay: 0s; }
.iss:nth-child(2) { animation-delay: 0.04s; }
.iss:nth-child(3) { animation-delay: 0.08s; }
.iss:nth-child(4) { animation-delay: 0.12s; }
.iss:nth-child(5) { animation-delay: 0.16s; }
.iss:nth-child(6) { animation-delay: 0.2s; }
.cnt-anim {
display: inline-block;
transition: transform 0.3s ease;
}
.cnt-anim.bump {
transform: scale(1.3);
}
.flash-new {
animation: flashHighlight 1.5s ease-out;
}
@keyframes flashHighlight {
0% { background: var(--accent-light); }
100% { background: var(--card); }
}
.bar-fill {
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.sv-st, .ct-d {
transition: color 0.3s, background 0.3s;
}
@media (max-width: 1100px) { .cols { grid-template-columns: 1fr; } .stats { grid-template-columns: repeat(2,1fr); } } @media (max-width: 1100px) { .cols { grid-template-columns: 1fr; } .stats { grid-template-columns: repeat(2,1fr); } }
@media (max-width: 700px) { .nav { display: none; } .main { margin-left: 0; } .pj-grid { grid-template-columns: 1fr; } } @media (max-width: 700px) { .nav { display: none; } .main { margin-left: 0; } .pj-grid { grid-template-columns: 1fr; } }
</style> </style>
@@ -687,6 +759,26 @@ const MOCK = [
function ago(h) { return new Date(Date.now() - h*3600000).toISOString(); } function ago(h) { return new Date(Date.now() - h*3600000).toISOString(); }
let D = { repos: [] }; let D = { repos: [] };
let prevState = { commits: new Set(), stats: {} };
function animateValue(el, newVal) {
const old = el.textContent;
if (old === String(newVal)) return;
el.classList.add('updating');
setTimeout(() => {
el.textContent = newVal;
el.classList.remove('updating');
}, 150);
}
function bumpBadge(id) {
const el = document.getElementById(id);
if (!el) return;
el.classList.remove('bump');
void el.offsetWidth;
el.classList.add('bump');
setTimeout(() => el.classList.remove('bump'), 300);
}
// ===== PARTICLES ===== // ===== PARTICLES =====
(function(){ (function(){
@@ -790,11 +882,24 @@ async function loadData() {
} }
// ===== RENDER ===== // ===== RENDER =====
let isFirstRender = true;
function render() { function render() {
rStats(); rFeed(); rProjects(); rActivity(); rIssues(); rSvcs(); rStor(); rCerts(); rNavSvc(); rStats(); rFeed(); rProjects(); rActivity(); rIssues(); rSvcs(); rStor(); rCerts(); rNavSvc();
document.getElementById('nRC').textContent = D.repos.length;
const rc = D.repos.length;
const ti = D.repos.reduce((s,r)=>s+(r.issues?.length||0),0); const ti = D.repos.reduce((s,r)=>s+(r.issues?.length||0),0);
document.getElementById('nIC').textContent = ti||'0';
const nRC = document.getElementById('nRC');
const nIC = document.getElementById('nIC');
if (nRC.textContent !== String(rc)) { nRC.textContent = rc; bumpBadge('nRC'); }
if (nIC.textContent !== String(ti||'0')) { nIC.textContent = ti||'0'; bumpBadge('nIC'); }
// Track commits for next diff
const newSet = new Set();
D.repos.forEach(r => r.commits.forEach(c => newSet.add(c.sha)));
prevState.commits = newSet;
isFirstRender = false;
} }
function rStats() { function rStats() {
@@ -807,16 +912,35 @@ function rStats() {
if(Date.now()-t<86400000)ar++; if(Date.now()-t<86400000)ar++;
} }
}); });
// Animate existing stat values if they exist
const existing = document.querySelectorAll('.st-val[data-key]');
if (existing.length && !isFirstRender) {
const vals = { repos: D.repos.length, commits: tc, push: lp?tAgo(new Date(lp).toISOString()):'—', active: ar };
existing.forEach(el => {
const k = el.dataset.key;
if (vals[k] !== undefined && el.textContent !== String(vals[k])) {
animateValue(el, vals[k]);
}
});
// Update push label
const lbl = document.getElementById('pushLabel');
if (lbl) lbl.textContent = `Последний push · ${lpN}`;
return;
}
document.getElementById('stG').innerHTML = ` document.getElementById('stG').innerHTML = `
<div class="st"><div class="st-top"><div class="st-icon" style="background:var(--accent-light);color:var(--accent)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></div></div><div class="st-val">${D.repos.length}</div><div class="st-lbl">Репозиториев</div></div> <div class="st"><div class="st-top"><div class="st-icon" style="background:var(--accent-light);color:var(--accent)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></div></div><div class="st-val" data-key="repos">${D.repos.length}</div><div class="st-lbl">Репозиториев</div></div>
<div class="st"><div class="st-top"><div class="st-icon" style="background:var(--green-bg);color:var(--green)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div>${tc?'<span class="st-tag" style="background:var(--green-bg);color:var(--green)">сегодня</span>':''}</div><div class="st-val">${tc}</div><div class="st-lbl">Коммитов сегодня</div></div> <div class="st"><div class="st-top"><div class="st-icon" style="background:var(--green-bg);color:var(--green)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div>${tc?'<span class="st-tag" style="background:var(--green-bg);color:var(--green)">сегодня</span>':''}</div><div class="st-val" data-key="commits">${tc}</div><div class="st-lbl">Коммитов сегодня</div></div>
<div class="st"><div class="st-top"><div class="st-icon" style="background:var(--orange-bg);color:var(--orange)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div></div><div class="st-val">${lp?tAgo(new Date(lp).toISOString()):'—'}</div><div class="st-lbl">Последний push · ${lpN}</div></div> <div class="st"><div class="st-top"><div class="st-icon" style="background:var(--orange-bg);color:var(--orange)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div></div><div class="st-val" data-key="push">${lp?tAgo(new Date(lp).toISOString()):'—'}</div><div class="st-lbl" id="pushLabel">Последний push · ${lpN}</div></div>
<div class="st"><div class="st-top"><div class="st-icon" style="background:var(--blue-bg);color:var(--blue)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg></div></div><div class="st-val">${ar}</div><div class="st-lbl">Активных (24ч)</div></div>`; <div class="st"><div class="st-top"><div class="st-icon" style="background:var(--blue-bg);color:var(--blue)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg></div></div><div class="st-val" data-key="active">${ar}</div><div class="st-lbl">Активных (24ч)</div></div>`;
} }
function cmRow(c,rn,sr){ function cmRow(c,rn,sr){
const m=c.commit.message.split('\n')[0],t=cType(m); const m=c.commit.message.split('\n')[0],t=cType(m);
return `<div class="cm"><div class="cm-dot" style="background:${dotC(t)}"></div><div class="cm-body"><div class="cm-msg">${esc(m)}</div><div class="cm-meta">${sr?`<span class="cm-repo">${rn}</span>`:''} <span class="cm-sha">${c.sha.slice(0,7)}</span> <span>${c.commit.author.name}</span></div></div><div class="cm-t">${hm(c.commit.author.date)}</div></div>`; const isNew = !isFirstRender && !prevState.commits.has(c.sha);
const cls = isNew ? 'cm flash-new' : 'cm';
return `<div class="${cls}"><div class="cm-dot" style="background:${dotC(t)}"></div><div class="cm-body"><div class="cm-msg">${esc(m)}</div><div class="cm-meta">${sr?`<span class="cm-repo">${rn}</span>`:''} <span class="cm-sha">${c.sha.slice(0,7)}</span> <span>${c.commit.author.name}</span></div></div><div class="cm-t">${hm(c.commit.author.date)}</div></div>`;
} }
function rFeed(){ function rFeed(){