From 80f704a61689ebd0c61a6f006936bdda327e4b45 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Fri, 26 Jun 2026 06:36:25 +0000 Subject: [PATCH] v2: reminders + monthly tests per block --- app.js | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++++- index.html | 25 +++++- style.css | 48 +++++++++++ 3 files changed, 300 insertions(+), 5 deletions(-) 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 @@
+ + diff --git a/style.css b/style.css index ce38ca1..58a931e 100644 --- a/style.css +++ b/style.css @@ -180,3 +180,51 @@ textarea:focus{outline:none;border-color:var(--cyan)} @media(min-width:641px){ .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} +