/* 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 => (
onPick(e.id)}>
{e.label}
{e.sub}
{e.description}
))}
);
}
/* ===== 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':''}
{setLearnTopic&&setLearnTopic(n.topicId);setView('learn');}}>Open topic →
{if(confirm('Delete this note?'))window.deleteNote(n.id);}}>Delete
))}
);})
}
);
}
/* ===== TOPBAR ===== */
function TopBar({activeTab,setView,prog,streak,certMode,onSwitchCert}){
const acc=prog.totalAnswered>0?Math.round((prog.totalCorrect/prog.totalAnswered)*100):0;
return(
{certMode?(window.getExamLabel?window.getExamLabel(certMode):certMode).toLowerCase():'—'} @ cyberstudy
▌
{[['home','Home'],['learn','Learn'],['practice','Practice'],['progress','Progress'],['final-exam','Final Exam'],['notes','Notes']].map(([v,l])=>(
setView(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`:'—'}
Quick Actions
setView('learn')}>📖
Learn Mode
Concept pages, diagrams, NetworkChuck videos
{pool.some(q=>q.type==='cli')&&
onStart('cli')}>💻
CLI Drills
Practice Cisco IOS commands in a real terminal
}
setView('progress')}>📊
Progress
{prog.missedIds.length>0?`${prog.missedIds.length} missed questions to review`:'Track mastery by domain'}
setView('notes')}>✎
Notes
Your highlights and margin notes from Learn mode
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(
onStart&&onStart('domain',d.id)} style={{font:'inherit',color:'inherit'}}>
{d.id}
{d.name}
{s.answered>0?`${s.answered}/${qs}`:`${qs}q`}
);})}
);
}
/* ===== 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
onStart('quick')}>
⚡
Quick Drill
10 random questions
onStart('weak')} style={{borderColor:weakCount>0?'var(--amber)':''}}>
🎯
Weak Spot Review
{weakCount>0?`${weakCount} missed to tackle`:'Practice areas you struggle with'}
{cliCount>0&&
onStart('cli')}>
💻
CLI Drills
{cliCount} Cisco IOS simulations
}
onStart('full')}>
📋
Full Session
All {pool.length} questions, randomized
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(
setSelDomain(isSel?null:d.id)}>
{d.id}
{d.name}{qs}q{cqs>0?` · ${cqs} CLI`:''}
{s.answered>0?`${s.answered}/${qs}`:''}
);
})}
{selDomain&&(
{onStart('domain',selDomain);setSelDomain(null);}}>
Start Domain {selDomain} →
setSelDomain(null)}>Cancel
)}
);
}
/* ===== 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&&
onStart('weak')}>Review Missed → }
);
}
/* ===== 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( );