Stash mostly working demo

This commit is contained in:
illyum 2024-09-05 23:01:35 -06:00
parent 0a6dce64ec
commit 5cebf50093
3 changed files with 161 additions and 50 deletions

View File

@ -5,6 +5,19 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Find Classmates</title> <title>Find Classmates</title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<style>
.class-shared {
font-weight: bold;
color: black;
}
.class-other {
color: gray;
}
.class-not-shared {
color: gray;
text-decoration: line-through;
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@ -36,14 +49,18 @@
<div class="matches-section"> <div class="matches-section">
<h2>People with Similar Classes</h2> <h2>People with Similar Classes</h2>
<div id="class-filter-container">
<p>Filter by classes:</p>
</div>
<div id="matches-container"> <div id="matches-container">
<p>No one has a similar class yet!</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script src="/static/theme.js"></script>
<script src="/static/util.js"></script> <script src="/static/util.js"></script>
</body> </body>
</html> </html>

56
main.go
View File

@ -79,6 +79,7 @@ func handleFormSubmit(w http.ResponseWriter, r *http.Request) {
// Insert the classes associated with this user // Insert the classes associated with this user
classList := strings.Split(classes, ",") classList := strings.Split(classes, ",")
for _, class := range classList { 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)) _, err := tx.Exec(`INSERT INTO classes (user_id, class_name) VALUES (?, ?)`, userID, strings.TrimSpace(class))
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
@ -261,59 +262,60 @@ func handleFormSubmit(w http.ResponseWriter, r *http.Request) {
func handleFetchMatches(w http.ResponseWriter, r *http.Request) { func handleFetchMatches(w http.ResponseWriter, r *http.Request) {
log.Println("[DEBUG] Entering handleFetchMatches handler") log.Println("[DEBUG] Entering handleFetchMatches handler")
// Parse the request body // Parse JSON request body
var requestData struct { var requestData struct {
Classes []string `json:"classes"` Classes []string `json:"classes"`
} }
err := json.NewDecoder(r.Body).Decode(&requestData) err := json.NewDecoder(r.Body).Decode(&requestData)
if err != nil { if err != nil || len(requestData.Classes) == 0 {
http.Error(w, "Invalid request body", http.StatusBadRequest) http.Error(w, "Invalid request data", http.StatusBadRequest)
return 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 := ` query := `
SELECT u.name, u.email, GROUP_CONCAT(c.class_name, ', ') AS classes SELECT u.name, u.email, GROUP_CONCAT(c.class_name) as class_list
FROM users u FROM users u
JOIN classes c ON u.id = c.user_id JOIN classes c ON u.id = c.user_id
WHERE c.class_name IN (` + strings.Repeat("?,", len(classes)-1) + `?)
GROUP BY u.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)) // Prepare the arguments for the query
for i, class := range classes { args := make([]interface{}, len(requestData.Classes))
args[i] = class for i, class := range requestData.Classes {
args[i] = strings.TrimSpace(class)
} }
// Execute the query
rows, err := db.Query(query, args...) rows, err := db.Query(query, args...)
if err != nil { if err != nil {
http.Error(w, "Database query failed", http.StatusInternalServerError) log.Printf("[ERROR] Failed to fetch matching users: %v\n", err)
log.Fatalf("[ERROR] Query failed: %v\n", err) http.Error(w, "Failed to fetch matches", http.StatusInternalServerError)
return return
} }
defer rows.Close() defer rows.Close()
var people []map[string]interface{} var matches []Match
for rows.Next() { for rows.Next() {
var name, email, classList string var match Match
if err := rows.Scan(&name, &email, &classList); err != nil { var classList string
log.Fatalf("[ERROR] Row scan failed: %v\n", err) if err := rows.Scan(&match.Name, &match.Email, &classList); err != nil {
return log.Printf("[ERROR] Failed to scan result row: %v\n", err)
continue
}
match.Classes = strings.Split(classList, ",")
matches = append(matches, match)
} }
person := map[string]interface{}{ // Return matches as JSON, always return an array, even if empty
"name": name,
"email": email,
"classes": strings.Split(classList, ", "),
}
people = append(people, person)
}
// Send the matching people as JSON response
w.Header().Set("Content-Type", "application/json") 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") log.Println("[DEBUG] Exiting handleFetchMatches handler")
} }

View File

@ -39,7 +39,9 @@ document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const tagsInput = document.getElementById('classes-input'); 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) { function addClass(classname) {
const container = document.getElementById('classes-container'); const container = document.getElementById('classes-container');
@ -53,12 +55,114 @@ document.addEventListener('DOMContentLoaded', function () {
closeIcon.addEventListener('click', function () { closeIcon.addEventListener('click', function () {
container.removeChild(classElement); container.removeChild(classElement);
classes = classes.filter(c => c !== classname); classes = classes.filter(c => c !== classname);
fetchMatchingPeople(); // Fetch updated matches when a class is removed
}); });
classElement.appendChild(closeIcon); classElement.appendChild(closeIcon);
container.appendChild(classElement); container.appendChild(classElement);
} }
function fetchMatchingPeople() {
if (classes.length === 0) {
matchesContainer.innerHTML = '<p>No one has a similar class yet!</p>';
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 = '<p>No one has that class yet!</p>';
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 = '<p>No one has that class yet!</p>'; // 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 `<span class="class-shared">${classname}</span>`;
} else {
// Non-shared classes: gray out
return `<span class="class-other">${classname}</span>`;
}
}).join(', ');
// Display the person's name, email, and full class list
personDiv.innerHTML = `<strong>${person.name}</strong><br>Classes: ${classesHtml}<br>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) { tagsInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ',') { if (event.key === 'Enter' || event.key === ',') {
event.preventDefault(); event.preventDefault();
@ -66,22 +170,10 @@ document.addEventListener('DOMContentLoaded', function () {
if (value && !classes.includes(value)) { // Check for empty and duplicate values if (value && !classes.includes(value)) { // Check for empty and duplicate values
addClass(value); addClass(value);
classes.push(value); classes.push(value);
renderClassFilters(); // Update the class filters UI
fetchMatchingPeople(); // Fetch updated matches when a class is added
tagsInput.value = ''; // Clear the input 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
});
}); });