(function(){ "use strict"; var CUR = window.CURRICULUM || []; var N = CUR.length; var SECTIONS = [ {id:"words", icon:"📖", label:"Слова"}, {id:"dialogue", icon:"💬", label:"Диалог"}, {id:"reading", icon:"📰", label:"Чтение"}, {id:"listening", icon:"🎧", label:"Аудио"}, {id:"speaking", icon:"🎤", label:"Говорю"}, {id:"writing", icon:"✍️", label:"Пишу"} ]; var ACTIVE = "words"; var curIdx = 0; var todayContentIdx = 0; var dayKey = 0; var state = null; /* ---------- storage ---------- */ function todayStr(){ var d=new Date(); return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0"); } function dateStr(d){ return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0"); } 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:[]}; } // streak if(state.lastVisit !== today){ var y=new Date(); y.setDate(y.getDate()-1); state.streak = (state.lastVisit===dateStr(y)) ? state.streak+1 : 1; state.lastVisit = today; } if(!state.completed) state.completed={}; if(!state.completed[today]) state.completed[today]=[]; if(!state.wordsLearned) state.wordsLearned=[]; save(); } function save(){ localStorage.setItem("enTrainer", JSON.stringify(state)); } function completedToday(){ return state.completed[todayStr()] || (state.completed[todayStr()]=[]); } function isDone(id){ return completedToday().indexOf(id)>=0; } function setDone(id, on){ var c=completedToday(); var i=c.indexOf(id); if(on && i<0){ c.push(id); } if(!on && i>=0){ c.splice(i,1); } save(); } /* ---------- day math ---------- */ function computeDay(){ var s=new Date(state.startDate+"T00:00:00"); var t=new Date(); t.setHours(0,0,0,0); dayKey = Math.max(0, Math.round((t-s)/86400000)); todayContentIdx = ((dayKey % N)+N)%N; curIdx = todayContentIdx; } function day(){ return CUR[curIdx]; } /* ---------- speech ---------- */ var voices=[]; var synth = window.speechSynthesis; function loadVoices(){ if(synth) voices = synth.getVoices(); } if(synth){ loadVoices(); synth.onvoiceschanged=loadVoices; } function enVoice(pref){ var en = voices.filter(function(v){ return /en(-|_)?US/i.test(v.lang) || /en(-|_)?GB/i.test(v.lang); }); if(!en.length) en = voices.filter(function(v){ return /^en/i.test(v.lang); }); if(pref) for(var i=0;i=0.8?"good":sim>=0.5?"ok":"bad"; } function stopRec(){ if(currentRec){ try{currentRec.stop();}catch(e){} currentRec=null; } } function startRec(onResult, onEnd){ if(!REC){ toast("Микрофон не поддерживается в этом браузере 🙁 Попробуй Chrome"); return false; } stopRec(); stopSpeak(); var r=new REC(); r.lang="en-US"; r.interimResults=false; r.maxAlternatives=3; r.onresult=function(e){ var t=""; for(var i=0;i/g,">"); } var toastT; function toast(msg){ var t=$("toast"); t.textContent=msg; t.classList.add("show"); clearTimeout(toastT); toastT=setTimeout(function(){ t.classList.remove("show"); },2200); } /* ---------- render: header + dashboard ---------- */ function renderTop(){ $("streak").innerHTML="🔥 "+state.streak; $("level").textContent="Intermediate"; } function renderDashboard(){ var elapsed = dayKey+1; var total=180; var pct = Math.min(100, Math.round(elapsed/total*100)); var left = Math.max(0, total-elapsed); $("goalTitle").textContent = "Цель: свободный английский за 6 месяцев"; $("goalSub").textContent = "Ты на пути! Каждый день — новый шаг к свободной речи с коллегами."; $("goalBar").style.width = pct+"%"; $("goalMetaL").innerHTML = "День "+elapsed+" из "+total; $("goalMetaR").innerHTML = "Осталось "+left+" дн."; // mission chips var h=""; SECTIONS.forEach(function(s){ var d=isDone(s.id); h+='
'+s.icon+''+s.label+'
'; }); $("mission").innerHTML=h; $("mission").querySelectorAll(".mchip").forEach(function(el){ el.addEventListener("click",function(){ switchTab(el.getAttribute("data-sec")); window.scrollTo({top:0,behavior:"smooth"}); }); }); } /* ---------- render: tabs ---------- */ function renderTabs(){ var h=""; SECTIONS.forEach(function(s){ h+=''; }); $("tabrow").innerHTML=h; $("tabrow").querySelectorAll(".tab").forEach(function(el){ el.addEventListener("click",function(){ switchTab(el.getAttribute("data-sec")); }); }); } function switchTab(id){ stopSpeak(); stopRec(); ACTIVE=id; document.querySelectorAll(".tab").forEach(function(el){ el.classList.toggle("active", el.getAttribute("data-sec")===id); }); document.querySelectorAll(".panel").forEach(function(p){ p.classList.remove("active"); }); $(id).classList.add("active"); renderPanel(id); renderDoneBar(); } /* ---------- day bar ---------- */ 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(); }; } /* ---------- panel router ---------- */ function renderPanel(id){ var d=day(); if(id==="words") renderWords(d); if(id==="dialogue") renderDialogue(d); if(id==="reading") renderReading(d); if(id==="listening") renderListening(d); if(id==="speaking") renderSpeaking(d); if(id==="writing") renderWriting(d); } function renderTopicCard(d){ return '
'+d.emoji+'
'+esc(d.topic)+'
Тема дня · Intermediate
'; } /* ---------- words ---------- */ function renderWords(d){ var h=renderTopicCard(d); h+='

📖 Новые слова ('+d.words.length+')

Нажми на карточку, чтобы увидеть значение. ▶ — послушать произношение.
'; d.words.forEach(function(w,i){ var learned = state.wordsLearned.indexOf(w.en)>=0; h+='
'+esc(w.en)+'
'+esc(w.ru)+'
'+esc(w.def)+'
"'+esc(w.ex)+'"
'+(learned?"⭐ Изучено":"")+'
'; }); h+='
'; h+='
'; $("words").innerHTML=h; $("words").querySelectorAll(".word").forEach(function(el){ el.addEventListener("click",function(e){ if(e.target.closest(".play")) return; el.classList.toggle("open"); }); }); $("words").querySelectorAll(".play").forEach(function(el){ el.addEventListener("click",function(e){ e.stopPropagation(); var i=+el.getAttribute("data-play"); var w=d.words[i]; stopSpeak(); speak(w.en); if(state.wordsLearned.indexOf(w.en)<0){ state.wordsLearned.push(w.en); save(); } var card=el.closest(".word"); card.querySelector(".stars").textContent="⭐ Изучено"; }); }); $("playAll").onclick=function(){ stopSpeak(); var p=Promise.resolve(); d.words.forEach(function(w){ p=p.then(function(){ return speak(w.en,{rate:0.95}).then(function(){ return new Promise(function(r){ setTimeout(r,250); }); }); }); }); }; } /* ---------- dialogue ---------- */ function renderDialogue(d){ var h=renderTopicCard(d); h+='

💬 Диалог дня

▶ — проиграть. Нажми строку, чтобы увидеть перевод.
'; h+='
'; h+='
'; d.dialogue.forEach(function(l,i){ h+='
'+esc(l.name)+'
'+esc(l.text)+'
'+esc(l.tr)+'
'; }); h+='
'; h+='
Совет: повторяй реплики вслух за диктором — это тренирует произношение и скорость речи.
'; h+='
'; $("dialogue").innerHTML=h; $("lines").querySelectorAll(".line").forEach(function(el){ el.addEventListener("click",function(){ el.classList.toggle("show-tr"); }); }); $("toggleTr").onclick=function(){ var all=$("lines").querySelectorAll(".line"); var anyOff=false; all.forEach(function(el){ if(!el.classList.contains("show-tr")) anyOff=true; }); all.forEach(function(el){ el.classList.toggle("show-tr", anyOff); }); }; $("playAllDlg").onclick=function(){ stopSpeak(); stopRec(); var p=Promise.resolve(); d.dialogue.forEach(function(l){ p=p.then(function(){ return speak(l.text,{rate:0.95, pitch: l.who==="A"?1:1.15, voice: l.who==="A"?"Male":"Female"}); }).then(function(){ return new Promise(function(r){ setTimeout(r,350); }); }); }); }; } /* ---------- reading ---------- */ function renderReading(d){ var r=d.reading; var h=renderTopicCard(d); h+='

📰 '+esc(r.title)+'

Прочитай текст и ответь на вопросы.
'; h+='

'+esc(r.text)+'

'; r.questions.forEach(function(qi,idx){ h+='
'+(idx+1)+'. '+esc(qi.q)+'
'; qi.options.forEach(function(op,oi){ h+=''; }); h+='
'+esc(qi.ex)+'
'; }); h+='
'; $("reading").innerHTML=h; bindQuiz("reading", r.questions); } /* ---------- listening ---------- */ function renderListening(d){ var l=d.listening; var h=renderTopicCard(d); h+='

🎧 Аудирование

Послушай аудио и ответь на вопросы. Можно менять скорость.
'; h+='
'; h+=''; h+='
'+esc(l.text)+'
'; l.questions.forEach(function(qi,idx){ h+='
'+(idx+1)+'. '+esc(qi.q)+'
'; qi.options.forEach(function(op,oi){ h+=''; }); h+='
'+esc(qi.ex)+'
'; }); h+='
'; $("listening").innerHTML=h; var rate=1; $("playL").onclick=function(){ stopSpeak(); speak(l.text,{rate:rate}); }; $("listening").querySelectorAll(".speed button").forEach(function(b){ b.onclick=function(){ rate=parseFloat(b.getAttribute("data-sp")); $("listening").querySelectorAll(".speed button").forEach(function(x){ x.classList.remove("on"); }); b.classList.add("on"); }; }); $("showText").onclick=function(){ $("lText").classList.toggle("show"); this.textContent=$("lText").classList.contains("show")?"Скрыть текст":"Показать текст"; }; bindQuiz("listening", l.questions); } /* ---------- quiz shared ---------- */ function bindQuiz(panelId, questions){ var root=$(panelId); root.querySelectorAll(".q").forEach(function(qEl, qIdx){ var answered=false; qEl.querySelectorAll(".opt").forEach(function(oEl){ oEl.addEventListener("click",function(){ if(answered) return; answered=true; var o=+oEl.getAttribute("data-o"); var correct=questions[qIdx].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"); updateScore(root, questions.length, panelId); }); }); }); } function updateScore(root, total, panelId){ var got=0; root.querySelectorAll(".q").forEach(function(qEl){ if(qEl.querySelector(".opt.correct") && !qEl.querySelector(".opt.wrong")) got++; }); var sc=$(panelId==="reading"?"rScore":"lScore"); if(sc){ sc.textContent="Результат: "+got+" / "+total+(got===total?" 🎉 Отлично!":""); sc.classList.add("show"); } } /* ---------- speaking ---------- */ function renderSpeaking(d){ var s=d.speaking; var sup = !!REC; var h=renderTopicCard(d); h+='

🎤 Практика говорения

Прочитай задание и ответь вслух. Я распознаю речь и сравню с образцом.
'; h+='
Задание: '+esc(s.prompt)+'
'; h+='
'; if(sup){ h+=''; h+='
Нажми на микрофон и говори
'; } else { h+='
🎤
'; h+='
Микрофон недоступен в этом браузере
'; h+='
Открой в Chrome на телефоне или компьютере, чтобы практиковать речь.
'; } h+='
Твой ответ появится здесь…
'; h+='
'; h+='
Образцовый ответ:'+esc(s.target)+'
'; h+='
'; h+='
💡 Не бойся ошибок — главное пробовать говорить вслух каждый день. Это ключ к свободной речи.
'; h+='
'; $("speaking").innerHTML=h; $("hearModel").onclick=function(){ stopSpeak(); speak(s.target,{rate:0.9}); }; $("showTarget").onclick=function(){ $("target").classList.toggle("show"); }; if(sup){ var mic=$("mic"), state2=$("micState"), tr=$("tr"); mic.onclick=function(){ if(mic.classList.contains("rec")){ stopRec(); return; } var ok=startRec(function(t){ tr.innerHTML=esc(t); var sim=similarity(t, s.target); var lv=levelOf(sim); var label = lv==="good"?"Отлично! 🎉":lv==="ok"?"Неплохо, почти! 👍":"Попробуй ещё раз 💪"; var tr2=document.createElement("div"); tr2.className="match "+lv; tr2.textContent=label+" ("+Math.round(sim*100)+"% совпадение)"; tr.appendChild(tr2); },function(){ mic.classList.remove("rec"); mic.textContent="🎤"; state2.textContent="Нажми на микрофон и говори"; }); if(ok){ mic.classList.add("rec"); mic.textContent="⏹"; state2.innerHTML='Слушаю…'; } }; } } /* ---------- writing ---------- */ function renderWriting(d){ var w=d.writing; var key="w_"+todayStr()+"_"+curIdx; var saved=localStorage.getItem(key)||""; var h=renderTopicCard(d); h+='

✍️ Практика письма

Напиши ответ и сравни с образцом.
'; h+='
Задание: '+esc(w.prompt)+'
'; h+=''; h+='
'; h+='
Образцовый ответ:'+esc(w.model)+'
'; h+='
💡 '+esc(w.tips)+'
'; h+='
'; $("writing").innerHTML=h; $("saveW").onclick=function(){ localStorage.setItem(key, $("ta").value); toast("Сохранено ✅"); }; $("showModel").onclick=function(){ $("model").classList.toggle("show"); }; } /* ---------- done bar ---------- */ function renderDoneBar(){ var sec=SECTIONS.filter(function(s){return s.id===ACTIVE;})[0]; var done=isDone(ACTIVE); var c=completedToday(); $("doneInfo").innerHTML = 'Сегодня выполнено: '+c.length+"/"+SECTIONS.length+' · '+sec.label; var b=$("doneBtn"); b.textContent=done?"✓ Выполнено":"Отметить выполненным"; b.classList.toggle("done", done); } function bindDoneBar(){ $("doneBtn").onclick=function(){ var on=!isDone(ACTIVE); setDone(ACTIVE, on); renderDoneBar(); renderDashboard(); if(on){ var c=completedToday(); if(c.length===SECTIONS.length) toast("Все задания дня готовы! Так держать 🚀"); else toast("Готово! +1 к прогрессу 💪"); } }; } /* ---------- rerender ---------- */ function rerender(){ renderDayBar(); renderPanel(ACTIVE); renderDoneBar(); } /* ---------- init ---------- */ function init(){ load(); computeDay(); renderTop(); renderDashboard(); renderTabs(); renderDayBar(); bindDayNav(); bindDoneBar(); switchTab("words"); } if(document.readyState!=="loading") init(); else document.addEventListener("DOMContentLoaded", init); })();