diff --git a/data/projects.json b/data/projects.json index 26b1bcaa..d4fdd5d2 100644 --- a/data/projects.json +++ b/data/projects.json @@ -2,7 +2,9 @@ { "id": 1, "title": "Personal Expense Tracker", - "skills": ["Python"], + "skills": [ + "Python" + ], "level": "Beginner", "interest": "Data", "time": "Low", @@ -13,7 +15,11 @@ "View monthly summary", "Export data to CSV file" ], - "tech_stack": ["Python", "CSV module", "datetime module"], + "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", @@ -28,12 +34,23 @@ "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" + "starter_code": "starter_code/expense_tracker.py", + "estimated_hours": 4, + "prerequisite_skills": [ + "Basic Programming Concepts", + "Loops and Functions" + ], + "views": 180, + "recommendations": 45 }, { "id": 2, "title": "Weather Dashboard", - "skills": ["JavaScript", "HTML", "CSS"], + "skills": [ + "JavaScript", + "HTML", + "CSS" + ], "level": "Beginner", "interest": "Web", "time": "Low", @@ -44,10 +61,13 @@ "Show a weather icon based on conditions", "Toggle between Celsius and Fahrenheit" ], - "tech_stack": ["HTML", "CSS", "JavaScript", "OpenWeatherMap API"], + "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", @@ -59,12 +79,22 @@ "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" + "starter_code": "starter_code/weather_dashboard.html", + "estimated_hours": 6, + "prerequisite_skills": [ + "HTML Basics", + "CSS Basics", + "JavaScript Variables" + ], + "views": 320, + "recommendations": 98 }, { "id": 3, "title": "Student Grade Manager", - "skills": ["Python"], + "skills": [ + "Python" + ], "level": "Beginner", "interest": "Education", "time": "Medium", @@ -75,7 +105,11 @@ "Assign letter grades automatically", "Save and load data from a JSON file" ], - "tech_stack": ["Python", "json module", "os module"], + "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", @@ -90,12 +124,21 @@ "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" + "starter_code": "starter_code/grade_manager.py", + "estimated_hours": 12, + "prerequisite_skills": [ + "Python Dictionaries", + "File Handling in Python" + ], + "views": 95, + "recommendations": 30 }, { "id": 4, "title": "Task Manager REST API", - "skills": ["Python"], + "skills": [ + "Python" + ], "level": "Intermediate", "interest": "Web", "time": "Medium", @@ -106,7 +149,12 @@ "Assign priority levels to tasks", "Persist data to a JSON file" ], - "tech_stack": ["Python", "Flask", "JSON", "Postman (for testing)"], + "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", @@ -122,12 +170,24 @@ "REST API design guide: https://restfulapi.net", "Postman download: https://www.postman.com/downloads" ], - "starter_code": "starter_code/task_api.py" + "starter_code": "starter_code/task_api.py", + "estimated_hours": 18, + "prerequisite_skills": [ + "Python functions", + "Basic HTTP concepts", + "JSON data structure" + ], + "views": 240, + "recommendations": 75 }, { "id": 5, "title": "Portfolio Website", - "skills": ["HTML", "CSS", "JavaScript"], + "skills": [ + "HTML", + "CSS", + "JavaScript" + ], "level": "Beginner", "interest": "Web", "time": "Low", @@ -138,7 +198,11 @@ "Skills list with visual indicators", "Contact form with basic validation" ], - "tech_stack": ["HTML", "CSS", "JavaScript"], + "tech_stack": [ + "HTML", + "CSS", + "JavaScript" + ], "roadmap": [ "Step 1: Plan the page sections on paper first", "Step 2: Write the HTML structure for all sections", @@ -154,12 +218,24 @@ "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" + "starter_code": "starter_code/portfolio.html", + "estimated_hours": 5, + "prerequisite_skills": [ + "HTML Structure", + "CSS Selectors" + ], + "views": 480, + "recommendations": 135 }, { "id": 6, "title": "URL Shortener", - "skills": ["Python", "JavaScript", "HTML", "CSS"], + "skills": [ + "Python", + "JavaScript", + "HTML", + "CSS" + ], "level": "Intermediate", "interest": "Web", "time": "High", @@ -170,7 +246,14 @@ "Track how many times a link was clicked", "List all shortened links in a dashboard" ], - "tech_stack": ["Python", "Flask", "HTML", "CSS", "JavaScript", "JSON"], + "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", @@ -186,12 +269,22 @@ "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" + "starter_code": "starter_code/url_shortener.py", + "estimated_hours": 35, + "prerequisite_skills": [ + "Python basics", + "Web forms", + "HTML/CSS basics" + ], + "views": 410, + "recommendations": 115 }, { "id": 7, "title": "Data Analysis Report Generator", - "skills": ["Python"], + "skills": [ + "Python" + ], "level": "Intermediate", "interest": "Data", "time": "High", @@ -202,7 +295,12 @@ "Calculate mean, median, and mode per column", "Generate bar charts for categorical data" ], - "tech_stack": ["Python", "pandas", "matplotlib", "os module"], + "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", @@ -217,12 +315,21 @@ "matplotlib tutorials: https://matplotlib.org/stable/tutorials", "Real Python data analysis: https://realpython.com/pandas-dataframe" ], - "starter_code": "starter_code/data_report.py" + "starter_code": "starter_code/data_report.py", + "estimated_hours": 30, + "prerequisite_skills": [ + "Python Basics", + "Understanding of lists and tabular data" + ], + "views": 210, + "recommendations": 60 }, { "id": 8, "title": "Password Strength Checker", - "skills": ["Python"], + "skills": [ + "Python" + ], "level": "Beginner", "interest": "Cybersecurity", "time": "Low", @@ -233,7 +340,10 @@ "Suggest stronger password improvements", "Prevent weak password patterns" ], - "tech_stack": ["Python", "Regex"], + "tech_stack": [ + "Python", + "Regex" + ], "roadmap": [ "Step 1: Create the password input system", "Step 2: Check password length", @@ -246,12 +356,21 @@ "Python regex docs: https://docs.python.org/3/library/re.html", "OWASP password guidelines: https://owasp.org" ], - "starter_code": "starter_code/password_checker.py" + "starter_code": "starter_code/password_checker.py", + "estimated_hours": 3, + "prerequisite_skills": [ + "Basic Python Syntax", + "String operations" + ], + "views": 150, + "recommendations": 35 }, { "id": 9, "title": "Feedback Survey Form", - "skills": ["HTML"], + "skills": [ + "HTML" + ], "level": "Beginner", "interest": "Web", "time": "Low", @@ -261,7 +380,9 @@ "Dropdown menu for experience selection", "Text area for detailed user suggestions" ], - "tech_stack": ["HTML"], + "tech_stack": [ + "HTML" + ], "roadmap": [ "Step 1: Create the HTML folder structure inside starter_code", "Step 2: Build the input text fields and labels", @@ -272,12 +393,22 @@ "resources": [ "MDN HTML Forms: https://developer.mozilla.org/en-US/docs/Learn/Forms" ], - "starter_code": "starter_code/survey_form/index.html" + "starter_code": "starter_code/survey_form/index.html", + "estimated_hours": 3, + "prerequisite_skills": [ + "HTML Basics" + ], + "views": 110, + "recommendations": 25 }, { - "id":10, + "id": 10, "title": "API ETL Pipeline", - "skills": ["Python","pandas","requests"], + "skills": [ + "Python", + "pandas", + "requests" + ], "level": "Intermediate", "interest": "Data", "time": "Medium", @@ -289,7 +420,12 @@ "Generate summary statistics", "Export the processed CSV for any other Analytics projects" ], - "tech_stack": ["Python", "pandas","requests","JSON"], + "tech_stack": [ + "Python", + "pandas", + "requests", + "JSON" + ], "roadmap": [ "Step 1: Install required modules via pip", "Step 2: Find a public API key for this project", @@ -309,9 +445,14 @@ "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" + "starter_code": "starter_code/api_data_pipeline.py", + "estimated_hours": 25, + "prerequisite_skills": [ + "Python Basics", + "Understanding APIs", + "pandas library basics" + ], + "views": 180, + "recommendations": 55 } -] - - - +] \ No newline at end of file diff --git a/data/search_analytics.json b/data/search_analytics.json new file mode 100644 index 00000000..f6f5ec6b --- /dev/null +++ b/data/search_analytics.json @@ -0,0 +1,293 @@ +[ + { + "q": "weather", + "filters": { + "q": "weather", + "sort_by": "views" + }, + "result_count": 1, + "timestamp": "2026-05-18T14:18:41.549438" + }, + { + "q": "weather", + "filters": { + "q": "weather", + "sort_by": "views" + }, + "result_count": 1, + "timestamp": "2026-05-18T14:18:38.005235" + }, + { + "q": "weather", + "filters": { + "q": "weather", + "level": "Advanced", + "interest": "Web", + "time": "High", + "tech_stack": "Python", + "min_hours": "1", + "max_hours": "10", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:15:07.502670" + }, + { + "q": "weather", + "filters": { + "q": "weather", + "level": "Advanced", + "interest": "Web", + "time": "High", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:14:15.807509" + }, + { + "q": "weather", + "filters": { + "q": "weather", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:13:58.162471" + }, + { + "q": "weather", + "filters": { + "q": "weather", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python", + "prerequisite_skills": "Basic Programming Concepts", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:13:54.568062" + }, + { + "q": "Python", + "filters": { + "q": "Python", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python", + "prerequisite_skills": "Basic Programming Concepts", + "sort_by": "relevance" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:12:23.079858" + }, + { + "q": "Python", + "filters": { + "q": "Python", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python", + "sort_by": "relevance" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:12:18.151602" + }, + { + "q": "Matplotlib", + "filters": { + "q": "Matplotlib", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python", + "min_hours": "15", + "max_hours": "24", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:11:53.983472" + }, + { + "q": "Weather", + "filters": { + "q": "Weather", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python", + "min_hours": "15", + "max_hours": "24", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:11:52.399515" + }, + { + "q": "Matplotlib", + "filters": { + "q": "Matplotlib", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python", + "min_hours": "15", + "max_hours": "24", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:11:50.727012" + }, + { + "q": "Flask", + "filters": { + "q": "Flask", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python", + "min_hours": "15", + "max_hours": "24", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:11:48.975190" + }, + { + "q": "Python", + "filters": { + "q": "Python", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python", + "min_hours": "15", + "max_hours": "24", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:11:46.527072" + }, + { + "q": "Python", + "filters": { + "q": "Python", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python", + "min_hours": "15", + "max_hours": "24", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:11:43.863352" + }, + { + "q": "Python", + "filters": { + "q": "Python", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "tech_stack": "Python,pandas,matplotlib", + "prerequisite_skills": "Basic Programming Concepts,Loops and Functions,File Handling in Python", + "min_hours": "0", + "max_hours": "10", + "sort_by": "views" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:11:26.000485" + }, + { + "q": "Python", + "filters": { + "q": "Python", + "skills": "Python", + "level": "Advanced", + "interest": "Web", + "time": "Medium", + "sort_by": "relevance" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:11:02.217383" + }, + { + "q": "weather", + "filters": { + "q": "weather", + "sort_by": "views" + }, + "result_count": 1, + "timestamp": "2026-05-18T14:09:30.838594" + }, + { + "q": "weather", + "filters": { + "q": "weather", + "sort_by": "views" + }, + "result_count": 1, + "timestamp": "2026-05-18T14:08:40.816360" + }, + { + "q": "Flask", + "filters": { + "q": "Flask", + "skills": "Python", + "level": "Intermediate", + "interest": "Data", + "time": "Medium", + "tech_stack": "pandas", + "sort_by": "relevance" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:07:55.295821" + }, + { + "q": "Flask", + "filters": { + "q": "Flask", + "skills": "Python", + "level": "Intermediate", + "interest": "Data", + "time": "Medium", + "sort_by": "relevance" + }, + "result_count": 0, + "timestamp": "2026-05-18T14:07:51.321866" + }, + { + "q": "weather", + "filters": { + "q": "weather", + "sort_by": "views" + }, + "result_count": 1, + "timestamp": "2026-05-18T14:06:19.575494" + }, + { + "q": "weather", + "filters": { + "q": "weather", + "sort_by": "views" + }, + "result_count": 1, + "timestamp": "2026-05-18T14:06:11.797241" + } +] \ No newline at end of file diff --git a/routes/main_routes.py b/routes/main_routes.py index 5cb34592..d89a37bc 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -8,6 +8,8 @@ from utils.recommender import get_recommendations, validate_recommendation_inputs from utils.data_loader import find_project_by_id, get_project_stats from utils.file_server import read_starter_code, resolve_starter_file, get_starter_code_dir +from utils.search_engine import search_projects +from utils.analytics import log_search_query, get_trending_searches import os # Create the Blueprint that app.py will register @@ -72,6 +74,52 @@ def recommend(): return jsonify({"projects": results}), 200 +@main.route("/api/search", methods=["GET"]) +def search(): + """ + Advanced search route (GET). + Accepts query params: q, skills, level, interest, time, tech_stack, + prerequisite_skills, min_hours, max_hours, sort_by + Returns matching projects list as JSON, and logs the search query in analytics. + """ + query_params = { + "q": request.args.get("q", ""), + "skills": request.args.get("skills", ""), + "level": request.args.get("level", ""), + "interest": request.args.get("interest", ""), + "time": request.args.get("time", ""), + "tech_stack": request.args.get("tech_stack", ""), + "prerequisite_skills": request.args.get("prerequisite_skills", ""), + "min_hours": request.args.get("min_hours", ""), + "max_hours": request.args.get("max_hours", ""), + "sort_by": request.args.get("sort_by", "relevance") + } + + results = search_projects(query_params) + + # Log to search analytics + q_text = query_params["q"].strip() + log_search_query(q_text, query_params, len(results)) + + return jsonify({ + "projects": results, + "count": len(results) + }), 200 + + +@main.route("/api/trending", methods=["GET"]) +def trending_searches(): + """ + Trending searches endpoint (GET). + Returns a list of popular search keywords based on search history. + """ + limit = request.args.get("limit", 5, type=int) + trending = get_trending_searches(limit=limit) + return jsonify({ + "trending": trending + }), 200 + + @main.route("/project/") def project_detail(project_id): """Render the full detail page for a single project.""" diff --git a/static/script.js b/static/script.js index 1480ca34..117b50c7 100644 --- a/static/script.js +++ b/static/script.js @@ -454,6 +454,59 @@ if (isIndexPage) { hideSuggestions(); } + if (isAdvancedMode) { + // In Advanced Search Mode, we skip dropdown validation (allow empty criteria) + setLoadingState(true); + + var queryParams = []; + + var q = document.getElementById("adv-keyword-input").value.trim(); + if (q) queryParams.push("q=" + encodeURIComponent(q)); + + var skills = skillsHidden.value.trim() || skillsTextInput.value.trim(); + if (skills) queryParams.push("skills=" + encodeURIComponent(skills)); + + var level = document.getElementById("level").value; + if (level) queryParams.push("level=" + encodeURIComponent(level)); + + var interest = document.getElementById("interest").value; + if (interest) queryParams.push("interest=" + encodeURIComponent(interest)); + + var timeVal = document.getElementById("time").value; + if (timeVal) queryParams.push("time=" + encodeURIComponent(timeVal)); + + var techStack = getSelectedChips("tech-stack-group"); + if (techStack) queryParams.push("tech_stack=" + encodeURIComponent(techStack)); + + var prereqs = getSelectedChips("prereq-group"); + if (prereqs) queryParams.push("prerequisite_skills=" + encodeURIComponent(prereqs)); + + var minHours = document.getElementById("adv-min-hours").value.trim(); + if (minHours) queryParams.push("min_hours=" + encodeURIComponent(minHours)); + + var maxHours = document.getElementById("adv-max-hours").value.trim(); + if (maxHours) queryParams.push("max_hours=" + encodeURIComponent(maxHours)); + + var sortBy = document.getElementById("adv-sort").value; + if (sortBy) queryParams.push("sort_by=" + encodeURIComponent(sortBy)); + + var url = "/api/search?" + queryParams.join("&"); + + fetch(url) + .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; + } + // Refresh trending keywords list in real-time + fetchTrending(); + renderResults(data.projects || [], "No projects found matching your advanced search criteria. Try adjusting your filters.", true); + }) + .catch(function (err) { + setLoadingState(false); if (!validateForm()) return; //stop - anything missing/invalid setLoadingState(true); @@ -524,18 +577,45 @@ if (isIndexPage) { }); if (data.error) { var generalErr = document.getElementById("form-error-general"); - if (generalErr) generalErr.textContent = data.error; - return; - } - renderResults(data.projects || [], data.message); + if (generalErr) generalErr.textContent = "Something went wrong. Please try again."; + console.error("API request failed:", err); + }); + + } else { + // Legacy recommendation mode + if (!validateForm()) return; //stop - anything missing/invalid + + setLoadingState(true); + + 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) }) - .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."; - console.error("API request failed:", err); - }); + .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, false); + }) + .catch(function (err) { + setLoadingState(false); + var generalErr = document.getElementById("form-error-general"); + if (generalErr) generalErr.textContent = "Something went wrong. Please try again."; + console.error("API request failed:", err); + }); + } }); // Manages the loading state of the form and results section(whats visible or not) @@ -569,12 +649,32 @@ if (isIndexPage) { //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) { + function renderResults(projects, message, isAdvanced) { resultsSection.style.display = "block"; resultsLoadingEl.style.display = "none"; // Clear out any cards from a previous search before showing new ones resultsGrid.innerHTML = ""; + // Dynamically update result titles & badges + var titleEl = resultsSection.querySelector(".section-title"); + var subtitleEl = document.getElementById("results-subtitle"); + + if (isAdvanced) { + if (titleEl) { + titleEl.innerHTML = 'Search Results ' + projects.length + ' ' + (projects.length === 1 ? 'Project' : 'Projects') + ' Found'; + } + if (subtitleEl) { + subtitleEl.textContent = "We found these projects matching your advanced filters."; + } + } else { + if (titleEl) { + titleEl.innerHTML = 'Recommended Projects'; + } + if (subtitleEl) { + subtitleEl.textContent = "Based on your inputs, here are your top matches."; + } + } + if (!projects || projects.length === 0) { resultsGrid.style.display = "none"; resultsEmptyEl.style.display = "block"; @@ -634,6 +734,21 @@ if (isIndexPage) { // Time tag tagsRow.appendChild(createTag("Time: " + project.time, "time")); + // Add Estimated Hours tag if present + if (project.estimated_hours) { + tagsRow.appendChild(createTag(project.estimated_hours + " hrs", "hours")); + } + + // Add Views tag if present + if (project.views !== undefined) { + tagsRow.appendChild(createTag(project.views + " views", "views")); + } + + // Add Recommendations tag if present + if (project.recommendations !== undefined) { + tagsRow.appendChild(createTag(project.recommendations + " recs", "recs")); + } + // Footer with view-details link var footer = document.createElement("div"); footer.className = "project-card-footer"; @@ -670,6 +785,80 @@ if (isIndexPage) { return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; } + // ---------------------------------------------------------- + // Advanced Search Setup & Control Flow + // ---------------------------------------------------------- + var advToggleBtn = document.getElementById("btn-advanced-toggle"); + var advPanel = document.getElementById("advanced-search-panel"); + var trendingChips = document.getElementById("trending-chips"); + var isAdvancedMode = false; + + // Toggle advanced options panel + if (advToggleBtn && advPanel) { + advToggleBtn.addEventListener("click", function () { + isAdvancedMode = advPanel.classList.toggle("expanded"); + advToggleBtn.classList.toggle("active", isAdvancedMode); + advToggleBtn.setAttribute("aria-expanded", isAdvancedMode ? "true" : "false"); + if (btnLabel) { + btnLabel.textContent = isAdvancedMode ? "Search Projects" : "Generate My Projects"; + } + }); + } + + // Handle checkable tags/chips + document.querySelectorAll(".checkbox-chip-group .checkbox-chip").forEach(function (chip) { + chip.addEventListener("click", function () { + chip.classList.toggle("active"); + }); + }); + + function getSelectedChips(groupId) { + var group = document.getElementById(groupId); + if (!group) return ""; + var selected = []; + group.querySelectorAll(".checkbox-chip.active").forEach(function (chip) { + var val = chip.getAttribute("data-value"); + if (val) selected.push(val); + }); + return selected.join(","); + } + + // Fetch trending keywords from endpoint + function fetchTrending() { + if (!trendingChips) return; + fetch("/api/trending?limit=5") + .then(function (res) { return res.json(); }) + .then(function (data) { + trendingChips.innerHTML = ""; + if (data.trending && data.trending.length > 0) { + data.trending.forEach(function (term) { + var chip = document.createElement("span"); + chip.className = "trending-chip"; + chip.textContent = term; + chip.addEventListener("click", function () { + var kwInput = document.getElementById("adv-keyword-input"); + if (kwInput) { + kwInput.value = term; + // Expand panel if not already open + if (!isAdvancedMode && advToggleBtn) { + advToggleBtn.click(); + } + // Auto trigger search + form.dispatchEvent(new Event("submit")); + } + }); + trendingChips.appendChild(chip); + }); + } + }) + .catch(function (e) { + console.error("Failed to load trending items:", e); + }); + } + + // Initial call to load trending chips + fetchTrending(); + } // end isIndexPage diff --git a/static/style.css b/static/style.css index 13cd1c94..0e12e75f 100644 --- a/static/style.css +++ b/static/style.css @@ -2819,6 +2819,240 @@ select:focus { display: flex; } +/* ============================================================= + Advanced Search Styling + ============================================================= */ + +.advanced-search-trigger-wrap { + display: flex; + justify-content: center; + margin-top: 20px; + margin-bottom: 8px; +} + +.btn-advanced-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + background: transparent; + border: 1.5px solid var(--indigo-100); + color: var(--indigo-600); + font-family: var(--font-display); + font-size: 0.88rem; + font-weight: 700; + padding: 8px 18px; + border-radius: var(--r-full); + cursor: pointer; + transition: var(--t); +} + +.btn-advanced-toggle:hover { + background: var(--indigo-50); + border-color: var(--indigo-300); + transform: translateY(-1px); +} + +.btn-advanced-toggle .toggle-icon { + display: inline-block; + transition: transform var(--t); + font-size: 0.75rem; +} + +.btn-advanced-toggle.active .toggle-icon { + transform: rotate(180deg); +} + +.advanced-search-panel { + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height 0.4s cubic-bezier(0.16, 1, 0.3, 1), + opacity 0.3s ease, + margin var(--t), + padding var(--t); + background: var(--white); + border: 0px solid transparent; + border-radius: var(--r-md); + padding: 0 20px; + margin: 0; +} + +.advanced-search-panel.expanded { + max-height: 800px; + opacity: 1; + padding: 24px; + margin-top: 24px; + margin-bottom: 12px; + border: 1.5px solid var(--border); + background: var(--gray-50); +} + +.advanced-search-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 20px; +} + +@media (max-width: 768px) { + .advanced-search-grid { + grid-template-columns: 1fr; + gap: 16px; + } +} + +.checkbox-chip-group { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; +} + +.checkbox-chip { + display: inline-flex; + align-items: center; + padding: 6px 12px; + background: var(--white); + border: 1.5px solid var(--border); + border-radius: var(--r-full); + font-size: 0.8rem; + font-weight: 600; + color: var(--gray-700); + cursor: pointer; + transition: var(--t); + user-select: none; +} + +.checkbox-chip:hover { + border-color: var(--indigo-400); + background: var(--indigo-50); +} + +.checkbox-chip.active { + background: var(--indigo-600); + color: var(--white); + border-color: var(--indigo-600); + box-shadow: 0 2px 8px rgba(79, 110, 247, 0.28); +} + +.hours-range-wrap { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; +} + +.hours-input-box { + display: flex; + align-items: center; + background: var(--white); + border: 1.5px solid var(--border); + border-radius: var(--r-xs); + padding: 2px 8px; + width: 90px; +} + +.hours-input-box input { + width: 100%; + border: none; + outline: none; + font-size: 0.88rem; + font-family: var(--font-body); + color: var(--gray-800); + font-weight: 600; + text-align: center; +} + +.hours-range-to { + font-size: 0.85rem; + font-weight: 600; + color: var(--gray-400); +} + +/* Trending Searches */ +.trending-searches-wrap { + margin-top: 16px; + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 4px 0; +} + +.trending-title { + font-size: 0.75rem; + font-weight: 700; + color: var(--gray-400); + text-transform: uppercase; + letter-spacing: 0.06em; + white-space: nowrap; +} + +.trending-chips { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.trending-chip { + font-size: 0.78rem; + font-weight: 600; + color: var(--indigo-600); + background: var(--indigo-50); + border: 1px solid transparent; + padding: 4px 10px; + border-radius: var(--r-full); + cursor: pointer; + transition: var(--t); +} + +.trending-chip:hover { + background: var(--indigo-100); + border-color: var(--indigo-200); + transform: translateY(-1px); +} + +/* Result counts */ +.results-count-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px 12px; + background: var(--indigo-100); + color: var(--indigo-700); + font-size: 0.82rem; + font-weight: 700; + border-radius: var(--r-full); + margin-left: 12px; + vertical-align: middle; +} + +/* Project card stats tags */ +.project-tag--views { + background: #eff6ff !important; + color: #2563eb !important; +} + +.project-tag--recs { + background: #faf5ff !important; + color: #9333ea !important; +} + +.project-tag--hours { + background: #f0fdf4 !important; + color: #16a34a !important; +.noscript-warning { + background-color: #fff3cd; + color: #856404; + border: 1px solid #ffc107; + border-bottom: 2px solid #ffc107; + padding: 1rem 1.5rem; + margin-top: 64px; /* pushes below the fixed 64px navbar */ + text-align: center; + font-size: 1rem; + font-weight: 500; + width: 100%; /* Line-numbered code rows */ .code-line { diff --git a/templates/index.html b/templates/index.html index d4baee9c..aa256d13 100644 --- a/templates/index.html +++ b/templates/index.html @@ -520,6 +520,104 @@

Find Your Next Project

+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ Matches title, description, skills or tech stack. +
+ + +
+ +
+ +
+ Choose how matching projects are sorted. +
+
+ +
+ +
+ +
+ Python + JavaScript + HTML + CSS + Flask + pandas + Regex + matplotlib +
+ Multi-select technologies to include. +
+ + +
+ +
+ Basic Programming + Loops & Functions + HTML Basics + CSS Basics + Dictionaries + File Handling +
+ Select prerequisite knowledge levels. +
+
+ +
+ +
+ +
+
+ +
+ to +
+ +
+ hours +
+ Filter by specific hour commitments. +
+
+ +
+ + + +