Progressively enhanced register form

Thanks copilot for all the shit I didn't have to write.
This commit is contained in:
Tyler Hallada 2023-12-18 22:27:03 -05:00
parent 5881412b59
commit 7abffb2729
2 changed files with 110 additions and 77 deletions

View File

@ -1,6 +1,7 @@
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::TypedHeader; use axum::TypedHeader;
use axum::{extract::State, Form}; use axum::{extract::State, Form};
use http::HeaderValue;
use lettre::SmtpTransport; use lettre::SmtpTransport;
use maud::html; use maud::html;
use serde::Deserialize; use serde::Deserialize;
@ -25,8 +26,17 @@ pub struct Register {
pub name: Option<String>, pub name: Option<String>,
} }
pub async fn get(hx_target: Option<TypedHeader<HXTarget>>, layout: Layout) -> Result<Response> { pub fn register_page(
Ok(layout hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
form_props: RegisterFormProps,
) -> Response {
if let Some(hx_target) = &hx_target {
if hx_target.target == HeaderValue::from_static("register-form") {
return register_form(form_props).into_response();
}
}
layout
.with_subtitle("register") .with_subtitle("register")
.targeted(hx_target) .targeted(hx_target)
.render(html! { .render(html! {
@ -34,9 +44,18 @@ pub async fn get(hx_target: Option<TypedHeader<HXTarget>>, layout: Layout) -> Re
header class="center-text" { header class="center-text" {
h2 { "Register" } h2 { "Register" }
} }
(register_form(RegisterFormProps::default())) (register_form(form_props))
} }
})) })
.into_response()
}
pub async fn get(hx_target: Option<TypedHeader<HXTarget>>, layout: Layout) -> Result<Response> {
Ok(register_page(
hx_target,
layout,
RegisterFormProps::default(),
))
} }
pub async fn post( pub async fn post(
@ -49,22 +68,16 @@ pub async fn post(
Form(register): Form<Register>, Form(register): Form<Register>,
) -> Result<Response> { ) -> Result<Response> {
if register.password != register.password_confirmation { if register.password != register.password_confirmation {
return Ok(layout return Ok(register_page(
.with_subtitle("register") hx_target,
.targeted(hx_target) layout,
.render(html! { RegisterFormProps {
div class="center-horizontal" { email: Some(register.email),
header class="center-text" { name: register.name,
h2 { "Register" } password_error: Some("passwords do not match".to_string()),
} ..Default::default()
(register_form(RegisterFormProps { },
email: Some(register.email), ));
name: register.name,
password_error: Some("passwords do not match".to_string()),
..Default::default()
}))
}
}));
} }
let user = match User::create( let user = match User::create(
&pool, &pool,
@ -80,62 +93,50 @@ pub async fn post(
Err(err) => { Err(err) => {
if let Error::InvalidEntity(validation_errors) = err { if let Error::InvalidEntity(validation_errors) = err {
let field_errors = validation_errors.field_errors(); let field_errors = validation_errors.field_errors();
return Ok(layout return Ok(register_page(
.with_subtitle("register") hx_target,
.targeted(hx_target) layout,
.render(html! { RegisterFormProps {
div class="center-horizontal" { email: Some(register.email),
header class="center-text" { name: register.name,
h2 { "Register" } email_error: field_errors.get("email").map(|&errors| {
} errors
(register_form(RegisterFormProps { .iter()
email: Some(register.email), .filter_map(|error| error.message.clone().map(|m| m.to_string()))
name: register.name, .collect::<Vec<String>>()
email_error: field_errors.get("email").map(|&errors| { .join(", ")
errors }),
.iter() name_error: field_errors.get("name").map(|&errors| {
.filter_map(|error| error.message.clone().map(|m| m.to_string())) errors
.collect::<Vec<String>>() .iter()
.join(", ") .filter_map(|error| error.message.clone().map(|m| m.to_string()))
}), .collect::<Vec<String>>()
name_error: field_errors.get("name").map(|&errors| { .join(", ")
errors }),
.iter() password_error: field_errors.get("password").map(|&errors| {
.filter_map(|error| error.message.clone().map(|m| m.to_string())) errors
.collect::<Vec<String>>() .iter()
.join(", ") .filter_map(|error| error.message.clone().map(|m| m.to_string()))
}), .collect::<Vec<String>>()
password_error: field_errors.get("password").map(|&errors| { .join(", ")
errors }),
.iter() ..Default::default()
.filter_map(|error| error.message.clone().map(|m| m.to_string())) },
.collect::<Vec<String>>() ));
.join(", ")
}),
..Default::default()
}))
}
}));
} }
if let Error::Sqlx(sqlx::error::Error::Database(db_error)) = &err { if let Error::Sqlx(sqlx::error::Error::Database(db_error)) = &err {
if let Some(constraint) = db_error.constraint() { if let Some(constraint) = db_error.constraint() {
if constraint == "users_email_idx" { if constraint == "users_email_idx" {
return Ok(layout return Ok(register_page(
.with_subtitle("register") hx_target,
.targeted(hx_target) layout,
.render(html! { RegisterFormProps {
div class="center-horizontal" { email: Some(register.email),
header class="center-text" { name: register.name,
h2 { "Register" } email_error: Some("email already exists".to_string()),
} ..Default::default()
(register_form(RegisterFormProps { },
email: Some(register.email), ));
name: register.name,
email_error: Some("email already exists".to_string()),
..Default::default()
}))
}
}));
} }
} }
} }

View File

@ -20,24 +20,56 @@ pub fn register_form(props: RegisterFormProps) -> Markup {
general_error, general_error,
} = props; } = props;
html! { html! {
form action="/register" method="post" class="auth-form-grid" { form
action="/register"
method="post"
hx-post="/register"
hx-target="#register-form"
hx-swap="outerHTML"
id="register-form"
class="auth-form-grid"
{
label for="email" { "Email *" } label for="email" { "Email *" }
input type="email" name="email" id="email" placeholder="Email" value=(email.unwrap_or_default()) required; input
type="email"
name="email"
id="email"
placeholder="Email"
value=(email.unwrap_or_default())
required;
@if let Some(email_error) = email_error { @if let Some(email_error) = email_error {
span class="error" { (email_error) } span class="error" { (email_error) }
} }
label for="name" { (PreEscaped("Name &nbsp;")) } label for="name" { (PreEscaped("Name &nbsp;")) }
input type="text" name="name" id="name" value=(name.unwrap_or_default()) placeholder="Name" maxlength="255"; input
type="text"
name="name"
id="name"
value=(name.unwrap_or_default())
placeholder="Name"
maxlength="255";
@if let Some(name_error) = name_error { @if let Some(name_error) = name_error {
span class="error" { (name_error) } span class="error" { (name_error) }
} }
label for="email" { "Password *" } label for="email" { "Password *" }
input type="password" name="password" id="password" placeholder="Password" minlength="8" maxlength="255" required; input
type="password"
name="password"
id="password"
placeholder="Password"
minlength="8"
maxlength="255"
required;
@if let Some(password_error) = password_error { @if let Some(password_error) = password_error {
span class="error" { (password_error) } span class="error" { (password_error) }
} }
label for="password_confirmation" { "Confirm Password *" } label for="password_confirmation" { "Confirm Password *" }
input type="password" name="password_confirmation" id="password_confirmation" placeholder="Confirm Password" required; input
type="password"
name="password_confirmation"
id="password_confirmation"
placeholder="Confirm Password"
required;
button type="submit" { "Submit" } button type="submit" { "Submit" }
@if let Some(general_error) = general_error { @if let Some(general_error) = general_error {
span class="error" { (general_error) } span class="error" { (general_error) }