diff --git a/.gitignore b/.gitignore index 89060cf..c83bba1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,122 +1,122 @@ -# Miscellaneous -*.class -*.lock -*.log -*.pyc -*.swp -.buildlog/ -.history - - - -# Flutter repo-specific -/bin/cache/ -/bin/internal/bootstrap.bat -/bin/internal/bootstrap.sh -/bin/mingit/ -/dev/benchmarks/mega_gallery/ -/dev/bots/.recipe_deps -/dev/bots/android_tools/ -/dev/devicelab/ABresults*.json -/dev/docs/doc/ -/dev/docs/flutter.docs.zip -/dev/docs/lib/ -/dev/docs/pubspec.yaml -/dev/integration_tests/**/xcuserdata -/dev/integration_tests/**/Pods -/packages/flutter/coverage/ -version -analysis_benchmark.json - -# packages file containing multi-root paths -.packages.generated - -# Flutter/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -**/generated_plugin_registrant.dart -.packages -.pub-preload-cache/ -.pub/ -build/ -flutter_*.png -linked_*.ds -unlinked.ds -unlinked_spec.ds - -# Android related -**/android/**/gradle-wrapper.jar -.gradle/ -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java -**/android/key.properties -*.jks - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/.last_build_id -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/ephemeral -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# macOS -**/Flutter/ephemeral/ -**/Pods/ -**/macos/Flutter/GeneratedPluginRegistrant.swift -**/macos/Flutter/ephemeral -**/xcuserdata/ - -# Windows -**/windows/flutter/generated_plugin_registrant.cc -**/windows/flutter/generated_plugin_registrant.h -**/windows/flutter/generated_plugins.cmake - -# Linux -**/linux/flutter/generated_plugin_registrant.cc -**/linux/flutter/generated_plugin_registrant.h -**/linux/flutter/generated_plugins.cmake - -# Coverage -coverage/ - -# Symbols -app.*.symbols - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -!/dev/ci/**/Gemfile.lock.venv/ -# Python virtual environment -venv/ -.venv/ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.buildlog/ +.history + + + +# Flutter repo-specific +/bin/cache/ +/bin/internal/bootstrap.bat +/bin/internal/bootstrap.sh +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-preload-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +.gradle/ +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock.venv/ +# Python virtual environment +venv/ +.venv/ diff --git a/static/script.js b/static/script.js index 28dbbeb..b0a0926 100644 --- a/static/script.js +++ b/static/script.js @@ -453,6 +453,70 @@ updateProfileWidgets(); return valid; } + + + // ---------------------------------------------------------- + // Form submission and API call + // ---------------------------------------------------------- + + form.addEventListener("submit", function (evt) { + evt.preventDefault(); //stop the browser from reloading the page on form submit + clearAllErrors(); + + if (skillsTextInput.value.trim()) { + addSkill(skillsTextInput.value); + skillsTextInput.value = ""; + hideSuggestions(); + } + + if (!validateForm()) return; //stop - anything missing/invalid + + setLoadingState(true); + + // Allow browser to paint spinner before request starts + requestAnimationFrame(function () { + + //combine form values into an object to send to server/api + var payload = { + // Prefer the hidden input value; fall back to raw text box if hidden input is empty + skills: skillsHidden.value.trim() || skillsTextInput.value.trim(), + level: document.getElementById("level").value, + interest: document.getElementById("interest").value, + time: document.getElementById("time").value + }; + + //post the data to backend api as JSON, then handle the response + fetch("/api/recommend", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) //convert object to json string + }) + .then(function (res) { + return res.json(); //parse the response as 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); + }) + .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); + }); + }); + }); + + + // Manages the loading state of the form and results section(whats visible or not) function setLoadingState(isLoading) { submitBtn.disabled = isLoading; submitBtn.setAttribute("aria-busy", isLoading ? "true" : "false"); @@ -466,9 +530,56 @@ updateProfileWidgets(); resultsSection.scrollIntoView({ behavior: "smooth" }); } else { resultsLoadingEl.style.display = "none"; + resultsGrid.style.display = "grid"; //switch back to grid layout } } + + // ---------------------------------------------------------- + // Render result cards + // ---------------------------------------------------------- + + //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) { //if no projects returned from api, show the "no results" message and hide the grid + resultsGrid.style.display = "none"; + resultsEmptyEl.style.display = "block"; + + var interestEl = document.getElementById("interest"); + var selectedInterest = interestEl ? interestEl.value : null; + + // Show a friendly custom message when the user selected an interest + if (emptyMessageEl) { + 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" }); + } + function truncate(text, maxLength) { text = text || ""; return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;