summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaymaekers Luca <luca@spacehb.net>2025-11-05 14:15:06 +0100
committerRaymaekers Luca <luca@spacehb.net>2025-11-05 14:15:06 +0100
commite4d96353278d953c7746318e536f40b637476199 (patch)
treecac5a0f928656b6ff0255962892da017c759f0a5
parent8009da602986853a80b874f0d13ae87bb9e20d39 (diff)
Set textarea to readonly only; Added Lola in default people; Added reshuffle, unpickall and addlola debug routes; Encode data when there is no data file; Unset the seed since it does not really matter anymore after importing; Added validation with token; Changed UI logic to be based on one request and cache the data in local storage; Only show the list of people to the user so they cannot inspect html to cheat anymore.
-rw-r--r--index.tmpl.html224
-rw-r--r--main.go183
2 files changed, 248 insertions, 159 deletions
diff --git a/index.tmpl.html b/index.tmpl.html
index 25b7186..f339e4d 100644
--- a/index.tmpl.html
+++ b/index.tmpl.html
@@ -1,17 +1,17 @@
- <!DOCTYPE html>
- <html lang="fr" data-theme="dark">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
+<!DOCTYPE html>
+<html lang="fr" data-theme="dark">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta property="og:title" content="Cadeaux de noël 2025"/>
- <meta property="og:description" content="À qui offrir le cadeau de noël?"/>
+ <meta property="og:title" content="Cadeaux de noël 2025"/>
+ <meta property="og:description" content="À qui offrir le cadeau de noël?"/>
- <meta property="og:image" content="/static/favicon.ico" />
+ <meta property="og:image" content="/static/favicon.ico" />
- <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
- <style>
+ <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
+ <style>
h1 {
display: flex;
justify-content: center;
@@ -57,16 +57,45 @@ textarea {
}
}
- </style>
- <script>
+ </style>
+ <script>
+"use strict";
+
+//- Globals
let people =
[{{ range .People }}
- {"name": "{{.Name}}", "other":{{.Other}}, "has_picked": {{.HasPicked}},}, {{ end }}
+ {"name": "{{.Name}}", "has_picked": {{.HasPicked}},}, {{ end }}
];
let local_storage_key = "{{.KeyID}}";
-function setPageFirst() {
+let global_all_data = {
+ initialized: false,
+ token: null,
+ thisName: null,
+ thisWishlist: null,
+ otherName: null,
+ otherWishlist: null,
+};
+
+//- Helpers
+function findPersonByName(picking_person_name) {
+ let result = null;
+
+ for(let peopleIndex = 0; peopleIndex < people.length; peopleIndex += 1)
+ {
+ if(people[peopleIndex].name === picking_person_name)
+ {
+ result = people[peopleIndex];
+ break;
+ }
+ }
+
+ return result;
+}
+
+//- Pages
+function setPageChoose() {
document.body.innerHTML = `
<div>
<h1>Qui es-tu?</h1>
@@ -87,15 +116,7 @@ function setPageFirst() {
event.preventDefault();
let name = person.name;
- let thisPerson = null;
- for(let person_index = 0; person_index < people.length; person_index += 1)
- {
- if(people[person_index].name === name)
- {
- thisPerson = people[person_index];
- break;
- }
- }
+ let thisPerson = findPersonByName(name);
document.body.innerHTML = `
<h1>Tu es bien ${thisPerson.name}?</h1>
@@ -107,74 +128,86 @@ function setPageFirst() {
let yes_button = document.getElementById("yes");
let no_button = document.getElementById("no");
- yes_button.addEventListener("click", function(event) {
+ yes_button.addEventListener("click", async function(event) {
event.preventDefault();
- localStorage.setItem(local_storage_key, thisPerson.name);
- setPagePickingperson(thisPerson);
+ await fetch(`/choose/?name=${thisPerson.name}`)
+ .then(function(response) {
+ if (!response.ok) {
+ console.error('Network response was not ok');
+ }
+ return response.json();
+ })
+ .then(function(response) {
+ global_all_data.token = response.Token;
+ global_all_data.thisName = thisPerson.name;
+ global_all_data.thisWishlist = response.ThisWishlist;
+ global_all_data.otherName = response.OtherName;
+ global_all_data.otherWishlist = response.OtherWishlist;
+ global_all_data.initialized = true;
+
+ localStorage.setItem(local_storage_key, JSON.stringify(global_all_data));
+ })
+ .catch(function(error) {
+ console.error('There was a problem with the fetch operation:', error);
+ });
+
+ setPageThisPerson();
});
no_button.addEventListener("click", function(event) {
event.preventDefault();
- setPageFirst();
+ setPageChoose();
});
}); // button click even listener
} // has_picked
} // for loop
-
-}
-
-function findPersonByName(picking_person_name) {
- let result = null;
-
- for(let peopleIndex = 0; peopleIndex < people.length; peopleIndex += 1)
- {
- if(people[peopleIndex].name === picking_person_name)
- {
- result = people[peopleIndex];
- break;
- }
- }
-
- return result;
}
-function setPagePerson(person) {
+function setPageOtherPerson() {
let wishlistID = "wishlist";
document.body.innerHTML = `
<div>
- <h1>Tu as&nbsp;<span class="name">${person}</span>.</h1>
+ <h1>Tu as&nbsp;<span class="name">${global_all_data.otherName}</span>.</h1>
<h4>Sa liste des souhaits:</h4>
- <textarea disabled readonly rows="5" cols="40" id="${wishlistID}" placeholder="vide..."></textarea>
+ <textarea readonly rows="5" cols="40" id="${wishlistID}" placeholder="vide..."></textarea>
</div>
`;
- let list = null;
- fetch(`/list/?name=${person}`)
- .then(response => {
- if (!response.ok) {
- console.error('Network response was not ok');
- }
- return response.text();
- })
- .then(data => {
- let wishlist = document.getElementById(wishlistID);
- wishlist.value = data;
- })
- .catch(error => {
- console.error('There was a problem with the fetch operation:', error);
- });
+ let wishlist = document.getElementById(wishlistID);
+
+ wishlist.textContent = global_all_data.otherWishlist;
+
+ fetch(`/list/?name=${global_all_data.thisName}&token=${global_all_data.token}`)
+ .then(function(response) {
+ if (!response.ok) {
+ console.error('Network response was not ok');
+ }
+ return response.text();
+ })
+ .then(function(response) {
+ if(wishlist.textContent != response)
+ {
+ global_all_data.otherWishlist = response;
+ wishlist.textContent = global_all_data.otherWishlist;
+ localStorage.setItem(local_storage_key, JSON.stringify(global_all_data));
+ }
+ })
+ .catch(function(error) {
+ console.error('There was a problem with the fetch operation:', error);
+ });
+
}
-function setPagePickingperson(thisPerson) {
+function setPageThisPerson() {
let showPersonButtonID = "show-person";
let writeLetterButtonID = "write-letter";
document.body.innerHTML = `
<main class="container">
- <h1>Noël-An de &nbsp;<span class="name">${thisPerson.name}</span></h1>
+ <h1>Noël-An de &nbsp;<span class="name">${global_all_data.thisName}</span></h1>
<div class="buttons">
<button id="${showPersonButtonID}">Voir qui j'ai choisi</button>
- <button id="${writeLetterButtonID}">Écrire ma liste des souhaits</button>
+ <button id="${writeLetterButtonID}">Ma liste des souhaits</button>
</div>
<h4><b>Infos:</b></h4>
<ul>
@@ -189,7 +222,7 @@ function setPagePickingperson(thisPerson) {
showPersonButton.addEventListener("click", function(event) {
event.preventDefault();
- setPagePerson(people[thisPerson.other].name);
+ setPageOtherPerson();
});
let writeLetterButton = document.getElementById(writeLetterButtonID);
@@ -209,30 +242,21 @@ function setPagePickingperson(thisPerson) {
</div>
`;
-
let letterTextArea = document.getElementById(letterTextAreaID);
- fetch(`/list/?name=${thisPerson.name}`)
- .then(response => {
- if (!response.ok) {
- console.error('Network response was not ok');
- }
- return response.text();
- })
- .then(data => {
- letterTextArea.textContent = data;
- })
- .catch(error => {
- console.error('There was a problem with the fetch operation:', error);
- });
+ letterTextArea.textContent = global_all_data.thisWishlist;
let sendLetterButton = document.getElementById(sendLetterButtonID);
sendLetterButton.addEventListener("click", function(event) {
event.preventDefault();
- console.log("Sending list of", thisPerson.name);
+ console.log("Sending list of", global_all_data.thisName);
+
+ global_all_data.thisWishlist = letterTextArea.value
+ localStorage.setItem(local_storage_key, JSON.stringify(global_all_data));
let messageBody = {
- name: thisPerson.name,
+ name: global_all_data.thisName,
+ token: global_all_data.token,
text: letterTextArea.value,
};
const postBody = new URLSearchParams(messageBody).toString();
@@ -253,45 +277,43 @@ function setPagePickingperson(thisPerson) {
textElement.classList.add('fade');
let buttons = document.querySelector("div.buttons");
buttons.appendChild(textElement);
- setTimeout(() => {
+ setTimeout(function() {
textElement.remove();
}, 1000);
})
})
- .catch(error => console.error('Error:', error));
+ .catch(function(error) { console.error('Error:', error); });
}); // add event listener send button
-
}); // add event listener write letter
-
}
+//- Main
window.onload = function() {
-
// NOTE(luca): Users will expect going back to go to the home page so we do this manually.
- var stateObj = { foo: "bar" };
+ var stateObj = { home: "home" };
history.pushState(stateObj, "home", "/");
window.addEventListener('popstate', function(event) {
window.location.reload();
});
- let picking_person_name = localStorage.getItem(local_storage_key);
- let thisPerson = findPersonByName(picking_person_name);
-
- if(!picking_person_name)
+ let all_data = localStorage.getItem(local_storage_key);
+ if(!all_data)
{
- setPageFirst();
+ setPageChoose();
}
else
{
- setPagePickingperson(thisPerson);
+ let data_parsed = JSON.parse(all_data);
+ global_all_data = data_parsed;
+ console.log(global_all_data);
+
+ setPageThisPerson();
}
};
+ </script>
+ </head>
+ <body>
+ </body>
- </script>
-
- </head>
- <body>
- </body>
-
- </html>
+</html>
diff --git a/main.go b/main.go
index c16c169..1d63898 100644
--- a/main.go
+++ b/main.go
@@ -17,9 +17,9 @@
// 1. Have no ID
// 2. Choose name
// 3. Get (and store in local storage)
-// - other person name
+// - other person's name & list
+// - this person's name & list
// - token to make requests
-// - this name's id
// 4. Set this ID picked on server
//
// -> This data is used to know if the person has picked or not.
@@ -33,6 +33,7 @@ package main
//- Libraries
import (
"encoding/gob"
+ "encoding/json"
"errors"
"fmt"
"html/template"
@@ -41,6 +42,7 @@ import (
"net/http"
"os/signal"
"strings"
+ "strconv"
_ "embed"
"log"
@@ -69,11 +71,12 @@ var local_storage_key_id int64
//go:embed index.tmpl.html
var page_html string
-// This is the default list when it is not decoded from the data file see below
+//- Globals
var global_people = []Person{
{Name: "Nawel"},
{Name: "Tobias"},
{Name: "Luca"},
+ {Name: "Lola"},
{Name: "Aeris"},
{Name: "Lionel"},
{Name: "Aurélie"},
@@ -84,9 +87,26 @@ var global_people = []Person{
}
var global_version int = 1
-
var data_file_name string = "people.gob"
+//- Serializing
+func EncodeData(logger *log.Logger, file_name string) {
+ file, err := os.Create(file_name)
+ if err != nil {
+ logger.Fatalln(err)
+ }
+ defer file.Close()
+
+ enc := gob.NewEncoder(file)
+ if err := enc.Encode(global_version); err != nil {
+ logger.Fatalln(err)
+ }
+ if err := enc.Encode(global_people); err != nil {
+ logger.Fatalln(err)
+ }
+}
+
+
func DecodeData(logger *log.Logger, file_name string) {
file, err := os.Open(file_name)
if errors.Is(err, os.ErrNotExist) {
@@ -95,22 +115,21 @@ func DecodeData(logger *log.Logger, file_name string) {
if err != nil {
logger.Fatalln(err)
}
+ EncodeData(logger, file_name)
} else if err != nil {
logger.Fatalln(err)
} else {
dec := gob.NewDecoder(file)
- // Check the version
var version int
if err := dec.Decode(&version); err != io.EOF && err != nil {
logger.Fatalln(err)
}
if version != global_version {
- logger.Fatalf("Version mismatch for datafile@%s != package@%s\n", version, global_version)
+ logger.Fatalf("Version mismatch for datafile@%d != package@%d\n", version, global_version)
}
- // Decode the data and import it into global_people
if err := dec.Decode(&global_people); err != nil && err != io.EOF {
logger.Fatalln(err)
}
@@ -123,22 +142,7 @@ func DecodeData(logger *log.Logger, file_name string) {
}
}
-func EncodeData(logger *log.Logger, file_name string) {
- file, err := os.Create(file_name)
- if err != nil {
- logger.Fatalln(err)
- }
- defer file.Close()
-
- enc := gob.NewEncoder(file)
- if err := enc.Encode(global_version); err != nil {
- logger.Fatalln(err)
- }
- if err := enc.Encode(global_people); err != nil {
- logger.Fatalln(err)
- }
-}
-
+//- Person
func (person Person) String() string {
var digits string
if person.Token > 99999 {
@@ -196,24 +200,14 @@ func FindPersonByOtherName(people []Person, name string) (bool, *Person) {
return found, found_person
}
-// - Main
-func main() {
- logger := log.New(os.Stdout, "[noel] ", log.Ldate|log.Ltime)
-
- seed := rand.Int63()
- seed = 1623876946084255669
- logger.Println("seed:", seed)
- source := rand.NewSource(seed)
- seeded_rand := rand.New(source)
- rand.Seed(seed)
-
+func ShufflePeople(rand *rand.Rand, people []Person, logger *log.Logger) {
// Get a shuffled list that has following constaints
// 1. One person cannot choose itself
// 2. Every person has picked another person
var list []int
correct := false
for !correct {
- list = seeded_rand.Perm(len(global_people))
+ list = rand.Perm(len(people))
correct = true
for i, version := range list {
@@ -225,17 +219,37 @@ func main() {
}
}
- local_storage_key_id = seeded_rand.Int63()
-
// Initialize people
for index, value := range list {
- global_people[index].Other = value
- global_people[index].Token = seeded_rand.Int63()
+ people[index].Other = value
}
+}
+
+
+// - Main
+func main() {
+ logger := log.New(os.Stdout, "[noel] ", log.Ldate|log.Ltime)
+
+ seed := rand.Int63()
+ // seed = 1623876946084255669
+ logger.Println("seed:", seed)
+ source := rand.NewSource(seed)
+ seeded_rand := rand.New(source)
+ rand.Seed(seed)
+
+ // Init people
+ {
+ ShufflePeople(seeded_rand, global_people, logger)
+
+ local_storage_key_id = rand.Int63()
+ for index := range global_people {
+ global_people[index].Token = seeded_rand.Int63() / 10000
+ }
- DecodeData(logger, data_file_name)
+ DecodeData(logger, data_file_name)
- fmt.Println(global_people)
+ fmt.Println(global_people)
+ }
go func() {
c := make(chan os.Signal, 1)
@@ -252,45 +266,98 @@ func main() {
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./assets"))))
- http.HandleFunc("/unpickall/", func(writer http.ResponseWriter, request *http.Request) {
- for index := range global_people {
- global_people[index].HasPicked = false
- }
- fmt.Fprintln(writer, "Done.")
- })
+ // TODO: replace these by command line flags
+ // http.HandleFunc("/unpickall/", func(writer http.ResponseWriter, request *http.Request) {
+ // for index := range global_people {
+ // global_people[index].HasPicked = false
+ // }
+ // fmt.Fprintln(writer, "Done.")
+ // })
+
+ // http.HandleFunc("/shuffle/", func(writer http.ResponseWriter, request *http.Request) {
+ // ShufflePeople(seeded_rand, global_people, logger)
+ //
+ // fmt.Fprintln(writer, "Done.")
+ // })
+
+ // http.HandleFunc("/addlola/", func(writer http.ResponseWriter, request *http.Request) {
+ // global_people = append(global_people, Person{Name:"Lola", Token:seeded_rand.Int63()})
+ //
+ // fmt.Fprintln(writer, "Done.")
+ // })
http.HandleFunc("/list/", func(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodPost {
name := request.FormValue("name")
text := request.FormValue("text")
+ token := request.FormValue("token")
logger.Println("Edit wishlist of", name)
found, person := FindPersonByName(global_people, name)
if found {
- person.Wishlist = text
- person.HasPicked = true
- logger.Println(global_people)
-
- fmt.Fprintln(writer, "ok")
-
+ tokenString := strconv.FormatInt(person.Token, 10)
+ if token == tokenString {
+ person.Wishlist = text
+ person.HasPicked = true
+ logger.Println(global_people)
+
+ fmt.Fprintln(writer, "ok")
+ } else {
+ http.Error(writer, "invalid token", http.StatusNotFound)
+ }
} else {
- fmt.Fprintln(writer, "error")
+ http.Error(writer, "no such person", http.StatusNotFound)
}
} else if request.Method == http.MethodGet {
params := request.URL.Query()
name := params.Get("name")
+ token := params.Get("token")
+
+ found, person := FindPersonByName(global_people, name)
- found, picking_person := FindPersonByOtherName(global_people, name)
if found {
- picked_person := &global_people[picking_person.Other]
- fmt.Fprintln(writer, picked_person.Wishlist)
+ logger.Println(len(token))
+ tokenString := strconv.FormatInt(person.Token, 10)
+
+ if token == tokenString {
+ fmt.Fprint(writer, global_people[person.Other].Wishlist)
+ } else {
+ http.Error(writer, "invalid token", http.StatusNotFound)
+ }
} else {
- fmt.Fprintln(writer, "error")
+ http.Error(writer, "no such person", http.StatusNotFound)
}
}
})
+ http.HandleFunc("/choose/", func(writer http.ResponseWriter, request *http.Request) {
+ params := request.URL.Query()
+ name := params.Get("name")
+ found, person := FindPersonByName(global_people, name)
+ if found {
+ if !person.HasPicked {
+ person.HasPicked = true
+
+ type Response struct {
+ Token int64
+ ThisWishlist string
+ OtherName string
+ OtherWishlist string
+ }
+ other_person := global_people[person.Other]
+
+ response := Response{person.Token, person.Wishlist, other_person.Name, other_person.Wishlist}
+
+ json.NewEncoder(writer).Encode(response)
+ } else {
+ http.Error(writer, "person already picked", http.StatusNotFound)
+ }
+ } else {
+ http.Error(writer, "no such person", http.StatusNotFound)
+ }
+ })
+
// Execute the template before-hand since the contents won't change.
response := TemplateToString(page_html, global_people, seed)
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {