diff --git a/app.js b/app.js
index a470a22..b171a4b 100644
--- a/app.js
+++ b/app.js
@@ -22,7 +22,11 @@ function dateStr(d){ return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(
function load(){
try{ state = JSON.parse(localStorage.getItem("enTrainer")||"null"); }catch(e){ state=null; }
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
if(state.lastVisit !== today){
var y=new Date(); y.setDate(y.getDate()-1);
@@ -55,6 +59,12 @@ function computeDay(){
}
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 ---------- */
var voices=[];
var synth = window.speechSynthesis;
@@ -123,6 +133,7 @@ function toast(msg){
function renderTop(){
$("streak").innerHTML="🔥 "+state.streak;
$("level").textContent="Intermediate";
+ $("bellBtn").classList.toggle("on", !!state.reminderOn);
}
function renderDashboard(){
var elapsed = dayKey+1;
@@ -152,6 +163,7 @@ function renderTabs(){
SECTIONS.forEach(function(s){
h+='';
});
+ h+='';
$("tabrow").innerHTML=h;
$("tabrow").querySelectorAll(".tab").forEach(function(el){
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"); });
$(id).classList.add("active");
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 ---------- */
@@ -172,8 +188,8 @@ function renderDayBar(){
$("dayTag").innerHTML = 'День '+(curIdx+1)+' / '+N+(curIdx===todayContentIdx?' · сегодня':' · повторение');
}
function bindDayNav(){
- $("prevDay").onclick=function(){ curIdx=(curIdx-1+N)%N; rerender(); };
- $("nextDay").onclick=function(){ curIdx=(curIdx+1)%N; rerender(); };
+ $("prevDay").onclick=function(){ if(ACTIVE==="test") return; curIdx=(curIdx-1+N)%N; rerender(); };
+ $("nextDay").onclick=function(){ if(ACTIVE==="test") return; curIdx=(curIdx+1)%N; rerender(); };
}
/* ---------- panel router ---------- */
@@ -185,6 +201,7 @@ function renderPanel(id){
if(id==="listening") renderListening(d);
if(id==="speaking") renderSpeaking(d);
if(id==="writing") renderWriting(d);
+ if(id==="test") renderTest();
}
function renderTopicCard(d){
return '
'+d.emoji+'
'+esc(d.topic)+'
Тема дня · Intermediate
';
@@ -428,6 +445,207 @@ function rerender(){
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='🔔 Пора заниматься! Осталось '+(SECTIONS.length-c.length)+' из '+SECTIONS.length+' блоков сегодня.';
+ 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=''+(i+1)+'. '+esc(qi.q)+'
';
+ qi.options.forEach(function(op,oi){ h+=''; });
+ h+='
'+esc(qi.ex)+'
';
+ return h;
+}
+function speakTestHtml(i,s){
+ var sup=!!REC;
+ var h='Задание: '+esc(s.prompt)+'
';
+ if(sup){ h+='
Нажми и говори
'; }
+ else { h+='
🎤
Микрофон недоступен
'; }
+ h+='
Твой ответ…
';
+ h+='
';
+ h+='
Образец:'+esc(s.target)+'
';
+ return h;
+}
+function renderTest(){
+ var m=currentMonth();
+ var past=state.tests[m];
+ var h='🎯
Тест месяца
'+monthLabel(m)+' · проверка по всем блокам
';
+ h+='📅 История тестов
';
+ var any=false;
+ for(var i=0;i<=5;i++){
+ if(state.tests[i]){ any=true; h+='
'+monthLabel(i)+(i===m?' · текущий':'')+''+state.tests[i].pct+'%
'; }
+ else { h+='
'+monthLabel(i)+(i===m?' · текущий':'')+'не пройден
'; }
+ }
+ if(!any) h+='
Пройди тест в конце месяца, чтобы проверить прогресс по каждому блоку.
';
+ h+='
';
+ if(testState){
+ h+='📖 Блок: Слова
'; testState.words.forEach(function(qi,i){ h+=qhtml("words",i,qi); }); h+='';
+ h+='💬 Блок: Диалог
'; testState.dialogue.forEach(function(qi,i){ h+=qhtml("dialogue",i,qi); }); h+='';
+ h+='📰 Блок: Чтение
'; testState.reading.forEach(function(qi,i){ if(qi.title) h+='
Текст: '+esc(qi.title)+'
'; h+=qhtml("reading",i,qi); }); h+='
';
+ h+='🎧 Блок: Аудирование
'; testState.listening.forEach(function(qi,i){ h+='
'; h+=qhtml("listening",i,qi); }); h+='
';
+ h+='🎤 Блок: Говорение
'; testState.speaking.forEach(function(s,i){ h+=speakTestHtml(i,s); }); h+='';
+ h+='✍️ Блок: Письмо
';
+ h+='
Задание: '+esc(testState.writing.prompt)+'
';
+ h+='
';
+ h+='
';
+ h+='
Образец:'+esc(testState.writing.model)+'
';
+ h+='
Сравни с образцом и оцени себя:
';
+ h+='
';
+ h+='
';
+ h+='';
+ } else if(past){
+ h+='✅ Результат за '+monthLabel(m)+'
';
+ 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+='
'+s.icon+'
'+s.label.toUpperCase()+'
'+(r?r.score+'/'+r.total:'—')+'
'; });
+ h+='
Итог: '+past.pct+'%
';
+ h+='
';
+ } else {
+ h+='Готов к проверке?
Тест по 6 блокам: слова, диалог, чтение, аудио, говорение и письмо. Вопросы собираются из всех тем курса.
';
+ }
+ $("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='Слушаю…'; }
+ };
+ });
+ 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 ---------- */
function init(){
load();
@@ -438,6 +656,12 @@ function init(){
renderDayBar();
bindDayNav();
bindDoneBar();
+ $("bellBtn").onclick=openReminder;
+ $("remSave").onclick=saveReminder;
+ $("remCancel").onclick=closeReminder;
+ $("remModal").addEventListener("click",function(e){ if(e.target===$("remModal")) closeReminder(); });
+ scheduleReminder();
+ maybeNotify();
switchTab("words");
}
if(document.readyState!=="loading") init();
diff --git a/index.html b/index.html
index f7c9f51..eafb130 100644
--- a/index.html
+++ b/index.html
@@ -17,6 +17,7 @@
SpeakDailyанглийский для работы
+
🔥 1
Intermediate
@@ -26,6 +27,7 @@
Привет! Рад, что ты здесь 👋
+
Цель: свободный английский за 6 месяцев
@@ -46,7 +48,7 @@
-
+
@@ -60,6 +62,7 @@
+
@@ -72,6 +75,26 @@
+
+
+
🔔 Напоминания
+
Напоминание приходит в выбранное время, пока вкладка открыта. А при входе мы подскажем, если ты ещё не занимался сегодня.
+
+
+
Ежедневное напоминание
+
+
+
+
+
+
+
+
+
+