diff --git a/.gitignore b/.gitignore index c537ea5..44bfe18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,13 @@ +# Don't commit git +.git + # Remove database formdata.db + +# tmp files +output.txt +typer.py +README.md +go.mod +go.sum +demo.html \ No newline at end of file diff --git a/admin.html b/admin.html index d0727a5..121277d 100644 --- a/admin.html +++ b/admin.html @@ -10,23 +10,26 @@

Submitted Answers

- + + {{range .}} + {{else}} - + {{end}}
Question AnswerTags
{{.QuestionText}} {{.Answer}}{{.Tags}}
No submissions found.No submissions found.
+
diff --git a/index.html b/index.html index 9729640..da37a5f 100644 --- a/index.html +++ b/index.html @@ -3,23 +3,47 @@ - Form + Find Classmates
-

Submit Your Information

- -
- {{range .}} - -

- {{end}} +
+
+

Enter Your Information

+ + + +

- - + +

+ + +

+ + +

+ + +
+ +


+ + + +
+ +
+

People with Similar Classes

+
+ +
+
+
+ diff --git a/main.go b/main.go index 1352837..8510555 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,12 @@ package main import ( "database/sql" + "encoding/json" "fmt" "html/template" "log" "net/http" + "strings" _ "github.com/mattn/go-sqlite3" ) @@ -14,225 +16,351 @@ type Question struct { ID int QuestionText string QuestionType string + Options []string +} + +type Match struct { + Name string `json:"name"` + Email string `json:"email"` + Classes []string `json:"classes"` } var db *sql.DB func renderForm(w http.ResponseWriter, r *http.Request) { - rows, err := db.Query("SELECT id, question_text, question_type FROM questions ORDER BY question_order") - if err != nil { - log.Printf("Error fetching questions: %v\n", err) - http.Error(w, "Failed to fetch form questions", http.StatusInternalServerError) - return - } - defer rows.Close() - - var questions []Question - - for rows.Next() { - var question Question - err := rows.Scan(&question.ID, &question.QuestionText, &question.QuestionType) - if err != nil { - log.Printf("Error scanning question: %v\n", err) - continue - } - questions = append(questions, question) - } - + log.Println("[DEBUG] Entering renderForm handler") tmpl := template.Must(template.ParseFiles("index.html")) - tmpl.Execute(w, questions) + log.Println("[DEBUG] Rendering template with questions") + err := tmpl.Execute(w, nil) + if err != nil { + log.Println("[ERROR] Error rendering template:", err) + } } func handleFormSubmit(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + log.Println("[DEBUG] Entering handleFormSubmit handler") + + // Parse the form data + if err := r.ParseForm(); err != nil { + http.Error(w, "Invalid form data", http.StatusBadRequest) return } - err := r.ParseForm() - if err != nil { - log.Printf("Error parsing form: %v\n", err) - http.Error(w, "Failed to parse form", http.StatusInternalServerError) - return - } - insertAnswerQuery := `INSERT INTO answers (question_id, answer) VALUES (?, ?)` + name := r.FormValue("name") + email := r.FormValue("email") + phone := r.FormValue("phone") + dorm := r.FormValue("dorm") + classes := r.FormValue("classes") + // Insert the user data into the database tx, err := db.Begin() if err != nil { - log.Printf("Error starting transaction: %v\n", err) - http.Error(w, "Failed to save answers", http.StatusInternalServerError) + log.Fatalf("[ERROR] Failed to begin transaction: %v\n", err) return } - defer func() { + + result, err := tx.Exec(`INSERT INTO users (name, email, phone, dorm) VALUES (?, ?, ?, ?)`, name, email, phone, dorm) + if err != nil { + tx.Rollback() + log.Fatalf("[ERROR] Failed to insert user: %v\n", err) + return + } + + userID, err := result.LastInsertId() + if err != nil { + tx.Rollback() + log.Fatalf("[ERROR] Failed to get last insert ID: %v\n", err) + return + } + + // Insert the classes associated with this user + classList := strings.Split(classes, ",") + for _, class := range classList { + _, err := tx.Exec(`INSERT INTO classes (user_id, class_name) VALUES (?, ?)`, userID, strings.TrimSpace(class)) if err != nil { tx.Rollback() - } else { - tx.Commit() - } - }() - - for key, values := range r.Form { - if len(key) > 7 && key[:7] == "custom_" { - var questionID int - _, err := fmt.Sscanf(key, "custom_%d", &questionID) - if err != nil { - log.Printf("Error parsing question ID from field name %s: %v\n", key, err) - continue - } - - answer := values[0] - _, err = tx.Exec(insertAnswerQuery, questionID, answer) - if err != nil { - log.Printf("Error saving answer for question %d: %v\n", questionID, err) - http.Error(w, "Failed to save answers", http.StatusInternalServerError) - return - } + log.Fatalf("[ERROR] Failed to insert class: %v\n", err) + return } } + + err = tx.Commit() + if err != nil { + log.Fatalf("[ERROR] Failed to commit transaction: %v\n", err) + return + } + + log.Println("[DEBUG] User and classes inserted successfully") fmt.Fprintf(w, "Thank you for your submission!") + log.Println("[DEBUG] Exiting handleFormSubmit handler") } -func handleAdmin(w http.ResponseWriter, r *http.Request) { - rows, err := db.Query(` - SELECT q.question_text, a.answer - FROM answers a - JOIN questions q ON a.question_id = q.id - ORDER BY q.question_order - `) +// func handleAdmin(w http.ResponseWriter, r *http.Request) { +// log.Println("[DEBUG] Entering handleAdmin handler") +// rows, err := db.Query(` +// SELECT q.question_text, a.answer, GROUP_CONCAT(t.tag, ', ') as tags +// FROM answers a +// JOIN questions q ON a.question_id = q.id +// LEFT JOIN tags t ON a.id = t.answer_id +// GROUP BY a.id +// ORDER BY q.question_order +// `) +// if err != nil { +// log.Printf("[ERROR] Error fetching submissions: %v\n", err) +// http.Error(w, "Failed to fetch submissions", http.StatusInternalServerError) +// return +// } +// defer rows.Close() +// +// var submissions []struct { +// QuestionText string +// Answer string +// Tags string +// } +// log.Println("[DEBUG] Fetching submissions") +// +// for rows.Next() { +// var submission struct { +// QuestionText string +// Answer string +// Tags string +// } +// err := rows.Scan(&submission.QuestionText, &submission.Answer, &submission.Tags) +// if err != nil { +// log.Printf("[ERROR] Error scanning submission: %v\n", err) +// continue +// } +// submissions = append(submissions, submission) +// log.Printf("[DEBUG] Fetched submission: %v\n", submission) +// } +// +// tmpl := template.Must(template.ParseFiles("admin.html")) +// log.Println("[DEBUG] Rendering admin template with submissions") +// tmpl.Execute(w, submissions) +// } +// +// func handleAddQuestion(w http.ResponseWriter, r *http.Request) { +// log.Println("[DEBUG] Entering handleAddQuestion handler") +// if r.Method != http.MethodPost { +// log.Printf("[ERROR] Invalid request method: %v\n", r.Method) +// http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) +// return +// } +// +// r.ParseForm() +// questionText := r.FormValue("question_text") +// questionType := r.FormValue("question_type") +// options := r.FormValue("options") +// +// log.Printf("[DEBUG] Adding new question: %s, Type: %s\n", questionText, questionType) +// +// insertQuery := `INSERT INTO questions (question_text, question_type, question_order) VALUES (?, ?, (SELECT IFNULL(MAX(question_order), 0) + 1 FROM questions));` +// res, err := db.Exec(insertQuery, questionText, questionType) +// if err != nil { +// log.Printf("[ERROR] Error adding new question: %v\n", err) +// http.Error(w, "Failed to add question", http.StatusInternalServerError) +// return +// } +// +// if questionType == "multiple_choice" || questionType == "single_choice" || questionType == "dropdown" || questionType == "tags" { +// questionID, err := res.LastInsertId() +// if err != nil { +// log.Printf("[ERROR] Error getting question ID: %v\n", err) +// http.Error(w, "Failed to add question", http.StatusInternalServerError) +// return +// } +// log.Printf("[DEBUG] Inserted question ID: %d\n", questionID) +// +// optionsList := strings.Split(options, ",") +// for _, option := range optionsList { +// option = strings.TrimSpace(option) +// log.Printf("[DEBUG] Adding option: %s to question ID: %d\n", option, questionID) +// _, err = db.Exec("INSERT INTO question_options (question_id, option_text) VALUES (?, ?)", questionID, option) +// if err != nil { +// log.Printf("[ERROR] Error adding options: %v\n", err) +// http.Error(w, "Failed to add options", http.StatusInternalServerError) +// return +// } +// } +// } +// +// log.Println("[DEBUG] Redirecting to manage page") +// http.Redirect(w, r, "/manage", http.StatusSeeOther) +// } +// +// func handleManage(w http.ResponseWriter, r *http.Request) { +// log.Println("[DEBUG] Entering handleManage handler") +// rows, err := db.Query("SELECT id, question_text, question_type FROM questions ORDER BY question_order") +// if err != nil { +// log.Printf("[ERROR] Error fetching questions: %v\n", err) +// http.Error(w, "Failed to fetch questions", http.StatusInternalServerError) +// return +// } +// defer rows.Close() +// +// var questions []Question +// log.Println("[DEBUG] Fetching questions for management") +// +// for rows.Next() { +// var question Question +// err := rows.Scan(&question.ID, &question.QuestionText, &question.QuestionType) +// if err != nil { +// log.Printf("[ERROR] Error scanning question: %v\n", err) +// continue +// } +// log.Printf("[DEBUG] Found question: %v\n", question) +// +// if question.QuestionType == "multiple_choice" || question.QuestionType == "single_choice" || question.QuestionType == "dropdown" { +// optionRows, err := db.Query("SELECT option_text FROM question_options WHERE question_id = ?", question.ID) +// if err != nil { +// log.Printf("[ERROR] Error fetching options: %v\n", err) +// continue +// } +// defer optionRows.Close() +// +// for optionRows.Next() { +// var optionText string +// err = optionRows.Scan(&optionText) +// if err != nil { +// log.Printf("[ERROR] Error scanning option: %v\n", err) +// continue +// } +// question.Options = append(question.Options, optionText) +// log.Printf("[DEBUG] Found option: %s\n", optionText) +// } +// } +// +// questions = append(questions, question) +// } +// +// tmpl := template.Must(template.ParseFiles("manage.html")) +// log.Println("[DEBUG] Rendering manage template with questions") +// tmpl.Execute(w, questions) +// } +// +// func handleRemoveQuestion(w http.ResponseWriter, r *http.Request) { +// log.Println("[DEBUG] Entering handleRemoveQuestion handler") +// questionID := r.URL.Query().Get("id") +// log.Printf("[DEBUG] Removing question with ID: %s\n", questionID) +// +// deleteQuery := `DELETE FROM questions WHERE id = ?` +// _, err := db.Exec(deleteQuery, questionID) +// if err != nil { +// log.Printf("[ERROR] Error removing question: %v\n", err) +// http.Error(w, "Failed to remove question", http.StatusInternalServerError) +// return +// } +// +// log.Println("[DEBUG] Redirecting to manage page after deletion") +// http.Redirect(w, r, "/manage", http.StatusSeeOther) +// } + +func handleFetchMatches(w http.ResponseWriter, r *http.Request) { + log.Println("[DEBUG] Entering handleFetchMatches handler") + + // Parse the request body + var requestData struct { + Classes []string `json:"classes"` + } + err := json.NewDecoder(r.Body).Decode(&requestData) if err != nil { - log.Printf("Error fetching submissions: %v\n", err) - http.Error(w, "Failed to fetch submissions", http.StatusInternalServerError) + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + classes := requestData.Classes + + // Prepare the SQL query to find matching users based on 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 + ` + + args := make([]interface{}, len(classes)) + for i, class := range classes { + args[i] = class + } + + 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) return } defer rows.Close() - var submissions []struct { - QuestionText string - Answer string - } - + var people []map[string]interface{} for rows.Next() { - var submission struct { - QuestionText string - Answer string + var name, email, classList string + if err := rows.Scan(&name, &email, &classList); err != nil { + log.Fatalf("[ERROR] Row scan failed: %v\n", err) + return } - err := rows.Scan(&submission.QuestionText, &submission.Answer) - if err != nil { - log.Printf("Error scanning submission: %v\n", err) - continue + + person := map[string]interface{}{ + "name": name, + "email": email, + "classes": strings.Split(classList, ", "), } - submissions = append(submissions, submission) + people = append(people, person) } - tmpl := template.Must(template.ParseFiles("admin.html")) - tmpl.Execute(w, submissions) -} + // Send the matching people as JSON response + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(people) -func handleAddQuestion(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) - return - } - - r.ParseForm() - questionText := r.FormValue("question_text") - - insertQuery := `INSERT INTO questions (question_text, question_type, question_order) VALUES (?, 'text', (SELECT IFNULL(MAX(question_order), 0) + 1 FROM questions));` - _, err := db.Exec(insertQuery, questionText) - if err != nil { - log.Printf("Error adding new question: %v\n", err) - http.Error(w, "Failed to add question", http.StatusInternalServerError) - return - } - - http.Redirect(w, r, "/manage", http.StatusSeeOther) -} - -func handleManage(w http.ResponseWriter, r *http.Request) { - rows, err := db.Query("SELECT id, question_text, question_type FROM questions ORDER BY question_order") - if err != nil { - log.Printf("Error fetching questions: %v\n", err) - http.Error(w, "Failed to fetch questions", http.StatusInternalServerError) - return - } - defer rows.Close() - - var questions []Question - - for rows.Next() { - var question Question - err := rows.Scan(&question.ID, &question.QuestionText, &question.QuestionType) - if err != nil { - log.Printf("Error scanning question: %v\n", err) - continue - } - questions = append(questions, question) - } - - if len(questions) == 0 { - log.Println("No questions found") - } - - tmpl := template.Must(template.ParseFiles("manage.html")) - err = tmpl.Execute(w, questions) - if err != nil { - log.Printf("Error executing template: %v\n", err) - } -} - -func handleRemoveQuestion(w http.ResponseWriter, r *http.Request) { - questionID := r.URL.Query().Get("id") - - deleteQuery := `DELETE FROM questions WHERE id = ?` - _, err := db.Exec(deleteQuery, questionID) - if err != nil { - log.Printf("Error removing question: %v\n", err) - http.Error(w, "Failed to remove question", http.StatusInternalServerError) - return - } - - http.Redirect(w, r, "/manage", http.StatusSeeOther) + log.Println("[DEBUG] Exiting handleFetchMatches handler") } func main() { var err error + log.Println("[DEBUG] Opening database connection") db, err = sql.Open("sqlite3", "./formdata.db") + // db, err = sql.Open("sqlite3", ":memory:") if err != nil { log.Fatal(err) } createTableQueries := ` - CREATE TABLE IF NOT EXISTS questions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - question_text TEXT NOT NULL, - question_type TEXT NOT NULL DEFAULT 'text', - question_order INTEGER NOT NULL - ); + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT, + phone TEXT, + dorm TEXT + ); - CREATE TABLE IF NOT EXISTS answers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - question_id INTEGER NOT NULL, - answer TEXT NOT NULL, - FOREIGN KEY (question_id) REFERENCES questions(id) -); - ` + CREATE TABLE IF NOT EXISTS classes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + class_name TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + ` + log.Println("[DEBUG] Creating database tables") _, err = db.Exec(createTableQueries) if err != nil { - log.Fatal(err) + log.Fatalf("[ERROR] Error creating tables: %v\n", err) return } + log.Println("[DEBUG] Database tables created successfully") http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) http.HandleFunc("/", renderForm) http.HandleFunc("/submit", handleFormSubmit) - http.HandleFunc("/admin", handleAdmin) - http.HandleFunc("/manage", handleManage) - http.HandleFunc("/manage/add", handleAddQuestion) - http.HandleFunc("/manage/remove", handleRemoveQuestion) + // http.HandleFunc("/admin", handleAdmin) + // http.HandleFunc("/manage", handleManage) + // http.HandleFunc("/manage/add", handleAddQuestion) + // http.HandleFunc("/manage/remove", handleRemoveQuestion) + http.HandleFunc("/fetch-matches", handleFetchMatches) + log.Println("[DEBUG] Server starting at :8080") fmt.Println("Server started at :8080") log.Fatal(http.ListenAndServe(":8080", nil)) diff --git a/manage.html b/manage.html index 8ed607e..d1655c6 100644 --- a/manage.html +++ b/manage.html @@ -10,35 +10,63 @@

Manage Questions

-

Add New Question



+ + +

+ + +
+

Existing Questions

+ + {{range .}} + + {{else}} - + {{end}}
Question TextQuestion TypeOptions Actions
{{.QuestionText}}{{.QuestionType}} + {{if .Options}} +
    + {{range .Options}} +
  • {{.}}
  • + {{end}} +
+ {{else}}None{{end}} +
Remove
No questions found.No questions found.
+ diff --git a/static/style.css b/static/style.css index 3bdf986..ef71848 100644 --- a/static/style.css +++ b/static/style.css @@ -145,3 +145,73 @@ a.dark-mode:hover { color: #3399ff; text-decoration: underline; } + +.content { + display: flex; + flex-wrap: wrap; +} + +.form-section, .matches-section { + flex: 1; + padding: 20px; +} + +@media (min-width: 768px) { + .form-section { + width: 50%; + } + + .matches-section { + width: 50%; + } +} + +@media (max-width: 767px) { + .form-section, .matches-section { + width: 100%; + } +} + +.tags-input-container { + display: flex; + flex-wrap: wrap; + border: 1px solid #ccc; + padding: 8px; + width: 100%; + min-height: 40px; + cursor: text; +} + +.tags-input-container:focus-within { + border-color: #0073e6; +} + +.tag { + display: inline-flex; + align-items: center; + background-color: #e0e0e0; + border-radius: 3px; + padding: 4px 8px; + margin: 4px; + font-size: 14px; +} + +.tag:hover .close { + display: inline-block; +} + +.tag .close { + margin-left: 8px; + cursor: pointer; + display: none; + font-size: 14px; + color: red; +} + +.tags-input-container input { + border: none; + outline: none; + flex-grow: 1; + font-size: 14px; + padding: 4px; +} \ No newline at end of file diff --git a/static/util.js b/static/util.js new file mode 100644 index 0000000..2cde80a --- /dev/null +++ b/static/util.js @@ -0,0 +1,127 @@ +console.log("entered script"); +document.addEventListener('DOMContentLoaded', function () { + const questionTypeElement = document.getElementById('question_type'); + + console.log("dom content loaded!"); + + if (questionTypeElement) { + questionTypeElement.addEventListener('change', function () { + const optionsSection = document.getElementById('options-section'); + const selectedType = this.value; + if (selectedType === 'multiple_choice' || selectedType === 'single_choice' || selectedType === 'dropdown') { + optionsSection.style.display = 'block'; + } else { + optionsSection.style.display = 'none'; + } + }); + } + + document.querySelectorAll('input[id^="other_"]').forEach(el => { + const id = el.id.split('_')[1]; + const otherTextElement = document.getElementById(`other_text_${id}`); + if (otherTextElement) { + el.addEventListener('change', function () { + otherTextElement.style.display = this.checked ? 'block' : 'none'; + }); + } + }); + + document.querySelectorAll('input[id^="radio_other_"]').forEach(el => { + const id = el.id.split('_')[2]; + const radioOtherTextElement = document.getElementById(`radio_other_text_${id}`); + if (radioOtherTextElement) { + el.addEventListener('change', function () { + radioOtherTextElement.style.display = this.checked ? 'block' : 'none'; + }); + } + }); +}); + +document.addEventListener('DOMContentLoaded', function () { + const tagsInput = document.getElementById('classes-input'); + let classes = []; + + // Define the addClass function before it's used + function addClass(classname) { + const container = document.getElementById('classes-container'); + const classElement = document.createElement('span'); + classElement.classList.add('tag'); + classElement.textContent = classname; + + const closeIcon = document.createElement('span'); + closeIcon.classList.add('close'); + closeIcon.textContent = 'x'; + closeIcon.addEventListener('click', function () { + container.removeChild(classElement); + classes = classes.filter(c => c !== classname); + fetchMatchingPeople(classes); // Call fetchMatchingPeople when a class is removed + }); + + classElement.appendChild(closeIcon); + container.appendChild(classElement); + } + + // Define the fetchMatchingPeople function before it's used + function fetchMatchingPeople(classes) { + const xhr = new XMLHttpRequest(); + xhr.open("POST", "/fetch-matches", true); + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + + xhr.onload = function () { + if (xhr.status === 200) { + const matchesContainer = document.getElementById('matches-container'); + const people = JSON.parse(xhr.responseText); + + matchesContainer.innerHTML = ''; + + // Log the response to see if "people" is populated + console.log("Fetched people: ", people); + + if (people && people.length > 0) { + people.forEach(person => { + const personDiv = document.createElement('div'); + personDiv.classList.add('person'); + personDiv.innerHTML = `${person.name}
Classes: ${person.classes.join(', ')}
Email: ${person.email || 'N/A'}`; + matchesContainer.appendChild(personDiv); + }); + } else { + matchesContainer.innerHTML = '

No matches found for these classes.

'; + } + } else { + console.error("Error fetching matches:", xhr.status, xhr.responseText); + } + }; + + xhr.onerror = function () { + console.error("Network error while fetching matches."); + }; + + xhr.send(JSON.stringify({ classes: classes })); + } + + tagsInput.addEventListener('keydown', function (event) { + if (event.key === 'Enter' || event.key === ',') { + event.preventDefault(); + let value = tagsInput.value.trim().replace(/,$/, ''); + if (value) { + addClass(value); // Add class tag + classes.push(value); // Add to classes array + fetchMatchingPeople(classes); // Fetch matching people + tagsInput.value = ''; + } + } + }); + + document.querySelector('form').addEventListener('submit', function (event) { + console.log('Classes on submit:', classes); + const hiddenClassesInput = document.createElement('input'); + hiddenClassesInput.type = 'hidden'; + hiddenClassesInput.name = 'classes'; + + // Filter out empty class names and trim spaces + const cleanedClasses = classes.filter(classname => classname.trim() !== "").map(classname => classname.trim()); + hiddenClassesInput.value = cleanedClasses.join(','); + + this.appendChild(hiddenClassesInput); + }); +});