v2: reminders + monthly tests per block
This commit is contained in:
parent
eb682e5d2f
commit
80f704a616
232
app.js
232
app.js
@ -22,7 +22,11 @@ function dateStr(d){ return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(
|
|||||||
function load(){
|
function load(){
|
||||||
try{ state = JSON.parse(localStorage.getItem("enTrainer")||"null"); }catch(e){ state=null; }
|
try{ state = JSON.parse(localStorage.getItem("enTrainer")||"null"); }catch(e){ state=null; }
|
||||||
var today = todayStr();
|
var today = todayStr();
|
||||||
if(!state){ state={startDate:today, streak:1, lastVisit:today, completed:{}, wordsLearned:[]}; }
|
if(!state){ state={startDate:today, streak:1, lastVisit:today, completed:{}, wordsLearned:[], reminderOn:false, reminderTime:"20:00", lastNotified:null, tests:{}}; }
|
||||||
|
if(state.reminderOn===undefined) state.reminderOn=false;
|
||||||
|
if(!state.reminderTime) state.reminderTime="20:00";
|
||||||
|
if(state.lastNotified===undefined) state.lastNotified=null;
|
||||||
|
if(!state.tests) state.tests={};
|
||||||
// streak
|
// streak
|
||||||
if(state.lastVisit !== today){
|
if(state.lastVisit !== today){
|
||||||
var y=new Date(); y.setDate(y.getDate()-1);
|
var y=new Date(); y.setDate(y.getDate()-1);
|
||||||
@ -55,6 +59,12 @@ function computeDay(){
|
|||||||
}
|
}
|
||||||
function day(){ return CUR[curIdx]; }
|
function day(){ return CUR[curIdx]; }
|
||||||
|
|
||||||
|
/* ---------- misc helpers ---------- */
|
||||||
|
function shuffle(a){ a=a.slice(); for(var i=a.length-1;i>0;i--){ var j=Math.floor(Math.random()*(i+1)); var t=a[i]; a[i]=a[j]; a[j]=t; } return a; }
|
||||||
|
function flatten(a){ var r=[]; a.forEach(function(x){ x.forEach(function(y){ r.push(y); }); }); return r; }
|
||||||
|
function currentMonth(){ return Math.min(5, Math.floor(dayKey/30)); }
|
||||||
|
function monthLabel(m){ return "Месяц "+(m+1)+" из 6"; }
|
||||||
|
|
||||||
/* ---------- speech ---------- */
|
/* ---------- speech ---------- */
|
||||||
var voices=[];
|
var voices=[];
|
||||||
var synth = window.speechSynthesis;
|
var synth = window.speechSynthesis;
|
||||||
@ -123,6 +133,7 @@ function toast(msg){
|
|||||||
function renderTop(){
|
function renderTop(){
|
||||||
$("streak").innerHTML="🔥 "+state.streak;
|
$("streak").innerHTML="🔥 "+state.streak;
|
||||||
$("level").textContent="Intermediate";
|
$("level").textContent="Intermediate";
|
||||||
|
$("bellBtn").classList.toggle("on", !!state.reminderOn);
|
||||||
}
|
}
|
||||||
function renderDashboard(){
|
function renderDashboard(){
|
||||||
var elapsed = dayKey+1;
|
var elapsed = dayKey+1;
|
||||||
@ -152,6 +163,7 @@ function renderTabs(){
|
|||||||
SECTIONS.forEach(function(s){
|
SECTIONS.forEach(function(s){
|
||||||
h+='<button class="tab '+(ACTIVE===s.id?"active":"")+'" data-sec="'+s.id+'"><span class="dot"></span>'+s.icon+" "+s.label+'</button>';
|
h+='<button class="tab '+(ACTIVE===s.id?"active":"")+'" data-sec="'+s.id+'"><span class="dot"></span>'+s.icon+" "+s.label+'</button>';
|
||||||
});
|
});
|
||||||
|
h+='<button class="tab tab-test '+(ACTIVE==="test"?"active":"")+'" data-sec="test">🎯 Тест</button>';
|
||||||
$("tabrow").innerHTML=h;
|
$("tabrow").innerHTML=h;
|
||||||
$("tabrow").querySelectorAll(".tab").forEach(function(el){
|
$("tabrow").querySelectorAll(".tab").forEach(function(el){
|
||||||
el.addEventListener("click",function(){ switchTab(el.getAttribute("data-sec")); });
|
el.addEventListener("click",function(){ switchTab(el.getAttribute("data-sec")); });
|
||||||
@ -164,7 +176,11 @@ function switchTab(id){
|
|||||||
document.querySelectorAll(".panel").forEach(function(p){ p.classList.remove("active"); });
|
document.querySelectorAll(".panel").forEach(function(p){ p.classList.remove("active"); });
|
||||||
$(id).classList.add("active");
|
$(id).classList.add("active");
|
||||||
renderPanel(id);
|
renderPanel(id);
|
||||||
renderDoneBar();
|
var isTest = id==="test";
|
||||||
|
$("dayBar").style.display = isTest ? "none" : "";
|
||||||
|
document.querySelector(".done-bar").style.display = isTest ? "none" : "";
|
||||||
|
document.body.style.paddingBottom = isTest ? "24px" : "84px";
|
||||||
|
if(!isTest) renderDoneBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- day bar ---------- */
|
/* ---------- day bar ---------- */
|
||||||
@ -172,8 +188,8 @@ function renderDayBar(){
|
|||||||
$("dayTag").innerHTML = 'День <b>'+(curIdx+1)+'</b> / '+N+(curIdx===todayContentIdx?' · сегодня':' · повторение');
|
$("dayTag").innerHTML = 'День <b>'+(curIdx+1)+'</b> / '+N+(curIdx===todayContentIdx?' · сегодня':' · повторение');
|
||||||
}
|
}
|
||||||
function bindDayNav(){
|
function bindDayNav(){
|
||||||
$("prevDay").onclick=function(){ curIdx=(curIdx-1+N)%N; rerender(); };
|
$("prevDay").onclick=function(){ if(ACTIVE==="test") return; curIdx=(curIdx-1+N)%N; rerender(); };
|
||||||
$("nextDay").onclick=function(){ curIdx=(curIdx+1)%N; rerender(); };
|
$("nextDay").onclick=function(){ if(ACTIVE==="test") return; curIdx=(curIdx+1)%N; rerender(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- panel router ---------- */
|
/* ---------- panel router ---------- */
|
||||||
@ -185,6 +201,7 @@ function renderPanel(id){
|
|||||||
if(id==="listening") renderListening(d);
|
if(id==="listening") renderListening(d);
|
||||||
if(id==="speaking") renderSpeaking(d);
|
if(id==="speaking") renderSpeaking(d);
|
||||||
if(id==="writing") renderWriting(d);
|
if(id==="writing") renderWriting(d);
|
||||||
|
if(id==="test") renderTest();
|
||||||
}
|
}
|
||||||
function renderTopicCard(d){
|
function renderTopicCard(d){
|
||||||
return '<div class="topic-card"><div class="em">'+d.emoji+'</div><div><div class="t">'+esc(d.topic)+'</div><div class="s">Тема дня · Intermediate</div></div></div>';
|
return '<div class="topic-card"><div class="em">'+d.emoji+'</div><div><div class="t">'+esc(d.topic)+'</div><div class="s">Тема дня · Intermediate</div></div></div>';
|
||||||
@ -428,6 +445,207 @@ function rerender(){
|
|||||||
renderDoneBar();
|
renderDoneBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- reminders ---------- */
|
||||||
|
function openReminder(){
|
||||||
|
$("remTime").value=state.reminderTime;
|
||||||
|
$("remOn").checked=!!state.reminderOn;
|
||||||
|
var perm = window.Notification ? Notification.permission : "unsupported";
|
||||||
|
$("remPerm").textContent = perm==="granted"?"Уведомления разрешены ✅":perm==="denied"?"Уведомления заблокированы в браузере":"Разрешение ещё не запрошено";
|
||||||
|
$("remModal").classList.add("show");
|
||||||
|
}
|
||||||
|
function closeReminder(){ $("remModal").classList.remove("show"); }
|
||||||
|
function saveReminder(){
|
||||||
|
state.reminderTime=$("remTime").value||"20:00";
|
||||||
|
var on=$("remOn").checked;
|
||||||
|
if(on && window.Notification && Notification.permission!=="granted" && Notification.permission!=="denied"){
|
||||||
|
Notification.requestPermission();
|
||||||
|
}
|
||||||
|
state.reminderOn=on; save();
|
||||||
|
renderTop();
|
||||||
|
scheduleReminder();
|
||||||
|
closeReminder();
|
||||||
|
toast(on ? ("Напоминания включены на "+state.reminderTime) : "Напоминания выключены");
|
||||||
|
}
|
||||||
|
function maybeNotify(){
|
||||||
|
if(!state.reminderOn) return;
|
||||||
|
var c=completedToday();
|
||||||
|
if(c.length>=SECTIONS.length) return;
|
||||||
|
var today=todayStr();
|
||||||
|
if(state.lastNotified===today) return;
|
||||||
|
var now=new Date();
|
||||||
|
var hh=+state.reminderTime.slice(0,2), mm=+state.reminderTime.slice(3,5);
|
||||||
|
if(now.getHours()*60+now.getMinutes() < hh*60+mm) return;
|
||||||
|
state.lastNotified=today; save();
|
||||||
|
showBanner();
|
||||||
|
if(window.Notification && Notification.permission==="granted"){
|
||||||
|
try{ new Notification("SpeakDaily 🔔",{body:"Пора заниматься английским! Осталось "+(SECTIONS.length-c.length)+" блок(ов) сегодня."}); }catch(e){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function showBanner(){
|
||||||
|
var b=$("reminderBanner");
|
||||||
|
var c=completedToday();
|
||||||
|
b.innerHTML='<span>🔔 Пора заниматься! Осталось '+(SECTIONS.length-c.length)+' из '+SECTIONS.length+' блоков сегодня.</span><button class="link" id="banGo">Начать →</button>';
|
||||||
|
b.hidden=false;
|
||||||
|
$("banGo").onclick=function(){ switchTab("words"); window.scrollTo({top:0,behavior:"smooth"}); };
|
||||||
|
}
|
||||||
|
function scheduleReminder(){
|
||||||
|
clearTimeout(window._remT);
|
||||||
|
if(!state.reminderOn) return;
|
||||||
|
var now=new Date();
|
||||||
|
var hh=+state.reminderTime.slice(0,2), mm=+state.reminderTime.slice(3,5);
|
||||||
|
var target=new Date(now); target.setHours(hh,mm,0,0);
|
||||||
|
if(target<=now) target.setDate(target.getDate()+1);
|
||||||
|
window._remT=setTimeout(function(){ maybeNotify(); scheduleReminder(); }, target-now+1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- monthly test ---------- */
|
||||||
|
var testState=null;
|
||||||
|
function genTest(){
|
||||||
|
var allWords=flatten(CUR.map(function(d){return d.words;}));
|
||||||
|
var wq=shuffle(allWords).slice(0,6).map(function(w){
|
||||||
|
var distr=shuffle(allWords.filter(function(x){return x.en!==w.en;})).slice(0,2).map(function(x){return x.ru;});
|
||||||
|
var opts=shuffle([w.ru].concat(distr));
|
||||||
|
return {q:'Перевод слова: "'+w.en+'"', options:opts, answer:opts.indexOf(w.ru), ex:'Правильно: '+w.ru};
|
||||||
|
});
|
||||||
|
var dq=[];
|
||||||
|
shuffle(CUR.slice()).slice(0,3).forEach(function(d){
|
||||||
|
var i=1+Math.floor(Math.random()*(d.dialogue.length-1));
|
||||||
|
var prev=d.dialogue[i-1], cur=d.dialogue[i];
|
||||||
|
var pool=flatten(CUR.map(function(x){return x.dialogue;})).filter(function(l){return l.text!==cur.text;});
|
||||||
|
var distr=shuffle(pool).slice(0,2).map(function(l){return l.text;});
|
||||||
|
var opts=shuffle([cur.text].concat(distr));
|
||||||
|
dq.push({q:'После реплики: "'+prev.text+'" что отвечает '+cur.name+'?', options:opts, answer:opts.indexOf(cur.text), ex:cur.text});
|
||||||
|
});
|
||||||
|
var rq=[]; shuffle(CUR.slice()).slice(0,2).forEach(function(d){ var qi=d.reading.questions[Math.floor(Math.random()*d.reading.questions.length)]; rq.push({title:d.reading.title, q:qi.q, options:qi.options, answer:qi.answer, ex:qi.ex}); });
|
||||||
|
var lq=[]; shuffle(CUR.slice()).slice(0,2).forEach(function(d){ var qi=d.listening.questions[Math.floor(Math.random()*d.listening.questions.length)]; lq.push({audio:d.listening.text, q:qi.q, options:qi.options, answer:qi.answer, ex:qi.ex}); });
|
||||||
|
var sq=shuffle(CUR.slice()).slice(0,2).map(function(d){return d.speaking;});
|
||||||
|
var wri=shuffle(CUR.slice())[0].writing;
|
||||||
|
return {words:wq, dialogue:dq, reading:rq, listening:lq, speaking:sq, writing:wri, answers:{speaking:{}, writing:0}};
|
||||||
|
}
|
||||||
|
function qhtml(block,i,qi){
|
||||||
|
var h='<div class="q" data-block="'+block+'" data-i="'+i+'"><div class="qt">'+(i+1)+'. '+esc(qi.q)+'</div><div class="opts">';
|
||||||
|
qi.options.forEach(function(op,oi){ h+='<button class="opt" data-o="'+oi+'">'+esc(op)+'</button>'; });
|
||||||
|
h+='</div><div class="explain">'+esc(qi.ex)+'</div></div>';
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
function speakTestHtml(i,s){
|
||||||
|
var sup=!!REC;
|
||||||
|
var h='<div class="speak-box" style="margin-bottom:12px" data-sp="'+i+'"><div class="tips" style="width:100%;margin:0 0 6px"><b>Задание:</b> '+esc(s.prompt)+'</div>';
|
||||||
|
if(sup){ h+='<button class="mic tmic" data-i="'+i+'">🎤</button><div class="tstate" data-i="'+i+'">Нажми и говори</div>'; }
|
||||||
|
else { h+='<div style="font-size:32px">🎤</div><div class="rec-badge">Микрофон недоступен</div>'; }
|
||||||
|
h+='<div class="transcript ttr" data-i="'+i+'"><span class="ph">Твой ответ…</span></div>';
|
||||||
|
h+='<div class="btnrow"><button class="btn ghost thear" data-i="'+i+'">▶ Образец</button><button class="btn outline tshow" data-i="'+i+'">Показать ответ</button></div>';
|
||||||
|
h+='<div class="target ttarget" data-i="'+i+'"><b>Образец:</b>'+esc(s.target)+'</div></div>';
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
function renderTest(){
|
||||||
|
var m=currentMonth();
|
||||||
|
var past=state.tests[m];
|
||||||
|
var h='<div class="topic-card"><div class="em">🎯</div><div><div class="t">Тест месяца</div><div class="s">'+monthLabel(m)+' · проверка по всем блокам</div></div></div>';
|
||||||
|
h+='<div class="card"><h3>📅 История тестов</h3>';
|
||||||
|
var any=false;
|
||||||
|
for(var i=0;i<=5;i++){
|
||||||
|
if(state.tests[i]){ any=true; h+='<div class="mrow"><span>'+monthLabel(i)+(i===m?' · текущий':'')+'</span><b>'+state.tests[i].pct+'%</b></div>'; }
|
||||||
|
else { h+='<div class="mrow"><span class="miss">'+monthLabel(i)+(i===m?' · текущий':'')+'</span><span class="miss">не пройден</span></div>'; }
|
||||||
|
}
|
||||||
|
if(!any) h+='<div class="hint" style="margin-top:6px">Пройди тест в конце месяца, чтобы проверить прогресс по каждому блоку.</div>';
|
||||||
|
h+='</div>';
|
||||||
|
if(testState){
|
||||||
|
h+='<div class="card"><h3>📖 Блок: Слова</h3>'; testState.words.forEach(function(qi,i){ h+=qhtml("words",i,qi); }); h+='</div>';
|
||||||
|
h+='<div class="card"><h3>💬 Блок: Диалог</h3>'; testState.dialogue.forEach(function(qi,i){ h+=qhtml("dialogue",i,qi); }); h+='</div>';
|
||||||
|
h+='<div class="card"><h3>📰 Блок: Чтение</h3>'; testState.reading.forEach(function(qi,i){ if(qi.title) h+='<div class="hint" style="margin-bottom:4px">Текст: '+esc(qi.title)+'</div>'; h+=qhtml("reading",i,qi); }); h+='</div>';
|
||||||
|
h+='<div class="card"><h3>🎧 Блок: Аудирование</h3>'; testState.listening.forEach(function(qi,i){ h+='<div class="btnrow" style="margin-bottom:8px"><button class="btn ghost tplay" data-i="'+i+'">▶ Прослушать</button></div>'; h+=qhtml("listening",i,qi); }); h+='</div>';
|
||||||
|
h+='<div class="card"><h3>🎤 Блок: Говорение</h3>'; testState.speaking.forEach(function(s,i){ h+=speakTestHtml(i,s); }); h+='</div>';
|
||||||
|
h+='<div class="card"><h3>✍️ Блок: Письмо</h3>';
|
||||||
|
h+='<div class="tips" style="margin-bottom:10px"><b>Задание:</b> '+esc(testState.writing.prompt)+'</div>';
|
||||||
|
h+='<textarea id="tw" placeholder="Пиши на английском…"></textarea>';
|
||||||
|
h+='<div class="btnrow" style="margin-top:8px"><button class="btn ghost" id="twModel">Показать образец</button></div>';
|
||||||
|
h+='<div class="model" id="twModelBox"><b>Образец:</b>'+esc(testState.writing.model)+'</div>';
|
||||||
|
h+='<div class="hint" style="margin-top:10px">Сравни с образцом и оцени себя:</div>';
|
||||||
|
h+='<div class="btnrow" style="margin-top:6px"><button class="btn outline wrate" data-r="0">0 — плохо</button><button class="btn outline wrate" data-r="1">1 — средне</button><button class="btn outline wrate" data-r="2">2 — хорошо</button></div>';
|
||||||
|
h+='</div>';
|
||||||
|
h+='<button class="btn primary block" id="finishTest" style="margin-top:6px">Завершить тест месяца</button>';
|
||||||
|
} else if(past){
|
||||||
|
h+='<div class="card"><h3>✅ Результат за '+monthLabel(m)+'</h3><div class="result-grid">';
|
||||||
|
SECTIONS.forEach(function(s){ var r=past[s.id]; var cls=r?(r.score/r.total>=0.7?"good":r.score/r.total>=0.4?"ok":"bad"):""; h+='<div class="rcell '+cls+'"><div class="ric">'+s.icon+'</div><div class="rlabel">'+s.label.toUpperCase()+'</div><div class="rscore">'+(r?r.score+'/'+r.total:'—')+'</div></div>'; });
|
||||||
|
h+='</div><div class="score show" style="margin-top:12px">Итог: '+past.pct+'%</div>';
|
||||||
|
h+='<div class="btnrow" style="margin-top:14px"><button class="btn primary" id="retakeTest">Пройти заново</button></div></div>';
|
||||||
|
} else {
|
||||||
|
h+='<div class="card"><h3>Готов к проверке?</h3><div class="hint">Тест по 6 блокам: слова, диалог, чтение, аудио, говорение и письмо. Вопросы собираются из всех тем курса.</div><button class="btn primary block" id="startTest">Начать тест месяца</button></div>';
|
||||||
|
}
|
||||||
|
$("test").innerHTML=h;
|
||||||
|
if($("startTest")) $("startTest").onclick=function(){ testState=genTest(); renderTest(); window.scrollTo({top:0,behavior:"smooth"}); };
|
||||||
|
if($("retakeTest")) $("retakeTest").onclick=function(){ testState=genTest(); renderTest(); window.scrollTo({top:0,behavior:"smooth"}); };
|
||||||
|
if($("finishTest")) $("finishTest").onclick=finishTest;
|
||||||
|
if(testState) bindTestInteractions();
|
||||||
|
}
|
||||||
|
function bindTestInteractions(){
|
||||||
|
var root=$("test");
|
||||||
|
// MCQ
|
||||||
|
root.querySelectorAll(".q").forEach(function(qEl){
|
||||||
|
var answered=false;
|
||||||
|
qEl.querySelectorAll(".opt").forEach(function(oEl){
|
||||||
|
oEl.addEventListener("click",function(){
|
||||||
|
if(answered) return; answered=true;
|
||||||
|
var block=qEl.getAttribute("data-block");
|
||||||
|
var o=+oEl.getAttribute("data-o");
|
||||||
|
var qs=testState[block][+qEl.getAttribute("data-i")];
|
||||||
|
var correct=qs.answer;
|
||||||
|
qEl.querySelectorAll(".opt").forEach(function(x){ x.classList.add("disabled"); });
|
||||||
|
if(o===correct){ oEl.classList.add("correct"); }
|
||||||
|
else{ oEl.classList.add("wrong"); var c=qEl.querySelector('.opt[data-o="'+correct+'"]'); if(c) c.classList.add("correct"); }
|
||||||
|
qEl.querySelector(".explain").classList.add("show");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// listening play
|
||||||
|
root.querySelectorAll(".tplay").forEach(function(b){
|
||||||
|
b.onclick=function(){ var i=+b.getAttribute("data-i"); stopSpeak(); speak(testState.listening[i].audio); };
|
||||||
|
});
|
||||||
|
// speaking
|
||||||
|
root.querySelectorAll(".tmic").forEach(function(mic){
|
||||||
|
mic.onclick=function(){ var i=+mic.getAttribute("data-i"); var s=testState.speaking[i];
|
||||||
|
if(mic.classList.contains("rec")){ stopRec(); return; }
|
||||||
|
var ok=startRec(function(t){
|
||||||
|
var tr=root.querySelector('.ttr[data-i="'+i+'"]'); tr.innerHTML=esc(t);
|
||||||
|
var sim=similarity(t,s.target); testState.answers.speaking[i]=sim;
|
||||||
|
var lv=levelOf(sim);
|
||||||
|
var d=document.createElement("div"); d.className="match "+lv; d.textContent=(lv==="good"?"Отлично!":lv==="ok"?"Неплохо!":"Ещё раз")+" ("+Math.round(sim*100)+"%)";
|
||||||
|
tr.appendChild(d);
|
||||||
|
},function(){ mic.classList.remove("rec"); mic.textContent="🎤"; root.querySelector('.tstate[data-i="'+i+'"]').textContent="Нажми и говори"; });
|
||||||
|
if(ok){ mic.classList.add("rec"); mic.textContent="⏹"; root.querySelector('.tstate[data-i="'+i+'"]').innerHTML='<span class="rec-badge"><span class="blink"></span>Слушаю…</span>'; }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
root.querySelectorAll(".thear").forEach(function(b){ b.onclick=function(){ var i=+b.getAttribute("data-i"); stopSpeak(); speak(testState.speaking[i].target,{rate:0.9}); }; });
|
||||||
|
root.querySelectorAll(".tshow").forEach(function(b){ b.onclick=function(){ var i=+b.getAttribute("data-i"); root.querySelector('.ttarget[data-i="'+i+'"]').classList.toggle("show"); }; });
|
||||||
|
// writing
|
||||||
|
if($("twModel")) $("twModel").onclick=function(){ $("twModelBox").classList.toggle("show"); };
|
||||||
|
root.querySelectorAll(".wrate").forEach(function(b){
|
||||||
|
b.onclick=function(){ testState.answers.writing=+b.getAttribute("data-r");
|
||||||
|
root.querySelectorAll(".wrate").forEach(function(x){ x.classList.remove("primary"); x.classList.add("outline"); });
|
||||||
|
b.classList.remove("outline"); b.classList.add("primary");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function finishTest(){
|
||||||
|
var t=testState, ans=t.answers;
|
||||||
|
function blockScore(block){ var got=0,total=0; $("test").querySelectorAll('.q[data-block="'+block+'"]').forEach(function(qEl){ total++; if(qEl.querySelector(".opt.correct")&&!qEl.querySelector(".opt.wrong")) got++; }); return {score:got,total:total}; }
|
||||||
|
var res={};
|
||||||
|
res.words=blockScore("words");
|
||||||
|
res.dialogue=blockScore("dialogue");
|
||||||
|
res.reading=blockScore("reading");
|
||||||
|
res.listening=blockScore("listening");
|
||||||
|
var sg=0; t.speaking.forEach(function(s,i){ if((ans.speaking[i]||0)>=0.6) sg++; }); res.speaking={score:sg,total:t.speaking.length};
|
||||||
|
res.writing={score:ans.writing||0,total:2};
|
||||||
|
var ts=0,tt=0; SECTIONS.forEach(function(s){ ts+=res[s.id].score; tt+=res[s.id].total; });
|
||||||
|
res.pct=Math.round(ts/tt*100); res.date=todayStr();
|
||||||
|
var m=currentMonth(); state.tests[m]=res; save();
|
||||||
|
testState=null;
|
||||||
|
renderTest();
|
||||||
|
toast("Тест завершён! Итог: "+res.pct+"%");
|
||||||
|
window.scrollTo({top:0,behavior:"smooth"});
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- init ---------- */
|
/* ---------- init ---------- */
|
||||||
function init(){
|
function init(){
|
||||||
load();
|
load();
|
||||||
@ -438,6 +656,12 @@ function init(){
|
|||||||
renderDayBar();
|
renderDayBar();
|
||||||
bindDayNav();
|
bindDayNav();
|
||||||
bindDoneBar();
|
bindDoneBar();
|
||||||
|
$("bellBtn").onclick=openReminder;
|
||||||
|
$("remSave").onclick=saveReminder;
|
||||||
|
$("remCancel").onclick=closeReminder;
|
||||||
|
$("remModal").addEventListener("click",function(e){ if(e.target===$("remModal")) closeReminder(); });
|
||||||
|
scheduleReminder();
|
||||||
|
maybeNotify();
|
||||||
switchTab("words");
|
switchTab("words");
|
||||||
}
|
}
|
||||||
if(document.readyState!=="loading") init();
|
if(document.readyState!=="loading") init();
|
||||||
|
|||||||
25
index.html
25
index.html
@ -17,6 +17,7 @@
|
|||||||
<span>SpeakDaily<small>английский для работы</small></span>
|
<span>SpeakDaily<small>английский для работы</small></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pills">
|
<div class="pills">
|
||||||
|
<button class="pill bell" id="bellBtn" title="Напоминания">🔔</button>
|
||||||
<span class="pill" id="streak">🔥 1</span>
|
<span class="pill" id="streak">🔥 1</span>
|
||||||
<span class="pill" id="level">Intermediate</span>
|
<span class="pill" id="level">Intermediate</span>
|
||||||
</div>
|
</div>
|
||||||
@ -26,6 +27,7 @@
|
|||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="hello">Привет! Рад, что ты здесь 👋</div>
|
<div class="hello">Привет! Рад, что ты здесь 👋</div>
|
||||||
|
<div class="banner" id="reminderBanner" hidden></div>
|
||||||
<div class="goal-card">
|
<div class="goal-card">
|
||||||
<div class="goal-title" id="goalTitle">Цель: свободный английский за 6 месяцев</div>
|
<div class="goal-title" id="goalTitle">Цель: свободный английский за 6 месяцев</div>
|
||||||
<div class="goal-sub" id="goalSub"></div>
|
<div class="goal-sub" id="goalSub"></div>
|
||||||
@ -46,7 +48,7 @@
|
|||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="day-bar">
|
<div class="day-bar" id="dayBar">
|
||||||
<span class="day-tag" id="dayTag"></span>
|
<span class="day-tag" id="dayTag"></span>
|
||||||
<div class="btnrow">
|
<div class="btnrow">
|
||||||
<button class="btn ghost" id="prevDay" style="padding:8px 14px">←</button>
|
<button class="btn ghost" id="prevDay" style="padding:8px 14px">←</button>
|
||||||
@ -60,6 +62,7 @@
|
|||||||
<section class="panel" id="listening"></section>
|
<section class="panel" id="listening"></section>
|
||||||
<section class="panel" id="speaking"></section>
|
<section class="panel" id="speaking"></section>
|
||||||
<section class="panel" id="writing"></section>
|
<section class="panel" id="writing"></section>
|
||||||
|
<section class="panel" id="test"></section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -72,6 +75,26 @@
|
|||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
|
<div class="modal" id="remModal">
|
||||||
|
<div class="modal-card">
|
||||||
|
<h3 class="modal-h">🔔 Напоминания</h3>
|
||||||
|
<p class="hint">Напоминание приходит в выбранное время, пока вкладка открыта. А при входе мы подскажем, если ты ещё не занимался сегодня.</p>
|
||||||
|
<div class="row-between">
|
||||||
|
<div>
|
||||||
|
<div class="rl">Ежедневное напоминание</div>
|
||||||
|
<div class="hint" id="remPerm" style="margin:0"></div>
|
||||||
|
</div>
|
||||||
|
<label class="switch"><input type="checkbox" id="remOn"><span class="sl"></span></label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="rl" style="margin-bottom:8px">Время напоминания</div>
|
||||||
|
<input type="time" id="remTime" class="time-input">
|
||||||
|
</div>
|
||||||
|
<button class="btn primary block" id="remSave">Сохранить</button>
|
||||||
|
<button class="btn ghost block" id="remCancel" style="margin-top:8px">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="data.js"></script>
|
<script src="data.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
48
style.css
48
style.css
@ -180,3 +180,51 @@ textarea:focus{outline:none;border-color:var(--cyan)}
|
|||||||
@media(min-width:641px){
|
@media(min-width:641px){
|
||||||
.hero{padding:12px 16px 26px}
|
.hero{padding:12px 16px 26px}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- bell ---------- */
|
||||||
|
.bell{cursor:pointer;border:none}
|
||||||
|
.bell.on::after{content:"";width:7px;height:7px;border-radius:50%;background:var(--cyan);display:inline-block;margin-left:4px;vertical-align:middle}
|
||||||
|
|
||||||
|
/* ---------- reminder banner ---------- */
|
||||||
|
.banner{background:var(--cyan);color:var(--ink);border-radius:12px;padding:11px 14px;margin:10px 0;font-weight:700;font-size:14px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
|
||||||
|
.banner[hidden]{display:none}
|
||||||
|
.banner .link{background:var(--ink);color:var(--white);padding:7px 12px;border-radius:8px;font-weight:800;font-size:13px;border:none;cursor:pointer}
|
||||||
|
|
||||||
|
/* ---------- modal ---------- */
|
||||||
|
.modal{position:fixed;inset:0;z-index:70;background:rgba(15,18,24,.55);display:none;align-items:center;justify-content:center;padding:16px}
|
||||||
|
.modal.show{display:flex}
|
||||||
|
.modal-card{background:var(--white);width:100%;max-width:420px;border-radius:20px;padding:22px;max-height:90vh;overflow:auto;animation:slideup .25s}
|
||||||
|
@keyframes slideup{from{transform:translateY(24px);opacity:.5}to{transform:none;opacity:1}}
|
||||||
|
.modal-h{font-size:19px;font-weight:800;margin-bottom:8px}
|
||||||
|
.row-between{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:18px 0}
|
||||||
|
.rl{font-weight:700;font-size:15px}
|
||||||
|
.field{margin-bottom:18px}
|
||||||
|
.time-input{font:inherit;font-size:18px;padding:10px 12px;border:2px solid var(--gray-200);border-radius:10px;width:100%}
|
||||||
|
.time-input:focus{outline:none;border-color:var(--cyan)}
|
||||||
|
|
||||||
|
/* switch */
|
||||||
|
.switch{position:relative;width:48px;height:28px;display:inline-block;flex-shrink:0}
|
||||||
|
.switch input{opacity:0;width:0;height:0}
|
||||||
|
.switch .sl{position:absolute;inset:0;background:var(--gray-200);border-radius:999px;transition:.2s;cursor:pointer}
|
||||||
|
.switch .sl:before{content:"";position:absolute;width:22px;height:22px;left:3px;top:3px;background:#fff;border-radius:50%;transition:.2s;box-shadow:var(--shadow-sm)}
|
||||||
|
.switch input:checked+.sl{background:var(--cyan)}
|
||||||
|
.switch input:checked+.sl:before{transform:translateX(20px)}
|
||||||
|
|
||||||
|
/* ---------- test tab ---------- */
|
||||||
|
.tab-test{color:var(--violet)}
|
||||||
|
.tab-test.active{background:var(--violet);color:#fff}
|
||||||
|
|
||||||
|
.mrow{display:flex;justify-content:space-between;align-items:center;padding:10px 2px;border-bottom:1px solid var(--gray-100);font-weight:600;font-size:15px}
|
||||||
|
.mrow:last-child{border-bottom:none}
|
||||||
|
.mrow b{color:var(--cyan);font-size:16px}
|
||||||
|
.mrow .miss{color:var(--gray-500);font-size:13px;font-weight:500}
|
||||||
|
|
||||||
|
.result-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
||||||
|
.rcell{background:var(--gray-100);border-radius:12px;padding:12px 6px;text-align:center}
|
||||||
|
.rcell .ric{font-size:22px}
|
||||||
|
.rcell .rlabel{font-size:11px;color:var(--gray-500);font-weight:700;margin:3px 0;letter-spacing:.3px}
|
||||||
|
.rcell .rscore{font-size:15px;font-weight:800}
|
||||||
|
.rcell.good{background:#dcfce7}
|
||||||
|
.rcell.ok{background:#fef9c3}
|
||||||
|
.rcell.bad{background:#fee2e2}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user