Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adddec5617 | |||
| 9b2daca0e6 | |||
| 00cb33d542 | |||
| 726c83dbbb | |||
| d26f3dd3bf | |||
| f238fc7a63 | |||
| 469e1a22cc | |||
| 7e8c91af2c | |||
| ebea1c07fd | |||
| 363fe9ea19 |
+40
@@ -0,0 +1,40 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM golang:1.22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy and download dependencies
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy the source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install required packages for cgo
|
||||||
|
RUN apk add --no-cache gcc musl-dev
|
||||||
|
|
||||||
|
# Build with cgo enabled
|
||||||
|
ENV CGO_ENABLED=1
|
||||||
|
|
||||||
|
# Build the Go application
|
||||||
|
RUN go build -o app .
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
# Install SQLite CLI for runtime
|
||||||
|
RUN apk add --no-cache sqlite
|
||||||
|
|
||||||
|
# Copy the binary from the builder stage
|
||||||
|
COPY --from=builder /app/app .
|
||||||
|
COPY --from=builder /app/static/index.html.tmpl ./static/index.html.tmpl
|
||||||
|
COPY --from=builder /app/static/result.html.tmpl ./static/result.html.tmpl
|
||||||
|
|
||||||
|
# Expose the port the app runs on
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["./app"]
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
module wa5p.eu/datefinder
|
module wa5p.eu/datefinder
|
||||||
|
|
||||||
go 1.22.0
|
go 1.22
|
||||||
|
|
||||||
require github.com/mattn/go-sqlite3 v1.14.24
|
require github.com/mattn/go-sqlite3 v1.14.24
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
@@ -19,6 +19,7 @@ const (
|
|||||||
var (
|
var (
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
indexHtmlTemplate *template.Template
|
indexHtmlTemplate *template.Template
|
||||||
|
resultHtmlTemplate *template.Template
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChoicesPost struct {
|
type ChoicesPost struct {
|
||||||
@@ -64,23 +65,183 @@ func initDatabase() {
|
|||||||
|
|
||||||
func prepareStaticContent() {
|
func prepareStaticContent() {
|
||||||
var err error
|
var err error
|
||||||
indexHtmlTemplate, err = template.ParseFiles("./static/index.html")
|
|
||||||
|
indexHtmlTemplate, err = template.ParseFiles("./static/index.html.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[!] Could not parse index.html template, error: %s\n", err)
|
log.Fatalf("[!] Could not parse index.html template, error: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resultHtmlTemplate, err = template.ParseFiles("./static/result.html.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[!] Could not parse result.html template, error: %s\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetRoot(w http.ResponseWriter, r *http.Request) {
|
func handleGetRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
err := indexHtmlTemplate.Execute(w, nil)
|
var pollName string
|
||||||
|
pollId, err := strconv.Atoi(r.PathValue("pollId"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[!] Invallid poll id url segment, error: %s\n", err)
|
||||||
|
http.Error(w, "Poll not found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT name
|
||||||
|
FROM polls
|
||||||
|
WHERE id = ?;
|
||||||
|
`
|
||||||
|
|
||||||
|
err = db.QueryRow(query, pollId).Scan(&pollName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[!] No poll with id %d was found, error: %s\n", pollId, err)
|
||||||
|
http.Error(w, "Poll not found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = indexHtmlTemplate.Execute(w, struct {
|
||||||
|
PollId int
|
||||||
|
PollName string
|
||||||
|
}{
|
||||||
|
PollId: pollId,
|
||||||
|
PollName: pollName,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[!] Could not execute index.html template, error: %s\n", err)
|
log.Printf("[!] Could not execute index.html template, error: %s\n", err)
|
||||||
http.Error(w, "Error rendering template", http.StatusInternalServerError)
|
http.Error(w, "Error rendering template", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetPing(w http.ResponseWriter, r *http.Request) {
|
func handleGetResult(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("[*] Received request %v %v\n", r.Method, r.URL)
|
var pollName string
|
||||||
fmt.Fprintf(w, "PONG\n")
|
pollId, err := strconv.Atoi(r.PathValue("pollId"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[!] Invallid poll id url segment, error: %s\n", err)
|
||||||
|
http.Error(w, "Poll not found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT name
|
||||||
|
FROM polls
|
||||||
|
WHERE id = ?;
|
||||||
|
`
|
||||||
|
|
||||||
|
err = db.QueryRow(query, pollId).Scan(&pollName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[!] No poll with id %d was found, error: %s\n", pollId, err)
|
||||||
|
http.Error(w, "Poll not found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = resultHtmlTemplate.Execute(w, struct {
|
||||||
|
PollId int
|
||||||
|
PollName string
|
||||||
|
}{
|
||||||
|
PollId: pollId,
|
||||||
|
PollName: pollName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[!] Could not execute result.html template, error: %s\n", err)
|
||||||
|
http.Error(w, "Error rendering template", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetResultApi(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pollId, err := strconv.Atoi(r.PathValue("pollId"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[!] Invallid poll id url segment, error: %s\n", err)
|
||||||
|
http.Error(w, "Poll not found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT polls.name, choices.username, choices.choices
|
||||||
|
FROM choices
|
||||||
|
JOIN polls ON choices.pollid = polls.id
|
||||||
|
WHERE choices.pollid = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := db.Query(query, pollId)
|
||||||
|
defer rows.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[!] No poll with id %d was found, error: %s\n", pollId, err)
|
||||||
|
http.Error(w, "Poll not found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []struct {
|
||||||
|
pollName string
|
||||||
|
username string
|
||||||
|
choices string
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
var pollName string
|
||||||
|
var username string
|
||||||
|
var choices string
|
||||||
|
if err := rows.Scan(&pollName, &username, &choices); err != nil {
|
||||||
|
log.Printf("[!] Could not scan result row %v, error: %s\n", rows, err)
|
||||||
|
}
|
||||||
|
results = append(results, struct {
|
||||||
|
pollName string
|
||||||
|
username string
|
||||||
|
choices string
|
||||||
|
}{
|
||||||
|
pollName: pollName,
|
||||||
|
username: username,
|
||||||
|
choices: choices,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type HourlyVote struct {
|
||||||
|
Usernames []string `json:"usernames"`
|
||||||
|
Votes int `json:"votes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollResult struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Votes [7][24]HourlyVote `json:"votes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var pollResult PollResult
|
||||||
|
|
||||||
|
pollResult.Id = pollId
|
||||||
|
pollResult.Name = results[0].pollName
|
||||||
|
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
for j := 0; j < 24; j++ {
|
||||||
|
pollResult.Votes[i][j] = HourlyVote{
|
||||||
|
Usernames: []string{},
|
||||||
|
Votes: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
var choices [][]bool
|
||||||
|
err := json.Unmarshal([]byte(result.choices), &choices)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[!] Could not unmarshal choices %s, error: %s\n", result.choices, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for didx, day := range choices {
|
||||||
|
for hidx, hour := range day {
|
||||||
|
if hour {
|
||||||
|
pollResult.Votes[didx][hidx].Usernames =
|
||||||
|
append(pollResult.Votes[didx][hidx].Usernames, result.username)
|
||||||
|
pollResult.Votes[didx][hidx].Votes++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(pollResult); err != nil {
|
||||||
|
http.Error(w, "Failed to encode JSON", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePostSubmit(w http.ResponseWriter, r *http.Request) {
|
func handlePostSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -97,6 +258,7 @@ func handlePostSubmit(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[!] Error decoding request Body %v, error: %s\n", r.Body, err)
|
log.Printf("[!] Error decoding request Body %v, error: %s\n", r.Body, err)
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
choicesJson, err := json.Marshal(choicesPost.Choices)
|
choicesJson, err := json.Marshal(choicesPost.Choices)
|
||||||
@@ -123,7 +285,6 @@ func handlePostSubmit(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
|
||||||
log.Printf("[*] Persisted choices for poll %d, username %s to database\n",
|
log.Printf("[*] Persisted choices for poll %d, username %s to database\n",
|
||||||
@@ -146,15 +307,22 @@ func main() {
|
|||||||
prepareStaticContent()
|
prepareStaticContent()
|
||||||
|
|
||||||
http.HandleFunc(
|
http.HandleFunc(
|
||||||
"GET /api/ping",
|
"GET /favicon.ico",
|
||||||
handleGetPing,
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Error(w, "There is no favicon.ico!", http.StatusBadRequest)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
http.HandleFunc(
|
http.HandleFunc(
|
||||||
"GET /",
|
"GET /{pollId}",
|
||||||
handleGetRoot,
|
handleGetRoot,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
http.HandleFunc(
|
||||||
|
"GET /{pollId}/result",
|
||||||
|
handleGetResult,
|
||||||
|
)
|
||||||
|
|
||||||
http.HandleFunc(
|
http.HandleFunc(
|
||||||
"POST /api/submit",
|
"POST /api/submit",
|
||||||
handlePostSubmit,
|
handlePostSubmit,
|
||||||
@@ -165,6 +333,11 @@ func main() {
|
|||||||
handleOptionsSubmit,
|
handleOptionsSubmit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
http.HandleFunc(
|
||||||
|
"GET /api/result/{pollId}",
|
||||||
|
handleGetResultApi,
|
||||||
|
)
|
||||||
|
|
||||||
log.Printf("[*] Datefinder server listening on :%s\n", PORT)
|
log.Printf("[*] Datefinder server listening on :%s\n", PORT)
|
||||||
if err := http.ListenAndServe(":8080", nil); err != nil {
|
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||||
log.Fatalf("[!] Could not start server on %s, error: %s\n", PORT, err)
|
log.Fatalf("[!] Could not start server on %s, error: %s\n", PORT, err)
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar {
|
.calendar {
|
||||||
@@ -44,16 +47,84 @@
|
|||||||
background-color: #87CEEB;
|
background-color: #87CEEB;
|
||||||
/* Change color when clicked */
|
/* Change color when clicked */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#submit-name {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
color: #666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit-btn:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit-name-label {
|
||||||
|
color: red;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.calendar {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.calendar {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-column {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit-btn {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>Datefinder</h1>
|
<h1>Datefinder</h1>
|
||||||
|
<h2>{{ .PollName }}</h2>
|
||||||
<div class="calendar" id="calendar"></div>
|
<div class="calendar" id="calendar"></div>
|
||||||
<div>
|
<div>
|
||||||
<input id="submit-name" type="text" placeholder="Enter your name" />
|
<input id="submit-name" type="text" placeholder="Enter your name" />
|
||||||
<button id="submit-btn" type="submit" onclick="submit">Submit</button>
|
<button id="submit-btn" type="submit" onclick="submit">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
|
<label id="submit-name-label" for="submit-name">Enter Submitter Name!</label>
|
||||||
<script>
|
<script>
|
||||||
// Days of the week
|
// Days of the week
|
||||||
const daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
const daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
||||||
@@ -91,47 +162,62 @@
|
|||||||
calendar.appendChild(dayColumn);
|
calendar.appendChild(dayColumn);
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitName = document.getElementById("submit-name")
|
const submitName = document.getElementById("submit-name");
|
||||||
const submitBtn = document.getElementById("submit-btn")
|
const submitBtn = document.getElementById("submit-btn");
|
||||||
|
const submitNameLabel = document.getElementById("submit-name-label");
|
||||||
|
|
||||||
submitBtn.onclick = function () {
|
submitBtn.onclick = function () {
|
||||||
if (submitName.value == "") {
|
if (submitName.value == "") {
|
||||||
console.log("Submitter name is missing.")
|
submitNameLabel.style.visibility = "visible";
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const days = calendar.children
|
submitBtn.disabled = true;
|
||||||
const choices = Array.from(days).map(
|
|
||||||
day => Array.from(day.children).slice(1, 25).map(
|
|
||||||
hour => hour.classList.contains("clicked")))
|
|
||||||
console.log(`Submitting dates for ${submitName.value}`)
|
|
||||||
console.log(JSON.stringify(choices))
|
|
||||||
|
|
||||||
const url = "http://localhost:8080/api/submit";
|
const days = calendar.children;
|
||||||
|
const choices = Array.from(days).map(day =>
|
||||||
|
Array.from(day.children)
|
||||||
|
.slice(1, 25)
|
||||||
|
.map(hour => hour.classList.contains("clicked"))
|
||||||
|
);
|
||||||
|
console.log(`Submitting dates for ${submitName.value}`);
|
||||||
|
console.log(JSON.stringify(choices));
|
||||||
|
|
||||||
|
const url = "/api/submit";
|
||||||
|
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: "POST", // Specify the HTTP method
|
method: "POST", // Specify the HTTP method
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json", // Inform the server about the data format
|
"Content-Type": "application/json", // Inform the server about the data format
|
||||||
},
|
},
|
||||||
body: JSON.stringify({pollId: 1, username: submitName.value, choices: choices}), // Convert the data to JSON string
|
body: JSON.stringify({
|
||||||
|
pollId: {{ .PollId }},
|
||||||
|
username: submitName.value,
|
||||||
|
choices: choices
|
||||||
|
}), // Convert the data to JSON string
|
||||||
})
|
})
|
||||||
/*
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Network response was not ok ' + response.statusText);
|
console.log(response);
|
||||||
}
|
}
|
||||||
return response.json(); // Parse the JSON response
|
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.log('Success:', data); // Handle the response data
|
calendar.remove();
|
||||||
|
submitName.remove();
|
||||||
|
submitBtn.remove();
|
||||||
|
submitNameLabel.remove();
|
||||||
|
|
||||||
|
const textNode = document.createTextNode(
|
||||||
|
"Your choices have been submitted, thx :)"
|
||||||
|
);
|
||||||
|
document.body.appendChild(textNode);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error); // Handle errors
|
console.error("Error:", error); // Handle errors
|
||||||
});
|
});
|
||||||
*/
|
};
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// Days of the week
|
|
||||||
const daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
||||||
|
|
||||||
// Select the calendar container
|
|
||||||
const calendar = document.getElementById("calendar");
|
|
||||||
|
|
||||||
// Create the weekly calendar
|
|
||||||
daysOfWeek.forEach((day) => {
|
|
||||||
// Create a column for each day
|
|
||||||
const dayColumn = document.createElement("div");
|
|
||||||
dayColumn.classList.add("day-column");
|
|
||||||
|
|
||||||
// Add a header for the day
|
|
||||||
const dayHeader = document.createElement("div");
|
|
||||||
dayHeader.classList.add("day-header");
|
|
||||||
dayHeader.textContent = day;
|
|
||||||
dayColumn.appendChild(dayHeader);
|
|
||||||
|
|
||||||
// Add hourly blocks for each day
|
|
||||||
for (let hour = 0; hour < 24; hour++) {
|
|
||||||
const hourBlock = document.createElement("div");
|
|
||||||
hourBlock.classList.add("hour-block");
|
|
||||||
hourBlock.textContent = `${hour}:00`;
|
|
||||||
|
|
||||||
// Add a click event listener to toggle color
|
|
||||||
hourBlock.addEventListener("click", () => {
|
|
||||||
hourBlock.classList.toggle("clicked");
|
|
||||||
});
|
|
||||||
|
|
||||||
dayColumn.appendChild(hourBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the day column to the calendar
|
|
||||||
calendar.appendChild(dayColumn);
|
|
||||||
});
|
|
||||||
|
|
||||||
const submitName = document.getElementById("submit-name")
|
|
||||||
const submitBtn = document.getElementById("submit-btn")
|
|
||||||
|
|
||||||
submitBtn.onclick = function() {
|
|
||||||
if (submitName.value == "") {
|
|
||||||
console.log("Submitter name is missing.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const days = calendar.children
|
|
||||||
const choices = Array.from(days).map(
|
|
||||||
day => Array.from(day.children).slice(1, 25).map(
|
|
||||||
hour => hour.classList.contains("clicked")))
|
|
||||||
console.log(`Submitting dates for ${submitName.value}`)
|
|
||||||
console.log(JSON.stringify(choices))
|
|
||||||
|
|
||||||
const url = "http://localhost:8080/api/submit";
|
|
||||||
|
|
||||||
fetch(url, {
|
|
||||||
method: "POST", // Specify the HTTP method
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json", // Inform the server about the data format
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ pollId: 1, username: submitName.value, choices: choices }), // Convert the data to JSON string
|
|
||||||
})
|
|
||||||
/*
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok ' + response.statusText);
|
|
||||||
}
|
|
||||||
return response.json(); // Parse the JSON response
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log('Success:', data); // Handle the response data
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error); // Handle errors
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Datefinder</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin: 20px auto;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-column {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-header {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 2px 0;
|
||||||
|
background-color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block:hover {
|
||||||
|
border-color: #87CEEB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block.clicked {
|
||||||
|
background-color: #87CEEB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
visibility: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1000;
|
||||||
|
transform: translate(-50%, -150%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block:hover .tooltip {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.calendar {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.calendar {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-column {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Datefinder</h1>
|
||||||
|
<h2>{{ .PollName }}</h2>
|
||||||
|
<div class="calendar" id="calendar"></div>
|
||||||
|
<script>
|
||||||
|
function getColorBetween(upperColor, num, upperBound) {
|
||||||
|
const parseColor = (color) => {
|
||||||
|
const bigint = parseInt(color.slice(1), 16);
|
||||||
|
return {
|
||||||
|
r: (bigint >> 16) & 255,
|
||||||
|
g: (bigint >> 8) & 255,
|
||||||
|
b: bigint & 255,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const upperRGB = parseColor(upperColor);
|
||||||
|
const whiteRGB = { r: 255, g: 255, b: 255 };
|
||||||
|
const clampedNum = Math.max(0, Math.min(num, upperBound));
|
||||||
|
const weight = clampedNum / upperBound;
|
||||||
|
|
||||||
|
const interpolatedRGB = {
|
||||||
|
r: Math.round(whiteRGB.r + weight * (upperRGB.r - whiteRGB.r)),
|
||||||
|
g: Math.round(whiteRGB.g + weight * (upperRGB.g - whiteRGB.g)),
|
||||||
|
b: Math.round(whiteRGB.b + weight * (upperRGB.b - whiteRGB.b)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const toHex = (val) => val.toString(16).padStart(2, "0");
|
||||||
|
return `#${toHex(interpolatedRGB.r)}${toHex(interpolatedRGB.g)}${toHex(interpolatedRGB.b)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResultView(result) {
|
||||||
|
const daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
||||||
|
const calendar = document.getElementById("calendar");
|
||||||
|
const maxVotes = result.votes.flat().reduce((max, obj) => Math.max(max, obj.votes), 0);
|
||||||
|
|
||||||
|
for (let day = 0; day < 7; day++) {
|
||||||
|
const dayColumn = document.createElement("div");
|
||||||
|
dayColumn.classList.add("day-column");
|
||||||
|
|
||||||
|
const dayHeader = document.createElement("div");
|
||||||
|
dayHeader.classList.add("day-header");
|
||||||
|
dayHeader.textContent = daysOfWeek[day];
|
||||||
|
dayColumn.appendChild(dayHeader);
|
||||||
|
|
||||||
|
for (let hour = 0; hour < 24; hour++) {
|
||||||
|
const hourBlock = document.createElement("div");
|
||||||
|
hourBlock.classList.add("hour-block");
|
||||||
|
hourBlock.textContent = `${hour}:00`;
|
||||||
|
|
||||||
|
const tooltip = document.createElement("div");
|
||||||
|
tooltip.classList.add("tooltip");
|
||||||
|
tooltip.textContent = result.votes[day][hour].usernames.join(", ");
|
||||||
|
hourBlock.appendChild(tooltip);
|
||||||
|
|
||||||
|
hourBlock.style.backgroundColor = getColorBetween(
|
||||||
|
"#87CEEB",
|
||||||
|
result.votes[day][hour].votes,
|
||||||
|
maxVotes
|
||||||
|
);
|
||||||
|
|
||||||
|
dayColumn.appendChild(hourBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
calendar.appendChild(dayColumn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = "/api/result/{{ .PollId }}";
|
||||||
|
|
||||||
|
fetch(url, { method: "GET" })
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
createResultView(data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Fetch error:", error);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in New Issue
Block a user