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(){
|
||||
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+='<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").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 = 'День <b>'+(curIdx+1)+'</b> / '+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 '<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();
|
||||
}
|
||||
|
||||
/* ---------- 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 ---------- */
|
||||
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();
|
||||
|
||||
25
index.html
25
index.html
@ -17,6 +17,7 @@
|
||||
<span>SpeakDaily<small>английский для работы</small></span>
|
||||
</div>
|
||||
<div class="pills">
|
||||
<button class="pill bell" id="bellBtn" title="Напоминания">🔔</button>
|
||||
<span class="pill" id="streak">🔥 1</span>
|
||||
<span class="pill" id="level">Intermediate</span>
|
||||
</div>
|
||||
@ -26,6 +27,7 @@
|
||||
<section class="hero">
|
||||
<div class="wrap">
|
||||
<div class="hello">Привет! Рад, что ты здесь 👋</div>
|
||||
<div class="banner" id="reminderBanner" hidden></div>
|
||||
<div class="goal-card">
|
||||
<div class="goal-title" id="goalTitle">Цель: свободный английский за 6 месяцев</div>
|
||||
<div class="goal-sub" id="goalSub"></div>
|
||||
@ -46,7 +48,7 @@
|
||||
|
||||
<main>
|
||||
<div class="wrap">
|
||||
<div class="day-bar">
|
||||
<div class="day-bar" id="dayBar">
|
||||
<span class="day-tag" id="dayTag"></span>
|
||||
<div class="btnrow">
|
||||
<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="speaking"></section>
|
||||
<section class="panel" id="writing"></section>
|
||||
<section class="panel" id="test"></section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -72,6 +75,26 @@
|
||||
|
||||
<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="app.js"></script>
|
||||
</body>
|
||||
|
||||
48
style.css
48
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}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user