From 5cebf5009319ce35755cc24e170883f42530c670 Mon Sep 17 00:00:00 2001 From: illyum Date: Thu, 5 Sep 2024 23:01:35 -0600 Subject: [PATCH] Stash mostly working demo --- index.html | 21 +++++++- main.go | 62 ++++++++++++------------ static/util.js | 128 ++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 161 insertions(+), 50 deletions(-) diff --git a/index.html b/index.html index 6f81081..d6b49ee 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,19 @@ Find Classmates +
@@ -36,14 +49,18 @@

People with Similar Classes

+
+

Filter by classes:

+
- +

No one has a similar class yet!

- + + diff --git a/main.go b/main.go index 1f19363..0ff0c2f 100644 --- a/main.go +++ b/main.go @@ -79,6 +79,7 @@ func handleFormSubmit(w http.ResponseWriter, r *http.Request) { // Insert the classes associated with this user classList := strings.Split(classes, ",") for _, class := range classList { + log.Printf("[DEBUG] Proccessing class: %s\n", class) _, err := tx.Exec(`INSERT INTO classes (user_id, class_name) VALUES (?, ?)`, userID, strings.TrimSpace(class)) if err != nil { tx.Rollback() @@ -261,59 +262,60 @@ func handleFormSubmit(w http.ResponseWriter, r *http.Request) { func handleFetchMatches(w http.ResponseWriter, r *http.Request) { log.Println("[DEBUG] Entering handleFetchMatches handler") - // Parse the request body + // Parse JSON request body var requestData struct { Classes []string `json:"classes"` } err := json.NewDecoder(r.Body).Decode(&requestData) - if err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + if err != nil || len(requestData.Classes) == 0 { + http.Error(w, "Invalid request data", http.StatusBadRequest) return } - classes := requestData.Classes + log.Printf("[DEBUG] Classes to match: %v\n", requestData.Classes) - // Prepare the SQL query to find matching users based on classes + // Prepare query to fetch users who share any of the classes query := ` - SELECT u.name, u.email, GROUP_CONCAT(c.class_name, ', ') AS classes - FROM users u - JOIN classes c ON u.id = c.user_id - WHERE c.class_name IN (` + strings.Repeat("?,", len(classes)-1) + `?) - GROUP BY u.id - ` + SELECT u.name, u.email, GROUP_CONCAT(c.class_name) as class_list + FROM users u + JOIN classes c ON u.id = c.user_id + GROUP BY u.id + HAVING COUNT(CASE WHEN c.class_name IN (` + strings.Trim(strings.Repeat("?,", len(requestData.Classes)), ",") + `) THEN 1 END) > 0; + ` - args := make([]interface{}, len(classes)) - for i, class := range classes { - args[i] = class + // Prepare the arguments for the query + args := make([]interface{}, len(requestData.Classes)) + for i, class := range requestData.Classes { + args[i] = strings.TrimSpace(class) } + // Execute the query rows, err := db.Query(query, args...) if err != nil { - http.Error(w, "Database query failed", http.StatusInternalServerError) - log.Fatalf("[ERROR] Query failed: %v\n", err) + log.Printf("[ERROR] Failed to fetch matching users: %v\n", err) + http.Error(w, "Failed to fetch matches", http.StatusInternalServerError) return } defer rows.Close() - var people []map[string]interface{} + var matches []Match for rows.Next() { - var name, email, classList string - if err := rows.Scan(&name, &email, &classList); err != nil { - log.Fatalf("[ERROR] Row scan failed: %v\n", err) - return + var match Match + var classList string + if err := rows.Scan(&match.Name, &match.Email, &classList); err != nil { + log.Printf("[ERROR] Failed to scan result row: %v\n", err) + continue } - - person := map[string]interface{}{ - "name": name, - "email": email, - "classes": strings.Split(classList, ", "), - } - people = append(people, person) + match.Classes = strings.Split(classList, ",") + matches = append(matches, match) } - // Send the matching people as JSON response + // Return matches as JSON, always return an array, even if empty w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(people) + if err := json.NewEncoder(w).Encode(matches); err != nil { + log.Printf("[ERROR] Failed to encode matches: %v\n", err) + http.Error(w, "Failed to return matches", http.StatusInternalServerError) + } log.Println("[DEBUG] Exiting handleFetchMatches handler") } diff --git a/static/util.js b/static/util.js index faa809e..723d335 100644 --- a/static/util.js +++ b/static/util.js @@ -39,7 +39,9 @@ document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () { const tagsInput = document.getElementById('classes-input'); - let classes = []; + const matchesContainer = document.getElementById('matches-container'); + let classes = []; // Stores the classes entered by the user + let fetchedPeople = []; // Stores the people returned from the server function addClass(classname) { const container = document.getElementById('classes-container'); @@ -53,35 +55,125 @@ document.addEventListener('DOMContentLoaded', function () { closeIcon.addEventListener('click', function () { container.removeChild(classElement); classes = classes.filter(c => c !== classname); + fetchMatchingPeople(); // Fetch updated matches when a class is removed }); classElement.appendChild(closeIcon); container.appendChild(classElement); } + function fetchMatchingPeople() { + if (classes.length === 0) { + matchesContainer.innerHTML = '

No one has a similar class yet!

'; + return; + } + + fetch("/fetch-matches", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ classes: classes }) + }) + .then(response => { + if (!response.ok) { + throw new Error("Network response was not OK"); + } + return response.json(); + }) + .then(data => { + // Handle null or empty data + if (!data || data.length === 0) { + matchesContainer.innerHTML = '

No one has that class yet!

'; + return; + } + + fetchedPeople = data; // Store the fetched people + matchesContainer.innerHTML = ''; // Clear the matches container + + renderPeople(fetchedPeople, classes); // Render matches + }) + .catch(error => { + console.error("Error fetching matches:", error); + matchesContainer.innerHTML = '

No one has that class yet!

'; // Fallback message in case of any error + }); + } + + + function renderPeople(people, filterClasses = []) { + matchesContainer.innerHTML = ''; // Clear current people list + + people.forEach(person => { + const personDiv = document.createElement('div'); + personDiv.classList.add('person'); + + // Render all classes: shared ones in bold, others in gray + let classesHtml = person.classes.map(classname => { + // Check if the classname is in the currently selected filters + if (filterClasses.includes(classname.trim())) { + // Shared classes: bold + return `${classname}`; + } else { + // Non-shared classes: gray out + return `${classname}`; + } + }).join(', '); + + // Display the person's name, email, and full class list + personDiv.innerHTML = `${person.name}
Classes: ${classesHtml}
Email: ${person.email || 'N/A'}`; + matchesContainer.appendChild(personDiv); + }); + } + + + function renderClassFilters() { + const filterContainer = document.getElementById('class-filter-container'); + filterContainer.innerHTML = ''; // Clear existing filters + + classes.forEach(classname => { + const filterLabel = document.createElement('label'); + const filterCheckbox = document.createElement('input'); + filterCheckbox.type = 'checkbox'; + filterCheckbox.value = classname; + filterCheckbox.addEventListener('change', function () { + applyClassFilter(); // Apply filter when a checkbox is checked/unchecked + }); + + filterLabel.appendChild(filterCheckbox); + filterLabel.appendChild(document.createTextNode(classname)); + filterContainer.appendChild(filterLabel); + }); + } + + function applyClassFilter() { + // Get the list of currently checked classes from the checkboxes + const selectedClasses = Array.from(document.querySelectorAll('#class-filter-container input:checked')) + .map(input => input.value); + + // If no checkboxes are selected, use all classes (to keep shared ones bold) + const filterClasses = selectedClasses.length === 0 ? classes : selectedClasses; + + // Filter people by selected classes, or show all people if no classes are checked + const filteredPeople = fetchedPeople.filter(person => + selectedClasses.length === 0 || selectedClasses.some(cls => person.classes.includes(cls)) + ); + + // Render the people with the selected filters and apply the shared class highlighting logic + renderPeople(filteredPeople, filterClasses); + } + + tagsInput.addEventListener('keydown', function (event) { if (event.key === 'Enter' || event.key === ',') { event.preventDefault(); let value = tagsInput.value.trim().replace(/,$/, ''); if (value && !classes.includes(value)) { // Check for empty and duplicate values - addClass(value); - classes.push(value); - tagsInput.value = ''; // Clear the input + addClass(value); + classes.push(value); + renderClassFilters(); // Update the class filters UI + fetchMatchingPeople(); // Fetch updated matches when a class is added + tagsInput.value = ''; // Clear the input } } }); - - document.querySelector('form').addEventListener('submit', function (event) { - const hiddenClassesInput = document.createElement('input'); - hiddenClassesInput.type = 'hidden'; - hiddenClassesInput.name = 'classes'; - - const cleanedClasses = classes.filter(classname => classname.trim() !== "").map(classname => classname.trim()); - hiddenClassesInput.value = cleanedClasses.join(','); - - console.log("Hidden input value being submitted: ", hiddenClassesInput.value); // Log the value - - this.appendChild(hiddenClassesInput); - console.log("Form data before submission: ", new FormData(this)); // Log the form data - }); });