Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
359 changes: 91 additions & 268 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = "";
Expand Down Expand Up @@ -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");
Expand Down
Loading
Loading