from the HTML",
""
],
ans: 0,
why: "display: none hides the element AND removes its space. It's better than deleting from HTML because you can easily show it again by removing the CSS. Perfect for seasonal banners."
},
{
level: 3,
title: "Make Container Full Width on Mobile",
original: ".container {\n width: 800px;\n}",
task: "Change so the container fills the full screen width on any phone size",
expected: ".container {\n width: 100%;\n}",
opts: [
".container {\n width: 100%;\n}",
".container {\n width: mobile;\n}",
".container {\n width: auto;\n}"
],
ans: 0,
why: "Fixed pixel widths (like 800px) break on phones. Using 100% makes the container adapt to any screen size — whether it's a small Android or a large iPhone."
},
{
level: 3,
title: "Fix a Dead Home Link",
original: '
Home ',
task: "Most websites use 'index.html' as the home page. Fix this link.",
expected: '
Home ',
opts: [
'
Home ',
'
Home ',
'
Home '
],
ans: 0,
why: "The default home page for any website is almost always called index.html. If you link to home.html but the file is index.html, you'll get a 404 error (page not found)."
},
{
level: 3,
title: "Update Copyright Year",
original: "
© 2022 Oghale Limited. All rights reserved.
",
task: "Update the copyright year to 2025",
expected: "
© 2025 Oghale Limited. All rights reserved.
",
opts: [
"
© 2025 Oghale Limited. All rights reserved.
",
"
Copyright 2025 Oghale Limited. All rights reserved.
",
"
© 2025 Oghale Limited.
"
],
ans: 0,
why: "Only the year number changes. © is the HTML code for the © symbol — keep it as is. The rest of the text stays the same unless you want to edit it too."
},
{
level: 3,
title: "Upgrade a Heading Level",
original: "
Our Story ",
task: "Make this heading more prominent — change it to an h2",
expected: "
Our Story ",
opts: [
"
Our Story ",
"
Our Story",
"Our Story "
],
ans: 0,
why: "You must change BOTH the opening tag ( → ) AND the closing tag ( → ). They must always match. Also note: HTML tags are lowercase."
}
];
const CONFIDENCE = [
{
q: "Can you accidentally DELETE your live website by editing the code?",
a: "No! Editing code in a text editor just changes text on your computer. Your live website only changes when you deliberately upload or deploy new files. Always keep a backup copy before making changes.",
mantra: "Editing is safe. Uploading is when it matters."
},
{
q: "Can you BREAK a website just by READING its source code?",
a: "Absolutely not. Reading code never changes anything. You can view-source on any website, read every line, and nothing will be affected. You can only break something by saving and uploading changed files.",
mantra: "Reading is always safe. Be curious!"
},
{
q: "If your page looks wrong after editing — is it permanent?",
a: "Never! Every mistake in code can be fixed. There is no error that cannot be reversed. Use Ctrl+Z to undo. Or restore from your backup copy. Nothing in code is truly permanent.",
mantra: "Every error is a lesson, not a disaster."
},
{
q: "Do you need to understand every single line of code to edit a page?",
a: "No! You only need to understand the specific part you're changing. A mechanic doesn't rebuild an entire engine just to change a tyre. Focus on finding your text, phone number, or color — and change only that.",
mantra: "Understand what you need. Leave the rest alone."
},
{
q: "Is HTML a complicated programming language you need years to learn?",
a: "No! HTML is not really a 'programming language' — it's a markup language. It's like filling out a structured form. Once you know the tags and what they do, you can read and change HTML with confidence.",
mantra: "HTML is a readable recipe — not a secret code."
},
{
q: "What should you do if AI generates code you don't fully understand?",
a: "That's normal — even professionals do this! Focus on the parts that need YOUR specific information: your business name, phone number, prices, and colors. The rest of the structure can stay as the AI wrote it.",
mantra: "You don't need to understand everything. Just know what to change."
},
{
q: "Can someone with no tech background learn to read and edit code?",
a: "Yes — absolutely! You don't need a computer science degree. You need to practice reading code like learning to read a new language. With repetition, the patterns become familiar. Thousands of Nigerian business owners have done it.",
mantra: "If you can read English, you can learn to read HTML."
},
{
q: "What if your page looks broken on a phone but fine on laptop?",
a: "That's a CSS sizing issue — very common and very fixable! Usually it's one or two CSS lines like changing 'width: 600px' to 'width: 100%'. It's not a sign that everything is broken — it's a small, specific fix.",
mantra: "Phone layout problems always have small solutions."
},
{
q: "Is making mistakes in code something to be ashamed of?",
a: "Never! Every developer — including senior ones at Google and Amazon — makes code mistakes every single day. The skill is not avoiding mistakes. The skill is knowing how to find and fix them quickly.",
mantra: "Mistakes mean you're building. Builders celebrate fixes."
},
{
q: "Does your website need to be perfect before you can show it to customers?",
a: "No! A working page with a phone number and product info is infinitely better than a perfect page that doesn't exist yet. Launch what you have. Fix as you go. Done is better than perfect — especially for Nigerian SMBs.",
mantra: "Ship it, then improve it. Progress beats perfection."
}
];
// ===================================================
// ============= APP STATE ===========================
// ===================================================
let state = {
screen: 'home',
mode: null,
level: 1,
questions: [],
currentQ: 0,
score: 0,
answered: false,
sessionType: null, // 'quiz','error','drill','daily'
xp: 0,
streak: 0,
lastPlayDate: null,
dailyDone: false,
totalPlayed: 0,
levelName: 'Fresher'
};
// ===================================================
// ============= PERSISTENCE =========================
// ===================================================
function saveState() {
const save = {
xp: state.xp,
streak: state.streak,
lastPlayDate: state.lastPlayDate,
dailyDone: state.dailyDone,
totalPlayed: state.totalPlayed,
levelName: state.levelName
};
localStorage.setItem('tn_codelab', JSON.stringify(save));
}
function loadState() {
try {
const saved = JSON.parse(localStorage.getItem('tn_codelab') || '{}');
state.xp = saved.xp || 0;
state.streak = saved.streak || 0;
state.lastPlayDate = saved.lastPlayDate || null;
state.totalPlayed = saved.totalPlayed || 0;
state.levelName = saved.levelName || 'Fresher';
// Check streak
const today = getTodayStr();
const yesterday = getYesterdayStr();
if (saved.lastPlayDate === today) {
state.dailyDone = saved.dailyDone || false;
} else if (saved.lastPlayDate === yesterday) {
state.dailyDone = false;
} else if (saved.lastPlayDate && saved.lastPlayDate !== yesterday) {
state.streak = 0;
state.dailyDone = false;
} else {
state.dailyDone = false;
}
} catch(e) {}
}
function getTodayStr() {
return new Date().toISOString().slice(0, 10);
}
function getYesterdayStr() {
const d = new Date();
d.setDate(d.getDate() - 1);
return d.toISOString().slice(0, 10);
}
// ===================================================
// ============= HOME SCREEN =========================
// ===================================================
function updateHomeUI() {
document.getElementById('streak-val').textContent = state.streak;
document.getElementById('xp-val').textContent = state.xp;
// Level calculation
let lvl = 1, lvlName = 'Fresher';
if (state.xp >= 500) { lvl = 5; lvlName = 'Senior Dev'; }
else if (state.xp >= 300) { lvl = 4; lvlName = 'Developer'; }
else if (state.xp >= 150) { lvl = 3; lvlName = 'Builder'; }
else if (state.xp >= 60) { lvl = 2; lvlName = 'Learner'; }
document.getElementById('level-val').textContent = lvl;
document.getElementById('level-name').textContent = lvlName;
const prevName = state.levelName;
if (lvlName !== prevName && prevName !== 'Fresher') {
state.levelName = lvlName;
showLevelUp(lvlName);
} else {
state.levelName = lvlName;
}
// Daily status
if (state.dailyDone) {
document.getElementById('dc-status').textContent = '✓ Done today!';
document.getElementById('dc-status').className = 'tag tag-green';
document.getElementById('dc-title').textContent = 'Daily Complete — Well done! 🎉';
document.getElementById('dc-desc').textContent = 'Come back tomorrow to extend your streak!';
} else {
document.getElementById('dc-status').textContent = 'Not done yet';
document.getElementById('dc-status').className = 'tag tag-gold';
}
}
// ===================================================
// ============= NAVIGATION ==========================
// ===================================================
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => {
s.classList.remove('active', 'fade-in');
});
const el = document.getElementById(id + '-screen');
if (el) {
el.classList.add('active', 'fade-in');
el.scrollTop = 0;
}
state.screen = id;
}
function goHome() {
updateHomeUI();
showScreen('home');
}
function goToMode(mode) {
state.mode = mode;
if (mode === 'quiz') startQuiz(shuffle([...QUIZ]).slice(0, 10), 'quiz');
else if (mode === 'error') startErrors(shuffle([...ERRORS]).slice(0, 10), 'error');
else if (mode === 'drill') startDrills(shuffle([...DRILLS]).slice(0, 10), 'drill');
else if (mode === 'confidence') showConfidence();
}
function startLevel(level) {
state.level = level;
if (level === 1) startQuiz(shuffle(QUIZ.filter(q => q.level === 1)).slice(0, 10), 'quiz');
else if (level === 2) startErrors(shuffle(ERRORS).slice(0, 10), 'error');
else if (level === 3) startDrills(shuffle(DRILLS).slice(0, 10), 'drill');
}
function startDaily() {
if (state.dailyDone) {
showToast("Daily already done! Come back tomorrow 🔥");
return;
}
// Mix: 4 quiz + 3 error + 3 drill
const q4 = shuffle([...QUIZ]).slice(0, 4);
const e3 = shuffle([...ERRORS]).slice(0, 3);
const d3 = shuffle([...DRILLS]).slice(0, 3);
const daily = [
...q4.map(q => ({...q, _type: 'quiz'})),
...e3.map(e => ({...e, _type: 'error'})),
...d3.map(d => ({...d, _type: 'drill'}))
];
startMixed(shuffle(daily));
}
function retryMode() {
if (state.mode === 'quiz') goToMode('quiz');
else if (state.mode === 'error') goToMode('error');
else if (state.mode === 'drill') goToMode('drill');
else if (state.mode === 'daily') startDaily();
else goHome();
}
// ===================================================
// ============= QUIZ MODE ===========================
// ===================================================
let quizData = [], quizIdx = 0, quizScore = 0;
function startQuiz(qs, type) {
quizData = qs;
quizIdx = 0;
quizScore = 0;
state.sessionType = type;
document.getElementById('quiz-nav-title').textContent = type === 'daily' ? '🎯 Daily Challenge' : '🧠 Code Quiz';
showScreen('quiz');
renderQuizQ();
}
function renderQuizQ() {
const q = quizData[quizIdx];
const total = quizData.length;
const pct = Math.round(((quizIdx + 1) / total) * 100);
document.getElementById('quiz-qnum').textContent = `Question ${quizIdx+1} of ${total}`;
document.getElementById('quiz-score-live').textContent = `Score: ${quizScore}`;
document.getElementById('quiz-prog').style.width = pct + '%';
document.getElementById('quiz-next-btn').classList.add('hidden');
const optLetters = ['A', 'B', 'C'];
let html = `
${q.category || 'HTML & CSS'}
${q.q}
${q.code ? `
${escHtml(q.code)}
` : ''}
`;
q.opts.forEach((opt, i) => {
html += `
${optLetters[i]}
${escHtml(opt)}
`;
});
html += `
`;
html += `
`;
document.getElementById('quiz-content').innerHTML = html;
state.answered = false;
}
function answerQuiz(idx) {
if (state.answered) return;
state.answered = true;
const q = quizData[quizIdx];
const correct = idx === q.ans;
document.querySelectorAll('.option-btn').forEach((btn, i) => {
btn.disabled = true;
if (i === q.ans) btn.classList.add('correct');
if (i === idx && !correct) btn.classList.add('wrong');
});
if (correct) {
quizScore++;
showToast('✓ Correct! +10 XP');
addXP(10);
} else {
showToast('✗ Wrong — check the explanation below');
}
document.getElementById('exp-card').classList.add('show');
document.getElementById('quiz-next-btn').classList.remove('hidden');
if (quizIdx === quizData.length - 1) {
document.getElementById('quiz-next-btn').textContent = 'See Results 🎉';
}
}
function nextQuestion() {
quizIdx++;
if (quizIdx >= quizData.length) {
showResults(quizScore, quizData.length, 'Quiz', state.sessionType);
} else {
renderQuizQ();
document.getElementById('quiz-content').scrollTop = 0;
}
}
// ===================================================
// ============= ERROR HUNT MODE =====================
// ===================================================
let errorData = [], errorIdx = 0, errorScore = 0;
function startErrors(es, type) {
errorData = es;
errorIdx = 0;
errorScore = 0;
state.sessionType = type || 'error';
showScreen('error');
renderErrorQ();
}
function renderErrorQ() {
const e = errorData[errorIdx];
const total = errorData.length;
const pct = Math.round(((errorIdx + 1) / total) * 100);
document.getElementById('error-qnum').textContent = `Bug ${errorIdx+1} of ${total}`;
document.getElementById('error-score-live').textContent = `Score: ${errorScore}`;
document.getElementById('error-prog').style.width = pct + '%';
document.getElementById('error-next-btn').classList.add('hidden');
const optLetters = ['A', 'B', 'C'];
let html = `
🔴 Find the Bug
${escHtml(e.intro)}
// BROKEN CODE — what's wrong?
${escHtml(e.broken)}
${escHtml(e.q)}
`;
e.opts.forEach((opt, i) => {
html += `${optLetters[i]} ${escHtml(opt)} `;
});
html += `
`;
html += `
💡
${escHtml(e.exp)}
✅ FIXED CODE:
${escHtml(e.fix)}
`;
document.getElementById('error-content').innerHTML = html;
state.answered = false;
}
function answerError(idx) {
if (state.answered) return;
state.answered = true;
const e = errorData[errorIdx];
const correct = idx === e.ans;
document.querySelectorAll('[id^="err-opt-"]').forEach((btn, i) => {
btn.disabled = true;
if (i === e.ans) btn.classList.add('correct');
if (i === idx && !correct) btn.classList.add('wrong');
});
if (correct) {
errorScore++;
showToast('✓ Bug found! +10 XP');
addXP(10);
} else {
showToast('✗ Not quite — read the fix below');
}
document.getElementById('fix-reveal').classList.add('show');
document.getElementById('error-next-btn').classList.remove('hidden');
if (errorIdx === errorData.length - 1) {
document.getElementById('error-next-btn').textContent = 'See Results 🎉';
}
}
function nextErrorQ() {
errorIdx++;
if (errorIdx >= errorData.length) {
showResults(errorScore, errorData.length, 'Error Hunt', state.sessionType);
} else {
renderErrorQ();
document.getElementById('error-content').scrollTop = 0;
}
}
// ===================================================
// ============= DRILL MODE ==========================
// ===================================================
let drillData = [], drillIdx = 0, drillScore = 0;
function startDrills(ds, type) {
drillData = ds;
drillIdx = 0;
drillScore = 0;
state.sessionType = type || 'drill';
showScreen('drill');
renderDrillQ();
}
function renderDrillQ() {
const d = drillData[drillIdx];
const total = drillData.length;
const pct = Math.round(((drillIdx + 1) / total) * 100);
document.getElementById('drill-qnum').textContent = `Drill ${drillIdx+1} of ${total}`;
document.getElementById('drill-score-live').textContent = `Score: ${drillScore}`;
document.getElementById('drill-prog').style.width = pct + '%';
document.getElementById('drill-next-btn').classList.add('hidden');
const optLetters = ['A', 'B', 'C'];
let html = `
✏️ ${escHtml(d.title)}
// ORIGINAL CODE:
${escHtml(d.original)}
Your Task
${escHtml(d.task)}
Which is the correct result?
`;
d.opts.forEach((opt, i) => {
html += `${optLetters[i]} ${escHtml(opt)} `;
});
html += `
`;
html += `
✅ Correct Result:
${escHtml(d.expected)}
${escHtml(d.why)}
`;
document.getElementById('drill-content').innerHTML = html;
state.answered = false;
}
function answerDrill(idx) {
if (state.answered) return;
state.answered = true;
const d = drillData[drillIdx];
const correct = idx === d.ans;
document.querySelectorAll('[id^="drill-opt-"]').forEach((btn, i) => {
btn.disabled = true;
if (i === d.ans) btn.classList.add('correct');
if (i === idx && !correct) btn.classList.add('wrong');
});
if (correct) {
drillScore++;
showToast('✓ Perfect edit! +10 XP');
addXP(10);
} else {
showToast('✗ Not quite right — see correct answer below');
}
document.getElementById('drill-result').classList.add('show');
document.getElementById('drill-next-btn').classList.remove('hidden');
if (drillIdx === drillData.length - 1) {
document.getElementById('drill-next-btn').textContent = 'See Results 🎉';
}
}
function nextDrillQ() {
drillIdx++;
if (drillIdx >= drillData.length) {
showResults(drillScore, drillData.length, 'Edit Drills', state.sessionType);
} else {
renderDrillQ();
document.getElementById('drill-content').scrollTop = 0;
}
}
// ===================================================
// ============= MIXED / DAILY MODE ==================
// ===================================================
let mixedData = [], mixedIdx = 0, mixedScore = 0;
function startMixed(items) {
state.mode = 'daily';
state.sessionType = 'daily';
// Use quiz mode with unified handler
// Split into appropriate rendering
mixedData = items;
mixedIdx = 0;
mixedScore = 0;
startMixedQ();
}
function startMixedQ() {
const item = mixedData[mixedIdx];
const type = item._type || 'quiz';
if (type === 'quiz') {
quizData = mixedData.filter(i => i._type === 'quiz');
quizIdx = mixedData.slice(0, mixedIdx+1).filter(i => i._type === 'quiz').length - 1;
// Actually, let's just use per-item rendering
}
renderMixedItem();
}
function renderMixedItem() {
const item = mixedData[mixedIdx];
const type = item._type;
const total = mixedData.length;
const pct = Math.round(((mixedIdx + 1) / total) * 100);
document.getElementById('quiz-nav-title').textContent = '🎯 Daily Challenge';
document.getElementById('quiz-qnum').textContent = `${mixedIdx+1} of ${total}`;
document.getElementById('quiz-score-live').textContent = `Score: ${mixedScore}`;
document.getElementById('quiz-prog').style.width = pct + '%';
document.getElementById('quiz-next-btn').classList.add('hidden');
showScreen('quiz');
let html = '';
const optLetters = ['A', 'B', 'C'];
if (type === 'quiz') {
html = `
${item.category || 'HTML & CSS'} Quiz
${escHtml(item.q)}
${item.code ? `
${escHtml(item.code)}
` : ''}
`;
item.opts.forEach((opt, i) => {
html += `${optLetters[i] ? `${optLetters[i]} ` : ''}${escHtml(opt)} `;
});
html += `
`;
} else if (type === 'error') {
html = `
🔴 Find the Bug Error Hunt
${escHtml(item.intro)}
${escHtml(item.broken)}
${escHtml(item.q)}
`;
item.opts.forEach((opt, i) => {
html += `${optLetters[i]} ${escHtml(opt)} `;
});
html += `
`;
html += `
💡
${escHtml(item.exp)}
✅ FIXED:
${escHtml(item.fix)}
`;
} else if (type === 'drill') {
html = `
✏️ ${escHtml(item.title)} Edit Drill
${escHtml(item.original)}
Task
${escHtml(item.task)}
Which is the correct result?
`;
item.opts.forEach((opt, i) => {
html += `${optLetters[i]} ${escHtml(opt)} `;
});
html += `
`;
html += `
✅ Correct:
${escHtml(item.expected)}
${escHtml(item.why)}
`;
}
document.getElementById('quiz-content').innerHTML = html;
state.answered = false;
}
function answerMixed(idx) {
if (state.answered) return;
state.answered = true;
const item = mixedData[mixedIdx];
const correct = idx === item.ans;
document.querySelectorAll('.option-btn').forEach((btn, i) => {
btn.disabled = true;
if (i === item.ans) btn.classList.add('correct');
if (i === idx && !correct) btn.classList.add('wrong');
});
if (correct) {
mixedScore++;
showToast('✓ Correct! +10 XP');
addXP(10);
} else {
showToast('✗ Wrong — see explanation');
}
const expCard = document.getElementById('exp-card');
if (expCard) expCard.classList.add('show');
document.getElementById('quiz-next-btn').classList.remove('hidden');
if (mixedIdx === mixedData.length - 1) {
document.getElementById('quiz-next-btn').textContent = 'See Results 🎉';
}
}
// Override nextQuestion to handle mixed
const _origNextQ = nextQuestion;
function nextQuestion() {
if (state.mode === 'daily') {
mixedIdx++;
if (mixedIdx >= mixedData.length) {
// Daily complete!
state.dailyDone = true;
updateStreak();
addXP(50); // bonus
showResults(mixedScore, mixedData.length, 'Daily Challenge', 'daily');
} else {
renderMixedItem();
document.getElementById('quiz-content').scrollTop = 0;
}
} else {
quizIdx++;
if (quizIdx >= quizData.length) {
showResults(quizScore, quizData.length, 'Quiz', state.sessionType);
} else {
renderQuizQ();
document.getElementById('quiz-content').scrollTop = 0;
}
}
}
// ===================================================
// ============= CONFIDENCE MODE =====================
// ===================================================
function showConfidence() {
showScreen('confidence');
let html = '';
CONFIDENCE.forEach((c, i) => {
html += `
Mindset ${i+1} of ${CONFIDENCE.length}
${escHtml(c.q)}
👆 Tap to reveal the truth
THE TRUTH:
${escHtml(c.a)}
"${escHtml(c.mantra)}"
`;
});
document.getElementById('confidence-cards').innerHTML = html;
addXP(5);
}
function toggleConfidence(i) {
const ans = document.getElementById(`cc-ans-${i}`);
const hint = document.querySelector(`#cc-${i} .cc-tap-hint`);
if (ans.classList.contains('show')) {
ans.classList.remove('show');
hint.textContent = '👆 Tap to reveal the truth';
} else {
ans.classList.add('show');
hint.textContent = '👆 Tap to hide';
}
}
// ===================================================
// ============= RESULTS SCREEN ======================
// ===================================================
function showResults(score, total, modeName, type) {
const pct = Math.round((score / total) * 100);
let trophy = '🥈', msg = 'Good effort! Keep practicing.', stars = '⭐⭐';
if (pct === 100) { trophy = '🏆'; msg = 'Perfect score! You are a Code Champion!'; stars = '⭐⭐⭐'; }
else if (pct >= 80) { trophy = '🥇'; msg = 'Excellent! You really understand this!'; stars = '⭐⭐⭐'; }
else if (pct >= 60) { trophy = '🥈'; msg = 'Good work! A little more practice and you\'ll nail it.'; stars = '⭐⭐'; }
else { trophy = '📚'; msg = 'Keep going! Every attempt makes you better.'; stars = '⭐'; }
const baseXP = score * 10;
const bonusXP = type === 'daily' ? 50 : 0;
const totalXP = baseXP + bonusXP;
document.getElementById('res-trophy').textContent = trophy;
document.getElementById('res-stars').textContent = stars;
document.getElementById('res-grade').textContent = pct + '%';
document.getElementById('res-msg').textContent = msg;
document.getElementById('res-correct').textContent = score;
document.getElementById('res-wrong').textContent = total - score;
document.getElementById('res-total').textContent = total;
document.getElementById('res-xp').textContent = `+${totalXP} XP`;
state.totalPlayed++;
saveState();
if (pct >= 80) fireConfetti();
showScreen('results');
}
// ===================================================
// ============= XP & STREAK =========================
// ===================================================
function addXP(amount) {
const prev = state.xp;
state.xp += amount;
const prevLevel = getLevel(prev);
const newLevel = getLevel(state.xp);
if (newLevel > prevLevel) {
const names = ['', 'Fresher', 'Learner', 'Builder', 'Developer', 'Senior Dev'];
showLevelUp(names[newLevel] || 'Master');
}
document.getElementById('xp-val').textContent = state.xp;
saveState();
}
function getLevel(xp) {
if (xp >= 500) return 5;
if (xp >= 300) return 4;
if (xp >= 150) return 3;
if (xp >= 60) return 2;
return 1;
}
function updateStreak() {
const today = getTodayStr();
const yesterday = getYesterdayStr();
if (state.lastPlayDate === today) return;
if (state.lastPlayDate === yesterday || !state.lastPlayDate) {
state.streak++;
} else {
state.streak = 1;
}
state.lastPlayDate = today;
document.getElementById('streak-val').textContent = state.streak;
const icon = document.getElementById('streak-icon');
icon.classList.add('pop');
setTimeout(() => icon.classList.remove('pop'), 600);
saveState();
}
// ===================================================
// ============= LEVEL UP OVERLAY ====================
// ===================================================
function showLevelUp(name) {
document.getElementById('lu-title').textContent = '🎉 Level Up!';
document.getElementById('lu-sub').textContent = `You are now a ${name}! Keep building!`;
document.getElementById('levelup-overlay').classList.add('show');
}
function closeLevelUp() {
document.getElementById('levelup-overlay').classList.remove('show');
}
// ===================================================
// ============= CONFETTI ============================
// ===================================================
function fireConfetti() {
const colors = ['#006B3F', '#F5A623', '#60A5FA', '#F87171', '#4ADE80', '#FCD34D'];
for (let i = 0; i < 20; i++) {
setTimeout(() => {
const el = document.createElement('div');
el.className = 'confetti-piece';
el.style.cssText = `
left: ${Math.random() * 100}vw;
top: ${60 + Math.random() * 30}vh;
background: ${colors[Math.floor(Math.random() * colors.length)]};
animation-duration: ${0.6 + Math.random() * 0.6}s;
animation-delay: ${Math.random() * 0.3}s;
transform: rotate(${Math.random() * 360}deg);
`;
document.body.appendChild(el);
setTimeout(() => el.remove(), 1200);
}, i * 40);
}
}
// ===================================================
// ============= TOAST ===============================
// ===================================================
let toastTimer;
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('show'), 2000);
}
// ===================================================
// ============= PWA =================================
// ===================================================
let installPrompt = null;
window.addEventListener('beforeinstallprompt', e => {
e.preventDefault();
installPrompt = e;
setTimeout(() => {
document.getElementById('install-banner').classList.add('show');
}, 3000);
});
function installApp() {
if (!installPrompt) return;
installPrompt.prompt();
installPrompt.userChoice.then(r => {
if (r.outcome === 'accepted') showToast('App installed! 🎉');
dismissInstall();
});
}
function dismissInstall() {
document.getElementById('install-banner').classList.remove('show');
}
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js').catch(() => {});
});
}
// ===================================================
// ============= UTILITIES ===========================
// ===================================================
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function escHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// ===================================================
// ============= INIT ================================
// ===================================================
loadState();
updateHomeUI();
// Check if launched via shortcut
const params = new URLSearchParams(window.location.search);
if (params.get('mode') === 'daily') {
setTimeout(startDaily, 300);
}