From 3a77eb1593804ad7f61cce14ead2c70f33978047 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Wed, 6 Dec 2023 17:24:33 +0100 Subject: [PATCH 01/10] backend: frontend: cleaned up the codebase and added more logging for debuging --- handlers/about_handler.go | 16 +++++++++++++--- handlers/add_monster_handler.go | 27 +++++++++++++++++++++++--- handlers/contact_handler.go | 9 ++++++++- handlers/form_handler.go | 31 ++++++++++++++++++++++++++---- handlers/main_handler.go | 11 +++++++++-- handlers/monster_table_handler.go | 9 ++++++++- handlers/submit_handler.go | 32 ++++++++++++++++++------------- main.go | 19 ++++++++++++++---- model/model.go | 31 ++++++++++++++++++++++++------ templates/about.html | 4 ++++ templates/contact.html | 1 - 11 files changed, 152 insertions(+), 38 deletions(-) diff --git a/handlers/about_handler.go b/handlers/about_handler.go index 12c02f8..fa7270c 100644 --- a/handlers/about_handler.go +++ b/handlers/about_handler.go @@ -7,18 +7,28 @@ import ( "net/http" ) +// AboutHandler returns an http.HandlerFunc that handles requests to the /about endpoint. +// It renders the about.html template and passes in the title "Dungeons & Dragons Monster Generator". func AboutHandler(content embed.FS) http.HandlerFunc { log.Print("AboutHandler called") + return func(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/about.html") + log.Print("AboutHandler request received") + + // Parse the template files + tmplFiles := []string{"templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/about.html"} + tmpl, err := template.ParseFS(content, tmplFiles...) if err != nil { + log.Printf("Template parsing error: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - err = tmpl.ExecuteTemplate(w, "about", map[string]interface{}{ + // Execute the template with the provided data + data := map[string]interface{}{ "Title": "Dungeons & Dragons Monster Generator", - }) + } + err = tmpl.ExecuteTemplate(w, "about", data) if err != nil { log.Printf("Template execution error: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/handlers/add_monster_handler.go b/handlers/add_monster_handler.go index bd84af3..8f08685 100644 --- a/handlers/add_monster_handler.go +++ b/handlers/add_monster_handler.go @@ -7,21 +7,28 @@ import ( "strconv" ) +// AddMonster is a http.HandlerFunc that adds a new monster to the Monsters slice. +// It expects a POST request with form data containing the details of the monster. +// The monster is then appended to the Monsters slice and a redirect response is sent. func AddMonster(Monsters *[]model.Monster) http.HandlerFunc { log.Print("AddMonster called") return func(w http.ResponseWriter, r *http.Request) { - // TODO + // Check if the request method is POST if r.Method != http.MethodPost { + log.Print("Method not allowed") http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } + // Parse the form data err := r.ParseForm() if err != nil { + log.Printf("Error parsing form data: %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } + // Create a new monster with the form data monster := model.Monster{ Name: r.FormValue("name"), Source: r.FormValue("source"), @@ -73,19 +80,33 @@ func AddMonster(Monsters *[]model.Monster) http.HandlerFunc { }, }, } + + // Lock the Monsters slice, append the monster, and unlock the slice mu.Lock() defer mu.Unlock() *Monsters = append(*Monsters, monster) - log.Printf("Monster hinzugefügt. Anzahl der Monster jetzt: %d\n", len(*Monsters)) + + // Log the number of monsters and redirect to the monster table + log.Printf("Monster added. Number of monsters now: %d\n", len(*Monsters)) http.Redirect(w, r, "/monsterTable", http.StatusFound) } } -// parseInt konvertiert einen String zu einem Integer und gibt 0 zurück, wenn die Konvertierung fehlschlägt +// parseInt converts a string to an integer and returns 0 if the conversion fails func parseInt(s string) int { + // Add logging statement to print the input string + log.Println("Input string:", s) + + // Atoi is used to convert the string to an integer i, err := strconv.Atoi(s) + // If there is an error in the conversion, return 0 and log the error if err != nil { + log.Println("Conversion error:", err) return 0 } + // Log the converted integer + log.Println("Converted integer:", i) + + // Return the converted integer return i } diff --git a/handlers/contact_handler.go b/handlers/contact_handler.go index a683377..a01cad4 100644 --- a/handlers/contact_handler.go +++ b/handlers/contact_handler.go @@ -7,15 +7,22 @@ import ( "net/http" ) +// ContactHandler handles the contact page request. +// It takes the content embed.FS as a parameter and returns an http.HandlerFunc. +// The returned http.HandlerFunc renders the contact page using the provided templates. func ContactHandler(content embed.FS) http.HandlerFunc { - log.Print("ContactHandler called") return func(w http.ResponseWriter, r *http.Request) { + log.Print("ContactHandler called") + + // Parse the templates tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/about.html", "templates/contact.html") if err != nil { + log.Printf("Template parsing error: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } + // Execute the contact template err = tmpl.ExecuteTemplate(w, "contact", map[string]interface{}{ "Title": "Dungeons & Dragons Monster Generator", }) diff --git a/handlers/form_handler.go b/handlers/form_handler.go index 878ba66..20e0f05 100644 --- a/handlers/form_handler.go +++ b/handlers/form_handler.go @@ -8,25 +8,48 @@ import ( "net/http" ) +// FormHandler returns an http.HandlerFunc that handles form submissions. +// It takes the content embed.FS, a pointer to a slice of model.Monster, +// and a filename string as parameters. +// The function parses the template files from the content FS, +// executes the template with the provided data, and renders it as a response. func FormHandler(content embed.FS, monsters *[]model.Monster, filename string) http.HandlerFunc { log.Print("FormHandler called") + + // Lock the mutex to ensure exclusive access to the monsters slice. mu.Lock() defer mu.Unlock() + return func(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/monsterForm.html", "templates/monster.html", "templates/monsterTable.html") + log.Print("FormHandler handler called") + + // Parse the template files. + templateFiles := []string{ + "templates/base.html", + "templates/header.html", + "templates/main.html", + "templates/footer.html", + "templates/monsterForm.html", + "templates/monster.html", + "templates/monsterTable.html", + } + tmpl, err := template.ParseFS(content, templateFiles...) if err != nil { + log.Printf("Template parsing error: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - err = tmpl.ExecuteTemplate(w, "base", map[string]interface{}{ + // Execute the template and render the response. + data := map[string]interface{}{ "Title": "Dungeons & Dragons Monster Generator", "Monsters": *monsters, - }) + } + err = tmpl.ExecuteTemplate(w, "base", data) if err != nil { log.Printf("Template execution error: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) } - log.Printf("Template mit %d Monstern gerendert\n", len(*monsters)) + log.Printf("Template rendered with %d Monsters\n", len(*monsters)) } } diff --git a/handlers/main_handler.go b/handlers/main_handler.go index 7f25b6a..f060e3b 100644 --- a/handlers/main_handler.go +++ b/handlers/main_handler.go @@ -8,19 +8,26 @@ import ( "net/http" ) -// MainHandler +// MainHandler handles the main HTTP request. +// It returns an http.HandlerFunc that renders the main page +// with the provided content and monsters. func MainHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc { - log.Print("MainHandler called") return func(w http.ResponseWriter, r *http.Request) { + log.Print("MainHandler called") + + // Parse the templates from the embedded file system tmpl, err := template.ParseFS(content, "templates/main.html", "templates/monsterForm.html", "templates/monster.html", "templates/monsterTable.html", "templates/base.html") if err != nil { + log.Printf("Template parsing error: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } + // Lock the mutex to ensure exclusive access to the monsters slice mu.Lock() defer mu.Unlock() + // Execute the main template with the provided data err = tmpl.ExecuteTemplate(w, "main", map[string]interface{}{ "Title": "Dungeons & Dragons Monster Generator", "Monsters": *monsters, diff --git a/handlers/monster_table_handler.go b/handlers/monster_table_handler.go index ebba144..4755386 100644 --- a/handlers/monster_table_handler.go +++ b/handlers/monster_table_handler.go @@ -8,15 +8,22 @@ import ( "net/http" ) +// MonsterTableHandler returns a http.HandlerFunc that handles requests to display a table of monsters. func MonsterTableHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc { - log.Print("AboutHandler called") + log.Print("MonsterTableHandler called") + return func(w http.ResponseWriter, r *http.Request) { + log.Print("Handling request for monster table") + + // Parse the template files tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/monsterTable.html", "templates/monster.html") if err != nil { + log.Printf("Template parsing error: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } + // Execute the template and pass the necessary data err = tmpl.ExecuteTemplate(w, "monsterTable", map[string]interface{}{ "Title": "Dungeons & Dragons Monster Generator", "Monsters": *monsters, diff --git a/handlers/submit_handler.go b/handlers/submit_handler.go index 02a6fde..4a7b50e 100644 --- a/handlers/submit_handler.go +++ b/handlers/submit_handler.go @@ -13,53 +13,59 @@ import ( var mu sync.Mutex -// submitHandler verarbeitet die Formulardaten +// SubmitHandler processes the form data. func SubmitHandler(content embed.FS, chars *[]model.Character, Monsters *[]model.Monster, filename string) http.HandlerFunc { log.Print("SubmitHandler called") return func(w http.ResponseWriter, r *http.Request) { + log.Print("SubmitHandler called") if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - // Formulardaten parsen + // Parse form data. err := r.ParseForm() if err != nil { + log.Printf("Error parsing form data: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - // Monster-Objekt erstellen + // Create monster object. filename := r.FormValue("filename") - // Charakter-Objekt erstellen oder aktualisieren + // Create or update character object. mu.Lock() defer mu.Unlock() char := model.GetOrCreateCharacter(filename, *chars) char.Monster = append(char.Monster, *Monsters...) - // Charakterdaten in JSON umwandeln + // Convert character data to JSON. charJSON, err := json.Marshal(char) if err != nil { + log.Printf("Error marshalling character data to JSON: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - // JSON-Daten in die Datei schreiben + // Write JSON data to file. err = model.WriteToFile(filename, charJSON) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - // Dateiinhalt lesen - fileContent, err := os.ReadFile(filename) - if err != nil { + log.Printf("Error writing JSON data to file: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - // Datei zum Download anbieten + // Read file contents. + fileContent, err := os.ReadFile(filename) + if err != nil { + log.Printf("Error reading file contents: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Offer file for download. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) w.Header().Set("Content-Type", "application/json") w.Write(fileContent) diff --git a/main.go b/main.go index 2060623..d52656d 100644 --- a/main.go +++ b/main.go @@ -20,10 +20,14 @@ var ( Monsters []model.Monster ) +// main is the entry point of the program. func main() { filename := "" + + // Print the message indicating that 'static' has been included. log.Printf("Eingebunden is %v\n", static) + // Set up the HTTP handlers for different routes. http.HandleFunc("/", handlers.FormHandler(content, &Monsters, filename)) http.HandleFunc("/submit", handlers.SubmitHandler(content, &chars, &Monsters, filename)) http.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.FS(content)))) @@ -33,27 +37,34 @@ func main() { http.HandleFunc("/contact", handlers.ContactHandler(content)) http.HandleFunc("/monsterTable", handlers.MonsterTableHandler(content, &Monsters)) - // Lade die CSS-Datei + // Load the CSS file. css, err := loadCSS(static) if err != nil { log.Fatal(err) } - // Füge eine Route für die CSS-Datei hinzu + // Add a route for the CSS file. http.HandleFunc("/static/darkly_bulmawatch.css", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css") w.Write([]byte(css)) }) + // Print the message indicating that the server has started. log.Print("Server gestartet, erreichbar unter http://localhost:8080") - http.ListenAndServe(":8080", nil) + + // Start the server and listen for incoming requests on port 8080. + log.Fatal(http.ListenAndServe(":8080", nil)) } -// loadCSS liest die CSS-Datei aus dem eingebetteten Dateisystem. +// loadCSS reads the CSS file from the embedded filesystem. +// It takes the content embed.FS as input. +// It returns the content of the CSS file as a string and an error if any. func loadCSS(content embed.FS) (string, error) { + // Read the CSS file "static/darkly_bulmawatch.css" from the embedded filesystem file, err := content.ReadFile("static/darkly_bulmawatch.css") if err != nil { return "", err } + // Convert the file content to a string and return return string(file), nil } diff --git a/model/model.go b/model/model.go index 1ad1bb0..fa6605e 100644 --- a/model/model.go +++ b/model/model.go @@ -2,6 +2,7 @@ package model import ( "fmt" + "log" "os" "time" ) @@ -89,32 +90,47 @@ type Source struct { ConvertedBy []string `json:"convertedBy"` } -// writeToFile schreibt Daten in eine Datei +// WriteToFile writes data to a file. +// It takes in a filename string and a data byte slice. +// It returns an error if there was an issue writing to the file, otherwise it returns nil. func WriteToFile(filename string, data []byte) error { + log.Println("Writing data to file:", filename) + + // Create a file with the given filename file, err := os.Create(filename) if err != nil { + log.Println("Error creating file:", err) return err } - defer file.Close() + defer func() { + if err := file.Close(); err != nil { + log.Println("Error closing file:", err) + } + }() - _, err = file.Write(data) + // Write the data to the file + n, err := file.Write(data) if err != nil { + log.Println("Error writing to file:", err) return err } + log.Printf("Successfully wrote %d bytes to file", n) return nil } -// getOrCreateCharacter gibt das aktuelle Charakterobjekt zurück oder erstellt ein neues +// getOrCreateCharacter returns the current character object or creates a new one func GetOrCreateCharacter(filename string, chars []Character) Character { + // Check if there is an empty character object for _, char := range chars { if char.Meta.DateLastModified == 0 { - // Ein leeres Charakterobjekt wurde gefunden + // Return the empty character object + log.Println("Returning existing character object") return char } } - // Erstelle ein neues Charakterobjekt + // Create a new character object now := time.Now().Unix() newChar := Character{ Meta: Meta{ @@ -134,7 +150,10 @@ func GetOrCreateCharacter(filename string, chars []Character) Character { Monster: []Monster{}, } + // Append the new character object to the list of characters chars = append(chars, newChar) + // Return the newly created character object + log.Println("Returning newly created character object") return newChar } diff --git a/templates/about.html b/templates/about.html index 9d29c17..6133b97 100644 --- a/templates/about.html +++ b/templates/about.html @@ -1,15 +1,19 @@ {{ define "about" }} +
+

About Us

+

Welcome to the Dungeons and Dragons Monster Generator website! We are a team of enthusiasts...

+ {{ end }} diff --git a/templates/contact.html b/templates/contact.html index 4b1dbea..ce0085d 100644 --- a/templates/contact.html +++ b/templates/contact.html @@ -13,7 +13,6 @@ From 2d0d0697fbb738ff266d4fccce549cd12b88cdaa Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 7 Dec 2023 08:08:04 +0100 Subject: [PATCH 02/10] change route handling to ServeMux for better performance and readability --- .gitignore | 1 + handlers/form_handler.go | 2 +- main.go | 28 +++++++++++++++------------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index cde0123..9fdf7ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist/ +test_data/ diff --git a/handlers/form_handler.go b/handlers/form_handler.go index 20e0f05..f809eeb 100644 --- a/handlers/form_handler.go +++ b/handlers/form_handler.go @@ -13,7 +13,7 @@ import ( // and a filename string as parameters. // The function parses the template files from the content FS, // executes the template with the provided data, and renders it as a response. -func FormHandler(content embed.FS, monsters *[]model.Monster, filename string) http.HandlerFunc { +func FormHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc { log.Print("FormHandler called") // Lock the mutex to ensure exclusive access to the monsters slice. diff --git a/main.go b/main.go index d52656d..546583c 100644 --- a/main.go +++ b/main.go @@ -24,27 +24,29 @@ var ( func main() { filename := "" + // Create a new ServeMux instance + routes := http.NewServeMux() + + // Register the handlers for different routes + routes.HandleFunc("/", handlers.FormHandler(content, &Monsters)) + routes.HandleFunc("/submit", handlers.SubmitHandler(content, &chars, &Monsters, filename)) + routes.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.FS(content)))) + routes.HandleFunc("/addMonster", handlers.AddMonster(&Monsters)) + routes.HandleFunc("/main", handlers.MainHandler(content, &Monsters)) + routes.HandleFunc("/about", handlers.AboutHandler(content)) + routes.HandleFunc("/contact", handlers.ContactHandler(content)) + routes.HandleFunc("/monsterTable", handlers.MonsterTableHandler(content, &Monsters)) // Print the message indicating that 'static' has been included. log.Printf("Eingebunden is %v\n", static) - // Set up the HTTP handlers for different routes. - http.HandleFunc("/", handlers.FormHandler(content, &Monsters, filename)) - http.HandleFunc("/submit", handlers.SubmitHandler(content, &chars, &Monsters, filename)) - http.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.FS(content)))) - http.HandleFunc("/addMonster", handlers.AddMonster(&Monsters)) - http.HandleFunc("/main", handlers.MainHandler(content, &Monsters)) - http.HandleFunc("/about", handlers.AboutHandler(content)) - http.HandleFunc("/contact", handlers.ContactHandler(content)) - http.HandleFunc("/monsterTable", handlers.MonsterTableHandler(content, &Monsters)) - // Load the CSS file. css, err := loadCSS(static) if err != nil { log.Fatal(err) } - // Add a route for the CSS file. - http.HandleFunc("/static/darkly_bulmawatch.css", func(w http.ResponseWriter, r *http.Request) { + // Add a route for the CSS file + routes.HandleFunc("/static/darkly_bulmawatch.css", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css") w.Write([]byte(css)) }) @@ -53,7 +55,7 @@ func main() { log.Print("Server gestartet, erreichbar unter http://localhost:8080") // Start the server and listen for incoming requests on port 8080. - log.Fatal(http.ListenAndServe(":8080", nil)) + log.Fatal(http.ListenAndServe(":8080", routes)) } // loadCSS reads the CSS file from the embedded filesystem. From eda80cf0f00975f678aa5f4b9376b980a0bb7daf Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Wed, 6 Dec 2023 19:28:26 +0100 Subject: [PATCH 03/10] backend: added unit tests for main.go Unit tests are testing all routes and Handlers --- go.mod | 8 +++ go.sum | 10 +++ main_test.go | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 go.sum create mode 100644 main_test.go diff --git a/go.mod b/go.mod index 8a6ca08..dfe5d47 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module ddServer go 1.21.4 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..d3acb55 --- /dev/null +++ b/main_test.go @@ -0,0 +1,176 @@ +package main + +import ( + "ddServer/handlers" + "log" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMain(t *testing.T) { + // Test case 1: Check if the root route ("/") returns the expected response. + t.Run("Root Route", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/", nil) + rr := httptest.NewRecorder() + handler := http.HandlerFunc(handlers.FormHandler(content, &Monsters)) + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + }) + + // Test case 2: Check if the "/submit" route returns the expected response. + t.Run("Submit Route", func(t *testing.T) { + dir, err := filepath.Abs("test_data") + if err != nil { + log.Fatal(err) + } + + filename := filepath.Join(dir, "monster.json") + EnsureDirExists(dir) + + formData := url.Values{ + "filename": {filename}, + "name": {"Monster Name"}, + "source": {"Monster Source"}, + "size": {"Monster Size"}, + "type": {"Monster Type"}, + "alignment": {"Monster Alignment"}, + "ac": {"15"}, // Beispielwert für AC + "acFrom": {"Natural Armor"}, // Beispielwert für AC From + "hpAverage": {"30"}, // Beispielwert für HP Average + "hpFormula": {"2d10+5"}, // Beispielwert für HP Formula + "speed": {"30"}, // Beispielwert für Speed + "str": {"16"}, // Beispielwert für Str + "dex": {"14"}, // Beispielwert für Dex + "con": {"18"}, // Beispielwert für Con + "int": {"10"}, // Beispielwert für Int + "wis": {"12"}, // Beispielwert für Wis + "cha": {"8"}, // Beispielwert für Cha + "saveDex": {"+2"}, // Beispielwert für Save Dex + "saveCon": {"+4"}, // Beispielwert für Save Con + "saveWis": {"+1"}, // Beispielwert für Save Wis + "perception": {"+3"}, // Beispielwert für Perception + "stealth": {"+2"}, // Beispielwert für Stealth + "damageRes": {"Fire, Cold"}, // Beispielwert für Damage Resistances + "senses": {"Darkvision"}, // Beispielwert für Senses + "languages": {"Common"}, // Beispielwert für Languages + "cr": {"2"}, // Beispielwert für CR + "traitName": {"Trait Name"}, // Beispielwert für Trait Name + "traitEntry": {"Trait Entry"}, // Beispielwert für Trait Entry + "actionName": {"Action Name"}, // Beispielwert für Action Name + "actionEntry": {"Action Entry"}, // Beispielwert für Action Entry + } + + log.Println("Writing data to file:", filename) + + req, _ := http.NewRequest("POST", "/submit", strings.NewReader(formData.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(handlers.SubmitHandler(content, &chars, &Monsters, filename)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + }) + + // Test case 3: Check if the "/images/" route returns the expected response. + t.Run("Images Route", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/images/", nil) + rr := httptest.NewRecorder() + handler := http.StripPrefix("/images/", http.FileServer(http.FS(content))) + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + }) + + // Test case 4: Check if the "/addMonster" route returns the expected response. + t.Run("AddMonster Route", func(t *testing.T) { + dir, err := filepath.Abs("test_data") + if err != nil { + log.Fatal(err) + } + + filename := filepath.Join(dir, "monster.json") + EnsureDirExists(dir) + formData := url.Values{ + "filename": {filename}, + "name": {"Monster Name"}, + "source": {"Monster Source"}, + "size": {"Monster Size"}, + "type": {"Monster Type"}, + "alignment": {"Monster Alignment"}, + "ac": {"15"}, // Beispielwert für AC + "acFrom": {"Natural Armor"}, // Beispielwert für AC From + "hpAverage": {"30"}, // Beispielwert für HP Average + "hpFormula": {"2d10+5"}, // Beispielwert für HP Formula + "speed": {"30"}, // Beispielwert für Speed + "str": {"16"}, // Beispielwert für Str + "dex": {"14"}, // Beispielwert für Dex + "con": {"18"}, // Beispielwert für Con + "int": {"10"}, // Beispielwert für Int + "wis": {"12"}, // Beispielwert für Wis + "cha": {"8"}, // Beispielwert für Cha + "saveDex": {"+2"}, // Beispielwert für Save Dex + "saveCon": {"+4"}, // Beispielwert für Save Con + "saveWis": {"+1"}, // Beispielwert für Save Wis + "perception": {"+3"}, // Beispielwert für Perception + "stealth": {"+2"}, // Beispielwert für Stealth + "damageRes": {"Fire, Cold"}, // Beispielwert für Damage Resistances + "senses": {"Darkvision"}, // Beispielwert für Senses + "languages": {"Common"}, // Beispielwert für Languages + "cr": {"2"}, // Beispielwert für CR + "traitName": {"Trait Name"}, // Beispielwert für Trait Name + "traitEntry": {"Trait Entry"}, // Beispielwert für Trait Entry + "actionName": {"Action Name"}, // Beispielwert für Action Name + "actionEntry": {"Action Entry"}, // Beispielwert für Action Entry + } + req, _ := http.NewRequest("POST", "/addMonster", strings.NewReader(formData.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(handlers.SubmitHandler(content, &chars, &Monsters, filename)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + }) + + // Test case 5: Check if the "/main" route returns the expected response. + t.Run("Main Route", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/main", nil) + rr := httptest.NewRecorder() + handler := http.HandlerFunc(handlers.MainHandler(content, &Monsters)) + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + }) + + // Test case 6: Check if the "/about" route returns the expected response. + t.Run("About Route", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/about", nil) + rr := httptest.NewRecorder() + handler := http.HandlerFunc(handlers.AboutHandler(content)) + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + }) + + // Test case 7: Check if the "/contact" route returns the expected response. + t.Run("Contact Route", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/contact", nil) + rr := httptest.NewRecorder() + handler := http.HandlerFunc(handlers.ContactHandler(content)) + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + }) +} + +func EnsureDirExists(dir string) error { + err := os.MkdirAll(dir, os.ModePerm) + if err != nil { + log.Println("Error creating directory:", err) + } + return err +} From d50f5dab03dec5c89b177b17f239b2053e1dd421 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 7 Dec 2023 13:24:57 +0100 Subject: [PATCH 04/10] backend: frontend: added new fields to model structs and frontend form --- handlers/add_monster_handler.go | 2 +- model/model.go | 76 ++-- templates/monsterForm.html | 659 +++++++++++++++++++++----------- 3 files changed, 488 insertions(+), 249 deletions(-) diff --git a/handlers/add_monster_handler.go b/handlers/add_monster_handler.go index 8f08685..da20ac2 100644 --- a/handlers/add_monster_handler.go +++ b/handlers/add_monster_handler.go @@ -63,7 +63,7 @@ func AddMonster(Monsters *[]model.Monster) http.HandlerFunc { Perception: r.FormValue("perception"), Stealth: r.FormValue("stealth"), }, - DamageRes: []string{r.FormValue("damageRes")}, + Resist: []string{r.FormValue("resist")}, Senses: []string{r.FormValue("senses")}, Languages: []string{r.FormValue("languages")}, CR: r.FormValue("cr"), diff --git a/model/model.go b/model/model.go index fa6605e..577eefc 100644 --- a/model/model.go +++ b/model/model.go @@ -9,28 +9,31 @@ import ( // Monster struct für die Daten des Monsters type Monster struct { - Save Save `json:"save"` - Skill Skill `json:"skill"` - HP HP `json:"hp"` - Source string `json:"source"` - CR string `json:"cr"` - Type string `json:"type"` - Name string `json:"name"` - DamageRes []string `json:"damageResistances"` - Traits []Trait `json:"trait"` - AC []AC `json:"ac"` - Alignment []string `json:"alignment"` - Senses []string `json:"senses"` - Languages []string `json:"languages"` - Size []string `json:"size"` - Actions []Action `json:"action"` - Speed Speed `json:"speed"` - Str int `json:"str"` - Dex int `json:"dex"` - Con int `json:"con"` - Int int `json:"int"` - Wis int `json:"wis"` - Cha int `json:"cha"` + Save Save `json:"save"` + Skill Skill `json:"skill"` + HP HP `json:"hp"` + Source string `json:"source"` + CR string `json:"cr"` + Type string `json:"type"` + Name string `json:"name"` + Vulnerable []string `json:"vulnerable"` + ConditionImmnue []string `json:"conditionImmune"` + Resist []string `json:"resist"` + Immune []string `json:"immune"` + Traits []Trait `json:"trait"` + AC []AC `json:"ac"` + Alignment []string `json:"alignment"` + Senses []string `json:"senses"` + Languages []string `json:"languages"` + Size []string `json:"size"` + Actions []Action `json:"action"` + Speed Speed `json:"speed"` + Str int `json:"str"` + Dex int `json:"dex"` + Con int `json:"con"` + Int int `json:"int"` + Wis int `json:"wis"` + Cha int `json:"cha"` } type AC struct { @@ -44,18 +47,41 @@ type HP struct { } type Speed struct { - Walk int `json:"walk"` + Walk int `json:"walk"` + burrow int `json:"burrow"` + climb int `json:"climb"` + fly int `json:"fly"` + swim int `json:"swim"` } type Save struct { Dex string `json:"dex"` Con string `json:"con"` Wis string `json:"wis"` + Cha string `json:"cha"` + Str string `json:"str"` + Int string `json:"int"` } type Skill struct { - Perception string `json:"perception"` - Stealth string `json:"stealth"` + Stealth string `json:"stealth"` + Acrobatics string `json:"acrobatics"` + AnimalHandling string `json:"animalHandling"` + Arcana string `json:"arcana"` + Athletics string `json:"athletics"` + Deception string `json:"deception"` + History string `json:"history"` + Insight string `json:"insight"` + Intimidation string `json:"intimidation"` + Investigation string `json:"investigation"` + Medicine string `json:"medicine"` + Nature string `json:"nature"` + Perception string `json:"perception"` + Performance string `json:"performance"` + Persuation string `json:"persuation"` + SleightOfHand string `json:"sleightOfHand"` + Survival string `json:"survival"` + Religion string `json:"religion"` } type Trait struct { diff --git a/templates/monsterForm.html b/templates/monsterForm.html index 51b6be1..6042658 100644 --- a/templates/monsterForm.html +++ b/templates/monsterForm.html @@ -1,266 +1,479 @@ {{ define "monsterform" }}
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+
+ +
+
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
+
+
+
+
+

+ Speed +

+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
-
-
-
- -
- -
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+

+ Save +

+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+

+ Skill +

+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
-
-
- -
- -
+
+
+ +
+ +
+
-
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
{{end}} From 0fd2775aa3671c11368592a07830ba94ac00d640 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 7 Dec 2023 13:59:46 +0100 Subject: [PATCH 05/10] backend: frontend: add missing fields --- handlers/add_monster_handler.go | 34 +++++++++++++++---- model/model.go | 4 +-- templates/monster.html | 24 +++++++++++++- templates/monsterForm.html | 58 +++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 9 deletions(-) diff --git a/handlers/add_monster_handler.go b/handlers/add_monster_handler.go index da20ac2..ed44271 100644 --- a/handlers/add_monster_handler.go +++ b/handlers/add_monster_handler.go @@ -58,15 +58,37 @@ func AddMonster(Monsters *[]model.Monster) http.HandlerFunc { Dex: r.FormValue("saveDex"), Con: r.FormValue("saveCon"), Wis: r.FormValue("saveWis"), + Str: r.FormValue("saveStr"), + Cha: r.FormValue("saveCha"), + Int: r.FormValue("saveInt"), }, Skill: model.Skill{ - Perception: r.FormValue("perception"), - Stealth: r.FormValue("stealth"), + Perception: r.FormValue("perception"), + Stealth: r.FormValue("stealth"), + Acrobatics: r.FormValue("acrobatics"), + AnimalHandling: r.FormValue("animalHandling"), + Arcana: r.FormValue("arcana"), + Athletics: r.FormValue("athletics"), + Deception: r.FormValue("deception"), + History: r.FormValue("history"), + Insight: r.FormValue("insight"), + Intimidation: r.FormValue("intimidation"), + Investigation: r.FormValue("investigation"), + Medicine: r.FormValue("medicine"), + Nature: r.FormValue("nature"), + Performance: r.FormValue("performance"), + Persuasion: r.FormValue("persuasion"), + SleightOfHand: r.FormValue("sleightOfHand"), + Survival: r.FormValue("survival"), + Religion: r.FormValue("religion"), }, - Resist: []string{r.FormValue("resist")}, - Senses: []string{r.FormValue("senses")}, - Languages: []string{r.FormValue("languages")}, - CR: r.FormValue("cr"), + Resist: []string{r.FormValue("resist")}, + ConditionImmune: []string{r.FormValue("conditionImmune")}, + Immune: []string{r.FormValue("immune")}, + Vulnerable: []string{r.FormValue("vulnerable")}, + Senses: []string{r.FormValue("senses")}, + Languages: []string{r.FormValue("languages")}, + CR: r.FormValue("cr"), Traits: []model.Trait{ { Name: r.FormValue("traitName"), diff --git a/model/model.go b/model/model.go index 577eefc..01bd301 100644 --- a/model/model.go +++ b/model/model.go @@ -17,7 +17,7 @@ type Monster struct { Type string `json:"type"` Name string `json:"name"` Vulnerable []string `json:"vulnerable"` - ConditionImmnue []string `json:"conditionImmune"` + ConditionImmune []string `json:"conditionImmune"` Resist []string `json:"resist"` Immune []string `json:"immune"` Traits []Trait `json:"trait"` @@ -78,7 +78,7 @@ type Skill struct { Nature string `json:"nature"` Perception string `json:"perception"` Performance string `json:"performance"` - Persuation string `json:"persuation"` + Persuasion string `json:"persuasion"` SleightOfHand string `json:"sleightOfHand"` Survival string `json:"survival"` Religion string `json:"religion"` diff --git a/templates/monster.html b/templates/monster.html index 2cd8b84..ef7f9c9 100644 --- a/templates/monster.html +++ b/templates/monster.html @@ -19,9 +19,31 @@ {{.Save.Dex}} {{.Save.Con}} {{.Save.Wis}} + {{.Save.Str}} + {{.Save.Con}} + {{.Save.Cha}} {{.Skill.Perception}} {{.Skill.Stealth}} - {{range .DamageRes}}{{.}}{{end}} + {{.Skill.Acrobatics}} + {{.Skill.AnimalHandling}} + {{.Skill.Arcana}} + {{.Skill.Athletics}} + {{.Skill.Deception}} + {{.Skill.History}} + {{.Skill.Insight}} + {{.Skill.Intimidation}} + {{.Skill.Investigation}} + {{.Skill.Medicine}} + {{.Skill.Nature}} + {{.Skill.Performance}} + {{.Skill.Persuasion}} + {{.Skill.SleightOfHand}} + {{.Skill.Survival}} + {{.Skill.Religion}} + {{range .Resist}}{{.}}{{end}} + {{range .Immune}}{{.}}{{end}} + {{range .Vulnerable}}{{.}}{{end}} + {{range .ConditionImmune}}{{.}}{{end}} {{range .Senses}}{{.}}{{end}} {{range .Languages}}{{.}}{{end}} {{.CR}} diff --git a/templates/monsterForm.html b/templates/monsterForm.html index 6042658..76aab71 100644 --- a/templates/monsterForm.html +++ b/templates/monsterForm.html @@ -406,6 +406,40 @@
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
@@ -419,6 +453,30 @@
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
From 1a4ac687f49e7587b8ce48c00f7decb261b03344 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 7 Dec 2023 15:55:47 +0100 Subject: [PATCH 06/10] backend: frontend: fix errors in model struct and add new fields to result table view --- handlers/about_handler.go | 4 +- handlers/add_monster_handler.go | 157 ++++++++++++++++-------------- model/model.go | 8 +- templates/main.html | 167 ++++++++++++++++++-------------- templates/monster.html | 4 + templates/monsterForm.html | 2 +- 6 files changed, 189 insertions(+), 153 deletions(-) diff --git a/handlers/about_handler.go b/handlers/about_handler.go index fa7270c..5f8a1dd 100644 --- a/handlers/about_handler.go +++ b/handlers/about_handler.go @@ -20,7 +20,7 @@ func AboutHandler(content embed.FS) http.HandlerFunc { tmpl, err := template.ParseFS(content, tmplFiles...) if err != nil { log.Printf("Template parsing error: %v\n", err) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -31,7 +31,7 @@ func AboutHandler(content embed.FS) http.HandlerFunc { err = tmpl.ExecuteTemplate(w, "about", data) if err != nil { log.Printf("Template execution error: %v\n", err) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) } } } diff --git a/handlers/add_monster_handler.go b/handlers/add_monster_handler.go index ed44271..a2306aa 100644 --- a/handlers/add_monster_handler.go +++ b/handlers/add_monster_handler.go @@ -24,84 +24,12 @@ func AddMonster(Monsters *[]model.Monster) http.HandlerFunc { err := r.ParseForm() if err != nil { log.Printf("Error parsing form data: %s", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusNoContent) return } // Create a new monster with the form data - monster := model.Monster{ - Name: r.FormValue("name"), - Source: r.FormValue("source"), - Size: []string{r.FormValue("size")}, - Type: r.FormValue("type"), - Alignment: []string{r.FormValue("alignment")}, - AC: []model.AC{ - { - AC: parseInt(r.FormValue("ac")), - From: []string{r.FormValue("acFrom")}, - }, - }, - HP: model.HP{ - Average: parseInt(r.FormValue("hpAverage")), - Formula: r.FormValue("hpFormula"), - }, - Speed: model.Speed{ - Walk: parseInt(r.FormValue("speed")), - }, - Str: parseInt(r.FormValue("str")), - Dex: parseInt(r.FormValue("dex")), - Con: parseInt(r.FormValue("con")), - Int: parseInt(r.FormValue("int")), - Wis: parseInt(r.FormValue("wis")), - Cha: parseInt(r.FormValue("cha")), - Save: model.Save{ - Dex: r.FormValue("saveDex"), - Con: r.FormValue("saveCon"), - Wis: r.FormValue("saveWis"), - Str: r.FormValue("saveStr"), - Cha: r.FormValue("saveCha"), - Int: r.FormValue("saveInt"), - }, - Skill: model.Skill{ - Perception: r.FormValue("perception"), - Stealth: r.FormValue("stealth"), - Acrobatics: r.FormValue("acrobatics"), - AnimalHandling: r.FormValue("animalHandling"), - Arcana: r.FormValue("arcana"), - Athletics: r.FormValue("athletics"), - Deception: r.FormValue("deception"), - History: r.FormValue("history"), - Insight: r.FormValue("insight"), - Intimidation: r.FormValue("intimidation"), - Investigation: r.FormValue("investigation"), - Medicine: r.FormValue("medicine"), - Nature: r.FormValue("nature"), - Performance: r.FormValue("performance"), - Persuasion: r.FormValue("persuasion"), - SleightOfHand: r.FormValue("sleightOfHand"), - Survival: r.FormValue("survival"), - Religion: r.FormValue("religion"), - }, - Resist: []string{r.FormValue("resist")}, - ConditionImmune: []string{r.FormValue("conditionImmune")}, - Immune: []string{r.FormValue("immune")}, - Vulnerable: []string{r.FormValue("vulnerable")}, - Senses: []string{r.FormValue("senses")}, - Languages: []string{r.FormValue("languages")}, - CR: r.FormValue("cr"), - Traits: []model.Trait{ - { - Name: r.FormValue("traitName"), - Entries: []string{r.FormValue("traitEntry")}, - }, - }, - Actions: []model.Action{ - { - Name: r.FormValue("actionName"), - Entries: []string{r.FormValue("actionEntry")}, - }, - }, - } + monster := parseMonster(r) // Lock the Monsters slice, append the monster, and unlock the slice mu.Lock() @@ -132,3 +60,84 @@ func parseInt(s string) int { // Return the converted integer return i } + +// parseMonster parses the Monster from monsterForm.html and return it. +func parseMonster(r *http.Request) model.Monster { + return model.Monster{ + Name: r.FormValue("name"), + Source: r.FormValue("source"), + Size: []string{r.FormValue("size")}, + Type: r.FormValue("type"), + Alignment: []string{r.FormValue("alignment")}, + AC: []model.AC{ + { + AC: parseInt(r.FormValue("ac")), + From: []string{r.FormValue("acFrom")}, + }, + }, + HP: model.HP{ + Average: parseInt(r.FormValue("hpAverage")), + Formula: r.FormValue("hpFormula"), + }, + Speed: model.Speed{ + Walk: parseInt(r.FormValue("walk")), + Burrow: parseInt(r.FormValue("burrow")), + Fly: parseInt(r.FormValue("fly")), + Swim: parseInt(r.FormValue("swim")), + Climb: parseInt(r.FormValue("climb")), + }, + Str: parseInt(r.FormValue("str")), + Dex: parseInt(r.FormValue("dex")), + Con: parseInt(r.FormValue("con")), + Int: parseInt(r.FormValue("int")), + Wis: parseInt(r.FormValue("wis")), + Cha: parseInt(r.FormValue("cha")), + Save: model.Save{ + Dex: r.FormValue("saveDex"), + Con: r.FormValue("saveCon"), + Wis: r.FormValue("saveWis"), + Str: r.FormValue("saveStr"), + Cha: r.FormValue("saveCha"), + Int: r.FormValue("saveInt"), + }, + Skill: model.Skill{ + Perception: r.FormValue("perception"), + Stealth: r.FormValue("stealth"), + Acrobatics: r.FormValue("acrobatics"), + AnimalHandling: r.FormValue("animalHandling"), + Arcana: r.FormValue("arcana"), + Athletics: r.FormValue("athletics"), + Deception: r.FormValue("deception"), + History: r.FormValue("history"), + Insight: r.FormValue("insight"), + Intimidation: r.FormValue("intimidation"), + Investigation: r.FormValue("investigation"), + Medicine: r.FormValue("medicine"), + Nature: r.FormValue("nature"), + Performance: r.FormValue("performance"), + Persuasion: r.FormValue("persuasion"), + SleightOfHand: r.FormValue("sleightOfHand"), + Survival: r.FormValue("survival"), + Religion: r.FormValue("religion"), + }, + Resist: []string{r.FormValue("resist")}, + ConditionImmune: []string{r.FormValue("conditionImmune")}, + Immune: []string{r.FormValue("immune")}, + Vulnerable: []string{r.FormValue("vulnerable")}, + Senses: []string{r.FormValue("senses")}, + Languages: []string{r.FormValue("languages")}, + CR: r.FormValue("cr"), + Traits: []model.Trait{ + { + Name: r.FormValue("traitName"), + Entries: []string{r.FormValue("traitEntry")}, + }, + }, + Actions: []model.Action{ + { + Name: r.FormValue("actionName"), + Entries: []string{r.FormValue("actionEntry")}, + }, + }, + } +} diff --git a/model/model.go b/model/model.go index 01bd301..0c8ec00 100644 --- a/model/model.go +++ b/model/model.go @@ -48,10 +48,10 @@ type HP struct { type Speed struct { Walk int `json:"walk"` - burrow int `json:"burrow"` - climb int `json:"climb"` - fly int `json:"fly"` - swim int `json:"swim"` + Burrow int `json:"burrow"` + Climb int `json:"climb"` + Fly int `json:"fly"` + Swim int `json:"swim"` } type Save struct { diff --git a/templates/main.html b/templates/main.html index eb95065..30ffc66 100644 --- a/templates/main.html +++ b/templates/main.html @@ -1,83 +1,106 @@ {{ define "main" }}
-
-
-
-

Monster Form

-
-
-
-
- -
+
+
+
+

Monster Form

-
-
-
- +
+ +
+ +
+
+
+
+
+ +
+
+ {{ template "monsterform" . }} + + +
-
- {{ template "monsterform" . }} - - - -
+
-
-
-
-
-

Existing Monsters

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ template "monsterTable" }} -
NameSourceSizeTypeAlignmentACAC FormHP AverageHP FormulaSpeedStrDexConIntWisChaSave DexSave ConSavve WisPerceptionStealthDamage ResistanceSensesLanguagesCRTrait NameTrait EntryAction NameAction Entry
-
+
+
+
+

Existing Monsters

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ template "monsterTable" }} +
NameSourceSizeTypeAlignmentACAC FormHP AverageHP FormulaWalkSwimBurrowClimbFlyStrDexConIntWisChaSave DexSave ConSave WisSave StrSave ConSave ChaPerceptionStealthAcrobaticsAnimalHandlingArcanaAthleticsDeceptionHistoryInsightIntimidationInvestigationMedicineNaturePerformancePersuasionSleightOfHandSurvivalReligionDamage ResistanceSensesLanguagesCRTrait NameTrait EntryAction NameAction Entry
+
+
-
{{ end }} diff --git a/templates/monster.html b/templates/monster.html index ef7f9c9..d3a59d6 100644 --- a/templates/monster.html +++ b/templates/monster.html @@ -10,6 +10,10 @@ {{.HP.Average}} {{.HP.Formula}} {{.Speed.Walk}} + {{.Speed.Swim}} + {{.Speed.Burrow}} + {{.Speed.Climb}} + {{.Speed.Fly}} {{.Str}} {{.Dex}} {{.Con}} diff --git a/templates/monsterForm.html b/templates/monsterForm.html index 76aab71..04e2dbc 100644 --- a/templates/monsterForm.html +++ b/templates/monsterForm.html @@ -22,7 +22,7 @@
- From 7ed139ff9734e1a11185821d1dd7b2ae2882f014 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 7 Dec 2023 16:00:37 +0100 Subject: [PATCH 07/10] frontend: add forgotten resistances to resulttable --- templates/main.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/main.html b/templates/main.html index 30ffc66..ed4581f 100644 --- a/templates/main.html +++ b/templates/main.html @@ -87,6 +87,9 @@ Survival Religion Damage Resistance + Damage Immune + Vulnerable + Condition Immune Senses Languages CR From 206b5bb5063f18ce6a79503b2bb4ac150c9ef243 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Tue, 12 Dec 2023 20:56:55 +0100 Subject: [PATCH 08/10] add everything necessary to be able to calculate skills interactive. added new route to main to handle skill field update added new handler to work with htmx requests for skill calculation made skill fields readonly adapted templates for reloading of skills only --- handlers/add_monster_handler.go | 3 +- handlers/form_handler.go | 1 + handlers/skill_calculation_handler.go | 142 +++++ main.go | 1 + templates/monsterForm.html | 785 ++++++++++---------------- templates/skills.html | 185 ++++++ 6 files changed, 644 insertions(+), 473 deletions(-) create mode 100644 handlers/skill_calculation_handler.go create mode 100644 templates/skills.html diff --git a/handlers/add_monster_handler.go b/handlers/add_monster_handler.go index a2306aa..38cb1a1 100644 --- a/handlers/add_monster_handler.go +++ b/handlers/add_monster_handler.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "strconv" + "strings" ) // AddMonster is a http.HandlerFunc that adds a new monster to the Monsters slice. @@ -67,7 +68,7 @@ func parseMonster(r *http.Request) model.Monster { Name: r.FormValue("name"), Source: r.FormValue("source"), Size: []string{r.FormValue("size")}, - Type: r.FormValue("type"), + Type: strings.ToLower(r.FormValue("type")), Alignment: []string{r.FormValue("alignment")}, AC: []model.AC{ { diff --git a/handlers/form_handler.go b/handlers/form_handler.go index f809eeb..c31cee4 100644 --- a/handlers/form_handler.go +++ b/handlers/form_handler.go @@ -31,6 +31,7 @@ func FormHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc { "templates/footer.html", "templates/monsterForm.html", "templates/monster.html", + "templates/skills.html", "templates/monsterTable.html", } tmpl, err := template.ParseFS(content, templateFiles...) diff --git a/handlers/skill_calculation_handler.go b/handlers/skill_calculation_handler.go new file mode 100644 index 0000000..48935cf --- /dev/null +++ b/handlers/skill_calculation_handler.go @@ -0,0 +1,142 @@ +package handlers + +import ( + "embed" + "html/template" + "log" + "net/http" + "strconv" +) + +// SkillCalculationHandler ist ein http.HandlerFunc, der von htmx getriggert wird, +// wenn der Benutzer Einträge in bestimmten Feldern macht, und dann die Skill-Felder befüllt. +func SkillCalculationHandler(content embed.FS) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log.Print("SkillCalculationHandler called") + + // Überprüfen Sie, ob die Anfrage eine POST-Anfrage ist. + if r.Method != http.MethodPost { + log.Print("Method not allowed") + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse Formulardaten. + err := r.ParseForm() + if err != nil { + log.Printf("Error parsing form data: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + tmplFiles := []string{"templates/base.html", "templates/header.html", "templates/skills.html", "templates/main.html", "templates/footer.html", "templates/about.html"} + tmpl, err := template.ParseFS(content, tmplFiles...) + if err != nil { + log.Printf("Template parsing error: %v\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + str := parseFieldValue(r.FormValue("str")) + dex := parseFieldValue(r.FormValue("dex")) + int := parseFieldValue(r.FormValue("int")) + cha := parseFieldValue(r.FormValue("cha")) + wis := parseFieldValue(r.FormValue("wis")) + cr := parseFieldValue(r.FormValue("cr")) + crBonus := calcBonus(cr) + + skillValues := map[string]string{ + "acrobatics": strconv.Itoa(calcAbilityScore(dex) + crBonus), + "animalHandling": strconv.Itoa(calcAbilityScore(wis) + crBonus), + "arcana": strconv.Itoa(calcAbilityScore(int) + crBonus), + "athletics": strconv.Itoa(calcAbilityScore(str) + crBonus), + "deception": strconv.Itoa(calcAbilityScore(cha) + crBonus), + "history": strconv.Itoa(calcAbilityScore(int) + crBonus), + "insight": strconv.Itoa(calcAbilityScore(wis) + crBonus), + "intimidation": strconv.Itoa(calcAbilityScore(cha) + crBonus), + "investigation": strconv.Itoa(calcAbilityScore(int) + crBonus), + "medicine": strconv.Itoa(calcAbilityScore(wis) + crBonus), + "nature": strconv.Itoa(calcAbilityScore(int) + crBonus), + "perception": strconv.Itoa(calcAbilityScore(wis) + crBonus), + "performance": strconv.Itoa(calcAbilityScore(cha) + crBonus), + "persuasion": strconv.Itoa(calcAbilityScore(cha) + crBonus), + "religion": strconv.Itoa(calcAbilityScore(int) + crBonus), + "sleightOfHand": strconv.Itoa(calcAbilityScore(dex) + crBonus), + "stealth": strconv.Itoa(calcAbilityScore(dex) + crBonus), + "survival": strconv.Itoa(calcAbilityScore(wis) + crBonus), + } + + err = tmpl.ExecuteTemplate(w, "skills", skillValues) + if err != nil { + log.Printf("Template execution error: %v\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + } + } +} + +func calcBonus(cr int) int { + if cr >= 0 && cr < 5 { + return 2 + } else if cr >= 5 && cr < 9 { + return 3 + } else if cr >= 9 && cr < 14 { + return 4 + } else if cr >= 14 && cr < 18 { + return 5 + } else if cr >= 18 && cr < 21 { + return 6 + } else if cr >= 21 && cr < 25 { + return 7 + } else if cr >= 25 && cr < 28 { + return 8 + } else if cr >= 28 && cr < 31 { + return 9 + } else { + return 0 + } +} + +func calcAbilityScore(val int) int { + if val < 2 { + return -5 + } else if val >= 2 && val < 4 { + return -4 + } else if val >= 4 && val < 6 { + return -3 + } else if val >= 6 && val < 8 { + return -2 + } else if val >= 8 && val < 10 { + return -1 + } else if val >= 10 && val < 12 { + return 0 + } else if val >= 12 && val < 14 { + return 1 + } else if val >= 14 && val < 16 { + return 2 + } else if val >= 16 && val < 18 { + return 3 + } else if val >= 18 && val < 20 { + return 4 + } else if val >= 20 && val < 22 { + return 5 + } else if val >= 22 && val < 24 { + return 6 + } else if val >= 24 && val < 26 { + return 7 + } else if val >= 26 && val < 28 { + return 8 + } else if val >= 28 && val < 30 { + return 9 + } else { + return 10 + } +} + +func parseFieldValue(value string) int { + val, err := strconv.Atoi(value) + if err != nil { + log.Printf("Error converting field value to integer: %v", err) + return 0 + } + return val +} diff --git a/main.go b/main.go index 546583c..d163e79 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ func main() { routes.HandleFunc("/about", handlers.AboutHandler(content)) routes.HandleFunc("/contact", handlers.ContactHandler(content)) routes.HandleFunc("/monsterTable", handlers.MonsterTableHandler(content, &Monsters)) + routes.HandleFunc("/calculate-skills", handlers.SkillCalculationHandler(content)) // Print the message indicating that 'static' has been included. log.Printf("Eingebunden is %v\n", static) diff --git a/templates/monsterForm.html b/templates/monsterForm.html index 04e2dbc..ca69873 100644 --- a/templates/monsterForm.html +++ b/templates/monsterForm.html @@ -1,537 +1,378 @@ {{ define "monsterform" }}
-
-
- -
- -
-
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
-
-
- -
-
- -
-
+
+
+
+ +
+
+
+
-
-
- -
- -
-
+
+
+
+ +
+ +
+
-
-
- -
- -
-
+
+
+ +
+ +
+
-
-
- -
- -
-
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
+
-
-
- -
- -
-
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
+
-
-

- Speed -

-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
+
+

+ Speed +

+
+
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
-
-
- -
- -
-
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
+
-
-
- -
- -
-
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
+
-
-

- Save -

-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
+
+

+ Save +

+
+
+
+
+
+
+ +
+
+
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
-
-
-

- Skill -

-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
-
+
+ {{ template "skills" }}
-
-
- -
- -
-
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
+
-
-
- -
- -
-
+
+
+ +
+ +
+
-
-
- -
- -
-
+
+
+ +
+ +
+
-
-
- -
- -
-
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
+
-
-
- -
- -
-
+
+
+ +
+ +
-
-
- -
- -
-
+
+
+
+ +
+ +
+
{{end}} diff --git a/templates/skills.html b/templates/skills.html new file mode 100644 index 0000000..9322260 --- /dev/null +++ b/templates/skills.html @@ -0,0 +1,185 @@ +{{ define "skills" }} +
+

+ Skill +

+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+{{ end }} From 445455f3417bd3544d0c05140777ff07294088b0 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 7 Mar 2024 08:14:27 +0100 Subject: [PATCH 09/10] added README.md --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..34428ea --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# ddServer + +This is a simple Webserver, which helps zou creating monsters for a dungeons & dragons game + +## Installation +Either clone the repo and build it your self. +- You need to have go installed for that + +Or download the latest release and start your server with +```bash +./ddServer +``` From e67cc692ba8cd3b0879b5abf41bcc8054ab7afd4 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Fri, 20 Jun 2025 13:21:08 +0200 Subject: [PATCH 10/10] refactor: replace interface with any --- handlers/about_handler.go | 2 +- handlers/contact_handler.go | 2 +- handlers/form_handler.go | 2 +- handlers/main_handler.go | 2 +- handlers/monster_table_handler.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/handlers/about_handler.go b/handlers/about_handler.go index 5f8a1dd..1d628e0 100644 --- a/handlers/about_handler.go +++ b/handlers/about_handler.go @@ -25,7 +25,7 @@ func AboutHandler(content embed.FS) http.HandlerFunc { } // Execute the template with the provided data - data := map[string]interface{}{ + data := map[string]any{ "Title": "Dungeons & Dragons Monster Generator", } err = tmpl.ExecuteTemplate(w, "about", data) diff --git a/handlers/contact_handler.go b/handlers/contact_handler.go index a01cad4..611abc2 100644 --- a/handlers/contact_handler.go +++ b/handlers/contact_handler.go @@ -23,7 +23,7 @@ func ContactHandler(content embed.FS) http.HandlerFunc { } // Execute the contact template - err = tmpl.ExecuteTemplate(w, "contact", map[string]interface{}{ + err = tmpl.ExecuteTemplate(w, "contact", map[string]any{ "Title": "Dungeons & Dragons Monster Generator", }) if err != nil { diff --git a/handlers/form_handler.go b/handlers/form_handler.go index c31cee4..1232463 100644 --- a/handlers/form_handler.go +++ b/handlers/form_handler.go @@ -42,7 +42,7 @@ func FormHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc { } // Execute the template and render the response. - data := map[string]interface{}{ + data := map[string]any{ "Title": "Dungeons & Dragons Monster Generator", "Monsters": *monsters, } diff --git a/handlers/main_handler.go b/handlers/main_handler.go index f060e3b..e4b575e 100644 --- a/handlers/main_handler.go +++ b/handlers/main_handler.go @@ -28,7 +28,7 @@ func MainHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc { defer mu.Unlock() // Execute the main template with the provided data - err = tmpl.ExecuteTemplate(w, "main", map[string]interface{}{ + err = tmpl.ExecuteTemplate(w, "main", map[string]any{ "Title": "Dungeons & Dragons Monster Generator", "Monsters": *monsters, }) diff --git a/handlers/monster_table_handler.go b/handlers/monster_table_handler.go index 4755386..f227962 100644 --- a/handlers/monster_table_handler.go +++ b/handlers/monster_table_handler.go @@ -24,7 +24,7 @@ func MonsterTableHandler(content embed.FS, monsters *[]model.Monster) http.Handl } // Execute the template and pass the necessary data - err = tmpl.ExecuteTemplate(w, "monsterTable", map[string]interface{}{ + err = tmpl.ExecuteTemplate(w, "monsterTable", map[string]any{ "Title": "Dungeons & Dragons Monster Generator", "Monsters": *monsters, })