diff --git a/static/script.js b/static/script.js index 577a5c2..d28dd71 100644 --- a/static/script.js +++ b/static/script.js @@ -68,6 +68,79 @@ }); })(); + +// ============================================================ +// INDEX PAGE +// ============================================================ +if (isIndexPage) { + + // DOM references + var form = document.getElementById("recommend-form"); + 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 searchContainer = document.getElementById("results-search-container"); + var skillsHidden = document.getElementById("skills"); + var skillsTextInput = document.getElementById("skills-input"); + var chipsSelectedEl = document.getElementById("skill-chips-selected"); + var quickPickChips = document.querySelectorAll(".skill-chip"); + + // Tracks currently selected skills to prevent duplicates + var selectedSkills = []; + var currentProjects = []; + + + // ---------------------------------------------------------- + // Skill chip manager + // ---------------------------------------------------------- + + // Skills list for autocomplete (from skills.js) + var availableSkills = []; + if (typeof skills !== "undefined" && Array.isArray(skills) && skills.length > 0) { + availableSkills = skills.map(function (s) { return s.label; }); + } else { + // Fallback if skills.js doesn't load + availableSkills = [ + "Python", "JavaScript", "Java", "C++", "HTML", "CSS", "React", "Node.js", + "Django", "Flask", "SQL", "MongoDB", "AWS", "Docker", "Kubernetes", "Git", + "C#", "Ruby", "PHP", "Go", "Swift", "TypeScript", "Angular", "Vue.js", + "Spring", "Flutter", "TensorFlow", "PyTorch", "Data Science", + "Machine Learning", "Artificial Intelligence", "DevOps", "Cybersecurity", + "Blockchain", "UI/UX Design", "Game Development", "CI/CD", "REST API", "GraphQL", + "Rust", "Kotlin" + ]; + } + + var suggestionsDiv = document.getElementById("skills-suggestions"); + var skillWrap = document.getElementById("skill-input-wrap"); + var visibleSuggestions = []; + var activeSuggestionIndex = -1; + + availableSkills = availableSkills.filter(function (skill, index, list) { + return typeof skill === "string" && skill.trim() && + list.findIndex(function (item) { + return item.toLowerCase() === skill.toLowerCase(); + }) === index; + }); + + if (suggestionsDiv) { + suggestionsDiv.setAttribute("role", "listbox"); + } + + function normalizeSkill(skill) { + return skill.trim().toLowerCase(); + } + + function isSkillSelected(skill) { + var normalizedSkill = normalizeSkill(skill); + return selectedSkills.some(function (selectedSkill) { + return normalizeSkill(selectedSkill) === normalizedSkill; + }); var STORAGE_KEY = "devpathUserProgress"; var progress = { searches: 0, @@ -660,6 +733,8 @@ updateProfileWidgets(); //if array is empty it shows the "no results" message instead function renderResults(projects, message) { resultsSection.style.display = "block"; + searchContainer.style.display = projects.length ? "block" : "none"; + currentProjects = projects; resultsLoadingEl.style.display = "none"; // Clear out any cards from a previous search before showing new ones resultsGrid.innerHTML = ""; @@ -804,280 +879,28 @@ updateProfileWidgets(); 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()); - }); - } - - 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(); }); - } - - 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 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 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."); - }); + 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; } - 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 -} +} // end isIndexPage - 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"; - }); - }); - } -})(); -(function initDetailPage() { - if (typeof PROJECT_ID === "undefined") return; - recordProjectView(); +// ============================================================ +// DETAIL PAGE +// ============================================================ +if (isDetailPage) { var codePanel = document.getElementById("code-panel"); var codePanelOverlay = document.getElementById("code-panel-overlay"); diff --git a/templates/index.html b/templates/index.html index fd3cb94..a4e6571 100644 --- a/templates/index.html +++ b/templates/index.html @@ -734,6 +734,14 @@