diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | index.tmpl.html | 294 | ||||
| -rw-r--r-- | main.go | 301 |
3 files changed, 468 insertions, 128 deletions
@@ -1 +1,2 @@ tmp +people.gob diff --git a/index.tmpl.html b/index.tmpl.html index 43e51c4..25b7186 100644 --- a/index.tmpl.html +++ b/index.tmpl.html @@ -1,5 +1,5 @@ <!DOCTYPE html> - <html lang="fr"> + <html lang="fr" data-theme="dark"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> @@ -18,16 +18,15 @@ h1 { } button { - font-size: 1em; + font-size: 1rem; } -#buttons { - margin: 1.5em; +.buttons { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; - gap: 1em; + gap: 1rem; } body { @@ -37,106 +36,255 @@ body { height: 50vh; } -span#name { - color: blue; +span.name { + color: #87bfcf; } +textarea { + display: block !important; +} + +.fade { + animation: fadeOut 1s forwards; /* Duration and direction of the animation */ +} + +@keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} </style> <script> let people = [{{ range .People }} - {"name": "{{.Name}}", "other": {{.Other}}}, {{ end }} + {"name": "{{.Name}}", "other":{{.Other}}, "has_picked": {{.HasPicked}},}, {{ end }} ]; -let page1_html = ` +let local_storage_key = "{{.KeyID}}"; + +function setPageFirst() { + document.body.innerHTML = ` <div> <h1>Qui es-tu?</h1> - <div id="buttons"> - {{ range .People }} - <button>{{ .Name }}</button> - {{ end }} + <div class="buttons"> </div> -</div> -`; +</div>`; -let local_storage_key = "clicked_{{.Seed}}"; - -function setPage1() { - document.body.innerHTML = page1_html; - let buttons = document.querySelectorAll("button"); - for(let index = 0; index < buttons.length; index += 1) + let buttons = document.querySelector("div.buttons"); + for(let index = 0; index < people.length; index += 1) { - let button = buttons[index]; - button.addEventListener("click", function(event) { - event.preventDefault(); - - let name = button.innerText; - let person = null; - for(let person_index = 0; person_index < people.length; person_index += 1) - { - if(people[person_index].name === name) + let person = people[index]; + if(!person.has_picked) + { + let button = document.createElement('button'); + button.textContent = people[index].name; + buttons.appendChild(button); + button.addEventListener("click", function(event) { + event.preventDefault(); + + let name = person.name; + let thisPerson = null; + for(let person_index = 0; person_index < people.length; person_index += 1) { - person = people[person_index]; - break; + if(people[person_index].name === name) + { + thisPerson = people[person_index]; + break; + } } - } - {{ if false }} - fetch('/person/', { - method: 'POST', - headers: - { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: "name=" + person.name - }) - .then(function(response) { - response.text(). - then(function(text_response) { - if(text_response !== 'ok') { - console.log("Error"); + document.body.innerHTML = ` + <h1>Tu es bien ${thisPerson.name}?</h1> + <div class="buttons"> + <button id="yes">Oui</button> + <button id="no">Non</button> + </div>`; + + let yes_button = document.getElementById("yes"); + let no_button = document.getElementById("no"); + + yes_button.addEventListener("click", function(event) { + event.preventDefault(); + localStorage.setItem(local_storage_key, thisPerson.name); + setPagePickingperson(thisPerson); + }); + + no_button.addEventListener("click", function(event) { + event.preventDefault(); + setPageFirst(); + }); + + }); // 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) { + let wishlistID = "wishlist"; + document.body.innerHTML = ` + <div> + <h1>Tu as <span class="name">${person}</span>.</h1> + <h4>Sa liste des souhaits:</h4> + <textarea disabled 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'); } - }) - }) - .catch(error => console.error('Error:', error)); - {{ end }} + 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); + }); +} - document.body.innerHTML = ` - <h1>Tu es bien ${person.name}?</h1> - <div id="buttons"> - <button id="yes">Oui</button> - <button id="no">Non</button> - </div>`; +function setPagePickingperson(thisPerson) { + let showPersonButtonID = "show-person"; + let writeLetterButtonID = "write-letter"; - let yes_button = document.getElementById("yes"); - let no_button = document.getElementById("no"); + document.body.innerHTML = ` + <main class="container"> + <h1>Noël-An de <span class="name">${thisPerson.name}</span></h1> + <div class="buttons"> + <button id="${showPersonButtonID}">Voir qui j'ai choisi</button> + <button id="${writeLetterButtonID}">Écrire ma liste des souhaits</button> + </div> + <h4><b>Infos:</b></h4> + <ul> + <li>Ne dévoilez à persone qui vous avez tiré au sort!</li> + <li>Vous pouvez écrire une liste de souhaits, la personne ayant tiré votre nom y a accès.</li> + <li>Il n'est pas obligé de respecter la liste ou d'en écrire une. Elle sert pour inspirer des idées de cadeaux.</li> + </ul> + </main> + `; - yes_button.addEventListener("click", function(event) { - event.preventDefault(); - let other_person = people[person.other]; - document.body.innerHTML = `<h1>Tu as <span id="name">${other_person.name}</span>.</h1>`; - localStorage.setItem(local_storage_key, other_person.name); + let showPersonButton = document.getElementById(showPersonButtonID); + showPersonButton.addEventListener("click", function(event) { + event.preventDefault(); + + setPagePerson(people[thisPerson.other].name); + }); + + let writeLetterButton = document.getElementById(writeLetterButtonID); + writeLetterButton.addEventListener("click", function(event) { + event.preventDefault(); + + let sendLetterButtonID = "send-letter"; + let letterTextAreaID = "letter-text"; + + document.body.innerHTML = ` + <div> + <h1>Liste des souhaits</h1> + <textarea id="${letterTextAreaID}" rows="5" cols="40" placeholder="..."></textarea> + <div class="buttons"> + <button id="${sendLetterButtonID}">sauvegarder</button> + </div> + </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); }); - no_button.addEventListener("click", function(event) { + let sendLetterButton = document.getElementById(sendLetterButtonID); + sendLetterButton.addEventListener("click", function(event) { event.preventDefault(); - setPage1(); - }); - }); // button click even listener - } + console.log("Sending list of", thisPerson.name); + + let messageBody = { + name: thisPerson.name, + text: letterTextArea.value, + }; + const postBody = new URLSearchParams(messageBody).toString(); + + fetch('/list/', { + method: 'POST', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: postBody, + }) + .then(function(response) { + response.text(). + then(function(text_response) { + const textElement = document.createElement('div'); + textElement.textContent = 'Sauvegardé'; + textElement.classList.add('fade'); + let buttons = document.querySelector("div.buttons"); + buttons.appendChild(textElement); + setTimeout(() => { + textElement.remove(); + }, 1000); + }) + }) + .catch(error => console.error('Error:', error)); + + }); // add event listener send button + + }); // add event listener write letter + } window.onload = function() { - let clicked = localStorage.getItem(local_storage_key); - if(!clicked) + + // NOTE(luca): Users will expect going back to go to the home page so we do this manually. + var stateObj = { foo: "bar" }; + 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) { - setPage1(); + setPageFirst(); } else { - document.body.innerHTML = `<h1>Tu as déjà choisi.</h1>`; + setPagePickingperson(thisPerson); } }; @@ -1,32 +1,76 @@ +// Documentation +// +// Secret santa app that's random so people don't have to worry about the picking process. +// No emails or login, just use the domain. +// +// Run it with `go run .` +// +// +// TODOs +// +// TODO: Command line flags +// - serve to serve it +// - unpick to unpick all +// - editor (to have one where i can edit the gob file manually) +// +// TODO: Make it safer by doing this. +// 1. Have no ID +// 2. Choose name +// 3. Get (and store in local storage) +// - other person name +// - 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. +// +// 1. Already have an ID (in local storage) +// 2. Get +// - Wishlists (for sync) + package main //- Libraries -import "fmt" -import "net/http" -import "math/rand" -import "html/template" -import "strings" -import _ "embed" +import ( + "encoding/gob" + "errors" + "fmt" + "html/template" + "io" + "math/rand" + "net/http" + "os/signal" + "strings" + + _ "embed" + "log" + "os" +) // - Types type Person struct { - Name string - Other int + Name string + Other int + Wishlist string + HasPicked bool + Token int64 } type PageData struct { - People []Person - Seed int64 + People []Person + KeyID int64 } -var DEBUG = true - //- Globals +var DEBUG = true +var local_storage_key_id int64 + //go:embed index.tmpl.html var page_html string -var people = []Person{ +// This is the default list when it is not decoded from the data file see below +var global_people = []Person{ {Name: "Nawel"}, {Name: "Tobias"}, {Name: "Luca"}, @@ -38,81 +82,228 @@ var people = []Person{ {Name: "Yves"}, {Name: "Marthe"}, } -var people_count = len(people) + +var global_version int = 1 + +var data_file_name string = "people.gob" + +func DecodeData(logger *log.Logger, file_name string) { + file, err := os.Open(file_name) + if errors.Is(err, os.ErrNotExist) { + logger.Println("Datafile does not exist. Creating", file_name) + file, err = os.Create(file_name) + if err != nil { + logger.Fatalln(err) + } + + } 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) + } + + // Decode the data and import it into global_people + if err := dec.Decode(&global_people); err != nil && err != io.EOF { + logger.Fatalln(err) + } + + logger.Printf("Imported %d people.\n", len(global_people)) + + if err := file.Close(); err != nil { + logger.Fatalln(err) + } + } +} + +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 (person Person) String() string { + var digits string + if person.Token > 99999 { + digits = fmt.Sprintf("%d", person.Token)[:6] + } else { + digits = fmt.Sprintf("%d", person.Token) + } + + return fmt.Sprintf("%s_%s(%t)\n%s\n", + person.Name, digits, person.HasPicked, person.Wishlist) +} func TemplateToString(page_html string, people []Person, seed int64) string { - var buf strings.Builder - template_response, err := template.New("roulette").Parse(page_html) - if err != nil { - fmt.Println(err) - } - err = template_response.ExecuteTemplate(&buf, "roulette", PageData{people, seed}) - if err != nil { - fmt.Println(err) - } - response := buf.String() + var buf strings.Builder + template_response, err := template.New("roulette").Parse(page_html) + if err != nil { + fmt.Println(err) + } + err = template_response.ExecuteTemplate(&buf, "roulette", PageData{people, local_storage_key_id}) + if err != nil { + fmt.Println(err) + } + response := buf.String() - return response + return response +} + +func FindPersonByName(people []Person, name string) (bool, *Person) { + var found_person *Person + var found bool + + for index, value := range people { + if name == value.Name { + found_person = &people[index] + found = true + break + } + } + + return found, found_person +} + +func FindPersonByOtherName(people []Person, name string) (bool, *Person) { + var found_person *Person + var found bool + + for index, person := range people { + if name == people[person.Other].Name { + found = true + found_person = &people[index] + break + } + } + + return found, found_person } // - Main func main() { - var seed int64 - seed = rand.Int63() - seed = 1924480304604450476 - fmt.Println("seed:", seed) + logger := log.New(os.Stdout, "[noel] ", log.Ldate|log.Ltime) - src := rand.NewSource(seed) - r := rand.New(src) + seed := rand.Int63() + seed = 1623876946084255669 + logger.Println("seed:", seed) + source := rand.NewSource(seed) + seeded_rand := rand.New(source) rand.Seed(seed) + // 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 = r.Perm(people_count) + list = seeded_rand.Perm(len(global_people)) correct = true - for i, v := range list { - if v == i { - fmt.Println("incorrect, need to reshuffle") + for i, version := range list { + if version == i { + logger.Println("incorrect, need to reshuffle") correct = false break } } } - for i, v := range list { - people[i].Other = v + 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() } + DecodeData(logger, data_file_name) + + fmt.Println(global_people) + + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + + EncodeData(logger, data_file_name) + + logger.Println("data saved.") + os.Exit(0) + }() + + defer EncodeData(logger, data_file_name) + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./assets")))) - http.HandleFunc("/person/", func(writer http.ResponseWriter, request *http.Request) { - name := request.FormValue("name") + 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("/list/", func(writer http.ResponseWriter, request *http.Request) { + if request.Method == http.MethodPost { + name := request.FormValue("name") + text := request.FormValue("text") + + logger.Println("Edit wishlist of", name) + found, person := FindPersonByName(global_people, name) + + if found { + person.Wishlist = text + person.HasPicked = true + logger.Println(global_people) - var found bool - for _, value := range people { - if name == value.Name { - found = true + fmt.Fprintln(writer, "ok") + + } else { + fmt.Fprintln(writer, "error") } - } + } else if request.Method == http.MethodGet { + params := request.URL.Query() + name := params.Get("name") - if found { - fmt.Fprintln(writer, "ok") - } else { - fmt.Fprintln(writer, "error") + found, picking_person := FindPersonByOtherName(global_people, name) + if found { + picked_person := &global_people[picking_person.Other] + fmt.Fprintln(writer, picked_person.Wishlist) + } else { + fmt.Fprintln(writer, "error") + } } }) // Execute the template before-hand since the contents won't change. - - response := TemplateToString(page_html, people, seed); - + response := TemplateToString(page_html, global_people, seed) http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { if DEBUG { - response = TemplateToString(page_html, people, seed); + buffer, err := os.ReadFile("index.tmpl.html") + if err != nil { + fmt.Print(err) + } + + file_contents := string(buffer) + response = TemplateToString(file_contents, global_people, seed) } - fmt.Fprint(writer, response) + fmt.Fprint(writer, response) }) var address string @@ -121,8 +312,8 @@ func main() { } else { address = "localhost:15118" } - fmt.Printf("Listening on http://%s\n", address) - if err := http.ListenAndServe(address, nil); err != nil { + logger.Printf("Listening on http://%s\n", address) + if err := http.ListenAndServe(address, nil); err != nil { panic(err) } |
