feat: live updates + animations
This commit is contained in:
+131
-7
@@ -3,7 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
<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">
|
||||
<style>
|
||||
:root {
|
||||
@@ -493,6 +496,75 @@
|
||||
|
||||
.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: 700px) { .nav { display: none; } .main { margin-left: 0; } .pj-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
@@ -687,6 +759,26 @@ const MOCK = [
|
||||
function ago(h) { return new Date(Date.now() - h*3600000).toISOString(); }
|
||||
|
||||
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 =====
|
||||
(function(){
|
||||
@@ -790,11 +882,24 @@ async function loadData() {
|
||||
}
|
||||
|
||||
// ===== RENDER =====
|
||||
let isFirstRender = true;
|
||||
|
||||
function render() {
|
||||
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);
|
||||
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() {
|
||||
@@ -807,16 +912,35 @@ function rStats() {
|
||||
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 = `
|
||||
<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(--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(--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(--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(--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" 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" 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" data-key="active">${ar}</div><div class="st-lbl">Активных (24ч)</div></div>`;
|
||||
}
|
||||
|
||||
function cmRow(c,rn,sr){
|
||||
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(){
|
||||
|
||||
Reference in New Issue
Block a user