Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
2964af1227 | |||
5cebf50093 | |||
![]() |
0a6dce64ec | ||
9cee572955 |
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,2 +1,13 @@
|
|||||||
|
# Don't commit git
|
||||||
|
.git
|
||||||
|
|
||||||
# Remove database
|
# Remove database
|
||||||
formdata.db
|
formdata.db
|
||||||
|
|
||||||
|
# tmp files
|
||||||
|
output.txt
|
||||||
|
typer.py
|
||||||
|
README.md
|
||||||
|
go.mod
|
||||||
|
go.sum
|
||||||
|
demo.html
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
9
.idea/FormMaker.iml
generated
Normal file
9
.idea/FormMaker.iml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/FormMaker.iml" filepath="$PROJECT_DIR$/.idea/FormMaker.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -15,18 +15,21 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Question</th>
|
<th>Question</th>
|
||||||
<th>Answer</th>
|
<th>Answer</th>
|
||||||
|
<th>Tags</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{range .}}
|
{{range .}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{.QuestionText}}</td>
|
<td>{{.QuestionText}}</td>
|
||||||
<td>{{.Answer}}</td>
|
<td>{{.Answer}}</td>
|
||||||
|
<td>{{.Tags}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">No submissions found.</td>
|
<td colspan="3">No submissions found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/theme.js"></script>
|
<script src="/static/theme.js"></script>
|
||||||
|
97
index.html
97
index.html
@ -3,23 +3,96 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Form</title>
|
<title>Find Classmates</title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<script>
|
||||||
|
/*to prevent Firefox FOUC, this must be here*/
|
||||||
|
let FF_FOUC_FIX;
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container" id="main-container">
|
||||||
<h1>Submit Your Information</h1>
|
<div class="form-section" id="form-section">
|
||||||
<button id="dark-mode-toggle">Toggle Dark Mode</button>
|
<h1>Enter Your Information</h1>
|
||||||
<form action="/submit" method="post">
|
<form action="/submit" method="post">
|
||||||
{{range .}}
|
<label for="name">Name:</label>
|
||||||
<label>{{.QuestionText}}:</label>
|
<input type="text" id="name" name="name" required>
|
||||||
<input type="{{.QuestionType}}" name="custom_{{.ID}}" required><br><br>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<input type="submit" value="Submit">
|
<label for="email">Email:</label>
|
||||||
</form>
|
<input type="email" id="email" name="email">
|
||||||
|
|
||||||
|
<label for="phone">Phone:</label>
|
||||||
|
<input type="text" id="phone" name="phone">
|
||||||
|
|
||||||
|
<label for="dorm">Dorm:</label>
|
||||||
|
<input type="text" id="dorm" name="dorm">
|
||||||
|
|
||||||
|
<label>Classes:</label>
|
||||||
|
<div class="tags-input-container" id="classes-container">
|
||||||
|
<input type="text" id="classes-input" placeholder="Enter classes">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden matches section until classes are entered -->
|
||||||
|
<div class="matches-section" id="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>
|
||||||
|
<script>
|
||||||
|
const classInput = document.getElementById('classes-input');
|
||||||
|
const formSection = document.getElementById('form-section');
|
||||||
|
const matchesSection = document.getElementById('matches-section');
|
||||||
|
const mainContainer = document.getElementById('main-container');
|
||||||
|
const tagsInputContainer = document.getElementById('classes-container');
|
||||||
|
|
||||||
|
// Add event listener to handle Enter key for adding chips
|
||||||
|
classInput.addEventListener('keydown', function(event) {
|
||||||
|
if (event.key === 'Enter' && classInput.value.trim() !== "") {
|
||||||
|
event.preventDefault();
|
||||||
|
addClassChip(classInput.value.trim());
|
||||||
|
classInput.value = ''; // Clear input after adding
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to add a chip (tag)
|
||||||
|
function addClassChip(className) {
|
||||||
|
const tag = document.createElement('div');
|
||||||
|
tag.classList.add('tag');
|
||||||
|
tag.innerHTML = className + '<span class="close">x</span>';
|
||||||
|
|
||||||
|
// Append the new tag to the tags container
|
||||||
|
tagsInputContainer.insertBefore(tag, classInput);
|
||||||
|
|
||||||
|
// Check if any chips exist, and show the matches section
|
||||||
|
updateMatchesVisibility();
|
||||||
|
|
||||||
|
// Remove tag on close click
|
||||||
|
tag.querySelector('.close').addEventListener('click', function() {
|
||||||
|
tag.remove();
|
||||||
|
updateMatchesVisibility(); // Check visibility when a chip is removed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMatchesVisibility() {
|
||||||
|
const chipCount = tagsInputContainer.getElementsByClassName('tag').length;
|
||||||
|
console.log('Chip count:', chipCount); // Debugging message
|
||||||
|
if (chipCount > 0) {
|
||||||
|
mainContainer.classList.add('show-matches');
|
||||||
|
} else {
|
||||||
|
mainContainer.classList.remove('show-matches');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
453
main.go
453
main.go
@ -2,10 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
@ -14,225 +16,356 @@ type Question struct {
|
|||||||
ID int
|
ID int
|
||||||
QuestionText string
|
QuestionText string
|
||||||
QuestionType string
|
QuestionType string
|
||||||
|
Options []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Match struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Classes []string `json:"classes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
|
|
||||||
func renderForm(w http.ResponseWriter, r *http.Request) {
|
func renderForm(w http.ResponseWriter, r *http.Request) {
|
||||||
rows, err := db.Query("SELECT id, question_text, question_type FROM questions ORDER BY question_order")
|
log.Println("[DEBUG] Entering renderForm handler")
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl := template.Must(template.ParseFiles("index.html"))
|
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) {
|
func handleFormSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
log.Println("[DEBUG] Entering handleFormSubmit handler")
|
||||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
|
||||||
|
// Parse the form data
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Invalid form data", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.ParseForm()
|
// Log all form data
|
||||||
if err != nil {
|
log.Printf("[DEBUG] Form data: %v\n", r.Form)
|
||||||
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()
|
tx, err := db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error starting transaction: %v\n", err)
|
log.Fatalf("[ERROR] Failed to begin transaction: %v\n", err)
|
||||||
http.Error(w, "Failed to save answers", http.StatusInternalServerError)
|
|
||||||
return
|
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 {
|
||||||
|
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 {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
} else {
|
log.Fatalf("[ERROR] Failed to insert class: %v\n", err)
|
||||||
tx.Commit()
|
return
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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!")
|
fmt.Fprintf(w, "Thank you for your submission!")
|
||||||
|
log.Println("[DEBUG] Exiting handleFormSubmit handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAdmin(w http.ResponseWriter, r *http.Request) {
|
// func handleAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
rows, err := db.Query(`
|
// log.Println("[DEBUG] Entering handleAdmin handler")
|
||||||
SELECT q.question_text, a.answer
|
// rows, err := db.Query(`
|
||||||
FROM answers a
|
// SELECT q.question_text, a.answer, GROUP_CONCAT(t.tag, ', ') as tags
|
||||||
JOIN questions q ON a.question_id = q.id
|
// FROM answers a
|
||||||
ORDER BY q.question_order
|
// 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 JSON request body
|
||||||
|
var requestData struct {
|
||||||
|
Classes []string `json:"classes"`
|
||||||
|
}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&requestData)
|
||||||
|
if err != nil || len(requestData.Classes) == 0 {
|
||||||
|
http.Error(w, "Invalid request data", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Classes to match: %v\n", requestData.Classes)
|
||||||
|
|
||||||
|
// Prepare query to fetch users who share any of the classes
|
||||||
|
query := `
|
||||||
|
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;
|
||||||
|
`
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
log.Printf("Error fetching submissions: %v\n", err)
|
log.Printf("[ERROR] Failed to fetch matching users: %v\n", err)
|
||||||
http.Error(w, "Failed to fetch submissions", http.StatusInternalServerError)
|
http.Error(w, "Failed to fetch matches", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var submissions []struct {
|
var matches []Match
|
||||||
QuestionText string
|
|
||||||
Answer string
|
|
||||||
}
|
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var submission struct {
|
var match Match
|
||||||
QuestionText string
|
var classList string
|
||||||
Answer string
|
if err := rows.Scan(&match.Name, &match.Email, &classList); err != nil {
|
||||||
}
|
log.Printf("[ERROR] Failed to scan result row: %v\n", err)
|
||||||
err := rows.Scan(&submission.QuestionText, &submission.Answer)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error scanning submission: %v\n", err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
submissions = append(submissions, submission)
|
match.Classes = strings.Split(classList, ",")
|
||||||
|
matches = append(matches, match)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := template.Must(template.ParseFiles("admin.html"))
|
// Return matches as JSON, always return an array, even if empty
|
||||||
tmpl.Execute(w, submissions)
|
w.Header().Set("Content-Type", "application/json")
|
||||||
}
|
if err := json.NewEncoder(w).Encode(matches); err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to encode matches: %v\n", err)
|
||||||
func handleAddQuestion(w http.ResponseWriter, r *http.Request) {
|
http.Error(w, "Failed to return matches", http.StatusInternalServerError)
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ParseForm()
|
log.Println("[DEBUG] Exiting handleFetchMatches handler")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
log.Println("[DEBUG] Opening database connection")
|
||||||
db, err = sql.Open("sqlite3", "./formdata.db")
|
db, err = sql.Open("sqlite3", "./formdata.db")
|
||||||
|
// db, err = sql.Open("sqlite3", ":memory:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
createTableQueries := `
|
createTableQueries := `
|
||||||
CREATE TABLE IF NOT EXISTS questions (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
question_text TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
question_type TEXT NOT NULL DEFAULT 'text',
|
email TEXT,
|
||||||
question_order INTEGER NOT NULL
|
phone TEXT,
|
||||||
);
|
dorm TEXT
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS answers (
|
CREATE TABLE IF NOT EXISTS classes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
question_id INTEGER NOT NULL,
|
user_id INTEGER,
|
||||||
answer TEXT NOT NULL,
|
class_name TEXT NOT NULL,
|
||||||
FOREIGN KEY (question_id) REFERENCES questions(id)
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
log.Println("[DEBUG] Creating database tables")
|
||||||
_, err = db.Exec(createTableQueries)
|
_, err = db.Exec(createTableQueries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatalf("[ERROR] Error creating tables: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Println("[DEBUG] Database tables created successfully")
|
||||||
|
|
||||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||||
|
|
||||||
http.HandleFunc("/", renderForm)
|
http.HandleFunc("/", renderForm)
|
||||||
http.HandleFunc("/submit", handleFormSubmit)
|
http.HandleFunc("/submit", handleFormSubmit)
|
||||||
http.HandleFunc("/admin", handleAdmin)
|
// http.HandleFunc("/admin", handleAdmin)
|
||||||
http.HandleFunc("/manage", handleManage)
|
// http.HandleFunc("/manage", handleManage)
|
||||||
http.HandleFunc("/manage/add", handleAddQuestion)
|
// http.HandleFunc("/manage/add", handleAddQuestion)
|
||||||
http.HandleFunc("/manage/remove", handleRemoveQuestion)
|
// http.HandleFunc("/manage/remove", handleRemoveQuestion)
|
||||||
|
http.HandleFunc("/fetch-matches", handleFetchMatches)
|
||||||
|
|
||||||
|
log.Println("[DEBUG] Server starting at :8080")
|
||||||
fmt.Println("Server started at :8080")
|
fmt.Println("Server started at :8080")
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
32
manage.html
32
manage.html
@ -10,35 +10,63 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Manage Questions</h1>
|
<h1>Manage Questions</h1>
|
||||||
|
|
||||||
<!-- Dark mode toggle button -->
|
|
||||||
<button id="dark-mode-toggle">Toggle Dark Mode</button>
|
<button id="dark-mode-toggle">Toggle Dark Mode</button>
|
||||||
|
|
||||||
<h2>Add New Question</h2>
|
<h2>Add New Question</h2>
|
||||||
<form action="/manage/add" method="post">
|
<form action="/manage/add" method="post">
|
||||||
<label for="question_text">Question Text:</label>
|
<label for="question_text">Question Text:</label>
|
||||||
<input type="text" id="question_text" name="question_text" required><br><br>
|
<input type="text" id="question_text" name="question_text" required><br><br>
|
||||||
|
|
||||||
|
<label for="question_type">Question Type:</label>
|
||||||
|
<select id="question_type" name="question_type" required>
|
||||||
|
<option value="text">Text</option>
|
||||||
|
<option value="multiple_choice">Multiple Choice</option>
|
||||||
|
<option value="single_choice">Single Choice</option>
|
||||||
|
<option value="dropdown">Dropdown</option>
|
||||||
|
<option value="tags">Tag Input</option>
|
||||||
|
</select><br><br>
|
||||||
|
|
||||||
|
<div id="options-section" style="display:none;">
|
||||||
|
<label for="options">Options (comma-separated):</label>
|
||||||
|
<textarea id="options" name="options" rows="4" cols="50"></textarea><br><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="submit" value="Add Question">
|
<input type="submit" value="Add Question">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<h2>Existing Questions</h2>
|
<h2>Existing Questions</h2>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Question Text</th>
|
<th>Question Text</th>
|
||||||
|
<th>Question Type</th>
|
||||||
|
<th>Options</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{range .}}
|
{{range .}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{.QuestionText}}</td>
|
<td>{{.QuestionText}}</td>
|
||||||
|
<td>{{.QuestionType}}</td>
|
||||||
|
<td>
|
||||||
|
{{if .Options}}
|
||||||
|
<ul>
|
||||||
|
{{range .Options}}
|
||||||
|
<li>{{.}}</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{else}}None{{end}}
|
||||||
|
</td>
|
||||||
<td><a href="/manage/remove?id={{.ID}}">Remove</a></td>
|
<td><a href="/manage/remove?id={{.ID}}">Remove</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">No questions found.</td>
|
<td colspan="4">No questions found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/theme.js"></script>
|
<script src="/static/theme.js"></script>
|
||||||
|
<script src="/static/util.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
BIN
static/default_icon.png
Normal file
BIN
static/default_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 125 B |
210
static/style.css
210
static/style.css
@ -1,147 +1,151 @@
|
|||||||
|
/* General Styles */
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background-color: #f4f4f9;
|
background-color: #f9f9fc;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Main container for both form and matches */
|
||||||
.container {
|
.container {
|
||||||
width: 80%;
|
display: flex;
|
||||||
margin: 0 auto;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 90%; /* Increased width for a more uniform layout */
|
||||||
|
max-width: 1000px;
|
||||||
|
transition: all 0.6s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form and Matches Sections */
|
||||||
|
.form-section, .matches-section {
|
||||||
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
border-radius: 15px;
|
||||||
margin-top: 50px;
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 10px;
|
transition: all 0.6s ease-in-out;
|
||||||
|
box-sizing: border-box; /* Ensure padding is included in the width */
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
/* Ensure both form and matches have the same width */
|
||||||
text-align: center;
|
.form-section {
|
||||||
color: #333; /* Default color for light mode */
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
/* Hidden matches section by default */
|
||||||
margin: 20px 0;
|
.matches-section {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.6s ease-in-out; /* Smooth transition for visibility */
|
||||||
}
|
}
|
||||||
|
|
||||||
form label {
|
/* Show matches when class chip is added */
|
||||||
display: block;
|
.show-matches .matches-section {
|
||||||
margin-bottom: 10px;
|
opacity: 1;
|
||||||
font-weight: bold;
|
height: auto;
|
||||||
color: #555;
|
visibility: visible;
|
||||||
|
margin-left: 20px;
|
||||||
|
transition: all 0.6s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input[type="text"], form input[type="email"], form textarea {
|
/* Smooth fade-in animation for the matches section */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(50px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Section Animation */
|
||||||
|
.show-matches .form-section {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
transition: all 0.6s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input fields */
|
||||||
|
form input[type="text"], form input[type="email"], form input[type="submit"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
border: 1px solid #ccc;
|
border-radius: 8px;
|
||||||
border-radius: 5px;
|
border: 1px solid #ddd;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background-color: white;
|
box-sizing: border-box;
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Submit button */
|
||||||
form input[type="submit"] {
|
form input[type="submit"] {
|
||||||
background-color: #28a745;
|
background-color: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input[type="submit"]:hover {
|
form input[type="submit"]:hover {
|
||||||
background-color: #218838;
|
background-color: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
/* Tags (Class Chips) Styling */
|
||||||
width: 100%;
|
.tags-input-container {
|
||||||
border-collapse: collapse;
|
display: flex;
|
||||||
margin-top: 20px;
|
flex-wrap: wrap;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 40px;
|
||||||
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
table, th, td {
|
.tags-input-container:focus-within {
|
||||||
border: 1px solid #ddd;
|
border-color: #007bff;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
.tag {
|
||||||
padding: 10px;
|
display: inline-flex;
|
||||||
text-align: left;
|
align-items: center;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 4px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
.tag:hover .close {
|
||||||
background-color: #f2f2f2;
|
display: inline-block;
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
.tag .close {
|
||||||
background-color: white;
|
margin-left: 8px;
|
||||||
color: #333;
|
cursor: pointer;
|
||||||
|
display: none;
|
||||||
|
font-size: 14px;
|
||||||
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
/* Fix for text overflow in input fields */
|
||||||
color: #007bff;
|
.tags-input-container input {
|
||||||
text-decoration: none;
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 4px;
|
||||||
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
/* Matches section styling (not a card for the 'No similar classes' text) */
|
||||||
text-decoration: underline;
|
#matches-container p {
|
||||||
}
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
/*
|
text-align: center;
|
||||||
* DARK MODE
|
|
||||||
*/
|
|
||||||
body.dark-mode {
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
color: #f4f4f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container.dark-mode {
|
|
||||||
background-color: #333;
|
|
||||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1.dark-mode {
|
|
||||||
color: #f4f4f9; /* Ensure that the heading is white and visible */
|
|
||||||
}
|
|
||||||
|
|
||||||
form label.dark-mode {
|
|
||||||
color: #f4f4f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input[type="text"].dark-mode, form input[type="email"].dark-mode, form textarea.dark-mode {
|
|
||||||
background-color: #555;
|
|
||||||
color: #f4f4f9;
|
|
||||||
border: 1px solid #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input[type="submit"].dark-mode {
|
|
||||||
background-color: #28a745;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.dark-mode {
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
th.dark-mode {
|
|
||||||
background-color: #555;
|
|
||||||
color: #f4f4f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.dark-mode {
|
|
||||||
background-color: #666;
|
|
||||||
color: #f4f4f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.dark-mode {
|
|
||||||
color: #66b3ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.dark-mode:hover {
|
|
||||||
color: #3399ff;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
|
177
static/util.js
Normal file
177
static/util.js
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
console.log("entered script");
|
||||||
|
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
console.log("All resources finished loading!");
|
||||||
|
|
||||||
|
const questionTypeElement = document.getElementById('question_type');
|
||||||
|
|
||||||
|
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';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
const tagsInput = document.getElementById('classes-input');
|
||||||
|
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');
|
||||||
|
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(); // 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);
|
||||||
|
renderClassFilters(); // Update the class filters UI
|
||||||
|
fetchMatchingPeople(); // Fetch updated matches when a class is added
|
||||||
|
tagsInput.value = ''; // Clear the input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user