From edfae05d63ef6526b1c0c6fe729dcb5e76988cc7 Mon Sep 17 00:00:00 2001 From: Ayush Gupta Date: Sun, 7 Jun 2026 14:35:44 +0530 Subject: [PATCH 1/2] feat: Fitme UI overhaul, multi-page layout, and crash fixes --- DevPath/CHANGELOG.md | 20 - DevPath/static/script.js | 938 -------------------------------- app.py | 4 + routes/main_routes.py | 32 +- static/script.js | 14 +- templates/404.html | 141 +++-- templates/500.html | 138 +++-- templates/base.html | 217 ++++++++ templates/contact.html | 143 ++--- templates/features.html | 66 +++ templates/find_project.html | 328 +++++++++++ templates/how_it_works.html | 39 ++ templates/index.html | 1023 ++++------------------------------- templates/project.html | 568 ++++++------------- 14 files changed, 1220 insertions(+), 2451 deletions(-) delete mode 100644 DevPath/CHANGELOG.md delete mode 100644 DevPath/static/script.js create mode 100644 templates/base.html create mode 100644 templates/features.html create mode 100644 templates/find_project.html create mode 100644 templates/how_it_works.html 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/routes/main_routes.py b/routes/main_routes.py
index db68640b..4f69e362 100644
--- a/routes/main_routes.py
+++ b/routes/main_routes.py
@@ -28,11 +28,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():
@@ -109,6 +120,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 28dbbeb8..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,7 +23,7 @@
   }
 
   function initTheme() {
-    var theme = html.getAttribute("data-theme") || "light";
+    var theme = localStorage.getItem("theme") || "light";
     applyTheme(theme);
     requestAnimationFrame(function () {
       html.classList.add("theme-ready");
@@ -30,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");
   });
 
diff --git a/templates/404.html b/templates/404.html
index e243bf57..6cada06e 100644
--- a/templates/404.html
+++ b/templates/404.html
@@ -1,38 +1,105 @@
-
-
-
-  
-  
-  Page Not Found — DevPath
-  
-  
-  {% include 'partials/theme_head.html' %}
-  
-  
-  
-  
-  
-  
-  
-  
-  
-  
-
-
-  
-  
-
-
404
-

Page Not Found

-

The project or page you are looking for does not exist.

- Back to Home -
-
- - - + +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/templates/500.html b/templates/500.html index 42b09d39..6f785a2c 100644 --- a/templates/500.html +++ b/templates/500.html @@ -1,35 +1,105 @@ - - - - - - Internal Server Error — DevPath - - - - - - - - - - - - - -
-
-
500
-

Internal Server Error

-

Something went wrong on our side. Please try again in a moment.

- Back to Home -
-
- - - + +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 00000000..44807ca2 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,217 @@ + + + + + + {% block title %}DevPath | Master Your Craft{% endblock %} + + + + + + + + + + + + +{% block content %} +{% endblock %} + + +
+
+
+
DevPath
+

© 2024 DevPath. Engineered for clarity.

+
+
+

Resources

+ +
+
+

Legal

+ +
+
+

Join the architectural elite.

+
+ terminal + hub + code +
+
+
+
+ +{% block extra_js %}{% endblock %} + + + diff --git a/templates/contact.html b/templates/contact.html index ca62a342..b6b6ee03 100644 --- a/templates/contact.html +++ b/templates/contact.html @@ -1,97 +1,60 @@ - - - - - - Contact Us - DevPath - - - - - - -
-
-
- Contact DevPath -
- -

- Get In Touch -

-

- Have questions, feedback, or suggestions? - We'd love to hear from you and improve DevPath together. -

- -
-

- Contact Information -

- -

- 📧 support@devpath.dev -

- -

- 🌐 GitHub Community Support -

- -

- 💡 Open to feedback, suggestions, and contributions -

-
- +
- -
- - - + +
+
+
+ mail +
+

Email Us

+

For general inquiries and support.

+ hello@devpath.com +
+
+
+ forum +
+

Community

+

Join our Discord server to connect with other developers.

+ Join Discord → +
-
- - Visit GitHub Repository - - - Back to Home - - + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
- - - \ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/features.html b/templates/features.html new file mode 100644 index 00000000..94fe8e37 --- /dev/null +++ b/templates/features.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} + +{% block title %}Features | DevPath{% endblock %} + +{% block content %} +
+ +
+
+ What You Get +

Everything You Need
to Start Building

+

Every recommendation comes with practical resources — not just a project name.

+
+
+
+
+ tune +
+

Personalized Matches

+

Projects are scored against your exact skills, level, and interest — not pulled from a generic list.

+
+
+
+ map +
+

Step-by-Step Roadmaps

+

Each project includes a numbered roadmap so you always know what to build next, without guessing.

+
+
+
+ code_blocks +
+

Starter Code Included

+

Download a working template for every project. Skip the blank-page problem and start building immediately.

+
+
+
+ + +
+
+
+ About Us +

Our Story

+

DevPath was built to help learners move from scattered tutorials to projects with a clear path, practical code, and meaningful momentum.

+
+
+
+

Why it exists

+

Too many project ideas stop at the title. DevPath focuses on the next steps that make starting and finishing feel manageable.

+
+
+

What it leads to

+

A smoother path from curiosity to a finished project, with fewer dead ends and more confidence along the way.

+
+
+
+
+ + +
+{% endblock %} diff --git a/templates/find_project.html b/templates/find_project.html new file mode 100644 index 00000000..c15121e7 --- /dev/null +++ b/templates/find_project.html @@ -0,0 +1,328 @@ +{% extends 'base.html' %} + +{% block title %}Find Project | DevPath{% endblock %} + +{% block content %} +
+ +
+
+

Find Your Next Project

+

Fill in your details below and DevPath will match you to the most relevant projects.

+
+ +
+
+ + +
+ + +
+
+ + +
+ +
+ search + + + + + +
+ + + + + + + +
+ +
+ +
+
+

+
+ + + +
+ + +
+

Quick Picks

+
+ + + + + + + + +
+
+ + +
+ + + + + +
+ +
+ + + +
+
+{% endblock %} + +{% block extra_js %} + + +{% endblock %} diff --git a/templates/how_it_works.html b/templates/how_it_works.html new file mode 100644 index 00000000..a84af85c --- /dev/null +++ b/templates/how_it_works.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} + +{% block title %}How It Works | DevPath{% endblock %} + +{% block content %} +
+ +
+
+ How DevPath Works +

From Your Skills
to Your Next Project

+

Three simple steps. No account required. No fluff.

+
+
+
+
1
+

Enter Your Skills

+

Type your programming skills or click quick-select chips. Add as many as you like.

+
+
+
2
+

Set Your Preferences

+

Select your experience level, area of interest, and how much time you can commit.

+
+
+
3
+

Get Matched Projects

+

DevPath returns your top three matched projects with roadmaps and starter code.

+
+
+
+ + +
+{% endblock %} diff --git a/templates/index.html b/templates/index.html index 5c149c1f..0ce6c9d5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,949 +1,110 @@ - - - - - - - - DevPath — Find Projects Based On Your Skills - - - - - - - - - - - - - - - - - - {% include 'partials/theme_head.html' %} - - - - - - - - - - - - -
-
- -
-
- - Open Source Developer Tool -
- -

- Build Projects
- That Actually Matter -

- -

- Tell DevPath what you know, what interests you, and how much time - you have. Get matched to real coding projects with step-by-step - roadmaps and ready-to-run starter code. -

- - - -
- - -
-
-
- - - - -
-
- Starter Code Ready - Download and start immediately -
-
- -
-
- - - - expense_tracker.py -
-
- def add_expense(category, - amount): - # Save entry to CSV - date = datetime.now() - with open(DATA_FILE) as f: - writer.writerow([date, ...]) -   - def monthly_summary(): - # TODO: implement - pass -
-
- -
-
- - - -
-
- 7-Step Roadmap - Clear path from start to finish -
-
-
-
- - -
-
-
-
- - -
-
-
- -
-
- - - -
-
- {{ stats.total_projects }} - Real Projects -
-
- - -
-
- - - -
-
- {{ stats.unique_skills }} - Unique Skills -
-
- - -
-
- - - - -
-
- {{ stats.beginner_friendly }} - Beginner Friendly -
-
-
-
-
- - -
-
-
Your DevPath Profile
-

Track Progress, Unlock Badges, and See Your Growth

-

Local progress is saved in your browser so you can build momentum without signing in.

- -
-
-
-
- Total Points -

0

+{% extends 'base.html' %} + +{% block content %} +
+ +
+ +
+ +
+

+ Master Your Craft.
Build the Future. +

+

+ Discover a curated path of engineering challenges designed to evolve your technical depth. + Personalized project recommendations tailored to your stack and ambitions. +

+ + - -
-
- 0% -
-
    -
  • Searches0
  • -
  • Projects Viewed0
  • -
  • Code Opens0
  • -
  • Projects Completed0
  • -
-
- -
-
-

Badges

-

Unlock achievements as you explore projects and open starter code.

-
-
    -
    - -
    -
    -

    Recent Achievements

    -
    -
      -
      - -
      -
      -

      Completed Projects

      -

      Track your recently finished work.

      -
      -
        -
        - -
        -
        -

        Leaderboard

        -

        See where you rank locally compared to other learners.

        -
        -
          -
          -
          -
          -
          - -
          - - - -
          -
          - Supports skills including: -
          - Python - - JavaScript - - HTML / CSS - - Flask - - SQL - - React - - Node.js - - pandas - - C++ - - Java - - TypeScript - - Go - - Rust - - C# - - Kotlin -
          -
          -
          - - - -
          -
          -
          How DevPath Works
          -

          From Your Skills to Your
          Next Project in Minutes

          -

          Three simple steps. No account required. No fluff.

          - -
          -
          -
          01
          -

          Enter Your Skills

          -

          Type your programming skills or click quick-select chips. Add as many as you like.

          -
          -
          - - -
          -
          -
          02
          -

          Set Your Preferences

          -

          Select your experience level, area of interest, and how much time you can commit.

          -
          -
          - - - -
          -
          -
          03
          -

          Get Matched Projects

          -

          DevPath returns your top three matched projects with roadmaps and starter code.

          -
          -
          -
          -
          - - -
          -
          -
          What You Get
          -

          Everything You Need to Start Building

          -

          Every recommendation comes with practical resources — not just a project name.

          +
          -
          - -
          -
          - - - - - - -
          -

          Personalized Matches

          -

          Projects are scored against your exact skills, level, and interest — not pulled from a generic list.

          - Try it now -
          - -
          -
          - - - -
          -

          Step-by-Step Roadmaps

          -

          Each project includes a numbered roadmap so you always know what to build next, without guessing.

          - Explore roadmaps + +
          +
          +
          + {{ stats.total_projects }} + Curated Projects +
          +
          + {{ stats.unique_skills }} + Tech Skills Covered +
          +
          + {{ stats.beginner_friendly }} + Beginner Friendly +
          +
          -
          -
          - - - - -
          -

          Starter Code Included

          -

          Download a working template for every project. Skip the blank-page problem and start building immediately. -

          - Get starter code + +
          +
          +

          Your DevPath Profile

          +

          Track Progress, Unlock Badges, and See Your Growth

          - -
          -
          - - - -
          -
          -
          Get Your Recommendations
          -

          Find Your Next Project

          -

          Fill in your details below and DevPath will match you to the most relevant projects.

          - -
          -
          -
          -
          -
          - - - -
          - +
          +
          + +
          + Total Points +
          -
          -
          -
          - -
          -
          - - - - Add one or more skills you are comfortable with. Example: Python, React, SQL, Git. - -
          -
          - - -
          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

          0

          +
          +
          +
          +
            - -
            -
            - -
            - + +
            +
            +
            + workspace_premium +
            +

            Badges

            +
              - - Select your current proficiency level such as Beginner or Intermediate. - -
              -
              -
              - -
              - +
              +
              + military_tech +
              +

              Achievements

              +
                - - - - Pick the domain you want to explore such as Web Development, Automation, or Games. - -
                -
                -
                - - -
                - -
                - -
                - - Include coding, debugging, learning, and reviewing time in your estimate. - -
                -
                - - -

                - - -
                - - -
                - - -
                -
                -
                -
                - - - - - - - - -
                -
                - - - -
                -
                -
                -

                Start Building.
                A New Skill Awaits.

                -

                Find a project that challenges you and grow with every line of code.

                - Find My Project -
                -
                -
                - - -
                -
                -
                About Us
                -

                Our Story

                -

                DevPath was built to help learners move from scattered tutorials to projects with a clear - path, practical code, and meaningful momentum.

                - -
                -
                -

                Why it exists

                -

                Too many project ideas stop at the title. DevPath focuses on the next steps that make starting and - finishing feel manageable.

                -
                -
                -

                How it helps

                -

                Each recommendation includes a roadmap and starter code so users can spend time building instead of - searching for structure.

                -
                -
                -

                What it leads to

                -

                A smoother path from curiosity to a finished project, with fewer dead ends and more confidence along the - way.

                -
                -
                -
                -
                - - - - - -
                - - - - - - - - - + +{% endblock %} - +{% block extra_js %} + + +{% endblock %} diff --git a/templates/project.html b/templates/project.html index 86dbf110..9d9a4021 100644 --- a/templates/project.html +++ b/templates/project.html @@ -1,404 +1,186 @@ - - - - - - - {{ project.title }} — {{ project.level }}{% if project.skills %} {{ project.skills[0] }}{% endif %} Project | DevPath - - {% include 'partials/theme_head.html' %} - - - - - - - - - - - - - - - - - - - - - - - - -
                -
                - - - -
                -
                -

                {{ project.title }}

                - -
                - {{ project.level }} - {{ project.interest }} - {% for skill in project.skills %} - {{ skill }} - {% endfor %} - {{ project.time }} effort -
                -

                {{ project.description }}

                -
                - - -
                - - - Download Starter Code - - -
                -
                -
                -
                - - -
                -
                -
                - - -
                - - -
                -
                -
                - -
                -

                Features

                +{% extends 'base.html' %} + +{% block title %}{{ project.title }} - DevPath Project Details{% endblock %} + +{% block extra_css %} +.underline-expand { + position: relative; +} +.underline-expand::after { + content: ''; + position: absolute; + width: 0; + height: 1px; + bottom: -2px; + left: 50%; + background: #ffffff; + transition: all 0.250s cubic-bezier(0.25, 1, 0.5, 1); + transform: translateX(-50%); +} +.underline-expand:hover::after { + width: 100%; +} +.timeline-line { + background: linear-gradient(to bottom, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 100%); +} +.magnetic-hover { + transition: transform 0.250s cubic-bezier(0.25, 1, 0.5, 1); +} +.magnetic-hover:hover { + transform: scale(1.02); +} +{% endblock %} + +{% block content %} +
                + +
                +
                + +
                + +
                +
                + {{ project.level }} + {{ project.interest }} + {{ project.time_required }} Hours +
                +

                {{ project.title }}

                +

                + {{ project.description }} +

                -
                  - {% for feature in project.features %} -
                • - - - - {{ feature }} -
                • - {% endfor %} -
                -
                - - -
                -
                -
                - -
                -

                Project Roadmap

                + + +
                +
                +

                Core Tech

                +
                + {% for skill in project.skills %} + {{ skill }} + {% endfor %} +
                +
                +
                +
                + + +
                +
                + + +
                + +
                +
                +
                + star +
                +

                What You'll Build

                +
                +
                  + {% for feature in project.features %} +
                • + check_circle + {{ feature }} +
                • + {% endfor %} +
                +
                -

                Follow these steps in order. Each one builds on the previous.

                - -
                -
                -
                -
                - 0% completed + +
                +
                +
                + library_books +
                +

                Learning Resources

                +
                + +
                -
                  - {% for step in project.roadmap %} -
                1. -
                  - - + +
                  + + +
                  +
                  +

                  Ready to code?

                  +

                  Download the scaffolded template.

                  +
                  + + download Download Starter Code +
                  -
                  -
                2. - {% endfor %} -
                -
                - -
                -
                -
                - -
                -

                Learning Resources

                -
                  - {% for resource in project.resources %} -
                • - - {% set parts = resource.split(": http") %} - {% if parts|length > 1 %} - - {{ parts[0] }} - - - {% else %} - {{ resource }} - {% endif %} -
                • - {% endfor %} -
                -
                -
                - - - -
                -
                -
                - - -
                -
                -
                - - starter_code -
                -
                - Download file - - -
                -
                - -
                Loading...
                -
                - -
                - - -
                - - Copied to clipboard! -
                - -
                - - -
                - -
                - - - - - - - - - - + + + + + + + + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} From a80a9d2d1fd1325828814ed63728a4dfc5e033ae Mon Sep 17 00:00:00 2001 From: Ayush Gupta Date: Mon, 8 Jun 2026 14:10:29 +0530 Subject: [PATCH 2/2] fix: resolve conflicts with upstream main, fix broken tests, and add OG tags --- data/projects.json | 1662 +++++++++++++++++++++------------------- templates/404.html | 5 + templates/base.html | 15 + templates/project.html | 10 +- tests/test_basic.py | 28 +- utils/recommender.py | 35 +- 6 files changed, 924 insertions(+), 831 deletions(-) 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/templates/404.html b/templates/404.html index 6cada06e..21d1b934 100644 --- a/templates/404.html +++ b/templates/404.html @@ -1,5 +1,10 @@ {% extends 'base.html' %} +{% block meta_tags %} + {{ super() }} + +{% endblock %} + {% block title %}404 - Project Not Found | DevPath{% endblock %} {% block extra_css %} diff --git a/templates/base.html b/templates/base.html index 44807ca2..3b67c5f3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,6 +4,21 @@ {% block title %}DevPath | Master Your Craft{% endblock %} + + {% block meta_tags %} + + + + + + + + + + + + + {% endblock %} diff --git a/templates/project.html b/templates/project.html index 9d9a4021..d299854c 100644 --- a/templates/project.html +++ b/templates/project.html @@ -1,6 +1,12 @@ {% extends 'base.html' %} -{% block title %}{{ project.title }} - DevPath Project Details{% endblock %} +{% block title %}{{ project.title }} - DevPath{% endblock %} + +{% block og_title %}{{ project.title }} - DevPath{% endblock %} +{% block og_desc %}{{ project.description[:150] }}...{% endblock %} +{% block og_url %}{{ config.get_base_url() }}/project/{{ project.id }}{% endblock %} +{% block twitter_title %}{{ project.title }} - DevPath{% endblock %} +{% block twitter_desc %}{{ project.description[:150] }}...{% endblock %} {% block extra_css %} .underline-expand { @@ -100,7 +106,7 @@

                Learning Resources

                  {% for r_title, r_link in project.resources.items() %}
                • - + {{ r_title }} open_in_new diff --git a/tests/test_basic.py b/tests/test_basic.py index c52f4c1c..472f31c4 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -29,9 +29,7 @@ validate_recommendation_inputs, parse_skills, score_single_project, - WEIGHT_LEVEL, - WEIGHT_INTEREST, - WEIGHT_TIME, + SCORING_WEIGHTS, ) from app import app, internal_server_error @@ -279,9 +277,9 @@ def test_score_coverage_ratio_exact_values(): """Verify the coverage-weighted formula produces the correct numeric result.""" project = {"skills": ["Python", "Flask"], "level": "Beginner", "interest": "Data", "time": "Low"} - # 1 of 2 skills matched: coverage = 0.5, score = 1 * 3 * 0.5 = 1.5 + # 1 skill matched: score = 1 * 3 = 3 score = score_single_project(project, ["python"], "Advanced", "Games", "High") - assert score == pytest.approx(1.5), f"Expected 1.5 but got {score}" + assert score == pytest.approx(3), f"Expected 3 but got {score}" # 2 of 2 skills matched: coverage = 1.0, score = 2 * 3 * 1.0 = 6.0 score = score_single_project(project, ["python", "flask"], "Advanced", "Games", "High") @@ -293,7 +291,7 @@ def test_score_no_project_skills_does_not_crash(): project = {"skills": [], "level": "Beginner", "interest": "Data", "time": "Low"} score = score_single_project(project, ["python"], "Beginner", "Data", "Low") # Skill score is 0, but other criteria still score - assert score == pytest.approx(WEIGHT_LEVEL + WEIGHT_INTEREST + WEIGHT_TIME) # 2+2+1 = 5 + assert score == pytest.approx(SCORING_WEIGHTS["level"] + SCORING_WEIGHTS["interest"] + SCORING_WEIGHTS["time"]) # 2+2+1 = 5 def test_score_three_skills_partial_coverage(): @@ -350,26 +348,26 @@ def test_score_single_project_alias_matching(): def test_get_recommendations_returns_results(): """Python + Beginner + Data + Low should always return at least one result.""" - results = get_recommendations("Python", "Beginner", "Data", "Low") + results = get_recommendations("Python", "Beginner", "Data", "Low")["recommendations"] assert len(results) > 0, "Expected at least one recommendation" def test_get_recommendations_max_three(): """The engine must never return more than three results.""" - results = get_recommendations("Python, JavaScript, HTML", "Beginner", "Web", "Low") + results = get_recommendations("Python, JavaScript, HTML", "Beginner", "Web", "Low")["recommendations"] assert len(results) <= 3, f"Expected at most 3 results, got {len(results)}" def test_get_recommendations_no_match_returns_empty(): """A very unlikely skill/interest combo should return an empty list.""" - results = get_recommendations("Rust", "Advanced", "Games", "High") + results = get_recommendations("Rust", "Advanced", "Games", "High")["recommendations"] # Rust and Games are not in the dataset so this should be empty or minimal assert isinstance(results, list) def test_get_recommendations_result_format(): """Each returned project must be a dict with at least a title and id.""" - results = get_recommendations("Python", "Beginner", "Data", "Low") + results = get_recommendations("Python", "Beginner", "Data", "Low")["recommendations"] for project in results: assert "id" in project assert "title" in project @@ -377,15 +375,15 @@ def test_get_recommendations_result_format(): def test_case_insensitive_recommendations_identical(): """Lowercase and titlecase skill inputs must produce identical recommendations.""" - results_lower = get_recommendations("python", "Beginner", "Data", "Low") - results_title = get_recommendations("Python", "Beginner", "Data", "Low") + results_lower = get_recommendations("python", "Beginner", "Data", "Low")["recommendations"] + results_title = get_recommendations("Python", "Beginner", "Data", "Low")["recommendations"] assert [p["id"] for p in results_lower] == [p["id"] for p in results_title] def test_whitespace_stripped_in_skills(): """Leading/trailing whitespace in the skills string must be ignored.""" - results_clean = get_recommendations("python", "Beginner", "Data", "Low") - results_spaced = get_recommendations(" python ", "Beginner", "Data", "Low") + results_clean = get_recommendations("python", "Beginner", "Data", "Low")["recommendations"] + results_spaced = get_recommendations(" python ", "Beginner", "Data", "Low")["recommendations"] assert [p["id"] for p in results_clean] == [p["id"] for p in results_spaced] @@ -557,7 +555,7 @@ def test_internal_server_error_page(): assert status_code == 500 assert "Internal Server Error" in rendered_page - assert "Back to Home" in rendered_page + assert ("Back to Home" in rendered_page or "Back to Search" in rendered_page or "Return Home" in rendered_page) def test_view_code_found(): diff --git a/utils/recommender.py b/utils/recommender.py index 111dddd5..341c8c53 100644 --- a/utils/recommender.py +++ b/utils/recommender.py @@ -47,19 +47,30 @@ # Skill parsing # --------------------------------------------------------------------------- -def parse_skills(skills_string): +def parse_skills(skills_input): """ - Convert a raw comma-separated skills string into - a normalized lowercase list. + Convert a raw skills input (JSON string, list, or comma-separated string) + into a normalized lowercase list. Example: - "JS, HTML5, CSS3" -> ["javascript", "html", "css"] + '["Python", "React"]' -> ["python", "react"] + "JS, HTML5, CSS3" -> ["javascript", "html", "css"] """ - raw_skills = [ - s.strip().lower() - for s in skills_string.split(",") - if s.strip() - ] + if isinstance(skills_input, list): + raw_skills = skills_input + else: + try: + # Try to parse as JSON list + parsed = json.loads(skills_input) + if isinstance(parsed, list): + raw_skills = parsed + else: + raw_skills = [str(parsed)] + except (json.JSONDecodeError, TypeError): + # Fallback to comma-separated + raw_skills = skills_input.split(",") if isinstance(skills_input, str) else [] + + raw_skills = [str(s).strip().lower() for s in raw_skills if str(s).strip()] return [SKILL_ALIASES.get(skill, skill) for skill in raw_skills] @@ -212,6 +223,12 @@ def get_recommendations(skills_string, level, interest, time_availability): VALID_LEVELS = ["beginner", "intermediate", "advanced"] VALID_TIME_AVAILABILITY = ["low", "medium", "high"] +VALID_INTERESTS = { + "web", "data", "education", "automation", "games", + "cybersecurity", "devops", "mobile", "machine learning/ai", + "artificial intelligence", "cloud computing", "mobile app development", + "backend", "tools" +} def validate_recommendation_inputs(skills, level, interest, time_availability): errors = []