Compare commits

...

23 commits
v0.0.1 ... main

Author SHA1 Message Date
e67cc692ba refactor: replace interface with any 2025-06-20 13:21:08 +02:00
Patryk Hegenberg
445455f341 added README.md 2024-03-07 08:14:27 +01:00
Patryk Hegenberg
206b5bb506 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
2023-12-12 20:56:55 +01:00
Patryk Hegenberg
7ed139ff97 frontend: add forgotten resistances to resulttable 2023-12-07 16:00:37 +01:00
Patryk Hegenberg
1a4ac687f4 backend: frontend: fix errors in model struct and add new fields to result table view 2023-12-07 15:55:47 +01:00
Patryk Hegenberg
0fd2775aa3 backend: frontend: add missing fields 2023-12-07 13:59:46 +01:00
Patryk Hegenberg
d50f5dab03 backend: frontend: added new fields to model structs and frontend form 2023-12-07 13:24:57 +01:00
Patryk Hegenberg
eda80cf0f0 backend: added unit tests for main.go
Unit tests are testing all routes and Handlers
2023-12-07 08:14:03 +01:00
Patryk Hegenberg
2d0d0697fb change route handling to ServeMux for better performance and readability 2023-12-07 08:08:04 +01:00
Patryk Hegenberg
3a77eb1593 backend: frontend: cleaned up the codebase and added more logging for debuging 2023-12-06 17:24:33 +01:00
Patryk Hegenberg
faf7c2f782 frontend: remove error causing hx-boost attribute on form and make title responsive and centered 2023-12-06 13:09:43 +01:00
Patryk Hegenberg
309ade3e72 frontend: fix bugs according to monsterTable 2023-12-06 11:42:44 +01:00
Patryk Hegenberg
b82e138c4d added content to about and contact page and added main-handler to achive spa-functionality 2023-12-06 09:12:49 +01:00
Patryk Hegenberg
d60d1df51e add dark-mode and theme switcher 2023-12-06 08:38:23 +01:00
Patryk Hegenberg
4f69762b1f improve usability and styling
change table layout for Form to flexbox using bulma tiles
2023-12-05 19:58:13 +01:00
Patryk Hegenberg
e230c0e10d switch classes from daisyui to bulma 2023-12-05 14:26:03 +01:00
Patryk Hegenberg
b8b51f1870 frontend: add tableview to display added monsters 2023-12-05 12:47:57 +01:00
Patryk Hegenberg
e556883972 frontend: revise website to display form in a table 2023-12-04 13:54:14 +01:00
Patryk Hegenberg
2d3f1ef951 added logging 2023-12-04 11:14:37 +01:00
Patryk Hegenberg
a408e1487d restructured and refactored codebase
For better code organisation the entire Codebase has been restructured and cleaned up. handlers have been separated into an own package, as well as the model.
2023-12-04 11:04:06 +01:00
Patryk Hegenberg
4dd9c18832 added a basic function to add multiple Monsters 2023-11-29 22:36:00 +01:00
Patryk Hegenberg
bf021090ab seperate main template from monster form 2023-11-29 16:17:36 +01:00
Patryk Hegenberg
24c11cbd50 seperator forms.html into templates and improve page styling 2023-11-29 13:53:29 +01:00
28 changed files with 1969 additions and 456 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
dist/ dist/
test_data/

12
README.md Normal file
View file

@ -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
```

View file

@ -1,191 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D&D Monster Form</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@2.31.0/dist/full.css" rel="stylesheet" type="text/css">
<style>
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
font-family: 'arial', sans-serif;
transition: background-color 0.3s, color 0.3s;
}
.form-box {
max-width: 700px;
width: 100%;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.form-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
padding: 10px;
}
.form-item2 {
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
</style>
</head>
<body class="bg-gray-100 h-screen flex items-center justify-center">
<div data-theme="dark" class="bg-white p-6 rounded-lg shadow-md w-full max-w-lg">
<div class="navbar bg-base-100">
<a class="btn btn-ghost text-xl">Dungeons & Dragons</a>
</div>
<div class="container mx-auto card w-96 bg-base-100 shadow-xl">
<div class="card-body form-box">
<h2 class="card-title">Monster Form</h2>
<form action="/submit" method="post"
class="grid grid-columns-2 gap-4 space-y-4 grid h-screen place-items-center">
<div class="form-item">
<label for="filename">Filename:</label>
<input type="text" name="filename" required placeholder="Dateiname"
class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="name">Monster Name:</label>
<input type="text" name="name" required placeholder="Type here"
class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="source">Monster Source:</label>
<input type="text" name="source" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="size">Size:</label>
<input type="text" name="size" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="type">Type:</label>
<input type="text" name="type" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="alignment">Alignment:</label>
<input type="text" name="alignment" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="ac">AC:</label>
<input type="number" name="ac" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="acFrom">AC From:</label>
<input type="text" name="acFrom" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="hpAverage">HP Average:</label>
<input type="number" name="hpAverage" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="hpFormula">HP Formula:</label>
<input type="text" name="hpFormula" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="speed">Speed:</label>
<input type="number" name="speed" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="str">Str:</label>
<input type="number" name="str" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="dex">Dex:</label>
<input type="number" name="dex" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="con">Con:</label>
<input type="number" name="con" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="int">Int:</label>
<input type="number" name="int" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="wis">Wis:</label>
<input type="number" name="wis" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="cha">Cha:</label>
<input type="number" name="cha" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="saveDex">Save Dex:</label>
<input type="text" name="saveDex" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="saveCon">Save Con:</label>
<input type="text" name="saveCon" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="saveWis">Save Wis:</label>
<input type="text" name="saveWis" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="perception">Perception:</label>
<input type="text" name="perception" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="stealth">Stealth:</label>
<input type="text" name="stealth" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="damageRes">Damage Resistances:</label>
<input type="text" name="damageRes" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="senses">Senses:</label>
<input type="text" name="senses" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="languages">Languages:</label>
<input type="text" name="languages" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="cr">CR:</label>
<input type="text" name="cr" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="traitName">Trait Name:</label>
<input type="text" name="traitName" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="traitEntry">Trait Entry:</label>
<input type="text" name="traitEntry" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="actionName">Action Name:</label>
<input type="text" name="actionName" required class="input input-bordered w-full max-w-xs"><br>
</div>
<div class="form-item">
<label for="actionEntry">Action Entry:</label>
<input type="text" name="actionEntry" required class="input input-bordered w-full max-w-xs"><br>
</div>
<input type="hidden" name="filename" value="{{.Filename}}">
<div class="form-item2">
<input type="submit" value="Submit" class="btn">
</div>
</form>
</div>
</div>
</div>
</body>
</html>

8
go.mod
View file

@ -1,3 +1,11 @@
module ddServer module ddServer
go 1.21.4 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
)

10
go.sum Normal file
View file

@ -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=

37
handlers/about_handler.go Normal file
View file

@ -0,0 +1,37 @@
package handlers
import (
"embed"
"html/template"
"log"
"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) {
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.StatusBadRequest)
return
}
// Execute the template with the provided data
data := map[string]any{
"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.StatusBadRequest)
}
}
}

View file

@ -0,0 +1,144 @@
package handlers
import (
"ddServer/model"
"log"
"net/http"
"strconv"
"strings"
)
// 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) {
// 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.StatusNoContent)
return
}
// Create a new monster with the form data
monster := parseMonster(r)
// Lock the Monsters slice, append the monster, and unlock the slice
mu.Lock()
defer mu.Unlock()
*Monsters = append(*Monsters, monster)
// 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 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
}
// 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: strings.ToLower(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")},
},
},
}
}

View file

@ -0,0 +1,34 @@
package handlers
import (
"embed"
"html/template"
"log"
"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 {
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]any{
"Title": "Dungeons & Dragons Monster Generator",
})
if err != nil {
log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}

56
handlers/form_handler.go Normal file
View file

@ -0,0 +1,56 @@
package handlers
import (
"ddServer/model"
"embed"
"html/template"
"log"
"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) 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) {
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/skills.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
}
// Execute the template and render the response.
data := map[string]any{
"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 rendered with %d Monsters\n", len(*monsters))
}
}

40
handlers/main_handler.go Normal file
View file

@ -0,0 +1,40 @@
package handlers
import (
"ddServer/model"
"embed"
"html/template"
"log"
"net/http"
)
// 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 {
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]any{
"Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters,
})
if err != nil {
log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}

View file

@ -0,0 +1,36 @@
package handlers
import (
"ddServer/model"
"embed"
"html/template"
"log"
"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("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]any{
"Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters,
})
if err != nil {
log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}

View file

@ -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
}

View file

@ -0,0 +1,73 @@
package handlers
import (
"ddServer/model"
"embed"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"sync"
)
var mu sync.Mutex
// 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
}
// 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
}
// Create monster object.
filename := r.FormValue("filename")
// Create or update character object.
mu.Lock()
defer mu.Unlock()
char := model.GetOrCreateCharacter(filename, *chars)
char.Monster = append(char.Monster, *Monsters...)
// 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
}
// Write JSON data to file.
err = model.WriteToFile(filename, charJSON)
if err != nil {
log.Printf("Error writing JSON data to file: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 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)
}
}

BIN
images/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

311
main.go
View file

@ -1,288 +1,73 @@
package main package main
import ( import (
"ddServer/handlers"
"ddServer/model"
"embed" "embed"
"encoding/json" "log"
"fmt"
"html/template"
"net/http" "net/http"
"os"
"strconv"
"sync" "sync"
"time"
) )
// Monster struct für die Daten des Monsters
type Monster struct {
Name string `json:"name"`
Source string `json:"source"`
Size []string `json:"size"`
Type string `json:"type"`
Alignment []string `json:"alignment"`
AC []AC `json:"ac"`
HP HP `json:"hp"`
Speed Speed `json:"speed"`
Save Save `json:"save"`
Skill Skill `json:"skill"`
DamageRes []string `json:"damageResistances"`
Senses []string `json:"senses"`
Languages []string `json:"languages"`
CR string `json:"cr"`
Traits []Trait `json:"trait"`
Actions []Action `json:"action"`
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 {
AC int `json:"ac"`
From []string `json:"from"`
}
type HP struct {
Average int `json:"average"`
Formula string `json:"formula"`
}
type Speed struct {
Walk int `json:"walk"`
}
type Save struct {
Dex string `json:"dex"`
Con string `json:"con"`
Wis string `json:"wis"`
}
type Skill struct {
Perception string `json:"perception"`
Stealth string `json:"stealth"`
}
type Trait struct {
Name string `json:"name"`
Entries []string `json:"entries"`
}
type Action struct {
Name string `json:"name"`
Entries []string `json:"entries"`
}
// Character struct für die Daten des Charakters
type Character struct {
Meta Meta `json:"_meta"`
Monster []Monster `json:"monster"`
}
// Meta struct für Meta-Informationen
type Meta struct {
Sources []Source `json:"sources"`
DateAdded int64 `json:"dateAdded"`
DateLastModified int64 `json:"dateLastModified"`
DateLastModifiedHash string `json:"_dateLastModifiedHash"`
}
type Source struct {
Json string `json:"json"`
Abbreviation string `json:"abbreviation"`
Authors []string `json:"authors"`
ConvertedBy []string `json:"convertedBy"`
Version string `json:"version"`
}
var ( var (
mu sync.Mutex mu sync.Mutex
chars []Character chars []model.Character
//go:embed forms.html //go:embed templates/*.html
page embed.FS //go:embed images/*
content embed.FS
//go:embed static/*
static embed.FS
Monsters []model.Monster
) )
// main is the entry point of the program.
func main() { func main() {
filename := "" filename := ""
http.HandleFunc("/", formHandler(filename)) // Create a new ServeMux instance
http.HandleFunc("/submit", submitHandler(filename)) routes := http.NewServeMux()
fmt.Println("Server gestartet, erreichbar unter http://localhost:8080") // Register the handlers for different routes
http.ListenAndServe(":8080", nil) 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))
routes.HandleFunc("/calculate-skills", handlers.SkillCalculationHandler(content))
// Print the message indicating that 'static' has been included.
log.Printf("Eingebunden is %v\n", static)
// formHandler zeigt das Formular an // Load the CSS file.
func formHandler(filename string) http.HandlerFunc { css, err := loadCSS(static)
return func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFS(page, "forms.html")
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) log.Fatal(err)
return
} }
tmpl.Execute(w, nil) // 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))
})
// Print the message indicating that the server has started.
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", routes))
} }
// submitHandler verarbeitet die Formulardaten // loadCSS reads the CSS file from the embedded filesystem.
func submitHandler(filename string) http.HandlerFunc { // It takes the content embed.FS as input.
return func(w http.ResponseWriter, r *http.Request) { // It returns the content of the CSS file as a string and an error if any.
if r.Method != http.MethodPost { func loadCSS(content embed.FS) (string, error) {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) // Read the CSS file "static/darkly_bulmawatch.css" from the embedded filesystem
return file, err := content.ReadFile("static/darkly_bulmawatch.css")
}
// Formulardaten parsen
err := r.ParseForm()
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return "", err
return
} }
// Convert the file content to a string and return
// Monster-Objekt erstellen return string(file), nil
filename := r.FormValue("filename")
monster := Monster{
Name: r.FormValue("name"),
Source: r.FormValue("source"),
Size: []string{r.FormValue("size")},
Type: r.FormValue("type"),
Alignment: []string{r.FormValue("alignment")},
AC: []AC{
{
AC: parseInt(r.FormValue("ac")),
From: []string{r.FormValue("acFrom")},
},
},
HP: HP{
Average: parseInt(r.FormValue("hpAverage")),
Formula: r.FormValue("hpFormula"),
},
Speed: 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: Save{
Dex: r.FormValue("saveDex"),
Con: r.FormValue("saveCon"),
Wis: r.FormValue("saveWis"),
},
Skill: Skill{
Perception: r.FormValue("perception"),
Stealth: r.FormValue("stealth"),
},
DamageRes: []string{r.FormValue("damageRes")},
Senses: []string{r.FormValue("senses")},
Languages: []string{r.FormValue("languages")},
CR: r.FormValue("cr"),
Traits: []Trait{
{
Name: r.FormValue("traitName"),
Entries: []string{r.FormValue("traitEntry")},
},
},
Actions: []Action{
{
Name: r.FormValue("actionName"),
Entries: []string{r.FormValue("actionEntry")},
},
},
}
// Charakter-Objekt erstellen oder aktualisieren
mu.Lock()
defer mu.Unlock()
char := getOrCreateCharacter(filename)
char.Monster = append(char.Monster, monster)
// Charakterdaten in JSON umwandeln
charJSON, err := json.Marshal(char)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// JSON-Daten in die Datei schreiben
err = writeToFile(filename, charJSON)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Dateiinhalt lesen
fileContent, err := os.ReadFile(filename)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Datei zum Download anbieten
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
w.Header().Set("Content-Type", "application/json")
w.Write(fileContent)
}
}
// writeToFile schreibt Daten in eine Datei
func writeToFile(filename string, data []byte) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
return err
}
return nil
}
// getOrCreateCharacter gibt das aktuelle Charakterobjekt zurück oder erstellt ein neues
func getOrCreateCharacter(filename string) Character {
for _, char := range chars {
if char.Meta.DateLastModified == 0 {
// Ein leeres Charakterobjekt wurde gefunden
return char
}
}
// Erstelle ein neues Charakterobjekt
now := time.Now().Unix()
newChar := Character{
Meta: Meta{
Sources: []Source{
{
Json: "Malgorgon",
Abbreviation: "MG",
Authors: []string{"Krzysztof"},
ConvertedBy: []string{"Krzysztof"},
Version: "unknown",
},
},
DateAdded: now,
DateLastModified: now,
DateLastModifiedHash: fmt.Sprintf("%x", now),
},
Monster: []Monster{},
}
chars = append(chars, newChar)
return newChar
}
// parseInt konvertiert einen String zu einem Integer und gibt 0 zurück, wenn die Konvertierung fehlschlägt
func parseInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
return 0
}
return i
} }

176
main_test.go Normal file
View file

@ -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
}

185
model/model.go Normal file
View file

@ -0,0 +1,185 @@
package model
import (
"fmt"
"log"
"os"
"time"
)
// 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"`
Vulnerable []string `json:"vulnerable"`
ConditionImmune []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 {
From []string `json:"from"`
AC int `json:"ac"`
}
type HP struct {
Formula string `json:"formula"`
Average int `json:"average"`
}
type Speed struct {
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 {
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"`
Persuasion string `json:"persuasion"`
SleightOfHand string `json:"sleightOfHand"`
Survival string `json:"survival"`
Religion string `json:"religion"`
}
type Trait struct {
Name string `json:"name"`
Entries []string `json:"entries"`
}
type Action struct {
Name string `json:"name"`
Entries []string `json:"entries"`
}
// Character struct für die Daten des Charakters
type Character struct {
Meta Meta `json:"_meta"`
Monster []Monster `json:"monster"`
}
// Meta struct für Meta-Informationen
type Meta struct {
DateLastModifiedHash string `json:"_dateLastModifiedHash"`
Sources []Source `json:"sources"`
DateAdded int64 `json:"dateAdded"`
DateLastModified int64 `json:"dateLastModified"`
}
type Source struct {
Json string `json:"json"`
Abbreviation string `json:"abbreviation"`
Version string `json:"version"`
Authors []string `json:"authors"`
ConvertedBy []string `json:"convertedBy"`
}
// 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 func() {
if err := file.Close(); err != nil {
log.Println("Error closing file:", err)
}
}()
// 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 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 {
// Return the empty character object
log.Println("Returning existing character object")
return char
}
}
// Create a new character object
now := time.Now().Unix()
newChar := Character{
Meta: Meta{
Sources: []Source{
{
Json: "Malgorgon",
Abbreviation: "MG",
Authors: []string{"Krzysztof"},
ConvertedBy: []string{"Krzysztof"},
Version: "unknown",
},
},
DateAdded: now,
DateLastModified: now,
DateLastModifiedHash: fmt.Sprintf("%x", now),
},
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
}

File diff suppressed because one or more lines are too long

19
templates/about.html Normal file
View file

@ -0,0 +1,19 @@
{{ define "about" }}
<!-- Start of about section -->
<div class="tile is-parent">
<div class="tile is-child card ">
<div class="card-content">
<div class="media-content">
<!-- Title -->
<p class="title is-4">About Us</p>
</div>
<div class="content">
<!-- Description -->
<p>Welcome to the Dungeons and Dragons Monster Generator website! We are a team of enthusiasts...</p>
</div>
</div>
</div>
</div>
<!-- End of about section -->
</div>
{{ end }}

54
templates/base.html Normal file
View file

@ -0,0 +1,54 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<script src="https://unpkg.com/htmx.org@1.9.9"
integrity="sha384-QFjmbokDn2DjBjq+fM+8LUIVrAgqcNW2s0PjAxHETgRn9l4fvX31ZxDxvwQnyMOX"
crossorigin="anonymous"></script>
<link id="customStylesheet" rel="stylesheet" href="/static/darkly_bulmawatch.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container is-widescreen">
{{ template "header" . }}
</div>
<div class="container is-widescreen" id="main-content">
{{ template "main" . }}
</div>
<div class="container is-widescreen">
{{ template "footer" . }}
</div>
</body>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach(el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
</script>
</html>
{{ end }}

57
templates/contact.html Normal file
View file

@ -0,0 +1,57 @@
{{ define "contact" }}
<div class="tile is-parent">
<div class="tile is-child card ">
<div class="card-content">
<div class="media-content">
<p class="title is-4">Contact Us</p>
</div>
<div class="content">
<div class="contact-info">
<h2>Our Contact Information</h2>
<p>You can reach us through the following channels:</p>
<ul>
<li>Email: example@example.com</li>
<li>Phone: +123456789</li>
</ul>
</div>
<div class="contact-form">
<h2>Contact Form</h2>
<form action="/submitContact" method="post">
<div class="field">
<label for="name">Your Name:</label>
<div class="control">
<input type="text" name="name" required placeholder="Your name"
class="input input-bordered w-full max-w-xs">
</div>
</div>
<div class="field">
<label for="email">Your Email:</label>
<div class="control">
<input type="email" name="email" required placeholder="Your email"
class="input input-bordered w-full max-w-xs">
</div>
</div>
<div class="field">
<label for="message">Your Message:</label>
<div class="control">
<textarea name="message" required placeholder="Type your message here" class="textarea"></textarea>
</div>
</div>
<div class="field">
<div class="control">
<button type="submit" class="button is-primary">Send Message</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

9
templates/footer.html Normal file
View file

@ -0,0 +1,9 @@
{{ define "footer" }}
<footer class="footer">
<div class="content has-text-centered is-widescreen">
<p>
&copy; {{.Year}} Dungeons and Dragons Monster Generator. Alle Rechte vorbehalten.
</p>
</div>
</footer>
{{ end }}

81
templates/header.html Normal file
View file

@ -0,0 +1,81 @@
{{ define "header" }}
<header>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a href="/" class="navbar-item" hx-get="/main" hx-target="#main-content" hx-boost="true">
Dungeons & Dragons
</a>
<a href="/about" class="navbar-item" hx-get="/about" hx-target="#main-content" hx-boost="true">
About
</a>
<a href="/contact" class="navbar-item" hx-get="/contact" hx-target="#main-content" hx-boost="true">
Contact
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<button id="switchButton" class="button is-light">
<span id="stylesheetIcon" class="icon">
<i class="fas fa-sun"></i>
</span>
</button>
</div>
</div>
</div>
</nav>
<div class="tile is-ancestor">
<div class="tile is-parent">
<section class="tile is-child hero box is-widescreen">
<div class="hero-body is-flex is-justify-content-center is-align-items-center">
<div class="container has-text-centered">
<div class="hero-background">
<img src="/images/images/banner.jpg" alt="Dungeons-and-Dragons-Banner">
</div>
<div class="hero-content is-overlay">
<p class="title is-size-5-mobile is-size-1-desktop"
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
Dungeons and Dragons Monster Generator
</p>
</div>
</div>
</div>
</section>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const switchButton = document.getElementById("switchButton");
const stylesheetIcon = document.getElementById("stylesheetIcon");
if (switchButton) {
switchButton.addEventListener('click', switchStylesheet);
}
function switchStylesheet() {
const link = document.getElementById("customStylesheet");
// Ändere das Icon basierend auf dem aktuellen Stylesheet
if (link.getAttribute("href") === "/static/darkly_bulmawatch.css") {
link.setAttribute("href", "https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css");
stylesheetIcon.innerHTML = '<i class="fas fa-moon"></i>'; // Zum Mond-Icon wechseln
} else {
link.setAttribute("href", "/static/darkly_bulmawatch.css");
stylesheetIcon.innerHTML = '<i class="fas fa-sun"></i>'; // Zum Sonnen-Icon wechseln
}
}
});
</script>
</header>
{{ end }}

109
templates/main.html Normal file
View file

@ -0,0 +1,109 @@
{{ define "main" }}
<div class="tile is-parent">
<div class="tile is-child card ">
<div class="card-content">
<div class="media-content">
<p class="title is-4">Monster Form</p>
</div>
<div class="content">
<form action="/submit" method="post" class="">
<div class="field">
<td><label for="filename">Filename:</label></td>
<div class="control"><input type="text" name="filename" required placeholder="Dateiname"
class="input input-bordered w-full max-w-xs">
</div>
</div>
<div class="field">
<div class="control">
<button type="button" hx-post="/addMonster" hx-trigger="click" hx-target="#monster-table"
hx-swap="outerHTML" class="button is-info" hx-boost="true">Add
Monster</button>
</div>
</div>
{{ template "monsterform" . }}
<input type="hidden" name="filename" value="{{.Filename}}">
<input type="submit" value="Submit" class="button is-primary">
</form>
</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child card">
<div class="card-content">
<div class="media-content">
<p class="title is-4 is-centered">Existing Monsters</p>
</div>
<div class="table-container">
<table class="table">
<colgroup>
<col style="width: 100px;">
<col style="width: 100px;">
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Source</th>
<th>Size</th>
<th>Type</th>
<th>Alignment</th>
<th>AC</th>
<th>AC Form</th>
<th>HP Average</th>
<th>HP Formula</th>
<th>Walk</th>
<th>Swim</th>
<th>Burrow</th>
<th>Climb</th>
<th>Fly</th>
<th>Str</th>
<th>Dex</th>
<th>Con</th>
<th>Int</th>
<th>Wis</th>
<th>Cha</th>
<th>Save Dex</th>
<th>Save Con</th>
<th>Save Wis</th>
<th>Save Str</th>
<th>Save Con</th>
<th>Save Cha</th>
<th>Perception</th>
<th>Stealth</th>
<th>Acrobatics</th>
<th>AnimalHandling</th>
<th>Arcana</th>
<th>Athletics</th>
<th>Deception</th>
<th>History</th>
<th>Insight</th>
<th>Intimidation</th>
<th>Investigation</th>
<th>Medicine</th>
<th>Nature</th>
<th>Performance</th>
<th>Persuasion</th>
<th>SleightOfHand</th>
<th>Survival</th>
<th>Religion</th>
<th>Damage Resistance</th>
<th>Damage Immune</th>
<th>Vulnerable</th>
<th>Condition Immune</th>
<th>Senses</th>
<th>Languages</th>
<th>CR</th>
<th>Trait Name</th>
<th>Trait Entry</th>
<th>Action Name</th>
<th>Action Entry</th>
</tr>
</thead>
{{ template "monsterTable" }}
</table>
</div>
</div>
</div>
</div>
</div>
{{ end }}

59
templates/monster.html Normal file
View file

@ -0,0 +1,59 @@
{{ define "monster" }}
<tr>
<td>{{.Name}}</td>
<td>{{.Source}}</td>
<td>{{range .Size}}{{.}}{{end}}</td>
<td>{{.Type}}</td>
<td>{{range .Alignment}}{{.}}{{end}}</td>
<td>{{range .AC}}{{.AC}}{{end}}</td>
<td>{{range .AC}}{{.From}}{{end}}</td>
<td>{{.HP.Average}}</td>
<td>{{.HP.Formula}}</td>
<td>{{.Speed.Walk}}</td>
<td>{{.Speed.Swim}}</td>
<td>{{.Speed.Burrow}}</td>
<td>{{.Speed.Climb}}</td>
<td>{{.Speed.Fly}}</td>
<td>{{.Str}}</td>
<td>{{.Dex}}</td>
<td>{{.Con}}</td>
<td>{{.Int}}</td>
<td>{{.Wis}}</td>
<td>{{.Cha}}</td>
<td>{{.Save.Dex}}</td>
<td>{{.Save.Con}}</td>
<td>{{.Save.Wis}}</td>
<td>{{.Save.Str}}</td>
<td>{{.Save.Con}}</td>
<td>{{.Save.Cha}}</td>
<td>{{.Skill.Perception}}</td>
<td>{{.Skill.Stealth}}</td>
<td>{{.Skill.Acrobatics}}</td>
<td>{{.Skill.AnimalHandling}}</td>
<td>{{.Skill.Arcana}}</td>
<td>{{.Skill.Athletics}}</td>
<td>{{.Skill.Deception}}</td>
<td>{{.Skill.History}}</td>
<td>{{.Skill.Insight}}</td>
<td>{{.Skill.Intimidation}}</td>
<td>{{.Skill.Investigation}}</td>
<td>{{.Skill.Medicine}}</td>
<td>{{.Skill.Nature}}</td>
<td>{{.Skill.Performance}}</td>
<td>{{.Skill.Persuasion}}</td>
<td>{{.Skill.SleightOfHand}}</td>
<td>{{.Skill.Survival}}</td>
<td>{{.Skill.Religion}}</td>
<td>{{range .Resist}}{{.}}{{end}}</td>
<td>{{range .Immune}}{{.}}{{end}}</td>
<td>{{range .Vulnerable}}{{.}}{{end}}</td>
<td>{{range .ConditionImmune}}{{.}}{{end}}</td>
<td>{{range .Senses}}{{.}}{{end}}</td>
<td>{{range .Languages}}{{.}}{{end}}</td>
<td>{{.CR}}</td>
<td>{{range .Traits}}{{.Name}}{{end}}</td>
<td>{{range .Traits}}{{range .Entries}}{{.}}{{end}}{{end}}</td>
<td>{{range .Actions}}{{.Name}}{{end}}</td>
<td>{{range .Actions}}{{range .Entries}}{{.}}{{end}}{{end}}</td>
</tr>
{{ end }}

378
templates/monsterForm.html Normal file
View file

@ -0,0 +1,378 @@
{{ define "monsterform" }}
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="name">Monster Name:</label>
<div class="control">
<input type="text" name="name" required placeholder="Type here" class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="source">Monster Source:</label>
<div class="control">
<input type="text" name="source" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="size">Size:</label>
<div class="control">
<div class="select">
<select name="size">
<option>H</option>
<option>T</option>
<option>S</option>
<option>L</option>
<option>G</option>
</select>
</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="type">Type:</label>
<div class="control">
<input type="text" name="type" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-vertical is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="alignment">Alignment:</label>
<div class="control">
<input type="text" name="alignment" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="ac">AC:</label>
<div class="control">
<input type="number" name="ac" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="acFrom">AC From:</label>
<div class="control">
<input type="text" name="acFrom" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="hpAverage">HP Average:</label>
<div class="control">
<input type="number" name="hpAverage" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="hpFormula">HP Formula:</label>
<div class="control">
<input type="text" name="hpFormula" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="card tile is-ancestor is-vertical">
<header class="card-header">
<p class="card-header-title">
Speed
</p>
</header>
<div class="card-content">
<div class="content">
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="walk">Walk:</label>
<div class="control">
<input type="number" name="walk" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="burrow">Burrow:</label>
<div class="control">
<input type="number" name="burrow" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="climb">Climb:</label>
<div class="control">
<input type="number" name="climb" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="fly">Fly:</label>
<div class="control">
<input type="number" name="fly" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="swim">Swim:</label>
<div class="control">
<input type="number" name="swim" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="str">Str:</label>
<div class="control">
<input type="number" name="str" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="dex">Dex:</label>
<div class="control">
<input type="number" name="dex" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="con">Con:</label>
<div class="control">
<input type="number" name="con" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="cr">CR:</label>
<div class="control">
<input type="text" name="cr" required class="input input-bordered w-full max-w-xs" hx-post="/calculate-skills"
hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML" hx-target="#skills"
hx-swap="outerHTML">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="int">Int:</label>
<div class="control">
<input type="number" name="int" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="wis">Wis:</label>
<div class="control">
<input type="number" name="wis" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="cha">Cha:</label>
<div class="control">
<input type="number" name="cha" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="passive">Passive:</label>
<div class="control">
<input type="number" name="passive" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div>
</div>
</div>
</div>
<div class="card tile is-ancestor is-vertical">
<header class="card-header">
<p class="card-header-title">
Save
</p>
</header>
<div class="card-content">
<div class="content">
<div class="tile is-ancestor">
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveDex">Dex:</label>
<div class="control">
<input type="text" name="saveDex" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveCon">Con:</label>
<div class="control">
<input type="text" name="saveCon" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveWis">Wis:</label>
<div class="control">
<input type="text" name="saveWis" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveCha">Cha:</label>
<div class="control">
<input type="text" name="saveCha" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveInt">Int:</label>
<div class="control">
<input type="text" name="saveInt" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveStr">Str:</label>
<div class="control">
<input type="text" name="saveStr" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
</div>
</div>
<div id="skills" class="card tile is-ancestor is-vertical">
{{ template "skills" }}
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="resist">Damage Resistances:</label>
<div class="control">
<input type="text" name="resist" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="immune">Damage Immunity:</label>
<div class="control">
<input type="text" name="immune" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="vulnerable">Vulnerability:</label>
<div class="control">
<input type="text" name="vulnerable" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="conditionImmune">Condition Immunity:</label>
<div class="control">
<input type="text" name="conditionImmune" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="senses">Senses:</label>
<div class="control">
<input type="text" name="senses" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="languages">Languages:</label>
<div class="control">
<input type="text" name="languages" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="traitName">Trait Name:</label>
<div class="control">
<input type="text" name="traitName" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="traitEntry">Trait Entry:</label>
<div class="control">
<input type="text" name="traitEntry" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="actionName">Action Name:</label>
<div class="control">
<input type="text" name="actionName" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="actionEntry">Action Entry:</label>
<div class="control">
<input type="text" name="actionEntry" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
{{end}}

View file

@ -0,0 +1,7 @@
{{ define "monsterTable" }}
<tbody id="monster-table">
{{ range .Monsters }}
{{ template "monster" . }}
</tbody>
{{ end }}
{{ end }}

185
templates/skills.html Normal file
View file

@ -0,0 +1,185 @@
{{ define "skills" }}
<header class="card-header">
<p class="card-header-title">
Skill
</p>
</header>
<div class="card-content">
<div class="content">
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="acrobatics">Acrobatics:</label>
<div class="control">
<input type="text" name="acrobatics" required class="input input-bordered w-full max-w-xs" value="{{
.acrobatics }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="animalHandling">Animal Handling:</label>
<div class="control">
<input type="text" name="animalHandling" required class="input input-bordered w-full max-w-xs"
value="{{ .animalHandling }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="arcana">Arcana:</label>
<div class="control">
<input type="text" name="arcana" required class="input input-bordered w-full max-w-xs" value="{{ .arcana }}"
readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="athletics">Athletics:</label>
<div class="control">
<input type="text" name="athletics" required class="input input-bordered w-full max-w-xs"
value="{{ .athletics }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="deception">Deception:</label>
<div class="control">
<input type="text" name="deception" required class="input input-bordered w-full max-w-xs"
value="{{ .deception }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="history">History:</label>
<div class="control">
<input type="text" name="history" required class="input input-bordered w-full max-w-xs"
value="{{ .history }}" readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="insight">Insight:</label>
<div class="control">
<input type="text" name="insight" required class="input input-bordered w-full max-w-xs"
value="{{ .insight }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="intimidation">Intimidation:</label>
<div class="control">
<input type="text" name="intimidation" required class="input input-bordered w-full max-w-xs"
value="{{ .intimidation }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="investigation">Investigation:</label>
<div class="control">
<input type="text" name="investigation" required class="input input-bordered w-full max-w-xs"
value="{{ .investigation }}" readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="medicine">Medicine:</label>
<div class="control">
<input type="text" name="medicine" required class="input input-bordered w-full max-w-xs"
value="{{ .medicine }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="nature">Nature:</label>
<div class="control">
<input type="text" name="nature" required class="input input-bordered w-full max-w-xs" value="{{ .nature }}"
readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="performance">Performance:</label>
<div class="control">
<input type="text" name="performance" required class="input input-bordered w-full max-w-xs"
value="{{ .performance }}" readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="perception">Perception:</label>
<div class="control">
<input type="text" name="perception" required class="input input-bordered w-full max-w-xs"
value="{{ .perception }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="stealth">Stealth:</label>
<div class="control">
<input type="text" name="stealth" required class="input input-bordered w-full max-w-xs"
value="{{ .stealth }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="persuasion">Persuasion:</label>
<div class="control">
<input type="text" name="persuasion" required class="input input-bordered w-full max-w-xs"
value="{{ .persuasion }}" readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="sleightOfHand">Sleight of Hand:</label>
<div class="control">
<input type="text" name="sleightOfHand" required class="input input-bordered w-full max-w-xs"
value="{{.sleightOfHand }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="religion">Religion:</label>
<div class="control">
<input type="text" name="religion" required class="input input-bordered w-full max-w-xs"
value="{{ .religion }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="survival">Survival:</label>
<div class="control">
<input type="text" name="survival" required class="input input-bordered w-full max-w-xs"
value="{{ .survival }}" readonly>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}