Stash mostly working demo
This commit is contained in:
parent
0a6dce64ec
commit
5cebf50093
21
index.html
21
index.html
@ -5,6 +5,19 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Find Classmates</title>
|
||||
<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>
|
||||
<body>
|
||||
<div class="container">
|
||||
@ -36,14 +49,18 @@
|
||||
|
||||
<div class="matches-section">
|
||||
<h2>People with Similar Classes</h2>
|
||||
<div id="class-filter-container">
|
||||
<p>Filter by classes:</p>
|
||||
</div>
|
||||
<div id="matches-container">
|
||||
|
||||
<p>No one has a similar class yet!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/theme.js"></script>
|
||||
<script src="/static/util.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
62
main.go
62
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")
|
||||
}
|
||||
|
128
static/util.js
128
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 = '<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) {
|
||||
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
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user