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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*.log
*.pyc
*.swp
.venv/
__pycache__/
.buildlog/
.history

Expand Down
5 changes: 3 additions & 2 deletions routes/main_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ def recommend():
return jsonify({
"projects": [],
"message": (
"No projects matched your inputs. "
"Try different skills or broaden your interest area."
"No projects matched your exact skills. "
"Try adding broader skills like Python, JavaScript, HTML, or CSS "
"to see more recommendations."
)
}), 200

Expand Down
49 changes: 36 additions & 13 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,21 @@ 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
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 suggestedSkillsContainer = document.getElementById("suggested-skills-container");
var suggestedSkillsList = document.getElementById("suggested-skills-list");
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 = [];
Expand Down Expand Up @@ -514,10 +516,31 @@ if (isIndexPage) {
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

// Suggest skills that have projects in the dataset
var suggestions = ["Python", "JavaScript", "HTML", "CSS"];
if (suggestedSkillsList && suggestedSkillsContainer) {
suggestedSkillsList.innerHTML = "";
suggestions.forEach(function (skill) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "skill-chip";
btn.textContent = skill;
btn.addEventListener("click", function () {
addSkill(skill);
form.dispatchEvent(new Event("submit"));
});
suggestedSkillsList.appendChild(btn);
});
suggestedSkillsContainer.style.display = "block";
}

resultsSection.scrollIntoView({ behavior: "smooth" });
return;
}

if (suggestedSkillsContainer) suggestedSkillsContainer.style.display = "none";

resultsEmptyEl.style.display = "none";
resultsGrid.style.display = "grid";

Expand Down
24 changes: 23 additions & 1 deletion static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,28 @@ select:focus {
.container { padding: 0 20px; }
.section-sub { margin-bottom: 40px; }
}
/* ---- Suggested Skills in Empty State ---------------------- */
.suggested-skills-container {
margin: 24px 0;
padding: 20px;
background: var(--indigo-50);
border-radius: var(--r-md);
border: 1px dashed var(--indigo-200);
}

.suggestion-text {
font-size: 0.88rem;
font-weight: 700;
color: var(--indigo-700);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
}

.skill-chips-row--center {
justify-content: center;
gap: 10px;
}

.github-input-reveal input {
border: 1px solid var(--border);
Expand Down Expand Up @@ -2011,4 +2033,4 @@ select:focus {

#scroll-top-btn.visible {
display: flex;
}
}
8 changes: 5 additions & 3 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,11 @@ <h2 class="section-title">Recommended Projects</h2>
</div>
<h3>No Projects Found</h3>
<p id="empty-message">Try adjusting your skills or selecting a different interest area.</p>
<button class="btn-try-again"
onclick="document.getElementById('find-project').scrollIntoView({behavior:'smooth'})">Try Different
Inputs</button>
<div id="suggested-skills-container" class="suggested-skills-container" style="display:none;">
<p class="suggestion-text">Suggested skills to try:</p>
<div id="suggested-skills-list" class="skill-chips-row skill-chips-row--center"></div>
</div>
<button class="btn-try-again" onclick="document.getElementById('find-project').scrollIntoView({behavior:'smooth'})">Try Different Inputs</button>
</div>
</div>

Expand Down
15 changes: 15 additions & 0 deletions templates/project.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@
<meta name="twitter:description" content="{{ project.description[:155] }}" />
<link rel="stylesheet" href="/static/style.css" />
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LearningResource",
"name": "{{ project.title }}",
"description": "{{ project.description }}",
"educationalLevel": "{{ project.level }}",
"teaches": {{ project.skills | tojson }},
"url": "https://mydevpath-github.vercel.app/project/{{ project.id }}",
"provider": {
"@type": "Organization",
"name": "DevPath"
}
}
</script>
</head>
<body class="detail-page">

Expand Down
15 changes: 12 additions & 3 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ def test_get_recommendations_max_three():
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")
# Rust and Games are not in the dataset so this should be empty or minimal
assert isinstance(results, list)
# Rust is not in the dataset, so zero skill overlap means zero results
assert results == []


def test_get_recommendations_result_format():
Expand Down Expand Up @@ -255,6 +255,14 @@ def test_project_detail_not_found():
assert response.status_code == 404


def test_project_detail_json_ld_present():
"""Project detail page must include JSON-LD structured data."""
client = get_client()
response = client.get("/project/1")
assert response.status_code == 200
assert b"application/ld+json" in response.data


def test_internal_server_error_page():
"""The 500 handler should render the friendly internal error template."""
with app.app_context():
Expand All @@ -280,7 +288,8 @@ def test_download_code_found():
response = client.get("/project/1/download")
assert response.status_code == 200

def test_health_check(client):
def test_health_check():
client = get_client()
response = client.get("/health")
assert response.status_code == 200
data = response.get_json()
Expand Down
6 changes: 6 additions & 0 deletions utils/recommender.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ def get_recommendations(skills_string, level, interest, time_availability):
scored_projects = []

for project in all_projects:
# Require at least one skill match for a project to be recommended
project_skills = [s.lower() for s in project.get("skills", [])]
has_skill_match = any(s in project_skills for s in user_skills)
if not has_skill_match:
continue # Never recommend a project with zero skill overlap

score = score_single_project(
project, user_skills, level, interest, time_availability
)
Expand Down
Loading