From 599c2003a8b25ebdbf762a60a0aeafe9a220989e Mon Sep 17 00:00:00 2001 From: "kacaii.dev" Date: Wed, 17 Jun 2026 10:41:27 -0300 Subject: [PATCH] page/signup: add username field --- src/client.gleam | 11 +- src/client/page/signup.gleam | 239 ++++++++++++++++++++++++++++++++--- 2 files changed, 227 insertions(+), 23 deletions(-) diff --git a/src/client.gleam b/src/client.gleam index 8e890c3..274397b 100644 --- a/src/client.gleam +++ b/src/client.gleam @@ -232,11 +232,14 @@ pub fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { fn handle_signup_message( model: Model, - _page: signup.Model, - _page_message: signup.Message, + page: signup.Model, + page_message: signup.Message, ) -> #(Model, Effect(Message)) { - // TODO: handle message - #(model, effect.none()) + let #(page_model, page_effect) = signup.update(page, page_message, model.lang) + + let page = page.Signup(page_model) + let effect = effect.map(page_effect, SignupMessage) + #(Model(..model, page: page), effect) } fn handle_navigation( diff --git a/src/client/page/signup.gleam b/src/client/page/signup.gleam index cc32252..b52795c 100644 --- a/src/client/page/signup.gleam +++ b/src/client/page/signup.gleam @@ -1,41 +1,153 @@ import client/language as lang import client/route +import gleam/http/response +import gleam/json +import gleam/string import lustre/attribute.{class} as attr import lustre/effect.{type Effect} import lustre/element.{type Element} import lustre/element/html +import lustre/event +import rsvp pub type Model { - Model + Model( + username: String, + email: String, + password: String, + loading: Bool, + message: String, + ) } -pub type Message +pub type Message { + /// User updated username field + UserTypedUsername(username: String) + /// User updated email field + UserTypedEmail(email: String) + /// User updated password field + UserTypedPassword(password: String) + + /// User sent login credentials to the Server + UserClickedSubmit + + ApiReturnedResponse( + returned: Result(response.Response(String), rsvp.Error(String)), + ) +} pub fn init() -> #(Model, Effect(_)) { - #(Model, effect.none()) + #(empty, effect.none()) } -pub fn update(_model: Model, _msg: Message) { - todo +pub const empty = Model( + username: "", + email: "", + password: "", + loading: False, + message: "", +) + +pub fn update(model: Model, message: Message, lang: lang.Language) { + case message { + UserTypedUsername(username:) -> #( + Model(..model, username:, message: ""), + effect.none(), + ) + + UserTypedEmail(email:) -> #( + Model(..model, email:, message: ""), + effect.none(), + ) + + UserTypedPassword(password:) -> #( + Model(..model, password:, message: ""), + effect.none(), + ) + + UserClickedSubmit -> { + let body = + json.object([ + #("nome", json.string(model.username)), + #("email", json.string(model.email)), + #("senha", json.string(model.password)), + ]) + + let handler = rsvp.expect_ok_response(ApiReturnedResponse) + let effect = rsvp.post("/api/clientes", body, handler) + + #(Model(..model, loading: True), effect) + } + + ApiReturnedResponse(returned: Ok(response)) -> #( + Model(..empty, message: response.body), + effect.none(), + ) + + ApiReturnedResponse(returned: Error(reason)) -> { + let message = case reason { + rsvp.HttpError(response) if response.status == 500 -> { + case lang { + lang.BrazillianPortuguese | lang.Portuguese -> + "Erro interno no Servidor" + lang.English -> "Internal Server Error" + lang.German -> "Interner Serverfehler" + lang.Spanish -> "Error Interno del Servidor" + } + } + + rsvp.HttpError(response) -> response.body + + rsvp.NetworkError -> + case lang { + lang.BrazillianPortuguese | lang.Portuguese -> + "Conexão não disponível" + _ -> "Connection not available" + } + + _ -> + case lang { + lang.BrazillianPortuguese | lang.Portuguese -> + "Ocorreu um erro ao enviar credenciais" + _ -> "An error occurred when sending credentials" + } + } + + #(Model(..model, loading: False, message:), effect.none()) + } + } } -pub fn view(_model: Model, lang: lang.Language) -> Element(Message) { +pub fn view(model: Model, lang: lang.Language) -> Element(Message) { + let disabled = + model.loading + || model.username == "" + || model.email == "" + || model.password == "" + || !string.contains(does: model.email, contain: "@") + || !string.contains(does: model.email, contain: ".") + let wrapper_attributes = [ class("p-4 mx-auto max-w-lg h-full"), class("flex flex-col gap-4 justify-center items-center"), ] + let input_style = [ + class("py-2 px-4 my-2 bg-gray-50 rounded-md text-subtext"), + class("text-sm font-light placeholder:text-gray-600"), + class("user-invalid:outline-1 user-invalid:outline-danger"), + class("border border-gray-300"), + ] + html.div(wrapper_attributes, [ html.article([class("w-full rounded-xl card @container")], [ html.header([class("hidden mb-4 md:block")], [ html.h2([class("my-2 text-2xl font-bold")], [ html.text(case lang { lang.BrazillianPortuguese | lang.Portuguese -> "Criar conta" - // TODO: - lang.German -> "todo" + lang.German -> "Benutzerkonto erstellen" lang.English -> "Create account" - // TODO: - lang.Spanish -> "todo" + lang.Spanish -> "Crear una cuenta" }), ]), @@ -44,10 +156,8 @@ pub fn view(_model: Model, lang: lang.Language) -> Element(Message) { html.text(case lang { lang.BrazillianPortuguese | lang.Portuguese -> "Ja tem conta?" lang.English -> "Already have an account?" - // TODO: - lang.German -> "todo" - // TODO: - lang.Spanish -> "todo" + lang.German -> "Haben Sie bereits ein Konto?" + lang.Spanish -> "¿Ya tienes una cuenta?" }), ]), html.a( @@ -56,16 +166,107 @@ pub fn view(_model: Model, lang: lang.Language) -> Element(Message) { html.text(case lang { lang.BrazillianPortuguese | lang.Portuguese -> "Entrar" lang.English -> "Login" - // TODO: - lang.German -> "todo" - // TODO: - lang.Spanish -> "todo" + lang.German -> "Zum Eintragen" + lang.Spanish -> "Para entrar" }), ], ), ]), - html.div([], []), + html.div([], [ + html.label([attr.data("field", ""), class("mt-4")], [ + html.text(case lang { + lang.BrazillianPortuguese | lang.Portuguese -> "Nome Completo" + lang.English -> "Username" + lang.German -> "todo:" + lang.Spanish -> "todo:" + }), + + html.input([ + attr.placeholder("Vertis Climate"), + event.on_input(UserTypedUsername), + attr.required(True), + attr.id("input-username"), + attr.type_("text"), + attr.value(model.username), + ..input_style + ]), + ]), + + html.label([attr.data("field", ""), class("mt-4")], [ + html.text("E-mail"), + html.input([ + attr.placeholder({ + case lang { + lang.BrazillianPortuguese | lang.Portuguese -> "seu" + lang.English -> "your" + lang.German -> "dein" + lang.Spanish -> "su" + } + <> "@email.com" + }), + event.on_input(UserTypedEmail), + attr.required(True), + attr.id("input-email"), + attr.type_("email"), + attr.value(model.email), + ..input_style + ]), + ]), + + //  Password + html.label([attr.data("field", ""), class("mt-4")], [ + html.text(case lang { + lang.BrazillianPortuguese | lang.Portuguese -> "Senha" + lang.English -> "Password" + lang.German -> "Passwort" + lang.Spanish -> "Contraseña" + }), + + html.input([ + attr.placeholder("********"), + event.on_input(UserTypedPassword), + attr.required(True), + attr.id("input-password"), + attr.type_("password"), + attr.value(model.password), + ..input_style + ]), + ]), + ]), + + html.footer([], [ + html.button( + [ + event.on_click(UserClickedSubmit), + attr.id("submit"), + attr.disabled(disabled), + attr.aria_busy(model.loading), + class("w-full bg-green-500"), + ], + [ + html.text(case lang { + lang.BrazillianPortuguese | lang.Portuguese -> + "Criar conta gratuitamente" + lang.English -> "Create account for free" + lang.German -> "Erstellen Sie ein kostenloses Konto" + lang.Spanish -> "Crea una cuenta gratuita" + }), + ], + ), + + case model.message { + "" -> element.none() + + value -> { + let alert_attr = [attr.role("alert")] + + html.div([class("mt-2"), ..alert_attr], [ + html.strong([], [html.text(value)]), + ]) + } + }, + ]), ]), ]), ]) -- 2.51.2