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
-
-
-
+ Finding matches...
+
+