diff options
| author | Raymaekers Luca <luca@spacehb.net> | 2025-11-05 14:15:06 +0100 |
|---|---|---|
| committer | Raymaekers Luca <luca@spacehb.net> | 2025-11-05 14:15:06 +0100 |
| commit | e4d96353278d953c7746318e536f40b637476199 (patch) | |
| tree | cac5a0f928656b6ff0255962892da017c759f0a5 | |
| parent | 8009da602986853a80b874f0d13ae87bb9e20d39 (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.html | 224 | ||||
| -rw-r--r-- | main.go | 183 |
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 <span class="name">${person}</span>.</h1> + <h1>Tu as <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 <span class="name">${thisPerson.name}</span></h1> + <h1>Noël-An de <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> @@ -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) { |
