diff --git a/.gitignore b/.gitignore index b921615..8016298 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ *.log *.pyc *.swp +.venv/ +__pycache__/ .buildlog/ .history diff --git a/routes/main_routes.py b/routes/main_routes.py index 5cb3459..1c43628 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -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 diff --git a/static/script.js b/static/script.js index d4de221..6ba12a8 100644 --- a/static/script.js +++ b/static/script.js @@ -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 = []; @@ -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"; diff --git a/static/style.css b/static/style.css index 3b11ca9..bb0881f 100644 --- a/static/style.css +++ b/static/style.css @@ -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); @@ -2011,4 +2033,4 @@ select:focus { #scroll-top-btn.visible { display: flex; -} \ No newline at end of file +} diff --git a/templates/index.html b/templates/index.html index 05ad04c..2d88d87 100644 --- a/templates/index.html +++ b/templates/index.html @@ -506,9 +506,11 @@
Try adjusting your skills or selecting a different interest area.
- + + diff --git a/templates/project.html b/templates/project.html index d964fb7..c4abdae 100644 --- a/templates/project.html +++ b/templates/project.html @@ -17,6 +17,21 @@ + diff --git a/tests/test_basic.py b/tests/test_basic.py index a66c75b..739a03a 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -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(): @@ -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(): @@ -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() diff --git a/utils/recommender.py b/utils/recommender.py index 308c14f..e5ae379 100644 --- a/utils/recommender.py +++ b/utils/recommender.py @@ -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 )