From 8dea5a0191369b1f2fb5361907ef6f64460257e7 Mon Sep 17 00:00:00 2001 From: Srinath Choul Date: Tue, 26 May 2026 17:33:35 +0530 Subject: [PATCH 1/8] fix: make the github import button functional --- DevPath/CHANGELOG.md | 20 + DevPath/static/script.js | 935 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 955 insertions(+) create mode 100644 DevPath/CHANGELOG.md create mode 100644 DevPath/static/script.js diff --git a/DevPath/CHANGELOG.md b/DevPath/CHANGELOG.md new file mode 100644 index 00000000..443c7915 --- /dev/null +++ b/DevPath/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] + +### Added + +- Initial CHANGELOG.md setup for tracking project history +- Documentation structure for future contributor updates + +### Changed + +- Contributors are now expected to document user-facing changes in CHANGELOG.md + +### Fixed + +- Fixed the 'Import from GitHub' modal to correctly fetch skills and handle invalid usernames \ No newline at end of file diff --git a/DevPath/static/script.js b/DevPath/static/script.js new file mode 100644 index 00000000..494f4e5e --- /dev/null +++ b/DevPath/static/script.js @@ -0,0 +1,935 @@ +// script.js — DevPath client-side logic +// +// Responsibilities: +// - Mobile navigation toggle +// - Skill chip manager (add/remove skills) +// - Form validation with per-field error messages +// - Recommendation API call and loading states +// - Result card rendering +// - Code viewer panel (detail page) + +// ============================================================ +// Detect which page we are on +// ============================================================ +// !! trick turns the DOM result into a simple true/false +var isIndexPage = !!document.getElementById("recommend-form"); +// PROJECT_ID is set by the server only on detail pages, so if it's missing we're elsewhere +var isDetailPage = typeof PROJECT_ID !== "undefined"; +var modal = document.getElementById('github-modal-overlay'); +var openModalBtn = document.getElementById('btn-show-github'); // The trigger in your main form +var closeModalBtn = document.getElementById('btn-close-github'); +var fetchBtn = document.getElementById('btn-fetch-github'); +var githubInput = document.getElementById('github-username'); +var errorMsg = document.getElementById('github-modal-error'); + + +// ============================================================ +// 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 + + // Nothing to do if the nav isn't on this page, just bail out + if (!toggle || !menu) return; + + toggle.addEventListener("click", function () { + // classList.toggle returns true if class was added, false if removed + var isOpen = menu.classList.toggle("open"); + toggle.classList.toggle("open", isOpen); + // 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 + menu.querySelectorAll(".nav-mobile-link").forEach(function (link) { + link.addEventListener("click", function () { + menu.classList.remove("open"); + toggle.classList.remove("open"); + }); + }); +})(); + + +// ============================================================ +// INDEX PAGE +// ============================================================ +if (isIndexPage) { + + // DOM references + // 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 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 + var quickPickChips = document.querySelectorAll(".skill-chip"); // predefined skills user can click + + // 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) { + quickPickChips.forEach(function(chip) { + chip.classList.remove("active", "selected"); + }); + } + } + }); +} + + + // ---------------------------------------------------------- + // Skill chip manager + // ---------------------------------------------------------- + + // Skills list for autocomplete (from skills.js) + var availableSkills = []; + if (typeof skills !== "undefined" && Array.isArray(skills) && skills.length > 0) { + availableSkills = skills.map(function (s) { return s.label; }); + } else { + // Fallback if skills.js doesn't load + availableSkills = [ + "Python", "JavaScript", "Java", "C++", "HTML", "CSS", "React", "Node.js", + "Django", "Flask", "SQL", "MongoDB", "AWS", "Docker", "Kubernetes", "Git", + "C#", "Ruby", "PHP", "Go", "Swift", "TypeScript", "Angular", "Vue.js", + "Spring", "Flutter", "TensorFlow", "PyTorch", "Data Science", + "Machine Learning", "Artificial Intelligence", "DevOps", "Cybersecurity", + "Blockchain", "UI/UX Design", "Game Development", "CI/CD", "REST API", "GraphQL", + "Rust", "Kotlin" + ]; + } + + var suggestionsDiv = document.getElementById("skills-suggestions"); + var skillWrap = document.getElementById("skill-input-wrap"); + var visibleSuggestions = []; + var activeSuggestionIndex = -1; + + function initSkillStripMarquee() { + var marquee = document.querySelector(".skill-strip-marquee"); + var track = marquee && marquee.querySelector(".skill-strip-track"); + + if (!marquee || !track || track.querySelector(".skill-strip-items[data-marquee-clone='true']")) { + return; + } + + var clone = track.querySelector(".skill-strip-items").cloneNode(true); + clone.setAttribute("aria-hidden", "true"); + clone.setAttribute("data-marquee-clone", "true"); + track.appendChild(clone); + } + + 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"); + } + + initSkillStripMarquee(); + + function normalizeSkill(skill) { + return skill.trim().toLowerCase(); + } + + function isSkillSelected(skill) { + var normalizedSkill = normalizeSkill(skill); + return selectedSkills.some(function (selectedSkill) { + return normalizeSkill(selectedSkill) === normalizedSkill; + }); + } + + function getCanonicalSkill(rawSkill) { + var normalizedSkill = normalizeSkill(rawSkill); + var matchedSkill = availableSkills.find(function (skill) { + return normalizeSkill(skill) === normalizedSkill; + }); + return matchedSkill || rawSkill.trim(); + } + + function getFilteredSkills(query) { + var normalizedQuery = normalizeSkill(query); + return availableSkills.filter(function (skill) { + return normalizeSkill(skill).includes(normalizedQuery) && !isSkillSelected(skill); + }).slice(0, 8); + } + + function syncSuggestionsA11yState() { + skillsTextInput.setAttribute("aria-expanded", visibleSuggestions.length > 0 ? "true" : "false"); + } + + function renderActiveSuggestion() { + if (!suggestionsDiv) return; + 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"); + }); + } + + function hideSuggestions() { + visibleSuggestions = []; + activeSuggestionIndex = -1; + if (suggestionsDiv) { + suggestionsDiv.style.display = "none"; + suggestionsDiv.innerHTML = ""; + } + syncSuggestionsA11yState(); + } + + function selectSuggestion(skill) { + addSkill(skill); + skillsTextInput.value = ""; + hideSuggestions(); + skillsTextInput.focus(); + } + + function displaySuggestions(items) { + if (!suggestionsDiv) return; + visibleSuggestions = items; + activeSuggestionIndex = -1; + if (items.length === 0) { + hideSuggestions(); + return; + } + suggestionsDiv.innerHTML = ""; + items.forEach(function (skill, index) { + var item = document.createElement("div"); + item.className = "suggestion-item"; + item.textContent = skill; + item.setAttribute("role", "option"); + item.setAttribute("id", "skills-suggestion-" + index); + item.setAttribute("aria-selected", "false"); + + // Prevent the input blur handler from closing the menu before click runs. + item.addEventListener("mousedown", function (evt) { + evt.preventDefault(); + }); + + item.addEventListener("mouseenter", function () { + activeSuggestionIndex = index; + renderActiveSuggestion(); + }); + + item.addEventListener("click", function () { + selectSuggestion(skill); + }); + + suggestionsDiv.appendChild(item); + }); + suggestionsDiv.style.display = "block"; + syncSuggestionsA11yState(); + } + + function updateQuickPickState() { + 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 + // when the user types a skill and hits Enter, add it we intercept Enter here so it doesn't accidentally submit the whole form + skillsTextInput.addEventListener("keydown", function (evt) { + if (evt.key === "ArrowDown" || evt.key === "ArrowUp") { + if (visibleSuggestions.length === 0) { + displaySuggestions(getFilteredSkills(skillsTextInput.value)); + } + if (visibleSuggestions.length === 0) return; + evt.preventDefault(); + if (evt.key === "ArrowDown") { + activeSuggestionIndex = (activeSuggestionIndex + 1) % visibleSuggestions.length; + } else { + activeSuggestionIndex = activeSuggestionIndex <= 0 + ? visibleSuggestions.length - 1 + : activeSuggestionIndex - 1; + } + renderActiveSuggestion(); + return; + } + + if (evt.key === "Escape") { + hideSuggestions(); + return; + } + + if (evt.key === "Enter") { + evt.preventDefault(); + if (activeSuggestionIndex >= 0 && visibleSuggestions[activeSuggestionIndex]) { + selectSuggestion(visibleSuggestions[activeSuggestionIndex]); + return; + } + if (skillsTextInput.value.trim()) { + addSkill(skillsTextInput.value); + skillsTextInput.value = ""; + } + hideSuggestions(); + } + }); + + // Add/toggle skill on quick-pick chip click + 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(); + }); + + if (isAlreadySelected) { + removeSkill(skill); + } else { + addSkill(skill); + } + hideSuggestions(); + skillsTextInput.value = ""; + }); + }); + + // Show suggestions on input + skillsTextInput.addEventListener("input", function (evt) { + var typedValue = evt.target.value.trim(); + if (typedValue.length === 0) { + hideSuggestions(); + return; + } + displaySuggestions(getFilteredSkills(typedValue)); + }); + + skillsTextInput.addEventListener("focus", function () { + if (skillsTextInput.value.trim()) { + displaySuggestions(getFilteredSkills(skillsTextInput.value)); + } + }); + + // Hide suggestions when input loses focus + skillsTextInput.addEventListener("blur", function () { + setTimeout(function () { hideSuggestions(); }, 150); + }); + + if (skillWrap) { + skillWrap.addEventListener("click", function () { + skillsTextInput.focus(); + }); + } + + + document.addEventListener("click", function (evt) { + if (skillWrap && !skillWrap.contains(evt.target)) { + hideSuggestions(); + } + }); + + //add a skill to the list if it's not empty or a duplicate + function addSkill(rawSkill) { + // Clean up any extra spaces and match to canonical skill name + var skill = getCanonicalSkill(rawSkill); + // Nothing to add if string is empty after trimming + if (!skill) return; + + // Block duplicate entries (case-insensitive) + if (isSkillSelected(skill)) return; + + selectedSkills.push(skill); + renderSelectedChips(); + syncSkillsHiddenInput(); + updateQuickPickState(); + // Once a skill is added, remove the "please add a skill" error if it was showing + clearFieldError("skills-error"); + } + + // remove a skill from the list and update the UI accordingly + function removeSkill(skill) { + // Rebuild the array without the skill that was just removed + selectedSkills = selectedSkills.filter(function (selectedSkill) { + return normalizeSkill(selectedSkill) !== normalizeSkill(skill); + }); + renderSelectedChips(); + syncSkillsHiddenInput(); + updateQuickPickState(); + } + + // recreate the selected skills chips based on the current array(selectedSkills) + // called every time we add or remove a skill + function renderSelectedChips() { + // Wipe out old chips first so we don't end up with duplicates in the UI + chipsSelectedEl.innerHTML = ""; + selectedSkills.forEach(function (skill) { + // Create a new chip element for each selected skill + var chipEl = document.createElement("span"); + chipEl.className = "skill-chip-selected"; + chipEl.textContent = skill; + + // Remove button for each chip (create lil "x" button) + var removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.className = "skill-chip-remove"; + removeBtn.innerHTML = "×"; //'x' symbol + 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(); + removeSkill(skill); + }); + + chipEl.appendChild(removeBtn); // put x button inside the chip + chipsSelectedEl.appendChild(chipEl); //add chip to page + }); + } + + function syncSkillsHiddenInput() { + 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(", "); + } + + updateQuickPickState(); + + + // ---------------------------------------------------------- + // Form validation + // ---------------------------------------------------------- + + //puts error msg under specific field + function showFieldError(fieldId, message) { + var el = document.getElementById(fieldId); + if (el) el.textContent = message; + } + + //clears error msg under specific field + function clearFieldError(fieldId) { + var el = document.getElementById(fieldId); + if (el) el.textContent = ""; //empty string = no error msg + } + + //clears all error msgs in the form, called at the start of form submission to reset any previous errors + function clearAllErrors() { + ["skills-error", "level-error", "interest-error", "time-error"].forEach(clearFieldError); + var generalErr = document.getElementById("form-error-general"); + if (generalErr) generalErr.textContent = ""; + } + + // checks form fields and shows error messages if any required field is missing or invalid. + // Returns true if the form is valid, false otherwise + function validateForm() { + var valid = true; + + // Check both the array and the hidden input since skills can come from either source + if (selectedSkills.length === 0 && !skillsHidden.value.trim()) { + showFieldError("skills-error", "Please add at least one skill."); + valid = false; + } + if (!document.getElementById("level").value) { + showFieldError("level-error", "Please select your experience level."); + valid = false; + } + if (!document.getElementById("interest").value) { + showFieldError("interest-error", "Please select an area of interest."); + valid = false; + } + if (!document.getElementById("time").value) { + showFieldError("time-error", "Please select your time availability."); + valid = false; + } + + return valid; + } + + + // ---------------------------------------------------------- + // Form submission and API call + // ---------------------------------------------------------- + + 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(); + } + + if (!validateForm()) return; //stop - anything missing/invalid + + setLoadingState(true); + + // Allow browser to paint spinner before request starts + requestAnimationFrame(function () { + + var payload = { + skills: skillsHidden.value.trim() || skillsTextInput.value.trim(), + level: document.getElementById("level").value, + interest: document.getElementById("interest").value, + time: document.getElementById("time").value + }; + + fetch("/api/recommend", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }) + .then(function (res) { + return res.json(); + }) + .then(function (data) { + + setLoadingState(false); + + if (data.error) { + var generalErr = document.getElementById("form-error-general"); + + if (generalErr) { + generalErr.textContent = data.error; + } + + return; + } + + renderResults(data.projects || [], data.message); + }) + .catch(function () { + setLoadingState(false); + var generalErr = document.getElementById("form-error-general"); + if (generalErr) generalErr.textContent = "Something went wrong. Please try again."; + }); + }); + }); + + // Manages the loading state of the form and results section(whats visible or not) + function setLoadingState(isLoading) { + // Disable the button so the user can't accidentally submit twice + submitBtn.disabled = isLoading; + submitBtn.setAttribute("aria-busy", isLoading); + btnLabel.style.display = isLoading ? "none" : "inline"; + btnLoading.style.display = isLoading ? "inline-flex" : "none"; + + if (isLoading) { + // Show the results section with only the loading indicator visible + resultsSection.style.display = "block"; + resultsLoadingEl.style.display = "block"; + resultsGrid.style.display = "none"; + resultsEmptyEl.style.display = "none"; + // 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 + } + } + + + // ---------------------------------------------------------- + // 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 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 = ""; + + if (!projects || projects.length === 0) { + resultsGrid.style.display = "none"; + resultsEmptyEl.style.display = "block"; + + // Show a friendly custom message when the user selected an interest + 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); + + // Tags row + var tagsRow = document.createElement("div"); + tagsRow.className = "project-card-tags"; + + // Show all project skills as tags so users can see the full match + (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("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 + + 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; + } + + 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; + } + + // ---------------------------------------------------------- + // GitHub Import Modal Logic + // ---------------------------------------------------------- + if (openModalBtn && closeModalBtn && modal && githubInput && fetchBtn && errorMsg) { + // Opens the GitHub input modal and focuses the input field + openModalBtn.addEventListener("click", function(e) { + e.preventDefault(); + modal.classList.add("active"); + githubInput.focus(); + }); + + // 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); + + // 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(); + if (!username) return; + + fetchBtn.disabled = true; + fetchBtn.textContent = "Syncing..."; + errorMsg.textContent = ""; + + fetch("https://api.github.com/users/" + encodeURIComponent(username) + "/repos") + .then(function(response) { + if (response.status === 404) { + throw new Error("Please enter a valid username."); + } + if (!response.ok) { + throw new Error("Failed to fetch repositories."); + } + 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) { + for (var j = 0; j < langs.length; j++) { + addSkill(langs[j]); + } + closeGithubModal(); + } else { + errorMsg.textContent = "No public languages found."; + } + fetchBtn.disabled = false; + fetchBtn.textContent = "Fetch Skills"; + }) + .catch(function(err) { + errorMsg.textContent = err.message || "Failed to fetch skills"; + fetchBtn.disabled = false; + fetchBtn.textContent = "Fetch Skills"; + }); + }); + } + +} // 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 = "";
+  }
+
+  //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.";
+        }
+      });
+  }
+
+  // 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 (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
+
+
+/* ---- Scroll-to-top button ---- */
+
+/* Show the button only when the user has scrolled more than 300px */
+var SCROLL_THRESHOLD = 300;
+
+/* Get the button element; guard against pages that do not have it */
+var scrollTopBtn = document.getElementById('scroll-top-btn');
+
+/* 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');
+  }
+}
+
+/* Smooth-scroll to the very top of the page */
+function scrollToTop() {
+  window.scrollTo({ top: 0, behavior: 'smooth' });
+}
+
+/* Only wire up listeners if the button exists on this page */
+if (scrollTopBtn) {
+    window.addEventListener('scroll', handleScroll);
+    scrollTopBtn.addEventListener('click', scrollToTop);
+}

From d14d633f75be207d76370c9e514227777d590b71 Mon Sep 17 00:00:00 2001
From: SrinathChoul 
Date: Fri, 5 Jun 2026 23:27:26 +0530
Subject: [PATCH 2/8] fix: resolve suggested changes and merge conflicts

---
 DevPath/static/script.js | 1540 ++++++++++++++++++++++++++------------
 1 file changed, 1047 insertions(+), 493 deletions(-)

diff --git a/DevPath/static/script.js b/DevPath/static/script.js
index 5deedc47..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,24 +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 - menu.querySelectorAll(".nav-mobile-link").forEach(function (link) { - link.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"); }); }); - var mobileLinks = menu.querySelectorAll(".nav-mobile-link"); - for (var i = 0; i < mobileLinks.length; i++) { - mobileLinks[i].addEventListener("click", function () { - menu.classList.remove("open"); + + window.addEventListener("resize", function () { + if (window.innerWidth >= 640) { + menu.classList.remove("open"); toggle.classList.remove("open"); - }); - } + toggle.setAttribute("aria-expanded", "false"); + } + }); })(); @@ -67,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 @@ -82,40 +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) { - quickPickChips.forEach(function(chip) { - chip.classList.remove("active", "selected"); - }); - 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); } @@ -145,19 +598,32 @@ 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); + } + + function initSkillStripMarquee() { + var marquee = document.querySelector(".skill-strip-marquee"); + if (!marquee) return; } availableSkills = availableSkills.filter(function (skill, index, list) { @@ -166,20 +632,6 @@ if (clearFiltersBtn) { return item.toLowerCase() === skill.toLowerCase(); }) === index; }); - // 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); - } - } - } - availableSkills = uniqueSkills; if (suggestionsDiv) { suggestionsDiv.setAttribute("role", "listbox"); @@ -187,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) { @@ -215,24 +665,6 @@ if (clearFiltersBtn) { }).slice(0, 8); } - // Retrieves the properly capitalized version of a skill if it exists - function getCanonicalSkill(rawSkill) { - var normalizedSkill = normalizeSkill(rawSkill); - var matchedSkill = availableSkills.filter(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); - }).slice(0, 8); - } - - // Updates ARIA attributes for screen readers based on dropdown visibility function syncSuggestionsA11yState() { skillsTextInput.setAttribute("aria-expanded", visibleSuggestions.length > 0 ? "true" : "false"); } @@ -246,18 +678,6 @@ if (clearFiltersBtn) { }); } - // 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"); - } - } - - // Hides and clears out the autocomplete suggestion box function hideSuggestions() { visibleSuggestions = []; activeSuggestionIndex = -1; @@ -268,7 +688,6 @@ if (clearFiltersBtn) { syncSuggestionsA11yState(); } - // Processes the selection of a skill from the dropdown menu function selectSuggestion(skill) { addSkill(skill); skillsTextInput.value = ""; @@ -276,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; @@ -320,14 +738,6 @@ if (clearFiltersBtn) { chip.classList.toggle("active", isActive); chip.setAttribute("aria-pressed", isActive ? "true" : "false"); }); - // Toggles the active visual state on the predefined skill buttons - function updateQuickPickState() { - for (var i = 0; i < quickPickChips.length; i++) { - var chip = quickPickChips[i]; - 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 @@ -386,24 +796,6 @@ if (clearFiltersBtn) { skillsTextInput.value = ""; }); }); - 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[i]); - } // Show suggestions on input skillsTextInput.addEventListener("input", function (evt) { @@ -455,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 @@ -466,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) @@ -484,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(); @@ -497,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(); @@ -563,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, @@ -595,34 +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 () { setLoadingState(false); var generalErr = document.getElementById("form-error-general"); - if (generalErr) generalErr.textContent = "Something went wrong. Please try again."; + if (generalErr) { + generalErr.textContent = "An unexpected error occurred. Please try again."; + } }); }); - .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."; - }); - }); }); // Manages the loading state of the form and results section(whats visible or not) @@ -632,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 @@ -644,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 } } @@ -654,19 +1052,28 @@ 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) { - resultsGrid.style.display = "none"; + resultsGrid.style.display = "none"; resultsEmptyEl.style.display = "block"; - - // Show a friendly custom message when the user selected an interest + var interestEl = document.getElementById("interest"); var selectedInterest = interestEl ? interestEl.value : null; @@ -677,420 +1084,567 @@ if (clearFiltersBtn) { } else { emptyMessageEl.textContent = "Try adjusting your skills or choosing a different interest area."; } - - 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 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 all project skills as tags so users can see the full match (project.skills || []).forEach(function (skill) { - // Show the first two skills as tags - (project.skills || []).slice(0, 2).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; - } - // ---------------------------------------------------------- - // GitHub Import Modal Logic - // ---------------------------------------------------------- - if (openModalBtn && closeModalBtn && modal && githubInput && fetchBtn && errorMsg) { - // Opens the GitHub input modal and focuses the input field - openModalBtn.addEventListener("click", function(e) { - e.preventDefault(); - modal.classList.add("active"); - githubInput.focus(); - }); + // ============================================================ + // 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 GitHub input modal and resets its values and errors
    -    var closeGithubModal = function() {
    -        modal.classList.remove("active");
    -        githubInput.value = "";
    -        errorMsg.textContent = "";
    -    };
    +    //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 = "";
    +    }
     
    -    closeModalBtn.addEventListener("click", closeGithubModal);
    +    // 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;
    +      });
    +    }
     
    -    // Closes the modal if the user clicks the background overlay outside the card
    -    modal.addEventListener("click", function(e) {
    -        if (e.target === modal) closeGithubModal();
    -    });
    +    //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...";
     
    -    // Fetches repositories for a GitHub user and adds unique languages to the skills list
    -    fetchBtn.addEventListener("click", function() {
    -        var username = githubInput.value.trim();
    -        if (!username) return;
    +      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.";
    +          }
    +        });
    +    }
     
    -        fetchBtn.disabled = true;
    -        fetchBtn.textContent = "Syncing...";
    -        errorMsg.textContent = "";
    +   // ============================================================
    +// ROADMAP PROGRESS TRACKER
    +// ============================================================
    +
    +
    +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];
     
    -        fetch("https://api.github.com/users/" + encodeURIComponent(username) + "/repos")
    -          .then(function(response) {
    -            if (response.status === 404) {
    -                throw new Error("Please enter a valid username.");
    -            }
    -            if (!response.ok) {
    -                throw new Error("Failed to fetch repositories.");
                 }
    -            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);
    -              }
    +        );
    +
    +    } 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"
    +                );
                 }
     
    -            if (langs.length > 0) {
    -                for (var j = 0; j < langs.length; j++) {
    -                    addSkill(langs[j]);
    -                }
    -                closeGithubModal();
    -            } else {
    -                errorMsg.textContent = "No public languages found.";
    +        } else {
    +
    +            if(step){
    +                step.classList.remove(
    +                    "completed"
    +                );
                 }
    -            fetchBtn.disabled = false;
    -            fetchBtn.textContent = "Fetch Skills";
    -          })
    -          .catch(function(err) {
    -            errorMsg.textContent = err.message || "Failed to fetch skills";
    -            fetchBtn.disabled = false;
    -            fetchBtn.textContent = "Fetch Skills";
    -          });
    +
    +        }
    +
         });
    -  }
     
    -} // end isIndexPage
    +    var percent = Math.round(
    +        (completed / roadmapCheckboxes.length)
    +        * 100
    +    );
     
    +    // Update progress bar fill
    +    if(progressFill){
     
    -// ============================================================
    -// 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();
    -  }
    +        progressFill.style.width =
    +            percent + "%";
     
    -  //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.";
    -        }
    -      });
    -  }
    +    // Update progress text
    +    if(progressText){
     
    -  // Attach open/close handlers
    -  if (btnViewCode) btnViewCode.addEventListener("click", openCodePanel);
    -  if (btnViewCodeSm) btnViewCodeSm.addEventListener("click", openCodePanel);
    -  if (btnClosePanel) btnClosePanel.addEventListener("click", closeCodePanel);
    +        progressText.textContent =
    +            percent + "% completed";
     
    -  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
    -  });
    +    // Accessibility update
    +    if(progressBar){
    +
    +        progressBar.setAttribute(
    +            "aria-valuenow",
    +            percent
    +        );
     
    -  // ----------------------------------------------------------
    -  // 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);
    -  }
    +    // Save checkbox state
    +    var savedState = [];
    +
    +    roadmapCheckboxes.forEach(function(cb){
    +
    +        savedState.push(
    +            cb.checked
    +        );
     
    -  if (btnCopyCode) {
    -    btnCopyCode.addEventListener("click", function () {
    -      var code = codeContentEl
    -        ? Array.from(codeContentEl.querySelectorAll(".line-content"))
    -        ? 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)
    +    );
    +
    +}
     
    -if (
    +
    +// ------------------------------------------------------------
    +// 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 (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();
    -  });
    +    });
     
    -  // 3. Fetch Skills Logic
    -  fetchBtn.addEventListener('click', async () => {
    -      const username = githubInput.value.trim();
    -      if (!username) return;
    +    // Fetches repositories for a GitHub user and adds unique languages to the skills list
    +    fetchBtn.addEventListener("click", function() {
    +      var username = githubInput.value.trim();
    +
    +      // 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();
    +  }
    +
    +})();
    
    From 63132b6cd828326fab185bbf806341f6446dfccd Mon Sep 17 00:00:00 2001
    From: SrinathChoul 
    Date: Fri, 5 Jun 2026 23:31:32 +0530
    Subject: [PATCH 3/8] fix: resolve merge conflicts
    
    ---
     DevPath/CHANGELOG.md | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/DevPath/CHANGELOG.md b/DevPath/CHANGELOG.md
    index 713b3171..95f8b9bf 100644
    --- a/DevPath/CHANGELOG.md
    +++ b/DevPath/CHANGELOG.md
    @@ -17,5 +17,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
     
     ### Fixed
     
    -- Fixed the 'Import from GitHub' modal to correctly fetch skills and handle invalid usernames
     - Fixed an issue where skill chips on the homepage were unclickable due to JavaScript syntax errors
    
    From 339229dddbfd135c16da9e925a13a8a574e3a1d8 Mon Sep 17 00:00:00 2001
    From: SrinathChoul 
    Date: Sat, 6 Jun 2026 00:08:51 +0530
    Subject: [PATCH 4/8] fix: testcase
    
    ---
     data/projects.json | 6 +++---
     1 file changed, 3 insertions(+), 3 deletions(-)
    
    diff --git a/data/projects.json b/data/projects.json
    index e92836c6..0eab1d57 100644
    --- a/data/projects.json
    +++ b/data/projects.json
    @@ -283,7 +283,7 @@
         "starter_code": "starter_code/realtime_chat_app.js"
       },
       {
    -    "id": 10,
    +    "id": 99,
         "title": "Password Strength Checker",
         "skills": ["Python"],
         "level": "Beginner",
    @@ -312,7 +312,7 @@
         "starter_code": "starter_code/password_checker.py"
       },
       {
    -    "id": 9,
    +    "id": 10,
         "title": "Feedback Survey Form",
         "skills": ["HTML"],
         "level": "Beginner",
    @@ -338,7 +338,7 @@
         "starter_code": "starter_code/survey_form/index.html"
       },
       {
    -    "id": 10,
    +    "id": 98,
         "title": "API ETL Pipeline",
         "skills": ["Python", "pandas", "requests"],
         "level": "Intermediate",
    
    From 56b73290b17cec3485c8037ddbe4a458a57fee13 Mon Sep 17 00:00:00 2001
    From: SrinathChoul 
    Date: Sat, 6 Jun 2026 00:10:13 +0530
    Subject: [PATCH 5/8] fix: testcase
    
    ---
     utils/data_loader.py | 3 +++
     1 file changed, 3 insertions(+)
    
    diff --git a/utils/data_loader.py b/utils/data_loader.py
    index 9d70f6d6..7065dd0d 100644
    --- a/utils/data_loader.py
    +++ b/utils/data_loader.py
    @@ -4,6 +4,9 @@
     
     DATA_FILE = os.path.join(os.path.dirname(__file__), "..", "data", "projects.json")
     
    +_projects_cache = None
    +_cache_lock = threading.Lock()
    +
     def validate_projects(projects):
         """
         Validate project dataset integrity.
    
    From 571af5ddbcb3dd4ddb3a96955a9294f7d8ab78ad Mon Sep 17 00:00:00 2001
    From: SrinathChoul 
    Date: Sat, 6 Jun 2026 00:10:55 +0530
    Subject: [PATCH 6/8] fix: testcase
    
    ---
     app.py | 7 +++++++
     1 file changed, 7 insertions(+)
    
    diff --git a/app.py b/app.py
    index 8b84fff1..f50bbad5 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('
    Date: Sat, 6 Jun 2026 00:17:22 +0530
    Subject: [PATCH 7/8] fix: test case
    
    ---
     data/projects.json | 1217 --------------------------------------------
     1 file changed, 1217 deletions(-)
    
    diff --git a/data/projects.json b/data/projects.json
    index 1cc5d3c3..0eab1d57 100644
    --- a/data/projects.json
    +++ b/data/projects.json
    @@ -995,1220 +995,3 @@
         "starter_code": null
       }
     ]
    -[
    -  {
    -    "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": 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"
    -  }
    -]
    
    From a635e82d96de81e7c80f040a220f5a45587cdc01 Mon Sep 17 00:00:00 2001
    From: SrinathChoul 
    Date: Sat, 6 Jun 2026 17:08:28 +0530
    Subject: [PATCH 8/8] fix: testcase
    
    ---
     data/projects.json | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/data/projects.json b/data/projects.json
    index 0eab1d57..8c96f249 100644
    --- a/data/projects.json
    +++ b/data/projects.json
    @@ -995,3 +995,5 @@
         "starter_code": null
       }
     ]
    +
    +