diff --git a/static/script.js b/static/script.js index 577a5c2..a69113b 100644 --- a/static/script.js +++ b/static/script.js @@ -1,557 +1,381 @@ -// DevPath client-side behavior. - -(function () { - var html = document.documentElement; - - function applyTheme(theme) { - var isDark = theme === "dark"; - html.setAttribute("data-theme", theme); - try { - localStorage.setItem("theme", theme); - } catch (err) { - // Storage can be unavailable in private browsing. - } - - document.querySelectorAll(".theme-toggle").forEach(function (button) { - button.setAttribute("aria-pressed", isDark ? "true" : "false"); - button.setAttribute("aria-label", isDark ? "Switch to light mode" : "Switch to dark mode"); - }); - } - - function initTheme() { - var theme = "light"; - try { - theme = localStorage.getItem("theme") || html.getAttribute("data-theme") || "light"; - } catch (err) { - theme = html.getAttribute("data-theme") || "light"; - } - applyTheme(theme); - requestAnimationFrame(function () { - html.classList.add("theme-ready"); - }); - } - - document.addEventListener("click", function (event) { - var toggle = event.target.closest(".theme-toggle"); - if (!toggle) return; - event.preventDefault(); - var current = html.getAttribute("data-theme") || "light"; - applyTheme(current === "dark" ? "light" : "dark"); - }); - - initTheme(); -})(); - +// 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"); - var menu = document.getElementById("nav-mobile-menu"); - if (!toggle || !menu) return; + var toggle = document.getElementById("nav-mobile-toggle"); //hamburger button + var menu = document.getElementById("nav-mobile-menu"); //dropdown menu - function setOpen(isOpen) { - menu.classList.toggle("open", isOpen); - toggle.classList.toggle("open", isOpen); - toggle.setAttribute("aria-expanded", isOpen ? "true" : "false"); - } + // Nothing to do if the nav isn't on this page, just bail out + if (!toggle || !menu) return; toggle.addEventListener("click", function () { - setOpen(!menu.classList.contains("open")); + // 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); }); - menu.querySelectorAll(".nav-mobile-link").forEach(function (link) { - link.addEventListener("click", function () { - setOpen(false); + // 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"); }); }); - - window.addEventListener("resize", function () { - if (window.innerWidth >= 640) setOpen(false); - }); })(); -var STORAGE_KEY = "devpathUserProgress"; -var progress = { - searches: 0, - projectViews: 0, - codeOpens: 0, - completions: 0, - points: 0, - viewedProjects: [], - completedProjects: [], - achievements: [], - badges: { - first_search: false, - project_explorer: false, - code_starter: false, - completionist: false, - roadmap_runner: false - }, - bestScore: 0 -}; - -function loadProgressState() { - try { - var saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null"); - if (!saved || typeof saved !== "object") return; - 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 || 0, progress.points || 0); - localStorage.setItem(STORAGE_KEY, JSON.stringify(progress)); - } catch (err) { - console.warn("Unable to save progress state", err); - } -} +// ============================================================ +// 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 = []; -function computeProgressPoints() { - progress.points = progress.searches * 5 + progress.projectViews * 10 + - progress.codeOpens * 15 + progress.completions * 30; -} + // ---------------------------------------------------------- + // Reset skill selection (used by Clear Filters and form reset) + // ---------------------------------------------------------- + function resetSkillSelection() { + selectedSkills = []; + if (skillsHidden) skillsHidden.value = ""; + if (chipsSelectedEl) chipsSelectedEl.innerHTML = ""; + if (skillsTextInput) skillsTextInput.value = ""; + if (suggestionsDiv) { suggestionsDiv.innerHTML = ""; suggestionsDiv.style.display = "none"; } + if (quickPickChips) { + quickPickChips.forEach(function (chip) { + chip.classList.remove("active", "selected"); + chip.setAttribute("aria-pressed", "false"); + }); + } + clearFieldError("skills-error"); + } -function showAchievementToast(title, detail) { - var toast = document.getElementById("achievement-toast"); - if (!toast) return; - toast.textContent = ""; - var strong = document.createElement("strong"); - strong.textContent = title; - var span = document.createElement("span"); - span.textContent = detail; - toast.appendChild(strong); - toast.appendChild(span); - toast.classList.add("show"); - window.clearTimeout(showAchievementToast.timeout); - showAchievementToast.timeout = window.setTimeout(function () { - toast.classList.remove("show"); - }, 3200); -} + // 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) { + recommendForm.reset(); + resetSkillSelection(); + if (skillsTextInput) skillsTextInput.focus(); + } + }); + } -function addAchievement(title, detail) { - if (progress.achievements.some(function (item) { return item.title === title; })) return; - progress.achievements.unshift({ - title: title, - description: detail, - date: new Date().toLocaleDateString() + // Also reset skills when the native form reset event fires + form.addEventListener("reset", function () { + window.setTimeout(function () { + resetSkillSelection(); + if (skillsTextInput) skillsTextInput.focus(); + }, 0); }); - progress.achievements = progress.achievements.slice(0, 5); -} - -function unlockBadge(id, title, detail) { - if (progress.badges[id]) return; - progress.badges[id] = true; - addAchievement(title, detail); - showAchievementToast("Badge unlocked", title + " - " + detail); -} -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 projectIsCompleted(projectId) { - return progress.completedProjects.some(function (item) { - return (item && typeof item === "object" ? item.id : item) === projectId; - }); -} + // ---------------------------------------------------------- + // Skill chip manager + // ---------------------------------------------------------- -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", String(percentage)); - meterFill.textContent = percentage + "%"; - } - if (badgesEl) { - var badges = [ - ["first_search", "First Search"], - ["project_explorer", "Project Explorer"], - ["code_starter", "Code Starter"], - ["completionist", "Completionist"], - ["roadmap_runner", "Roadmap Runner"] + // 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" ]; - badgesEl.innerHTML = badges.map(function (badge) { - var unlocked = progress.badges[badge[0]]; - return "
  • " + (unlocked ? "OK" : "*") + "" + badge[1] + "
  • "; - }).join(""); } - if (achievementList) { - achievementList.innerHTML = progress.achievements.length - ? progress.achievements.map(function (item) { - return "
  • " + item.title + "" + - item.description + "" + item.date + "
  • "; - }).join("") - : "
  • No achievements yet. Use DevPath and unlock the first badge.
  • "; - } - if (leaderboardList) { - var entries = [ - { name: "Ava", points: 245 }, - { name: "Kai", points: 192 }, - { name: "Sam", points: 176 }, - { name: "You", points: progress.points } - ].sort(function (a, b) { return b.points - a.points; }); - leaderboardList.innerHTML = entries.map(function (entry, index) { - return "
  • " + (index + 1) + ". " + entry.name + "" + entry.points + " pts
  • "; - }).join(""); - } - if (historyList) { - historyList.innerHTML = progress.completedProjects.length - ? progress.completedProjects.slice(0, 5).map(function (item) { - var title = item && typeof item === "object" ? item.title : "Project " + item; - return "
  • " + title + "Completed
  • "; - }).join("") - : "
  • No completed projects yet. Mark one complete from a project page.
  • "; - } - if (completionBtn && typeof PROJECT_ID !== "undefined") { - var completed = projectIsCompleted(PROJECT_ID); - completionBtn.textContent = completed ? "Project Completed" : "Mark Project Complete"; - completionBtn.disabled = completed; - } -} -function recordSearch() { - progress.searches += 1; - computeProgressPoints(); - tryUnlockBadges(); - saveProgressState(); - updateProfileWidgets(); -} + var suggestionsDiv = document.getElementById("skills-suggestions"); + var skillWrap = document.getElementById("skill-input-wrap"); + var visibleSuggestions = []; + var activeSuggestionIndex = -1; -function recordProjectView() { - if (typeof PROJECT_ID === "undefined") return; - if (progress.viewedProjects.indexOf(PROJECT_ID) === -1) { - progress.viewedProjects.push(PROJECT_ID); - progress.projectViews = progress.viewedProjects.length; - computeProgressPoints(); - tryUnlockBadges(); - saveProgressState(); - updateProfileWidgets(); - } -} + // Deduplicate available skills (case-insensitive) + availableSkills = availableSkills.filter(function (skill, index, list) { + return typeof skill === "string" && skill.trim() && + list.findIndex(function (item) { + return item.toLowerCase() === skill.toLowerCase(); + }) === index; + }); -function recordCodeOpen() { - progress.codeOpens += 1; - computeProgressPoints(); - tryUnlockBadges(); - saveProgressState(); - updateProfileWidgets(); -} + if (suggestionsDiv) suggestionsDiv.setAttribute("role", "listbox"); -function recordCompletion(projectId, projectTitle) { - if (!projectId || projectIsCompleted(projectId)) return; - progress.completedProjects.push({ id: projectId, title: projectTitle || "Project " + projectId }); - progress.completions = progress.completedProjects.length; - computeProgressPoints(); - tryUnlockBadges(); - saveProgressState(); - updateProfileWidgets(); -} + function normalizeSkill(skill) { return skill.trim().toLowerCase(); } -loadProgressState(); -updateProfileWidgets(); - -(function initIndexPage() { - var form = document.getElementById("recommend-form"); - if (!form) return; - - var submitBtn = document.getElementById("submit-btn"); - var btnLabel = document.getElementById("btn-label"); - var btnLoading = document.getElementById("btn-loading"); - var resultsSection = document.getElementById("results-section"); - var resultsGrid = document.getElementById("results-grid"); - var resultsLoadingEl = document.getElementById("results-loading"); - var resultsEmptyEl = document.getElementById("results-empty"); - var emptyMessageEl = document.getElementById("empty-message"); - var skillsHidden = document.getElementById("skills"); - var skillsInput = document.getElementById("skills-input"); - var selectedChips = document.getElementById("skill-chips-selected"); - var suggestions = document.getElementById("skills-suggestions"); - var skillWrap = document.getElementById("skill-input-wrap"); - var quickPickChips = Array.prototype.slice.call(document.querySelectorAll(".skill-chip")); - var selectedSkills = []; - var availableSkills = (typeof skills !== "undefined" && Array.isArray(skills)) - ? skills.map(function (item) { return item.label; }).filter(Boolean) - : quickPickChips.map(function (chip) { return chip.getAttribute("data-skill"); }); - var activeSuggestionIndex = -1; - var visibleSuggestions = []; - var SAVED_PROJECTS_KEY = "devpathSavedProjects"; - - function normalize(value) { - return String(value || "").trim().toLowerCase(); + function isSkillSelected(skill) { + var normalizedSkill = normalizeSkill(skill); + return selectedSkills.some(function (s) { return normalizeSkill(s) === normalizedSkill; }); } - function getSavedProjects() { - try { - var saved = JSON.parse(localStorage.getItem(SAVED_PROJECTS_KEY) || "[]"); - return Array.isArray(saved) ? saved : []; - } catch (err) { - console.warn("Unable to load saved projects", err); - return []; - } + function getCanonicalSkill(rawSkill) { + var normalizedSkill = normalizeSkill(rawSkill); + var matched = availableSkills.find(function (s) { return normalizeSkill(s) === normalizedSkill; }); + return matched || rawSkill.trim(); } - function saveSavedProjects(projects) { - try { - localStorage.setItem(SAVED_PROJECTS_KEY, JSON.stringify(projects)); - } catch (err) { - console.warn("Unable to save projects", err); - } + function getFilteredSkills(query) { + var normalizedQuery = normalizeSkill(query); + return availableSkills.filter(function (skill) { + return normalizeSkill(skill).includes(normalizedQuery) && !isSkillSelected(skill); + }).slice(0, 8); } - function projectIsSaved(projectId) { - return getSavedProjects().some(function (project) { - return String(project.id) === String(projectId); + 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 saveProject(project) { - var saved = getSavedProjects(); - if (saved.some(function (item) { return String(item.id) === String(project.id); })) return; + function hideSuggestions() { + visibleSuggestions = []; + activeSuggestionIndex = -1; + if (suggestionsDiv) { suggestionsDiv.style.display = "none"; suggestionsDiv.innerHTML = ""; } + } - saved.unshift({ - id: project.id, - title: project.title, - level: project.level || "", - time: project.time || "", - skills: Array.isArray(project.skills) ? project.skills.slice(0, 4) : [] - }); - saveSavedProjects(saved); - renderSavedProjects(); + function selectSuggestion(skill) { + addSkill(skill); + skillsTextInput.value = ""; + hideSuggestions(); + skillsTextInput.focus(); } - function removeSavedProject(projectId) { - var saved = getSavedProjects().filter(function (project) { - return String(project.id) !== String(projectId); - }); - saveSavedProjects(saved); - renderSavedProjects(); - document.querySelectorAll("[data-save-project-id='" + projectId + "']").forEach(function (button) { - button.classList.remove("saved"); - button.textContent = "Save Project"; - button.setAttribute("aria-pressed", "false"); + 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"; + skillsTextInput.setAttribute("aria-expanded", "true"); } - function toggleSavedProject(project, button) { - if (projectIsSaved(project.id)) { - removeSavedProject(project.id); - return; - } - - saveProject(project); - button.classList.add("saved"); - button.textContent = "Saved"; - button.setAttribute("aria-pressed", "true"); + 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"); + }); } - function renderSavedProjects() { - var list = document.getElementById("saved-projects-list"); - var count = document.getElementById("saved-projects-count"); - if (!list || !count) return; - - var saved = getSavedProjects(); - count.textContent = saved.length + " saved"; - list.textContent = ""; - - if (!saved.length) { - var empty = document.createElement("p"); - empty.className = "saved-projects-empty"; - empty.textContent = "No saved projects yet."; - list.appendChild(empty); + // Add skill on Enter key in the text input + // 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(); + } + }); - saved.forEach(function (project) { - var item = document.createElement("article"); - item.className = "saved-project-item"; - - var title = document.createElement("a"); - title.href = "/project/" + project.id; - title.textContent = project.title; - - var meta = document.createElement("span"); - meta.textContent = [project.level, project.time].filter(Boolean).join(" - "); - - var remove = document.createElement("button"); - remove.type = "button"; - remove.className = "saved-project-remove"; - remove.textContent = "Remove"; - remove.addEventListener("click", function () { - removeSavedProject(project.id); - }); - - item.appendChild(title); - item.appendChild(meta); - item.appendChild(remove); - list.appendChild(item); + // Add/toggle skill on quick-pick chip click + quickPickChips.forEach(function (chip) { + chip.addEventListener("click", function () { + var skill = chip.getAttribute("data-skill"); + if (!skill) return; + if (isSkillSelected(skill)) { removeSkill(skill); } else { addSkill(skill); } + skillsTextInput.value = ""; + hideSuggestions(); }); - } + }); - function syncSkillsHiddenInput() { - skillsHidden.value = JSON.stringify(selectedSkills); - } + // Show suggestions on input + skillsTextInput.addEventListener("input", function (evt) { + var typedValue = evt.target.value.trim(); + if (typedValue.length === 0) { hideSuggestions(); return; } + displaySuggestions(getFilteredSkills(typedValue)); + }); - function isSelected(skill) { - return selectedSkills.some(function (item) { return normalize(item) === normalize(skill); }); - } + skillsTextInput.addEventListener("focus", function () { + if (skillsTextInput.value.trim()) displaySuggestions(getFilteredSkills(skillsTextInput.value)); + }); - function canonicalSkill(rawSkill) { - var trimmed = String(rawSkill || "").trim(); - var match = availableSkills.find(function (skill) { return normalize(skill) === normalize(trimmed); }); - return match || trimmed; - } + // Hide suggestions when input loses focus + skillsTextInput.addEventListener("blur", function () { + setTimeout(function () { hideSuggestions(); }, 150); + }); - function updateQuickPickState() { - quickPickChips.forEach(function (chip) { - var active = isSelected(chip.getAttribute("data-skill")); - chip.classList.toggle("active", active); - chip.classList.toggle("selected", active); - chip.setAttribute("aria-pressed", active ? "true" : "false"); - }); + if (skillWrap) { + skillWrap.addEventListener("click", function () { skillsTextInput.focus(); }); } - function renderSelectedChips() { - selectedChips.textContent = ""; - selectedSkills.forEach(function (skill) { - var chip = document.createElement("span"); - chip.className = "skill-chip-selected"; - chip.appendChild(document.createTextNode(skill)); - var button = document.createElement("button"); - button.type = "button"; - button.className = "skill-chip-remove"; - button.setAttribute("aria-label", "Remove " + skill); - button.textContent = "x"; - button.addEventListener("click", function (event) { - event.stopPropagation(); - removeSkill(skill); - }); - chip.appendChild(button); - selectedChips.appendChild(chip); - }); - } + document.addEventListener("click", function (evt) { + if (skillWrap && !skillWrap.contains(evt.target)) hideSuggestions(); + }); - window.addSkill = function addSkill(rawSkill) { - var skill = canonicalSkill(rawSkill); - if (!skill || isSelected(skill)) return; + //add a skill to the list if it's not empty or a duplicate + function addSkill(rawSkill) { + var skill = getCanonicalSkill(rawSkill); + if (!skill) return; + 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"); - if (skillsInput) skillsInput.focus(); - }; + } + // remove a skill from the list and update the UI accordingly function removeSkill(skill) { - selectedSkills = selectedSkills.filter(function (item) { return normalize(item) !== normalize(skill); }); + // Rebuild the array without the skill that was just removed + selectedSkills = selectedSkills.filter(function (s) { return normalizeSkill(s) !== normalizeSkill(skill); }); renderSelectedChips(); syncSkillsHiddenInput(); updateQuickPickState(); } - function clearFieldError(id) { - var el = document.getElementById(id); - if (el) el.textContent = ""; + // 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"); - } + if (!skillsHidden) return; // 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(", "); } - function clearAllErrors() { - ["skills-error", "level-error", "interest-error", "time-error"].forEach(clearFieldError); - var general = document.getElementById("form-error-general"); - if (general) general.textContent = ""; - } + updateQuickPickState(); - function hideSuggestions() { - visibleSuggestions = []; - activeSuggestionIndex = -1; - suggestions.style.display = "none"; - suggestions.textContent = ""; - skillsInput.setAttribute("aria-expanded", "false"); - } - function filteredSkills(query) { - var q = normalize(query); - if (!q) return []; - return availableSkills.filter(function (skill) { - return normalize(skill).indexOf(q) !== -1 && !isSelected(skill); - }).slice(0, 8); + // ---------------------------------------------------------- + // Form validation + // ---------------------------------------------------------- + + //puts error msg under specific field + function showFieldError(fieldId, message) { + var el = document.getElementById(fieldId); + if (el) el.textContent = message; } - function renderSuggestionState() { - suggestions.querySelectorAll(".suggestion-item").forEach(function (item, index) { - item.classList.toggle("suggestion-item--active", index === activeSuggestionIndex); - item.setAttribute("aria-selected", index === activeSuggestionIndex ? "true" : "false"); - }); + //clears error msg under specific field + function clearFieldError(fieldId) { + var el = document.getElementById(fieldId); + if (el) el.textContent = ""; //empty string = no error msg } - function showSuggestions(items) { - visibleSuggestions = items; - activeSuggestionIndex = -1; - suggestions.textContent = ""; - if (!items.length) { - hideSuggestions(); - return; - } - items.forEach(function (skill, index) { - var item = document.createElement("div"); - item.className = "suggestion-item"; - item.id = "skills-suggestion-" + index; - item.setAttribute("role", "option"); - item.setAttribute("aria-selected", "false"); - item.textContent = skill; - item.addEventListener("mousedown", function (event) { event.preventDefault(); }); - item.addEventListener("mouseenter", function () { - activeSuggestionIndex = index; - renderSuggestionState(); - }); - item.addEventListener("click", function () { - window.addSkill(skill); - skillsInput.value = ""; - hideSuggestions(); - }); - suggestions.appendChild(item); - }); - suggestions.style.display = "block"; - skillsInput.setAttribute("aria-expanded", "true"); + //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; - if (!selectedSkills.length) { + + // 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; } @@ -567,19 +391,19 @@ updateProfileWidgets(); 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(); - + clearAllErrors() + if (skillsTextInput.value.trim()) { addSkill(skillsTextInput.value); skillsTextInput.value = ""; @@ -593,61 +417,56 @@ updateProfileWidgets(); // Allow browser to paint spinner before request starts requestAnimationFrame(function () { - //combine form values into an object to send to server/api var payload = { - // Prefer the hidden input value; fall back to raw text box if hidden input is empty skills: skillsHidden.value.trim() || skillsTextInput.value.trim(), level: document.getElementById("level").value, interest: document.getElementById("interest").value, time: document.getElementById("time").value }; - //post the data to backend api as JSON, then handle the response fetch("/api/recommend", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload) //convert object to json string + body: JSON.stringify(payload) }) - .then(function (res) { - return res.json(); //parse the response as JSON - }) + .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 (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."; - console.error("API request failed:", err); + console.error(err); }); }); }); - - // Manages the loading state of the form and results section(whats visible or not) + // Manages the loading state of the form and results section (what's 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 ? "true" : "false"); + 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 grid layout + resultsLoadingEl.style.display = "none"; + resultsGrid.style.display = "grid"; //switch back to gird layout } } @@ -664,61 +483,9 @@ updateProfileWidgets(); // Clear out any cards from a previous search before showing new ones resultsGrid.innerHTML = ""; - if (!projects || projects.length === 0) { //if no projects returned from api, show the "no results" message and hide the grid - resultsGrid.style.display = "none"; + if (!projects || projects.length === 0) { // if no projects returned from api, show "no results" and hide the grid + resultsGrid.style.display = "none"; resultsEmptyEl.style.display = "block"; - - var interestEl = document.getElementById("interest"); - var selectedInterest = interestEl ? interestEl.value : null; - - // Show a friendly custom message when the user selected an interest - if (emptyMessageEl) { - 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" }); - } - - function truncate(text, maxLength) { - text = text || ""; - return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; - } - - function createTag(text, type) { - var span = document.createElement("span"); - span.className = "project-tag project-tag--" + normalize(type).replace(/[^a-z0-9_-]/g, "-"); - span.textContent = text; - return span; - - //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"; if (message && emptyMessageEl) emptyMessageEl.textContent = message; resultsSection.scrollIntoView({ behavior: "smooth" }); return; @@ -727,523 +494,331 @@ updateProfileWidgets(); 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" }); - main } + // 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"; - var descText = document.createElement("span"); - descText.className = "project-card-desc-text"; - descText.textContent = truncate(project.description, 120); - desc.appendChild(descText); - - if (project.description && project.description.length > 120) { - var expanded = false; - var readMore = document.createElement("button"); - readMore.type = "button"; - readMore.className = "read-more-btn"; - readMore.textContent = "Read more"; - readMore.setAttribute("aria-expanded", "false"); - readMore.addEventListener("click", function () { - expanded = !expanded; - descText.textContent = expanded ? project.description : truncate(project.description, 120); - readMore.textContent = expanded ? "Read less" : "Read more"; - readMore.setAttribute("aria-expanded", expanded ? "true" : "false"); - }); - desc.appendChild(readMore); - } + // 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"; - var tags = document.createElement("div"); - tags.className = "project-card-tags"; - (project.skills || []).forEach(function (skill) { tags.appendChild(createTag(skill, "skill")); }); - tags.appendChild(createTag(project.level, project.level)); - tags.appendChild(createTag("Time: " + project.time, "time")); + // 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" + tagsRow.appendChild(createTag(project.level, "level " + (project.level || "").toLowerCase())); + + // 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 saveButton = document.createElement("button"); - saveButton.type = "button"; - saveButton.className = "btn-save-project"; - saveButton.setAttribute("data-save-project-id", project.id); - saveButton.setAttribute("aria-pressed", projectIsSaved(project.id) ? "true" : "false"); - if (projectIsSaved(project.id)) { - saveButton.classList.add("saved"); - saveButton.textContent = "Saved"; - } else { - saveButton.textContent = "Save Project"; - } - saveButton.addEventListener("click", function () { - toggleSavedProject(project, saveButton); - }); - var link = document.createElement("a"); link.className = "btn-details"; link.textContent = "View Full Project"; - link.href = "/project/" + project.id; - footer.appendChild(saveButton); + 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(tags); + card.appendChild(tagsRow); card.appendChild(footer); - return card; - } - - renderSavedProjects(); - - function renderResults(projects, message) { - resultsSection.style.display = "block"; - resultsLoadingEl.style.display = "none"; - resultsGrid.textContent = ""; - if (!projects || projects.length === 0) { - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "block"; - emptyMessageEl.textContent = message || "Try adjusting your skills or choosing a different interest area."; - resultsSection.scrollIntoView({ behavior: "smooth" }); - return; - } - resultsEmptyEl.style.display = "none"; - resultsGrid.style.display = "grid"; - projects.forEach(function (project) { resultsGrid.appendChild(buildProjectCard(project)); }); - resultsSection.scrollIntoView({ behavior: "smooth" }); - } - - function runProjectSearch(query) { - if (!query) return; - setLoadingState(true); - fetch("/api/search?q=" + encodeURIComponent(query)) - .then(function (response) { - return response.json().then(function (data) { - if (!response.ok) throw new Error("Search failed. Please try again."); - return data; - }); - }) - .then(function (projects) { - setLoadingState(false); - recordSearch(); - var message = projects.length - ? null - : "No projects matched \"" + query + "\". Try a different keyword."; - renderResults(projects, message); - var mobileMenu = document.getElementById("nav-mobile-menu"); - var mobileToggle = document.getElementById("nav-mobile-toggle"); - if (mobileMenu && mobileMenu.classList.contains("open")) { - mobileMenu.classList.remove("open"); - if (mobileToggle) { - mobileToggle.classList.remove("open"); - mobileToggle.setAttribute("aria-expanded", "false"); - } - } - }) - .catch(function (err) { - setLoadingState(false); - var general = document.getElementById("form-error-general"); - if (general) general.textContent = err.message || "Search failed. Please try again."; - }); - } - function bindSearchForm(form, input) { - if (!form || !input) return; - form.addEventListener("submit", function (event) { - event.preventDefault(); - runProjectSearch(input.value.trim()); - }); + return card; } - bindSearchForm(document.getElementById("topic-search-form"), document.getElementById("topic-search")); - bindSearchForm(document.getElementById("topic-search-form-mobile"), document.getElementById("topic-search-mobile")); - - skillsInput.setAttribute("role", "combobox"); - skillsInput.setAttribute("aria-expanded", "false"); - suggestions.setAttribute("role", "listbox"); - - skillsInput.addEventListener("input", function () { - showSuggestions(filteredSkills(skillsInput.value)); - }); - skillsInput.addEventListener("focus", function () { - if (skillsInput.value.trim()) showSuggestions(filteredSkills(skillsInput.value)); - }); - skillsInput.addEventListener("blur", function () { - window.setTimeout(hideSuggestions, 150); - }); - skillsInput.addEventListener("keydown", function (event) { - if (event.key === "ArrowDown" || event.key === "ArrowUp") { - if (!visibleSuggestions.length) showSuggestions(filteredSkills(skillsInput.value)); - if (!visibleSuggestions.length) return; - event.preventDefault(); - activeSuggestionIndex = event.key === "ArrowDown" - ? (activeSuggestionIndex + 1) % visibleSuggestions.length - : (activeSuggestionIndex <= 0 ? visibleSuggestions.length - 1 : activeSuggestionIndex - 1); - renderSuggestionState(); - return; - } - if (event.key === "Escape") { - hideSuggestions(); - return; - } - if (event.key === "Enter") { - event.preventDefault(); - if (activeSuggestionIndex >= 0 && visibleSuggestions[activeSuggestionIndex]) { - window.addSkill(visibleSuggestions[activeSuggestionIndex]); - } else { - window.addSkill(skillsInput.value); - } - skillsInput.value = ""; - hideSuggestions(); - } - }); - - quickPickChips.forEach(function (chip) { - chip.addEventListener("click", function () { - var skill = chip.getAttribute("data-skill"); - if (isSelected(skill)) removeSkill(skill); - else window.addSkill(skill); - skillsInput.value = ""; - hideSuggestions(); - }); - }); - - if (skillWrap) { - skillWrap.addEventListener("click", function () { skillsInput.focus(); }); + // 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; } - var clearBtn = document.getElementById("clear-filters-btn"); - if (clearBtn) { - clearBtn.addEventListener("click", function () { - form.reset(); - selectedSkills = []; - renderSelectedChips(); - syncSkillsHiddenInput(); - updateQuickPickState(); - clearAllErrors(); - hideSuggestions(); - resultsSection.style.display = "none"; - skillsInput.focus(); - }); + 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; } - var resetProgressBtn = document.getElementById("reset-progress-btn"); - if (resetProgressBtn) { - resetProgressBtn.addEventListener("click", function () { - progress.searches = 0; - progress.projectViews = 0; - progress.codeOpens = 0; - progress.completions = 0; - progress.points = 0; - progress.viewedProjects = []; - progress.completedProjects = []; - progress.achievements = []; - progress.badges = { - first_search: false, - project_explorer: false, - code_starter: false, - completionist: false, - roadmap_runner: false - }; - saveProgressState(); - updateProfileWidgets(); - showAchievementToast("Progress reset", "Your local profile has been cleared."); - }); - } +} // end isIndexPage - form.addEventListener("submit", function (event) { - event.preventDefault(); - clearAllErrors(); - if (skillsInput.value.trim()) { - window.addSkill(skillsInput.value); - skillsInput.value = ""; - hideSuggestions(); - } - if (!validateForm()) return; - setLoadingState(true); - fetch("/api/recommend", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - skills: JSON.stringify(selectedSkills), - level: document.getElementById("level").value, - interest: document.getElementById("interest").value, - time: document.getElementById("time").value - }) - }) - .then(function (response) { - return response.json().then(function (data) { - if (!response.ok) throw new Error(data.error || "Unable to generate recommendations."); - return data; - }); - }) - .then(function (data) { - setLoadingState(false); - recordSearch(); - renderResults(data.projects || [], data.message); - }) - .catch(function (err) { - setLoadingState(false); - var general = document.getElementById("form-error-general"); - if (general) general.textContent = err.message || "An unexpected error occurred. Please try again."; - }); - }); - var modal = document.getElementById("github-modal-overlay"); - var openModalBtn = document.getElementById("btn-show-github"); - 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"); - - function closeGithubModal() { - modal.classList.remove("active"); - githubInput.value = ""; - errorMsg.textContent = ""; - openModalBtn.focus(); // add this line -} +// ============================================================ +// DETAIL PAGE +// ============================================================ +if (isDetailPage) { - if (modal && openModalBtn && closeModalBtn && fetchBtn && githubInput && errorMsg) { - openModalBtn.addEventListener("click", function () { - modal.classList.add("active"); - githubInput.focus(); - }); - modal.addEventListener("keydown", function (event) { - if (!modal.classList.contains("active")) return; - var focusable = modal.querySelectorAll("button, input"); - var first = focusable[0]; - var last = focusable[focusable.length - 1]; - if (event.key === "Tab") { - if (event.shiftKey && document.activeElement === first) { - event.preventDefault(); - last.focus(); - } else if (!event.shiftKey && document.activeElement === last) { - event.preventDefault(); - first.focus(); - } - } - if (event.key === "Escape") closeGithubModal(); -}); - closeModalBtn.addEventListener("click", closeGithubModal); - modal.addEventListener("click", function (event) { - if (event.target === modal) closeGithubModal(); - }); - fetchBtn.addEventListener("click", function () { - var username = githubInput.value.trim(); - errorMsg.textContent = ""; - if (!username) { - errorMsg.textContent = "Please enter a GitHub username."; - return; - } - fetchBtn.disabled = true; - fetchBtn.textContent = "Syncing..."; - fetch("https://api.github.com/users/" + encodeURIComponent(username) + "/repos?sort=updated&per_page=100") - .then(function (response) { - if (!response.ok) throw new Error(response.status === 404 ? "Username not found." : "Unable to fetch GitHub repositories."); - return response.json(); - }) - .then(function (repos) { - var languages = []; - repos.forEach(function (repo) { - if (repo.language && languages.indexOf(repo.language) === -1) languages.push(repo.language); - }); - if (!languages.length) { - errorMsg.textContent = "No public languages found."; - return; - } - languages.forEach(window.addSkill); - closeGithubModal(); - }) - .catch(function (err) { - errorMsg.textContent = err.message || "Failed to fetch skills."; - }) - .finally(function () { - fetchBtn.disabled = false; - fetchBtn.textContent = "Fetch Skills"; - }); - }); - } -})(); + 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
     
    -(function initDetailPage() {
    -  if (typeof PROJECT_ID === "undefined") return;
    -  recordProjectView();
    -
    -  var codePanel = document.getElementById("code-panel");
    -  var codePanelOverlay = document.getElementById("code-panel-overlay");
    -  var codeContentEl = document.getElementById("code-content");
    -  var codePanelFilename = document.getElementById("code-panel-filename");
    -  var btnViewCode = document.getElementById("btn-view-code");
    -  var btnViewCodeSm = document.getElementById("btn-view-code-sm");
    -  var btnClosePanel = document.getElementById("code-panel-close");
    -  var btnCopyCode = document.getElementById("btn-copy-code");
    -  var copyToast = document.getElementById("copy-toast");
    -  var completionBtn = document.getElementById("btn-mark-complete");
    +  // Cache flag so code is only fetched once per page load
       var codeFetched = false;
     
    -  function renderCode(code) {
    -    codeContentEl.textContent = "";
    -    String(code || "").split("\n").forEach(function (line, index) {
    -      var row = document.createElement("div");
    -      row.className = "code-line";
    -      var number = document.createElement("span");
    -      number.className = "code-line-number";
    -      number.setAttribute("aria-hidden", "true");
    -      number.textContent = index + 1;
    -      var content = document.createElement("span");
    -      content.className = "code-line-content";
    -      content.textContent = line;
    -      row.appendChild(number);
    -      row.appendChild(content);
    -      codeContentEl.appendChild(row);
    -    });
    -  }
    -
    -  function fetchStarterCode() {
    -    codeContentEl.textContent = "Loading starter code...";
    -    fetch("/project/" + PROJECT_ID + "/code")
    -      .then(function (response) {
    -        return response.json().then(function (data) {
    -          if (!response.ok) throw new Error(data.error || "Starter code unavailable.");
    -          return data;
    -        });
    -      })
    -      .then(function (data) {
    -        codePanelFilename.textContent = data.filename;
    -        renderCode(data.code);
    -        codeFetched = true;
    -      })
    -      .catch(function (err) {
    -        codeContentEl.textContent = err.message || "Could not load starter code. Try downloading it instead.";
    -      });
    -  }
    -
    +  //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";
    -    recordCodeOpen();
    +
    +    // 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);
    -  document.addEventListener("keydown", function (event) {
    -    if (event.key === "Escape") 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 = Array.prototype.slice.call(codeContentEl.querySelectorAll(".code-line-content"))
    -        .map(function (line) { return line.textContent; })
    -        .join("\n");
    -      if (!code) return;
    -      var done = function () {
    -        if (copyToast) {
    -          copyToast.classList.add("show");
    -          window.setTimeout(function () { copyToast.classList.remove("show"); }, 2500);
    -        }
    -      };
    +      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(done);
    +        navigator.clipboard.writeText(code).then(showCopySuccess).catch(function () {
    +          fallbackCopy(code); // clipboard api failed, try the old way
    +        });
           } else {
    -        var textarea = document.createElement("textarea");
    -        textarea.value = code;
    -        textarea.style.cssText = "position:fixed;top:-9999px;left:-9999px";
    -        document.body.appendChild(textarea);
    -        textarea.focus();
    -        textarea.select();
    -        try { document.execCommand("copy"); } catch (err) {}
    -        document.body.removeChild(textarea);
    -        done();
    +        fallbackCopy(code); // Clipboard API not supported, use fallback method
           }
         });
       }
     
    -  var roadmapCheckboxes = Array.prototype.slice.call(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");
    -  var roadmapStorageKey = "devpath-roadmap-progress-" + PROJECT_ID;
    -
    -  function updateRoadmapProgress() {
    -    if (!roadmapCheckboxes.length) return;
    -    var completed = roadmapCheckboxes.filter(function (checkbox) { return checkbox.checked; }).length;
    -    var percent = Math.round((completed / roadmapCheckboxes.length) * 100);
    -    roadmapCheckboxes.forEach(function (checkbox) {
    -      var step = checkbox.closest(".roadmap-step");
    -      if (step) step.classList.toggle("completed", checkbox.checked);
    -    });
    -    if (progressFill) progressFill.style.width = percent + "%";
    -    if (progressText) progressText.textContent = percent + "% completed";
    -    if (progressBar) progressBar.setAttribute("aria-valuenow", String(percent));
    -    try {
    -      localStorage.setItem(roadmapStorageKey, JSON.stringify(roadmapCheckboxes.map(function (checkbox) {
    -        return checkbox.checked;
    -      })));
    -    } catch (err) {}
    -  }
    +  // 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 (
    +    openModalBtn &&
    +    closeModalBtn &&
    +    modal &&
    +    githubInput &&
    +    fetchBtn &&
    +    errorMsg
    +) {
    +// 1. Open Github Input Modal
    +  openModalBtn.addEventListener('click', (e) => {
    +      e.preventDefault();
    +      modal.classList.add('active');
    +      githubInput.focus();
    +  });
     
    -  try {
    -    var saved = JSON.parse(localStorage.getItem(roadmapStorageKey) || "[]");
    -    roadmapCheckboxes.forEach(function (checkbox, index) {
    -      checkbox.checked = !!saved[index];
    -    });
    -  } catch (err) {}
    -  roadmapCheckboxes.forEach(function (checkbox) {
    -    checkbox.addEventListener("change", updateRoadmapProgress);
    +  // 2. Close Github Input Modal
    +  const closeGithubModal = () => {
    +      modal.classList.remove('active');
    +      githubInput.value = '';
    +      errorMsg.textContent = '';
    +  };
    +
    +  closeModalBtn.addEventListener('click', closeGithubModal);
    +
    +  // Close on clicking outside the card
    +  modal.addEventListener('click', (e) => {
    +      if (e.target === modal) closeGithubModal();
       });
    -  updateRoadmapProgress();
     
    -  if (completionBtn) {
    -    completionBtn.addEventListener("click", function () {
    -      recordCompletion(PROJECT_ID, typeof PROJECT_TITLE !== "undefined" ? PROJECT_TITLE : "");
    -      showAchievementToast("Project completed", "Nice work finishing this project.");
    -    });
    -  }
    -})();
    +  // 3. Fetch Skills Logic
    +  fetchBtn.addEventListener('click', async () => {
    +      const username = githubInput.value.trim();
    +      if (!username) return;
     
    -(function initScrollButton() {
    -  var button = document.getElementById("scroll-top-btn");
    -  var icon = document.getElementById("scroll-btn-icon");
    -  if (!button) return;
    -  var atBottom = false;
    +      fetchBtn.disabled = true;
    +      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))];
    +
    +          if (langs.length > 0) {
    +              langs.forEach(lang => {
    +                  if (typeof addSkill === 'function') addSkill(lang);
    +              });
    +              closeGithubModal();
    +          } else {
    +              errorMsg.textContent = "No public languages found.";
    +          }
    +      } catch (err) {
    +          errorMsg.textContent = err.message ?? "Failed to fetch skills";
    +      } finally {
    +          fetchBtn.disabled = false;
    +          fetchBtn.textContent = 'Fetch Skills';
    +      }
    +  });
    +}
     
    -  function nearBottom() {
    -    return window.innerHeight + window.pageYOffset >= document.body.scrollHeight - 40;
    -  }
    +/* ---- Scroll-to-top button ---- */
     
    -  function update() {
    -    button.classList.toggle("visible", window.pageYOffset > 200);
    -    atBottom = nearBottom();
    -    button.setAttribute("aria-label", atBottom ? "Scroll to top" : "Scroll to bottom");
    -    button.title = atBottom ? "Scroll to top" : "Scroll to bottom";
    -    if (icon) icon.innerHTML = atBottom ? '' : '';
    +/* 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');
       }
    +}
     
    -  window.addEventListener("scroll", update, { passive: true });
    -  button.addEventListener("click", function () {
    -    window.scrollTo({ top: atBottom ? 0 : document.body.scrollHeight, behavior: "smooth" });
    -  });
    -  update();
    -})();
    +/* 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);
    +}
    diff --git a/templates/index.html b/templates/index.html
    index fd3cb94..b691747 100644
    --- a/templates/index.html
    +++ b/templates/index.html
    @@ -528,8 +528,12 @@ 

    Find Your Next Project

    - - + + +
    +
    +
    - + +
    @@ -683,13 +693,9 @@

    Find Your Next Project

    - - -
    + + +