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">
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
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
|
// 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, ",")
|
||||||
person := map[string]interface{}{
|
matches = append(matches, match)
|
||||||
"name": name,
|
|
||||||
"email": email,
|
|
||||||
"classes": strings.Split(classList, ", "),
|
|
||||||
}
|
|
||||||
people = append(people, person)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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")
|
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")
|
||||||
}
|
}
|
||||||
|
128
static/util.js
128
static/util.js
@ -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,35 +55,125 @@ 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();
|
||||||
let value = tagsInput.value.trim().replace(/,$/, '');
|
let value = tagsInput.value.trim().replace(/,$/, '');
|
||||||
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);
|
||||||
tagsInput.value = ''; // Clear the input
|
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