From 085b645265eac1e0949e2c233df4ac40b64526ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Mon, 29 Sep 2025 23:53:04 +0200 Subject: [PATCH] initial commit --- .idea/.gitignore | 8 +++ .idea/modules.xml | 8 +++ .idea/templating.iml | 9 +++ go.mod | 32 ++++++++++ go.sum | 59 +++++++++++++++++ main.go | 109 ++++++++++++++++++++++++++++++++ templates/components/head.html | 28 ++++++++ templates/components/input.html | 20 ++++++ templates/layouts/base.html | 14 ++++ templates/pages/index.html | 12 ++++ ui/components.go | 67 ++++++++++++++++++++ ui/helpers.go | 35 ++++++++++ ui/validation.go | 75 ++++++++++++++++++++++ 13 files changed, 476 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/templating.iml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 templates/components/head.html create mode 100644 templates/components/input.html create mode 100644 templates/layouts/base.html create mode 100644 templates/pages/index.html create mode 100644 ui/components.go create mode 100644 ui/helpers.go create mode 100644 ui/validation.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..86eff7f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/templating.iml b/.idea/templating.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/templating.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a25d076 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module templating + +go 1.25.0 + +require ( + github.com/go-playground/validator/v10 v10.27.0 + github.com/gofiber/fiber/v2 v2.52.9 + github.com/gofiber/template/html/v2 v2.1.3 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/gofiber/template v1.8.3 // indirect + github.com/gofiber/utils v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b217de6 --- /dev/null +++ b/go.sum @@ -0,0 +1,59 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= +github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= +github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= +github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o= +github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE= +github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= +github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..85af8ec --- /dev/null +++ b/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "log/slog" + "templating/ui" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/html/v2" +) + +func main() { + engine := html.New("./templates", ".html") + engine.Reload(true) + + app := fiber.New( + fiber.Config{ + Views: engine, + }) + + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("pages/index", fiber.Map{ + "Message": "Hello World", + "UsernameField": ui.UsernameField().ToMap(), + "EmailField": ui.EmailField().ToMap(), + "PasswordField": ui.PasswordField().ToMap(), + }, "layouts/base") + }) + + type SomeForm struct { + Username string `json:"username" validate:"required,min=3,max=30,username"` + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=8,max=20"` + } + + app.Post("/validate/username", func(c *fiber.Ctx) error { + slog.Info(`validate username triggered`) + + username := c.FormValue("username") + input := SomeForm{ + Username: username, + } + + validate := ui.GetValidator() + err := validate.StructPartial(input, "Username") + if err != nil { + field := ui.UsernameField(). + WithValue(username). + WithError(ui.GetFirstError(err)) + return c.Render("components/input", field.ToMap()) + } + + // Emular que el nombre de usuario existe + if username == "juan" { + slog.Info("username already exists") + field := ui.UsernameField(). + WithValue(username). + WithError("El nombre de usuario ya existe") + return c.Render("components/input", field.ToMap()) + } + + field := ui.UsernameField().WithValue(username) + return c.Render("components/input", field.ToMap()) + }) + + app.Post("/validate/email", func(c *fiber.Ctx) error { + slog.Info(`validate email triggered`) + + email := c.FormValue("email") + input := SomeForm{ + Email: email, + } + + validate := ui.GetValidator() + err := validate.StructPartial(input, "Email") + if err != nil { + field := ui.EmailField(). + WithValue(email). + WithError(ui.GetFirstError(err)) + return c.Render("components/input", field.ToMap()) + + } + + field := ui.EmailField().WithValue(email) + return c.Render("components/input", field.ToMap()) + }) + + app.Post("/validate/password", func(c *fiber.Ctx) error { + slog.Info(`validate password triggered`) + + password := c.FormValue("password") + input := SomeForm{ + Password: password, + } + + validate := ui.GetValidator() + err := validate.StructPartial(input, "Password") + if err != nil { + field := ui.PasswordField(). + WithValue(password). + WithError(ui.GetFirstError(err)) + return c.Render("components/input", field.ToMap()) + } + + field := ui.PasswordField().WithValue(password) + return c.Render("components/input", field.ToMap()) + }) + + app.Listen(":3000") +} diff --git a/templates/components/head.html b/templates/components/head.html new file mode 100644 index 0000000..d2c91c0 --- /dev/null +++ b/templates/components/head.html @@ -0,0 +1,28 @@ + + + +{{ .Title }} + + + + + + + + \ No newline at end of file diff --git a/templates/components/input.html b/templates/components/input.html new file mode 100644 index 0000000..7646528 --- /dev/null +++ b/templates/components/input.html @@ -0,0 +1,20 @@ +
+ {{ .Legend }} + +

+ {{ .Hint }} +

+
\ No newline at end of file diff --git a/templates/layouts/base.html b/templates/layouts/base.html new file mode 100644 index 0000000..a445bfa --- /dev/null +++ b/templates/layouts/base.html @@ -0,0 +1,14 @@ + + + + + {{ template "components/head" . }} + + + + + {{ embed }} + + + + diff --git a/templates/pages/index.html b/templates/pages/index.html new file mode 100644 index 0000000..35ee788 --- /dev/null +++ b/templates/pages/index.html @@ -0,0 +1,12 @@ +
+ + +

Inicio de sesión

+ + + {{ template "components/input" .EmailField }} + + {{ template "components/input" .PasswordField }} + + +
diff --git a/ui/components.go b/ui/components.go new file mode 100644 index 0000000..50728e6 --- /dev/null +++ b/ui/components.go @@ -0,0 +1,67 @@ +package ui + +import "github.com/gofiber/fiber/v2" + +type InputField struct { + ID string + Legend string + Type string + Placeholder string + Name string + Value string + Endpoint string + Trigger string + Hint string + InputClass string +} + +func NewInputField(id, name, legend, placeholder, endpoint string) InputField { + return InputField{ + ID: id, + Legend: legend, + Type: "text", + Placeholder: placeholder, + Name: name, + Value: "", + Endpoint: endpoint, + Trigger: "blur", + Hint: "", + InputClass: "", + } +} + +func (f InputField) ToMap() fiber.Map { + return fiber.Map{ + "ID": f.ID, + "Legend": f.Legend, + "Type": f.Type, + "Placeholder": f.Placeholder, + "Name": f.Name, + "Value": f.Value, + "Endpoint": f.Endpoint, + "Trigger": f.Trigger, + "Hint": f.Hint, + "InputClass": f.InputClass, + } +} + +func (f InputField) WithValue(value string) InputField { + f.Value = value + return f +} + +func (f InputField) WithError(hint string) InputField { + f.Hint = hint + f.InputClass = "input-error text-error" + return f +} + +func (f InputField) WithType(inputType string) InputField { + f.Type = inputType + return f +} + +func (f InputField) WithTrigger(trigger string) InputField { + f.Trigger = trigger + return f +} diff --git a/ui/helpers.go b/ui/helpers.go new file mode 100644 index 0000000..49fd4fd --- /dev/null +++ b/ui/helpers.go @@ -0,0 +1,35 @@ +package ui + +// Aquí puedes añadir helpers predefinidos para campos comunes + +// UsernameField crea un campo de username preconfigurado +func UsernameField() InputField { + return NewInputField( + "username-container", + "username", + "Nombre de usuario", + "juan.01", + "/validate/username", + ) +} + +// EmailField crea un campo de email preconfigurado +func EmailField() InputField { + return NewInputField( + "email-container", + "email", + "Correo electrónico", + "juan.01@email.com", + "/validate/email", + ).WithType("email") +} + +func PasswordField() InputField { + return NewInputField( + "password-container", + "password", + "Contraseña", + "••••••••", + "/validate/password", + ).WithType("password") +} diff --git a/ui/validation.go b/ui/validation.go new file mode 100644 index 0000000..2d612aa --- /dev/null +++ b/ui/validation.go @@ -0,0 +1,75 @@ +package ui + +import ( + "errors" + "fmt" + "github.com/go-playground/validator/v10" + "regexp" +) + +// Instancia global del validador +var validate *validator.Validate + +func init() { + validate = validator.New() + _ = validate.RegisterValidation("username", validateUsername) +} + +// GetValidator devuelve la instancia del validador +func GetValidator() *validator.Validate { + return validate +} + +// Validación personalizada de username (ejemplo) +func validateUsername(fl validator.FieldLevel) bool { + value := fl.Field().String() + re := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9_]$`) + if !re.MatchString(value) { + return false + } + if regexp.MustCompile(`[.-]{2}`).MatchString(value) { + return false + } + + return true +} + +var ErrorMessages = map[string]string{ + "required": "Este campo es obligatorio", + "min": "Debe tener al menos %s caracteres", + "max": "No puede tener más de %s caracteres", + "email": "Correo electrónico no válido", + "alphanum": "Solo se permiten letras y números", + "alpha": "Solo se permiten letras", + "numeric": "Solo se permiten números", + "len": "Debe tener exactamente %s caracteres", + "url": "Formato de URL no válido", + "containsany": "Debe contener al menos uno de estos caracteres: %s", + "username": "Formato de usuario no válido", +} + +// translateError convierte un error de validación a mensaje legible +func translateError(err validator.FieldError) string { + template, exists := ErrorMessages[err.Tag()] + if !exists { + return "Valor inválido" + } + + // Si el mensaje tiene placeholder, reemplazar con el parámetro + if err.Param() != "" { + return fmt.Sprintf(template, err.Param()) + } + + return template +} + +// GetFirstError extrae el primer error de validación +func GetFirstError(err error) string { + var validationErrors validator.ValidationErrors + if errors.As(err, &validationErrors) { + if len(validationErrors) > 0 { + return translateError(validationErrors[0]) + } + } + return "Error de validación" +}