Compare commits
10 Commits
e4e745cb57
...
main
| 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
|
||||
|
||||
go 1.22.0
|
||||
go 1.22
|
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.24
|
||||
|
||||
@@ -3,10 +3,10 @@ package main
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
@@ -17,8 +17,9 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
indexHtmlTemplate *template.Template
|
||||
db *sql.DB
|
||||
indexHtmlTemplate *template.Template
|
||||
resultHtmlTemplate *template.Template
|
||||
)
|
||||
|
||||
type ChoicesPost struct {
|
||||
@@ -64,23 +65,183 @@ func initDatabase() {
|
||||
|
||||
func prepareStaticContent() {
|
||||
var err error
|
||||
indexHtmlTemplate, err = template.ParseFiles("./static/index.html")
|
||||
|
||||
indexHtmlTemplate, err = template.ParseFiles("./static/index.html.tmpl")
|
||||
if err != nil {
|
||||
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) {
|
||||
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 {
|
||||
log.Printf("[!] Could not execute index.html template, error: %s\n", err)
|
||||
http.Error(w, "Error rendering template", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetPing(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[*] Received request %v %v\n", r.Method, r.URL)
|
||||
fmt.Fprintf(w, "PONG\n")
|
||||
func handleGetResult(w http.ResponseWriter, r *http.Request) {
|
||||
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 = 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) {
|
||||
@@ -97,6 +258,7 @@ func handlePostSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
log.Printf("[!] Error decoding request Body %v, error: %s\n", r.Body, err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
choicesJson, err := json.Marshal(choicesPost.Choices)
|
||||
@@ -123,7 +285,6 @@ func handlePostSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
log.Printf("[*] Persisted choices for poll %d, username %s to database\n",
|
||||
@@ -146,15 +307,22 @@ func main() {
|
||||
prepareStaticContent()
|
||||
|
||||
http.HandleFunc(
|
||||
"GET /api/ping",
|
||||
handleGetPing,
|
||||
"GET /favicon.ico",
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "There is no favicon.ico!", http.StatusBadRequest)
|
||||
},
|
||||
)
|
||||
|
||||
http.HandleFunc(
|
||||
"GET /",
|
||||
"GET /{pollId}",
|
||||
handleGetRoot,
|
||||
)
|
||||
|
||||
http.HandleFunc(
|
||||
"GET /{pollId}/result",
|
||||
handleGetResult,
|
||||
)
|
||||
|
||||
http.HandleFunc(
|
||||
"POST /api/submit",
|
||||
handlePostSubmit,
|
||||
@@ -165,6 +333,11 @@ func main() {
|
||||
handleOptionsSubmit,
|
||||
)
|
||||
|
||||
http.HandleFunc(
|
||||
"GET /api/result/{pollId}",
|
||||
handleGetResultApi,
|
||||
)
|
||||
|
||||
log.Printf("[*] Datefinder server listening on :%s\n", PORT)
|
||||
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||
log.Fatalf("[!] Could not start server on %s, error: %s\n", PORT, err)
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
@@ -44,16 +47,84 @@
|
||||
background-color: #87CEEB;
|
||||
/* 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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Datefinder</h1>
|
||||
<h2>{{ .PollName }}</h2>
|
||||
<div class="calendar" id="calendar"></div>
|
||||
<div>
|
||||
<input id="submit-name" type="text" placeholder="Enter your name" />
|
||||
<button id="submit-btn" type="submit" onclick="submit">Submit</button>
|
||||
</div>
|
||||
<label id="submit-name-label" for="submit-name">Enter Submitter Name!</label>
|
||||
<script>
|
||||
// Days of the week
|
||||
const daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
||||
@@ -91,47 +162,62 @@
|
||||
calendar.appendChild(dayColumn);
|
||||
});
|
||||
|
||||
const submitName = document.getElementById("submit-name")
|
||||
const submitBtn = document.getElementById("submit-btn")
|
||||
const submitName = document.getElementById("submit-name");
|
||||
const submitBtn = document.getElementById("submit-btn");
|
||||
const submitNameLabel = document.getElementById("submit-name-label");
|
||||
|
||||
submitBtn.onclick = function () {
|
||||
if (submitName.value == "") {
|
||||
console.log("Submitter name is missing.")
|
||||
return
|
||||
submitNameLabel.style.visibility = "visible";
|
||||
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))
|
||||
submitBtn.disabled = true;
|
||||
|
||||
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, {
|
||||
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
|
||||
body: JSON.stringify({
|
||||
pollId: {{ .PollId }},
|
||||
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);
|
||||
console.log(response);
|
||||
}
|
||||
return response.json(); // Parse the JSON response
|
||||
})
|
||||
.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 => {
|
||||
console.error('Error:', error); // Handle errors
|
||||
console.error("Error:", error); // Handle errors
|
||||
});
|
||||
*/
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</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