feat(algebra-8): синхронизация прогресса учебника с каталогом
Раньше: алгебра 1 и 2 главы хранили прогресс только в localStorage, поэтому каталог /textbooks показывал 0/N прочитано и кнопку 'Открыть' даже после активной работы с учебником. Теперь обе главы шлют POST /api/textbooks/:slug/progress: - markLastPara(id) — при каждом goTo(); сервер запоминает last_para, каталог показывает кнопку 'Продолжить'. - markParaRead(id) — когда STATE.progress[key] первый раз ≥ 50% (внутрипараграфный прогресс достаточен); сервер добавляет id в paragraphs_read[], каталог показывает '1/7 прочитано'. - Дебаунс 600мс — несколько быстрых переходов схлопываются в один POST. - keepalive:true + beforeunload-flush, чтобы последний переход не потерялся при закрытии вкладки. - loadServerReadState() при init() — если на другом устройстве уже прочитаны параграфы, локальный STATE.progress поднимается до 100% для них (визуально совпадает с каталогом). Slug: 'algebra-8' для ch1, 'algebra-8-ch2' для ch2.
This commit is contained in:
@@ -1069,6 +1069,52 @@ function bumpProgress(key, delta){
|
||||
STATE.progress[key] = v;
|
||||
saveProgress();
|
||||
refreshProgressUI();
|
||||
if(v >= 50) markParaRead(key);
|
||||
}
|
||||
|
||||
/* Server sync of read/last_para — пишем в БД, чтобы каталог /textbooks показывал прогресс */
|
||||
const _TB_SLUG = 'algebra-8';
|
||||
const _markedRead = new Set();
|
||||
let _pendingProgressBody = null, _progressTimer = null;
|
||||
function _flushProgress(){
|
||||
const body = _pendingProgressBody;
|
||||
_pendingProgressBody = null;
|
||||
if(!body) return;
|
||||
const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
|
||||
if(!tok) return;
|
||||
fetch('/api/textbooks/' + _TB_SLUG + '/progress', {
|
||||
method:'POST',
|
||||
headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + tok },
|
||||
body: JSON.stringify(body),
|
||||
keepalive: true,
|
||||
}).catch(()=>{});
|
||||
}
|
||||
function _queueProgress(patch){
|
||||
_pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch);
|
||||
if(_progressTimer) clearTimeout(_progressTimer);
|
||||
_progressTimer = setTimeout(_flushProgress, 600);
|
||||
}
|
||||
function markLastPara(id){ _queueProgress({ last_para: id }); }
|
||||
function markParaRead(id){
|
||||
if(_markedRead.has(id)) return;
|
||||
_markedRead.add(id);
|
||||
_queueProgress({ mark_read: id });
|
||||
}
|
||||
window.addEventListener('beforeunload', _flushProgress);
|
||||
function loadServerReadState(){
|
||||
const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
|
||||
if(!tok) return;
|
||||
fetch('/api/textbooks/' + _TB_SLUG, { headers:{ 'Authorization':'Bearer ' + tok } })
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => {
|
||||
if(!d || !d.progress) return;
|
||||
(d.progress.read || []).forEach(k => {
|
||||
_markedRead.add(k);
|
||||
if((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100;
|
||||
});
|
||||
saveProgress(); refreshProgressUI();
|
||||
})
|
||||
.catch(()=>{});
|
||||
}
|
||||
function refreshProgressUI(){
|
||||
const total = Object.values(STATE.progress).reduce((a,b)=>a+b,0) / 7;
|
||||
@@ -1204,6 +1250,7 @@ function _goToFinish(id){
|
||||
if(window.renderMathInElement){
|
||||
setTimeout(()=>renderMathInElement(el, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}),0);
|
||||
}
|
||||
markLastPara(id);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════
|
||||
@@ -1460,6 +1507,7 @@ function init(){
|
||||
buildParaSelector();
|
||||
refreshProgressUI();
|
||||
initMobileSidebar();
|
||||
loadServerReadState();
|
||||
goTo('p1'); // строит только §1, остальные — лениво при переходе
|
||||
setTimeout(()=>achievement('start','Начало пути по корням!'), 800);
|
||||
// Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс
|
||||
|
||||
@@ -541,6 +541,52 @@ function bumpProgress(key, delta){
|
||||
STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key]||0) + delta));
|
||||
saveProgress();
|
||||
refreshProgressUI();
|
||||
if(STATE.progress[key] >= 50) markParaRead(key);
|
||||
}
|
||||
|
||||
/* Server sync of read/last_para — пишем в БД, чтобы каталог /textbooks показывал прогресс */
|
||||
const _TB_SLUG = 'algebra-8-ch2';
|
||||
const _markedRead = new Set();
|
||||
let _pendingProgressBody = null, _progressTimer = null;
|
||||
function _flushProgress(){
|
||||
const body = _pendingProgressBody;
|
||||
_pendingProgressBody = null;
|
||||
if(!body) return;
|
||||
const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
|
||||
if(!tok) return;
|
||||
fetch('/api/textbooks/' + _TB_SLUG + '/progress', {
|
||||
method:'POST',
|
||||
headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + tok },
|
||||
body: JSON.stringify(body),
|
||||
keepalive: true,
|
||||
}).catch(()=>{});
|
||||
}
|
||||
function _queueProgress(patch){
|
||||
_pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch);
|
||||
if(_progressTimer) clearTimeout(_progressTimer);
|
||||
_progressTimer = setTimeout(_flushProgress, 600);
|
||||
}
|
||||
function markLastPara(id){ _queueProgress({ last_para: id }); }
|
||||
function markParaRead(id){
|
||||
if(_markedRead.has(id)) return;
|
||||
_markedRead.add(id);
|
||||
_queueProgress({ mark_read: id });
|
||||
}
|
||||
window.addEventListener('beforeunload', _flushProgress);
|
||||
function loadServerReadState(){
|
||||
const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
|
||||
if(!tok) return;
|
||||
fetch('/api/textbooks/' + _TB_SLUG, { headers:{ 'Authorization':'Bearer ' + tok } })
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => {
|
||||
if(!d || !d.progress) return;
|
||||
(d.progress.read || []).forEach(k => {
|
||||
_markedRead.add(k);
|
||||
if((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100;
|
||||
});
|
||||
saveProgress(); refreshProgressUI();
|
||||
})
|
||||
.catch(()=>{});
|
||||
}
|
||||
function addXp(n, src){
|
||||
if(!n) return;
|
||||
@@ -646,6 +692,7 @@ function goTo(id){
|
||||
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
|
||||
// glossary wrap — пройти по тексту секции и обернуть термины
|
||||
setTimeout(()=>{ try { wrapGlossary(el); } catch(e){} }, 60);
|
||||
markLastPara(id);
|
||||
}
|
||||
|
||||
/* SIDEBAR */
|
||||
@@ -1206,6 +1253,7 @@ function init(){
|
||||
initSearch();
|
||||
buildParaSelector();
|
||||
refreshProgressUI();
|
||||
loadServerReadState();
|
||||
goTo('p7');
|
||||
setTimeout(()=>achievement('start','Начало главы 2!'), 600);
|
||||
// Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс
|
||||
|
||||
Reference in New Issue
Block a user