feat(trainer): «сцена-герой» редизайн + конструктор только для админов
- мощный визуал: уравнение на яркой градиентной «сцене» (бело на индиго→фиолет, текстура-клетка), рабочая зона снизу на белом; верный ответ заливает сцену изумрудом, неверный — красным (с pop/shake) - конструктор генераторов — ТОЛЬКО админ: страница /trainer-builder гейт ip.isAdmin; роуты POST/PUT/DELETE /generators → requireRole(admin); сайдбар-пункт hidden:!isAdm - выделен отдельным цветом: янтарная кнопка «Конструктор» в баре режима (только админ) → /trainer-builder - тема пользовательских генераторов: «Мои генераторы» для админа / «Авторские» для остальных (видят published) - тесты custom-generators 13/13 (админ создаёт; учитель/ученик 403); страница-смоук 33/33; эмодзи/eval 0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,12 +22,13 @@ router.post('/assign', requireRole('teacher', 'admin'), c.assignToClass);
|
|||||||
// Аналитика класса — только учитель/админ (владение проверяется в хендлере).
|
// Аналитика класса — только учитель/админ (владение проверяется в хендлере).
|
||||||
router.get('/class-stats', requireRole('teacher', 'admin'), c.classStats);
|
router.get('/class-stats', requireRole('teacher', 'admin'), c.classStats);
|
||||||
|
|
||||||
// Конструктор генераторов (P13): чтение — own+published; мутации — учитель/админ + ownership.
|
// Конструктор генераторов (P13): чтение — own+published (ученики видят published);
|
||||||
|
// СОЗДАНИЕ/правка — ТОЛЬКО админ (конструктор — админский инструмент).
|
||||||
const cg = require('../controllers/customGeneratorController');
|
const cg = require('../controllers/customGeneratorController');
|
||||||
router.get('/generators', cg.genList);
|
router.get('/generators', cg.genList);
|
||||||
router.post('/generators', requireRole('teacher', 'admin'), cg.genCreate);
|
router.post('/generators', requireRole('admin'), cg.genCreate);
|
||||||
router.get('/generators/:id', cg.genGet); // @public-by-design: own/published в хендлере
|
router.get('/generators/:id', cg.genGet); // @public-by-design: own/published в хендлере
|
||||||
router.put('/generators/:id', requireRole('teacher', 'admin'), cg.genUpdate);
|
router.put('/generators/:id', requireRole('admin'), cg.genUpdate);
|
||||||
router.delete('/generators/:id', requireRole('teacher', 'admin'), cg.genDelete);
|
router.delete('/generators/:id', requireRole('admin'), cg.genDelete);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -41,60 +41,66 @@ describe('validateGenSpec', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('/api/practice/generators CRUD', () => {
|
describe('/api/practice/generators CRUD (конструктор — только админ)', () => {
|
||||||
let teacher, other, student, gid;
|
let admin, other, teacher, student, gid;
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
admin = (await getToken('admin')).token;
|
||||||
|
other = (await getToken('admin')).token;
|
||||||
teacher = (await getToken('teacher')).token;
|
teacher = (await getToken('teacher')).token;
|
||||||
other = (await getToken('teacher')).token;
|
|
||||||
student = (await getToken('student')).token;
|
student = (await getToken('student')).token;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('учитель создаёт генератор', async () => {
|
it('админ создаёт генератор', async () => {
|
||||||
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, teacher);
|
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, admin);
|
||||||
assert.equal(res.status, 200, `got ${res.status}`);
|
assert.equal(res.status, 200, `got ${res.status}`);
|
||||||
assert.equal(res.body.ok, true);
|
assert.equal(res.body.ok, true);
|
||||||
assert.ok(/^cg\d+$/.test(res.body.generator.id), 'id вида cg<dbid>');
|
assert.ok(/^cg\d+$/.test(res.body.generator.id), 'id вида cg<dbid>');
|
||||||
gid = res.body.generator.dbid;
|
gid = res.body.generator.dbid;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('учителю создавать запрещено (403)', async () => {
|
||||||
|
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, teacher);
|
||||||
|
assert.equal(res.status, 403, `got ${res.status}`);
|
||||||
|
});
|
||||||
|
|
||||||
it('ученику создавать запрещено (403)', async () => {
|
it('ученику создавать запрещено (403)', async () => {
|
||||||
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, student);
|
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, student);
|
||||||
assert.equal(res.status, 403, `got ${res.status}`);
|
assert.equal(res.status, 403, `got ${res.status}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('невалидный спек → 400', async () => {
|
it('невалидный спек → 400', async () => {
|
||||||
const res = await inject('POST', '/api/practice/generators', { spec: { title: '' } }, teacher);
|
const res = await inject('POST', '/api/practice/generators', { spec: { title: '' } }, admin);
|
||||||
assert.equal(res.status, 400, `got ${res.status}`);
|
assert.equal(res.status, 400, `got ${res.status}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('автор видит свой генератор в списке', async () => {
|
it('автор видит свой генератор в списке', async () => {
|
||||||
const res = await inject('GET', '/api/practice/generators', null, teacher);
|
const res = await inject('GET', '/api/practice/generators', null, admin);
|
||||||
assert.equal(res.status, 200);
|
assert.equal(res.status, 200);
|
||||||
assert.ok(res.body.generators.some(g => g.dbid === gid), 'свой генератор в списке');
|
assert.ok(res.body.generators.some(g => g.dbid === gid), 'свой генератор в списке');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('чужой draft не виден другому учителю', async () => {
|
it('чужой draft не виден другому админу', async () => {
|
||||||
const res = await inject('GET', '/api/practice/generators', null, other);
|
const res = await inject('GET', '/api/practice/generators', null, other);
|
||||||
assert.ok(!res.body.generators.some(g => g.dbid === gid), 'чужой draft скрыт');
|
assert.ok(!res.body.generators.some(g => g.dbid === gid), 'чужой draft скрыт');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('чужой не может изменить (403)', async () => {
|
it('учителю изменять запрещено (403, роль)', async () => {
|
||||||
const res = await inject('PUT', '/api/practice/generators/' + gid, { spec: SPEC }, other);
|
const res = await inject('PUT', '/api/practice/generators/' + gid, { spec: SPEC }, teacher);
|
||||||
assert.equal(res.status, 403, `got ${res.status}`);
|
assert.equal(res.status, 403, `got ${res.status}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('публикация делает генератор видимым другим', async () => {
|
it('публикация делает генератор видимым другим (и ученику)', async () => {
|
||||||
const pub = await inject('PUT', '/api/practice/generators/' + gid, { status: 'published' }, teacher);
|
const pub = await inject('PUT', '/api/practice/generators/' + gid, { status: 'published' }, admin);
|
||||||
assert.equal(pub.status, 200);
|
assert.equal(pub.status, 200);
|
||||||
assert.equal(pub.body.generator.status, 'published');
|
assert.equal(pub.body.generator.status, 'published');
|
||||||
const res = await inject('GET', '/api/practice/generators', null, other);
|
const res = await inject('GET', '/api/practice/generators', null, student);
|
||||||
assert.ok(res.body.generators.some(g => g.dbid === gid), 'published виден другому');
|
assert.ok(res.body.generators.some(g => g.dbid === gid), 'published виден ученику');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('автор удаляет свой генератор', async () => {
|
it('автор удаляет свой генератор', async () => {
|
||||||
const res = await inject('DELETE', '/api/practice/generators/' + gid, null, teacher);
|
const res = await inject('DELETE', '/api/practice/generators/' + gid, null, admin);
|
||||||
assert.equal(res.status, 200);
|
assert.equal(res.status, 200);
|
||||||
const after = await inject('GET', '/api/practice/generators/' + gid, null, teacher);
|
const after = await inject('GET', '/api/practice/generators/' + gid, null, admin);
|
||||||
assert.equal(after.status, 404, 'после удаления 404');
|
assert.equal(after.status, 404, 'после удаления 404');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -190,7 +190,7 @@
|
|||||||
if (typeof LS === 'undefined') return;
|
if (typeof LS === 'undefined') return;
|
||||||
var ip = LS.initPage();
|
var ip = LS.initPage();
|
||||||
if (!ip) return;
|
if (!ip) return;
|
||||||
if (!(ip.isTeacher || ip.isAdmin)) { location.href = '/dashboard'; return; }
|
if (!ip.isAdmin) { location.href = '/dashboard'; return; } // конструктор — только админам
|
||||||
|
|
||||||
var TE = window.TrainerEngine;
|
var TE = window.TrainerEngine;
|
||||||
var $ = function (id) { return document.getElementById(id); };
|
var $ = function (id) { return document.getElementById(id); };
|
||||||
|
|||||||
+47
-17
@@ -68,25 +68,41 @@
|
|||||||
/* ── карточка задачи (герой) ── */
|
/* ── карточка задачи (герой) ── */
|
||||||
.tr-card {
|
.tr-card {
|
||||||
position: relative; overflow: hidden; background: var(--card);
|
position: relative; overflow: hidden; background: var(--card);
|
||||||
border: 1px solid rgba(99,102,241,.10); border-radius: var(--r-lg);
|
border: 1px solid rgba(99,102,241,.12); border-radius: var(--r-lg);
|
||||||
padding: 34px 30px 30px; box-shadow: var(--sh); transition: box-shadow .3s var(--ease), transform .3s var(--ease);
|
box-shadow: var(--sh); transition: box-shadow .3s var(--ease), transform .3s var(--ease);
|
||||||
}
|
}
|
||||||
.tr-card::before { content: ''; position: absolute; left: 0; right: 0; top: 0; height: 5px; background: linear-gradient(90deg, var(--g1), var(--g2)); }
|
.tr-card.tr-correct { box-shadow: 0 22px 56px rgba(16,185,129,.30); animation: trPop .5s var(--ease); }
|
||||||
.tr-card.tr-correct { box-shadow: 0 20px 54px rgba(16,185,129,.24); animation: trPop .5s var(--ease); }
|
|
||||||
.tr-card.tr-correct::before { height: 6px; background: linear-gradient(90deg, var(--ok), #34d399); }
|
|
||||||
.tr-card.tr-wrong { animation: trShake .42s var(--ease); }
|
.tr-card.tr-wrong { animation: trShake .42s var(--ease); }
|
||||||
.tr-card.tr-wrong::before { background: linear-gradient(90deg, var(--bad), #fb7185); }
|
|
||||||
@keyframes trPop { 0% { transform: scale(1); } 32% { transform: scale(1.014); } 100% { transform: scale(1); } }
|
@keyframes trPop { 0% { transform: scale(1); } 32% { transform: scale(1.014); } 100% { transform: scale(1); } }
|
||||||
@keyframes trShake { 0%,100% { transform: translateX(0); } 18% { transform: translateX(-7px); } 38% { transform: translateX(6px); } 58% { transform: translateX(-4px); } 78% { transform: translateX(2px); } }
|
@keyframes trShake { 0%,100% { transform: translateX(0); } 18% { transform: translateX(-7px); } 38% { transform: translateX(6px); } 58% { transform: translateX(-4px); } 78% { transform: translateX(2px); } }
|
||||||
|
|
||||||
|
/* ── «сцена»-герой: уравнение крупно на ярком градиенте ── */
|
||||||
|
.tr-stage {
|
||||||
|
position: relative; overflow: hidden; text-align: center; color: #fff;
|
||||||
|
padding: 32px 28px 36px; transition: background .35s var(--ease);
|
||||||
|
background: linear-gradient(135deg, #4f46e5 0%, #6d3aed 52%, #8b5cf6 100%);
|
||||||
|
}
|
||||||
|
.tr-stage::before {
|
||||||
|
content: ''; position: absolute; inset: 0; pointer-events: none; opacity: .55;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(440px 220px at 80% -25%, rgba(255,255,255,.20), transparent 60%),
|
||||||
|
linear-gradient(rgba(255,255,255,.07) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,.07) 1px, transparent 1px);
|
||||||
|
background-size: 100% 100%, 22px 22px, 22px 22px;
|
||||||
|
}
|
||||||
|
.tr-stage > * { position: relative; }
|
||||||
|
.tr-card.tr-correct .tr-stage { background: linear-gradient(135deg, #059669, #10b981 58%, #34d399); }
|
||||||
|
.tr-card.tr-wrong .tr-stage { background: linear-gradient(135deg, #dc2626, #ef4444 58%, #fb7185); }
|
||||||
|
.tr-work { padding: 24px 26px 28px; }
|
||||||
|
|
||||||
#tr-skill {
|
#tr-skill {
|
||||||
color: var(--accent-ink); font-family: 'Manrope', sans-serif; font-size: .74rem; font-weight: 800;
|
color: rgba(255,255,255,.75); font-family: 'Manrope', sans-serif; font-size: .72rem; font-weight: 800;
|
||||||
text-transform: uppercase; letter-spacing: .07em; margin-bottom: 16px;
|
text-transform: uppercase; letter-spacing: .1em; margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
.tr-eq {
|
.tr-eq {
|
||||||
font-family: 'Cambria Math', 'Times New Roman', Georgia, serif;
|
font-family: 'Cambria Math', 'Times New Roman', Georgia, serif;
|
||||||
font-size: clamp(1.8rem, 5.2vw, 2.5rem); font-weight: 600; letter-spacing: .01em;
|
font-size: clamp(1.9rem, 5.6vw, 2.7rem); font-weight: 600; letter-spacing: .01em;
|
||||||
color: var(--ink); text-align: center; padding: 8px 0 26px; user-select: none;
|
color: #fff; text-align: center; padding: 2px 0; user-select: none; text-shadow: 0 2px 14px rgba(0,0,0,.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tr-inrow { display: flex; gap: 10px; align-items: stretch; max-width: 440px; margin: 0 auto; }
|
.tr-inrow { display: flex; gap: 10px; align-items: stretch; max-width: 440px; margin: 0 auto; }
|
||||||
@@ -152,9 +168,9 @@
|
|||||||
.tr-step-math { display: block; font-family: 'Cambria Math', serif; font-size: 1.18rem; color: var(--ink); margin-left: 30px; }
|
.tr-step-math { display: block; font-family: 'Cambria Math', serif; font-size: 1.18rem; color: var(--ink); margin-left: 30px; }
|
||||||
.tr-step-n { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(135deg, var(--g1), var(--g2)); color: #fff; font-family: 'Manrope', sans-serif; font-size: .72rem; font-weight: 800; margin-right: 9px; vertical-align: 1px; }
|
.tr-step-n { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(135deg, var(--g1), var(--g2)); color: #fff; font-family: 'Manrope', sans-serif; font-size: .72rem; font-weight: 800; margin-right: 9px; vertical-align: 1px; }
|
||||||
.tr-eq .katex-display { margin: 0; }
|
.tr-eq .katex-display { margin: 0; }
|
||||||
.tr-eq .katex { font-size: 1.12em; }
|
.tr-eq .katex { font-size: 1.12em; color: #fff; }
|
||||||
/* текстовый prompt (проценты/упрощение) — компактнее уравнения */
|
/* текстовый prompt (проценты/упрощение) — компактнее уравнения, на сцене белым */
|
||||||
.tr-eq.tr-eq-text { font-family: 'Manrope', sans-serif; font-weight: 600; font-size: clamp(1.15rem, 3.4vw, 1.6rem); line-height: 1.45; color: var(--ink); }
|
.tr-eq.tr-eq-text { font-family: 'Manrope', sans-serif; font-weight: 600; font-size: clamp(1.15rem, 3.4vw, 1.6rem); line-height: 1.45; color: #fff; }
|
||||||
|
|
||||||
/* выбор навыка внутри темы */
|
/* выбор навыка внутри темы */
|
||||||
.tr-skills { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 24px; }
|
.tr-skills { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 24px; }
|
||||||
@@ -222,6 +238,9 @@
|
|||||||
.tr-mode-btn.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 8px 20px rgba(99,102,241,.32); }
|
.tr-mode-btn.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 8px 20px rgba(99,102,241,.32); }
|
||||||
.tr-session { font-size: .82rem; font-weight: 800; color: var(--accent-ink); padding: 4px 12px; border-radius: 99px; background: rgba(99,102,241,.1); }
|
.tr-session { font-size: .82rem; font-weight: 800; color: var(--accent-ink); padding: 4px 12px; border-radius: 99px; background: rgba(99,102,241,.1); }
|
||||||
.tr-session:empty { display: none; }
|
.tr-session:empty { display: none; }
|
||||||
|
/* конструктор — отдельный (янтарный) цвет, только админ */
|
||||||
|
.tr-admin-btn { color: #fff; border-color: transparent; background: linear-gradient(135deg, #f59e0b, #f97316); box-shadow: 0 8px 20px rgba(245,158,11,.32); }
|
||||||
|
.tr-admin-btn:hover { color: #fff; border-color: transparent; background: linear-gradient(135deg, #f59e0b, #f97316); transform: translateY(-1px); }
|
||||||
|
|
||||||
/* ── итог сессии ── */
|
/* ── итог сессии ── */
|
||||||
.tr-summary {
|
.tr-summary {
|
||||||
@@ -278,6 +297,10 @@
|
|||||||
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><rect x="7" y="10" width="3" height="7"/><rect x="13" y="6" width="3" height="11"/></svg>
|
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><rect x="7" y="10" width="3" height="7"/><rect x="13" y="6" width="3" height="11"/></svg>
|
||||||
Аналитика класса
|
Аналитика класса
|
||||||
</button>
|
</button>
|
||||||
|
<button class="tr-mode-btn tr-admin-btn" id="tr-builder-btn" type="button" style="display:none">
|
||||||
|
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8 19 13M15 9h.01M17.8 6.2 19 5M3 21l9-9M12.2 6.2 11 5"/></svg>
|
||||||
|
Конструктор
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tr-modal" id="tr-analytics" style="display:none">
|
<div class="tr-modal" id="tr-analytics" style="display:none">
|
||||||
@@ -308,8 +331,11 @@
|
|||||||
<div class="tr-skills" id="tr-skills"></div>
|
<div class="tr-skills" id="tr-skills"></div>
|
||||||
|
|
||||||
<div class="tr-card">
|
<div class="tr-card">
|
||||||
<div class="tr-skill" id="tr-skill"></div>
|
<div class="tr-stage">
|
||||||
<div class="tr-eq" id="tr-eq">—</div>
|
<div class="tr-skill" id="tr-skill"></div>
|
||||||
|
<div class="tr-eq" id="tr-eq">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="tr-work">
|
||||||
|
|
||||||
<div id="tr-answerbox">
|
<div id="tr-answerbox">
|
||||||
<div class="tr-inrow">
|
<div class="tr-inrow">
|
||||||
@@ -355,6 +381,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tr-solution" id="tr-solution" style="display:none"></div>
|
<div class="tr-solution" id="tr-solution" style="display:none"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tr-summary" id="tr-summary" style="display:none"></div>
|
<div class="tr-summary" id="tr-summary" style="display:none"></div>
|
||||||
@@ -428,7 +455,8 @@
|
|||||||
|
|
||||||
var topics = (TG.topics ? TG.topics() : [{ key: null, label: 'Задачи' }]).concat([{ key: 'word', label: 'Текстовые задачи', word: true }]);
|
var topics = (TG.topics ? TG.topics() : [{ key: null, label: 'Задачи' }]).concat([{ key: 'word', label: 'Текстовые задачи', word: true }]);
|
||||||
var isTeacher = !!(ip && ip.isTeacher);
|
var isTeacher = !!(ip && ip.isTeacher);
|
||||||
var customGens = []; // пользовательские генераторы (P13), тема «Мои генераторы»
|
var isAdmin = !!(ip && ip.isAdmin);
|
||||||
|
var customGens = []; // пользовательские генераторы (P13), тема «Авторские»
|
||||||
function skillKey(g) { return g.skill || g.id; }
|
function skillKey(g) { return g.skill || g.id; }
|
||||||
function skillsOf(topicKey) {
|
function skillsOf(topicKey) {
|
||||||
if (topicKey === 'custom') return customGens;
|
if (topicKey === 'custom') return customGens;
|
||||||
@@ -931,6 +959,7 @@
|
|||||||
$('tr-tch-close').addEventListener('click', function () { $('tr-teacher').style.display = 'none'; });
|
$('tr-tch-close').addEventListener('click', function () { $('tr-teacher').style.display = 'none'; });
|
||||||
$('tr-teacher').addEventListener('click', function (e) { if (e.target === $('tr-teacher')) $('tr-teacher').style.display = 'none'; });
|
$('tr-teacher').addEventListener('click', function (e) { if (e.target === $('tr-teacher')) $('tr-teacher').style.display = 'none'; });
|
||||||
$('tr-analytics-btn').addEventListener('click', openAnalytics);
|
$('tr-analytics-btn').addEventListener('click', openAnalytics);
|
||||||
|
$('tr-builder-btn').addEventListener('click', function () { location.href = '/trainer-builder'; });
|
||||||
$('tr-an-close').addEventListener('click', function () { $('tr-analytics').style.display = 'none'; });
|
$('tr-an-close').addEventListener('click', function () { $('tr-analytics').style.display = 'none'; });
|
||||||
$('tr-analytics').addEventListener('click', function (e) { if (e.target === $('tr-analytics')) $('tr-analytics').style.display = 'none'; });
|
$('tr-analytics').addEventListener('click', function (e) { if (e.target === $('tr-analytics')) $('tr-analytics').style.display = 'none'; });
|
||||||
$('tr-an-body').addEventListener('click', function (e) {
|
$('tr-an-body').addEventListener('click', function (e) {
|
||||||
@@ -1001,6 +1030,7 @@
|
|||||||
if (smart) pickNext(null); // адаптивный первый навык (last=null — можно взять текущий)
|
if (smart) pickNext(null); // адаптивный первый навык (last=null — можно взять текущий)
|
||||||
renderTopics(); renderSkills(); updateSession(); updateOverall(); newProblem();
|
renderTopics(); renderSkills(); updateSession(); updateOverall(); newProblem();
|
||||||
if (isTeacher) $('tr-analytics-btn').style.display = '';
|
if (isTeacher) $('tr-analytics-btn').style.display = '';
|
||||||
|
if (isAdmin) $('tr-builder-btn').style.display = '';
|
||||||
}
|
}
|
||||||
Promise.all([
|
Promise.all([
|
||||||
LS.practiceProgressList ? LS.practiceProgressList().catch(function () { return null; }) : Promise.resolve(null),
|
LS.practiceProgressList ? LS.practiceProgressList().catch(function () { return null; }) : Promise.resolve(null),
|
||||||
@@ -1010,7 +1040,7 @@
|
|||||||
if (pr && pr.progress) pr.progress.forEach(function (row) { prog[row.skill] = row; });
|
if (pr && pr.progress) pr.progress.forEach(function (row) { prog[row.skill] = row; });
|
||||||
if (cgr && cgr.generators && cgr.generators.length) {
|
if (cgr && cgr.generators && cgr.generators.length) {
|
||||||
customGens = cgr.generators;
|
customGens = cgr.generators;
|
||||||
topics.push({ key: 'custom', label: 'Мои генераторы', custom: true });
|
topics.push({ key: 'custom', label: isAdmin ? 'Мои генераторы' : 'Авторские', custom: true });
|
||||||
}
|
}
|
||||||
boot();
|
boot();
|
||||||
}).catch(boot);
|
}).catch(boot);
|
||||||
|
|||||||
+1
-1
@@ -90,7 +90,7 @@
|
|||||||
${G('practice', 'Практика и игры', `
|
${G('practice', 'Практика и игры', `
|
||||||
${L('/lab', 'atom', 'Лаборатория')}
|
${L('/lab', 'atom', 'Лаборатория')}
|
||||||
${L('/trainer', 'dumbbell', 'Тренажёр')}
|
${L('/trainer', 'dumbbell', 'Тренажёр')}
|
||||||
${L('/trainer-builder', 'wand-2', 'Конструктор задач', { cls: 'sb-teacher-only', hidden: !isTch })}
|
${L('/trainer-builder', 'wand-2', 'Конструктор задач', { hidden: !isAdm })}
|
||||||
${L('/quantik', 'rocket', 'Квантик: Законы Мира')}
|
${L('/quantik', 'rocket', 'Квантик: Законы Мира')}
|
||||||
${L('/sim-builder', 'pencil-ruler', 'Конструктор симуляций', { cls: 'sb-teacher-only', hidden: !isTch })}
|
${L('/sim-builder', 'pencil-ruler', 'Конструктор симуляций', { cls: 'sb-teacher-only', hidden: !isTch })}
|
||||||
${L('/biochem', 'flask-conical', 'Биохимия')}
|
${L('/biochem', 'flask-conical', 'Биохимия')}
|
||||||
|
|||||||
Reference in New Issue
Block a user