diff --git a/DevPath/CHANGELOG.md b/DevPath/CHANGELOG.md deleted file mode 100644 index f775559a..00000000 --- a/DevPath/CHANGELOG.md +++ /dev/null @@ -1,20 +0,0 @@ -# Changelog - -All notable changes to this project are documented here. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - -## [Unreleased] - -### Added - -- Initial CHANGELOG.md setup for tracking project history -- Documentation structure for future contributor updates - -### Changed - -- Contributors are now expected to document user-facing changes in CHANGELOG.md - -### Fixed - -- Fixed an issue where skill chips on the homepage were unclickable due to JavaScript syntax errors \ No newline at end of file diff --git a/DevPath/static/script.js b/DevPath/static/script.js deleted file mode 100644 index 1496e90f..00000000 --- a/DevPath/static/script.js +++ /dev/null @@ -1,938 +0,0 @@ -// script.js — DevPath client-side logic -// -// Responsibilities: -// - Mobile navigation toggle -// - Skill chip manager (add/remove skills) -// - Form validation with per-field error messages -// - Recommendation API call and loading states -// - Result card rendering -// - Code viewer panel (detail page) - -// ============================================================ -// Detect which page we are on -// ============================================================ -// !! trick turns the DOM result into a simple true/false -var isIndexPage = !!document.getElementById("recommend-form"); -// PROJECT_ID is set by the server only on detail pages, so if it's missing we're elsewhere -var isDetailPage = typeof PROJECT_ID !== "undefined"; -var modal = document.getElementById('github-modal-overlay'); -var openModalBtn = document.getElementById('btn-show-github'); // The trigger in your main form -var closeModalBtn = document.getElementById('btn-close-github'); -var fetchBtn = document.getElementById('btn-fetch-github'); -var githubInput = document.getElementById('github-username'); -var errorMsg = document.getElementById('github-modal-error'); - - -// ============================================================ -// Mobile navigation toggle (runs on all pages) -// ============================================================ -(function initMobileNav() { - var toggle = document.getElementById("nav-mobile-toggle"); //hamburger button - var menu = document.getElementById("nav-mobile-menu"); //dropdown menu - - // Nothing to do if the nav isn't on this page, just bail out - if (!toggle || !menu) return; - - toggle.addEventListener("click", function () { - // classList.toggle returns true if class was added, false if removed - var isOpen = menu.classList.toggle("open"); - toggle.classList.toggle("open", isOpen); - // Keep aria-expanded in sync so screen readers know if menu is open or closed - toggle.setAttribute("aria-expanded", isOpen); - }); - - // Close menu when any mobile link is clicked - var mobileLinks = menu.querySelectorAll(".nav-mobile-link"); - for (var i = 0; i < mobileLinks.length; i++) { - mobileLinks[i].addEventListener("click", function () { - menu.classList.remove("open"); - toggle.classList.remove("open"); - }); - } -})(); - - -// ============================================================ -// INDEX PAGE -// ============================================================ -if (isIndexPage) { - - // DOM references - // grabbing all the elements we'll need so we're not calling getElementById over and over again throughout the code - var form = document.getElementById("recommend-form"); - var submitBtn = document.getElementById("submit-btn"); - var btnLabel = document.getElementById("btn-label"); // "get recommendations" text - var btnLoading = document.getElementById("btn-loading"); // spinner icon inside the button - var resultsSection = document.getElementById("results-section"); - var resultsGrid = document.getElementById("results-grid"); - var resultsLoadingEl = document.getElementById("results-loading"); // "Loading..." text in the results - var resultsEmptyEl = document.getElementById("results-empty"); - var emptyMessageEl = document.getElementById("empty-message"); - var skillsHidden = document.getElementById("skills"); // the hidden input that holds skills list - var skillsTextInput = document.getElementById("skills-input"); //visible text box in which user types skills - var chipsSelectedEl = document.getElementById("skill-chips-selected"); //selected skills tags container - var quickPickChips = document.querySelectorAll(".skill-chip"); // predefined skills user can click - - // Tracks currently selected skills to prevent duplicates - var selectedSkills = []; - // Clear Filters Button Functionality -var clearFiltersBtn = document.getElementById("clear-filters-btn"); -if (clearFiltersBtn) { - clearFiltersBtn.addEventListener("click", function() { - var recommendForm = document.getElementById("recommend-form"); - if (recommendForm) { - // 1. Reset standard form dropdowns and fields - recommendForm.reset(); - - // 2. Clear out the internal JavaScript array tracker completely - selectedSkills = []; - - // 3. Clear the hidden inputs and visual chips using the file's own variables - if (skillsHidden) skillsHidden.value = ""; - if (chipsSelectedEl) chipsSelectedEl.innerHTML = ""; - if (skillsTextInput) { - skillsTextInput.value = ""; - skillsTextInput.focus(); // Place cursor back on input - } - - // 4. Hide autocomplete suggestions if any are open - var suggestionsBox = document.getElementById("skills-suggestions"); - if (suggestionsBox) suggestionsBox.innerHTML = ""; - - // 5. Reset quick-pick chip visual active states if they have any - if (quickPickChips) { - for (var j = 0; j < quickPickChips.length; j++) { - quickPickChips[j].classList.remove("active", "selected"); - } - } - } - }); -} - - - // ---------------------------------------------------------- - // 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; - - // Duplicates marquee items to create a seamless infinite scrolling effect - function initSkillStripMarquee() { - var marquee = document.querySelector(".skill-strip-marquee"); - var track = marquee && marquee.querySelector(".skill-strip-track"); - - if (!marquee || !track || track.querySelector(".skill-strip-items[data-marquee-clone='true']")) { - return; - } - - var clone = track.querySelector(".skill-strip-items").cloneNode(true); - clone.setAttribute("aria-hidden", "true"); - clone.setAttribute("data-marquee-clone", "true"); - track.appendChild(clone); - } - - // Clean up the initial skills list by removing any empty or duplicate entries - var uniqueSkills = []; - var seenSkills = {}; - for (var k = 0; k < availableSkills.length; k++) { - var s = availableSkills[k]; - if (typeof s === "string" && s.trim()) { - var lower = s.toLowerCase(); - if (!seenSkills[lower]) { - seenSkills[lower] = true; - uniqueSkills.push(s); - } - } - } - availableSkills = uniqueSkills; - - if (suggestionsDiv) { - suggestionsDiv.setAttribute("role", "listbox"); - } - - initSkillStripMarquee(); - - // Standardizes skill strings to lowercase for reliable comparisons - function normalizeSkill(skill) { - return skill.trim().toLowerCase(); - } - - // Checks if the user has already added this specific skill - function isSkillSelected(skill) { - var normalizedSkill = normalizeSkill(skill); - return selectedSkills.some(function (selectedSkill) { - return normalizeSkill(selectedSkill) === normalizedSkill; - }); - } - - // Retrieves the properly capitalized version of a skill if it exists - function getCanonicalSkill(rawSkill) { - var normalizedSkill = normalizeSkill(rawSkill); - var matchedSkill = availableSkills.filter(function (skill) { - return normalizeSkill(skill) === normalizedSkill; - })[0]; - return matchedSkill || rawSkill.trim(); - } - - // Returns up to 8 available skills that match the user's search query - function getFilteredSkills(query) { - var normalizedQuery = normalizeSkill(query); - return availableSkills.filter(function (skill) { - return normalizeSkill(skill).indexOf(normalizedQuery) !== -1 && !isSkillSelected(skill); - }).slice(0, 8); - } - - // Updates ARIA attributes for screen readers based on dropdown visibility - function syncSuggestionsA11yState() { - skillsTextInput.setAttribute("aria-expanded", visibleSuggestions.length > 0 ? "true" : "false"); - } - - // Highlights the currently focused item in the autocomplete dropdown - function renderActiveSuggestion() { - if (!suggestionsDiv) return; - var suggestionItems = suggestionsDiv.querySelectorAll(".suggestion-item"); - for (var i = 0; i < suggestionItems.length; i++) { - var isActive = (i === activeSuggestionIndex); - suggestionItems[i].classList.toggle("suggestion-item--active", isActive); - suggestionItems[i].setAttribute("aria-selected", isActive ? "true" : "false"); - } - } - - // Hides and clears out the autocomplete suggestion box - function hideSuggestions() { - visibleSuggestions = []; - activeSuggestionIndex = -1; - if (suggestionsDiv) { - suggestionsDiv.style.display = "none"; - suggestionsDiv.innerHTML = ""; - } - syncSuggestionsA11yState(); - } - - // Processes the selection of a skill from the dropdown menu - function selectSuggestion(skill) { - addSkill(skill); - skillsTextInput.value = ""; - hideSuggestions(); - skillsTextInput.focus(); - } - - // Builds the DOM elements for the autocomplete dropdown based on matches - function displaySuggestions(items) { - if (!suggestionsDiv) return; - visibleSuggestions = items; - activeSuggestionIndex = -1; - if (items.length === 0) { - hideSuggestions(); - return; - } - suggestionsDiv.innerHTML = ""; - items.forEach(function (skill, index) { - var item = document.createElement("div"); - item.className = "suggestion-item"; - item.textContent = skill; - item.setAttribute("role", "option"); - item.setAttribute("id", "skills-suggestion-" + index); - item.setAttribute("aria-selected", "false"); - - // Prevent the input blur handler from closing the menu before click runs. - item.addEventListener("mousedown", function (evt) { - evt.preventDefault(); - }); - - item.addEventListener("mouseenter", function () { - activeSuggestionIndex = index; - renderActiveSuggestion(); - }); - - item.addEventListener("click", function () { - selectSuggestion(skill); - }); - - suggestionsDiv.appendChild(item); - }); - suggestionsDiv.style.display = "block"; - syncSuggestionsA11yState(); - } - - // Toggles the active visual state on the predefined skill buttons - function updateQuickPickState() { - for (var i = 0; i < quickPickChips.length; i++) { - var chip = quickPickChips[i]; - var isActive = isSkillSelected(chip.getAttribute("data-skill") || ""); - chip.classList.toggle("active", isActive); - chip.setAttribute("aria-pressed", isActive ? "true" : "false"); - } - } - - // Add skill on Enter key in the text input - // when the user types a skill and hits Enter, add it we intercept Enter here so it doesn't accidentally submit the whole form - skillsTextInput.addEventListener("keydown", function (evt) { - if (evt.key === "ArrowDown" || evt.key === "ArrowUp") { - if (visibleSuggestions.length === 0) { - displaySuggestions(getFilteredSkills(skillsTextInput.value)); - } - if (visibleSuggestions.length === 0) return; - evt.preventDefault(); - if (evt.key === "ArrowDown") { - activeSuggestionIndex = (activeSuggestionIndex + 1) % visibleSuggestions.length; - } else { - activeSuggestionIndex = activeSuggestionIndex <= 0 - ? visibleSuggestions.length - 1 - : activeSuggestionIndex - 1; - } - renderActiveSuggestion(); - return; - } - - if (evt.key === "Escape") { - hideSuggestions(); - return; - } - - if (evt.key === "Enter") { - evt.preventDefault(); - if (activeSuggestionIndex >= 0 && visibleSuggestions[activeSuggestionIndex]) { - selectSuggestion(visibleSuggestions[activeSuggestionIndex]); - return; - } - if (skillsTextInput.value.trim()) { - addSkill(skillsTextInput.value); - skillsTextInput.value = ""; - } - hideSuggestions(); - } - }); - - // Add/toggle skill on quick-pick chip click - for (var i = 0; i < quickPickChips.length; i++) { - (function (chip) { - chip.addEventListener("click", function () { - var skill = chip.getAttribute("data-skill"); - var isAlreadySelected = selectedSkills.some(function (s) { - return s.toLowerCase() === skill.toLowerCase(); - }); - - if (isAlreadySelected) { - removeSkill(skill); - } else { - addSkill(skill); - } - hideSuggestions(); - skillsTextInput.value = ""; - }); - })(quickPickChips[i]); - } - - // Show suggestions on input - skillsTextInput.addEventListener("input", function (evt) { - var typedValue = evt.target.value.trim(); - if (typedValue.length === 0) { - hideSuggestions(); - return; - } - displaySuggestions(getFilteredSkills(typedValue)); - }); - - skillsTextInput.addEventListener("focus", function () { - if (skillsTextInput.value.trim()) { - displaySuggestions(getFilteredSkills(skillsTextInput.value)); - } - }); - - // Hide suggestions when input loses focus - skillsTextInput.addEventListener("blur", function () { - setTimeout(function () { hideSuggestions(); }, 150); - }); - - if (skillWrap) { - skillWrap.addEventListener("click", function () { - skillsTextInput.focus(); - }); - } - - - document.addEventListener("click", function (evt) { - if (skillWrap && !skillWrap.contains(evt.target)) { - hideSuggestions(); - } - }); - - //add a skill to the list if it's not empty or a duplicate - function addSkill(rawSkill) { - // Clean up any extra spaces and match to canonical skill name - var skill = getCanonicalSkill(rawSkill); - // Nothing to add if string is empty after trimming - if (!skill) return; - - // Block duplicate entries (case-insensitive) - if (isSkillSelected(skill)) return; - - selectedSkills.push(skill); - renderSelectedChips(); - syncSkillsHiddenInput(); - updateQuickPickState(); - // Once a skill is added, remove the "please add a skill" error if it was showing - clearFieldError("skills-error"); - } - - // remove a skill from the list and update the UI accordingly - function removeSkill(skill) { - // Rebuild the array without the skill that was just removed - selectedSkills = selectedSkills.filter(function (selectedSkill) { - return normalizeSkill(selectedSkill) !== normalizeSkill(skill); - }); - renderSelectedChips(); - syncSkillsHiddenInput(); - updateQuickPickState(); - } - - // recreate the selected skills chips based on the current array(selectedSkills) - // called every time we add or remove a skill - function renderSelectedChips() { - // Wipe out old chips first so we don't end up with duplicates in the UI - chipsSelectedEl.innerHTML = ""; - selectedSkills.forEach(function (skill) { - // Create a new chip element for each selected skill - var chipEl = document.createElement("span"); - chipEl.className = "skill-chip-selected"; - chipEl.textContent = skill; - - // Remove button for each chip (create lil "x" button) - var removeBtn = document.createElement("button"); - removeBtn.type = "button"; - removeBtn.className = "skill-chip-remove"; - removeBtn.innerHTML = "×"; //'x' symbol - removeBtn.setAttribute("aria-label", "Remove " + skill); - removeBtn.addEventListener("click", function (e) { - // Stop click from bubbling up to the chip wrap's click listener - e.stopPropagation(); - removeSkill(skill); - }); - - chipEl.appendChild(removeBtn); // put x button inside the chip - chipsSelectedEl.appendChild(chipEl); //add chip to page - }); - } - - function syncSkillsHiddenInput() { - if (!skillsHidden){ - skillsHidden = document.getElementById("skills"); - } - // Keep the hidden in sync for form serialisation - // The API expects a comma-separated string, so join the array that way - skillsHidden.value = selectedSkills.join(", "); - } - - updateQuickPickState(); - - - // ---------------------------------------------------------- - // Form validation - // ---------------------------------------------------------- - - //puts error msg under specific field - function showFieldError(fieldId, message) { - var el = document.getElementById(fieldId); - if (el) el.textContent = message; - } - - //clears error msg under specific field - function clearFieldError(fieldId) { - var el = document.getElementById(fieldId); - if (el) el.textContent = ""; //empty string = no error msg - } - - //clears all error msgs in the form, called at the start of form submission to reset any previous errors - function clearAllErrors() { - ["skills-error", "level-error", "interest-error", "time-error"].forEach(clearFieldError); - var generalErr = document.getElementById("form-error-general"); - if (generalErr) generalErr.textContent = ""; - } - - // checks form fields and shows error messages if any required field is missing or invalid. - // Returns true if the form is valid, false otherwise - function validateForm() { - var valid = true; - - // Check both the array and the hidden input since skills can come from either source - if (selectedSkills.length === 0 && !skillsHidden.value.trim()) { - showFieldError("skills-error", "Please add at least one skill."); - valid = false; - } - if (!document.getElementById("level").value) { - showFieldError("level-error", "Please select your experience level."); - valid = false; - } - if (!document.getElementById("interest").value) { - showFieldError("interest-error", "Please select an area of interest."); - valid = false; - } - if (!document.getElementById("time").value) { - showFieldError("time-error", "Please select your time availability."); - valid = false; - } - - return valid; - } - - - // ---------------------------------------------------------- - // Form submission and API call - // ---------------------------------------------------------- - - form.addEventListener("submit", function (evt) { - evt.preventDefault(); //stop the browser from reloading the page on form submit - clearAllErrors() - - if (skillsTextInput.value.trim()) { - addSkill(skillsTextInput.value); - skillsTextInput.value = ""; - hideSuggestions(); - } - - if (!validateForm()) return; //stop - anything missing/invalid - - setLoadingState(true); - - // Allow browser to paint spinner before request starts - 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) { - // this runs if the network request itself fails - setLoadingState(false); - var generalErr = document.getElementById("form-error-general"); - if (generalErr) generalErr.textContent = "Something went wrong. Please try again."; - }); - }); - }); - - // Manages the loading state of the form and results section(whats visible or not) - function setLoadingState(isLoading) { - // Disable the button so the user can't accidentally submit twice - submitBtn.disabled = isLoading; - submitBtn.setAttribute("aria-busy", isLoading); - btnLabel.style.display = isLoading ? "none" : "inline"; - btnLoading.style.display = isLoading ? "inline-flex" : "none"; - btnLabel.style.display = isLoading ? "none" : "inline"; - btnLoading.style.display = isLoading ? "inline" : "none"; - - if (isLoading) { - // Show the results section with only the loading indicator visible - resultsSection.style.display = "block"; - resultsLoadingEl.style.display = "block"; - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "none"; - // Scroll down so the user can see the spinner without manually scrolling - resultsSection.scrollIntoView({ behavior: "smooth" }); - } else { - resultsLoadingEl.style.display = "none"; - resultsGrid.style.display = "grid"; //switch back to gird layout - } - } - - - // ---------------------------------------------------------- - // Render result cards - // ---------------------------------------------------------- - - //takes the array of projects from the api and draws them on the page as cards - //if array is empty it shows the "no results" message instead - function renderResults(projects, message) { - resultsSection.style.display = "block"; - resultsLoadingEl.style.display = "none"; - // Clear out any cards from a previous search before showing new ones - resultsGrid.innerHTML = ""; - - if (!projects || projects.length === 0) { //if no projects returned from api, show the "no results" message and hide the grid - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "block"; - if (message && emptyMessageEl) emptyMessageEl.textContent = message; //if api sent back a message (e.g. "no projects found matching your criteria"), show that - resultsSection.scrollIntoView({ behavior: "smooth" }); - return; - } - - resultsEmptyEl.style.display = "none"; - resultsGrid.style.display = "grid"; - - //build a card for each project and add it to the grid - projects.forEach(function (project) { - resultsGrid.appendChild(buildProjectCard(project)); - }); - - resultsSection.scrollIntoView({ behavior: "smooth" }); - } - - // builds one project card as a DOM element and returns it - // the card has title, short description, tags and link - function buildProjectCard(project) { - var card = document.createElement("div"); - card.className = "project-card"; - - // Title - var title = document.createElement("h3"); - title.className = "project-card-title"; - title.textContent = project.title; - - // Description (truncated for visual consistency) - var desc = document.createElement("p"); - desc.className = "project-card-desc"; - // Cut description to 120 chars so all cards stay the same height - desc.textContent = truncate(project.description, 120); - - // Tags row - var tagsRow = document.createElement("div"); - tagsRow.className = "project-card-tags"; - - // Show the first two skills as tags - (project.skills || []).slice(0, 2).forEach(function (skill) { - tagsRow.appendChild(createTag(skill, "skill")); - }); - - // Level tag (colour-coded via CSS class) - // Lowercase so it matches the CSS class names like "level beginner", "level advanced" - var levelClass = "level " + (project.level || "").toLowerCase(); - tagsRow.appendChild(createTag(project.level, levelClass)); - - // Time tag - tagsRow.appendChild(createTag("Time: " + project.time, "time")); - - // Footer with view-details link - var footer = document.createElement("div"); - footer.className = "project-card-footer"; - - var link = document.createElement("a"); - link.className = "btn-details"; - link.textContent = "View Full Project"; - link.href = "/project/" + project.id; //each project has a unique id - - footer.appendChild(link); - - // Assemble the card in order - card.appendChild(title); - card.appendChild(desc); - card.appendChild(tagsRow); - card.appendChild(footer); - - return card; - } - - // helper to create a coloured tag element (used for skills, level, time tags on the cards) - function createTag(text, type) { - var span = document.createElement("span"); - // The type becomes a BEM modifier so CSS can style each tag differently - span.className = "project-tag project-tag--" + type; - span.textContent = text; - return span; - } - - 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; - } - -} // end isIndexPage - - -// ============================================================ -// DETAIL PAGE -// ============================================================ -if (isDetailPage) { - - var codePanel = document.getElementById("code-panel"); // sliding panel that shows the starter code " - var codePanelOverlay = document.getElementById("code-panel-overlay"); // background overlay - var codeContentEl = document.getElementById("code-content"); //
element inside the panel where the code will be inserted
- var codePanelFilename = document.getElementById("code-panel-filename"); // filename display
- var btnViewCode = document.getElementById("btn-view-code"); // button to open the code panel on desktop
- var btnViewCodeSm = document.getElementById("btn-view-code-sm"); // button to open the code panel on mobile (could be the same button with different styling, but we have two here for simplicity)
- var btnClosePanel = document.getElementById("code-panel-close"); // button inside the panel to close it
-
- // Cache flag so code is only fetched once per page load
- var codeFetched = false;
-
- //opens the sliding code panel
- function openCodePanel() {
- // Panel element might not exist on every detail page, so check first
- if (!codePanel) return;
- codePanel.classList.add("active");
- if (codePanelOverlay) codePanelOverlay.classList.add("active");
- // Lock background scroll so the page doesn't scroll behind the panel
- document.body.style.overflow = "hidden";
-
- // Only fetch the code on the first open, no need to re-fetch every time
- if (!codeFetched) fetchStarterCode();
- }
-
- //closes the code panel and hides the overlay
- function closeCodePanel() {
- if (!codePanel) return;
- codePanel.classList.remove("active");
- if (codePanelOverlay) codePanelOverlay.classList.remove("active");
- // Restore normal scrolling once the panel is closed
- document.body.style.overflow = "";
- }
-
- //fetches the starter code from the server via an API call
- //inserts the code into the panel and handles loading/error states
- function fetchStarterCode() {
- // Show a loading message while we wait for the API response
- if (codeContentEl) codeContentEl.textContent = "Loading starter code...";
-
- fetch("/project/" + PROJECT_ID + "/code")
- .then(function (res) { return res.json(); })
- .then(function (data) {
- if (data.error) {
- if (codeContentEl) codeContentEl.textContent = "Error: " + data.error;
- return;
- }
- if (codePanelFilename) codePanelFilename.textContent = data.filename;
- if (codeContentEl) {
- codeContentEl.textContent = "";
- renderCodeWithLineNumbers(data.code).forEach(function (row) {
- codeContentEl.appendChild(row);
- });
- }
- // Mark as fetched so we don't hit the API again on the next open
- codeFetched = true;
- })
- .catch(function () {
- if (codeContentEl) {
- codeContentEl.textContent = "Could not load starter code. Try downloading it instead.";
- }
- });
- }
-
- // Attach open/close handlers
- if (btnViewCode) btnViewCode.addEventListener("click", openCodePanel);
- if (btnViewCodeSm) btnViewCodeSm.addEventListener("click", openCodePanel);
- if (btnClosePanel) btnClosePanel.addEventListener("click", closeCodePanel);
-
- if (codePanelOverlay) {
- codePanelOverlay.addEventListener("click", closeCodePanel); //clicking on the background overlay to also close the panel
- }
-
- // Let keyboard users close the panel with Escape — important for accessibility
- document.addEventListener("keydown", function (evt) {
- if (evt.key === "Escape") closeCodePanel(); //esc key to close
- });
-
- // ----------------------------------------------------------
- // Copy Code button
- // ----------------------------------------------------------
- var btnCopyCode = document.getElementById("btn-copy-code");
- var copyToast = document.getElementById("copy-toast"); //popup msg when copied
- var toastTimeout = null;
-
- //shows the "copied to clipboard" state on the button and the toast message, then resets after a short delay
- function showCopySuccess() {
- if (!btnCopyCode) return;
-
- // Swap icons on the button(copy and checkmark icons)
- var copyIcon = btnCopyCode.querySelector(".copy-icon");
- var checkIcon = btnCopyCode.querySelector(".check-icon");
- var btnLabel = btnCopyCode.querySelector(".copy-btn-label");
-
- if (copyIcon) copyIcon.style.display = "none";
- if (checkIcon) checkIcon.style.display = "inline";
- if (btnLabel) btnLabel.textContent = "Copied!";
- btnCopyCode.classList.add("copied");
- // Disable button so user can't spam click it while toast is showing
- btnCopyCode.disabled = true;
-
- // Show toast
- if (copyToast) {
- copyToast.classList.add("show");
- }
-
- // Auto-reset after 2.5 s
- // Clear any previous timeout first so timers don't stack up
- clearTimeout(toastTimeout);
- toastTimeout = setTimeout(function () {
- if (copyIcon) copyIcon.style.display = "inline";
- if (checkIcon) checkIcon.style.display = "none";
- if (btnLabel) btnLabel.textContent = "Copy Code";
- btnCopyCode.classList.remove("copied");
- btnCopyCode.disabled = false;
- if (copyToast) copyToast.classList.remove("show");
- }, 2500);
- }
-
- if (btnCopyCode) {
- btnCopyCode.addEventListener("click", function () {
- var code = codeContentEl
- ? Array.prototype.slice.call(codeContentEl.querySelectorAll(".line-content"))
- .map(function (el) { return el.textContent; })
- .join("\n")
- : "";
- // Don't copy if the code hasn't loaded yet — just ignore the click
- if (!code || code === "Loading..." || code === "Loading starter code...") return;
-
- // Use Clipboard API with textarea fallback
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(code).then(showCopySuccess).catch(function () {
- fallbackCopy(code); // clipboard api failed, try the old way
- });
- } else {
- fallbackCopy(code); // Clipboard API not supported, use fallback method
- }
- });
- }
-
- // Fallback method to copy text using a hidden textarea and execCommand (for older browsers)
- function fallbackCopy(text) {
- // Some older browsers don't support navigator.clipboard, so we use a hidden textarea instead
- var ta = document.createElement("textarea");
- ta.value = text;
- // Push it off-screen so it's not visible but can still be selected
- ta.style.cssText = "position:fixed;top:-9999px;left:-9999px;opacity:0";
- document.body.appendChild(ta);
- ta.focus();
- ta.select();
- // execCommand is old and deprecated but works as a last resort — fail silently if it doesn't
- try { document.execCommand("copy"); showCopySuccess(); } catch (e) { /* silent fail */ }
- document.body.removeChild(ta);
- }
-} // end isDetailPage
-
-if (
- openModalBtn &&
- closeModalBtn &&
- modal &&
- githubInput &&
- fetchBtn &&
- errorMsg
-) {
-// 1. Open Github Input Modal
- openModalBtn.addEventListener('click', (e) => {
- e.preventDefault();
- modal.classList.add('active');
- githubInput.focus();
- });
-
- // 2. Close Github Input Modal
- const closeGithubModal = () => {
- modal.classList.remove('active');
- githubInput.value = '';
- errorMsg.textContent = '';
- };
-
- closeModalBtn.addEventListener('click', closeGithubModal);
-
- // Close on clicking outside the card
- modal.addEventListener('click', (e) => {
- if (e.target === modal) closeGithubModal();
- });
-
- // 3. Fetch Skills Logic
- fetchBtn.addEventListener('click', async () => {
- const username = githubInput.value.trim();
- if (!username) return;
-
- fetchBtn.disabled = true;
- fetchBtn.textContent = 'Syncing...';
-
- try {
- const response = await fetch(`https://api.github.com/users/${username}/repos`);
- if (!response.ok) throw new Error();
-
- const repos = await response.json();
- const langs = [...new Set(repos.map(r => r.language).filter(Boolean))];
-
- if (langs.length > 0) {
- langs.forEach(lang => {
- if (typeof addSkill === 'function') addSkill(lang);
- });
- closeGithubModal();
- } else {
- errorMsg.textContent = "No public languages found.";
- }
- } catch (err) {
- errorMsg.textContent = err.message ?? "Failed to fetch skills";
- } finally {
- fetchBtn.disabled = false;
- fetchBtn.textContent = 'Fetch Skills';
- }
- });
-}
-
-/* ---- Scroll-to-top button ---- */
-
-/* Show the button only when the user has scrolled more than 300px */
-var SCROLL_THRESHOLD = 300;
-
-/* Get the button element; guard against pages that do not have it */
-var scrollTopBtn = document.getElementById('scroll-top-btn');
-
-/* Add or remove the .visible class based on scroll position */
-function handleScroll() {
- if (!scrollTopBtn) return;
- if (window.pageYOffset > SCROLL_THRESHOLD) {
- scrollTopBtn.classList.add('visible');
- } else {
- scrollTopBtn.classList.remove('visible');
- }
-}
-
-/* Smooth-scroll to the very top of the page */
-function scrollToTop() {
- window.scrollTo({ top: 0, behavior: 'smooth' });
-}
-
-/* Only wire up listeners if the button exists on this page */
-if (scrollTopBtn) {
- window.addEventListener('scroll', handleScroll);
- scrollTopBtn.addEventListener('click', scrollToTop);
-}
diff --git a/app.py b/app.py
index f5e5a749..fb609ae5 100644
--- a/app.py
+++ b/app.py
@@ -28,6 +28,10 @@ def add_security_headers(response):
response.headers["Permissions-Policy"] = (
"geolocation=(), microphone=(), camera=()"
)
+ # Prevent browser caching during development
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
+ response.headers["Pragma"] = "no-cache"
+ response.headers["Expires"] = "0"
return response
# ---- Error handlers ----
diff --git a/data/projects.json b/data/projects.json
index f33cbe07..47efa5eb 100644
--- a/data/projects.json
+++ b/data/projects.json
@@ -1,806 +1,858 @@
[
- {
- "id": 1,
- "title": "Personal Expense Tracker",
- "skills": [
- "Python"
- ],
- "level": "Beginner",
- "interest": "Data",
- "time": "Low",
- "description": "A command-line tool that helps users track daily expenses, categorize spending, and generate simple summary reports. Great for learning file handling, loops, and basic data processing.",
- "features": [
- "Add and delete expense entries",
- "Categorize expenses (food, transport, bills)",
- "View monthly summary",
- "Export data to CSV file"
- ],
- "tech_stack": [
- "Python",
- "CSV module",
- "datetime module"
- ],
- "roadmap": [
- "Step 1: Set up the project folder and create main.py",
- "Step 2: Design the expense data structure as a dictionary",
- "Step 3: Write functions to add and delete expenses",
- "Step 4: Implement category filtering logic",
- "Step 5: Write the summary report generator",
- "Step 6: Add CSV export functionality",
- "Step 7: Test with sample data and fix bugs"
- ],
- "resources": [
- "Python official docs: https://docs.python.org",
- "CSV module guide: https://docs.python.org/3/library/csv.html",
- "Real Python beginner tutorials: https://realpython.com"
- ],
- "starter_code": "starter_code/expense_tracker.py"
- },
- {
- "id": 2,
- "title": "Weather Dashboard",
- "skills": [
- "JavaScript",
- "HTML",
- "CSS"
- ],
- "level": "Beginner",
- "interest": "Web",
- "time": "Low",
- "description": "A simple web page that fetches weather data from a free API and displays current conditions for any city. Teaches API calls, DOM manipulation, and basic UI design.",
- "features": [
- "Search weather by city name",
- "Display temperature, humidity, and conditions",
- "Show a weather icon based on conditions",
- "Toggle between Celsius and Fahrenheit"
- ],
- "tech_stack": [
- "HTML",
- "CSS",
- "JavaScript",
- "OpenWeatherMap API"
- ],
- "roadmap": [
- "Step 1: Create the HTML structure with a search form",
- "Step 2: Style the page with CSS flexbox",
- "Step 3: Sign up for a free OpenWeatherMap API key",
- "Step 4: Write the fetch() call to get weather data",
- "Step 5: Parse the JSON response and extract key fields",
- "Step 6: Display the data dynamically using DOM methods",
- "Step 7: Add the Celsius/Fahrenheit toggle button"
- ],
- "resources": [
- "MDN Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API",
- "OpenWeatherMap free tier: https://openweathermap.org/api",
- "CSS Flexbox guide: https://css-tricks.com/snippets/css/a-guide-to-flexbox"
- ],
- "starter_code": "starter_code/weather_dashboard.html"
- },
- {
- "id": 3,
- "title": "Student Grade Manager",
- "skills": [
- "Python"
- ],
- "level": "Beginner",
- "interest": "Education",
- "time": "Medium",
- "description": "A Python application to store student names and their grades, compute averages, and display a class report. Ideal for practicing data structures, functions, and file persistence.",
- "features": [
- "Add students and assign grades per subject",
- "Calculate individual and class averages",
- "Assign letter grades automatically",
- "Save and load data from a JSON file"
- ],
- "tech_stack": [
- "Python",
- "json module",
- "os module"
- ],
- "roadmap": [
- "Step 1: Define the student data structure using a dictionary",
- "Step 2: Write add_student() and add_grade() functions",
- "Step 3: Implement average calculation logic",
- "Step 4: Create a letter grade converter function",
- "Step 5: Build the JSON save/load functions",
- "Step 6: Create a simple text menu for user interaction",
- "Step 7: Write a class report printer function"
- ],
- "resources": [
- "Python JSON module: https://docs.python.org/3/library/json.html",
- "Python functions tutorial: https://realpython.com/defining-your-own-python-function",
- "W3Schools Python: https://www.w3schools.com/python"
- ],
- "starter_code": "starter_code/grade_manager.py"
- },
- {
- "id": 4,
- "title": "Task Manager REST API",
- "skills": [
- "Python"
- ],
- "level": "Intermediate",
- "interest": "Web",
- "time": "Medium",
- "description": "A RESTful API built with Flask that allows clients to create, read, update, and delete tasks. Perfect for learning API design, HTTP methods, and JSON responses.",
- "features": [
- "CRUD endpoints for tasks",
- "Filter tasks by status (pending, done)",
- "Assign priority levels to tasks",
- "Persist data to a JSON file"
- ],
- "tech_stack": [
- "Python",
- "Flask",
- "JSON",
- "Postman (for testing)"
- ],
- "roadmap": [
- "Step 1: Install Flask and create the app.py file",
- "Step 2: Define the task data model as a dictionary",
- "Step 3: Create the GET /tasks endpoint to list all tasks",
- "Step 4: Create the POST /tasks endpoint to add a task",
- "Step 5: Create PUT /tasks/ to update a task",
- "Step 6: Create DELETE /tasks/ to remove a task",
- "Step 7: Add JSON file persistence for saving tasks",
- "Step 8: Test all endpoints using Postman or curl"
- ],
- "resources": [
- "Flask quickstart: https://flask.palletsprojects.com/quickstart",
- "REST API design guide: https://restfulapi.net",
- "Postman download: https://www.postman.com/downloads"
- ],
- "starter_code": "starter_code/task_api.py"
- },
- {
- "id": 5,
- "title": "Portfolio Website",
- "skills": [
- "HTML",
- "CSS",
- "JavaScript"
- ],
- "level": "Beginner",
- "interest": "Web",
- "time": "Low",
- "description": "A personal portfolio site with sections for bio, projects, and contact. A great first project that teaches HTML layout, CSS styling, and a bit of JavaScript for interactivity.",
- "features": [
- "Hero section with name and tagline",
- "Projects grid with cards",
- "Skills list with visual indicators",
- "Contact form with basic validation"
- ],
- "tech_stack": [
- "HTML",
- "CSS",
- "JavaScript"
- ],
- "roadmap": [
- "Step 1: Plan the page sections on paper first",
- "Step 2: Write the HTML structure for all sections",
- "Step 3: Add CSS reset and base typography styles",
- "Step 4: Style the navigation and hero section",
- "Step 5: Build the projects grid using CSS Grid",
- "Step 6: Add the contact form with labels and inputs",
- "Step 7: Write JavaScript for form validation",
- "Step 8: Make the site responsive with media queries"
- ],
- "resources": [
- "HTML reference: https://developer.mozilla.org/en-US/docs/Web/HTML",
- "CSS Grid guide: https://css-tricks.com/snippets/css/complete-guide-grid",
- "Responsive design basics: https://web.dev/learn/design"
- ],
- "starter_code": "starter_code/portfolio.html"
- },
- {
- "id": 6,
- "title": "URL Shortener",
- "skills": [
- "Python",
- "JavaScript",
- "HTML",
- "CSS"
- ],
- "level": "Intermediate",
- "interest": "Web",
- "time": "High",
- "description": "A full-stack web app that takes long URLs and generates short codes. Users can paste a link and get a shorter one back. Teaches Flask routing, random code generation, and front-end form handling.",
- "features": [
- "Shorten any valid URL",
- "Redirect short codes to original URL",
- "Track how many times a link was clicked",
- "List all shortened links in a dashboard"
- ],
- "tech_stack": [
- "Python",
- "Flask",
- "HTML",
- "CSS",
- "JavaScript",
- "JSON"
- ],
- "roadmap": [
- "Step 1: Set up Flask app with two routes: home and redirect",
- "Step 2: Write a random 6-character code generator",
- "Step 3: Store URL mappings in a JSON file",
- "Step 4: Build the HTML form for pasting long URLs",
- "Step 5: Display the shortened URL after submission",
- "Step 6: Implement the redirect route using the short code",
- "Step 7: Add a click counter that updates on each visit",
- "Step 8: Build a simple dashboard to list all links"
- ],
- "resources": [
- "Flask routing docs: https://flask.palletsprojects.com/en/stable/quickstart/#routing",
- "Python secrets module: https://docs.python.org/3/library/secrets.html",
- "UUID in Python: https://docs.python.org/3/library/uuid.html"
- ],
- "starter_code": "starter_code/url_shortener.py"
- },
- {
- "id": 7,
- "title": "Data Analysis Report Generator",
- "skills": [
- "Python"
- ],
- "level": "Intermediate",
- "interest": "Data",
- "time": "High",
- "description": "Upload a CSV file and automatically generate a summary report with statistics, missing value counts, and basic charts. A practical project for learning data wrangling and pandas.",
- "features": [
- "Load and inspect CSV files",
- "Show column types and null counts",
- "Calculate mean, median, and mode per column",
- "Generate bar charts for categorical data"
- ],
- "tech_stack": [
- "Python",
- "pandas",
- "matplotlib",
- "os module"
- ],
- "roadmap": [
- "Step 1: Install pandas and matplotlib via pip",
- "Step 2: Write a CSV loader that validates the file path",
- "Step 3: Generate a summary table of column info",
- "Step 4: Compute descriptive statistics for numeric columns",
- "Step 5: Count and display missing values per column",
- "Step 6: Build chart generation functions using matplotlib",
- "Step 7: Export the full report to a text or HTML file"
- ],
- "resources": [
- "pandas docs: https://pandas.pydata.org/docs",
- "matplotlib tutorials: https://matplotlib.org/stable/tutorials",
- "Real Python data analysis: https://realpython.com/pandas-dataframe"
- ],
- "starter_code": "starter_code/data_report.py"
- },
- {
- "id": 8,
- "title": "Library Management System",
- "skills": [
- "Java"
- ],
- "level": "Beginner",
- "interest": "Backend",
- "time": "Medium",
- "description": "A Java application that helps manage books, students, and borrowing records in a library. This project teaches object-oriented programming concepts, file handling, and menu-driven application design.",
- "features": [
- "Add and remove books",
- "Issue and return books",
- "Store student records",
- "Search books by title or author"
- ],
- "tech_stack": [
- "Java",
- "OOP",
- "File Handling"
- ],
- "roadmap": [
- "Step 1: Create Book and Student classes",
- "Step 2: Design the menu-driven interface",
- "Step 3: Implement add and remove book features",
- "Step 4: Add issue and return book functionality",
- "Step 5: Store records using file handling",
- "Step 6: Implement search functionality",
- "Step 7: Test the system with sample records"
- ],
- "resources": [
- "Java official docs: https://docs.oracle.com/javase/tutorial",
- "OOP concepts in Java: https://www.geeksforgeeks.org/object-oriented-programming-oops-concept-in-java",
- "Java file handling: https://www.w3schools.com/java/java_files.asp"
- ],
- "starter_code": "starter_code/library_management.java"
- },
- {
- "id": 9,
- "title": "Real-Time Chat Application",
- "skills": [
- "JavaScript",
- "Node.js"
- ],
- "level": "Intermediate",
- "interest": "Web",
- "time": "High",
- "description": "A real-time chat application that allows multiple users to send and receive instant messages using WebSockets. This project introduces backend communication, event handling, and real-time systems.",
- "features": [
- "Multiple user chat support",
- "Real-time messaging",
- "User join and leave notifications",
- "Simple responsive chat interface"
- ],
- "tech_stack": [
- "Node.js",
- "Express.js",
- "Socket.IO",
- "HTML",
- "CSS"
- ],
- "roadmap": [
- "Step 1: Initialize the Node.js project",
- "Step 2: Install Express and Socket.IO",
- "Step 3: Create the server using Express",
- "Step 4: Build the frontend chat interface",
- "Step 5: Implement real-time messaging with Socket.IO",
- "Step 6: Add user connection notifications",
- "Step 7: Test the application with multiple users"
- ],
- "resources": [
- "Node.js docs: https://nodejs.org/en/docs",
- "Socket.IO guide: https://socket.io/docs/v4",
- "Express.js documentation: https://expressjs.com"
- ],
- "starter_code": "starter_code/realtime_chat_app.js"
- },
- {
- "id": 10,
- "title": "Password Strength Checker",
- "skills": [
- "Python"
- ],
- "level": "Beginner",
- "interest": "Cybersecurity",
- "time": "Low",
- "description": "A tool that checks password strength based on length, symbols, uppercase letters, and numbers. Helps beginners understand input validation and security basics.",
- "features": [
- "Check password complexity",
- "Display strength rating",
- "Suggest stronger password improvements",
- "Prevent weak password patterns"
- ],
- "tech_stack": [
- "Python",
- "Regex"
- ],
- "roadmap": [
- "Step 1: Create the password input system",
- "Step 2: Check password length",
- "Step 3: Detect uppercase and lowercase letters",
- "Step 4: Detect numbers and symbols",
- "Step 5: Create a scoring system",
- "Step 6: Display password strength feedback"
- ],
- "resources": [
- "Python regex docs: https://docs.python.org/3/library/re.html",
- "OWASP password guidelines: https://owasp.org"
- ],
- "starter_code": "starter_code/password_checker.py"
- },
- {
- "id": 11,
- "title": "Feedback Survey Form",
- "skills": [
- "HTML"
- ],
- "level": "Beginner",
- "interest": "Web",
- "time": "Low",
- "description": "A simple student feedback form that collects user names, emails, and ratings. Teaches basic HTML form handling and layout design.",
- "features": [
- "Collect user name, email, and age with validation",
- "Dropdown menu for experience selection",
- "Text area for detailed user suggestions"
- ],
- "tech_stack": [
- "HTML"
- ],
- "roadmap": [
- "Step 1: Create the HTML folder structure inside starter_code",
- "Step 2: Build the input text fields and labels",
- "Step 3: Add select options and textarea elements",
- "Step 4: Align the form to the center for better layout",
- "Step 5: Test the form using Live Server"
- ],
- "resources": [
- "MDN HTML Forms: https://developer.mozilla.org/en-US/docs/Learn/Forms"
- ],
- "starter_code": "starter_code/survey_form/index.html"
- },
- {
- "id": 10,
- "title": "API ETL Pipeline",
- "skills": ["Python", "pandas", "requests"],
- "level": "Intermediate",
- "interest": "Data",
- "time": "Medium",
- "description": "Enter a public API URL to fetch data and automatically transform it into a structured CSV dataset.",
- "features": [
- "Fetch data from public APIs",
- "Handle missing values",
- "Normalize nested JSON",
- "Generate summary statistics",
- "Export the processed CSV for any other analytics projects"
- ],
- "tech_stack": ["Python", "pandas", "requests", "JSON"],
- "roadmap": [
- "Step 1: Install required modules via pip",
- "Step 2: Find a public API key for this project",
- "Step 3: Fetch the data from the API using requests",
- "Step 4: Validate the response you just fetched from the API",
- "Step 5: Normalize the nested JSON data by flattening it",
- "Step 6: Use the fetched data to build a pandas dataframe",
- "Step 7: Handle missing values or duplicate values",
- "Step 8: Export the cleaned dataset to CSV format",
- "Step 9: Generate a summary for the newly created CSV dataset",
- "Step 10: Test the file with at least two different public APIs"
- ],
- "resources": [
- "pandas docs: https://pandas.pydata.org/docs",
- "requests docs: https://requests.readthedocs.io/en/latest/",
- "JSON handling in Python: https://docs.python.org/3/library/json.html",
- "REST API tutorial: https://restfulapi.net/",
- "Real Python API guide: https://realpython.com/api-integration-in-python/"
- ],
- "starter_code": "starter_code/api_data_pipeline.py"
- },
- {
- "id": 13,
- "title": "AI Resume Analyzer",
- "skills": [
- "Python",
- "Flask",
- "HTML",
- "CSS",
- "JavaScript"
- ],
- "level": "Intermediate",
- "interest": "Data",
- "time": "High",
- "description": "A Flask web app that compares a resume against a job description using TF-IDF similarity and keyword extraction. Users upload a PDF or paste text, and the app returns a match score, a list of missing keywords, and actionable feedback — with no external AI API required.",
- "features": [
- "Upload a resume as PDF or paste plain text",
- "Paste any job description for comparison",
- "TF-IDF cosine similarity match score (0–100%)",
- "Missing skills and keyword gap analysis",
- "Actionable written feedback based on score",
- "Single-page interface with interactive feedback display"
- ],
- "tech_stack": [
- "Python",
- "Flask",
- "PyPDF2",
- "scikit-learn",
- "HTML",
- "CSS",
- "JavaScript"
- ],
- "roadmap": [
- "Step 1: Run the server and verify the upload form renders",
- "Step 2: Complete extract_text_from_pdf() using PyPDF2",
- "Step 3: Complete clean_text() to normalise punctuation and whitespace",
- "Step 4: Complete extract_keywords() to remove stopwords and count frequency",
- "Step 5: Complete calculate_similarity() with TF-IDF and cosine distance",
- "Step 6: Complete find_missing_skills() by comparing two keyword sets",
- "Step 7: Complete generate_feedback() to produce written suggestions",
- "Step 8: Wire everything together inside the /analyze Flask route",
- "Step 9: Test with a real resume PDF and a real job posting"
- ],
- "resources": [
- "PyPDF2 documentation: https://pypdf2.readthedocs.io/",
- "scikit-learn TF-IDF guide: https://scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weighting",
- "Cosine similarity explained: https://www.machinelearningplus.com/nlp/cosine-similarity",
- "Flask quickstart: https://flask.palletsprojects.com/quickstart"
- ],
- "starter_code": "starter_code/ai_resume_analyzer.py"
- },
- {
- "id": 11,
- "title": "Number Guessing Game",
- "skills": [
- "Python"
- ],
- "level": "Beginner",
- "interest": "Games",
- "time": "Low",
- "description": "A fun command-line game where the computer picks a random number and the user tries to guess it. Great for learning loops, conditionals, and user input handling.",
- "features": [
- "Generate random number between 1 and 100",
- "Give hints: too high or too low",
- "Count number of attempts",
- "Show final score at the end"
- ],
- "tech_stack": [
- "Python",
- "random module"
- ],
- "roadmap": [
- "Step 1: Set up the Python file and import random module",
- "Step 2: Generate a random number using random.randint()",
- "Step 3: Write a loop to take user input",
- "Step 4: Compare guess with the number",
- "Step 5: Give hints if guess is too high or too low",
- "Step 6: Count the number of attempts",
- "Step 7: Display win message with attempt count"
- ],
- "resources": [
- "Python random module: https://docs.python.org/3/library/random.html",
- "W3Schools Python: https://www.w3schools.com/python",
- "Real Python: https://realpython.com"
- ],
- "starter_code": "starter_code/number_guessing.py"
- },
- {
- "id": 12,
- "title": "Simple Email Automation",
- "skills": [
- "Python"
- ],
- "level": "Beginner",
- "interest": "Automation",
- "time": "Low",
- "description": "A Python script that sends automated emails using the smtplib library. Learn how to automate repetitive tasks and work with Python standard libraries.",
- "features": [
- "Compose and send emails via Python",
- "Send to multiple recipients",
- "Add subject and body text",
- "Read recipient list from a text file"
- ],
- "tech_stack": [
- "Python",
- "smtplib",
- "email module"
- ],
- "roadmap": [
- "Step 1: Set up Python file and import smtplib",
- "Step 2: Configure sender email and password",
- "Step 3: Write the email composition function",
- "Step 4: Connect to Gmail SMTP server",
- "Step 5: Send email to one recipient and test",
- "Step 6: Read recipient list from a text file",
- "Step 7: Loop through recipients and send to all"
- ],
- "resources": [
- "Python smtplib docs: https://docs.python.org/3/library/smtplib.html",
- "Real Python email guide: https://realpython.com/python-send-email",
- "Gmail SMTP settings: https://support.google.com/mail"
- ],
- "starter_code": "starter_code/email_automation.py"
- },
- {
- "id": 13,
- "title": "Quiz App",
- "skills": [
- "HTML",
- "CSS",
- "JavaScript"
- ],
- "level": "Beginner",
- "interest": "Games",
- "time": "Low",
- "description": "A browser-based quiz app with multiple choice questions, a score counter, and a results screen. Perfect for practising DOM manipulation and event handling in JavaScript.",
- "features": [
- "Display one question at a time",
- "Four multiple choice options per question",
- "Show correct or incorrect feedback instantly",
- "Display final score on results screen"
- ],
- "tech_stack": [
- "HTML",
- "CSS",
- "JavaScript"
- ],
- "roadmap": [
- "Step 1: Create HTML structure for question and options",
- "Step 2: Style the quiz card with CSS",
- "Step 3: Store questions as a JavaScript array of objects",
- "Step 4: Write a function to display each question",
- "Step 5: Add click event listeners to option buttons",
- "Step 6: Check the selected answer and update score",
- "Step 7: Move to the next question automatically",
- "Step 8: Show the results screen with final score"
- ],
- "resources": [
- "MDN DOM guide: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model",
- "JavaScript events: https://javascript.info/events",
- "W3Schools JavaScript: https://www.w3schools.com/js"
- ],
- "starter_code": "starter_code/quiz_app.html"
- },
- {
- "id": 14,
- "title": "File Organiser Script",
- "skills": ["Python"],
- "level": "Beginner",
- "interest": "Automation",
- "time": "Low",
- "description": "A Python script that scans a folder and automatically sorts files into subfolders by type — images, documents, videos, code files. Great for learning os and shutil modules.",
- "features": [
- "Detect file type by extension",
- "Create subfolders automatically",
- "Move files into the correct folder",
- "Print a summary of what was moved"
- ],
- "tech_stack": ["Python", "os module", "shutil module"],
- "roadmap": [
- "Step 1: Import os and shutil",
- "Step 2: Define a dictionary mapping extensions to folder names",
- "Step 3: Loop through files in the target directory",
- "Step 4: Check each file's extension",
- "Step 5: Create the destination folder if it doesn't exist",
- "Step 6: Move the file using shutil.move()",
- "Step 7: Print a summary of moved files"
- ],
- "resources": [
- "Python os module: https://docs.python.org/3/library/os.html",
- "Python shutil module: https://docs.python.org/3/library/shutil.html",
- "Real Python file handling: https://realpython.com/working-with-files-in-python"
- ],
- "starter_code": "starter_code/file_organiser.py"
- },
- {
- "id": 15,
- "title": "Flashcard Study App",
- "skills": ["HTML", "CSS", "JavaScript"],
- "level": "Beginner",
- "interest": "Education",
- "time": "Low",
- "description": "A browser-based flashcard app where users can flip cards to reveal answers. Reinforces DOM manipulation, CSS transitions, and basic data storage in JavaScript.",
- "features": [
- "Flip card animation on click",
- "Navigate between cards",
- "Track how many cards reviewed",
- "Shuffle deck order"
- ],
- "tech_stack": ["HTML", "CSS", "JavaScript"],
- "roadmap": [
- "Step 1: Create the card HTML structure with front and back faces",
- "Step 2: Write CSS for the 3D flip animation",
- "Step 3: Store flashcard data as a JavaScript array",
- "Step 4: Render the current card from the array",
- "Step 5: Add click handler to trigger the flip",
- "Step 6: Add next/previous navigation buttons",
- "Step 7: Implement the shuffle function"
- ],
- "resources": [
- "CSS 3D transforms: https://developer.mozilla.org/en-US/docs/Web/CSS/transform",
- "JavaScript arrays: https://javascript.info/array",
- "W3Schools CSS: https://www.w3schools.com/css"
- ],
- "starter_code": "starter_code/flashcard_app.html"
- },
- {
- "id": 16,
- "title": "Budget Tracker Web App",
- "skills": ["HTML", "CSS", "JavaScript"],
- "level": "Intermediate",
- "interest": "Data",
- "time": "Medium",
- "description": "A browser-based personal finance tracker that lets users add income and expense entries and visualises the balance over time with a simple chart.",
- "features": [
- "Add income and expense entries",
- "Show running balance",
- "Colour-code entries by type",
- "Render a bar chart of monthly totals"
- ],
- "tech_stack": ["HTML", "CSS", "JavaScript", "Chart.js"],
- "roadmap": [
- "Step 1: Build the HTML form for adding entries",
- "Step 2: Store entries in a JavaScript array",
- "Step 3: Render the entry list dynamically",
- "Step 4: Calculate and display the running balance",
- "Step 5: Group entries by month for chart data",
- "Step 6: Import Chart.js via CDN",
- "Step 7: Render a bar chart using the monthly totals",
- "Step 8: Add delete functionality for individual entries"
- ],
- "resources": [
- "Chart.js docs: https://www.chartjs.org/docs/latest",
- "MDN DOM: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model",
- "JavaScript arrays: https://javascript.info/array"
- ],
- "starter_code": "starter_code/budget_tracker.html"
- },
- {
- "id": 17,
- "title": "Network Port Scanner",
- "skills": ["Python"],
- "level": "Intermediate",
- "interest": "Cybersecurity",
- "time": "Medium",
- "description": "A Python tool that scans a target host for open ports within a given range. Teaches socket programming, threading for speed, and basic network concepts.",
- "features": [
- "Accept host and port range as input",
- "Check each port using sockets",
- "Display open ports with service names",
- "Use threading to speed up scanning"
- ],
- "tech_stack": ["Python", "socket module", "threading module"],
- "roadmap": [
- "Step 1: Import socket and threading modules",
- "Step 2: Write a function to test a single port",
- "Step 3: Loop through the port range and test each",
- "Step 4: Add threading to run scans concurrently",
- "Step 5: Map common ports to service names",
- "Step 6: Display results sorted by port number",
- "Step 7: Add input validation for host and port range"
- ],
- "resources": [
- "Python socket docs: https://docs.python.org/3/library/socket.html",
- "Python threading: https://docs.python.org/3/library/threading.html",
- "OWASP testing guide: https://owasp.org/www-project-web-security-testing-guide"
- ],
- "starter_code": "starter_code/port_scanner.py"
- },
- {
- "id": 18,
- "title": "Typing Speed Test",
- "skills": ["HTML", "CSS", "JavaScript"],
- "level": "Beginner",
- "interest": "Games",
- "time": "Medium",
- "description": "A browser-based typing test that measures words per minute and accuracy. Great for practising timers, string comparison, and dynamic DOM updates.",
- "features": [
- "Display a random passage to type",
- "Start timer on first keypress",
- "Highlight correct and incorrect characters in real time",
- "Show WPM and accuracy on completion"
- ],
- "tech_stack": ["HTML", "CSS", "JavaScript"],
- "roadmap": [
- "Step 1: Store a list of sample passages",
- "Step 2: Display a random passage in the UI",
- "Step 3: Listen for keypress events in the input field",
- "Step 4: Start the timer on the first keypress",
- "Step 5: Compare typed characters to the passage character by character",
- "Step 6: Highlight correct characters green and errors red",
- "Step 7: Stop the timer when the passage is complete",
- "Step 8: Calculate and display WPM and accuracy"
- ],
- "resources": [
- "JavaScript timers: https://developer.mozilla.org/en-US/docs/Web/API/setInterval",
- "JavaScript string methods: https://javascript.info/string",
- "MDN keyboard events: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent"
- ],
- "starter_code": "starter_code/typing_test.html"
- },
- {
- "id": 19,
- "title": "Course Progress Tracker",
- "skills": ["Python"],
- "level": "Intermediate",
- "interest": "Education",
- "time": "Medium",
- "description": "A CLI tool to track progress through online courses. Users can add courses, mark lessons complete, and see a visual progress bar per course.",
- "features": [
- "Add courses with a total lesson count",
- "Mark individual lessons as complete",
- "Display a text progress bar per course",
- "Save and load state from a JSON file"
- ],
- "tech_stack": ["Python", "json module", "os module"],
- "roadmap": [
- "Step 1: Define the course data structure",
- "Step 2: Write add_course() and add_lesson() functions",
- "Step 3: Implement mark_complete() logic",
- "Step 4: Build a text progress bar renderer",
- "Step 5: Write JSON save and load functions",
- "Step 6: Create a menu loop for user interaction",
- "Step 7: Display all courses with progress on startup"
- ],
- "resources": [
- "Python JSON module: https://docs.python.org/3/library/json.html",
- "Real Python CLI apps: https://realpython.com/command-line-interfaces-python-argparse",
- "Python os module: https://docs.python.org/3/library/os.html"
- ],
- "starter_code": "starter_code/course_tracker.py"
- }
-]
+ {
+ "id": 1,
+ "title": "Personal Expense Tracker",
+ "skills": [
+ "Python"
+ ],
+ "level": "Beginner",
+ "interest": "Data",
+ "time": "Low",
+ "description": "A command-line tool that helps users track daily expenses, categorize spending, and generate simple summary reports. Great for learning file handling, loops, and basic data processing.",
+ "features": [
+ "Add and delete expense entries",
+ "Categorize expenses (food, transport, bills)",
+ "View monthly summary",
+ "Export data to CSV file"
+ ],
+ "tech_stack": [
+ "Python",
+ "CSV module",
+ "datetime module"
+ ],
+ "roadmap": [
+ "Step 1: Set up the project folder and create main.py",
+ "Step 2: Design the expense data structure as a dictionary",
+ "Step 3: Write functions to add and delete expenses",
+ "Step 4: Implement category filtering logic",
+ "Step 5: Write the summary report generator",
+ "Step 6: Add CSV export functionality",
+ "Step 7: Test with sample data and fix bugs"
+ ],
+ "resources": [
+ "Python official docs: https://docs.python.org",
+ "CSV module guide: https://docs.python.org/3/library/csv.html",
+ "Real Python beginner tutorials: https://realpython.com"
+ ],
+ "starter_code": "starter_code/expense_tracker.py"
+ },
+ {
+ "id": 2,
+ "title": "Weather Dashboard",
+ "skills": [
+ "JavaScript",
+ "HTML",
+ "CSS"
+ ],
+ "level": "Beginner",
+ "interest": "Web",
+ "time": "Low",
+ "description": "A simple web page that fetches weather data from a free API and displays current conditions for any city. Teaches API calls, DOM manipulation, and basic UI design.",
+ "features": [
+ "Search weather by city name",
+ "Display temperature, humidity, and conditions",
+ "Show a weather icon based on conditions",
+ "Toggle between Celsius and Fahrenheit"
+ ],
+ "tech_stack": [
+ "HTML",
+ "CSS",
+ "JavaScript",
+ "OpenWeatherMap API"
+ ],
+ "roadmap": [
+ "Step 1: Create the HTML structure with a search form",
+ "Step 2: Style the page with CSS flexbox",
+ "Step 3: Sign up for a free OpenWeatherMap API key",
+ "Step 4: Write the fetch() call to get weather data",
+ "Step 5: Parse the JSON response and extract key fields",
+ "Step 6: Display the data dynamically using DOM methods",
+ "Step 7: Add the Celsius/Fahrenheit toggle button"
+ ],
+ "resources": [
+ "MDN Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API",
+ "OpenWeatherMap free tier: https://openweathermap.org/api",
+ "CSS Flexbox guide: https://css-tricks.com/snippets/css/a-guide-to-flexbox"
+ ],
+ "starter_code": "starter_code/weather_dashboard.html"
+ },
+ {
+ "id": 3,
+ "title": "Student Grade Manager",
+ "skills": [
+ "Python"
+ ],
+ "level": "Beginner",
+ "interest": "Education",
+ "time": "Medium",
+ "description": "A Python application to store student names and their grades, compute averages, and display a class report. Ideal for practicing data structures, functions, and file persistence.",
+ "features": [
+ "Add students and assign grades per subject",
+ "Calculate individual and class averages",
+ "Assign letter grades automatically",
+ "Save and load data from a JSON file"
+ ],
+ "tech_stack": [
+ "Python",
+ "json module",
+ "os module"
+ ],
+ "roadmap": [
+ "Step 1: Define the student data structure using a dictionary",
+ "Step 2: Write add_student() and add_grade() functions",
+ "Step 3: Implement average calculation logic",
+ "Step 4: Create a letter grade converter function",
+ "Step 5: Build the JSON save/load functions",
+ "Step 6: Create a simple text menu for user interaction",
+ "Step 7: Write a class report printer function"
+ ],
+ "resources": [
+ "Python JSON module: https://docs.python.org/3/library/json.html",
+ "Python functions tutorial: https://realpython.com/defining-your-own-python-function",
+ "W3Schools Python: https://www.w3schools.com/python"
+ ],
+ "starter_code": "starter_code/grade_manager.py"
+ },
+ {
+ "id": 4,
+ "title": "Task Manager REST API",
+ "skills": [
+ "Python"
+ ],
+ "level": "Intermediate",
+ "interest": "Web",
+ "time": "Medium",
+ "description": "A RESTful API built with Flask that allows clients to create, read, update, and delete tasks. Perfect for learning API design, HTTP methods, and JSON responses.",
+ "features": [
+ "CRUD endpoints for tasks",
+ "Filter tasks by status (pending, done)",
+ "Assign priority levels to tasks",
+ "Persist data to a JSON file"
+ ],
+ "tech_stack": [
+ "Python",
+ "Flask",
+ "JSON",
+ "Postman (for testing)"
+ ],
+ "roadmap": [
+ "Step 1: Install Flask and create the app.py file",
+ "Step 2: Define the task data model as a dictionary",
+ "Step 3: Create the GET /tasks endpoint to list all tasks",
+ "Step 4: Create the POST /tasks endpoint to add a task",
+ "Step 5: Create PUT /tasks/ to update a task",
+ "Step 6: Create DELETE /tasks/ to remove a task",
+ "Step 7: Add JSON file persistence for saving tasks",
+ "Step 8: Test all endpoints using Postman or curl"
+ ],
+ "resources": [
+ "Flask quickstart: https://flask.palletsprojects.com/quickstart",
+ "REST API design guide: https://restfulapi.net",
+ "Postman download: https://www.postman.com/downloads"
+ ],
+ "starter_code": "starter_code/task_api.py"
+ },
+ {
+ "id": 5,
+ "title": "Portfolio Website",
+ "skills": [
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "level": "Beginner",
+ "interest": "Web",
+ "time": "Low",
+ "description": "A personal portfolio site with sections for bio, projects, and contact. A great first project that teaches HTML layout, CSS styling, and a bit of JavaScript for interactivity.",
+ "features": [
+ "Hero section with name and tagline",
+ "Projects grid with cards",
+ "Skills list with visual indicators",
+ "Contact form with basic validation"
+ ],
+ "tech_stack": [
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "roadmap": [
+ "Step 1: Plan the page sections on paper first",
+ "Step 2: Write the HTML structure for all sections",
+ "Step 3: Add CSS reset and base typography styles",
+ "Step 4: Style the navigation and hero section",
+ "Step 5: Build the projects grid using CSS Grid",
+ "Step 6: Add the contact form with labels and inputs",
+ "Step 7: Write JavaScript for form validation",
+ "Step 8: Make the site responsive with media queries"
+ ],
+ "resources": [
+ "HTML reference: https://developer.mozilla.org/en-US/docs/Web/HTML",
+ "CSS Grid guide: https://css-tricks.com/snippets/css/complete-guide-grid",
+ "Responsive design basics: https://web.dev/learn/design"
+ ],
+ "starter_code": "starter_code/portfolio.html"
+ },
+ {
+ "id": 6,
+ "title": "URL Shortener",
+ "skills": [
+ "Python",
+ "JavaScript",
+ "HTML",
+ "CSS"
+ ],
+ "level": "Intermediate",
+ "interest": "Web",
+ "time": "High",
+ "description": "A full-stack web app that takes long URLs and generates short codes. Users can paste a link and get a shorter one back. Teaches Flask routing, random code generation, and front-end form handling.",
+ "features": [
+ "Shorten any valid URL",
+ "Redirect short codes to original URL",
+ "Track how many times a link was clicked",
+ "List all shortened links in a dashboard"
+ ],
+ "tech_stack": [
+ "Python",
+ "Flask",
+ "HTML",
+ "CSS",
+ "JavaScript",
+ "JSON"
+ ],
+ "roadmap": [
+ "Step 1: Set up Flask app with two routes: home and redirect",
+ "Step 2: Write a random 6-character code generator",
+ "Step 3: Store URL mappings in a JSON file",
+ "Step 4: Build the HTML form for pasting long URLs",
+ "Step 5: Display the shortened URL after submission",
+ "Step 6: Implement the redirect route using the short code",
+ "Step 7: Add a click counter that updates on each visit",
+ "Step 8: Build a simple dashboard to list all links"
+ ],
+ "resources": [
+ "Flask routing docs: https://flask.palletsprojects.com/en/stable/quickstart/#routing",
+ "Python secrets module: https://docs.python.org/3/library/secrets.html",
+ "UUID in Python: https://docs.python.org/3/library/uuid.html"
+ ],
+ "starter_code": "starter_code/url_shortener.py"
+ },
+ {
+ "id": 7,
+ "title": "Data Analysis Report Generator",
+ "skills": [
+ "Python"
+ ],
+ "level": "Intermediate",
+ "interest": "Data",
+ "time": "High",
+ "description": "Upload a CSV file and automatically generate a summary report with statistics, missing value counts, and basic charts. A practical project for learning data wrangling and pandas.",
+ "features": [
+ "Load and inspect CSV files",
+ "Show column types and null counts",
+ "Calculate mean, median, and mode per column",
+ "Generate bar charts for categorical data"
+ ],
+ "tech_stack": [
+ "Python",
+ "pandas",
+ "matplotlib",
+ "os module"
+ ],
+ "roadmap": [
+ "Step 1: Install pandas and matplotlib via pip",
+ "Step 2: Write a CSV loader that validates the file path",
+ "Step 3: Generate a summary table of column info",
+ "Step 4: Compute descriptive statistics for numeric columns",
+ "Step 5: Count and display missing values per column",
+ "Step 6: Build chart generation functions using matplotlib",
+ "Step 7: Export the full report to a text or HTML file"
+ ],
+ "resources": [
+ "pandas docs: https://pandas.pydata.org/docs",
+ "matplotlib tutorials: https://matplotlib.org/stable/tutorials",
+ "Real Python data analysis: https://realpython.com/pandas-dataframe"
+ ],
+ "starter_code": "starter_code/data_report.py"
+ },
+ {
+ "id": 8,
+ "title": "Library Management System",
+ "skills": [
+ "Java"
+ ],
+ "level": "Beginner",
+ "interest": "Backend",
+ "time": "Medium",
+ "description": "A Java application that helps manage books, students, and borrowing records in a library. This project teaches object-oriented programming concepts, file handling, and menu-driven application design.",
+ "features": [
+ "Add and remove books",
+ "Issue and return books",
+ "Store student records",
+ "Search books by title or author"
+ ],
+ "tech_stack": [
+ "Java",
+ "OOP",
+ "File Handling"
+ ],
+ "roadmap": [
+ "Step 1: Create Book and Student classes",
+ "Step 2: Design the menu-driven interface",
+ "Step 3: Implement add and remove book features",
+ "Step 4: Add issue and return book functionality",
+ "Step 5: Store records using file handling",
+ "Step 6: Implement search functionality",
+ "Step 7: Test the system with sample records"
+ ],
+ "resources": [
+ "Java official docs: https://docs.oracle.com/javase/tutorial",
+ "OOP concepts in Java: https://www.geeksforgeeks.org/object-oriented-programming-oops-concept-in-java",
+ "Java file handling: https://www.w3schools.com/java/java_files.asp"
+ ],
+ "starter_code": "starter_code/library_management.java"
+ },
+ {
+ "id": 9,
+ "title": "Real-Time Chat Application",
+ "skills": [
+ "JavaScript",
+ "Node.js"
+ ],
+ "level": "Intermediate",
+ "interest": "Web",
+ "time": "High",
+ "description": "A real-time chat application that allows multiple users to send and receive instant messages using WebSockets. This project introduces backend communication, event handling, and real-time systems.",
+ "features": [
+ "Multiple user chat support",
+ "Real-time messaging",
+ "User join and leave notifications",
+ "Simple responsive chat interface"
+ ],
+ "tech_stack": [
+ "Node.js",
+ "Express.js",
+ "Socket.IO",
+ "HTML",
+ "CSS"
+ ],
+ "roadmap": [
+ "Step 1: Initialize the Node.js project",
+ "Step 2: Install Express and Socket.IO",
+ "Step 3: Create the server using Express",
+ "Step 4: Build the frontend chat interface",
+ "Step 5: Implement real-time messaging with Socket.IO",
+ "Step 6: Add user connection notifications",
+ "Step 7: Test the application with multiple users"
+ ],
+ "resources": [
+ "Node.js docs: https://nodejs.org/en/docs",
+ "Socket.IO guide: https://socket.io/docs/v4",
+ "Express.js documentation: https://expressjs.com"
+ ],
+ "starter_code": "starter_code/realtime_chat_app.js"
+ },
+ {
+ "id": 10,
+ "title": "Password Strength Checker",
+ "skills": [
+ "Python"
+ ],
+ "level": "Beginner",
+ "interest": "Cybersecurity",
+ "time": "Low",
+ "description": "A tool that checks password strength based on length, symbols, uppercase letters, and numbers. Helps beginners understand input validation and security basics.",
+ "features": [
+ "Check password complexity",
+ "Display strength rating",
+ "Suggest stronger password improvements",
+ "Prevent weak password patterns"
+ ],
+ "tech_stack": [
+ "Python",
+ "Regex"
+ ],
+ "roadmap": [
+ "Step 1: Create the password input system",
+ "Step 2: Check password length",
+ "Step 3: Detect uppercase and lowercase letters",
+ "Step 4: Detect numbers and symbols",
+ "Step 5: Create a scoring system",
+ "Step 6: Display password strength feedback"
+ ],
+ "resources": [
+ "Python regex docs: https://docs.python.org/3/library/re.html",
+ "OWASP password guidelines: https://owasp.org"
+ ],
+ "starter_code": "starter_code/password_checker.py"
+ },
+ {
+ "id": 11,
+ "title": "Feedback Survey Form",
+ "skills": [
+ "HTML"
+ ],
+ "level": "Beginner",
+ "interest": "Web",
+ "time": "Low",
+ "description": "A simple student feedback form that collects user names, emails, and ratings. Teaches basic HTML form handling and layout design.",
+ "features": [
+ "Collect user name, email, and age with validation",
+ "Dropdown menu for experience selection",
+ "Text area for detailed user suggestions"
+ ],
+ "tech_stack": [
+ "HTML"
+ ],
+ "roadmap": [
+ "Step 1: Create the HTML folder structure inside starter_code",
+ "Step 2: Build the input text fields and labels",
+ "Step 3: Add select options and textarea elements",
+ "Step 4: Align the form to the center for better layout",
+ "Step 5: Test the form using Live Server"
+ ],
+ "resources": [
+ "MDN HTML Forms: https://developer.mozilla.org/en-US/docs/Learn/Forms"
+ ],
+ "starter_code": "starter_code/survey_form/index.html"
+ },
+ {
+ "id": 20,
+ "title": "API ETL Pipeline",
+ "skills": [
+ "Python",
+ "pandas",
+ "requests"
+ ],
+ "level": "Intermediate",
+ "interest": "Data",
+ "time": "Medium",
+ "description": "Enter a public API URL to fetch data and automatically transform it into a structured CSV dataset.",
+ "features": [
+ "Fetch data from public APIs",
+ "Handle missing values",
+ "Normalize nested JSON",
+ "Generate summary statistics",
+ "Export the processed CSV for any other analytics projects"
+ ],
+ "tech_stack": [
+ "Python",
+ "pandas",
+ "requests",
+ "JSON"
+ ],
+ "roadmap": [
+ "Step 1: Install required modules via pip",
+ "Step 2: Find a public API key for this project",
+ "Step 3: Fetch the data from the API using requests",
+ "Step 4: Validate the response you just fetched from the API",
+ "Step 5: Normalize the nested JSON data by flattening it",
+ "Step 6: Use the fetched data to build a pandas dataframe",
+ "Step 7: Handle missing values or duplicate values",
+ "Step 8: Export the cleaned dataset to CSV format",
+ "Step 9: Generate a summary for the newly created CSV dataset",
+ "Step 10: Test the file with at least two different public APIs"
+ ],
+ "resources": [
+ "pandas docs: https://pandas.pydata.org/docs",
+ "requests docs: https://requests.readthedocs.io/en/latest/",
+ "JSON handling in Python: https://docs.python.org/3/library/json.html",
+ "REST API tutorial: https://restfulapi.net/",
+ "Real Python API guide: https://realpython.com/api-integration-in-python/"
+ ],
+ "starter_code": "starter_code/api_data_pipeline.py"
+ },
+ {
+ "id": 13,
+ "title": "AI Resume Analyzer",
+ "skills": [
+ "Python",
+ "Flask",
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "level": "Intermediate",
+ "interest": "Data",
+ "time": "High",
+ "description": "A Flask web app that compares a resume against a job description using TF-IDF similarity and keyword extraction. Users upload a PDF or paste text, and the app returns a match score, a list of missing keywords, and actionable feedback \u2014 with no external AI API required.",
+ "features": [
+ "Upload a resume as PDF or paste plain text",
+ "Paste any job description for comparison",
+ "TF-IDF cosine similarity match score (0\u2013100%)",
+ "Missing skills and keyword gap analysis",
+ "Actionable written feedback based on score",
+ "Single-page interface with interactive feedback display"
+ ],
+ "tech_stack": [
+ "Python",
+ "Flask",
+ "PyPDF2",
+ "scikit-learn",
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "roadmap": [
+ "Step 1: Run the server and verify the upload form renders",
+ "Step 2: Complete extract_text_from_pdf() using PyPDF2",
+ "Step 3: Complete clean_text() to normalise punctuation and whitespace",
+ "Step 4: Complete extract_keywords() to remove stopwords and count frequency",
+ "Step 5: Complete calculate_similarity() with TF-IDF and cosine distance",
+ "Step 6: Complete find_missing_skills() by comparing two keyword sets",
+ "Step 7: Complete generate_feedback() to produce written suggestions",
+ "Step 8: Wire everything together inside the /analyze Flask route",
+ "Step 9: Test with a real resume PDF and a real job posting"
+ ],
+ "resources": [
+ "PyPDF2 documentation: https://pypdf2.readthedocs.io/",
+ "scikit-learn TF-IDF guide: https://scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weighting",
+ "Cosine similarity explained: https://www.machinelearningplus.com/nlp/cosine-similarity",
+ "Flask quickstart: https://flask.palletsprojects.com/quickstart"
+ ],
+ "starter_code": "starter_code/ai_resume_analyzer.py"
+ },
+ {
+ "id": 21,
+ "title": "Number Guessing Game",
+ "skills": [
+ "Python"
+ ],
+ "level": "Beginner",
+ "interest": "Games",
+ "time": "Low",
+ "description": "A fun command-line game where the computer picks a random number and the user tries to guess it. Great for learning loops, conditionals, and user input handling.",
+ "features": [
+ "Generate random number between 1 and 100",
+ "Give hints: too high or too low",
+ "Count number of attempts",
+ "Show final score at the end"
+ ],
+ "tech_stack": [
+ "Python",
+ "random module"
+ ],
+ "roadmap": [
+ "Step 1: Set up the Python file and import random module",
+ "Step 2: Generate a random number using random.randint()",
+ "Step 3: Write a loop to take user input",
+ "Step 4: Compare guess with the number",
+ "Step 5: Give hints if guess is too high or too low",
+ "Step 6: Count the number of attempts",
+ "Step 7: Display win message with attempt count"
+ ],
+ "resources": [
+ "Python random module: https://docs.python.org/3/library/random.html",
+ "W3Schools Python: https://www.w3schools.com/python",
+ "Real Python: https://realpython.com"
+ ],
+ "starter_code": "starter_code/number_guessing.py"
+ },
+ {
+ "id": 12,
+ "title": "Simple Email Automation",
+ "skills": [
+ "Python"
+ ],
+ "level": "Beginner",
+ "interest": "Automation",
+ "time": "Low",
+ "description": "A Python script that sends automated emails using the smtplib library. Learn how to automate repetitive tasks and work with Python standard libraries.",
+ "features": [
+ "Compose and send emails via Python",
+ "Send to multiple recipients",
+ "Add subject and body text",
+ "Read recipient list from a text file"
+ ],
+ "tech_stack": [
+ "Python",
+ "smtplib",
+ "email module"
+ ],
+ "roadmap": [
+ "Step 1: Set up Python file and import smtplib",
+ "Step 2: Configure sender email and password",
+ "Step 3: Write the email composition function",
+ "Step 4: Connect to Gmail SMTP server",
+ "Step 5: Send email to one recipient and test",
+ "Step 6: Read recipient list from a text file",
+ "Step 7: Loop through recipients and send to all"
+ ],
+ "resources": [
+ "Python smtplib docs: https://docs.python.org/3/library/smtplib.html",
+ "Real Python email guide: https://realpython.com/python-send-email",
+ "Gmail SMTP settings: https://support.google.com/mail"
+ ],
+ "starter_code": "starter_code/email_automation.py"
+ },
+ {
+ "id": 22,
+ "title": "Quiz App",
+ "skills": [
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "level": "Beginner",
+ "interest": "Games",
+ "time": "Low",
+ "description": "A browser-based quiz app with multiple choice questions, a score counter, and a results screen. Perfect for practising DOM manipulation and event handling in JavaScript.",
+ "features": [
+ "Display one question at a time",
+ "Four multiple choice options per question",
+ "Show correct or incorrect feedback instantly",
+ "Display final score on results screen"
+ ],
+ "tech_stack": [
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "roadmap": [
+ "Step 1: Create HTML structure for question and options",
+ "Step 2: Style the quiz card with CSS",
+ "Step 3: Store questions as a JavaScript array of objects",
+ "Step 4: Write a function to display each question",
+ "Step 5: Add click event listeners to option buttons",
+ "Step 6: Check the selected answer and update score",
+ "Step 7: Move to the next question automatically",
+ "Step 8: Show the results screen with final score"
+ ],
+ "resources": [
+ "MDN DOM guide: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model",
+ "JavaScript events: https://javascript.info/events",
+ "W3Schools JavaScript: https://www.w3schools.com/js"
+ ],
+ "starter_code": "starter_code/quiz_app.html"
+ },
+ {
+ "id": 14,
+ "title": "File Organiser Script",
+ "skills": [
+ "Python"
+ ],
+ "level": "Beginner",
+ "interest": "Automation",
+ "time": "Low",
+ "description": "A Python script that scans a folder and automatically sorts files into subfolders by type \u2014 images, documents, videos, code files. Great for learning os and shutil modules.",
+ "features": [
+ "Detect file type by extension",
+ "Create subfolders automatically",
+ "Move files into the correct folder",
+ "Print a summary of what was moved"
+ ],
+ "tech_stack": [
+ "Python",
+ "os module",
+ "shutil module"
+ ],
+ "roadmap": [
+ "Step 1: Import os and shutil",
+ "Step 2: Define a dictionary mapping extensions to folder names",
+ "Step 3: Loop through files in the target directory",
+ "Step 4: Check each file's extension",
+ "Step 5: Create the destination folder if it doesn't exist",
+ "Step 6: Move the file using shutil.move()",
+ "Step 7: Print a summary of moved files"
+ ],
+ "resources": [
+ "Python os module: https://docs.python.org/3/library/os.html",
+ "Python shutil module: https://docs.python.org/3/library/shutil.html",
+ "Real Python file handling: https://realpython.com/working-with-files-in-python"
+ ],
+ "starter_code": "starter_code/file_organiser.py"
+ },
+ {
+ "id": 15,
+ "title": "Flashcard Study App",
+ "skills": [
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "level": "Beginner",
+ "interest": "Education",
+ "time": "Low",
+ "description": "A browser-based flashcard app where users can flip cards to reveal answers. Reinforces DOM manipulation, CSS transitions, and basic data storage in JavaScript.",
+ "features": [
+ "Flip card animation on click",
+ "Navigate between cards",
+ "Track how many cards reviewed",
+ "Shuffle deck order"
+ ],
+ "tech_stack": [
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "roadmap": [
+ "Step 1: Create the card HTML structure with front and back faces",
+ "Step 2: Write CSS for the 3D flip animation",
+ "Step 3: Store flashcard data as a JavaScript array",
+ "Step 4: Render the current card from the array",
+ "Step 5: Add click handler to trigger the flip",
+ "Step 6: Add next/previous navigation buttons",
+ "Step 7: Implement the shuffle function"
+ ],
+ "resources": [
+ "CSS 3D transforms: https://developer.mozilla.org/en-US/docs/Web/CSS/transform",
+ "JavaScript arrays: https://javascript.info/array",
+ "W3Schools CSS: https://www.w3schools.com/css"
+ ],
+ "starter_code": "starter_code/flashcard_app.html"
+ },
+ {
+ "id": 16,
+ "title": "Budget Tracker Web App",
+ "skills": [
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "level": "Intermediate",
+ "interest": "Data",
+ "time": "Medium",
+ "description": "A browser-based personal finance tracker that lets users add income and expense entries and visualises the balance over time with a simple chart.",
+ "features": [
+ "Add income and expense entries",
+ "Show running balance",
+ "Colour-code entries by type",
+ "Render a bar chart of monthly totals"
+ ],
+ "tech_stack": [
+ "HTML",
+ "CSS",
+ "JavaScript",
+ "Chart.js"
+ ],
+ "roadmap": [
+ "Step 1: Build the HTML form for adding entries",
+ "Step 2: Store entries in a JavaScript array",
+ "Step 3: Render the entry list dynamically",
+ "Step 4: Calculate and display the running balance",
+ "Step 5: Group entries by month for chart data",
+ "Step 6: Import Chart.js via CDN",
+ "Step 7: Render a bar chart using the monthly totals",
+ "Step 8: Add delete functionality for individual entries"
+ ],
+ "resources": [
+ "Chart.js docs: https://www.chartjs.org/docs/latest",
+ "MDN DOM: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model",
+ "JavaScript arrays: https://javascript.info/array"
+ ],
+ "starter_code": "starter_code/budget_tracker.html"
+ },
+ {
+ "id": 17,
+ "title": "Network Port Scanner",
+ "skills": [
+ "Python"
+ ],
+ "level": "Intermediate",
+ "interest": "Cybersecurity",
+ "time": "Medium",
+ "description": "A Python tool that scans a target host for open ports within a given range. Teaches socket programming, threading for speed, and basic network concepts.",
+ "features": [
+ "Accept host and port range as input",
+ "Check each port using sockets",
+ "Display open ports with service names",
+ "Use threading to speed up scanning"
+ ],
+ "tech_stack": [
+ "Python",
+ "socket module",
+ "threading module"
+ ],
+ "roadmap": [
+ "Step 1: Import socket and threading modules",
+ "Step 2: Write a function to test a single port",
+ "Step 3: Loop through the port range and test each",
+ "Step 4: Add threading to run scans concurrently",
+ "Step 5: Map common ports to service names",
+ "Step 6: Display results sorted by port number",
+ "Step 7: Add input validation for host and port range"
+ ],
+ "resources": [
+ "Python socket docs: https://docs.python.org/3/library/socket.html",
+ "Python threading: https://docs.python.org/3/library/threading.html",
+ "OWASP testing guide: https://owasp.org/www-project-web-security-testing-guide"
+ ],
+ "starter_code": "starter_code/port_scanner.py"
+ },
+ {
+ "id": 18,
+ "title": "Typing Speed Test",
+ "skills": [
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "level": "Beginner",
+ "interest": "Games",
+ "time": "Medium",
+ "description": "A browser-based typing test that measures words per minute and accuracy. Great for practising timers, string comparison, and dynamic DOM updates.",
+ "features": [
+ "Display a random passage to type",
+ "Start timer on first keypress",
+ "Highlight correct and incorrect characters in real time",
+ "Show WPM and accuracy on completion"
+ ],
+ "tech_stack": [
+ "HTML",
+ "CSS",
+ "JavaScript"
+ ],
+ "roadmap": [
+ "Step 1: Store a list of sample passages",
+ "Step 2: Display a random passage in the UI",
+ "Step 3: Listen for keypress events in the input field",
+ "Step 4: Start the timer on the first keypress",
+ "Step 5: Compare typed characters to the passage character by character",
+ "Step 6: Highlight correct characters green and errors red",
+ "Step 7: Stop the timer when the passage is complete",
+ "Step 8: Calculate and display WPM and accuracy"
+ ],
+ "resources": [
+ "JavaScript timers: https://developer.mozilla.org/en-US/docs/Web/API/setInterval",
+ "JavaScript string methods: https://javascript.info/string",
+ "MDN keyboard events: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent"
+ ],
+ "starter_code": "starter_code/typing_test.html"
+ },
+ {
+ "id": 19,
+ "title": "Course Progress Tracker",
+ "skills": [
+ "Python"
+ ],
+ "level": "Intermediate",
+ "interest": "Education",
+ "time": "Medium",
+ "description": "A CLI tool to track progress through online courses. Users can add courses, mark lessons complete, and see a visual progress bar per course.",
+ "features": [
+ "Add courses with a total lesson count",
+ "Mark individual lessons as complete",
+ "Display a text progress bar per course",
+ "Save and load state from a JSON file"
+ ],
+ "tech_stack": [
+ "Python",
+ "json module",
+ "os module"
+ ],
+ "roadmap": [
+ "Step 1: Define the course data structure",
+ "Step 2: Write add_course() and add_lesson() functions",
+ "Step 3: Implement mark_complete() logic",
+ "Step 4: Build a text progress bar renderer",
+ "Step 5: Write JSON save and load functions",
+ "Step 6: Create a menu loop for user interaction",
+ "Step 7: Display all courses with progress on startup"
+ ],
+ "resources": [
+ "Python JSON module: https://docs.python.org/3/library/json.html",
+ "Real Python CLI apps: https://realpython.com/command-line-interfaces-python-argparse",
+ "Python os module: https://docs.python.org/3/library/os.html"
+ ],
+ "starter_code": "starter_code/course_tracker.py"
+ }
+]
\ No newline at end of file
diff --git a/routes/main_routes.py b/routes/main_routes.py
index f023c328..e0b2c347 100644
--- a/routes/main_routes.py
+++ b/routes/main_routes.py
@@ -29,11 +29,22 @@ def interest_has_no_projects(interest):
@main.route("/")
def index():
- """Render the homepage with the skill input form and dynamic stats."""
+ """Render the homepage."""
stats = get_project_stats()
- available_levels = get_available_levels()
+ return render_template("index.html", stats=stats, config=Config)
+
+@main.route("/how-it-works")
+def how_it_works():
+ return render_template("how_it_works.html", config=Config)
- return render_template("index.html", stats=stats, available_levels=available_levels, config=Config)
+@main.route("/features")
+def features():
+ return render_template("features.html", config=Config)
+
+@main.route("/find-project")
+def find_project():
+ available_levels = get_available_levels()
+ return render_template("find_project.html", available_levels=available_levels, config=Config)
@main.route("/contact")
def contact():
@@ -143,6 +154,21 @@ def project_detail(project_id):
project = find_project_by_id(project_id)
if not project:
abort(404)
+
+ # Convert resources list to dict if needed (since template expects .items())
+ if "resources" in project and isinstance(project["resources"], list):
+ parsed_resources = {}
+ for r in project["resources"]:
+ if isinstance(r, str) and ": http" in r:
+ title, link = r.split(": http", 1)
+ parsed_resources[title.strip()] = "http" + link.strip()
+ elif isinstance(r, str) and ": " in r:
+ title, link = r.split(": ", 1)
+ parsed_resources[title.strip()] = link.strip()
+ else:
+ parsed_resources[str(r)] = "#"
+ project["resources"] = parsed_resources
+
return render_template("project.html", project=project, config=Config)
diff --git a/static/script.js b/static/script.js
index 577a5c24..ad23ff4a 100644
--- a/static/script.js
+++ b/static/script.js
@@ -6,11 +6,15 @@
function applyTheme(theme) {
var isDark = theme === "dark";
html.setAttribute("data-theme", theme);
+ if (isDark) {
+ html.classList.add("dark");
+ } else {
+ html.classList.remove("dark");
+ }
+
try {
localStorage.setItem("theme", theme);
- } catch (err) {
- // Storage can be unavailable in private browsing.
- }
+ } catch (err) {}
document.querySelectorAll(".theme-toggle").forEach(function (button) {
button.setAttribute("aria-pressed", isDark ? "true" : "false");
@@ -19,12 +23,7 @@
}
function initTheme() {
- var theme = "light";
- try {
- theme = localStorage.getItem("theme") || html.getAttribute("data-theme") || "light";
- } catch (err) {
- theme = html.getAttribute("data-theme") || "light";
- }
+ var theme = localStorage.getItem("theme") || "light";
applyTheme(theme);
requestAnimationFrame(function () {
html.classList.add("theme-ready");
@@ -35,7 +34,7 @@
var toggle = event.target.closest(".theme-toggle");
if (!toggle) return;
event.preventDefault();
- var current = html.getAttribute("data-theme") || "light";
+ var current = html.classList.contains("dark") ? "dark" : "light";
applyTheme(current === "dark" ? "light" : "dark");
});
@@ -301,119 +300,11 @@ updateProfileWidgets();
: quickPickChips.map(function (chip) { return chip.getAttribute("data-skill"); });
var activeSuggestionIndex = -1;
var visibleSuggestions = [];
- var SAVED_PROJECTS_KEY = "devpathSavedProjects";
function normalize(value) {
return String(value || "").trim().toLowerCase();
}
- function getSavedProjects() {
- try {
- var saved = JSON.parse(localStorage.getItem(SAVED_PROJECTS_KEY) || "[]");
- return Array.isArray(saved) ? saved : [];
- } catch (err) {
- console.warn("Unable to load saved projects", err);
- return [];
- }
- }
-
- function saveSavedProjects(projects) {
- try {
- localStorage.setItem(SAVED_PROJECTS_KEY, JSON.stringify(projects));
- } catch (err) {
- console.warn("Unable to save projects", err);
- }
- }
-
- function projectIsSaved(projectId) {
- return getSavedProjects().some(function (project) {
- return String(project.id) === String(projectId);
- });
- }
-
- function saveProject(project) {
- var saved = getSavedProjects();
- if (saved.some(function (item) { return String(item.id) === String(project.id); })) return;
-
- saved.unshift({
- id: project.id,
- title: project.title,
- level: project.level || "",
- time: project.time || "",
- skills: Array.isArray(project.skills) ? project.skills.slice(0, 4) : []
- });
- saveSavedProjects(saved);
- renderSavedProjects();
- }
-
- function removeSavedProject(projectId) {
- var saved = getSavedProjects().filter(function (project) {
- return String(project.id) !== String(projectId);
- });
- saveSavedProjects(saved);
- renderSavedProjects();
- document.querySelectorAll("[data-save-project-id='" + projectId + "']").forEach(function (button) {
- button.classList.remove("saved");
- button.textContent = "Save Project";
- button.setAttribute("aria-pressed", "false");
- });
- }
-
- function toggleSavedProject(project, button) {
- if (projectIsSaved(project.id)) {
- removeSavedProject(project.id);
- return;
- }
-
- saveProject(project);
- button.classList.add("saved");
- button.textContent = "Saved";
- button.setAttribute("aria-pressed", "true");
- }
-
- function renderSavedProjects() {
- var list = document.getElementById("saved-projects-list");
- var count = document.getElementById("saved-projects-count");
- if (!list || !count) return;
-
- var saved = getSavedProjects();
- count.textContent = saved.length + " saved";
- list.textContent = "";
-
- if (!saved.length) {
- var empty = document.createElement("p");
- empty.className = "saved-projects-empty";
- empty.textContent = "No saved projects yet.";
- list.appendChild(empty);
- return;
- }
-
- saved.forEach(function (project) {
- var item = document.createElement("article");
- item.className = "saved-project-item";
-
- var title = document.createElement("a");
- title.href = "/project/" + project.id;
- title.textContent = project.title;
-
- var meta = document.createElement("span");
- meta.textContent = [project.level, project.time].filter(Boolean).join(" - ");
-
- var remove = document.createElement("button");
- remove.type = "button";
- remove.className = "saved-project-remove";
- remove.textContent = "Remove";
- remove.addEventListener("click", function () {
- removeSavedProject(project.id);
- });
-
- item.appendChild(title);
- item.appendChild(meta);
- item.appendChild(remove);
- list.appendChild(item);
- });
- }
-
function syncSkillsHiddenInput() {
skillsHidden.value = JSON.stringify(selectedSkills);
}
@@ -480,13 +371,9 @@ updateProfileWidgets();
if (el) el.textContent = "";
}
- function syncSkillsHiddenInput() {
- if (!skillsHidden){
- skillsHidden = document.getElementById("skills");
- }
- // Keep the hidden in sync for form serialisation
- // The API expects a comma-separated string, so join the array that way
- skillsHidden.value = selectedSkills.join(", ");
+ function showFieldError(id, message) {
+ var el = document.getElementById(id);
+ if (el) el.textContent = message;
}
function clearAllErrors() {
@@ -570,70 +457,6 @@ updateProfileWidgets();
return valid;
}
-
-
- // ----------------------------------------------------------
- // Form submission and API call
- // ----------------------------------------------------------
-
- form.addEventListener("submit", function (evt) {
- evt.preventDefault(); //stop the browser from reloading the page on form submit
- clearAllErrors();
-
- if (skillsTextInput.value.trim()) {
- addSkill(skillsTextInput.value);
- skillsTextInput.value = "";
- hideSuggestions();
- }
-
- if (!validateForm()) return; //stop - anything missing/invalid
-
- setLoadingState(true);
-
- // Allow browser to paint spinner before request starts
- requestAnimationFrame(function () {
-
- //combine form values into an object to send to server/api
- var payload = {
- // Prefer the hidden input value; fall back to raw text box if hidden input is empty
- skills: skillsHidden.value.trim() || skillsTextInput.value.trim(),
- level: document.getElementById("level").value,
- interest: document.getElementById("interest").value,
- time: document.getElementById("time").value
- };
-
- //post the data to backend api as JSON, then handle the response
- fetch("/api/recommend", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload) //convert object to json string
- })
- .then(function (res) {
- return res.json(); //parse the response as JSON
- })
- .then(function (data) {
- setLoadingState(false);
-
- if (data.error) {
- var generalErr = document.getElementById("form-error-general");
- if (generalErr) generalErr.textContent = data.error;
- return;
- }
-
- renderResults(data.projects || [], data.message);
- })
- .catch(function (err) {
- // this runs if the network request itself fails
- setLoadingState(false);
- var generalErr = document.getElementById("form-error-general");
- if (generalErr) generalErr.textContent = "Something went wrong. Please try again.";
- console.error("API request failed:", err);
- });
- });
- });
-
-
- // Manages the loading state of the form and results section(whats visible or not)
function setLoadingState(isLoading) {
submitBtn.disabled = isLoading;
submitBtn.setAttribute("aria-busy", isLoading ? "true" : "false");
@@ -647,56 +470,9 @@ updateProfileWidgets();
resultsSection.scrollIntoView({ behavior: "smooth" });
} else {
resultsLoadingEl.style.display = "none";
- resultsGrid.style.display = "grid"; //switch back to grid layout
}
}
-
- // ----------------------------------------------------------
- // Render result cards
- // ----------------------------------------------------------
-
- //takes the array of projects from the api and draws them on the page as cards
- //if array is empty it shows the "no results" message instead
- function renderResults(projects, message) {
- resultsSection.style.display = "block";
- resultsLoadingEl.style.display = "none";
- // Clear out any cards from a previous search before showing new ones
- resultsGrid.innerHTML = "";
-
- if (!projects || projects.length === 0) { //if no projects returned from api, show the "no results" message and hide the grid
- resultsGrid.style.display = "none";
- resultsEmptyEl.style.display = "block";
-
- var interestEl = document.getElementById("interest");
- var selectedInterest = interestEl ? interestEl.value : null;
-
- // Show a friendly custom message when the user selected an interest
- if (emptyMessageEl) {
- if (selectedInterest) {
- emptyMessageEl.textContent = "No projects are currently available for this interest. Please check back later or try a different area.";
- } else if (message) {
- emptyMessageEl.textContent = message;
- } else {
- emptyMessageEl.textContent = "Try adjusting your skills or choosing a different interest area.";
- }
- }
-
- resultsSection.scrollIntoView({ behavior: "smooth" });
- return;
- }
-
- resultsEmptyEl.style.display = "none";
- resultsGrid.style.display = "grid";
-
- //build a card for each project and add it to the grid
- projects.forEach(function (project) {
- resultsGrid.appendChild(buildProjectCard(project));
- });
-
- resultsSection.scrollIntoView({ behavior: "smooth" });
- }
-
function truncate(text, maxLength) {
text = text || "";
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
@@ -707,32 +483,6 @@ updateProfileWidgets();
span.className = "project-tag project-tag--" + normalize(type).replace(/[^a-z0-9_-]/g, "-");
span.textContent = text;
return span;
-
- //takes the array of projects from the api and draws them on the page as cards
- //if array is empty it shows the "no results" message instead
- function renderResults(projects, message) {
- resultsSection.style.display = "block";
- resultsLoadingEl.style.display = "none";
- // Clear out any cards from a previous search before showing new ones
- resultsGrid.innerHTML = "";
-
- if (!projects || projects.length === 0) {
- resultsGrid.style.display = "none";
- resultsEmptyEl.style.display = "block";
- if (message && emptyMessageEl) emptyMessageEl.textContent = message;
- resultsSection.scrollIntoView({ behavior: "smooth" });
- return;
- }
-
- resultsEmptyEl.style.display = "none";
- resultsGrid.style.display = "grid";
-
- projects.forEach(function (project) {
- resultsGrid.appendChild(buildProjectCard(project));
- });
-
- resultsSection.scrollIntoView({ behavior: "smooth" });
- main
}
function buildProjectCard(project) {
@@ -774,27 +524,10 @@ updateProfileWidgets();
var footer = document.createElement("div");
footer.className = "project-card-footer";
-
- var saveButton = document.createElement("button");
- saveButton.type = "button";
- saveButton.className = "btn-save-project";
- saveButton.setAttribute("data-save-project-id", project.id);
- saveButton.setAttribute("aria-pressed", projectIsSaved(project.id) ? "true" : "false");
- if (projectIsSaved(project.id)) {
- saveButton.classList.add("saved");
- saveButton.textContent = "Saved";
- } else {
- saveButton.textContent = "Save Project";
- }
- saveButton.addEventListener("click", function () {
- toggleSavedProject(project, saveButton);
- });
-
var link = document.createElement("a");
link.className = "btn-details";
link.textContent = "View Full Project";
link.href = "/project/" + project.id;
- footer.appendChild(saveButton);
footer.appendChild(link);
card.appendChild(title);
@@ -804,8 +537,6 @@ updateProfileWidgets();
return card;
}
- renderSavedProjects();
-
function renderResults(projects, message) {
resultsSection.style.display = "block";
resultsLoadingEl.style.display = "none";
@@ -823,51 +554,6 @@ updateProfileWidgets();
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");
@@ -1007,33 +693,16 @@ updateProfileWidgets();
var errorMsg = document.getElementById("github-modal-error");
function closeGithubModal() {
- modal.classList.remove("active");
- githubInput.value = "";
- errorMsg.textContent = "";
- openModalBtn.focus(); // add this line
-}
+ modal.classList.remove("active");
+ githubInput.value = "";
+ errorMsg.textContent = "";
+ }
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();
diff --git a/templates/404.html b/templates/404.html
index e243bf57..21d1b934 100644
--- a/templates/404.html
+++ b/templates/404.html
@@ -1,38 +1,110 @@
-
-
-
-
-
- Page Not Found — DevPath
-
-
- {% include 'partials/theme_head.html' %}
-
-
-
-
-
-
-
-
-
-
-
-
-