diff --git a/routes/main_routes.py b/routes/main_routes.py index 658553e..722581a 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -157,3 +157,32 @@ def sitemap(): def robots(): """Serve robots.txt from the static folder.""" return send_from_directory("static", "robots.txt", mimetype="text/plain") + +@main.route("/api/search") +def search_projects(): + """Return projects matching the user's search query.""" + + query = request.args.get("q", "").strip().lower() + + if not query: + return jsonify([]) + + projects = load_all_projects() + filtered_projects = [] + + for project in projects: + + # Combine searchable project fields into one lowercase string + searchable_text = " ".join([ + project.get("title", ""), + project.get("description", ""), + project.get("interest", ""), + " ".join(project.get("skills", [])), + " ".join(project.get("tech_stack", [])), + " ".join(project.get("features", [])) + ]).lower() + + if query in searchable_text: + filtered_projects.append(project) + + return jsonify(filtered_projects) diff --git a/static/script.js b/static/script.js index f97e5a0..da03986 100644 --- a/static/script.js +++ b/static/script.js @@ -567,47 +567,51 @@ if (clearFiltersBtn) { //takes the array of projects from the api and draws them on the page as cards //if array is empty it shows the "no results" message instead - function renderResults(projects, message) { - resultsSection.style.display = "block"; - resultsLoadingEl.style.display = "none"; - // Clear out any cards from a previous search before showing new ones - resultsGrid.innerHTML = ""; - - if (!projects || projects.length === 0) { - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "block"; - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "block"; - if (message && emptyMessageEl) emptyMessageEl.textContent = message; - 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"; - - // Show a friendly custom message when the user selected an interest - var selectedInterest = document.getElementById("interest")?.value; - if (selectedInterest) { - emptyMessageEl.textContent = "No projects are currently available for this interest. Please check back later or try a different area."; - } else if (message) { - emptyMessageEl.textContent = message; - } else { - emptyMessageEl.textContent = "Try adjusting your skills or choosing a different interest area."; - } +function renderResults(projects, message) { + resultsSection.style.display = "block"; + resultsLoadingEl.style.display = "none"; - resultsSection.scrollIntoView({ behavior: "smooth" }); - return; - } + // Clear out previous results before rendering new ones + resultsGrid.innerHTML = ""; - resultsEmptyEl.style.display = "none"; - resultsGrid.style.display = "grid"; + // If no projects are returned, show the empty state message + if (!projects || projects.length === 0) { + resultsGrid.style.display = "none"; + resultsEmptyEl.style.display = "block"; - //build a card for each project and add it to the grid - projects.forEach(function (project) { - resultsGrid.appendChild(buildProjectCard(project)); - }); + // Show a custom message when an interest is selected + var interestSelect = document.getElementById("interest"); + var selectedInterest = ""; + + if (interestSelect) { + selectedInterest = interestSelect.value; + } + + if (selectedInterest) { + emptyMessageEl.textContent = + "No projects are currently available for this interest. Please check back later or try a different area."; + } else if (message) { + emptyMessageEl.textContent = message; + } else { + emptyMessageEl.textContent = + "Try adjusting your skills or choosing a different interest area."; + } resultsSection.scrollIntoView({ behavior: "smooth" }); + return; } + resultsEmptyEl.style.display = "none"; + resultsGrid.style.display = "grid"; + + // Build a card for each project and add it to the grid + projects.forEach(function (project) { + resultsGrid.appendChild(buildProjectCard(project)); + }); + + resultsSection.scrollIntoView({ behavior: "smooth" }); +} + // builds one project card as a DOM element and returns it // the card has title, short description, tags and link function buildProjectCard(project) { @@ -930,3 +934,31 @@ if (scrollTopBtn) { window.addEventListener('scroll', handleScroll); scrollTopBtn.addEventListener('click', scrollToTop); } + +// Handle project search form submission and display matching results +function handleSearchSubmit(event) { + event.preventDefault(); + + var query = document.getElementById("topic-search").value; + + fetch("/api/search?q=" + encodeURIComponent(query)) + .then(function (response) { + if (!response.ok) { + throw new Error("Failed to fetch search results"); + } + + return response.json(); + }) + .then(function (projects) { + renderResults(projects); + }) + .catch(function (error) { + console.error("Search failed:", error); + }); +} + +var searchForm = document.getElementById("topic-search-form"); + +if (searchForm) { + searchForm.addEventListener("submit", handleSearchSubmit); +} diff --git a/static/style.css b/static/style.css index b399ee5..979a793 100644 --- a/static/style.css +++ b/static/style.css @@ -2983,4 +2983,46 @@ select:focus { flex: 1; white-space: pre; color: #e6edf3; -} \ No newline at end of file +} + +/* Search bar changes */ +.glass-search-btn { + width: 2.5rem; + height: 2.2rem; + padding: 5px; + border: 1px solid rgba(255, 255, 255, 0.18); + + display: flex; + align-items: center; + justify-content: center; + + color: white; + background: rgba(255, 255, 255, 0.08); + + cursor: pointer; + + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + + box-shadow: + 0 4px 18px rgba(0, 0, 0, 0.14), + inset 0 1px 0 rgba(255, 255, 255, 0.08); + + transition: all 0.2s ease; +} + +.glass-search-btn:hover { + background: rgba(255, 255, 255, 0.14); +} + +#topic-search-form { + display: flex; + + height: 2.2rem; + margin: 0 20px; +} + + +#topic-search { + border-radius: 0; +} diff --git a/templates/index.html b/templates/index.html index aa7751f..0fd73ac 100644 --- a/templates/index.html +++ b/templates/index.html @@ -22,6 +22,33 @@