diff --git a/DevPath/CHANGELOG.md b/DevPath/CHANGELOG.md index f775559a..95f8b9bf 100644 --- a/DevPath/CHANGELOG.md +++ b/DevPath/CHANGELOG.md @@ -17,4 +17,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed -- Fixed an issue where skill chips on the homepage were unclickable due to JavaScript syntax errors \ No newline at end of file +- Fixed an issue where skill chips on the homepage were unclickable due to JavaScript syntax errors diff --git a/DevPath/static/script.js b/DevPath/static/script.js index 1496e90f..c03b83c9 100644 --- a/DevPath/static/script.js +++ b/DevPath/static/script.js @@ -1,6 +1,7 @@ // script.js — DevPath client-side logic // // Responsibilities: +// - Dark mode toggle // - Mobile navigation toggle // - Skill chip manager (add/remove skills) // - Form validation with per-field error messages @@ -8,6 +9,169 @@ // - Result card rendering // - Code viewer panel (detail page) + +// ============================================================ +// THEME ENGINE +// ============================================================ +// The theme system works in three parts: +// +// Part A — Anti-FOUC inline script (in of each template): +// Sets html[data-theme] synchronously before the stylesheet is +// evaluated, so the browser paints the correct colours on frame 1. +// +// Part B — initTheme() (runs immediately below): +// Syncs the toggle button aria-pressed + aria-label with the +// already-applied theme. Adds the "theme-ready" class on the +// next animation frame so CSS transitions become active only +// AFTER the initial paint (preventing a colour transition flash +// when the page first loads). +// +// Part C — applyTheme(theme) (called on button click): +// The single source of truth for all theme changes. Updates +// data-theme, localStorage, aria-pressed, aria-label, and an +// aria-live region so screen readers announce the change. +// ============================================================ + +(function () { + + // ---- Part B: sync button state once DOM is ready ---------- + function initTheme() { + var html = document.documentElement; + var theme = html.dataset.theme || "light"; + + // Sync every toggle button on the page (desktop + mobile versions) + document.querySelectorAll(".theme-toggle").forEach(function (btn) { + var isDark = theme === "dark"; + // aria-pressed = true when dark mode is ON + btn.setAttribute("aria-pressed", isDark ? "true" : "false"); + // aria-label describes what clicking WILL do (not what IS active), + // which is the recommended accessible pattern for toggle buttons. + btn.setAttribute("aria-label", + isDark ? "Switch to light mode" : "Switch to dark mode" + ); + }); + + // Add .theme-ready on the NEXT frame so CSS transitions are + // suppressed during the initial render (avoids colour flash). + requestAnimationFrame(function () { + html.classList.add("theme-ready"); + }); + } + + // ---- Part C: apply a theme change ------------------------- + function applyTheme(theme) { + var html = document.documentElement; + var isDark = theme === "dark"; + + // 1. Apply via data attribute — CSS [data-theme="dark"] picks this up + html.dataset.theme = theme; + + // 2. Persist the user's choice across sessions + try { localStorage.setItem("theme", theme); } catch (e) { /* private browsing may block */ } + + // 3. Update every toggle button's accessible state + document.querySelectorAll(".theme-toggle").forEach(function (btn) { + btn.setAttribute("aria-pressed", isDark ? "true" : "false"); + btn.setAttribute("aria-label", + isDark ? "Switch to light mode" : "Switch to dark mode" + ); + }); + + // 4. Announce the change to screen readers via a visually-hidden + // aria-live="polite" region injected once into the DOM. + var liveRegion = document.getElementById("theme-announce"); + if (!liveRegion) { + liveRegion = document.createElement("span"); + liveRegion.id = "theme-announce"; + // Visually hidden but readable by screen readers + liveRegion.setAttribute("role", "status"); + liveRegion.setAttribute("aria-live", "polite"); + liveRegion.style.cssText = + "position:absolute;width:1px;height:1px;padding:0;overflow:hidden;" + + "clip:rect(0,0,0,0);white-space:nowrap;border:0;"; + document.body.appendChild(liveRegion); + } + liveRegion.textContent = isDark ? "Dark mode enabled." : "Light mode enabled."; + } + + + document.addEventListener("click", function (evt) { + var btn = evt.target.closest(".theme-toggle"); + if (!btn) return; + var current = document.documentElement.dataset.theme || "light"; + applyTheme(current === "dark" ? "light" : "dark"); + }); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initTheme); + } else { + initTheme(); + } + +}()); + + +// ============================================================ +// Dark Mode Toggle & Synchronization +// ============================================================ +// UX Behavior Design Note: +// 1. System Preference Sync: By default, the application respects the OS dark/light mode settings +// (using matchMedia("(prefers-color-scheme: dark)")). +// 2. Manual Override (Intentional UX Pattern): Once a user explicitly chooses a theme by clicking the toggle, +// their manual preference is cached in localStorage. This manual choice intentionally takes precedence +// over the system preferences to provide a stable, consistent theme across sessions. +// 3. System Re-sync: If the user wishes to revert back to system tracking, they can clear their browser data/localStorage. +// The media query listener will automatically resume tracking system preferences when no localStorage key exists. +(function initTheme() { + var toggle = document.getElementById("theme-toggle"); + var html = document.documentElement; + var sunIcon = toggle && toggle.querySelector(".theme-toggle-sun"); + var moonIcon = toggle && toggle.querySelector(".theme-toggle-moon"); + + function getPreferredTheme() { + var saved = localStorage.getItem("theme"); + if (saved) return saved; + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + + function setTheme(theme) { + html.setAttribute("data-theme", theme); + localStorage.setItem("theme", theme); + if (toggle) { + // Dynamic accessibility tracking using aria-pressed (true if dark mode is active) + toggle.setAttribute("aria-pressed", theme === "dark" ? "true" : "false"); + if (sunIcon && moonIcon) { + if (theme === "dark") { + sunIcon.style.display = "none"; + moonIcon.style.display = "inline"; + } else { + sunIcon.style.display = "inline"; + moonIcon.style.display = "none"; + } + } + } + } + + // Active theme is already initialized in to prevent Flash of Unstyled Content (FOUC). + // We sync buttons and accessibility attributes based on the current state. + var activeTheme = html.getAttribute("data-theme") || getPreferredTheme(); + setTheme(activeTheme); + + if (toggle) { + toggle.addEventListener("click", function () { + var current = html.getAttribute("data-theme") || "light"; + setTheme(current === "dark" ? "light" : "dark"); + }); + } + + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function (e) { + // Only sync dynamic system changes if no manual preference currently overrides it. + if (!localStorage.getItem("theme")) { + setTheme(e.matches ? "dark" : "light"); + } + }); +})(); + // ============================================================ // Detect which page we are on // ============================================================ @@ -22,13 +186,295 @@ var fetchBtn = document.getElementById('btn-fetch-github'); var githubInput = document.getElementById('github-username'); var errorMsg = document.getElementById('github-modal-error'); +var STORAGE_KEY = "devpathUserProgress"; +var progress = { + searches: 0, + projectViews: 0, + codeOpens: 0, + completions: 0, + points: 0, + viewedProjects: [], + completedProjects: [], + badges: { + first_search: false, + project_explorer: false, + code_starter: false, + completionist: false, + roadmap_runner: false + }, + achievements: [], + bestScore: 0 +}; + +var achievementToast = null; +var achievementToastTimeout = null; + +function loadProgressState() { + try { + var raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return; + var saved = JSON.parse(raw); + if (saved && typeof saved === "object") { + progress = Object.assign(progress, saved); + progress.viewedProjects = Array.isArray(saved.viewedProjects) ? saved.viewedProjects : []; + progress.completedProjects = Array.isArray(saved.completedProjects) ? saved.completedProjects : []; + progress.achievements = Array.isArray(saved.achievements) ? saved.achievements : []; + progress.badges = Object.assign(progress.badges, saved.badges || {}); + } + } catch (err) { + console.warn("Unable to load progress state", err); + } +} + +function saveProgressState() { + try { + progress.bestScore = Math.max(progress.bestScore, progress.points); + localStorage.setItem(STORAGE_KEY, JSON.stringify(progress)); + } catch (err) { + console.warn("Unable to save progress state", err); + } +} + +function resetProgressState() { + progress = { + searches: 0, + projectViews: 0, + codeOpens: 0, + completions: 0, + points: 0, + viewedProjects: [], + completedProjects: [], + badges: { + first_search: false, + project_explorer: false, + code_starter: false, + completionist: false, + roadmap_runner: false + }, + achievements: [], + bestScore: 0 + }; + saveProgressState(); + updateProfileWidgets(); + showAchievementToast("Progress reset", "Your local profile has been cleared."); +} + +function computeProgressPoints() { + progress.points = progress.searches * 5 + progress.projectViews * 10 + progress.codeOpens * 15 + progress.completions * 30; +} + +function addAchievement(title, description) { + if (!title || !description) return; + var exists = progress.achievements.some(function (achievement) { return achievement.title === title; }); + if (exists) return; + + progress.achievements.unshift({ + title: title, + description: description, + date: new Date().toLocaleDateString() + }); + + if (progress.achievements.length > 5) { + progress.achievements.pop(); + } +} + +function showAchievementToast(title, detail) { + if (!achievementToast) { + achievementToast = document.getElementById("achievement-toast"); + } + if (!achievementToast) return; + + achievementToast.innerHTML = "" + title + "" + + "" + detail + ""; + achievementToast.classList.add("show"); + + clearTimeout(achievementToastTimeout); + achievementToastTimeout = setTimeout(function () { + achievementToast.classList.remove("show"); + }, 3200); +} + +function updateProfileWidgets() { + var pointsEl = document.getElementById("progress-points"); + var statsEl = document.getElementById("progress-stats"); + var meterFill = document.getElementById("progress-meter-fill"); + var badgesEl = document.getElementById("progress-badges"); + var achievementList = document.getElementById("achievement-list"); + var leaderboardList = document.getElementById("leaderboard-list"); + var historyList = document.getElementById("completed-history-list"); + var completionBtn = document.getElementById("btn-mark-complete"); + + if (pointsEl) pointsEl.textContent = progress.points; + if (statsEl) { + statsEl.innerHTML = + "
  • Searches" + progress.searches + "
  • " + + "
  • Projects Viewed" + progress.projectViews + "
  • " + + "
  • Code Opens" + progress.codeOpens + "
  • " + + "
  • Projects Completed" + progress.completions + "
  • "; + } + if (meterFill) { + var percentage = Math.min(100, Math.round((progress.points / 250) * 100)); + meterFill.style.width = percentage + "%"; + meterFill.setAttribute("aria-valuenow", percentage); + meterFill.textContent = percentage + "%"; + } + + if (badgesEl) { + var badgeDefs = [ + {id: "first_search", label: "First Search", detail: "Run your first project search."}, + {id: "project_explorer", label: "Project Explorer", detail: "View a project detail page."}, + {id: "code_starter", label: "Code Starter", detail: "Open starter code."}, + {id: "completionist", label: "Completionist", detail: "Mark a project complete."}, + {id: "roadmap_runner", label: "Roadmap Runner", detail: "Search five times."} + ]; + badgesEl.innerHTML = badgeDefs.map(function (badge) { + var isUnlocked = progress.badges[badge.id]; + return "
  • " + + "" + (isUnlocked ? "✓" : "☆") + "" + + "" + badge.label + "" + + "
  • "; + }).join(""); + } + + if (achievementList) { + if (progress.achievements.length === 0) { + achievementList.innerHTML = "
  • No achievements yet. Use DevPath and unlock the first badge.
  • "; + } else { + achievementList.innerHTML = progress.achievements.map(function (achievement) { + return "
  • " + achievement.title + "" + + "" + achievement.description + "" + + "" + achievement.date + "
  • "; + }).join(""); + } + } + + if (leaderboardList) { + var ranked = getLeaderboardEntries(); + leaderboardList.innerHTML = ranked.map(function (entry, index) { + return "
  • " + (index + 1) + ". " + entry.name + "" + + "" + entry.points + " pts
  • "; + }).join(""); + } + + if (historyList) { + if (progress.completedProjects.length === 0) { + historyList.innerHTML = "
  • No completed projects yet. Mark one complete from a project page.
  • "; + } else { + historyList.innerHTML = progress.completedProjects.slice(0, 5).map(function (item) { + var title = item && typeof item === "object" ? item.title : "Project " + item; + return "
  • " + title + "Completed
  • "; + }).join(""); + } + } + + if (completionBtn) { + var completed = projectIsCompleted(PROJECT_ID); + completionBtn.textContent = completed ? "Project Completed" : "Mark Project Complete"; + completionBtn.disabled = completed; + } +} + +function projectIsCompleted(projectId) { + if (!projectId) return false; + return progress.completedProjects.some(function (item) { + var id = item && typeof item === "object" ? item.id : item; + return id === projectId; + }); +} + +function getLeaderboardEntries() { + var entries = [ + { name: "Ava", points: 245 }, + { name: "Kai", points: 192 }, + { name: "Sam", points: 176 }, + { name: "You", points: progress.points } + ]; + return entries.sort(function (a, b) { return b.points - a.points; }).slice(0, 5); +} + +function tryUnlockBadges() { + if (progress.searches >= 1) { + unlockBadge("first_search", "First Search", "You used DevPath to find your first project."); + } + if (progress.projectViews >= 1) { + unlockBadge("project_explorer", "Project Explorer", "You viewed a project detail."); + } + if (progress.codeOpens >= 1) { + unlockBadge("code_starter", "Code Starter", "You opened starter code."); + } + if (progress.completions >= 1) { + unlockBadge("completionist", "Completionist", "You marked a project complete."); + } + if (progress.searches >= 5) { + unlockBadge("roadmap_runner", "Roadmap Runner", "You searched five times."); + } +} + +function unlockBadge(id, title, detail) { + if (progress.badges[id]) return; + progress.badges[id] = true; + addAchievement(title, detail); + showAchievementToast("Badge unlocked", title + " — " + detail); + saveProgressState(); + updateProfileWidgets(); +} + +function recordSearch() { + progress.searches += 1; + computeProgressPoints(); + tryUnlockBadges(); + saveProgressState(); + updateProfileWidgets(); +} + +function recordProjectView() { + if (typeof PROJECT_ID === "undefined") return; + if (progress.viewedProjects.indexOf(PROJECT_ID) !== -1) return; + progress.viewedProjects.push(PROJECT_ID); + progress.projectViews = progress.viewedProjects.length; + computeProgressPoints(); + tryUnlockBadges(); + saveProgressState(); + updateProfileWidgets(); +} + +function recordCodeOpen() { + progress.codeOpens += 1; + computeProgressPoints(); + tryUnlockBadges(); + saveProgressState(); + updateProfileWidgets(); +} + +function recordCompletion(projectId, projectTitle) { + if (!projectId) return; + if (projectIsCompleted(projectId)) { + showAchievementToast("Already completed", "You've already marked this project complete."); + return; + } + progress.completedProjects.push({ + id: projectId, + title: projectTitle || "Project " + projectId + }); + progress.completions = progress.completedProjects.length; + computeProgressPoints(); + addAchievement("Completionist", "You finished a project and earned completion points."); + tryUnlockBadges(); + saveProgressState(); + updateProfileWidgets(); +} + +loadProgressState(); +updateProfileWidgets(); + // ============================================================ // Mobile navigation toggle (runs on all pages) // ============================================================ (function initMobileNav() { var toggle = document.getElementById("nav-mobile-toggle"); //hamburger button - var menu = document.getElementById("nav-mobile-menu"); //dropdown menu + var menu = document.getElementById("nav-mobile-menu"); //dropdown menu // Nothing to do if the nav isn't on this page, just bail out if (!toggle || !menu) return; @@ -37,18 +483,29 @@ var errorMsg = document.getElementById('github-modal-error'); // classList.toggle returns true if class was added, false if removed var isOpen = menu.classList.toggle("open"); toggle.classList.toggle("open", isOpen); + // aria-expanded reflects whether the controlled menu is expanded + toggle.setAttribute("aria-expanded", isOpen ? "true" : "false"); // Keep aria-expanded in sync so screen readers know if menu is open or closed toggle.setAttribute("aria-expanded", isOpen); }); // Close menu when any mobile link is clicked - var mobileLinks = menu.querySelectorAll(".nav-mobile-link"); - for (var i = 0; i < mobileLinks.length; i++) { - mobileLinks[i].addEventListener("click", function () { - menu.classList.remove("open"); + menu.querySelectorAll(".nav-mobile-link").forEach(function (link) { + link.addEventListener("click", function () { + menu.classList.remove("open"); toggle.classList.remove("open"); + // FIX: reset aria-expanded when menu closes via link click + toggle.setAttribute("aria-expanded", "false"); }); - } + }); + + window.addEventListener("resize", function () { + if (window.innerWidth >= 640) { + menu.classList.remove("open"); + toggle.classList.remove("open"); + toggle.setAttribute("aria-expanded", "false"); + } + }); })(); @@ -61,13 +518,13 @@ if (isIndexPage) { // grabbing all the elements we'll need so we're not calling getElementById over and over again throughout the code var form = document.getElementById("recommend-form"); var submitBtn = document.getElementById("submit-btn"); - var btnLabel = document.getElementById("btn-label"); // "get recommendations" text - var btnLoading = document.getElementById("btn-loading"); // spinner icon inside the button - var resultsSection = document.getElementById("results-section"); - var resultsGrid = document.getElementById("results-grid"); - var resultsLoadingEl = document.getElementById("results-loading"); // "Loading..." text in the results - var resultsEmptyEl = document.getElementById("results-empty"); - var emptyMessageEl = document.getElementById("empty-message"); + var btnLabel = document.getElementById("btn-label"); // "get recommendations" text + var btnLoading = document.getElementById("btn-loading"); // spinner icon inside the button + var resultsSection = document.getElementById("results-section"); + var resultsGrid = document.getElementById("results-grid"); + var resultsLoadingEl = document.getElementById("results-loading"); // "Loading..." text in the results + var resultsEmptyEl = document.getElementById("results-empty"); + var emptyMessageEl = document.getElementById("empty-message"); var skillsHidden = document.getElementById("skills"); // the hidden input that holds skills list var skillsTextInput = document.getElementById("skills-input"); //visible text box in which user types skills var chipsSelectedEl = document.getElementById("skill-chips-selected"); //selected skills tags container @@ -76,37 +533,42 @@ if (isIndexPage) { // Tracks currently selected skills to prevent duplicates var selectedSkills = []; // Clear Filters Button Functionality -var clearFiltersBtn = document.getElementById("clear-filters-btn"); -if (clearFiltersBtn) { - clearFiltersBtn.addEventListener("click", function() { - var recommendForm = document.getElementById("recommend-form"); - if (recommendForm) { - // 1. Reset standard form dropdowns and fields - recommendForm.reset(); - - // 2. Clear out the internal JavaScript array tracker completely - selectedSkills = []; - - // 3. Clear the hidden inputs and visual chips using the file's own variables - if (skillsHidden) skillsHidden.value = ""; - if (chipsSelectedEl) chipsSelectedEl.innerHTML = ""; - if (skillsTextInput) { - skillsTextInput.value = ""; - skillsTextInput.focus(); // Place cursor back on input - } - - // 4. Hide autocomplete suggestions if any are open - var suggestionsBox = document.getElementById("skills-suggestions"); - if (suggestionsBox) suggestionsBox.innerHTML = ""; - - // 5. Reset quick-pick chip visual active states if they have any - if (quickPickChips) { - for (var j = 0; j < quickPickChips.length; j++) { - quickPickChips[j].classList.remove("active", "selected"); - } - } + var clearFiltersBtn = document.getElementById("clear-filters-btn"); + if (clearFiltersBtn) { + clearFiltersBtn.addEventListener("click", function () { + var recommendForm = document.getElementById("recommend-form"); + if (recommendForm) { + // 1. Reset standard form dropdowns and fields + recommendForm.reset(); + + // 2. Clear out the internal JavaScript array tracker completely + selectedSkills = []; + + // 3. Clear the hidden inputs and visual chips using the file's own variables + if (skillsHidden) skillsHidden.value = ""; + if (chipsSelectedEl) chipsSelectedEl.innerHTML = ""; + if (skillsTextInput) { + skillsTextInput.value = ""; + skillsTextInput.focus(); // Place cursor back on input + } + + // 4. Hide autocomplete suggestions if any are open + var suggestionsBox = document.getElementById("skills-suggestions"); + if (suggestionsBox) suggestionsBox.innerHTML = ""; + + // 5. Reset quick-pick chip visual active states if they have any + if (quickPickChips) { + quickPickChips.forEach(function (chip) { + chip.classList.remove("active", "selected"); + }); } + } }); + } + +var resetProgressBtn = document.getElementById("reset-progress-btn"); +if (resetProgressBtn) { + resetProgressBtn.addEventListener("click", resetProgressState); } @@ -136,35 +598,40 @@ if (clearFiltersBtn) { var visibleSuggestions = []; var activeSuggestionIndex = -1; - // Duplicates marquee items to create a seamless infinite scrolling effect - function initSkillStripMarquee() { - var marquee = document.querySelector(".skill-strip-marquee"); - var track = marquee && marquee.querySelector(".skill-strip-track"); + // Capture Enter key at the form level to avoid accidental submits + // when the skills input is focused (some browsers can still submit). + if (form && skillsTextInput) { + form.addEventListener("keydown", function (evt) { + if (evt.key === "Enter" && document.activeElement === skillsTextInput) { + // Run in capture-phase to intercept before other handlers + evt.preventDefault(); + evt.stopPropagation(); - if (!marquee || !track || track.querySelector(".skill-strip-items[data-marquee-clone='true']")) { - return; - } + if (activeSuggestionIndex >= 0 && visibleSuggestions[activeSuggestionIndex]) { + selectSuggestion(visibleSuggestions[activeSuggestionIndex]); + return; + } - var clone = track.querySelector(".skill-strip-items").cloneNode(true); - clone.setAttribute("aria-hidden", "true"); - clone.setAttribute("data-marquee-clone", "true"); - track.appendChild(clone); + if (skillsTextInput.value && skillsTextInput.value.trim()) { + addSkill(skillsTextInput.value); + skillsTextInput.value = ""; + } + hideSuggestions(); + } + }, true); } - // Clean up the initial skills list by removing any empty or duplicate entries - var uniqueSkills = []; - var seenSkills = {}; - for (var k = 0; k < availableSkills.length; k++) { - var s = availableSkills[k]; - if (typeof s === "string" && s.trim()) { - var lower = s.toLowerCase(); - if (!seenSkills[lower]) { - seenSkills[lower] = true; - uniqueSkills.push(s); - } - } + function initSkillStripMarquee() { + var marquee = document.querySelector(".skill-strip-marquee"); + if (!marquee) return; } - availableSkills = uniqueSkills; + + availableSkills = availableSkills.filter(function (skill, index, list) { + return typeof skill === "string" && skill.trim() && + list.findIndex(function (item) { + return item.toLowerCase() === skill.toLowerCase(); + }) === index; + }); if (suggestionsDiv) { suggestionsDiv.setAttribute("role", "listbox"); @@ -172,12 +639,10 @@ if (clearFiltersBtn) { initSkillStripMarquee(); - // Standardizes skill strings to lowercase for reliable comparisons function normalizeSkill(skill) { return skill.trim().toLowerCase(); } - // Checks if the user has already added this specific skill function isSkillSelected(skill) { var normalizedSkill = normalizeSkill(skill); return selectedSkills.some(function (selectedSkill) { @@ -185,40 +650,34 @@ if (clearFiltersBtn) { }); } - // Retrieves the properly capitalized version of a skill if it exists function getCanonicalSkill(rawSkill) { var normalizedSkill = normalizeSkill(rawSkill); - var matchedSkill = availableSkills.filter(function (skill) { + var matchedSkill = availableSkills.find(function (skill) { return normalizeSkill(skill) === normalizedSkill; - })[0]; + }); return matchedSkill || rawSkill.trim(); } - // Returns up to 8 available skills that match the user's search query function getFilteredSkills(query) { var normalizedQuery = normalizeSkill(query); return availableSkills.filter(function (skill) { - return normalizeSkill(skill).indexOf(normalizedQuery) !== -1 && !isSkillSelected(skill); + return normalizeSkill(skill).includes(normalizedQuery) && !isSkillSelected(skill); }).slice(0, 8); } - // Updates ARIA attributes for screen readers based on dropdown visibility function syncSuggestionsA11yState() { skillsTextInput.setAttribute("aria-expanded", visibleSuggestions.length > 0 ? "true" : "false"); } - // Highlights the currently focused item in the autocomplete dropdown function renderActiveSuggestion() { if (!suggestionsDiv) return; - var suggestionItems = suggestionsDiv.querySelectorAll(".suggestion-item"); - for (var i = 0; i < suggestionItems.length; i++) { - var isActive = (i === activeSuggestionIndex); - suggestionItems[i].classList.toggle("suggestion-item--active", isActive); - suggestionItems[i].setAttribute("aria-selected", isActive ? "true" : "false"); - } + suggestionsDiv.querySelectorAll(".suggestion-item").forEach(function (item, index) { + var isActive = index === activeSuggestionIndex; + item.classList.toggle("suggestion-item--active", isActive); + item.setAttribute("aria-selected", isActive ? "true" : "false"); + }); } - // Hides and clears out the autocomplete suggestion box function hideSuggestions() { visibleSuggestions = []; activeSuggestionIndex = -1; @@ -229,7 +688,6 @@ if (clearFiltersBtn) { syncSuggestionsA11yState(); } - // Processes the selection of a skill from the dropdown menu function selectSuggestion(skill) { addSkill(skill); skillsTextInput.value = ""; @@ -237,7 +695,6 @@ if (clearFiltersBtn) { skillsTextInput.focus(); } - // Builds the DOM elements for the autocomplete dropdown based on matches function displaySuggestions(items) { if (!suggestionsDiv) return; visibleSuggestions = items; @@ -275,14 +732,12 @@ if (clearFiltersBtn) { syncSuggestionsA11yState(); } - // Toggles the active visual state on the predefined skill buttons function updateQuickPickState() { - for (var i = 0; i < quickPickChips.length; i++) { - var chip = quickPickChips[i]; + quickPickChips.forEach(function (chip) { var isActive = isSkillSelected(chip.getAttribute("data-skill") || ""); chip.classList.toggle("active", isActive); chip.setAttribute("aria-pressed", isActive ? "true" : "false"); - } + }); } // Add skill on Enter key in the text input @@ -325,24 +780,22 @@ if (clearFiltersBtn) { }); // Add/toggle skill on quick-pick chip click - for (var i = 0; i < quickPickChips.length; i++) { - (function (chip) { - chip.addEventListener("click", function () { - var skill = chip.getAttribute("data-skill"); - var isAlreadySelected = selectedSkills.some(function (s) { - return s.toLowerCase() === skill.toLowerCase(); - }); - - if (isAlreadySelected) { - removeSkill(skill); - } else { - addSkill(skill); - } - hideSuggestions(); - skillsTextInput.value = ""; + quickPickChips.forEach(function (chip) { + chip.addEventListener("click", function () { + var skill = chip.getAttribute("data-skill"); + var isAlreadySelected = selectedSkills.some(function (s) { + return s.toLowerCase() === skill.toLowerCase(); }); - })(quickPickChips[i]); - } + + if (isAlreadySelected) { + removeSkill(skill); + } else { + addSkill(skill); + } + hideSuggestions(); + skillsTextInput.value = ""; + }); + }); // Show suggestions on input skillsTextInput.addEventListener("input", function (evt) { @@ -394,6 +847,18 @@ if (clearFiltersBtn) { updateQuickPickState(); // Once a skill is added, remove the "please add a skill" error if it was showing clearFieldError("skills-error"); + // Ensure the corresponding quick-pick chip is visually active immediately + try { + var quickChip = document.querySelector('.skill-chip[data-skill="' + skill + '"]'); + if (quickChip) { + quickChip.classList.add('active', 'selected'); + quickChip.setAttribute('aria-pressed', 'true'); + } + } catch (e) { + // ignore DOM errors + } + // Keep focus in the input so user can continue typing + if (skillsTextInput) skillsTextInput.focus(); } // remove a skill from the list and update the UI accordingly @@ -405,6 +870,16 @@ if (clearFiltersBtn) { renderSelectedChips(); syncSkillsHiddenInput(); updateQuickPickState(); + // Also clear the visual active state on the quick-pick chip if present + try { + var quickChip = document.querySelector('.skill-chip[data-skill="' + skill + '"]'); + if (quickChip) { + quickChip.classList.remove('active', 'selected'); + quickChip.setAttribute('aria-pressed', 'false'); + } + } catch (e) { + // ignore DOM errors + } } // recreate the selected skills chips based on the current array(selectedSkills) @@ -423,7 +898,7 @@ if (clearFiltersBtn) { removeBtn.type = "button"; removeBtn.className = "skill-chip-remove"; removeBtn.innerHTML = "×"; //'x' symbol - removeBtn.setAttribute("aria-label", "Remove " + skill); + removeBtn.setAttribute("aria-label", "Remove " + skill); removeBtn.addEventListener("click", function (e) { // Stop click from bubbling up to the chip wrap's click listener e.stopPropagation(); @@ -436,12 +911,11 @@ if (clearFiltersBtn) { } function syncSkillsHiddenInput() { - if (!skillsHidden){ + if (!skillsHidden) { skillsHidden = document.getElementById("skills"); } - // Keep the hidden in sync for form serialisation - // The API expects a comma-separated string, so join the array that way - skillsHidden.value = selectedSkills.join(", "); + // Keep the hidden input in sync for form serialisation + if (skillsHidden) skillsHidden.value = selectedSkills.join(", "); } updateQuickPickState(); @@ -502,22 +976,21 @@ if (clearFiltersBtn) { // ---------------------------------------------------------- form.addEventListener("submit", function (evt) { - evt.preventDefault(); //stop the browser from reloading the page on form submit - clearAllErrors() - - if (skillsTextInput.value.trim()) { - addSkill(skillsTextInput.value); - skillsTextInput.value = ""; - hideSuggestions(); - } + evt.preventDefault(); - if (!validateForm()) return; //stop - anything missing/invalid + clearAllErrors(); - setLoadingState(true); + if (skillsTextInput.value.trim()) { + addSkill(skillsTextInput.value); + skillsTextInput.value = ""; + hideSuggestions(); + } - // Allow browser to paint spinner before request starts - requestAnimationFrame(function () { + if (!validateForm()) return; + + setLoadingState(true); + requestAnimationFrame(function () { var payload = { skills: skillsHidden.value.trim() || skillsTextInput.value.trim(), level: document.getElementById("level").value, @@ -534,28 +1007,22 @@ if (clearFiltersBtn) { return res.json(); }) .then(function (data) { - setLoadingState(false); - if (data.error) { var generalErr = document.getElementById("form-error-general"); - - if (generalErr) { - generalErr.textContent = data.error; - } - + if (generalErr) generalErr.textContent = data.error; return; } - renderResults(data.projects || [], data.message); }) - .catch(function (err) { - // this runs if the network request itself fails - setLoadingState(false); - var generalErr = document.getElementById("form-error-general"); - if (generalErr) generalErr.textContent = "Something went wrong. Please try again."; - }); - }); + .catch(function () { + setLoadingState(false); + var generalErr = document.getElementById("form-error-general"); + if (generalErr) { + generalErr.textContent = "An unexpected error occurred. Please try again."; + } + }); + }); }); // Manages the loading state of the form and results section(whats visible or not) @@ -565,8 +1032,6 @@ if (clearFiltersBtn) { submitBtn.setAttribute("aria-busy", isLoading); btnLabel.style.display = isLoading ? "none" : "inline"; btnLoading.style.display = isLoading ? "inline-flex" : "none"; - btnLabel.style.display = isLoading ? "none" : "inline"; - btnLoading.style.display = isLoading ? "inline" : "none"; if (isLoading) { // Show the results section with only the loading indicator visible @@ -577,8 +1042,8 @@ if (clearFiltersBtn) { // Scroll down so the user can see the spinner without manually scrolling resultsSection.scrollIntoView({ behavior: "smooth" }); } else { - resultsLoadingEl.style.display = "none"; - resultsGrid.style.display = "grid"; //switch back to gird layout + resultsLoadingEl.style.display = "none"; + resultsGrid.style.display = "grid"; //switch back to gird layout } } @@ -587,352 +1052,599 @@ if (clearFiltersBtn) { // Render result cards // ---------------------------------------------------------- - //takes the array of projects from the api and draws them on the page as cards - //if array is empty it shows the "no results" message instead + function truncate(text, maxLength) { + if (!text) return ""; + return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; + } + + function createTag(text, type) { + var span = document.createElement("span"); + span.className = "project-tag project-tag--" + type; + span.textContent = text; + return span; + } + function renderResults(projects, message) { resultsSection.style.display = "block"; resultsLoadingEl.style.display = "none"; - // Clear out any cards from a previous search before showing new ones resultsGrid.innerHTML = ""; + recordSearch(); - if (!projects || projects.length === 0) { //if no projects returned from api, show the "no results" message and hide the grid - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "block"; - if (message && emptyMessageEl) emptyMessageEl.textContent = message; //if api sent back a message (e.g. "no projects found matching your criteria"), show that + if (!projects || projects.length === 0) { + resultsGrid.style.display = "none"; + resultsEmptyEl.style.display = "block"; + + var interestEl = document.getElementById("interest"); + var selectedInterest = interestEl ? interestEl.value : null; + + if (selectedInterest) { + emptyMessageEl.textContent = "No projects are currently available for this interest. Please check back later or try a different area."; + } else if (message) { + emptyMessageEl.textContent = message; + } else { + emptyMessageEl.textContent = "Try adjusting your skills or choosing a different interest area."; + } resultsSection.scrollIntoView({ behavior: "smooth" }); return; } resultsEmptyEl.style.display = "none"; resultsGrid.style.display = "grid"; - - //build a card for each project and add it to the grid projects.forEach(function (project) { resultsGrid.appendChild(buildProjectCard(project)); }); - resultsSection.scrollIntoView({ behavior: "smooth" }); } - // builds one project card as a DOM element and returns it - // the card has title, short description, tags and link function buildProjectCard(project) { var card = document.createElement("div"); card.className = "project-card"; - // Title var title = document.createElement("h3"); title.className = "project-card-title"; title.textContent = project.title; - // Description (truncated for visual consistency) var desc = document.createElement("p"); desc.className = "project-card-desc"; - // Cut description to 120 chars so all cards stay the same height - desc.textContent = truncate(project.description, 120); + var descText = document.createElement("span"); + descText.className = "project-card-desc-text"; + var shortText = truncate(project.description, 120); + var fullText = project.description; + var isExpanded = false; + descText.textContent = shortText; + desc.appendChild(descText); + + if (fullText && fullText.length > 120) { + var readMoreBtn = document.createElement("button"); + readMoreBtn.className = "read-more-btn"; + readMoreBtn.textContent = "Read more"; + readMoreBtn.setAttribute("aria-expanded", "false"); + readMoreBtn.addEventListener("click", function () { + isExpanded = !isExpanded; + descText.textContent = isExpanded ? fullText : shortText; + readMoreBtn.textContent = isExpanded ? "Read less" : "Read more"; + readMoreBtn.setAttribute("aria-expanded", isExpanded ? "true" : "false"); + }); + desc.appendChild(readMoreBtn); + } - // Tags row var tagsRow = document.createElement("div"); tagsRow.className = "project-card-tags"; - - // Show the first two skills as tags - (project.skills || []).slice(0, 2).forEach(function (skill) { + (project.skills || []).forEach(function (skill) { tagsRow.appendChild(createTag(skill, "skill")); }); - - // Level tag (colour-coded via CSS class) - // Lowercase so it matches the CSS class names like "level beginner", "level advanced" - var levelClass = "level " + (project.level || "").toLowerCase(); - tagsRow.appendChild(createTag(project.level, levelClass)); - - // Time tag + tagsRow.appendChild(createTag(project.level, (project.level || "").toLowerCase())); tagsRow.appendChild(createTag("Time: " + project.time, "time")); - // Footer with view-details link var footer = document.createElement("div"); footer.className = "project-card-footer"; - var link = document.createElement("a"); link.className = "btn-details"; link.textContent = "View Full Project"; - link.href = "/project/" + project.id; //each project has a unique id - + link.href = "/project/" + project.id; footer.appendChild(link); - // Assemble the card in order card.appendChild(title); card.appendChild(desc); card.appendChild(tagsRow); card.appendChild(footer); - return card; } - // helper to create a coloured tag element (used for skills, level, time tags on the cards) - function createTag(text, type) { - var span = document.createElement("span"); - // The type becomes a BEM modifier so CSS can style each tag differently - span.className = "project-tag project-tag--" + type; - span.textContent = text; - return span; - } +} // end isIndexPage - function truncate(text, maxLength) { - // Safety check — just return empty string if text is missing - if (!text) return ""; - // Only add "..." if the text is actually longer than the limit - return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; - } -} // end isIndexPage + // ============================================================ + // DETAIL PAGE + // ============================================================ + if (isDetailPage) { + + var codePanel = document.getElementById("code-panel"); // sliding panel that shows the starter code " + var codePanelOverlay = document.getElementById("code-panel-overlay"); // background overlay + var codeContentEl = document.getElementById("code-content"); //
     element inside the panel where the code will be inserted
    +    var codePanelFilename = document.getElementById("code-panel-filename"); // filename display
    +    var btnViewCode = document.getElementById("btn-view-code"); // button to open the code panel on desktop
    +    var btnViewCodeSm = document.getElementById("btn-view-code-sm"); // button to open the code panel on mobile (could be the same button with different styling, but we have two here for simplicity)
    +    var btnClosePanel = document.getElementById("code-panel-close"); // button inside the panel to close it
    +
    +    // Cache flag so code is only fetched once per page load
    +    var codeFetched = false;
    +
    +    //opens the sliding code panel 
    +    function openCodePanel() {
    +      // Panel element might not exist on every detail page, so check first
    +      if (!codePanel) return;
    +      codePanel.classList.add("active");
    +      if (codePanelOverlay) codePanelOverlay.classList.add("active");
    +      // Lock background scroll so the page doesn't scroll behind the panel
    +      document.body.style.overflow = "hidden";
    +
    +      // Only fetch the code on the first open, no need to re-fetch every time
    +      if (!codeFetched) fetchStarterCode();
    +    }
     
    +    //closes the code panel and hides the overlay
    +    function closeCodePanel() {
    +      if (!codePanel) return;
    +      codePanel.classList.remove("active");
    +      if (codePanelOverlay) codePanelOverlay.classList.remove("active");
    +      // Restore normal scrolling once the panel is closed
    +      document.body.style.overflow = "";
    +    }
     
    +    // Render code string as a list of DOM rows where each row contains a
    +    // line-number gutter cell and a code cell. Returning DOM nodes instead
    +    // of an HTML string avoids innerHTML XSS risks from the code content.
    +    function renderCodeWithLineNumbers(code) {
    +      var lines = (code || "").split("\n");
    +      return lines.map(function (line, index) {
    +        var row = document.createElement("div");
    +        row.className = "code-line";
    +
    +        var lineNum = document.createElement("span");
    +        lineNum.className = "code-line-number";
    +        lineNum.setAttribute("aria-hidden", "true");
    +        lineNum.textContent = index + 1;
    +
    +        var lineCode = document.createElement("span");
    +        lineCode.className = "code-line-content";
    +        lineCode.textContent = line;
    +
    +        row.appendChild(lineNum);
    +        row.appendChild(lineCode);
    +        return row;
    +      });
    +    }
    +
    +    //fetches the starter code from the server via an API call
    +    //inserts the code into the panel and handles loading/error states
    +    function fetchStarterCode() {
    +      // Show a loading message while we wait for the API response
    +      if (codeContentEl) codeContentEl.textContent = "Loading starter code...";
    +
    +      fetch("/project/" + PROJECT_ID + "/code")
    +        .then(function (res) { return res.json(); })
    +        .then(function (data) {
    +          if (data.error) {
    +            if (codeContentEl) codeContentEl.textContent = "Error: " + data.error;
    +            return;
    +          }
    +          if (codePanelFilename) codePanelFilename.textContent = data.filename;
    +          if (codeContentEl) {
    +            codeContentEl.textContent = "";
    +            renderCodeWithLineNumbers(data.code).forEach(function (row) {
    +              codeContentEl.appendChild(row);
    +            });
    +          }
    +          // Mark as fetched so we don't hit the API again on the next open
    +          codeFetched = true;
    +        })
    +        .catch(function () {
    +          if (codeContentEl) {
    +            codeContentEl.textContent = "Could not load starter code. Try downloading it instead.";
    +          }
    +        });
    +    }
    +
    +   // ============================================================
    +// ROADMAP PROGRESS TRACKER
     // ============================================================
    -// DETAIL PAGE
    -// ============================================================
    -if (isDetailPage) {
    -
    -  var codePanel         = document.getElementById("code-panel"); // sliding panel that shows the starter code "
    -  var codePanelOverlay  = document.getElementById("code-panel-overlay"); // background overlay 
    -  var codeContentEl     = document.getElementById("code-content"); // 
     element inside the panel where the code will be inserted
    -  var codePanelFilename = document.getElementById("code-panel-filename"); // filename display
    -  var btnViewCode       = document.getElementById("btn-view-code"); // button to open the code panel on desktop
    -  var btnViewCodeSm     = document.getElementById("btn-view-code-sm"); // button to open the code panel on mobile (could be the same button with different styling, but we have two here for simplicity)
    -  var btnClosePanel     = document.getElementById("code-panel-close"); // button inside the panel to close it
    -
    -  // Cache flag so code is only fetched once per page load
    -  var codeFetched = false;
    -
    -  //opens the sliding code panel 
    -  function openCodePanel() {
    -    // Panel element might not exist on every detail page, so check first
    -    if (!codePanel) return;
    -    codePanel.classList.add("active");
    -    if (codePanelOverlay) codePanelOverlay.classList.add("active");
    -    // Lock background scroll so the page doesn't scroll behind the panel
    -    document.body.style.overflow = "hidden";
    -
    -    // Only fetch the code on the first open, no need to re-fetch every time
    -    if (!codeFetched) fetchStarterCode();
    -  }
    -
    -  //closes the code panel and hides the overlay
    -  function closeCodePanel() {
    -    if (!codePanel) return;
    -    codePanel.classList.remove("active");
    -    if (codePanelOverlay) codePanelOverlay.classList.remove("active");
    -    // Restore normal scrolling once the panel is closed
    -    document.body.style.overflow = "";
    -  }
    -
    -  //fetches the starter code from the server via an API call
    -  //inserts the code into the panel and handles loading/error states
    -  function fetchStarterCode() {
    -    // Show a loading message while we wait for the API response
    -    if (codeContentEl) codeContentEl.textContent = "Loading starter code...";
    -
    -    fetch("/project/" + PROJECT_ID + "/code")
    -      .then(function (res) { return res.json(); })
    -      .then(function (data) {
    -        if (data.error) {
    -          if (codeContentEl) codeContentEl.textContent = "Error: " + data.error;
    -          return;
    -        }
    -        if (codePanelFilename) codePanelFilename.textContent = data.filename;
    -        if (codeContentEl) {
    -          codeContentEl.textContent = "";
    -          renderCodeWithLineNumbers(data.code).forEach(function (row) {
    -            codeContentEl.appendChild(row);
    -          });
    -        }
    -        // Mark as fetched so we don't hit the API again on the next open
    -        codeFetched = true;
    -      })
    -      .catch(function () {
    -        if (codeContentEl) {
    -          codeContentEl.textContent = "Could not load starter code. Try downloading it instead.";
    +
    +
    +var roadmapCheckboxes = document.querySelectorAll(
    +    ".roadmap-checkbox"
    +);
    +
    +var progressFill = document.getElementById(
    +    "roadmap-progress-fill"
    +);
    +
    +var progressText = document.getElementById(
    +    "roadmap-progress-text"
    +);
    +
    +var progressBar = document.querySelector(
    +    ".roadmap-progress-bar"
    +);
    +
    +// Local storage key
    +var roadmapStorageKey =
    +    `devpath-roadmap-progress-${PROJECT_ID}`;
    +
    +
    +// ------------------------------------------------------------
    +// Restore saved roadmap state
    +// ------------------------------------------------------------
    +
    +var savedRoadmapState =
    +    localStorage.getItem(
    +        roadmapStorageKey
    +    );
    +
    +if(savedRoadmapState){
    +
    +    try{
    +
    +        var parsedState =
    +            JSON.parse(savedRoadmapState);
    +
    +        roadmapCheckboxes.forEach(
    +            function(cb,index){
    +
    +                cb.checked =
    +                    !!parsedState[index];
    +
    +            }
    +        );
    +
    +    } catch(error){
    +
    +        console.error(
    +            "Failed to restore roadmap progress",
    +            error
    +        );
    +
    +    }
    +}
    +
    +
    +// ------------------------------------------------------------
    +// Update roadmap progress
    +// ------------------------------------------------------------
    +
    +function updateRoadmapProgress(){
    +
    +    if(!roadmapCheckboxes.length){
    +        return;
    +    }
    +
    +    var completed = 0;
    +
    +    roadmapCheckboxes.forEach(function(cb){
    +
    +        var step = cb.closest(
    +            ".roadmap-step"
    +        );
    +
    +        if(cb.checked){
    +
    +            completed++;
    +
    +            if(step){
    +                step.classList.add(
    +                    "completed"
    +                );
    +            }
    +
    +        } else {
    +
    +            if(step){
    +                step.classList.remove(
    +                    "completed"
    +                );
    +            }
    +
             }
    -      });
    -  }
     
    -  // Attach open/close handlers
    -  if (btnViewCode) btnViewCode.addEventListener("click", openCodePanel);
    -  if (btnViewCodeSm) btnViewCodeSm.addEventListener("click", openCodePanel);
    -  if (btnClosePanel) btnClosePanel.addEventListener("click", closeCodePanel);
    +    });
     
    -  if (codePanelOverlay) {
    -    codePanelOverlay.addEventListener("click", closeCodePanel); //clicking on the background overlay to also close the panel
    -  }
    +    var percent = Math.round(
    +        (completed / roadmapCheckboxes.length)
    +        * 100
    +    );
     
    -  // Let keyboard users close the panel with Escape — important for accessibility
    -  document.addEventListener("keydown", function (evt) {
    -    if (evt.key === "Escape") closeCodePanel(); //esc key to close
    -  });
    +    // Update progress bar fill
    +    if(progressFill){
    +
    +        progressFill.style.width =
    +            percent + "%";
    +
    +    }
    +
    +    // Update progress text
    +    if(progressText){
    +
    +        progressText.textContent =
    +            percent + "% completed";
    +
    +    }
    +
    +    // Accessibility update
    +    if(progressBar){
    +
    +        progressBar.setAttribute(
    +            "aria-valuenow",
    +            percent
    +        );
    +
    +    }
    +
    +    // Save checkbox state
    +    var savedState = [];
    +
    +    roadmapCheckboxes.forEach(function(cb){
    +
    +        savedState.push(
    +            cb.checked
    +        );
     
    -  // ----------------------------------------------------------
    -  // Copy Code button
    -  // ----------------------------------------------------------
    -  var btnCopyCode  = document.getElementById("btn-copy-code");
    -  var copyToast    = document.getElementById("copy-toast"); //popup msg when copied 
    -  var toastTimeout = null; 
    -
    -  //shows the "copied to clipboard" state on the button and the toast message, then resets after a short delay
    -  function showCopySuccess() {
    -    if (!btnCopyCode) return;
    -
    -    // Swap icons on the button(copy and checkmark icons)
    -    var copyIcon  = btnCopyCode.querySelector(".copy-icon");
    -    var checkIcon = btnCopyCode.querySelector(".check-icon");
    -    var btnLabel = btnCopyCode.querySelector(".copy-btn-label");
    -
    -    if (copyIcon) copyIcon.style.display = "none";
    -    if (checkIcon) checkIcon.style.display = "inline";
    -    if (btnLabel) btnLabel.textContent = "Copied!";
    -    btnCopyCode.classList.add("copied");
    -    // Disable button so user can't spam click it while toast is showing
    -    btnCopyCode.disabled = true;
    -
    -    // Show toast
    -    if (copyToast) {
    -      copyToast.classList.add("show");
    -    }
    -
    -    // Auto-reset after 2.5 s
    -    // Clear any previous timeout first so timers don't stack up
    -    clearTimeout(toastTimeout);
    -    toastTimeout = setTimeout(function () {
    -      if (copyIcon) copyIcon.style.display = "inline";
    -      if (checkIcon) checkIcon.style.display = "none";
    -      if (btnLabel) btnLabel.textContent = "Copy Code";
    -      btnCopyCode.classList.remove("copied");
    -      btnCopyCode.disabled = false;
    -      if (copyToast) copyToast.classList.remove("show");
    -    }, 2500);
    -  }
    -
    -  if (btnCopyCode) {
    -    btnCopyCode.addEventListener("click", function () {
    -      var code = codeContentEl
    -        ? Array.prototype.slice.call(codeContentEl.querySelectorAll(".line-content"))
    -          .map(function (el) { return el.textContent; })
    -          .join("\n")
    -        : "";
    -      // Don't copy if the code hasn't loaded yet — just ignore the click
    -      if (!code || code === "Loading..." || code === "Loading starter code...") return;
    -
    -      // Use Clipboard API with textarea fallback
    -      if (navigator.clipboard && navigator.clipboard.writeText) {
    -        navigator.clipboard.writeText(code).then(showCopySuccess).catch(function () {
    -          fallbackCopy(code); // clipboard api failed, try the old way
    -        });
    -      } else {
    -        fallbackCopy(code); // Clipboard API not supported, use fallback method
    -      }
         });
    -  }
     
    -  // Fallback method to copy text using a hidden textarea and execCommand (for older browsers)
    -  function fallbackCopy(text) {
    -    // Some older browsers don't support navigator.clipboard, so we use a hidden textarea instead
    -    var ta = document.createElement("textarea");
    -    ta.value = text;
    -    // Push it off-screen so it's not visible but can still be selected
    -    ta.style.cssText = "position:fixed;top:-9999px;left:-9999px;opacity:0";
    -    document.body.appendChild(ta);
    -    ta.focus();
    -    ta.select();
    -    // execCommand is old and deprecated but works as a last resort — fail silently if it doesn't
    -    try { document.execCommand("copy"); showCopySuccess(); } catch (e) { /* silent fail */ }
    -    document.body.removeChild(ta);
    -  }
    -} // end isDetailPage
    +    localStorage.setItem(
    +        roadmapStorageKey,
    +        JSON.stringify(savedState)
    +    );
    +
    +}
    +
    +
    +// ------------------------------------------------------------
    +// Attach checkbox listeners
    +// ------------------------------------------------------------
    +
    +roadmapCheckboxes.forEach(function(cb){
    +
    +    cb.addEventListener(
    +        "change",
    +        updateRoadmapProgress
    +    );
    +
    +});
    +
    +
    +// ------------------------------------------------------------
    +// Initial progress render
    +// ------------------------------------------------------------
    +
    +updateRoadmapProgress();
    +
    +    // Attach open/close handlers
    +    if (btnViewCode) btnViewCode.addEventListener("click", openCodePanel);
    +    if (btnViewCodeSm) btnViewCodeSm.addEventListener("click", openCodePanel);
    +    if (btnClosePanel) btnClosePanel.addEventListener("click", closeCodePanel);
    +
    +    if (codePanelOverlay) {
    +      codePanelOverlay.addEventListener("click", closeCodePanel); //clicking on the background overlay to also close the panel
    +    }
    +
    +    // Let keyboard users close the panel with Escape — important for accessibility
    +    document.addEventListener("keydown", function (evt) {
    +      if (evt.key === "Escape") closeCodePanel(); //esc key to close
    +    });
    +
    +    // ----------------------------------------------------------
    +    // Copy Code button
    +    // ----------------------------------------------------------
    +    var btnCopyCode = document.getElementById("btn-copy-code");
    +    var copyToast = document.getElementById("copy-toast"); //popup msg when copied 
    +    var toastTimeout = null;
    +
    +    //shows the "copied to clipboard" state on the button and the toast message, then resets after a short delay
    +    function showCopySuccess() {
    +      if (!btnCopyCode) return;
    +
    +      // Swap icons on the button(copy and checkmark icons)
    +      var copyIcon = btnCopyCode.querySelector(".copy-icon");
    +      var checkIcon = btnCopyCode.querySelector(".check-icon");
    +      var btnLabel = btnCopyCode.querySelector(".copy-btn-label");
    +
    +      if (copyIcon) copyIcon.style.display = "none";
    +      if (checkIcon) checkIcon.style.display = "inline";
    +      if (btnLabel) btnLabel.textContent = "Copied!";
    +      btnCopyCode.classList.add("copied");
    +      // Disable button so user can't spam click it while toast is showing
    +      btnCopyCode.disabled = true;
    +
    +      // Show toast
    +      if (copyToast) {
    +        copyToast.classList.add("show");
    +      }
    +
    +      // Auto-reset after 2.5 s
    +      // Clear any previous timeout first so timers don't stack up
    +      clearTimeout(toastTimeout);
    +      toastTimeout = setTimeout(function () {
    +        if (copyIcon) copyIcon.style.display = "inline";
    +        if (checkIcon) checkIcon.style.display = "none";
    +        if (btnLabel) btnLabel.textContent = "Copy Code";
    +        btnCopyCode.classList.remove("copied");
    +        btnCopyCode.disabled = false;
    +        if (copyToast) copyToast.classList.remove("show");
    +      }, 2500);
    +    }
     
    -if (
    +    if (btnCopyCode) {
    +      btnCopyCode.addEventListener("click", function () {
    +        var code = codeContentEl
    +          ? Array.from(codeContentEl.querySelectorAll(".line-content"))
    +            .map(function (el) { return el.textContent; })
    +            .join("\n")
    +          : "";
    +        // Don't copy if the code hasn't loaded yet — just ignore the click
    +        if (!code || code === "Loading..." || code === "Loading starter code...") return;
    +
    +        // Use Clipboard API with textarea fallback
    +        if (navigator.clipboard && navigator.clipboard.writeText) {
    +          navigator.clipboard.writeText(code).then(showCopySuccess).catch(function () {
    +            fallbackCopy(code); // clipboard api failed, try the old way
    +          });
    +        } else {
    +          fallbackCopy(code); // Clipboard API not supported, use fallback method
    +        }
    +      });
    +    }
    +
    +    // Fallback method to copy text using a hidden textarea and execCommand (for older browsers)
    +    function fallbackCopy(text) {
    +      // Some older browsers don't support navigator.clipboard, so we use a hidden textarea instead
    +      var ta = document.createElement("textarea");
    +      ta.value = text;
    +      // Push it off-screen so it's not visible but can still be selected
    +      ta.style.cssText = "position:fixed;top:-9999px;left:-9999px;opacity:0";
    +      document.body.appendChild(ta);
    +      ta.focus();
    +      ta.select();
    +      // execCommand is old and deprecated but works as a last resort — fail silently if it doesn't
    +      try { document.execCommand("copy"); showCopySuccess(); } catch (e) { /* silent fail */ }
    +      document.body.removeChild(ta);
    +    }
    +  } // end isDetailPage
    +
    +if (isIndexPage) {
    +  if (
         openModalBtn &&
         closeModalBtn &&
         modal &&
         githubInput &&
         fetchBtn &&
         errorMsg
    -) {
    -// 1. Open Github Input Modal
    -  openModalBtn.addEventListener('click', (e) => {
    +  ) {
    +    // Opens the GitHub input modal and focuses the input field
    +    openModalBtn.addEventListener("click", function(e) {
           e.preventDefault();
    -      modal.classList.add('active');
    +      modal.classList.add("active");
           githubInput.focus();
    -  });
    +    });
     
    -  // 2. Close Github Input Modal
    -  const closeGithubModal = () => {
    -      modal.classList.remove('active');
    -      githubInput.value = '';
    -      errorMsg.textContent = '';
    -  };
    +    // Closes the GitHub input modal and resets its values and errors
    +    var closeGithubModal = function() {
    +      modal.classList.remove("active");
    +      githubInput.value = "";
    +      errorMsg.textContent = "";
    +    };
     
    -  closeModalBtn.addEventListener('click', closeGithubModal);
    +    closeModalBtn.addEventListener("click", closeGithubModal);
     
    -  // Close on clicking outside the card
    -  modal.addEventListener('click', (e) => {
    +    // Closes the modal if the user clicks the background overlay outside the card
    +    modal.addEventListener("click", function(e) {
           if (e.target === modal) closeGithubModal();
    -  });
    +    });
    +
    +    // Fetches repositories for a GitHub user and adds unique languages to the skills list
    +    fetchBtn.addEventListener("click", function() {
    +      var username = githubInput.value.trim();
     
    -  // 3. Fetch Skills Logic
    -  fetchBtn.addEventListener('click', async () => {
    -      const username = githubInput.value.trim();
    -      if (!username) return;
    +      // Clear any previous error before validating / retrying
    +      errorMsg.textContent = "";
    +
    +      if (!username) {
    +        errorMsg.textContent = "Please enter a GitHub username";
    +        githubInput.focus();
    +        return;
    +      }
     
           fetchBtn.disabled = true;
    -      fetchBtn.textContent = 'Syncing...';
    +      fetchBtn.textContent = "Syncing...";
     
    -      try {
    -          const response = await fetch(`https://api.github.com/users/${username}/repos`);
    -          if (!response.ok) throw new Error();
    -          
    -          const repos = await response.json();
    -          const langs = [...new Set(repos.map(r => r.language).filter(Boolean))];
    +      fetch("https://api.github.com/users/" + encodeURIComponent(username) + "/repos")
    +        .then(function(response) {
    +          if (!response.ok) {
    +            if (response.status === 404) {
    +              throw new Error("Username not found. Please check and try again.");
    +            }
    +            if (response.status === 403) {
    +              throw new Error("GitHub rate limit reached. Please try again later.");
    +            }
    +            throw new Error("Failed to fetch skills. Please try again.");
    +          }
    +          return response.json();
    +        })
    +        .then(function(repos) {
    +          var langs = [];
    +          for (var i = 0; i < repos.length; i++) {
    +            var lang = repos[i].language;
    +            if (lang && langs.indexOf(lang) === -1) {
    +              langs.push(lang);
    +            }
    +          }
     
               if (langs.length > 0) {
    -              langs.forEach(lang => {
    -                  if (typeof addSkill === 'function') addSkill(lang);
    -              });
    -              closeGithubModal();
    +            for (var j = 0; j < langs.length; j++) {
    +              if (typeof addSkill === "function") addSkill(langs[j]);
    +            }
    +            closeGithubModal();
               } else {
    -              errorMsg.textContent = "No public languages found.";
    +            errorMsg.textContent = "No public languages found.";
               }
    -      } catch (err) {
    -          errorMsg.textContent = err.message ?? "Failed to fetch skills";
    -      } finally {
               fetchBtn.disabled = false;
    -          fetchBtn.textContent = 'Fetch Skills';
    -      }
    -  });
    +          fetchBtn.textContent = "Fetch Skills";
    +        })
    +        .catch(function(err) {
    +          errorMsg.textContent = err.message || "Failed to fetch skills";
    +          fetchBtn.disabled = false;
    +          fetchBtn.textContent = "Fetch Skills";
    +        });
    +    });
    +  } // end github modal handlers
     }
     
    -/* ---- Scroll-to-top button ---- */
     
    -/* Show the button only when the user has scrolled more than 300px */
    -var SCROLL_THRESHOLD = 300;
    +// ============================================================
    +// SCROLL NAVIGATION BUTTON (runs on all pages)
    +// ============================================================
    +(function () {
    +  var SCROLL_THRESHOLD = 200;
    +  var scrollTopBtn = document.getElementById('scroll-top-btn');
    +  var scrollBtnIcon = document.getElementById('scroll-btn-icon');
    +  var atBottom = false;
     
    -/* Get the button element; guard against pages that do not have it */
    -var scrollTopBtn = document.getElementById('scroll-top-btn');
    +  var ARROW_UP   = '';
    +  var ARROW_DOWN = '';
     
    -/* Add or remove the .visible class based on scroll position */
    -function handleScroll() {
    -  if (!scrollTopBtn) return;
    -  if (window.pageYOffset > SCROLL_THRESHOLD) {
    -    scrollTopBtn.classList.add('visible');
    -  } else {
    -    scrollTopBtn.classList.remove('visible');
    +  function isNearBottom() {
    +    return (window.innerHeight + window.pageYOffset) >= document.body.scrollHeight - 40;
       }
    -}
     
    -/* Smooth-scroll to the very top of the page */
    -function scrollToTop() {
    -  window.scrollTo({ top: 0, behavior: 'smooth' });
    -}
    +  function handleScroll() {
    +    if (!scrollTopBtn) return;
    +    if (window.pageYOffset > SCROLL_THRESHOLD) {
    +      scrollTopBtn.classList.add('visible');
    +    } else {
    +      scrollTopBtn.classList.remove('visible');
    +    }
    +    if (isNearBottom()) {
    +      atBottom = true;
    +      scrollTopBtn.setAttribute('aria-label', 'Scroll to top');
    +      scrollTopBtn.title = 'Scroll to top';
    +      if (scrollBtnIcon) scrollBtnIcon.innerHTML = ARROW_UP;
    +    } else {
    +      atBottom = false;
    +      scrollTopBtn.setAttribute('aria-label', 'Scroll to bottom');
    +      scrollTopBtn.title = 'Scroll to bottom';
    +      if (scrollBtnIcon) scrollBtnIcon.innerHTML = ARROW_DOWN;
    +    }
    +  }
     
    -/* Only wire up listeners if the button exists on this page */
    -if (scrollTopBtn) {
    -    window.addEventListener('scroll', handleScroll);
    -    scrollTopBtn.addEventListener('click', scrollToTop);
    -}
    +  if (scrollTopBtn) {
    +    window.addEventListener('scroll', handleScroll, { passive: true });
    +    scrollTopBtn.addEventListener('click', function () {
    +      if (atBottom) {
    +        window.scrollTo({ top: 0, behavior: 'smooth' });
    +      } else {
    +        window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
    +      }
    +    });
    +    handleScroll();
    +  }
    +
    +})();
    diff --git a/app.py b/app.py
    index f5e5a749..de4bca85 100644
    --- a/app.py
    +++ b/app.py
    @@ -16,6 +16,13 @@
     
     app = Flask(__name__)
     
    +@app.template_filter('escapejs')
    +def escapejs_filter(val):
    +    """Safely escape strings for use inside JavaScript."""
    +    if val is None:
    +        return ''
    +    return str(val).replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n').replace('\r', '\\r').replace(' to update a task",
    -      "Step 6: Create DELETE /tasks/ to remove a task",
    -      "Step 7: Add JSON file persistence for saving tasks",
    -      "Step 8: Test all endpoints using Postman or curl"
    -    ],
    -    "resources": [
    -      "Flask quickstart: https://flask.palletsprojects.com/quickstart",
    -      "REST API design guide: https://restfulapi.net",
    -      "Postman download: https://www.postman.com/downloads"
    -    ],
    -    "starter_code": "starter_code/task_api.py"
    -  },
    -  {
    -    "id": 5,
    -    "title": "Portfolio Website",
    -    "skills": [
    -      "HTML",
    -      "CSS",
    -      "JavaScript"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Web",
    -    "time": "Low",
    -    "description": "A personal portfolio site with sections for bio, projects, and contact. A great first project that teaches HTML layout, CSS styling, and a bit of JavaScript for interactivity.",
    -    "features": [
    -      "Hero section with name and tagline",
    -      "Projects grid with cards",
    -      "Skills list with visual indicators",
    -      "Contact form with basic validation"
    -    ],
    -    "tech_stack": [
    -      "HTML",
    -      "CSS",
    -      "JavaScript"
    -    ],
    -    "roadmap": [
    -      "Step 1: Plan the page sections on paper first",
    -      "Step 2: Write the HTML structure for all sections",
    -      "Step 3: Add CSS reset and base typography styles",
    -      "Step 4: Style the navigation and hero section",
    -      "Step 5: Build the projects grid using CSS Grid",
    -      "Step 6: Add the contact form with labels and inputs",
    -      "Step 7: Write JavaScript for form validation",
    -      "Step 8: Make the site responsive with media queries"
    -    ],
    -    "resources": [
    -      "HTML reference: https://developer.mozilla.org/en-US/docs/Web/HTML",
    -      "CSS Grid guide: https://css-tricks.com/snippets/css/complete-guide-grid",
    -      "Responsive design basics: https://web.dev/learn/design"
    -    ],
    -    "starter_code": "starter_code/portfolio.html"
    -  },
    -  {
    -    "id": 6,
    -    "title": "URL Shortener",
    -    "skills": [
    -      "Python",
    -      "JavaScript",
    -      "HTML",
    -      "CSS"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Web",
    -    "time": "High",
    -    "description": "A full-stack web app that takes long URLs and generates short codes. Users can paste a link and get a shorter one back. Teaches Flask routing, random code generation, and front-end form handling.",
    -    "features": [
    -      "Shorten any valid URL",
    -      "Redirect short codes to original URL",
    -      "Track how many times a link was clicked",
    -      "List all shortened links in a dashboard"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "Flask",
    -      "HTML",
    -      "CSS",
    -      "JavaScript",
    -      "JSON"
    -    ],
    -    "roadmap": [
    -      "Step 1: Set up Flask app with two routes: home and redirect",
    -      "Step 2: Write a random 6-character code generator",
    -      "Step 3: Store URL mappings in a JSON file",
    -      "Step 4: Build the HTML form for pasting long URLs",
    -      "Step 5: Display the shortened URL after submission",
    -      "Step 6: Implement the redirect route using the short code",
    -      "Step 7: Add a click counter that updates on each visit",
    -      "Step 8: Build a simple dashboard to list all links"
    -    ],
    -    "resources": [
    -      "Flask routing docs: https://flask.palletsprojects.com/en/stable/quickstart/#routing",
    -      "Python secrets module: https://docs.python.org/3/library/secrets.html",
    -      "UUID in Python: https://docs.python.org/3/library/uuid.html"
    -    ],
    -    "starter_code": "starter_code/url_shortener.py"
    -  },
    -  {
    -    "id": 7,
    -    "title": "Data Analysis Report Generator",
    -    "skills": [
    -      "Python"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Data",
    -    "time": "High",
    -    "description": "Upload a CSV file and automatically generate a summary report with statistics, missing value counts, and basic charts. A practical project for learning data wrangling and pandas.",
    -    "features": [
    -      "Load and inspect CSV files",
    -      "Show column types and null counts",
    -      "Calculate mean, median, and mode per column",
    -      "Generate bar charts for categorical data"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "pandas",
    -      "matplotlib",
    -      "os module"
    -    ],
    -    "roadmap": [
    -      "Step 1: Install pandas and matplotlib via pip",
    -      "Step 2: Write a CSV loader that validates the file path",
    -      "Step 3: Generate a summary table of column info",
    -      "Step 4: Compute descriptive statistics for numeric columns",
    -      "Step 5: Count and display missing values per column",
    -      "Step 6: Build chart generation functions using matplotlib",
    -      "Step 7: Export the full report to a text or HTML file"
    -    ],
    -    "resources": [
    -      "pandas docs: https://pandas.pydata.org/docs",
    -      "matplotlib tutorials: https://matplotlib.org/stable/tutorials",
    -      "Real Python data analysis: https://realpython.com/pandas-dataframe"
    -    ],
    -    "starter_code": "starter_code/data_report.py"
    -  },
    -  {
    -    "id": 8,
    -    "title": "Library Management System",
    -    "skills": [
    -      "Java"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Backend",
    -    "time": "Medium",
    -    "description": "A Java application that helps manage books, students, and borrowing records in a library. This project teaches object-oriented programming concepts, file handling, and menu-driven application design.",
    -    "features": [
    -      "Add and remove books",
    -      "Issue and return books",
    -      "Store student records",
    -      "Search books by title or author"
    -    ],
    -    "tech_stack": [
    -      "Java",
    -      "OOP",
    -      "File Handling"
    -    ],
    -    "roadmap": [
    -      "Step 1: Create Book and Student classes",
    -      "Step 2: Design the menu-driven interface",
    -      "Step 3: Implement add and remove book features",
    -      "Step 4: Add issue and return book functionality",
    -      "Step 5: Store records using file handling",
    -      "Step 6: Implement search functionality",
    -      "Step 7: Test the system with sample records"
    -    ],
    -    "resources": [
    -      "Java official docs: https://docs.oracle.com/javase/tutorial",
    -      "OOP concepts in Java: https://www.geeksforgeeks.org/object-oriented-programming-oops-concept-in-java",
    -      "Java file handling: https://www.w3schools.com/java/java_files.asp"
    -    ],
    -    "starter_code": "starter_code/library_management.java"
    -  },
    -  {
    -    "id": 9,
    -    "title": "Real-Time Chat Application",
    -    "skills": [
    -      "JavaScript",
    -      "Node.js"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Web",
    -    "time": "High",
    -    "description": "A real-time chat application that allows multiple users to send and receive instant messages using WebSockets. This project introduces backend communication, event handling, and real-time systems.",
    -    "features": [
    -      "Multiple user chat support",
    -      "Real-time messaging",
    -      "User join and leave notifications",
    -      "Simple responsive chat interface"
    -    ],
    -    "tech_stack": [
    -      "Node.js",
    -      "Express.js",
    -      "Socket.IO",
    -      "HTML",
    -      "CSS"
    -    ],
    -    "roadmap": [
    -      "Step 1: Initialize the Node.js project",
    -      "Step 2: Install Express and Socket.IO",
    -      "Step 3: Create the server using Express",
    -      "Step 4: Build the frontend chat interface",
    -      "Step 5: Implement real-time messaging with Socket.IO",
    -      "Step 6: Add user connection notifications",
    -      "Step 7: Test the application with multiple users"
    -    ],
    -    "resources": [
    -      "Node.js docs: https://nodejs.org/en/docs",
    -      "Socket.IO guide: https://socket.io/docs/v4",
    -      "Express.js documentation: https://expressjs.com"
    -    ],
    -    "starter_code": "starter_code/realtime_chat_app.js"
    -  },
    -  {
    -    "id": 10,
    -    "title": "Password Strength Checker",
    -    "skills": [
    -      "Python"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Cybersecurity",
    -    "time": "Low",
    -    "description": "A tool that checks password strength based on length, symbols, uppercase letters, and numbers. Helps beginners understand input validation and security basics.",
    -    "features": [
    -      "Check password complexity",
    -      "Display strength rating",
    -      "Suggest stronger password improvements",
    -      "Prevent weak password patterns"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "Regex"
    -    ],
    -    "roadmap": [
    -      "Step 1: Create the password input system",
    -      "Step 2: Check password length",
    -      "Step 3: Detect uppercase and lowercase letters",
    -      "Step 4: Detect numbers and symbols",
    -      "Step 5: Create a scoring system",
    -      "Step 6: Display password strength feedback"
    -    ],
    -    "resources": [
    -      "Python regex docs: https://docs.python.org/3/library/re.html",
    -      "OWASP password guidelines: https://owasp.org"
    -    ],
    -    "starter_code": "starter_code/password_checker.py"
    -  },
    -  {
    -    "id": 11,
    -    "title": "Feedback Survey Form",
    -    "skills": [
    -      "HTML"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Web",
    -    "time": "Low",
    -    "description": "A simple student feedback form that collects user names, emails, and ratings. Teaches basic HTML form handling and layout design.",
    -    "features": [
    -      "Collect user name, email, and age with validation",
    -      "Dropdown menu for experience selection",
    -      "Text area for detailed user suggestions"
    -    ],
    -    "tech_stack": [
    -      "HTML"
    -    ],
    -    "roadmap": [
    -      "Step 1: Create the HTML folder structure inside starter_code",
    -      "Step 2: Build the input text fields and labels",
    -      "Step 3: Add select options and textarea elements",
    -      "Step 4: Align the form to the center for better layout",
    -      "Step 5: Test the form using Live Server"
    -    ],
    -    "resources": [
    -      "MDN HTML Forms: https://developer.mozilla.org/en-US/docs/Learn/Forms"
    -    ],
    -    "starter_code": "starter_code/survey_form/index.html"
    -  },
    -  {
    -    "id": 12,
    -    "title": "API ETL Pipeline",
    -    "skills": [
    -      "Python",
    -      "pandas",
    -      "requests"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Data",
    -    "time": "Medium",
    -    "description": "Enter a public API URL to fetch data and automatically transform it into a structured CSV dataset.",
    -    "features": [
    -      "Fetch data from public APIs",
    -      "handle missing values",
    -      "Normalize nested JSON",
    -      "Generate summary statistics",
    -      "Export the processed CSV for any other Analytics projects"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "pandas",
    -      "requests",
    -      "JSON"
    -    ],
    -    "roadmap": [
    -      "Step 1: Install required modules via pip",
    -      "Step 2: Find a public API key for this project",
    -      "Step 3: Fetch the data from the API using requests",
    -      "Step 4: Validate the response you just fetched From the API",
    -      "Step 5: Normalize the nested JSON data by flattening it",
    -      "Step 6: Use the fetched data to build a pandas dataframe",
    -      "Step 7: Handle missing values or duplicate values",
    -      "Step 8: Export the cleaned dataset to CSV format",
    -      "Step 9: Generate a summary for the newly created CSV dataset",
    -      "Step 10: Test the file with at least two different public APIs"
    -    ],
    -    "resources": [
    -      "pandas docs: https://pandas.pydata.org/docs",
    -      "requests docs: https://requests.readthedocs.io/en/latest/",
    -      "JSON handling in Python: https://docs.python.org/3/library/json.html",
    -      "REST API tutorial: https://restfulapi.net/",
    -      "Real Python API guide: https://realpython.com/api-integration-in-python/"
    -    ],
    -    "starter_code": "starter_code/api_data_pipeline.py"
    -  },
    -  {
    -    "id": 13,
    -    "title": "AI Resume Analyzer",
    -    "skills": [
    -      "Python",
    -      "Flask",
    -      "HTML",
    -      "CSS",
    -      "JavaScript"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Data",
    -    "time": "High",
    -    "description": "A Flask web app that compares a resume against a job description using TF-IDF similarity and keyword extraction. Users upload a PDF or paste text, and the app returns a match score, a list of missing keywords, and actionable feedback — with no external AI API required.",
    -    "features": [
    -      "Upload a resume as PDF or paste plain text",
    -      "Paste any job description for comparison",
    -      "TF-IDF cosine similarity match score (0–100%)",
    -      "Missing skills and keyword gap analysis",
    -      "Actionable written feedback based on score",
    -      "Single-page interface with interactive feedback display"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "Flask",
    -      "PyPDF2",
    -      "scikit-learn",
    -      "HTML",
    -      "CSS",
    -      "JavaScript"
    -    ],
    -    "roadmap": [
    -      "Step 1: Run the server and verify the upload form renders",
    -      "Step 2: Complete extract_text_from_pdf() using PyPDF2",
    -      "Step 3: Complete clean_text() to normalise punctuation and whitespace",
    -      "Step 4: Complete extract_keywords() to remove stopwords and count frequency",
    -      "Step 5: Complete calculate_similarity() with TF-IDF and cosine distance",
    -      "Step 6: Complete find_missing_skills() by comparing two keyword sets",
    -      "Step 7: Complete generate_feedback() to produce written suggestions",
    -      "Step 8: Wire everything together inside the /analyze Flask route",
    -      "Step 9: Test with a real resume PDF and a real job posting"
    -    ],
    -    "resources": [
    -      "PyPDF2 documentation: https://pypdf2.readthedocs.io/",
    -      "scikit-learn TF-IDF guide: https://scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weighting",
    -      "Cosine similarity explained: https://www.machinelearningplus.com/nlp/cosine-similarity",
    -      "Flask quickstart: https://flask.palletsprojects.com/quickstart"
    -    ],
    -    "starter_code": "starter_code/ai_resume_analyzer.py"
    -  },
    -  {
    -    "id": 14,
    -    "title": "Number Guessing Game",
    -    "skills": [
    -      "Python"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Games",
    -    "time": "Low",
    -    "description": "A fun command-line game where the computer picks a random number and the user tries to guess it. Great for learning loops, conditionals, and user input handling.",
    -    "features": [
    -      "Generate random number between 1 and 100",
    -      "Give hints: too high or too low",
    -      "Count number of attempts",
    -      "Show final score at the end"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "random module"
    -    ],
    -    "roadmap": [
    -      "Step 1: Set up the Python file and import random module",
    -      "Step 2: Generate a random number using random.randint()",
    -      "Step 3: Write a loop to take user input",
    -      "Step 4: Compare guess with the number",
    -      "Step 5: Give hints if guess is too high or too low",
    -      "Step 6: Count the number of attempts",
    -      "Step 7: Display win message with attempt count"
    -    ],
    -    "resources": [
    -      "Python random module: https://docs.python.org/3/library/random.html",
    -      "W3Schools Python: https://www.w3schools.com/python",
    -      "Real Python: https://realpython.com"
    -    ],
    -    "starter_code": "starter_code/number_guessing.py"
    -  },
    -  {
    -    "id": 15,
    -    "title": "Simple Email Automation",
    -    "skills": [
    -      "Python"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Automation",
    -    "time": "Low",
    -    "description": "A Python script that sends automated emails using the smtplib library. Learn how to automate repetitive tasks and work with Python standard libraries.",
    -    "features": [
    -      "Compose and send emails via Python",
    -      "Send to multiple recipients",
    -      "Add subject and body text",
    -      "Read recipient list from a text file"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "smtplib",
    -      "email module"
    -    ],
    -    "roadmap": [
    -      "Step 1: Set up Python file and import smtplib",
    -      "Step 2: Configure sender email and password",
    -      "Step 3: Write the email composition function",
    -      "Step 4: Connect to Gmail SMTP server",
    -      "Step 5: Send email to one recipient and test",
    -      "Step 6: Read recipient list from a text file",
    -      "Step 7: Loop through recipients and send to all"
    -    ],
    -    "resources": [
    -      "Python smtplib docs: https://docs.python.org/3/library/smtplib.html",
    -      "Real Python email guide: https://realpython.com/python-send-email",
    -      "Gmail SMTP settings: https://support.google.com/mail"
    -    ],
    -    "starter_code": "starter_code/email_automation.py"
    -  },
    -  {
    -    "id": 16,
    -    "title": "Quiz App",
    -    "skills": [
    -      "HTML",
    -      "CSS",
    -      "JavaScript"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Games",
    -    "time": "Low",
    -    "description": "A browser-based quiz app with multiple choice questions, a score counter, and a results screen. Perfect for practising DOM manipulation and event handling in JavaScript.",
    -    "features": [
    -      "Display one question at a time",
    -      "Four multiple choice options per question",
    -      "Show correct or incorrect feedback instantly",
    -      "Display final score on results screen"
    -    ],
    -    "tech_stack": [
    -      "HTML",
    -      "CSS",
    -      "JavaScript"
    -    ],
    -    "roadmap": [
    -      "Step 1: Create HTML structure for question and options",
    -      "Step 2: Style the quiz card with CSS",
    -      "Step 3: Store questions as a JavaScript array of objects",
    -      "Step 4: Write a function to display each question",
    -      "Step 5: Add click event listeners to option buttons",
    -      "Step 6: Check the selected answer and update score",
    -      "Step 7: Move to the next question automatically",
    -      "Step 8: Show the results screen with final score"
    -    ],
    -    "resources": [
    -      "MDN DOM guide: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model",
    -      "JavaScript events: https://javascript.info/events",
    -      "W3Schools JavaScript: https://www.w3schools.com/js"
    -    ],
    -    "starter_code": "starter_code/quiz_app.html"
    -  },
    -  {
    -    "id": 17,
    -    "title": "Movie Recommendation System",
    -    "skills": [
    -      "Python"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Data",
    -    "time": "High",
    -    "description": "A recommendation system that suggests movies to users based on ratings and similarity scores. An intermediate project for learning similarity scoring, pandas data handling, and basic recommendation logic.",
    -    "features": [
    -      "Recommend movies based on user preferences",
    -      "Find similar movies using similarity metrics",
    -      "Load and process movie datasets",
    -      "Display top recommended movies"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "pandas",
    -      "scikit-learn",
    -      "NumPy"
    -    ],
    -    "roadmap": [
    -      "Step 1: Download and inspect a movie ratings dataset",
    -      "Step 2: Load the dataset using pandas",
    -      "Step 3: Clean and preprocess missing data",
    -      "Step 4: Organize movie features into a pandas DataFrame",
    -      "Step 5: Compare movies using cosine similarity",
    -      "Step 6: Build the recommendation function",
    -      "Step 7: Display top movie recommendations to the user",
    -      "Step 8: Test the recommendation system with sample inputs"
    -    ],
    -    "resources": [
    -      "pandas documentation: https://pandas.pydata.org/docs",
    -      "scikit-learn user guide: https://scikit-learn.org/stable/user_guide.html",
    -      "MovieLens dataset: https://grouplens.org/datasets/movielens",
    -      "Cosine Similarity: https://naomy-gomes.medium.com/the-cosine-similarity-and-its-use-in-recommendation-systems-cb2ebd811ce1"
    -    ],
    -    "starter_code": "starter_code/movie_recommender.py"
    -  },
    -  {
    -    "id": 18,
    -    "title": "Sentiment Analysis Web App",
    -    "skills": [
    -      "Python",
    -      "HTML",
    -      "CSS"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Data",
    -    "time": "High",
    -    "description": "A web application that predicts whether user entered text has positive, negative, or neutral sentiment. Great for learning text preprocessing and basic sentiment prediction.",
    -    "features": [
    -      "Analyze sentiment from user input text",
    -      "Display sentiment prediction results",
    -      "Preprocess text using NLP techniques",
    -      "Deploy the model with a simple web interface"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "Flask",
    -      "scikit-learn",
    -      "HTML",
    -      "CSS"
    -    ],
    -    "roadmap": [
    -      "Step 1: Collect or download a sentiment dataset",
    -      "Step 2: Preprocess text data by cleaning and tokenizing",
    -      "Step 3: Convert text into numerical features",
    -      "Step 4: Build a simple sentiment prediction model",
    -      "Step 5: Test predictions with sample text inputs",
    -      "Step 6: Build a Flask app for user interaction",
    -      "Step 7: Connect the trained model to the web app",
    -      "Step 8: Test the application with sample text inputs"
    -    ],
    -    "resources": [
    -      "Flask documentation: https://flask.palletsprojects.com",
    -      "scikit-learn text tutorial: https://scikit-learn.org/stable/",
    -      "NLTK documentation: https://www.nltk.org",
    -      "Tokenization: https://www.geeksforgeeks.org/nlp/nlp-how-tokenizing-text-sentence-words-works/"
    -    ],
    -    "starter_code": "starter_code/sentiment_app.py"
    -  },
    -  {
    -    "id": 19,
    -    "title": "Sunrise Grand Hotel Management System",
    -    "skills": [
    -      "Python"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Business Logic",
    -    "time": "Medium",
    -    "description": "A robust CLI application to manage a 40-room inventory with tiered pricing, guest booking logic, and automatic billing calculation.",
    -    "features": [
    -      "Dynamic 40-room inventory across 4 luxury categories",
    -      "Real-time availability filtering and room status tracking",
    -      "Booking engine with 10-digit contact validation",
    -      "Automatic stay duration and total price calculation",
    -      "Cancellation logic and detailed guest lookup"
    -    ],
    -    "tech_stack": [
    -      "Python",
    -      "Dictionaries",
    -      "Input Validation"
    -    ],
    -    "roadmap": [
    -      "Step 1: Initialize the 40-room inventory with tiered pricing",
    -      "Step 2: Implement availability display and category filtering",
    -      "Step 3: Build the booking engine with contact validation",
    -      "Step 4: Create the cancellation and guest detail lookup logic",
    -      "Step 5: Wrap all logic in a persistent main menu loop"
    -    ],
    -    "resources": [
    -      "Python Dictionaries: https://docs.python.org/3/tutorial/datastructures.html#dictionaries",
    -      "Python Input Validation: https://realpython.com/python-input-output/",
    -      "Menu-driven CLI logic: https://www.geeksforgeeks.org/how-to-create-a-menu-driven-program-in-python/"
    -    ],
    -    "starter_code": "starter_code/hotel_management.py"
    -  },
    -  {
    -    "id": 20,
    -    "title": "CLI URL Shortener",
    -    "skills": [
    -      "Go"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Tools",
    -    "time": "Low",
    -    "description": "A command-line tool that generates short hash codes for long URLs and stores the mappings in a local JSON file. Great for learning Go's standard library, file I/O, and CLI argument handling.",
    -    "features": [
    -      "Accept a URL as a CLI argument",
    -      "Generate a short hash code using MD5",
    -      "Store URL mappings in a local JSON file",
    -      "Resolve a short code back to the original URL"
    -    ],
    -    "tech_stack": [
    -      "Go",
    -      "encoding/json",
    -      "crypto/md5",
    -      "os",
    -      "fmt"
    -    ],
    -    "roadmap": [
    -      "Step 1: Initialise a Go module with go mod init",
    -      "Step 2: Accept a URL as a CLI argument using os.Args",
    -      "Step 3: Generate a short key by hashing the URL with MD5",
    -      "Step 4: Read and write the mappings map to a JSON file",
    -      "Step 5: Add a resolve subcommand to look up a short code",
    -      "Step 6: Handle errors gracefully for missing or invalid input",
    -      "Step 7: Test shorten and resolve with several sample URLs"
    -    ],
    -    "resources": [
    -      "Go Tour: https://tour.golang.org",
    -      "Go by Example: https://gobyexample.com",
    -      "Go standard library docs: https://pkg.go.dev/std"
    -    ],
    -    "starter_code": "starter_code/generated/cli_url_shortener.py"
    -  },
    -  {
    -    "id": 21,
    -    "title": "Go REST API Server",
    -    "skills": [
    -      "Go"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Web",
    -    "time": "Medium",
    -    "description": "A RESTful API server using Go's net/http standard library with full CRUD endpoints for a notes resource. Teaches HTTP routing, JSON encoding, and in-memory data management in Go.",
    -    "features": [
    -      "GET /notes — list all notes",
    -      "POST /notes — create a new note",
    -      "PUT /notes/{id} — update an existing note",
    -      "DELETE /notes/{id} — remove a note",
    -      "JSON request and response handling throughout"
    -    ],
    -    "tech_stack": [
    -      "Go",
    -      "net/http",
    -      "encoding/json",
    -      "strconv"
    -    ],
    -    "roadmap": [
    -      "Step 1: Create main.go and start an HTTP server with http.ListenAndServe",
    -      "Step 2: Define a Note struct with ID, Title, and Body fields",
    -      "Step 3: Use a map as an in-memory store with a counter for IDs",
    -      "Step 4: Write handler functions for each HTTP method",
    -      "Step 5: Register routes using http.HandleFunc",
    -      "Step 6: Parse request bodies with json.NewDecoder",
    -      "Step 7: Write JSON responses with json.NewEncoder",
    -      "Step 8: Test all endpoints with curl or Postman"
    -    ],
    -    "resources": [
    -      "net/http docs: https://pkg.go.dev/net/http",
    -      "Go by Example — HTTP servers: https://gobyexample.com/http-servers",
    -      "Go by Example — JSON: https://gobyexample.com/json"
    -    ],
    -    "starter_code": "starter_code/generated/go_rest_api_server.py"
    -  },
    -  {
    -    "id": 22,
    -    "title": "File Duplicate Finder",
    -    "skills": [
    -      "Rust"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Tools",
    -    "time": "Low",
    -    "description": "A CLI tool that recursively scans a directory, hashes every file using SHA-256, and reports groups of duplicates. A practical first Rust project for learning ownership, iterators, and the standard library.",
    -    "features": [
    -      "Recursively walk a directory with std::fs",
    -      "Hash each file using the sha2 crate",
    -      "Group files by hash and print duplicate sets",
    -      "Accept target directory as a CLI argument"
    -    ],
    -    "tech_stack": [
    -      "Rust",
    -      "std::fs",
    -      "std::collections::HashMap",
    -      "sha2 crate"
    -    ],
    -    "roadmap": [
    -      "Step 1: Create a new project with cargo new and add sha2 to Cargo.toml",
    -      "Step 2: Accept a directory path from std::env::args",
    -      "Step 3: Walk the directory recursively using std::fs::read_dir",
    -      "Step 4: Read each file and compute its SHA-256 hash",
    -      "Step 5: Store results in a HashMap keyed by hash",
    -      "Step 6: Print any hash bucket that contains more than one path",
    -      "Step 7: Test on a folder with known duplicate files"
    -    ],
    -    "resources": [
    -      "The Rust Book: https://doc.rust-lang.org/book/",
    -      "sha2 crate: https://crates.io/crates/sha2",
    -      "Rust std::fs docs: https://doc.rust-lang.org/std/fs/"
    -    ],
    -    "starter_code": "starter_code/generated/file_duplicate_finder.py"
    -  },
    -  {
    -    "id": 23,
    -    "title": "Markdown to HTML Converter",
    -    "skills": [
    -      "Rust"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Tools",
    -    "time": "Medium",
    -    "description": "A CLI tool that reads a Markdown file and produces a styled HTML file using the pulldown-cmark parser. Teaches Rust's crate ecosystem, file handling, and string processing.",
    -    "features": [
    -      "Parse headings, bold, italic, lists, and links",
    -      "Generate a complete and valid HTML file",
    -      "Accept input and output file paths as CLI arguments",
    -      "Wrap output in a minimal HTML template with inline CSS"
    -    ],
    -    "tech_stack": [
    -      "Rust",
    -      "pulldown-cmark",
    -      "std::fs",
    -      "std::env"
    -    ],
    -    "roadmap": [
    -      "Step 1: Create a Cargo project and add pulldown-cmark to Cargo.toml",
    -      "Step 2: Read the input Markdown file path from CLI arguments",
    -      "Step 3: Load the file content into a String with std::fs::read_to_string",
    -      "Step 4: Use pulldown-cmark Parser and push_html to convert to HTML",
    -      "Step 5: Wrap the generated HTML in a full page template",
    -      "Step 6: Write the output to the specified file path",
    -      "Step 7: Test with a Markdown file containing all common elements"
    -    ],
    -    "resources": [
    -      "pulldown-cmark docs: https://docs.rs/pulldown-cmark",
    -      "Rust CLI book: https://rust-cli.github.io/book/",
    -      "The Rust Book: https://doc.rust-lang.org/book/"
    -    ],
    -    "starter_code": "starter_code/generated/markdown_to_html_converter.py"
    -  },
    -  {
    -    "id": 24,
    -    "title": "Android Tip Calculator",
    -    "skills": [
    -      "Kotlin"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Mobile",
    -    "time": "Low",
    -    "description": "An Android app that calculates the tip amount and total bill in real time as the user types a bill amount and adjusts a tip percentage slider. A solid first Kotlin project for learning Android UI basics.",
    -    "features": [
    -      "Input field for the bill amount",
    -      "SeekBar to select tip percentage from 0 to 30",
    -      "Live display of tip amount and total bill",
    -      "Handles empty or invalid input gracefully"
    -    ],
    -    "tech_stack": [
    -      "Kotlin",
    -      "Android SDK",
    -      "View Binding",
    -      "SeekBar"
    -    ],
    -    "roadmap": [
    -      "Step 1: Create a new Android project in Android Studio with Kotlin",
    -      "Step 2: Build the layout with EditText, SeekBar, and TextViews",
    -      "Step 3: Enable View Binding in build.gradle",
    -      "Step 4: Add a TextWatcher to the EditText to react to input changes",
    -      "Step 5: Add a SeekBar.OnSeekBarChangeListener for the slider",
    -      "Step 6: Calculate tip and total on every change event",
    -      "Step 7: Display results and handle the empty-input edge case"
    -    ],
    -    "resources": [
    -      "Android Kotlin Fundamentals: https://developer.android.com/courses/kotlin-android-fundamentals/overview",
    -      "Kotlin docs: https://kotlinlang.org/docs/",
    -      "Android View Binding: https://developer.android.com/topic/libraries/view-binding"
    -    ],
    -    "starter_code": "starter_code/generated/android_tip_calculator.py"
    -  },
    -  {
    -    "id": 25,
    -    "title": "Kotlin Weather App",
    -    "skills": [
    -      "Kotlin"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Mobile",
    -    "time": "Medium",
    -    "description": "An Android weather app that fetches current conditions from the OpenWeatherMap API and displays temperature, humidity, and a weather description. Teaches Retrofit, Kotlin Coroutines, and API integration.",
    -    "features": [
    -      "Search weather by city name",
    -      "Display temperature, humidity, and conditions",
    -      "Show a loading indicator while fetching",
    -      "Display an error message if the city is not found"
    -    ],
    -    "tech_stack": [
    -      "Kotlin",
    -      "Retrofit",
    -      "Kotlin Coroutines",
    -      "OpenWeatherMap API",
    -      "Gson"
    -    ],
    -    "roadmap": [
    -      "Step 1: Create an Android project and add Retrofit and Gson dependencies",
    -      "Step 2: Sign up for a free OpenWeatherMap API key",
    -      "Step 3: Define a data class matching the API JSON response",
    -      "Step 4: Build a Retrofit interface with a suspend GET function",
    -      "Step 5: Launch a coroutine in the button click listener to fetch data",
    -      "Step 6: Show a ProgressBar while the request is in flight",
    -      "Step 7: Update the UI with the result or display an error message"
    -    ],
    -    "resources": [
    -      "Retrofit docs: https://square.github.io/retrofit/",
    -      "Kotlin Coroutines guide: https://kotlinlang.org/docs/coroutines-overview.html",
    -      "OpenWeatherMap API: https://openweathermap.org/api"
    -    ],
    -    "starter_code": "starter_code/generated/kotlin_weather_app.py"
    -  },
    -  {
    -    "id": 26,
    -    "title": "Java Student Grade Tracker",
    -    "skills": [
    -      "Java"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Education",
    -    "time": "Low",
    -    "description": "A console application that stores student names and grades, calculates individual and class averages, and prints a formatted summary report. Great for practising Java OOP basics, ArrayLists, and Scanner input.",
    -    "features": [
    -      "Add students and record multiple grades per student",
    -      "Calculate individual student averages",
    -      "Calculate overall class average",
    -      "Print a formatted report for all students"
    -    ],
    -    "tech_stack": [
    -      "Java",
    -      "ArrayList",
    -      "HashMap",
    -      "Scanner"
    -    ],
    -    "roadmap": [
    -      "Step 1: Set up a Java project in IntelliJ IDEA or VS Code",
    -      "Step 2: Create a Student class with a name field and a grades ArrayList",
    -      "Step 3: Write an addGrade() method and an getAverage() method",
    -      "Step 4: Use a Scanner to accept student names and grades from the user",
    -      "Step 5: Store Student objects in an ArrayList",
    -      "Step 6: Implement a printReport() method that formats the output",
    -      "Step 7: Calculate and display the overall class average at the end"
    -    ],
    -    "resources": [
    -      "Java SE docs: https://docs.oracle.com/en/java/",
    -      "MOOC Java programming: https://java-programming.mooc.fi/",
    -      "W3Schools Java: https://www.w3schools.com/java"
    -    ],
    -    "starter_code": "starter_code/generated/java_student_grade_tracker.py"
    -  },
    -  {
    -    "id": 27,
    -    "title": "Java Bank Account System",
    -    "skills": [
    -      "Java"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Web",
    -    "time": "Medium",
    -    "description": "An OOP-based console banking application with deposit, withdrawal, balance inquiry, and transaction history. Demonstrates inheritance, encapsulation, and polymorphism through a BankAccount class hierarchy.",
    -    "features": [
    -      "BankAccount base class with deposit and withdrawal",
    -      "SavingsAccount subclass with minimum balance enforcement",
    -      "CheckingAccount subclass with overdraft protection",
    -      "Transaction history stored as an ArrayList of strings",
    -      "Menu-driven console interface"
    -    ],
    -    "tech_stack": [
    -      "Java",
    -      "OOP",
    -      "ArrayList",
    -      "Scanner",
    -      "Inheritance"
    -    ],
    -    "roadmap": [
    -      "Step 1: Design the class hierarchy on paper before writing any code",
    -      "Step 2: Implement the BankAccount base class with balance and transaction list",
    -      "Step 3: Write deposit() and withdraw() with input validation",
    -      "Step 4: Create SavingsAccount subclass with minimum balance check",
    -      "Step 5: Create CheckingAccount subclass with overdraft logic",
    -      "Step 6: Build a menu-driven console interface using Scanner",
    -      "Step 7: Add a printHistory() method to display all transactions",
    -      "Step 8: Test each account type with edge case inputs"
    -    ],
    -    "resources": [
    -      "Java OOP tutorial: https://docs.oracle.com/javase/tutorial/java/concepts/",
    -      "MOOC Java programming: https://java-programming.mooc.fi/",
    -      "Java inheritance guide: https://www.w3schools.com/java/java_inheritance.asp"
    -    ],
    -    "starter_code": "starter_code/generated/java_bank_account_system.py"
    -  },
    -  {
    -    "id": 28,
    -    "title": "TypeScript Todo App",
    -    "skills": [
    -      "TypeScript",
    -      "HTML",
    -      "CSS"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Productivity",
    -    "time": "Low",
    -    "description": "A browser-based todo list app written entirely in TypeScript with strongly typed interfaces, DOM manipulation, and in-page state management. A great first TypeScript project that reinforces type safety concepts.",
    -    "features": [
    -      "Add and delete tasks",
    -      "Mark tasks as complete with a strikethrough",
    -      "Filter view by All, Active, or Completed",
    -      "TypeScript interfaces used for all data structures"
    -    ],
    -    "tech_stack": [
    -      "TypeScript",
    -      "HTML",
    -      "CSS",
    -      "tsc compiler"
    -    ],
    -    "roadmap": [
    -      "Step 1: Initialise the project with npm init and install TypeScript",
    -      "Step 2: Create tsconfig.json targeting ES6 with strict mode on",
    -      "Step 3: Define a Task interface with id, title, and completed fields",
    -      "Step 4: Write addTask(), deleteTask(), and toggleTask() functions with full types",
    -      "Step 5: Build the HTML structure for the task list",
    -      "Step 6: Write a render() function that re-draws the list from state",
    -      "Step 7: Compile with tsc and wire up the HTML file",
    -      "Step 8: Test add, delete, toggle, and filter interactions"
    -    ],
    -    "resources": [
    -      "TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/intro.html",
    -      "TypeScript in 5 minutes: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html",
    -      "MDN DOM reference: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model"
    -    ],
    -    "starter_code": "starter_code/generated/typescript_todo_app.py"
    -  },
    -  {
    -    "id": 29,
    -    "title": "TypeScript REST API Client",
    -    "skills": [
    -      "TypeScript"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Web",
    -    "time": "Medium",
    -    "description": "A fully typed API client that queries a public REST API using the Fetch API, maps the response to TypeScript interfaces, and renders the data to the DOM. Teaches type-safe async programming and error handling.",
    -    "features": [
    -      "Typed interfaces matching the API response shape",
    -      "Async fetch wrapper with a typed return value",
    -      "Loading and error state handling",
    -      "Render fetched data to the DOM dynamically"
    -    ],
    -    "tech_stack": [
    -      "TypeScript",
    -      "Fetch API",
    -      "HTML",
    -      "CSS",
    -      "tsconfig"
    -    ],
    -    "roadmap": [
    -      "Step 1: Set up a TypeScript project with tsconfig.json",
    -      "Step 2: Choose a public API (e.g. JSONPlaceholder or Open Library)",
    -      "Step 3: Define interfaces that match the API response structure",
    -      "Step 4: Write an async fetchData() generic function",
    -      "Step 5: Call the function and handle the Promise with try/catch",
    -      "Step 6: Show a loading message while the request is in flight",
    -      "Step 7: Render the typed response data into the DOM",
    -      "Step 8: Display a user-friendly error message on failure"
    -    ],
    -    "resources": [
    -      "TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/",
    -      "Using Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch",
    -      "JSONPlaceholder (free test API): https://jsonplaceholder.typicode.com/"
    -    ],
    -    "starter_code": "starter_code/generated/typescript_rest_api_client.py"
    -  },
    -  {
    -    "id": 30,
    -    "title": "C++ Student Records Manager",
    -    "skills": [
    -      "C++"
    -    ],
    -    "level": "Beginner",
    -    "interest": "Education",
    -    "time": "Low",
    -    "description": "A console application to manage student records using structs, vectors, and file I/O in C++. Ideal for learning C++ fundamentals including structs, loops, and the fstream library.",
    -    "features": [
    -      "Add and display student records",
    -      "Save all records to a text file with ofstream",
    -      "Load existing records from file on startup",
    -      "Search for a student record by name"
    -    ],
    -    "tech_stack": [
    -      "C++",
    -      "struct",
    -      "vector",
    -      "fstream",
    -      "iostream"
    -    ],
    -    "roadmap": [
    -      "Step 1: Define a Student struct with name, roll number, and grade fields",
    -      "Step 2: Use a std::vector to store Student objects in memory",
    -      "Step 3: Write an addStudent() function that reads from cin",
    -      "Step 4: Write a displayAll() function that prints the vector",
    -      "Step 5: Implement saveToFile() using std::ofstream",
    -      "Step 6: Implement loadFromFile() using std::ifstream on startup",
    -      "Step 7: Add a searchByName() function using a linear scan",
    -      "Step 8: Wrap all functionality in a menu-driven main loop"
    -    ],
    -    "resources": [
    -      "LearnCpp: https://www.learncpp.com/",
    -      "cppreference std::fstream: https://en.cppreference.com/w/cpp/io/basic_fstream",
    -      "cppreference std::vector: https://en.cppreference.com/w/cpp/container/vector"
    -    ],
    -    "starter_code": "starter_code/generated/c_student_records_manager.py"
    -  },
    -  {
    -    "id": 31,
    -    "title": "C++ Task Scheduler",
    -    "skills": [
    -      "C++"
    -    ],
    -    "level": "Intermediate",
    -    "interest": "Automation",
    -    "time": "Medium",
    -    "description": "A CLI task scheduler that queues tasks with priority levels and processes them in order using a priority queue from the C++ STL. Teaches STL containers, custom comparators, and structured program design.",
    -    "features": [
    -      "Add tasks with a name and integer priority",
    -      "Process tasks in priority order using std::priority_queue",
    -      "Log each completed task to the console",
    -      "Display the pending queue at any time"
    -    ],
    -    "tech_stack": [
    -      "C++",
    -      "std::priority_queue",
    -      "struct",
    -      "iostream",
    -      "vector"
    -    ],
    -    "roadmap": [
    -      "Step 1: Define a Task struct with name and priority fields",
    -      "Step 2: Write a custom comparator struct for the priority queue",
    -      "Step 3: Implement addTask() to push onto the priority queue",
    -      "Step 4: Implement runNext() to pop and log the top task",
    -      "Step 5: Add a displayQueue() function that copies and drains a temp queue",
    -      "Step 6: Build a menu-driven console interface",
    -      "Step 7: Test with tasks of varying priorities to verify ordering"
    -    ],
    -    "resources": [
    -      "cppreference std::priority_queue: https://en.cppreference.com/w/cpp/container/priority_queue",
    -      "LearnCpp: https://www.learncpp.com/",
    -      "C++ STL tutorial: https://www.geeksforgeeks.org/the-c-standard-template-library-stl/"
    -    ],
    -    "starter_code": "starter_code/generated/c_task_scheduler.py"
    -  }
    -]
    +[
    +  {
    +    "id": 1,
    +    "title": "Personal Expense Tracker",
    +    "skills": ["Python"],
    +    "level": "Beginner",
    +    "interest": "Data",
    +    "time": "Low",
    +    "description": "A command-line tool that helps users track daily expenses, categorize spending, and generate simple summary reports. Great for learning file handling, loops, and basic data processing.",
    +    "features": [
    +      "Add and delete expense entries",
    +      "Categorize expenses (food, transport, bills)",
    +      "View monthly summary",
    +      "Export data to CSV file"
    +    ],
    +    "tech_stack": ["Python", "CSV module", "datetime module"],
    +    "roadmap": [
    +      "Step 1: Set up the project folder and create main.py",
    +      "Step 2: Design the expense data structure as a dictionary",
    +      "Step 3: Write functions to add and delete expenses",
    +      "Step 4: Implement category filtering logic",
    +      "Step 5: Write the summary report generator",
    +      "Step 6: Add CSV export functionality",
    +      "Step 7: Test with sample data and fix bugs"
    +    ],
    +    "resources": [
    +      "Python official docs: https://docs.python.org",
    +      "CSV module guide: https://docs.python.org/3/library/csv.html",
    +      "Real Python beginner tutorials: https://realpython.com"
    +    ],
    +    "starter_code": "starter_code/expense_tracker.py"
    +  },
    +  {
    +    "id": 2,
    +    "title": "Weather Dashboard",
    +    "skills": ["JavaScript", "HTML", "CSS"],
    +    "level": "Beginner",
    +    "interest": "Web",
    +    "time": "Low",
    +    "description": "A simple web page that fetches weather data from a free API and displays current conditions for any city. Teaches API calls, DOM manipulation, and basic UI design.",
    +    "features": [
    +      "Search weather by city name",
    +      "Display temperature, humidity, and conditions",
    +      "Show a weather icon based on conditions",
    +      "Toggle between Celsius and Fahrenheit"
    +    ],
    +    "tech_stack": ["HTML", "CSS", "JavaScript", "OpenWeatherMap API"],
    +    "roadmap": [
    +      "Step 1: Create the HTML structure with a search form",
    +      "Step 2: Style the page with CSS flexbox",
    +      "Step 3: Sign up for a free OpenWeatherMap API key",
    +      "Step 4: Write the fetch() call to get weather data",
    +      "Step 5: Parse the JSON response and extract key fields",
    +      "Step 6: Display the data dynamically using DOM methods",
    +      "Step 7: Add the Celsius/Fahrenheit toggle button"
    +    ],
    +    "resources": [
    +      "MDN Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API",
    +      "OpenWeatherMap free tier: https://openweathermap.org/api",
    +      "CSS Flexbox guide: https://css-tricks.com/snippets/css/a-guide-to-flexbox"
    +    ],
    +    "starter_code": "starter_code/weather_dashboard.html"
    +  },
    +  {
    +    "id": 3,
    +    "title": "Student Grade Manager",
    +    "skills": ["Python"],
    +    "level": "Beginner",
    +    "interest": "Education",
    +    "time": "Medium",
    +    "description": "A Python application to store student names and their grades, compute averages, and display a class report. Ideal for practicing data structures, functions, and file persistence.",
    +    "features": [
    +      "Add students and assign grades per subject",
    +      "Calculate individual and class averages",
    +      "Assign letter grades automatically",
    +      "Save and load data from a JSON file"
    +    ],
    +    "tech_stack": ["Python", "json module", "os module"],
    +    "roadmap": [
    +      "Step 1: Define the student data structure using a dictionary",
    +      "Step 2: Write add_student() and add_grade() functions",
    +      "Step 3: Implement average calculation logic",
    +      "Step 4: Create a letter grade converter function",
    +      "Step 5: Build the JSON save/load functions",
    +      "Step 6: Create a simple text menu for user interaction",
    +      "Step 7: Write a class report printer function"
    +    ],
    +    "resources": [
    +      "Python JSON module: https://docs.python.org/3/library/json.html",
    +      "Python functions tutorial: https://realpython.com/defining-your-own-python-function",
    +      "W3Schools Python: https://www.w3schools.com/python"
    +    ],
    +    "starter_code": "starter_code/grade_manager.py"
    +  },
    +  {
    +    "id": 4,
    +    "title": "Task Manager REST API",
    +    "skills": ["Python"],
    +    "level": "Intermediate",
    +    "interest": "Web",
    +    "time": "Medium",
    +    "description": "A RESTful API built with Flask that allows clients to create, read, update, and delete tasks. Perfect for learning API design, HTTP methods, and JSON responses.",
    +    "features": [
    +      "CRUD endpoints for tasks",
    +      "Filter tasks by status (pending, done)",
    +      "Assign priority levels to tasks",
    +      "Persist data to a JSON file"
    +    ],
    +    "tech_stack": ["Python", "Flask", "JSON", "Postman (for testing)"],
    +    "roadmap": [
    +      "Step 1: Install Flask and create the app.py file",
    +      "Step 2: Define the task data model as a dictionary",
    +      "Step 3: Create the GET /tasks endpoint to list all tasks",
    +      "Step 4: Create the POST /tasks endpoint to add a task",
    +      "Step 5: Create PUT /tasks/ to update a task",
    +      "Step 6: Create DELETE /tasks/ to remove a task",
    +      "Step 7: Add JSON file persistence for saving tasks",
    +      "Step 8: Test all endpoints using Postman or curl"
    +    ],
    +    "resources": [
    +      "Flask quickstart: https://flask.palletsprojects.com/quickstart",
    +      "REST API design guide: https://restfulapi.net",
    +      "Postman download: https://www.postman.com/downloads"
    +    ],
    +    "starter_code": "starter_code/task_api.py"
    +  },
    +  {
    +    "id": 5,
    +    "title": "Portfolio Website",
    +    "skills": ["HTML", "CSS", "JavaScript"],
    +    "level": "Beginner",
    +    "interest": "Web",
    +    "time": "Low",
    +    "description": "A personal portfolio site with sections for bio, projects, and contact. A great first project that teaches HTML layout, CSS styling, and a bit of JavaScript for interactivity.",
    +    "features": [
    +      "Hero section with name and tagline",
    +      "Projects grid with cards",
    +      "Skills list with visual indicators",
    +      "Contact form with basic validation"
    +    ],
    +    "tech_stack": ["HTML", "CSS", "JavaScript"],
    +    "roadmap": [
    +      "Step 1: Plan the page sections on paper first",
    +      "Step 2: Write the HTML structure for all sections",
    +      "Step 3: Add CSS reset and base typography styles",
    +      "Step 4: Style the navigation and hero section",
    +      "Step 5: Build the projects grid using CSS Grid",
    +      "Step 6: Add the contact form with labels and inputs",
    +      "Step 7: Write JavaScript for form validation",
    +      "Step 8: Make the site responsive with media queries"
    +    ],
    +    "resources": [
    +      "HTML reference: https://developer.mozilla.org/en-US/docs/Web/HTML",
    +      "CSS Grid guide: https://css-tricks.com/snippets/css/complete-guide-grid",
    +      "Responsive design basics: https://web.dev/learn/design"
    +    ],
    +    "starter_code": "starter_code/portfolio.html"
    +  },
    +  {
    +    "id": 6,
    +    "title": "URL Shortener",
    +    "skills": ["Python", "JavaScript", "HTML", "CSS"],
    +    "level": "Intermediate",
    +    "interest": "Web",
    +    "time": "High",
    +    "description": "A full-stack web app that takes long URLs and generates short codes. Users can paste a link and get a shorter one back. Teaches Flask routing, random code generation, and front-end form handling.",
    +    "features": [
    +      "Shorten any valid URL",
    +      "Redirect short codes to original URL",
    +      "Track how many times a link was clicked",
    +      "List all shortened links in a dashboard"
    +    ],
    +    "tech_stack": ["Python", "Flask", "HTML", "CSS", "JavaScript", "JSON"],
    +    "roadmap": [
    +      "Step 1: Set up Flask app with two routes: home and redirect",
    +      "Step 2: Write a random 6-character code generator",
    +      "Step 3: Store URL mappings in a JSON file",
    +      "Step 4: Build the HTML form for pasting long URLs",
    +      "Step 5: Display the shortened URL after submission",
    +      "Step 6: Implement the redirect route using the short code",
    +      "Step 7: Add a click counter that updates on each visit",
    +      "Step 8: Build a simple dashboard to list all links"
    +    ],
    +    "resources": [
    +      "Flask routing docs: https://flask.palletsprojects.com/en/stable/quickstart/#routing",
    +      "Python secrets module: https://docs.python.org/3/library/secrets.html",
    +      "UUID in Python: https://docs.python.org/3/library/uuid.html"
    +    ],
    +    "starter_code": "starter_code/url_shortener.py"
    +  },
    +  {
    +    "id": 7,
    +    "title": "Data Analysis Report Generator",
    +    "skills": ["Python"],
    +    "level": "Intermediate",
    +    "interest": "Data",
    +    "time": "High",
    +    "description": "Upload a CSV file and automatically generate a summary report with statistics, missing value counts, and basic charts. A practical project for learning data wrangling and pandas.",
    +    "features": [
    +      "Load and inspect CSV files",
    +      "Show column types and null counts",
    +      "Calculate mean, median, and mode per column",
    +      "Generate bar charts for categorical data"
    +    ],
    +    "tech_stack": ["Python", "pandas", "matplotlib", "os module"],
    +    "roadmap": [
    +      "Step 1: Install pandas and matplotlib via pip",
    +      "Step 2: Write a CSV loader that validates the file path",
    +      "Step 3: Generate a summary table of column info",
    +      "Step 4: Compute descriptive statistics for numeric columns",
    +      "Step 5: Count and display missing values per column",
    +      "Step 6: Build chart generation functions using matplotlib",
    +      "Step 7: Export the full report to a text or HTML file"
    +    ],
    +    "resources": [
    +      "pandas docs: https://pandas.pydata.org/docs",
    +      "matplotlib tutorials: https://matplotlib.org/stable/tutorials",
    +      "Real Python data analysis: https://realpython.com/pandas-dataframe"
    +    ],
    +    "starter_code": "starter_code/data_report.py"
    +  },
    +  {
    +    "id": 8,
    +
    +    "title": "Library Management System",
    +    "skills": ["Java"],
    +    "level": "Beginner",
    +    "interest": "Backend",
    +    "time": "Medium",
    +    "description": "A Java application that helps manage books, students, and borrowing records in a library. This project teaches object-oriented programming concepts, file handling, and menu-driven application design.",
    +    "features": [
    +      "Add and remove books",
    +      "Issue and return books",
    +      "Store student records",
    +      "Search books by title or author"
    +    ],
    +    "tech_stack": ["Java", "OOP", "File Handling"],
    +    "roadmap": [
    +      "Step 1: Create Book and Student classes",
    +      "Step 2: Design the menu-driven interface",
    +      "Step 3: Implement add and remove book features",
    +      "Step 4: Add issue and return book functionality",
    +      "Step 5: Store records using file handling",
    +      "Step 6: Implement search functionality",
    +      "Step 7: Test the system with sample records"
    +    ],
    +    "resources": [
    +      "Java official docs: https://docs.oracle.com/javase/tutorial",
    +      "OOP concepts in Java: https://www.geeksforgeeks.org/object-oriented-programming-oops-concept-in-java",
    +      "Java file handling: https://www.w3schools.com/java/java_files.asp"
    +    ],
    +    "starter_code": "starter_code/library_management.java"
    +  },
    +  {
    +    "id": 9,
    +    "title": "Real-Time Chat Application",
    +    "skills": ["JavaScript", "Node.js"],
    +    "level": "Intermediate",
    +    "interest": "Web",
    +    "time": "High",
    +    "description": "A real-time chat application that allows multiple users to send and receive instant messages using WebSockets. This project introduces backend communication, event handling, and real-time systems.",
    +    "features": [
    +      "Multiple user chat support",
    +      "Real-time messaging",
    +      "User join and leave notifications",
    +      "Simple responsive chat interface"
    +    ],
    +    "tech_stack": ["Node.js", "Express.js", "Socket.IO", "HTML", "CSS"],
    +    "roadmap": [
    +      "Step 1: Initialize the Node.js project",
    +      "Step 2: Install Express and Socket.IO",
    +      "Step 3: Create the server using Express",
    +      "Step 4: Build the frontend chat interface",
    +      "Step 5: Implement real-time messaging with Socket.IO",
    +      "Step 6: Add user connection notifications",
    +      "Step 7: Test the application with multiple users"
    +    ],
    +    "resources": [
    +      "Node.js docs: https://nodejs.org/en/docs",
    +      "Socket.IO guide: https://socket.io/docs/v4",
    +      "Express.js documentation: https://expressjs.com"
    +    ],
    +    "starter_code": "starter_code/realtime_chat_app.js"
    +  },
    +  {
    +    "id": 99,
    +    "title": "Password Strength Checker",
    +    "skills": ["Python"],
    +    "level": "Beginner",
    +    "interest": "Cybersecurity",
    +    "time": "Low",
    +    "description": "A tool that checks password strength based on length, symbols, uppercase letters, and numbers. Helps beginners understand input validation and security basics.",
    +    "features": [
    +      "Check password complexity",
    +      "Display strength rating",
    +      "Suggest stronger password improvements",
    +      "Prevent weak password patterns"
    +    ],
    +    "tech_stack": ["Python", "Regex"],
    +    "roadmap": [
    +      "Step 1: Create the password input system",
    +      "Step 2: Check password length",
    +      "Step 3: Detect uppercase and lowercase letters",
    +      "Step 4: Detect numbers and symbols",
    +      "Step 5: Create a scoring system",
    +      "Step 6: Display password strength feedback"
    +    ],
    +    "resources": [
    +      "Python regex docs: https://docs.python.org/3/library/re.html",
    +      "OWASP password guidelines: https://owasp.org"
    +    ],
    +    "starter_code": "starter_code/password_checker.py"
    +  },
    +  {
    +    "id": 10,
    +    "title": "Feedback Survey Form",
    +    "skills": ["HTML"],
    +    "level": "Beginner",
    +    "interest": "Web",
    +    "time": "Low",
    +    "description": "A simple student feedback form that collects user names, emails, and ratings. Teaches basic HTML form handling and layout design.",
    +    "features": [
    +      "Collect user name, email, and age with validation",
    +      "Dropdown menu for experience selection",
    +      "Text area for detailed user suggestions"
    +    ],
    +    "tech_stack": ["HTML"],
    +    "roadmap": [
    +      "Step 1: Create the HTML folder structure inside starter_code",
    +      "Step 2: Build the input text fields and labels",
    +      "Step 3: Add select options and textarea elements",
    +      "Step 4: Align the form to the center for better layout",
    +      "Step 5: Test the form using Live Server"
    +    ],
    +    "resources": [
    +      "MDN HTML Forms: https://developer.mozilla.org/en-US/docs/Learn/Forms"
    +    ],
    +    "starter_code": "starter_code/survey_form/index.html"
    +  },
    +  {
    +    "id": 98,
    +    "title": "API ETL Pipeline",
    +    "skills": ["Python", "pandas", "requests"],
    +    "level": "Intermediate",
    +    "interest": "Data",
    +    "time": "Medium",
    +    "description": "Enter a public API URL to fetch data and automatically transform it into a structured CSV dataset.",
    +    "features": [
    +      "Fetch data from public APIs",
    +      "handle missing values",
    +      "Normalize nested JSON",
    +      "Generate summary statistics",
    +      "Export the processed CSV for any other Analytics projects"
    +    ],
    +    "tech_stack": ["Python", "pandas", "requests", "JSON"],
    +    "roadmap": [
    +      "Step 1: Install required modules via pip",
    +      "Step 2: Find a public API key for this project",
    +      "Step 3: Fetch the data from the API using requests",
    +      "Step 4: Validate the response you just fetched From the API",
    +      "Step 5: Normalize the nested JSON data by flattening it",
    +      "Step 6: Use the fetched data to build a pandas dataframe",
    +      "Step 7: Handle missing values or duplicate values",
    +      "Step 8: Export the cleaned dataset to CSV format",
    +      "Step 9: Generate a summary for the newly created CSV dataset",
    +      "Step 10: Test the file with at least two different public APIs"
    +    ],
    +    "resources": [
    +      "pandas docs: https://pandas.pydata.org/docs",
    +      "requests docs: https://requests.readthedocs.io/en/latest/",
    +      "JSON handling in Python: https://docs.python.org/3/library/json.html",
    +      "REST API tutorial: https://restfulapi.net/",
    +      "Real Python API guide: https://realpython.com/api-integration-in-python/"
    +    ],
    +    "starter_code": "starter_code/api_data_pipeline.py"
    +  },
    + {
    +  "id": 11,
    +  "title": "AI Resume Analyzer",
    +  "skills": [
    +    "Python",
    +    "Flask",
    +    "HTML",
    +    "CSS",
    +    "JavaScript"
    +  ],
    +  "level": "Intermediate",
    +  "interest": "Data",
    +  "time": "High",
    +  "description": "A Flask web app that compares a resume against a job description using TF-IDF similarity and keyword extraction. Users upload a PDF or paste text, and the app returns a match score, a list of missing keywords, and actionable feedback — with no external AI API required.",
    +  "features": [
    +    "Upload a resume as PDF or paste plain text",
    +    "Paste any job description for comparison",
    +    "TF-IDF cosine similarity match score (0–100%)",
    +    "Missing skills and keyword gap analysis",
    +    "Actionable written feedback based on score",
    +    "Single-page interface with interactive feedback display"
    +  ],
    +  "tech_stack": [
    +    "Python",
    +    "Flask",
    +    "PyPDF2",
    +    "scikit-learn",
    +    "HTML",
    +    "CSS",
    +    "JavaScript"
    +  ],
    +  "roadmap": [
    +    "Step 1: Run the server and verify the upload form renders",
    +    "Step 2: Complete extract_text_from_pdf() using PyPDF2",
    +    "Step 3: Complete clean_text() to normalise punctuation and whitespace",
    +    "Step 4: Complete extract_keywords() to remove stopwords and count frequency",
    +    "Step 5: Complete calculate_similarity() with TF-IDF and cosine distance",
    +    "Step 6: Complete find_missing_skills() by comparing two keyword sets",
    +    "Step 7: Complete generate_feedback() to produce written suggestions",
    +    "Step 8: Wire everything together inside the /analyze Flask route",
    +    "Step 9: Test with a real resume PDF and a real job posting"
    +  ],
    +  "resources": [
    +    "PyPDF2 documentation: https://pypdf2.readthedocs.io/",
    +    "scikit-learn TF-IDF guide: https://scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weighting",
    +    "Cosine similarity explained: https://www.machinelearningplus.com/nlp/cosine-similarity",
    +    "Flask quickstart: https://flask.palletsprojects.com/quickstart"
    +  ],
    +  "starter_code": "starter_code/ai_resume_analyzer.py"
    +  },
    +  {
    +    "id": 12,
    +    "title": "Number Guessing Game",
    +    "skills": ["Python"],
    +    "level": "Beginner",
    +    "interest": "Games",
    +    "time": "Low",
    +    "description": "A fun command-line game where the computer picks a random number and the user tries to guess it. Great for learning loops, conditionals, and user input handling.",
    +    "features": [
    +      "Generate random number between 1 and 100",
    +      "Give hints: too high or too low",
    +      "Count number of attempts",
    +      "Show final score at the end"
    +    ],
    +    "tech_stack": ["Python", "random module"],
    +    "roadmap": [
    +      "Step 1: Set up the Python file and import random module",
    +      "Step 2: Generate a random number using random.randint()",
    +      "Step 3: Write a loop to take user input",
    +      "Step 4: Compare guess with the number",
    +      "Step 5: Give hints if guess is too high or too low",
    +      "Step 6: Count the number of attempts",
    +      "Step 7: Display win message with attempt count"
    +    ],
    +    "resources": [
    +      "Python random module: https://docs.python.org/3/library/random.html",
    +      "W3Schools Python: https://www.w3schools.com/python",
    +      "Real Python: https://realpython.com"
    +    ],
    +    "starter_code": "starter_code/number_guessing.py"
    +  },
    +  {
    +    "id": 13,
    +    "title": "Simple Email Automation",
    +    "skills": ["Python"],
    +    "level": "Beginner",
    +    "interest": "Automation",
    +    "time": "Low",
    +    "description": "A Python script that sends automated emails using the smtplib library. Learn how to automate repetitive tasks and work with Python standard libraries.",
    +    "features": [
    +      "Compose and send emails via Python",
    +      "Send to multiple recipients",
    +      "Add subject and body text",
    +      "Read recipient list from a text file"
    +    ],
    +    "tech_stack": ["Python", "smtplib", "email module"],
    +    "roadmap": [
    +      "Step 1: Set up Python file and import smtplib",
    +      "Step 2: Configure sender email and password",
    +      "Step 3: Write the email composition function",
    +      "Step 4: Connect to Gmail SMTP server",
    +      "Step 5: Send email to one recipient and test",
    +      "Step 6: Read recipient list from a text file",
    +      "Step 7: Loop through recipients and send to all"
    +    ],
    +    "resources": [
    +      "Python smtplib docs: https://docs.python.org/3/library/smtplib.html",
    +      "Real Python email guide: https://realpython.com/python-send-email",
    +      "Gmail SMTP settings: https://support.google.com/mail"
    +    ],
    +    "starter_code": "starter_code/email_automation.py"
    +  },
    +  {
    +    "id": 14,
    +    "title": "Quiz App",
    +    "skills": ["HTML", "CSS", "JavaScript"],
    +    "level": "Beginner",
    +    "interest": "Games",
    +    "time": "Low",
    +    "description": "A browser-based quiz app with multiple choice questions, a score counter, and a results screen. Perfect for practising DOM manipulation and event handling in JavaScript.",
    +    "features": [
    +      "Display one question at a time",
    +      "Four multiple choice options per question",
    +      "Show correct or incorrect feedback instantly",
    +      "Display final score on results screen"
    +    ],
    +    "tech_stack": ["HTML", "CSS", "JavaScript"],
    +    "roadmap": [
    +      "Step 1: Create HTML structure for question and options",
    +      "Step 2: Style the quiz card with CSS",
    +      "Step 3: Store questions as a JavaScript array of objects",
    +      "Step 4: Write a function to display each question",
    +      "Step 5: Add click event listeners to option buttons",
    +      "Step 6: Check the selected answer and update score",
    +      "Step 7: Move to the next question automatically",
    +      "Step 8: Show the results screen with final score"
    +    ],
    +    "resources": [
    +      "MDN DOM guide: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model",
    +      "JavaScript events: https://javascript.info/events",
    +      "W3Schools JavaScript: https://www.w3schools.com/js"
    +    ],
    +    "starter_code": "starter_code/quiz_app.html"
    +  },
    +  {
    +    "id": 15,
    +    "title": "Movie Recommendation System",
    +    "skills": ["Python"],
    +    "level": "Intermediate",
    +    "interest": "Data",
    +    "time": "High",
    +    "description": "A recommendation system that suggests movies to users based on ratings and similarity scores. An intermediate project for learning similarity scoring, pandas data handling, and basic recommendation logic.",
    +    "features": [
    +      "Recommend movies based on user preferences",
    +      "Find similar movies using similarity metrics",
    +      "Load and process movie datasets",
    +      "Display top recommended movies"
    +    ],
    +    "tech_stack": ["Python", "pandas", "scikit-learn", "NumPy"],
    +    "roadmap": [
    +      "Step 1: Download and inspect a movie ratings dataset",
    +      "Step 2: Load the dataset using pandas",
    +      "Step 3: Clean and preprocess missing data",
    +      "Step 4: Organize movie features into a pandas DataFrame",
    +      "Step 5: Compare movies using cosine similarity",
    +      "Step 6: Build the recommendation function",
    +      "Step 7: Display top movie recommendations to the user",
    +      "Step 8: Test the recommendation system with sample inputs"
    +    ],
    +    "resources": [
    +      "pandas documentation: https://pandas.pydata.org/docs",
    +      "scikit-learn user guide: https://scikit-learn.org/stable/user_guide.html",
    +      "MovieLens dataset: https://grouplens.org/datasets/movielens",
    +      "Cosine Similarity: https://naomy-gomes.medium.com/the-cosine-similarity-and-its-use-in-recommendation-systems-cb2ebd811ce1"
    +    ],
    +    "starter_code": "starter_code/movie_recommender.py"
    +  }, 
    +  {
    +    "id": 16,
    +    "title": "Sentiment Analysis Web App",
    +    "skills": ["Python", "HTML", "CSS"],
    +    "level": "Intermediate",
    +    "interest": "Data",
    +    "time": "High",
    +    "description": "A web application that predicts whether user entered text has positive, negative, or neutral sentiment. Great for learning text preprocessing and basic sentiment prediction.",
    +    "features": [
    +      "Analyze sentiment from user input text",
    +      "Display sentiment prediction results",
    +      "Preprocess text using NLP techniques",
    +      "Deploy the model with a simple web interface"
    +    ],
    +    "tech_stack": ["Python", "Flask", "scikit-learn", "HTML", "CSS"],
    +    "roadmap": [
    +      "Step 1: Collect or download a sentiment dataset",
    +      "Step 2: Preprocess text data by cleaning and tokenizing",
    +      "Step 3: Convert text into numerical features",
    +      "Step 4: Build a simple sentiment prediction model",
    +      "Step 5: Test predictions with sample text inputs",
    +      "Step 6: Build a Flask app for user interaction",
    +      "Step 7: Connect the trained model to the web app",
    +      "Step 8: Test the application with sample text inputs"
    +    ],
    +    "resources": [
    +      "Flask documentation: https://flask.palletsprojects.com",
    +      "scikit-learn text tutorial: https://scikit-learn.org/stable/",
    +      "NLTK documentation: https://www.nltk.org",
    +      "Tokenization: https://www.geeksforgeeks.org/nlp/nlp-how-tokenizing-text-sentence-words-works/"
    +    ],
    +    "starter_code": "starter_code/sentiment_app.py"
    +  }
    +  ,
    +  {
    +    "id": 17,
    +    "title": "Sunrise Grand Hotel Management System",
    +    "skills": ["Python"],
    +    "level": "Medium",
    +    "interest": "Business Logic",
    +    "time": "Medium",
    +    "description": "A robust CLI application to manage a 40-room inventory with tiered pricing, guest booking logic, and automatic billing calculation.",
    +    "features": [
    +      "Dynamic 40-room inventory across 4 luxury categories",
    +      "Real-time availability filtering and room status tracking",
    +      "Booking engine with 10-digit contact validation",
    +      "Automatic stay duration and total price calculation",
    +      "Cancellation logic and detailed guest lookup"
    +    ],
    +    "tech_stack": ["Python", "Dictionaries", "Input Validation"],
    +    "roadmap": [
    +      "Step 1: Initialize the 40-room inventory with tiered pricing",
    +      "Step 2: Implement availability display and category filtering",
    +      "Step 3: Build the booking engine with contact validation",
    +      "Step 4: Create the cancellation and guest detail lookup logic",
    +      "Step 5: Wrap all logic in a persistent main menu loop"
    +    ],
    +    "resources": [
    +      "Python Dictionaries: https://docs.python.org/3/tutorial/datastructures.html#dictionaries",
    +      "Python Input Validation: https://realpython.com/python-input-output/",
    +      "Menu-driven CLI logic: https://www.geeksforgeeks.org/how-to-create-a-menu-driven-program-in-python/"
    +    ],
    +    "starter_code": "starter_code/hotel_management.py"
    +  },
    +  {
    +    "id": 18,
    +    "title": "CLI URL Shortener",
    +    "skills": ["Go"],
    +    "level": "Beginner",
    +    "interest": "Tools",
    +    "time": "Low",
    +    "description": "A command-line tool that generates short hash codes for long URLs and stores the mappings in a local JSON file. Great for learning Go's standard library, file I/O, and CLI argument handling.",
    +    "features": [
    +      "Accept a URL as a CLI argument",
    +      "Generate a short hash code using MD5",
    +      "Store URL mappings in a local JSON file",
    +      "Resolve a short code back to the original URL"
    +    ],
    +    "tech_stack": ["Go", "encoding/json", "crypto/md5", "os", "fmt"],
    +    "roadmap": [
    +      "Step 1: Initialise a Go module with go mod init",
    +      "Step 2: Accept a URL as a CLI argument using os.Args",
    +      "Step 3: Generate a short key by hashing the URL with MD5",
    +      "Step 4: Read and write the mappings map to a JSON file",
    +      "Step 5: Add a resolve subcommand to look up a short code",
    +      "Step 6: Handle errors gracefully for missing or invalid input",
    +      "Step 7: Test shorten and resolve with several sample URLs"
    +    ],
    +    "resources": [
    +      "Go Tour: https://tour.golang.org",
    +      "Go by Example: https://gobyexample.com",
    +      "Go standard library docs: https://pkg.go.dev/std"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 19,
    +    "title": "Go REST API Server",
    +    "skills": ["Go"],
    +    "level": "Intermediate",
    +    "interest": "Web",
    +    "time": "Medium",
    +    "description": "A RESTful API server using Go's net/http standard library with full CRUD endpoints for a notes resource. Teaches HTTP routing, JSON encoding, and in-memory data management in Go.",
    +    "features": [
    +      "GET /notes — list all notes",
    +      "POST /notes — create a new note",
    +      "PUT /notes/{id} — update an existing note",
    +      "DELETE /notes/{id} — remove a note",
    +      "JSON request and response handling throughout"
    +    ],
    +    "tech_stack": ["Go", "net/http", "encoding/json", "strconv"],
    +    "roadmap": [
    +      "Step 1: Create main.go and start an HTTP server with http.ListenAndServe",
    +      "Step 2: Define a Note struct with ID, Title, and Body fields",
    +      "Step 3: Use a map as an in-memory store with a counter for IDs",
    +      "Step 4: Write handler functions for each HTTP method",
    +      "Step 5: Register routes using http.HandleFunc",
    +      "Step 6: Parse request bodies with json.NewDecoder",
    +      "Step 7: Write JSON responses with json.NewEncoder",
    +      "Step 8: Test all endpoints with curl or Postman"
    +    ],
    +    "resources": [
    +      "net/http docs: https://pkg.go.dev/net/http",
    +      "Go by Example — HTTP servers: https://gobyexample.com/http-servers",
    +      "Go by Example — JSON: https://gobyexample.com/json"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 20,
    +    "title": "File Duplicate Finder",
    +    "skills": ["Rust"],
    +    "level": "Beginner",
    +    "interest": "Tools",
    +    "time": "Low",
    +    "description": "A CLI tool that recursively scans a directory, hashes every file using SHA-256, and reports groups of duplicates. A practical first Rust project for learning ownership, iterators, and the standard library.",
    +    "features": [
    +      "Recursively walk a directory with std::fs",
    +      "Hash each file using the sha2 crate",
    +      "Group files by hash and print duplicate sets",
    +      "Accept target directory as a CLI argument"
    +    ],
    +    "tech_stack": ["Rust", "std::fs", "std::collections::HashMap", "sha2 crate"],
    +    "roadmap": [
    +      "Step 1: Create a new project with cargo new and add sha2 to Cargo.toml",
    +      "Step 2: Accept a directory path from std::env::args",
    +      "Step 3: Walk the directory recursively using std::fs::read_dir",
    +      "Step 4: Read each file and compute its SHA-256 hash",
    +      "Step 5: Store results in a HashMap keyed by hash",
    +      "Step 6: Print any hash bucket that contains more than one path",
    +      "Step 7: Test on a folder with known duplicate files"
    +    ],
    +    "resources": [
    +      "The Rust Book: https://doc.rust-lang.org/book/",
    +      "sha2 crate: https://crates.io/crates/sha2",
    +      "Rust std::fs docs: https://doc.rust-lang.org/std/fs/"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 21,
    +    "title": "Markdown to HTML Converter",
    +    "skills": ["Rust"],
    +    "level": "Intermediate",
    +    "interest": "Tools",
    +    "time": "Medium",
    +    "description": "A CLI tool that reads a Markdown file and produces a styled HTML file using the pulldown-cmark parser. Teaches Rust's crate ecosystem, file handling, and string processing.",
    +    "features": [
    +      "Parse headings, bold, italic, lists, and links",
    +      "Generate a complete and valid HTML file",
    +      "Accept input and output file paths as CLI arguments",
    +      "Wrap output in a minimal HTML template with inline CSS"
    +    ],
    +    "tech_stack": ["Rust", "pulldown-cmark", "std::fs", "std::env"],
    +    "roadmap": [
    +      "Step 1: Create a Cargo project and add pulldown-cmark to Cargo.toml",
    +      "Step 2: Read the input Markdown file path from CLI arguments",
    +      "Step 3: Load the file content into a String with std::fs::read_to_string",
    +      "Step 4: Use pulldown-cmark Parser and push_html to convert to HTML",
    +      "Step 5: Wrap the generated HTML in a full page template",
    +      "Step 6: Write the output to the specified file path",
    +      "Step 7: Test with a Markdown file containing all common elements"
    +    ],
    +    "resources": [
    +      "pulldown-cmark docs: https://docs.rs/pulldown-cmark",
    +      "Rust CLI book: https://rust-cli.github.io/book/",
    +      "The Rust Book: https://doc.rust-lang.org/book/"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 22,
    +    "title": "Android Tip Calculator",
    +    "skills": ["Kotlin"],
    +    "level": "Beginner",
    +    "interest": "Mobile",
    +    "time": "Low",
    +    "description": "An Android app that calculates the tip amount and total bill in real time as the user types a bill amount and adjusts a tip percentage slider. A solid first Kotlin project for learning Android UI basics.",
    +    "features": [
    +      "Input field for the bill amount",
    +      "SeekBar to select tip percentage from 0 to 30",
    +      "Live display of tip amount and total bill",
    +      "Handles empty or invalid input gracefully"
    +    ],
    +    "tech_stack": ["Kotlin", "Android SDK", "View Binding", "SeekBar"],
    +    "roadmap": [
    +      "Step 1: Create a new Android project in Android Studio with Kotlin",
    +      "Step 2: Build the layout with EditText, SeekBar, and TextViews",
    +      "Step 3: Enable View Binding in build.gradle",
    +      "Step 4: Add a TextWatcher to the EditText to react to input changes",
    +      "Step 5: Add a SeekBar.OnSeekBarChangeListener for the slider",
    +      "Step 6: Calculate tip and total on every change event",
    +      "Step 7: Display results and handle the empty-input edge case"
    +    ],
    +    "resources": [
    +      "Android Kotlin Fundamentals: https://developer.android.com/courses/kotlin-android-fundamentals/overview",
    +      "Kotlin docs: https://kotlinlang.org/docs/",
    +      "Android View Binding: https://developer.android.com/topic/libraries/view-binding"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 23,
    +    "title": "Kotlin Weather App",
    +    "skills": ["Kotlin"],
    +    "level": "Intermediate",
    +    "interest": "Mobile",
    +    "time": "Medium",
    +    "description": "An Android weather app that fetches current conditions from the OpenWeatherMap API and displays temperature, humidity, and a weather description. Teaches Retrofit, Kotlin Coroutines, and API integration.",
    +    "features": [
    +      "Search weather by city name",
    +      "Display temperature, humidity, and conditions",
    +      "Show a loading indicator while fetching",
    +      "Display an error message if the city is not found"
    +    ],
    +    "tech_stack": ["Kotlin", "Retrofit", "Kotlin Coroutines", "OpenWeatherMap API", "Gson"],
    +    "roadmap": [
    +      "Step 1: Create an Android project and add Retrofit and Gson dependencies",
    +      "Step 2: Sign up for a free OpenWeatherMap API key",
    +      "Step 3: Define a data class matching the API JSON response",
    +      "Step 4: Build a Retrofit interface with a suspend GET function",
    +      "Step 5: Launch a coroutine in the button click listener to fetch data",
    +      "Step 6: Show a ProgressBar while the request is in flight",
    +      "Step 7: Update the UI with the result or display an error message"
    +    ],
    +    "resources": [
    +      "Retrofit docs: https://square.github.io/retrofit/",
    +      "Kotlin Coroutines guide: https://kotlinlang.org/docs/coroutines-overview.html",
    +      "OpenWeatherMap API: https://openweathermap.org/api"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 24,
    +    "title": "Java Student Grade Tracker",
    +    "skills": ["Java"],
    +    "level": "Beginner",
    +    "interest": "Education",
    +    "time": "Low",
    +    "description": "A console application that stores student names and grades, calculates individual and class averages, and prints a formatted summary report. Great for practising Java OOP basics, ArrayLists, and Scanner input.",
    +    "features": [
    +      "Add students and record multiple grades per student",
    +      "Calculate individual student averages",
    +      "Calculate overall class average",
    +      "Print a formatted report for all students"
    +    ],
    +    "tech_stack": ["Java", "ArrayList", "HashMap", "Scanner"],
    +    "roadmap": [
    +      "Step 1: Set up a Java project in IntelliJ IDEA or VS Code",
    +      "Step 2: Create a Student class with a name field and a grades ArrayList",
    +      "Step 3: Write an addGrade() method and an getAverage() method",
    +      "Step 4: Use a Scanner to accept student names and grades from the user",
    +      "Step 5: Store Student objects in an ArrayList",
    +      "Step 6: Implement a printReport() method that formats the output",
    +      "Step 7: Calculate and display the overall class average at the end"
    +    ],
    +    "resources": [
    +      "Java SE docs: https://docs.oracle.com/en/java/",
    +      "MOOC Java programming: https://java-programming.mooc.fi/",
    +      "W3Schools Java: https://www.w3schools.com/java"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 25,
    +    "title": "Java Bank Account System",
    +    "skills": ["Java"],
    +    "level": "Intermediate",
    +    "interest": "Web",
    +    "time": "Medium",
    +    "description": "An OOP-based console banking application with deposit, withdrawal, balance inquiry, and transaction history. Demonstrates inheritance, encapsulation, and polymorphism through a BankAccount class hierarchy.",
    +    "features": [
    +      "BankAccount base class with deposit and withdrawal",
    +      "SavingsAccount subclass with minimum balance enforcement",
    +      "CheckingAccount subclass with overdraft protection",
    +      "Transaction history stored as an ArrayList of strings",
    +      "Menu-driven console interface"
    +    ],
    +    "tech_stack": ["Java", "OOP", "ArrayList", "Scanner", "Inheritance"],
    +    "roadmap": [
    +      "Step 1: Design the class hierarchy on paper before writing any code",
    +      "Step 2: Implement the BankAccount base class with balance and transaction list",
    +      "Step 3: Write deposit() and withdraw() with input validation",
    +      "Step 4: Create SavingsAccount subclass with minimum balance check",
    +      "Step 5: Create CheckingAccount subclass with overdraft logic",
    +      "Step 6: Build a menu-driven console interface using Scanner",
    +      "Step 7: Add a printHistory() method to display all transactions",
    +      "Step 8: Test each account type with edge case inputs"
    +    ],
    +    "resources": [
    +      "Java OOP tutorial: https://docs.oracle.com/javase/tutorial/java/concepts/",
    +      "MOOC Java programming: https://java-programming.mooc.fi/",
    +      "Java inheritance guide: https://www.w3schools.com/java/java_inheritance.asp"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 26,
    +    "title": "TypeScript Todo App",
    +    "skills": ["TypeScript", "HTML", "CSS"],
    +    "level": "Beginner",
    +    "interest": "Productivity",
    +    "time": "Low",
    +    "description": "A browser-based todo list app written entirely in TypeScript with strongly typed interfaces, DOM manipulation, and in-page state management. A great first TypeScript project that reinforces type safety concepts.",
    +    "features": [
    +      "Add and delete tasks",
    +      "Mark tasks as complete with a strikethrough",
    +      "Filter view by All, Active, or Completed",
    +      "TypeScript interfaces used for all data structures"
    +    ],
    +    "tech_stack": ["TypeScript", "HTML", "CSS", "tsc compiler"],
    +    "roadmap": [
    +      "Step 1: Initialise the project with npm init and install TypeScript",
    +      "Step 2: Create tsconfig.json targeting ES6 with strict mode on",
    +      "Step 3: Define a Task interface with id, title, and completed fields",
    +      "Step 4: Write addTask(), deleteTask(), and toggleTask() functions with full types",
    +      "Step 5: Build the HTML structure for the task list",
    +      "Step 6: Write a render() function that re-draws the list from state",
    +      "Step 7: Compile with tsc and wire up the HTML file",
    +      "Step 8: Test add, delete, toggle, and filter interactions"
    +    ],
    +    "resources": [
    +      "TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/intro.html",
    +      "TypeScript in 5 minutes: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html",
    +      "MDN DOM reference: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 27,
    +    "title": "TypeScript REST API Client",
    +    "skills": ["TypeScript"],
    +    "level": "Intermediate",
    +    "interest": "Web",
    +    "time": "Medium",
    +    "description": "A fully typed API client that queries a public REST API using the Fetch API, maps the response to TypeScript interfaces, and renders the data to the DOM. Teaches type-safe async programming and error handling.",
    +    "features": [
    +      "Typed interfaces matching the API response shape",
    +      "Async fetch wrapper with a typed return value",
    +      "Loading and error state handling",
    +      "Render fetched data to the DOM dynamically"
    +    ],
    +    "tech_stack": ["TypeScript", "Fetch API", "HTML", "CSS", "tsconfig"],
    +    "roadmap": [
    +      "Step 1: Set up a TypeScript project with tsconfig.json",
    +      "Step 2: Choose a public API (e.g. JSONPlaceholder or Open Library)",
    +      "Step 3: Define interfaces that match the API response structure",
    +      "Step 4: Write an async fetchData() generic function",
    +      "Step 5: Call the function and handle the Promise with try/catch",
    +      "Step 6: Show a loading message while the request is in flight",
    +      "Step 7: Render the typed response data into the DOM",
    +      "Step 8: Display a user-friendly error message on failure"
    +    ],
    +    "resources": [
    +      "TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/",
    +      "Using Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch",
    +      "JSONPlaceholder (free test API): https://jsonplaceholder.typicode.com/"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 28,
    +    "title": "C++ Student Records Manager",
    +    "skills": ["C++"],
    +    "level": "Beginner",
    +    "interest": "Education",
    +    "time": "Low",
    +    "description": "A console application to manage student records using structs, vectors, and file I/O in C++. Ideal for learning C++ fundamentals including structs, loops, and the fstream library.",
    +    "features": [
    +      "Add and display student records",
    +      "Save all records to a text file with ofstream",
    +      "Load existing records from file on startup",
    +      "Search for a student record by name"
    +    ],
    +    "tech_stack": ["C++", "struct", "vector", "fstream", "iostream"],
    +    "roadmap": [
    +      "Step 1: Define a Student struct with name, roll number, and grade fields",
    +      "Step 2: Use a std::vector to store Student objects in memory",
    +      "Step 3: Write an addStudent() function that reads from cin",
    +      "Step 4: Write a displayAll() function that prints the vector",
    +      "Step 5: Implement saveToFile() using std::ofstream",
    +      "Step 6: Implement loadFromFile() using std::ifstream on startup",
    +      "Step 7: Add a searchByName() function using a linear scan",
    +      "Step 8: Wrap all functionality in a menu-driven main loop"
    +    ],
    +    "resources": [
    +      "LearnCpp: https://www.learncpp.com/",
    +      "cppreference std::fstream: https://en.cppreference.com/w/cpp/io/basic_fstream",
    +      "cppreference std::vector: https://en.cppreference.com/w/cpp/container/vector"
    +    ],
    +    "starter_code": null
    +  },
    +  {
    +    "id": 29,
    +    "title": "C++ Task Scheduler",
    +    "skills": ["C++"],
    +    "level": "Intermediate",
    +    "interest": "Automation",
    +    "time": "Medium",
    +    "description": "A CLI task scheduler that queues tasks with priority levels and processes them in order using a priority queue from the C++ STL. Teaches STL containers, custom comparators, and structured program design.",
    +    "features": [
    +      "Add tasks with a name and integer priority",
    +      "Process tasks in priority order using std::priority_queue",
    +      "Log each completed task to the console",
    +      "Display the pending queue at any time"
    +    ],
    +    "tech_stack": ["C++", "std::priority_queue", "struct", "iostream", "vector"],
    +    "roadmap": [
    +      "Step 1: Define a Task struct with name and priority fields",
    +      "Step 2: Write a custom comparator struct for the priority queue",
    +      "Step 3: Implement addTask() to push onto the priority queue",
    +      "Step 4: Implement runNext() to pop and log the top task",
    +      "Step 5: Add a displayQueue() function that copies and drains a temp queue",
    +      "Step 6: Build a menu-driven console interface",
    +      "Step 7: Test with tasks of varying priorities to verify ordering"
    +    ],
    +    "resources": [
    +      "cppreference std::priority_queue: https://en.cppreference.com/w/cpp/container/priority_queue",
    +      "LearnCpp: https://www.learncpp.com/",
    +      "C++ STL tutorial: https://www.geeksforgeeks.org/the-c-standard-template-library-stl/"
    +    ],
    +    "starter_code": null
    +  }
    +]
    +
    +
    diff --git a/utils/data_loader.py b/utils/data_loader.py
    index bc529a3b..17da28ef 100644
    --- a/utils/data_loader.py
    +++ b/utils/data_loader.py
    @@ -6,6 +6,9 @@
     _projects_cache = None
     _cache_lock = threading.Lock()
     
    +_projects_cache = None
    +_cache_lock = threading.Lock()
    +
     def validate_projects(projects):
         """
         Validate project dataset integrity.