/* app.jsx — cert selection screen on load, no "both", no in-Practice cert toggle */ const PROG_KEY_PREFIX='ccna_progress_v2_'; const CERT_KEY='ccna_cert_mode'; function defaultProg(){const ds={};(window.DOMAINS||[]).forEach(d=>ds[d.id]={answered:0,correct:0});return{totalAnswered:0,totalCorrect:0,domainStats:ds,missedIds:[],streak:0,lastStudied:null,sessionCount:0};} function loadProg(examId){try{return JSON.parse(localStorage.getItem(PROG_KEY_PREFIX+examId))||defaultProg();}catch{return defaultProg();}} function saveProg(examId,p){localStorage.setItem(PROG_KEY_PREFIX+examId,JSON.stringify(p));} function updateProg(prog,session,finalAnswers){ const p=JSON.parse(JSON.stringify(prog)); const today=new Date().toDateString();const yesterday=new Date(Date.now()-86400000).toDateString(); if(p.lastStudied!==today){p.streak=p.lastStudied===yesterday?p.streak+1:1;p.lastStudied=today;} session.questions.forEach(q=>{const ans=finalAnswers[q.id];if(!ans)return;p.totalAnswered++;if(!p.domainStats[q.d])p.domainStats[q.d]={answered:0,correct:0};p.domainStats[q.d].answered++;if(ans.correct){p.totalCorrect++;p.domainStats[q.d].correct++;p.missedIds=p.missedIds.filter(id=>id!==q.id);}else{if(!p.missedIds.includes(q.id))p.missedIds.push(q.id);}}); p.sessionCount++;return p; } function buildSession(mode, domainId, tweaks, certMode){ let pool=(window.Q||[]).filter(q=>q.exam===certMode); if(mode==='cli') pool=pool.filter(q=>q.type==='cli'); else if(mode!=='full') pool=pool.filter(q=>q.type!=='cli'); if(mode==='quick'){ const qs=pool.sort(()=>Math.random()-0.5).slice(0,10); return{questions:qs,label:'Quick Drill',timerEnabled:tweaks.timerOn,timerDuration:tweaks.timerMins}; } if(mode==='weak'){ const prog=loadProg();const missed=prog.missedIds||[];const mq=pool.filter(q=>missed.includes(q.id));const rest=pool.filter(q=>!missed.includes(q.id)).sort(()=>Math.random()-0.5);const qs=[...mq,...rest].slice(0,20);return{questions:qs.length?qs:pool.sort(()=>Math.random()-0.5).slice(0,10),label:'Weak Spot Review',timerEnabled:tweaks.timerOn,timerDuration:tweaks.timerMins}; } if(mode==='domain'){ const target=100; const domainPool=pool.filter(q=>q.d===domainId); const qs=domainPool.sort(()=>Math.random()-0.5).slice(0,target); const name=(window.DOMAINS||[]).find(d=>d.id===domainId)?.name||'Domain'; return{questions:qs.length?qs:pool.sort(()=>Math.random()-0.5).slice(0,10),label:`${name} — ${qs.length}q`,timerEnabled:tweaks.timerOn,timerDuration:tweaks.timerMins}; } if(mode==='cli'){ return{questions:pool.sort(()=>Math.random()-0.5),label:'CLI Drills',timerEnabled:false,timerDuration:0}; } return{questions:pool.sort(()=>Math.random()-0.5),label:'Full Session',timerEnabled:tweaks.timerOn,timerDuration:tweaks.timerMins}; } /* ===== CERT SELECTION SCREEN (initial choice) ===== */ function CertSelectScreen({onPick}){ const exams = window.EXAMS || []; return(
study@cyberstudy

Choose your certification track

Sets your question pool, concept topics, and progress tracking. You can switch any time by clicking the badge in the top bar.

{exams.map(e => ( ))}
); } /* ===== NOTES VIEW ===== */ function NotesView({setView,setLearnTopic}){ const notes=window.useNotes?window.useNotes():[]; const byTopic={}; notes.forEach(n=>{(byTopic[n.topicId]=byTopic[n.topicId]||{title:n.topicTitle,items:[]}).items.push(n);}); const topicIds=Object.keys(byTopic); return(
$ show notes --all
{notes.length} note{notes.length===1?'':'s'} across {topicIds.length} topic{topicIds.length===1?'':'s'} — click any topic to jump back
{notes.length===0 ?
No notes yet. Open Learn Mode, highlight a passage, click “✎ Add note.”
:topicIds.map(tid=>{const grp=byTopic[tid];return(
{setLearnTopic&&setLearnTopic(tid);setView('learn');}}>{grp.title} {grp.items.length} note{grp.items.length===1?'':'s'}
{grp.items.map(n=>(
“{n.selectedText}”
{n.note}
{new Date(n.createdAt).toLocaleDateString()}{n.updatedAt?' · edited':''}
))}
);}) }
); } /* ===== TOPBAR ===== */ function TopBar({activeTab,setView,prog,streak,certMode,onSwitchCert}){ const acc=prog.totalAnswered>0?Math.round((prog.totalCorrect/prog.totalAnswered)*100):0; return(
{[['home','Home'],['learn','Learn'],['practice','Practice'],['progress','Progress'],['final-exam','Final Exam'],['notes','Notes']].map(([v,l])=>( ))}
{streak>0&&🔥 {streak}d} {prog.totalAnswered>0&&{acc}% acc · {prog.totalAnswered}q}
); } /* ===== HOME VIEW ===== */ function HomeView({prog,setView,onStart,certMode}){ const acc=prog.totalAnswered>0?Math.round((prog.totalCorrect/prog.totalAnswered)*100):0; const mastered=Object.values(prog.domainStats).filter(s=>s.answered>0&&Math.round((s.correct/s.answered)*100)>=80).length; const pool=(window.Q||[]).filter(q=>q.exam===certMode); return(
$ show dashboard
{pool.filter(q=>q.type!=='cli').length} {window.getExamLabel?window.getExamLabel(certMode):certMode.toUpperCase()} questions loaded · 6 domains
Total Answered
{prog.totalAnswered||'—'}
Accuracy
=70?'green':'amber'}`}>{prog.totalAnswered>0?`${acc}%`:'—'}
Streak
{prog.streak>0?`${prog.streak}d`:'—'}
Domains ≥80%
{mastered}/6
Quick Actions
{pool.some(q=>q.type==='cli')&&}
Domain Overview
{(window.DOMAINS||[]).map(d=>{const s=prog.domainStats[d.id]||{answered:0,correct:0};const qs=pool.filter(q=>q.d===d.id&&q.type!=='cli').length;const progressPct=qs>0?Math.min(100,Math.round((s.answered/qs)*100)):0;return( );})}
); } /* ===== PRACTICE VIEW ===== */ function PracticeView({certMode,prog,onStart}){ const [selDomain,setSelDomain]=React.useState(null); const pool=(window.Q||[]).filter(q=>q.exam===certMode); const cliCount=pool.filter(q=>q.type==='cli').length; const weakCount=prog.missedIds.length; return(
$ configure-session
{pool.filter(q=>q.type!=='cli').length} {window.getExamLabel?window.getExamLabel(certMode):certMode.toUpperCase()} questions · {cliCount} CLI drills in pool
Session Type
{cliCount>0&&}
Domain Focus
{(window.DOMAINS||[]).map(d=>{ const qs=pool.filter(q=>q.d===d.id&&q.type!=='cli').length; const cqs=pool.filter(q=>q.d===d.id&&q.type==='cli').length; const s=prog.domainStats[d.id]||{answered:0,correct:0}; const progressPct = qs>0 ? Math.min(100, Math.round((s.answered/qs)*100)) : 0; const isSel=selDomain===d.id; return( ); })}
{selDomain&&(
)}
); } /* ===== PROGRESS VIEW ===== */ function ProgressView({prog,onStart}){ const acc=prog.totalAnswered>0?Math.round((prog.totalCorrect/prog.totalAnswered)*100):0; const missed=(window.Q||[]).filter(q=>prog.missedIds.includes(q.id)); return(
$ show progress --all
Session #{prog.sessionCount} · Last studied: {prog.lastStudied||'never'}
Domain Mastery
{(window.DOMAINS||[]).map(d=>{const s=prog.domainStats[d.id]||{answered:0,correct:0};const pct=s.answered>0?Math.round((s.correct/s.answered)*100):0;return(
{d.id} {d.name}
{s.answered>0?`${pct}% (${s.answered}q)`:'—'}
);})}
Total Answered
{prog.totalAnswered}
Overall Accuracy
=70?'green':'amber'}`}>{prog.totalAnswered>0?`${acc}%`:'—'}
Study Streak
{prog.streak>0?`${prog.streak}d`:'—'}
Need Review
{prog.missedIds.length}
Missed Questions ({missed.length})
{missed.length===0?
No missed questions yet!
:
{missed.map(q=>(
onStart('domain',q.d)}> D{q.d} {q.stem.slice(0,80)}{q.stem.length>80?'…':''}
))}
} {missed.length>0&&}
); } /* ===== TWEAKS ===== */ function AppTweaks({tweaks,setTweak,certMode,onSwitchCert}){ return(
Currently studying: {certMode==='ccnp'?'CCNP Enterprise':'CCNA 200-301'}
setTweak('timerOn',v)}/> {tweaks.timerOn&&setTweak('timerMins',v)}/>} setTweak('accent',v)} options={['green','cyan','amber']}/> setTweak('fontSize',v)} options={['sm','md','lg']}/> {if(confirm('Reset progress for this cert?')){const cert=localStorage.getItem('ccna_cert_mode')||'ccna';saveProg(cert,defaultProg());location.reload();}}}/>
); } /* ===== ROOT APP ===== */ function App(){ const[view,setView]=React.useState('home'); const[certMode,setCertModeState]=React.useState(()=>localStorage.getItem(CERT_KEY)); const[session,setSession]=React.useState(null); const[finalAnswers,setFinalAnswers]=React.useState(null); const[prog,setProg]=React.useState(()=>loadProg(localStorage.getItem(CERT_KEY)||'ccna')); // Re-load progress when cert changes so each track has its own stats React.useEffect(()=>{ if(certMode) setProg(loadProg(certMode)); },[certMode]); function pickCert(c){ if(c){localStorage.setItem(CERT_KEY,c);}else{localStorage.removeItem(CERT_KEY);} setCertModeState(c); } const{useTweaks}=window; const TWEAK_DEFAULTS=/*EDITMODE-BEGIN*/{ "timerOn": false, "timerMins": 45, "accent": "green", "fontSize": "md" }/*EDITMODE-END*/; const[tweaks,setTweak]=useTweaks(TWEAK_DEFAULTS); React.useEffect(()=>{ const root=document.documentElement; if(tweaks.accent==='cyan'){root.style.setProperty('--green','var(--cyan)');root.style.setProperty('--green-bg','var(--cyan-bg)');} else if(tweaks.accent==='amber'){root.style.setProperty('--green','var(--amber)');root.style.setProperty('--green-bg','var(--amber-bg)');} else{root.style.removeProperty('--green');root.style.removeProperty('--green-bg');} document.body.style.fontSize=tweaks.fontSize==='sm'?'13px':tweaks.fontSize==='lg'?'15px':'14px'; },[tweaks.accent,tweaks.fontSize]); // No cert chosen yet → show selection screen if(!certMode) return ; function startSession(mode,domainId){ const s=buildSession(mode,domainId,tweaks,certMode); setSession(s);setFinalAnswers(null);setView('running'); } // Records a single answered question immediately (so progress saves even if user exits mid-session) function handleAnswer(q, correct){ setProg(prev => { const newProg = JSON.parse(JSON.stringify(prev)); const today = new Date().toDateString(); const yesterday = new Date(Date.now()-86400000).toDateString(); if (newProg.lastStudied !== today) { newProg.streak = newProg.lastStudied === yesterday ? newProg.streak+1 : 1; newProg.lastStudied = today; } newProg.totalAnswered++; if (!newProg.domainStats[q.d]) newProg.domainStats[q.d] = {answered:0, correct:0}; newProg.domainStats[q.d].answered++; if (correct) { newProg.totalCorrect++; newProg.domainStats[q.d].correct++; newProg.missedIds = newProg.missedIds.filter(id => id !== q.id); } else { if (!newProg.missedIds.includes(q.id)) newProg.missedIds.push(q.id); } saveProg(certMode, newProg); return newProg; }); } function handleComplete(answers){ setProg(prev => { const np = {...prev, sessionCount: (prev.sessionCount||0)+1}; saveProg(certMode, np); return np; }); setFinalAnswers(answers);setView('results'); } function handleRetry(){ setFinalAnswers(null);setView('running'); setSession(s=>({...s,questions:[...s.questions].sort(()=>Math.random()-0.5)})); } function switchCert(){pickCert(null);setView('home');} const activeTab=view==='running'||view==='results'?'practice':view==='home'?'home':view; const renderView=()=>{ if(view==='home') return ; if(view==='practice') return ; if(view==='running'&&session) return setView('practice')}/>; if(view==='results'&&session&&finalAnswers) return setView('home')} onRetry={handleRetry}/>; if(view==='learn') return setView('home')} certMode={certMode}/>; if(view==='notes') return {window.__learnTopic=t;}}/>; if(view==='progress') return ; if(view==='final-exam') return setView('home')}/>; return ; }; return(
{if(['practice','home','learn','progress','notes','final-exam'].includes(v))setView(v);}} prog={prog} streak={prog.streak} certMode={certMode} onSwitchCert={switchCert}/>
{renderView()}
); } ReactDOM.createRoot(document.getElementById('root')).render();