diff --git a/README.md b/README.md
deleted file mode 100644
index 34428ea..0000000
--- a/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# 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
-```
diff --git a/go.mod b/go.mod
index dfe5d47..7db1ebd 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,10 @@ module ddServer
go 1.21.4
-require github.com/stretchr/testify v1.8.4
+require (
+ github.com/stretchr/testify v1.8.4
+ golang.org/x/text v0.14.0
+)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index fa4b6e6..e228b7b 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
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=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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=
diff --git a/handlers/about_handler.go b/handlers/about_handler.go
index 1d628e0..5f8a1dd 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]any{
+ data := map[string]interface{}{
"Title": "Dungeons & Dragons Monster Generator",
}
err = tmpl.ExecuteTemplate(w, "about", data)
diff --git a/handlers/add_monster_handler.go b/handlers/add_monster_handler.go
index 38cb1a1..5174c3a 100644
--- a/handlers/add_monster_handler.go
+++ b/handlers/add_monster_handler.go
@@ -2,10 +2,14 @@ package handlers
import (
"ddServer/model"
+ "fmt"
"log"
"net/http"
"strconv"
"strings"
+
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
)
// AddMonster is a http.HandlerFunc that adds a new monster to the Monsters slice.
@@ -94,32 +98,32 @@ func parseMonster(r *http.Request) model.Monster {
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"),
+ Dex: checkCheckbox("savedex", r),
+ Con: checkCheckbox("savecon", r),
+ Wis: checkCheckbox("savewis", r),
+ Str: checkCheckbox("savestr", r),
+ Cha: checkCheckbox("savecha", r),
+ Int: checkCheckbox("saveint", r),
},
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"),
+ Perception: checkCheckbox("perception", r),
+ Stealth: checkCheckbox("stealth", r),
+ Acrobatics: checkCheckbox("acrobatics", r),
+ AnimalHandling: checkCheckbox("animalhandling", r),
+ Arcana: checkCheckbox("arcana", r),
+ Athletics: checkCheckbox("athletics", r),
+ Deception: checkCheckbox("deception", r),
+ History: checkCheckbox("history", r),
+ Insight: checkCheckbox("insight", r),
+ Intimidation: checkCheckbox("intimidation", r),
+ Investigation: checkCheckbox("investigation", r),
+ Medicine: checkCheckbox("medicine", r),
+ Nature: checkCheckbox("nature", r),
+ Performance: checkCheckbox("performance", r),
+ Persuasion: checkCheckbox("persuasion", r),
+ SleightOfHand: checkCheckbox("sleightofhand", r),
+ Survival: checkCheckbox("survival", r),
+ Religion: checkCheckbox("religion", r),
},
Resist: []string{r.FormValue("resist")},
ConditionImmune: []string{r.FormValue("conditionImmune")},
@@ -142,3 +146,10 @@ func parseMonster(r *http.Request) model.Monster {
},
}
}
+
+func checkCheckbox(field string, r *http.Request) string {
+ if r.FormValue(fmt.Sprintf("check%v", cases.Caser(cases.Title(language.Und)).String(field))) == "on" {
+ return r.FormValue(field)
+ }
+ return ""
+}
diff --git a/handlers/contact_handler.go b/handlers/contact_handler.go
index 611abc2..a01cad4 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]any{
+ err = tmpl.ExecuteTemplate(w, "contact", map[string]interface{}{
"Title": "Dungeons & Dragons Monster Generator",
})
if err != nil {
diff --git a/handlers/form_handler.go b/handlers/form_handler.go
index 1232463..c31cee4 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]any{
+ data := map[string]interface{}{
"Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters,
}
diff --git a/handlers/load_file_handler.go b/handlers/load_file_handler.go
new file mode 100644
index 0000000..cfaaac4
--- /dev/null
+++ b/handlers/load_file_handler.go
@@ -0,0 +1,46 @@
+package handlers
+
+import (
+ "ddServer/model"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+)
+
+func LoadFileHandler(monsters *[]model.Monster) http.HandlerFunc {
+ log.Print("LoadFileHandler called")
+ return func(w http.ResponseWriter, r *http.Request) {
+ r.ParseMultipartForm(10 << 20) // 10 MB limit
+
+ // Get the file from the request
+ file, _, err := r.FormFile("uploadFile")
+ if err != nil {
+ http.Error(w, "Error retrieving file", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ // Parse the file content
+ decoder := json.NewDecoder(file)
+ var loadedChars model.Character
+ err = decoder.Decode(&loadedChars)
+ if err != nil {
+ http.Error(w, "Error decoding file content", http.StatusInternalServerError)
+ return
+ }
+
+ // Lock the Monsters slice and append the loaded monsters, then unlock the slice
+ mu.Lock()
+ defer mu.Unlock()
+
+ // Assuming 'loadedChars' contains an array of Monster objects
+ for _, monster := range loadedChars.Monster {
+ *monsters = append(*monsters, monster)
+ }
+
+ fmt.Printf("%v\n", monsters)
+ // Send a success response
+ http.Redirect(w, r, "/monsterTable", http.StatusTemporaryRedirect)
+ }
+}
diff --git a/handlers/main_handler.go b/handlers/main_handler.go
index e4b575e..19d2cdf 100644
--- a/handlers/main_handler.go
+++ b/handlers/main_handler.go
@@ -16,7 +16,7 @@ func MainHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc {
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")
+ tmpl, err := template.ParseFS(content, "templates/main.html", "templates/monsterForm.html", "templates/monster.html", "templates/monsterTable.html", "templates/base.html", "templates/skills.html")
if err != nil {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -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]any{
+ 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 f227962..4755386 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]any{
+ err = tmpl.ExecuteTemplate(w, "monsterTable", map[string]interface{}{
"Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters,
})
diff --git a/handlers/skill_calculation_handler.go b/handlers/skill_calculation_handler.go
index 48935cf..55ce57b 100644
--- a/handlers/skill_calculation_handler.go
+++ b/handlers/skill_calculation_handler.go
@@ -8,40 +8,44 @@ import (
"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.
+// SkillCalculationHandler is an http.HandlerFunc triggered by htmx when the user makes entries in certain fields and then populates the skill fields.
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.
+ // Check if the request is a POST request.
if r.Method != http.MethodPost {
- log.Print("Method not allowed")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
- // Parse Formulardaten.
+ // 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
}
- tmplFiles := []string{"templates/base.html", "templates/header.html", "templates/skills.html", "templates/main.html", "templates/footer.html", "templates/about.html"}
+ // Parse template files.
+ 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
}
+ // Parse form field values and calculate skill values.
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"))
+ con := parseFieldValue(r.FormValue("con"))
cr := parseFieldValue(r.FormValue("cr"))
crBonus := calcBonus(cr)
@@ -64,79 +68,124 @@ func SkillCalculationHandler(content embed.FS) http.HandlerFunc {
"sleightOfHand": strconv.Itoa(calcAbilityScore(dex) + crBonus),
"stealth": strconv.Itoa(calcAbilityScore(dex) + crBonus),
"survival": strconv.Itoa(calcAbilityScore(wis) + crBonus),
+ "saveStr": strconv.Itoa(calcAbilityScore(str) + crBonus),
+ "saveWis": strconv.Itoa(calcAbilityScore(wis) + crBonus),
+ "saveCon": strconv.Itoa(calcAbilityScore(con) + crBonus),
+ "saveInt": strconv.Itoa(calcAbilityScore(int) + crBonus),
+ "saveCha": strconv.Itoa(calcAbilityScore(cha) + crBonus),
+ "saveDex": strconv.Itoa(calcAbilityScore(dex) + crBonus),
}
+ // Execute template with skill values.
err = tmpl.ExecuteTemplate(w, "skills", skillValues)
if err != nil {
- log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
}
+// calcBonus calculates the bonus based on the given credit rating.
+// It returns the bonus value as an integer.
func calcBonus(cr int) int {
- if cr >= 0 && cr < 5 {
+ switch {
+ case cr >= 0 && cr < 5:
+ log.Println("Bonus calculated for credit rating:", cr)
return 2
- } else if cr >= 5 && cr < 9 {
+ case cr >= 5 && cr < 9:
+ log.Println("Bonus calculated for credit rating:", cr)
return 3
- } else if cr >= 9 && cr < 14 {
+ case cr >= 9 && cr < 14:
+ log.Println("Bonus calculated for credit rating:", cr)
return 4
- } else if cr >= 14 && cr < 18 {
+ case cr >= 14 && cr < 18:
+ log.Println("Bonus calculated for credit rating:", cr)
return 5
- } else if cr >= 18 && cr < 21 {
+ case cr >= 18 && cr < 21:
+ log.Println("Bonus calculated for credit rating:", cr)
return 6
- } else if cr >= 21 && cr < 25 {
+ case cr >= 21 && cr < 25:
+ log.Println("Bonus calculated for credit rating:", cr)
return 7
- } else if cr >= 25 && cr < 28 {
+ case cr >= 25 && cr < 28:
+ log.Println("Bonus calculated for credit rating:", cr)
return 8
- } else if cr >= 28 && cr < 31 {
+ case cr >= 28 && cr < 31:
+ log.Println("Bonus calculated for credit rating:", cr)
return 9
- } else {
+ default:
+ log.Println("Invalid credit rating:", cr)
return 0
}
}
+// calcAbilityScore calculates the ability score based on the given value.
func calcAbilityScore(val int) int {
- if val < 2 {
+ switch {
+ case val < 2:
+ log.Println("Ability Score: -5")
return -5
- } else if val >= 2 && val < 4 {
+ case val < 4:
+ log.Println("Ability Score: -4")
return -4
- } else if val >= 4 && val < 6 {
+ case val < 6:
+ log.Println("Ability Score: -3")
return -3
- } else if val >= 6 && val < 8 {
+ case val < 8:
+ log.Println("Ability Score: -2")
return -2
- } else if val >= 8 && val < 10 {
+ case val < 10:
+ log.Println("Ability Score: -1")
return -1
- } else if val >= 10 && val < 12 {
+ case val < 12:
+ log.Println("Ability Score: 0")
return 0
- } else if val >= 12 && val < 14 {
+ case val < 14:
+ log.Println("Ability Score: 1")
return 1
- } else if val >= 14 && val < 16 {
+ case val < 16:
+ log.Println("Ability Score: 2")
return 2
- } else if val >= 16 && val < 18 {
+ case val < 18:
+ log.Println("Ability Score: 3")
return 3
- } else if val >= 18 && val < 20 {
+ case val < 20:
+ log.Println("Ability Score: 4")
return 4
- } else if val >= 20 && val < 22 {
+ case val < 22:
+ log.Println("Ability Score: 5")
return 5
- } else if val >= 22 && val < 24 {
+ case val < 24:
+ log.Println("Ability Score: 6")
return 6
- } else if val >= 24 && val < 26 {
+ case val < 26:
+ log.Println("Ability Score: 7")
return 7
- } else if val >= 26 && val < 28 {
+ case val < 28:
+ log.Println("Ability Score: 8")
return 8
- } else if val >= 28 && val < 30 {
+ case val < 30:
+ log.Println("Ability Score: 9")
return 9
- } else {
+ default:
+ log.Println("Ability Score: 10")
return 10
}
}
+// parseFieldValue takes a string value and returns an integer.
+// If the string value cannot be converted to an integer, it logs an error and returns 0.
+// The function follows these rules:
+// - No line is over 66 characters.
func parseFieldValue(value string) int {
+ // Convert the string value to an integer using strconv.Atoi.
+ // If an error occurs during the conversion, log the error and return 0.
val, err := strconv.Atoi(value)
if err != nil {
log.Printf("Error converting field value to integer: %v", err)
return 0
}
+ // Log the converted integer value for debugging purposes.
+ log.Printf("Converted field value to integer: %d", val)
+ // Return the converted integer value.
return val
}
diff --git a/main.go b/main.go
index d163e79..5bbd46f 100644
--- a/main.go
+++ b/main.go
@@ -37,6 +37,7 @@ func main() {
routes.HandleFunc("/contact", handlers.ContactHandler(content))
routes.HandleFunc("/monsterTable", handlers.MonsterTableHandler(content, &Monsters))
routes.HandleFunc("/calculate-skills", handlers.SkillCalculationHandler(content))
+ routes.HandleFunc("/loadFile", handlers.LoadFileHandler(&Monsters))
// Print the message indicating that 'static' has been included.
log.Printf("Eingebunden is %v\n", static)
diff --git a/templates/contact.html b/templates/contact.html
index ce0085d..c8481f1 100644
--- a/templates/contact.html
+++ b/templates/contact.html
@@ -1,57 +1,58 @@
{{ define "contact" }}
-
-
-
-
Contact Us
-
-
+
+
+
+
Contact Us
+
+
-
-
Our Contact Information
-
You can reach us through the following channels:
-
-
Email: example@example.com
-
Phone: +123456789
-
+
+
Our Contact Information
+
You can reach us through the following channels:
+
+
Email: example@example.com
+
Phone: +123456789
+
+
+
+
+
Contact Form
+
+
+
-
-
-
Contact Form
-
-
-
-
{{ end }}
diff --git a/templates/footer.html b/templates/footer.html
index cf8d261..9b0881a 100644
--- a/templates/footer.html
+++ b/templates/footer.html
@@ -6,4 +6,4 @@
-{{ end }}
+{{ end }}
\ No newline at end of file
diff --git a/templates/header.html b/templates/header.html
index 0efea19..46c13ec 100644
--- a/templates/header.html
+++ b/templates/header.html
@@ -78,4 +78,4 @@
});
-{{ end }}
+{{ end }}
\ No newline at end of file
diff --git a/templates/main.html b/templates/main.html
index ed4581f..359f1a9 100644
--- a/templates/main.html
+++ b/templates/main.html
@@ -1,109 +1,131 @@
{{ define "main" }}
-
-
-
-
Monster Form
+
+
+
+
Monster Form
+
+
+
+
+
-
-
-
-
Existing Monsters
-
-
-
-
-
-
-
-
-
-
Name
-
Source
-
Size
-
Type
-
Alignment
-
AC
-
AC Form
-
HP Average
-
HP Formula
-
Walk
-
Swim
-
Burrow
-
Climb
-
Fly
-
Str
-
Dex
-
Con
-
Int
-
Wis
-
Cha
-
Save Dex
-
Save Con
-
Save Wis
-
Save Str
-
Save Con
-
Save Cha
-
Perception
-
Stealth
-
Acrobatics
-
AnimalHandling
-
Arcana
-
Athletics
-
Deception
-
History
-
Insight
-
Intimidation
-
Investigation
-
Medicine
-
Nature
-
Performance
-
Persuasion
-
SleightOfHand
-
Survival
-
Religion
-
Damage Resistance
-
Damage Immune
-
Vulnerable
-
Condition Immune
-
Senses
-
Languages
-
CR
-
Trait Name
-
Trait Entry
-
Action Name
-
Action Entry
-
-
- {{ template "monsterTable" }}
-
-
-
+
+
+
+
Existing Monsters
+
+
+
+
+
+
+
+
+
+
Name
+
Source
+
Size
+
Type
+
Alignment
+
AC
+
AC Form
+
HP Average
+
HP Formula
+
Walk
+
Swim
+
Burrow
+
Climb
+
Fly
+
Str
+
Dex
+
Con
+
Int
+
Wis
+
Cha
+
Save Dex
+
Save Con
+
Save Wis
+
Save Str
+
Save Int
+
Save Cha
+
Perception
+
Stealth
+
Acrobatics
+
AnimalHandling
+
Arcana
+
Athletics
+
Deception
+
History
+
Insight
+
Intimidation
+
Investigation
+
Medicine
+
Nature
+
Performance
+
Persuasion
+
SleightOfHand
+
Survival
+
Religion
+
Damage Resistance
+
Damage Immune
+
Vulnerable
+
Condition Immune
+
Senses
+
Languages
+
CR
+
Trait Name
+
Trait Entry
+
Action Name
+
Action Entry
+
+
+ {{ template "monsterTable" }}
+
+
+
-{{ end }}
+{{ end }}
\ No newline at end of file
diff --git a/templates/monster.html b/templates/monster.html
index d3a59d6..0053cea 100644
--- a/templates/monster.html
+++ b/templates/monster.html
@@ -24,7 +24,7 @@
-{{ end }}
+{{ end }}
\ No newline at end of file
diff --git a/templates/monsterForm.html b/templates/monsterForm.html
index ca69873..5903e0d 100644
--- a/templates/monsterForm.html
+++ b/templates/monsterForm.html
@@ -87,61 +87,6 @@
-
-
-
- Speed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -209,20 +154,14 @@
-
-
-
-
-
-
-
-
+
+
+ {{ template "skills" }}
- Save
+ Speed
@@ -230,61 +169,50 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
- {{ template "skills" }}
-
@@ -375,4 +303,4 @@
-{{end}}
+{{end}}
\ No newline at end of file
diff --git a/templates/monsterTable.html b/templates/monsterTable.html
index d5aef50..9eb1b75 100644
--- a/templates/monsterTable.html
+++ b/templates/monsterTable.html
@@ -4,4 +4,4 @@
{{ template "monster" . }}
{{ end }}
-{{ end }}
+{{ end }}
\ No newline at end of file
diff --git a/templates/skills.html b/templates/skills.html
index 9322260..acd9da9 100644
--- a/templates/skills.html
+++ b/templates/skills.html
@@ -1,185 +1,265 @@
{{ define "skills" }}
-
-