feat(dashboard): hero-карточки главной — чтение, лаборатория дня, питомец

Пересборка верхней зоны дашборда по скриншоту (редизайн был утерян):
- 3 hero-карточки вместо action-cards: «Начать чтение» (продолжение
  курса через /api/courses/continue), «Лаборатория дня» (детерминир.
  выбор по дню + SVG-превью из lab-previews.js), «Питомец» (синхрон
  с модулем /pet через /api/pet + единый PetSprite.render).
- Подключены восстановленные ассеты pet-sprite.js и lab-previews.js.
- Убран weak-topics из hero; питомец показывает уровень/XP/стрик/
  цель дня/настроение, синхронно со страницей /pet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-31 11:11:20 +03:00
parent ca5dc3a4f3
commit 667054fa58
3 changed files with 504 additions and 42 deletions
+93 -42
View File
@@ -1440,23 +1440,64 @@
<div class="action-banner" id="action-banner" style="display:none">
<!-- populated by JS -->
</div>
<div class="action-cards" id="action-cards" style="display:none">
<a class="ac-card" id="ac-continue" style="display:none" href="#">
<span class="ac-emoji"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2V3zm20 0h-6a4 4 0 00-4 4v14a3 3 0 013-3h7V3z"/></svg></span>
<div class="ac-body">
<div class="ac-title" id="ac-cont-title"></div>
<div class="ac-sub" id="ac-cont-sub"></div>
<div class="hero-row" id="hero-row" style="display:none">
<!-- Card 1 — Continue / start reading -->
<a class="hero-card hc-read" id="hc-read" href="/textbooks">
<span class="hc-tag">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5a2 2 0 0 1 2-2h6v17H6a2 2 0 0 0-2 2z"/><path d="M20 5a2 2 0 0 0-2-2h-6v17h6a2 2 0 0 1 2 2z"/></svg>
<span id="hc-read-tag">Начать чтение</span>
</span>
<div class="hc-h" id="hc-read-title">Учебники</div>
<div class="hc-p" id="hc-read-sub">Открой учебник и продолжи курс с того места, где остановился.</div>
<div class="hc-progress" id="hc-read-prog-wrap" style="display:none"><i id="hc-read-prog" style="width:0%"></i></div>
<div class="hc-foot">
<span class="hc-meta" id="hc-read-meta">новый учебник</span>
<span class="hc-pct" id="hc-read-pct" style="display:none">0%</span>
<span class="hc-btn">Начать <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
</div>
<span class="ac-badge" id="ac-cont-pct"></span>
</a>
<a class="ac-card" id="ac-weak" style="display:none" href="#">
<span class="ac-emoji"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg></span>
<div class="ac-body">
<div class="ac-title" id="ac-weak-title"></div>
<div class="ac-sub" id="ac-weak-sub"></div>
<!-- Card 2 — Lab of the day -->
<a class="hero-card hc-lab" id="hc-lab" href="/lab">
<div class="hc-bg" id="hc-lab-bg"></div>
<span class="hc-tag">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6M10 3v6l-5.4 9.3A1.5 1.5 0 0 0 5.9 21h12.2a1.5 1.5 0 0 0 1.3-2.3L14 9V3"/><path d="M7.5 15h9"/></svg>
Лаборатория дня
</span>
<div class="hc-h" id="hc-lab-title">Газовые законы</div>
<div class="hc-p" id="hc-lab-sub">Давление, объём и температура газа.</div>
<div class="hc-chips" id="hc-lab-chips">
<span class="hc-chip subj" id="hc-lab-subj">Физика</span>
<span class="hc-chip" id="hc-lab-time">~10 мин</span>
<span class="hc-chip" id="hc-lab-level">средне</span>
</div>
<div class="hc-foot">
<span class="hc-meta" id="hc-lab-meta">Освой: уравнение состояния газа</span>
<span class="hc-btn">Открыть <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
</div>
<span class="ac-badge" id="ac-weak-pct" style="color:#E0335E"></span>
</a>
<!-- Card 3 — Pet (synced with /pet module) -->
<a class="hero-card hc-pet" id="hc-pet" href="/pet">
<span class="hc-tag">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="5.5" cy="11" r="2"/><circle cx="9.5" cy="6.5" r="2"/><circle cx="14.5" cy="6.5" r="2"/><circle cx="18.5" cy="11" r="2"/><path d="M8.2 16.4C8.2 14.3 9.9 13 12 13s3.8 1.3 3.8 3.4c0 1.7-1.3 2.8-2.6 3.2-.8.2-1.6.2-2.4 0-1.3-.4-2.6-1.5-2.6-3.2z"/></svg>
Питомец
</span>
<div class="hc-pet-top">
<div class="hc-pet-name" id="hc-pet-name">Квантик</div>
<div class="hc-pet-art" id="hc-pet-art"></div>
</div>
<div class="hc-xp-row"><span>Ур. <b id="hc-pet-lvl">1</b></span><span><b id="hc-pet-xp">0</b> / <span id="hc-pet-xpmax">500</span> XP</span></div>
<div class="hc-progress"><i id="hc-pet-prog" style="width:0%"></i></div>
<div class="hc-pet-chips">
<div class="hc-pchip"><b id="hc-pet-streak">0</b><span>стрик</span></div>
<div class="hc-pchip"><b id="hc-pet-goal">0/2</b><span>цель дня</span></div>
<div class="hc-pchip"><b id="hc-pet-mood">бодр</b><span>настроение</span></div>
</div>
<span class="hc-btn">Ухаживать <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
</a>
</div>
</div>
@@ -3177,41 +3218,49 @@
}
}
/* ══ WIDGET: Continue reading (action card) ══════════════════════ */
/* ══ HERO: Reading card (continue or start) ══════════════════════ */
async function loadContinueWidget() {
const card = document.getElementById('ac-continue');
const cardsWrap = document.getElementById('action-cards');
const card = document.getElementById('hc-read');
if (!card) return;
try {
const data = await LS.api('/api/courses/continue');
if (!data || !data.courseId) { card.style.display = 'none'; return; }
if (!data || !data.courseId) return; // оставляем дефолт «Начать чтение → Учебники»
const pct = data.lessonCount > 0 ? Math.round((data.doneCount || 0) / data.lessonCount * 100) : 0;
const href = `/lesson?course=${data.courseId}&lesson=${data.lessonId}`;
card.href = href;
card.style.display = '';
if (cardsWrap) cardsWrap.style.display = '';
document.getElementById('ac-cont-title').textContent = data.courseTitle || 'Продолжить';
document.getElementById('ac-cont-sub').textContent = `${data.lessonTitle || ''} · ${data.doneCount || 0}/${data.lessonCount || 0} уроков`;
document.getElementById('ac-cont-pct').textContent = pct + '%';
} catch { card.style.display = 'none'; }
card.href = `/lesson?course=${data.courseId}&lesson=${data.lessonId}`;
document.getElementById('hc-read-tag').textContent = 'Продолжить чтение';
document.getElementById('hc-read-title').textContent = data.courseTitle || 'Продолжить';
document.getElementById('hc-read-sub').textContent = data.lessonTitle || '';
document.getElementById('hc-read-meta').textContent = `${data.doneCount || 0} / ${data.lessonCount || 0} уроков`;
const progWrap = document.getElementById('hc-read-prog-wrap');
if (progWrap) { progWrap.style.display = ''; document.getElementById('hc-read-prog').style.width = pct + '%'; }
const pctEl = document.getElementById('hc-read-pct');
if (pctEl) { pctEl.style.display = ''; pctEl.textContent = pct + '%'; }
} catch { /* нет курса в процессе — карточка остаётся в дефолте */ }
}
/* ══ WIDGET: Weak topics (action card) ════════════════════════════ */
async function loadWeakWidget() {
const card = document.getElementById('ac-weak');
const cardsWrap = document.getElementById('action-cards');
/* ══ HERO: Lab of the day (deterministic daily pick) ═════════════ */
const LAB_OF_DAY = [
{ key:'isoprocess', href:'/lab?sim=molphys', title:'Газовые законы', sub:'Давление, объём и температура газа.', subj:'Физика', time:'~10 мин', level:'средне', goal:'уравнение состояния газа' },
{ key:'opticsbench', href:'/lab?sim=opticsbench', title:'Оптическая скамья', sub:'Собери систему линз и проследи ход лучей.', subj:'Физика', time:'~12 мин', level:'средне', goal:'построение изображения в линзе' },
{ key:'circuit', href:'/lab?sim=circuit', title:'Электрическая цепь', sub:'Закон Ома: ток, напряжение и сопротивление.', subj:'Физика', time:'~8 мин', level:'легко', goal:'расчёт цепи по закону Ома' },
{ key:'pendulum', href:'/lab?sim=pendulum', title:'Математический маятник', sub:'Период колебаний и зависимость от длины.', subj:'Физика', time:'~9 мин', level:'легко', goal:'формула периода маятника' },
{ key:'waves', href:'/lab?sim=waves', title:'Волны и колебания', sub:'Длина волны, частота и стоячие волны.', subj:'Физика', time:'~11 мин', level:'средне', goal:'связь v = λf' },
{ key:'stereo', href:'/lab?sim=stereo', title:'Стереометрия 3D', sub:'Сечения и объёмы пространственных фигур.', subj:'Геометрия', time:'~10 мин', level:'сложно', goal:'построение сечений' },
];
function loadLabOfDay() {
const card = document.getElementById('hc-lab');
if (!card) return;
try {
const topics = await LS.getWeakTopics();
if (!topics.length) { card.style.display = 'none'; return; }
const t = topics[0];
card.href = `/test-run?subject=${t.subject_slug}&mode=exam&count=15&topic=${t.topic_id}`;
card.style.display = '';
if (cardsWrap) cardsWrap.style.display = '';
document.getElementById('ac-weak-title').textContent = t.topic;
document.getElementById('ac-weak-sub').textContent = `${t.subject_name} · ${t.wrong} из ${t.total} неверно`;
document.getElementById('ac-weak-pct').textContent = t.error_pct + '%';
} catch { card.style.display = 'none'; }
const dayIdx = Math.floor(Date.now() / 86400000) % LAB_OF_DAY.length;
const lab = LAB_OF_DAY[dayIdx];
card.href = lab.href;
const bg = document.getElementById('hc-lab-bg');
if (bg && window.LabPreviews && LabPreviews[lab.key]) bg.innerHTML = LabPreviews[lab.key];
document.getElementById('hc-lab-title').textContent = lab.title;
document.getElementById('hc-lab-sub').textContent = lab.sub;
document.getElementById('hc-lab-subj').textContent = lab.subj;
document.getElementById('hc-lab-time').textContent = lab.time;
document.getElementById('hc-lab-level').textContent = lab.level;
document.getElementById('hc-lab-meta').textContent = 'Освой: ' + lab.goal;
}
/* ══ ACTIVITY: data structure ══════════════════════════════════════ */
@@ -3999,9 +4048,11 @@
loadSubjProgressWidget(rows);
renderStreakCalendar(rows);
} catch {}
const heroRow = document.getElementById('hero-row');
if (heroRow) heroRow.style.display = '';
loadContinueWidget();
loadWeakWidget();
loadTheoryWidget();
loadLabOfDay();
loadPetHero();
loadFlashcardWidget();
}