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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions routes/main_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
100 changes: 66 additions & 34 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
44 changes: 43 additions & 1 deletion static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2983,4 +2983,46 @@ select:focus {
flex: 1;
white-space: pre;
color: #e6edf3;
}
}

/* 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;
}
29 changes: 28 additions & 1 deletion templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,33 @@
<nav class="navbar" id="navbar">
<div class="nav-inner">
<a href="/" class="nav-logo">Dev<span class="nav-logo-accent">Path</span></a>

<form id="topic-search-form" class="navbar-search">
<input
type="text"
id="topic-search"
autocomplete="on"
aria-label="Search projects"
placeholder="Search Projects"
/>

<button class="glass-search-btn" type="submit" aria-label="Search">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-4.35-4.35m1.85-5.15a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</button>
</form>

<div class="nav-links">
<a href="#home" class="nav-link">Home</a>
<a href="#how-it-works" class="nav-link">How It Works</a>
Expand Down Expand Up @@ -691,4 +718,4 @@ <h4 class="footer-col-title">About Us</h4>
<script src="/static/script.js"></script>
</body>

</html>
</html>
24 changes: 24 additions & 0 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,31 @@ def test_scoring_weights_has_all_keys():
expected_keys = {"skill", "level", "interest", "time"}
assert set(SCORING_WEIGHTS.keys()) == expected_keys

def test_search_api_returns_results():
"""Search API should return matching projects for a valid query."""
client = get_client()
response = client.get("/api/search?q=python")
assert response.status_code == 200
data = response.get_json()
assert isinstance(data, list)

def test_search_api_empty_query():
"""Search API should return an empty list for blank queries."""
client = get_client()
response = client.get("/api/search?q=")
assert response.status_code == 200
data = response.get_json()
assert data == []

def test_search_api_no_match():
"""Search should return empty list for nonsense query."""
client = get_client()
response = client.get("/api/search?q=nonexistentqueryxyz")
assert response.status_code == 200

data = response.get_json()
assert isinstance(data, list)
assert len(data) == 0
# ============================================================
# Sitemap and robots.txt tests
# ============================================================
Expand Down
Loading