From 1d5c6bfdbb60f710ced6cb1705ff910a7e4f9ec7 Mon Sep 17 00:00:00 2001 From: pelin-sayar Date: Sun, 2 Nov 2025 14:35:51 -0500 Subject: [PATCH 1/8] autocomplete backend, js frontend, some html (needs work) --- tcf_website/api/serializers.py | 23 ++ tcf_website/static/search/autocomplete.js | 144 ++++++++ tcf_website/templates/search/searchbar.html | 359 ++++++++++---------- tcf_website/urls.py | 2 + tcf_website/views/__init__.py | 2 +- tcf_website/views/search.py | 59 +++- 6 files changed, 411 insertions(+), 178 deletions(-) create mode 100644 tcf_website/static/search/autocomplete.js diff --git a/tcf_website/api/serializers.py b/tcf_website/api/serializers.py index d7d66f7df..c3702e8d5 100644 --- a/tcf_website/api/serializers.py +++ b/tcf_website/api/serializers.py @@ -152,6 +152,18 @@ class Meta: ] +class CourseAutocompleteSerializer(serializers.ModelSerializer): + """DRF Serializer for autocomplete course""" + + subdepartment = serializers.CharField( + source="subdepartment.mnemonic", read_only=True + ) + + class Meta: + model = Course + fields = ["id", "title", "number", "subdepartment"] + + class InstructorSerializer(serializers.ModelSerializer): """DRF Serializer for Instructor""" @@ -160,6 +172,17 @@ class Meta: fields = "__all__" +class InstructorAutocompleteSerializer(serializers.ModelSerializer): + """DRF Serializer for autocomplete instructor""" + + class Meta: + model = Instructor + fields = [ + "id", + "full_name", + ] + + class ClubCategorySerializer(serializers.ModelSerializer): """DRF Serializer for ClubCategory""" diff --git a/tcf_website/static/search/autocomplete.js b/tcf_website/static/search/autocomplete.js new file mode 100644 index 000000000..f88c68b30 --- /dev/null +++ b/tcf_website/static/search/autocomplete.js @@ -0,0 +1,144 @@ +document.addEventListener("DOMContentLoaded", function () { + const searchInput = document.getElementById("search-input"); + if (!searchInput) return; // Prevents errors if the element doesn't exist + + // Create container for autocomplete suggestions + const suggestionsContainer = document.createElement("div"); + suggestionsContainer.classList.add("autocomplete-suggestions"); + + // Ensure parent node exists before modifying + if (searchInput.parentNode) { + searchInput.parentNode.style.position = "relative"; // Ensure dropdown aligns properly + searchInput.parentNode.appendChild(suggestionsContainer); + } else { + console.warn("Search input has no parent node for autocomplete"); + return; + } + + let debounceTimeout = null; + let currentRequestController = null; + + // Debounce to reduce API calls + function debounce(func, delay) { + return function (...args) { + clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(() => func(...args), delay); + }; + } + + // Fetch suggestions from backend + async function fetchSuggestions(query) { + if (!query.trim()) { // Ignores empty or whitespace queries + clearSuggestions(); + return; + } + + // Cancel previous request if still in-flight + if (currentRequestController) { + currentRequestController.abort(); + } + + currentRequestController = new AbortController(); + + try { + const response = await fetch( + `/api/autocomplete/?q=${encodeURIComponent(query)}`, + { signal: currentRequestController.signal } + ); + + if (!response.ok) { + clearSuggestions(); + return; + } + + const data = await response.json(); + renderSuggestions(data); + } catch (error) { + if (error.name !== "AbortError") { + console.error("Autocomplete fetch error:", error); + } + clearSuggestions(); + } + } + + // Render suggestions + function renderSuggestions(data) { + suggestionsContainer.innerHTML = ""; + + const hasCourses = data?.courses?.length > 0; + const hasInstructors = data?.instructors?.length > 0; + + if (!hasCourses && !hasInstructors) { + suggestionsContainer.style.display = "none"; + return; + } + + // Add group headers for clarity + if (hasCourses) { + const header = document.createElement("div"); + header.classList.add("autocomplete-header"); + header.textContent = "Courses"; + suggestionsContainer.appendChild(header); + } + + // Courses first + data.courses.forEach((course) => { + const item = document.createElement("ul"); + item.classList.add("autocomplete-item"); + item.textContent = `${course.subdepartment} ${course.number} — ${course.title}`; + item.addEventListener("click", () => { + searchInput.value = `${course.subdepartment} ${course.number}`; + clearSuggestions(); + if (searchInput.form) { + searchInput.form.submit(); + } + }); + suggestionsContainer.appendChild(item); + }); + + if (hasInstructors) { + const header = document.createElement("div"); + header.classList.add("autocomplete-header"); + header.textContent = "Instructors"; + suggestionsContainer.appendChild(header); + } + + // Instructor results + data.instructors.forEach((instructor) => { + const item = document.createElement("ul"); + item.classList.add("autocomplete-item"); + item.textContent = instructor.full_name; + item.addEventListener("click", () => { + searchInput.value = instructor.full_name; + clearSuggestions(); + if (searchInput.form) { + searchInput.form.submit(); + } + }); + suggestionsContainer.appendChild(item); + }); + + suggestionsContainer.style.display = "block"; + } + + // Helper to clear dropdown + function clearSuggestions() { + suggestionsContainer.innerHTML = ""; + suggestionsContainer.style.display = "none"; + } + + // Hide dropdown when clicking outside + document.addEventListener("click", (event) => { + if (!suggestionsContainer.contains(event.target) && event.target !== searchInput) { + clearSuggestions(); + } + }); + + // Debounced input listener + searchInput.addEventListener( + "input", + debounce((event) => { + fetchSuggestions(event.target.value); + }, 250) + ); +}); diff --git a/tcf_website/templates/search/searchbar.html b/tcf_website/templates/search/searchbar.html index 1851ad5cd..53da21689 100644 --- a/tcf_website/templates/search/searchbar.html +++ b/tcf_website/templates/search/searchbar.html @@ -2,209 +2,216 @@ - -
-
- -
- - {% include "../club/mode_toggle.html" with is_club=is_club toggle_type="radio" no_transition=True container_class="search-mode-toggle" %} - - -
-