diff --git a/static/script.js b/static/script.js index 28dbbeb..d38c83a 100644 --- a/static/script.js +++ b/static/script.js @@ -34,462 +34,695 @@ applyTheme(current === "dark" ? "light" : "dark"); }); - initTheme(); -})(); - + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initTheme); + } else { + initTheme(); + } + +}()); + + +// ============================================================ +// Detect which page we are on +// ============================================================ +var isIndexPage = !!document.getElementById("recommend-form"); +// !! 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"); +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 +// ============================================================ (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"); - } + if (!toggle || !menu) return; toggle.addEventListener("click", function () { - setOpen(!menu.classList.contains("open")); + var isOpen = menu.classList.toggle("open"); + + toggle.classList.toggle("open", isOpen); + toggle.setAttribute("aria-expanded", isOpen); }); menu.querySelectorAll(".nav-mobile-link").forEach(function (link) { link.addEventListener("click", function () { - setOpen(false); + menu.classList.remove("open"); + toggle.classList.remove("open"); + // FIX: reset aria-expanded when menu closes via link click + toggle.setAttribute("aria-expanded", "false"); }); }); - window.addEventListener("resize", function () { - if (window.innerWidth >= 640) setOpen(false); - }); -})(); +// ============================================================ +// INDEX PAGE +// ============================================================ +if (isIndexPage) { -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); - } -} + var form = document.getElementById("recommend-form"); -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); - } -} + var submitBtn = document.getElementById("submit-btn"); + var btnLabel = document.getElementById("btn-label"); + var btnLoading = document.getElementById("btn-loading"); -function computeProgressPoints() { - progress.points = progress.searches * 5 + progress.projectViews * 10 + - progress.codeOpens * 15 + progress.completions * 30; -} + 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"); -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); -} + var skillsHidden = document.getElementById("skills"); + var skillsTextInput = document.getElementById("skills-input"); + var chipsSelectedEl = document.getElementById("skill-chips-selected"); -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() - }); - progress.achievements = progress.achievements.slice(0, 5); -} + var quickPickChips = document.querySelectorAll(".skill-chip"); -function unlockBadge(id, title, detail) { - if (progress.badges[id]) return; - progress.badges[id] = true; - addAchievement(title, detail); - showAchievementToast("Badge unlocked", title + " - " + detail); -} + var selectedSkills = []; -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 resetSkillSelection() { -function projectIsCompleted(projectId) { - return progress.completedProjects.some(function (item) { - return (item && typeof item === "object" ? item.id : item) === projectId; - }); -} + selectedSkills = []; -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"] - ]; - 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; - } -} + if (skillsHidden) skillsHidden.value = ""; -function recordSearch() { - progress.searches += 1; - computeProgressPoints(); - tryUnlockBadges(); - saveProgressState(); - updateProfileWidgets(); -} + if (chipsSelectedEl) chipsSelectedEl.innerHTML = ""; + + if (skillsTextInput) { + skillsTextInput.value = ""; + } -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(); + 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 recordCodeOpen() { - progress.codeOpens += 1; - computeProgressPoints(); - tryUnlockBadges(); - saveProgressState(); - updateProfileWidgets(); -} + // ============================================================ + // Clear Filters + // ============================================================ + var clearFiltersBtn = document.getElementById("clear-filters-btn"); -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(); -} + if (clearFiltersBtn) { + clearFiltersBtn.addEventListener("click", function () { -loadProgressState(); -updateProfileWidgets(); + var recommendForm = document.getElementById("recommend-form"); -(function initIndexPage() { - var form = document.getElementById("recommend-form"); - if (!form) return; + if (recommendForm) { - 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 = []; + recommendForm.reset(); - function normalize(value) { - return String(value || "").trim().toLowerCase(); - } + resetSkillSelection(); - function syncSkillsHiddenInput() { - skillsHidden.value = JSON.stringify(selectedSkills); + if (skillsTextInput) { + skillsTextInput.focus(); + } + } + }); } - function isSelected(skill) { - return selectedSkills.some(function (item) { return normalize(item) === normalize(skill); }); + form.addEventListener("reset", function () { + + window.setTimeout(function () { + resetSkillSelection(); + + if (skillsTextInput) { + skillsTextInput.focus(); + } + }, 0); + }); +} + + // ============================================================ + // Skills + // ============================================================ + var availableSkills = []; + + if ( + typeof skills !== "undefined" && + Array.isArray(skills) && + skills.length > 0 + ) { + availableSkills = skills.map(function (s) { + return s.label; + }); + } else { + + availableSkills = [ + "Python", + "JavaScript", + "Java", + "C++", + "HTML", + "CSS", + "React", + "Node.js", + "Django", + "Flask", + "SQL", + "MongoDB" + ]; } +} + + var suggestionsDiv = document.getElementById("skills-suggestions"); - function canonicalSkill(rawSkill) { - var trimmed = String(rawSkill || "").trim(); - var match = availableSkills.find(function (skill) { return normalize(skill) === normalize(trimmed); }); - return match || trimmed; + var visibleSuggestions = []; + var activeSuggestionIndex = -1; + + function normalizeSkill(skill) { + return skill.trim().toLowerCase(); } - 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"); + function isSkillSelected(skill) { + + var normalizedSkill = normalizeSkill(skill); + + return selectedSkills.some(function (selectedSkill) { + return normalizeSkill(selectedSkill) === normalizedSkill; }); } +} - 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); + function getCanonicalSkill(rawSkill) { + + var normalizedSkill = normalizeSkill(rawSkill); + + var matchedSkill = availableSkills.find(function (skill) { + return normalizeSkill(skill) === normalizedSkill; }); + + return matchedSkill || rawSkill.trim(); } - window.addSkill = function addSkill(rawSkill) { - var skill = canonicalSkill(rawSkill); - if (!skill || isSelected(skill)) return; + function addSkill(rawSkill) { + + var skill = getCanonicalSkill(rawSkill); + + if (!skill) return; + + if (isSkillSelected(skill)) return; + selectedSkills.push(skill); + renderSelectedChips(); + syncSkillsHiddenInput(); + updateQuickPickState(); + clearFieldError("skills-error"); - if (skillsInput) skillsInput.focus(); - }; + } function removeSkill(skill) { - selectedSkills = selectedSkills.filter(function (item) { return normalize(item) !== normalize(skill); }); + + selectedSkills = selectedSkills.filter(function (selectedSkill) { + + return normalizeSkill(selectedSkill) !== normalizeSkill(skill); + + }); + renderSelectedChips(); + syncSkillsHiddenInput(); + updateQuickPickState(); } - function clearFieldError(id) { - var el = document.getElementById(id); - if (el) el.textContent = ""; + function renderSelectedChips() { + + chipsSelectedEl.innerHTML = ""; + + selectedSkills.forEach(function (skill) { + + var chipEl = document.createElement("span"); + + chipEl.className = "skill-chip-selected"; + + chipEl.textContent = skill; + + var removeBtn = document.createElement("button"); + + removeBtn.type = "button"; + + removeBtn.className = "skill-chip-remove"; + + removeBtn.innerHTML = "×"; + + removeBtn.addEventListener("click", function (e) { + + e.stopPropagation(); + + removeSkill(skill); + }); + + chipEl.appendChild(removeBtn); + + chipsSelectedEl.appendChild(chipEl); + }); } - function showFieldError(id, message) { - var el = document.getElementById(id); - if (el) el.textContent = message; + function syncSkillsHiddenInput() { + + if (!skillsHidden) return; + + 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 = ""; + function updateQuickPickState() { + + quickPickChips.forEach(function (chip) { + + var skill = chip.getAttribute("data-skill"); + + var isActive = isSkillSelected(skill || ""); + + chip.classList.toggle("active", isActive); + + chip.setAttribute("aria-pressed", isActive ? "true" : "false"); + }); + } + + quickPickChips.forEach(function (chip) { + + chip.addEventListener("click", function () { + + var skill = chip.getAttribute("data-skill"); + + if (!skill) return; + + var alreadySelected = isSkillSelected(skill); + + if (alreadySelected) { + removeSkill(skill); + } else { + addSkill(skill); + } + + skillsTextInput.value = ""; + }); + }); + + // ============================================================ + // Suggestions + // ============================================================ + function getFilteredSkills(query) { + + var normalizedQuery = normalizeSkill(query); + + return availableSkills + .filter(function (skill) { + + return ( + normalizeSkill(skill).includes(normalizedQuery) && + !isSkillSelected(skill) + ); + + }) + .slice(0, 8); } 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); + if (suggestionsDiv) { + suggestionsDiv.style.display = "none"; + suggestionsDiv.innerHTML = ""; + } } - 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"); - }); + function selectSuggestion(skill) { + + addSkill(skill); + + skillsTextInput.value = ""; + + hideSuggestions(); + + skillsTextInput.focus(); } - function showSuggestions(items) { + function displaySuggestions(items) { + + if (!suggestionsDiv) return; + visibleSuggestions = items; + activeSuggestionIndex = -1; - suggestions.textContent = ""; - if (!items.length) { + + if (items.length === 0) { hideSuggestions(); return; } + + suggestionsDiv.innerHTML = ""; + 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("mousedown", function (evt) { + evt.preventDefault(); }); + item.addEventListener("click", function () { - window.addSkill(skill); - skillsInput.value = ""; - hideSuggestions(); + selectSuggestion(skill); }); - suggestions.appendChild(item); + + suggestionsDiv.appendChild(item); }); - suggestions.style.display = "block"; - skillsInput.setAttribute("aria-expanded", "true"); + + suggestionsDiv.style.display = "block"; + } + + skillsTextInput.addEventListener("input", function (evt) { + + var typedValue = evt.target.value.trim(); + + if (typedValue.length === 0) { + hideSuggestions(); + return; + } + + displaySuggestions(getFilteredSkills(typedValue)); + }); + + skillsTextInput.addEventListener("keydown", function (evt) { + + 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(); + } + }); + + skillsTextInput.addEventListener("blur", function () { + + setTimeout(function () { + hideSuggestions(); + }, 150); + }); + + // ============================================================ + // Validation + // ============================================================ + function showFieldError(fieldId, message) { + + var el = document.getElementById(fieldId); + + if (el) el.textContent = message; + } + + function clearFieldError(fieldId) { + + var el = document.getElementById(fieldId); + + if (el) el.textContent = ""; + } + + function clearAllErrors() { + + [ + "skills-error", + "level-error", + "interest-error", + "time-error" + ].forEach(clearFieldError); + + var generalErr = document.getElementById("form-error-general"); + + if (generalErr) { + generalErr.textContent = ""; + } } function validateForm() { + var valid = true; - if (!selectedSkills.length) { - showFieldError("skills-error", "Please add at least one skill."); + + 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."); + + showFieldError( + "level-error", + "Please select your experience level." + ); + valid = false; } + if (!document.getElementById("interest").value) { - showFieldError("interest-error", "Please select an area of interest."); + + showFieldError( + "interest-error", + "Please select an area of interest." + ); + valid = false; } + if (!document.getElementById("time").value) { - showFieldError("time-error", "Please select your time availability."); + + showFieldError( + "time-error", + "Please select your time availability." + ); + valid = false; } return valid; } + // ============================================================ + // Submit + // ============================================================ + form.addEventListener("submit", function (evt) { + + evt.preventDefault(); + + clearAllErrors(); + + if (skillsTextInput.value.trim()) { + + addSkill(skillsTextInput.value); + + skillsTextInput.value = ""; + + hideSuggestions(); + } + + if (!validateForm()) return; + + setLoadingState(true); + + 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 (err) { + + setLoadingState(false); + + var generalErr = + document.getElementById("form-error-general"); + + if (generalErr) { + + generalErr.textContent = + "Something went wrong. Please try again."; + } + + console.error(err); + }); + }); + }); + + // ============================================================ + // Loading + // ============================================================ function setLoadingState(isLoading) { + submitBtn.disabled = isLoading; - submitBtn.setAttribute("aria-busy", isLoading ? "true" : "false"); - btnLabel.style.display = isLoading ? "none" : "inline"; - btnLoading.style.display = isLoading ? "inline-flex" : "none"; + + submitBtn.setAttribute("aria-busy", isLoading); + + btnLabel.style.display = + isLoading ? "none" : "inline"; + + btnLoading.style.display = + isLoading ? "inline-flex" : "none"; + if (isLoading) { + resultsSection.style.display = "block"; + resultsLoadingEl.style.display = "block"; + resultsGrid.style.display = "none"; + resultsEmptyEl.style.display = "none"; - resultsSection.scrollIntoView({ behavior: "smooth" }); + + resultsSection.scrollIntoView({ + behavior: "smooth" + }); + } else { + resultsLoadingEl.style.display = "none"; + + resultsGrid.style.display = "grid"; } } - function truncate(text, maxLength) { - text = text || ""; - return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; - } + // ============================================================ + // Results + // ============================================================ + function renderResults(projects, message) { - 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; + resultsSection.style.display = "block"; + + resultsLoadingEl.style.display = "none"; + + resultsGrid.innerHTML = ""; + + if (!projects || projects.length === 0) { + + resultsGrid.style.display = "none"; + + resultsEmptyEl.style.display = "block"; + + if (message && emptyMessageEl) { + emptyMessageEl.textContent = message; + } + + return; + } + + resultsEmptyEl.style.display = "none"; + + resultsGrid.style.display = "grid"; + + projects.forEach(function (project) { + + resultsGrid.appendChild(buildProjectCard(project)); + }); + + resultsSection.scrollIntoView({ + behavior: "smooth" + }); } function buildProjectCard(project) { + var card = document.createElement("div"); + card.className = "project-card"; var title = document.createElement("h3"); + title.className = "project-card-title"; + title.textContent = project.title; var desc = document.createElement("p"); + desc.className = "project-card-desc"; var descText = document.createElement("span"); descText.className = "project-card-desc-text"; @@ -512,16 +745,26 @@ updateProfileWidgets(); desc.appendChild(readMore); } - 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")); + var tagsRow = document.createElement("div"); + + tagsRow.className = "project-card-tags"; + + (project.skills || []).forEach(function (skill) { + + tagsRow.appendChild(createTag(skill, "skill")); + }); + + var levelClass = "level " + (project.level || "").toLowerCase(); + tagsRow.appendChild(createTag(project.level, levelClass)); + tagsRow.appendChild(createTag("Time: " + project.time, "time")); 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; footer.appendChild(link); @@ -533,352 +776,30 @@ updateProfileWidgets(); return card; } - 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" }); - } - - 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(); }); - } - - 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(); - }); - } - - 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."); - }); - } - - 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 = ""; - } + function createTag(text, type) { - if (modal && openModalBtn && closeModalBtn && fetchBtn && githubInput && errorMsg) { - openModalBtn.addEventListener("click", function () { - modal.classList.add("active"); - githubInput.focus(); - }); - 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 span = document.createElement("span"); -(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"); - 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); - }); - } + span.className = "project-tag project-tag--" + type; - 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."; - }); - } + span.textContent = text; - function openCodePanel() { - if (!codePanel) return; - codePanel.classList.add("active"); - if (codePanelOverlay) codePanelOverlay.classList.add("active"); - document.body.style.overflow = "hidden"; - recordCodeOpen(); - if (!codeFetched) fetchStarterCode(); + return span; } - function closeCodePanel() { - if (!codePanel) return; - codePanel.classList.remove("active"); - if (codePanelOverlay) codePanelOverlay.classList.remove("active"); - document.body.style.overflow = ""; - } + function truncate(text, maxLength) { - 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 (!text) return ""; - 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); - } - }; - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(code).then(done); - } 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(); - } - }); + return text.length > maxLength + ? text.slice(0, maxLength) + "..." + : text; } +} - 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) {} - } +} // end github modal handlers - 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); - }); - updateRoadmapProgress(); +/* ---- Scroll-to-top button ---- */ if (completionBtn) { completionBtn.addEventListener("click", function () { @@ -888,22 +809,30 @@ updateProfileWidgets(); } })(); -(function initScrollButton() { - var button = document.getElementById("scroll-top-btn"); - var icon = document.getElementById("scroll-btn-icon"); - if (!button) return; - var atBottom = false; +var scrollTopBtn = document.getElementById("scroll-top-btn"); - function nearBottom() { - return window.innerHeight + window.pageYOffset >= document.body.scrollHeight - 40; +function handleScroll() { + + if (!scrollTopBtn) return; + + if (window.pageYOffset > SCROLL_THRESHOLD) { + scrollTopBtn.classList.add("visible"); + } else { + scrollTopBtn.classList.remove("visible"); } +} + +function scrollToTop() { + + window.scrollTo({ + top: 0, + behavior: "smooth" + }); +} - 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 ? '' : ''; +if (scrollTopBtn) { + window.addEventListener('scroll', handleScroll); + scrollTopBtn.addEventListener('click', scrollToTop); } window.addEventListener("scroll", update, { passive: true }); diff --git a/templates/index.html b/templates/index.html index 5c149c1..4a7c314 100644 --- a/templates/index.html +++ b/templates/index.html @@ -531,8 +531,12 @@

    Find Your Next Project

    - - + + +
    +
    +
    - + +
    @@ -689,13 +703,9 @@

    Find Your Next Project

    - - -
    + + +