From d1d20c4c86b9838110fbabca10ebbb3a4e871aa7 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sun, 17 May 2026 15:07:18 +0300 Subject: [PATCH] polish(admin-dash): avatar pills, skeleton loader, mobile breakpoints, palette kept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Avatar circles in top/worst-5 tables: initials from name, hsl color from hash of name - Structural skeleton on first load: 4 shimmer card boxes + 5 row placeholders (replaces LS.state.loading spinner for better layout-anchored feedback) - @media ≤640px: 2-column main grid, hero card reverts to normal size, quick-grid 2-col - Palette: existing per-card colors (violet/cyan/green/amber) already form a good muted hue family with vivid pink/amber for alert cards — kept as is to avoid regression Co-Authored-By: Claude Sonnet 4.6 --- frontend/js/admin/sections/overview.js | 63 +++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/frontend/js/admin/sections/overview.js b/frontend/js/admin/sections/overview.js index 499a3c7..cf2fdc8 100644 --- a/frontend/js/admin/sections/overview.js +++ b/frontend/js/admin/sections/overview.js @@ -79,8 +79,27 @@ .ov-quick-btn { display: flex; align-items: center; gap: 10px; padding: 14px 16px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; cursor: pointer; font-family: inherit; font-size: 0.88rem; font-weight: 600; color: var(--text); text-align: left; transition: background .12s, border-color .12s, transform .12s; } .ov-quick-btn:hover { background: rgba(155,93,229,0.06); border-color: rgba(155,93,229,0.3); color: var(--violet); transform: translateY(-1px); } .ov-quick-btn svg { width: 16px; height: 16px; flex-shrink: 0; } + /* ── avatar pills ───────────────────────────────────────────── */ + .ov-avatar { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; font-size: 10px; font-weight: 700; color: #fff; margin-right: 6px; vertical-align: middle; flex-shrink: 0; } + .ov-cell-user { display: flex; align-items: center; } + /* ── skeleton loader ────────────────────────────────────────── */ + @keyframes ov-shimmer { 0%{background-position:-400px 0} 100%{background-position:400px 0} } + .ov-skel-box { border-radius: 12px; background: linear-gradient(90deg,var(--border) 25%,var(--surface) 50%,var(--border) 75%); background-size: 400px 100%; animation: ov-shimmer 1.4s infinite linear; } + .ov-skel-cards { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 16px; margin-bottom: 28px; } + .ov-skel-card { height: 110px; } + .ov-skel-rows { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; } + .ov-skel-row { height: 38px; } /* ── misc ───────────────────────────────────────────────────── */ .ov-empty { padding: 18px; text-align: center; color: var(--text-3); font-size: 0.85rem; } + /* ── mobile breakpoints ─────────────────────────────────────── */ + @media (max-width: 640px) { + .ov-grid.ov-grid-main { grid-template-columns: 1fr 1fr; } + .ov-card.hero .ov-card-val { font-size: 1.9rem; } + .ov-card.hero .ov-card-icon { width: 38px; height: 38px; border-radius: 12px; } + .ov-results-grid { grid-template-columns: 1fr; } + .ov-quick-grid { grid-template-columns: 1fr 1fr; } + .ov-skel-cards { grid-template-columns: 1fr 1fr; } + } `; document.head.appendChild(s); } @@ -129,6 +148,45 @@ return '
' + segs + '
' + legend + '
'; } + /* ── avatar helpers ─────────────────────────────────────────────────── */ + function initialsOf(name) { + if (!name) return '?'; + return name.trim().split(/\s+/).slice(0, 2).map(function (w) { return w[0] ? w[0].toUpperCase() : ''; }).join('') || '?'; + } + + function hashHue(str) { + var h = 0; + for (var i = 0; i < (str || '').length; i++) { + h = ((h << 5) - h + (str || '').charCodeAt(i)) | 0; + } + return Math.abs(h) % 360; + } + + function avatarHtml(name) { + var hue = hashHue(name); + var bg = 'hsl(' + hue + ',55%,60%)'; + return '' + initialsOf(name) + ''; + } + + /* ── skeleton loader ────────────────────────────────────────────────── */ + function renderSkeleton(el) { + ensureOvStyles(); + el.innerHTML = + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + } + function pctClassNum(p) { if (p === null || p === undefined) return ''; return p >= 75 ? 'hi' : p >= 50 ? 'mid' : 'lo'; @@ -181,8 +239,9 @@ function renderSessionRows(sessions, e) { return sessions.map(function (s) { + var name = s.user_name || '—'; return '' + - '' + e(s.user_name || '—') + '' + + '' + avatarHtml(name) + e(name) + '' + '' + e(s.subject_name || '—') + '' + '' + (s.score != null ? s.score : 0) + ' / ' + (s.total != null ? s.total : 0) + '' + '' + (s.percent != null ? s.percent : '—') + '%' + @@ -368,7 +427,7 @@ async function load() { const el = document.getElementById('overview-content'); if (!el) return; - LS.state.loading(el, 'Загружаю обзор…'); + renderSkeleton(el); try { const data = await LS.adminGetOverview(); _lastLoadTs = Date.now();