Implemented user menu. Replaced favicon with app logo.

main
Josef Rokos 2 years ago
parent 17f628739f
commit 2122b3a4f7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 260 KiB

@ -37,6 +37,7 @@ pub struct User {
pub login: String, pub login: String,
pub password: String, pub password: String,
pub full_name: Option<String>, pub full_name: Option<String>,
#[validate(email(message = "Enter valid email address"))]
pub email: Option<String>, pub email: Option<String>,
pub admin: bool, pub admin: bool,
pub get_emails: bool, pub get_emails: bool,
@ -48,6 +49,59 @@ impl User {
} }
} }
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)]
pub struct UserProfile {
login: String,
full_name: String,
#[validate(email(message = "Enter valid email address"))]
email: String,
get_emails: Option<String>
}
impl UserProfile {
pub fn login(&self) -> &str {
&self.login
}
pub fn full_name(&self) -> &str {
&self.full_name
}
pub fn email(&self) -> &str {
&self.email
}
pub fn get_emails(&self) -> bool {
self.get_emails.is_some()
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)]
pub struct PwdChange {
login: String,
old_password: String,
#[validate(length(min = 1, message = "Enter new password"),
must_match(other = "password_ver", message = "Passwords doesn't match"))]
password: String,
password_ver: String
}
impl PwdChange {
pub fn login(&self) -> &str {
&self.login
}
pub fn old_password(&self) -> &str {
&self.old_password
}
pub fn password(&self) -> &str {
&self.password
}
pub fn password_ver(&self) -> &str {
&self.password_ver
}
}
/*pub struct Property { /*pub struct Property {
id: u16, id: u16,
name: String, name: String,

@ -21,6 +21,23 @@ macro_rules! perm_check {
} }
} }
#[macro_export]
macro_rules! user_check {
($check:expr) => {
use crate::perm_check;
use crate::backend::user::logged_in_user;
perm_check!(is_logged_in);
if logged_in_user().await.unwrap_or(User::default()).login != $check {
let response = expect_context::<ResponseOptions>();
response.set_status(StatusCode::FORBIDDEN);
return Ok(ApiResponse::Error("You can change your own profile only".to_string()))
}
}
}
cfg_if!{ cfg_if!{
if #[cfg(feature = "ssr")] { if #[cfg(feature = "ssr")] {
use sqlx::PgPool; use sqlx::PgPool;

@ -1,11 +1,11 @@
use cfg_if::cfg_if; use cfg_if::cfg_if;
use leptos::*; use leptos::*;
use crate::backend::data::{ApiResponse, PwdChange, User, UserProfile};
cfg_if! { if #[cfg(feature = "ssr")] { cfg_if! { if #[cfg(feature = "ssr")] {
use sqlx::{query_as, Error, PgPool, query}; use sqlx::{query_as, Error, PgPool, query};
use actix_session::*; use actix_session::*;
use leptos_actix::{extract, redirect}; use leptos_actix::{extract, redirect};
use crate::backend::data::User;
pub async fn has_admin_user(pool: &PgPool) -> Result<bool, Error> { pub async fn has_admin_user(pool: &PgPool) -> Result<bool, Error> {
let count: (i64,) = query_as(r#"SELECT COUNT(id) FROM "user" WHERE admin = $1"#) let count: (i64,) = query_as(r#"SELECT COUNT(id) FROM "user" WHERE admin = $1"#)
@ -57,27 +57,32 @@ cfg_if! { if #[cfg(feature = "ssr")] {
}} }}
#[server(Login, "/api")] #[server(Login, "/api")]
pub async fn login(username: String, password: String) -> Result<(), ServerFnError> { pub async fn login(username: String, password: String) -> Result<ApiResponse<()>, ServerFnError> {
use crate::backend::AppData; use crate::backend::AppData;
use actix_session::*; use actix_session::*;
use actix_web::web::Data; use actix_web::web::Data;
use leptos_actix::extract; use leptos_actix::extract;
use actix_web::http::StatusCode;
use leptos_actix::ResponseOptions;
let pool = extract(|data: Data<AppData>| async move { data.db_pool().clone() }).await?; let pool = extract(|data: Data<AppData>| async move { data.db_pool().clone() }).await?;
let user = user_from_login(&pool, &username).await?; let user = user_from_login(&pool, &username).await.unwrap_or(User::default());
if pwhash::bcrypt::verify(password, &user.password) { if !user.login.is_empty() && pwhash::bcrypt::verify(password, &user.password) {
extract(|session: Session| async move { extract(|session: Session| async move {
let _ = session.insert("user", user); let _ = session.insert("user", user);
}) })
.await?; .await?;
redirect("/admin"); redirect("/admin");
return Ok(()); return Ok(ApiResponse::Data(()));
} }
Err(ServerFnError::ServerError("Bad login".to_string())) let response = expect_context::<ResponseOptions>();
response.set_status(StatusCode::UNAUTHORIZED);
return Ok(ApiResponse::Error("Bad username or password".to_string()))
} }
#[server] #[server]
@ -87,7 +92,6 @@ pub async fn logout() -> Result<(), ServerFnError> {
}).await?; }).await?;
redirect("/login"); redirect("/login");
Ok(()) Ok(())
} }
@ -100,3 +104,62 @@ pub async fn auth_check() -> Result<bool, ServerFnError> {
pub async fn admin_check() -> Result<bool, ServerFnError> { pub async fn admin_check() -> Result<bool, ServerFnError> {
Ok(is_admin().await) Ok(is_admin().await)
} }
#[server]
pub async fn get_user() -> Result<Option<User>, ServerFnError> {
Ok(logged_in_user().await)
}
#[server]
pub async fn update_profile(user: UserProfile) -> Result<ApiResponse<()>, ServerFnError> {
use crate::backend::AppData;
use actix_web::web::Data;
use leptos_actix::extract;
use crate::user_check;
user_check!(user.login());
let pool = extract(|data: Data<AppData>| async move { data.db_pool().clone() }).await?;
sqlx::query(r#"UPDATE "user" SET full_name = $1, email = $2, get_emails = $3 WHERE login = $4"#)
.bind(user.full_name())
.bind(user.email())
.bind(user.get_emails())
.bind(user.login())
.execute(&pool)
.await?;
let usr = user_from_login(&pool, user.login()).await?;
extract(|session: Session| async move {
let _ = session.insert("user", usr);
}).await?;
Ok(ApiResponse::Data(()))
}
#[server]
pub async fn change_pwd(new_pw: PwdChange) -> Result<ApiResponse<()>, ServerFnError> {
use crate::backend::AppData;
use actix_web::web::Data;
use leptos_actix::extract;
use crate::user_check;
user_check!(new_pw.login());
let pool = extract(|data: Data<AppData>| async move { data.db_pool().clone() }).await?;
let usr = user_from_login(&pool, new_pw.login()).await?;
if !pwhash::bcrypt::verify(new_pw.old_password(), &usr.password) {
let response = expect_context::<ResponseOptions>();
response.set_status(StatusCode::UNAUTHORIZED);
return Ok(ApiResponse::Error("Invalid old password".to_string()))
}
sqlx::query(r#"UPDATE "user" SET password = $1 WHERE login = $2"#)
.bind(pwhash::bcrypt::hash(new_pw.password()).unwrap())
.bind(new_pw.login())
.execute(&pool)
.await?;
Ok(ApiResponse::Data(()))
}

@ -1,8 +1,18 @@
use leptos::*; use leptos::*;
use crate::backend::data::User;
use crate::components::modal_box::DialogOpener;
use crate::components::user_menu::{MenuOpener, UserMenu};
use crate::locales::trl; use crate::locales::trl;
use crate::pages::change_pwd::ChangePassword;
use crate::pages::profile_edit::ProfileEdit;
#[component] #[component]
pub fn AdminPortal(children: Children) -> impl IntoView { pub fn AdminPortal(children: Children) -> impl IntoView {
let user_menu = MenuOpener::new();
let (user, set_user) = create_signal(User::default());
let editor = DialogOpener::new();
let pw_changer = DialogOpener::new();
view! { view! {
<div class="layout-wrapper layout-content-navbar"> <div class="layout-wrapper layout-content-navbar">
<div class="layout-container"> <div class="layout-container">
@ -13,6 +23,7 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
<a href="javascript:void(0);" class="layout-menu-toggle menu-link text-large ms-auto d-block d-xl-none"> <a href="javascript:void(0);" class="layout-menu-toggle menu-link text-large ms-auto d-block d-xl-none">
<i class="bx bx-chevron-left bx-sm align-middle"></i> <i class="bx bx-chevron-left bx-sm align-middle"></i>
</a> </a>
<img src="/rezervovator_l.svg" width="180"/>
</div> </div>
<div class="menu-inner-shadow"></div> <div class="menu-inner-shadow"></div>
@ -73,53 +84,14 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
</li> </li>
//<!-- User --> //<!-- User -->
<li class="nav-item navbar-dropdown dropdown-user dropdown"> <li class="nav-item navbar-dropdown dropdown-user dropdown">
<a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown"> <a class="nav-link dropdown-toggle hide-arrow" href="#"
on:click=move |_| user_menu.toggle()>
//<div class="avatar avatar-online"> //<div class="avatar avatar-online">
// <img src="/img/avatars/1.png" alt class="w-px-40 h-auto rounded-circle" /> // <img src="/img/avatars/1.png" alt class="w-px-40 h-auto rounded-circle" />
//</div> //</div>
<i class="bx bx-user fs-3 lh-0"></i> <i class="bx bx-user fs-3 lh-0"></i>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end"> <UserMenu opener=user_menu editor=editor pw_dialog=pw_changer user_profile=set_user/>
<li>
<a class="dropdown-item" href="#">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<div class="avatar avatar-online">
<img src="/img/avatars/1.png" alt class="w-px-40 h-auto rounded-circle" />
</div>
</div>
<div class="flex-grow-1">
<span class="fw-semibold d-block">"John Doe"</span>
<small class="text-muted">"Admin"</small>
</div>
</div>
</a>
</li>
<li>
<div class="dropdown-divider"></div>
</li>
<li>
<a class="dropdown-item" href="#">
<i class="bx bx-user me-2"></i>
<span class="align-middle">"My Profile"</span>
</a>
</li>
<li>
<a class="dropdown-item" href="#">
<i class="bx bx-cog me-2"></i>
<span class="align-middle">"Settings"</span>
</a>
</li>
<li>
<div class="dropdown-divider"></div>
</li>
<li>
<a class="dropdown-item" href="auth-login-basic.html">
<i class="bx bx-power-off me-2"></i>
<span class="align-middle">"Log Out"</span>
</a>
</li>
</ul>
</li> </li>
//<!--/ User --> //<!--/ User -->
</ul> </ul>
@ -130,6 +102,8 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
<div class="content-wrapper"> <div class="content-wrapper">
//<!-- Content --> //<!-- Content -->
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y">
<ProfileEdit user={user} opener=editor/>
<ChangePassword user={user} opener=pw_changer/>
{children()} {children()}
</div> </div>
</div> </div>

@ -13,6 +13,7 @@ pub fn Header() -> impl IntoView {
("data-template", "vertical-menu-template-free"), ("data-template", "vertical-menu-template-free"),
("data-assets-path", "/")]) ("data-assets-path", "/")])
/> />
<Title text="Rezervovator"/>
<Meta charset="utf-8"/> <Meta charset="utf-8"/>
<Meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"/> <Meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"/>

@ -3,4 +3,5 @@ pub mod server_err;
pub mod validation_err; pub mod validation_err;
pub mod header; pub mod header;
pub mod admin_portal; pub mod admin_portal;
pub mod user_menu;

@ -1,6 +1,7 @@
use crate::components::modal_box::DialogOpener; use crate::components::modal_box::DialogOpener;
use leptos::*; use leptos::*;
use crate::backend::data::ApiResponse; use crate::backend::data::ApiResponse;
use crate::locales::trl;
#[component] #[component]
pub fn ServerErr( pub fn ServerErr(
@ -9,7 +10,27 @@ pub fn ServerErr(
) -> impl IntoView { ) -> impl IntoView {
view! {{move || { view! {{move || {
if let Some(val) = result.get() { if let Some(val) = result.get() {
if let Err(e) = val { match val {
Ok(resp) => if let ApiResponse::Error(err) = resp {
view! {
<div class="alert alert-danger">
{trl(&err)}
</div>
}
}
else {
opener.hide();
view! {<div></div>}
}
Err(e) => {
view! {
<div class="alert alert-danger">
"Server error: " {e.to_string()}
</div>
}
}
}
/*if let Err(e) = val {
view! { view! {
<div class="alert alert-danger"> <div class="alert alert-danger">
"Server error: " {e.to_string()} "Server error: " {e.to_string()}
@ -18,7 +39,7 @@ pub fn ServerErr(
} else { } else {
opener.hide(); opener.hide();
view! {<div></div>} view! {<div></div>}
} }*/
} else { } else {
view! {<div></div>} view! {<div></div>}
} }

@ -0,0 +1,92 @@
use leptos::*;
use crate::backend::data::User;
use crate::backend::user::{get_user, logout};
use crate::components::modal_box::DialogOpener;
use crate::locales::trl;
#[derive(Copy, Clone)]
pub struct MenuOpener {
visible: ReadSignal<bool>,
set_visible: WriteSignal<bool>,
}
impl MenuOpener {
pub fn new() -> Self {
let (visible, set_visible) = create_signal(false);
MenuOpener {
visible,
set_visible,
}
}
pub fn visible(&self) -> bool {
self.visible.get()
}
pub fn toggle(&self) {
let visible = self.visible.get();
self.set_visible.update(|v| *v = !visible)
}
}
#[component]
pub fn UserMenu(
opener: MenuOpener,
editor: DialogOpener,
pw_dialog: DialogOpener,
user_profile: WriteSignal<User>) -> impl IntoView {
let user = create_resource(move || opener.visible(), move |_| get_user());
view! {
<ul class={move || if opener.visible() {"dropdown-menu dropdown-menu-end show"} else
{"dropdown-menu dropdown-menu-end"}}
data-bs-popper="none">
<li>
<a class="dropdown-item" href="#" on:click=move |_| {editor.show(); opener.toggle()}>
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<i class="bx bxs-user-account" />
</div>
<div class="flex-grow-1">
<Suspense fallback=move || view! {<span>"Loading..."</span>}>
{move || {
user.get().map(|u| match u {
Ok(user) => {
let usr = user.unwrap_or(User::default());
user_profile.update(|u| *u = usr.clone());
view! {
<span class="fw-semibold d-block">
{usr.full_name.unwrap_or("".to_string())}
</span>
//<small class="text-muted">"Admin"</small>
}},
Err(_) => view! {<span>"Error loading user"</span>}
})
}}
</Suspense>
</div>
</div>
</a>
</li>
<li>
<a class="dropdown-item" href="#" on:click=move |_| {pw_dialog.show(); opener.toggle()}>
<i class="bx bx-lock me-2"></i>
<span class="align-middle">{trl("Change password")}</span>
</a>
</li>
<li>
<div class="dropdown-divider"></div>
</li>
<li>
<a class="dropdown-item" href="/login" on:click=move |_| {
spawn_local(async move {
let _ = logout().await;
});
}>
<i class="bx bx-power-off me-2"></i>
<span class="align-middle">{trl("Log Out")}</span>
</a>
</li>
</ul>
}
}

@ -24,7 +24,7 @@ pub fn ValidationErr(
} else { } else {
view! { view! {
<div class="alert alert-danger"> <div class="alert alert-danger">
"Validation error" {trl("Validation error")}
</div> </div>
} }
} }

@ -14,6 +14,8 @@ lazy_static! {
("Save changes", "Uložit změny"), ("Save changes", "Uložit změny"),
("Company info", "Organizace"), ("Company info", "Organizace"),
("Name cannot be empty", "Jméno nesmí být prázdné"), ("Name cannot be empty", "Jméno nesmí být prázdné"),
("Invalid old password", "Neplatné staré heslo"),
("Please sign-in to your account", "Přihlaste se prosím k uživatelskému účtu"),
])), ])),
("sk", HashMap::from( [ ("sk", HashMap::from( [
("Dashboard", "Prehlad"), ("Dashboard", "Prehlad"),

@ -0,0 +1,90 @@
use leptos::*;
use leptos_router::*;
use crate::backend::data::{ApiResponse, User};
use crate::backend::user::ChangePwd;
use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog, ModalFooter};
use crate::components::server_err::ServerErr;
use crate::components::validation_err::ValidationErr;
use crate::locales::trl;
use crate::validator::Validator;
#[component]
pub fn change_password(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView {
let change_pwd = create_server_action::<ChangePwd>();
let upd_val = change_pwd.value();
let validator = Validator::new();
let empty = create_rw_signal("".to_string());
view! {
{move || {
if let Some(res) = upd_val.get() {
if let Ok(r) = res {
if let ApiResponse::Data(_) = r { empty.update(|e| *e = "".to_string())}
}
}
view! {
<ActionForm
on:submit=move |ev| {
let act = ChangePwd::from_event(&ev);
if !act.is_err() {
validator.check(&act.unwrap().new_pw, &ev);
}
}
action=change_pwd>
<ModalDialog opener=opener title="Change password">
<ModalBody>
<ServerErr result={upd_val} opener=opener/>
<ValidationErr validator=validator />
<input type="hidden" value={move || user.get().login} name="new_pw[login]"/>
<div class="row">
<div class="col mb-3">
<label for="oldPw" class="form-label">"Old password"</label>
<input
type="password"
id="oldPw"
class="form-control"
name="new_pw[old_password]"
prop:value={move || empty.get()}
/>
</div>
</div>
<div class="row">
<div class="col mb-3">
<label for="newPw" class="form-label">"New password"</label>
<input
type="password"
id="newPw"
class="form-control"
name="new_pw[password]"
prop:value={move || empty.get()}
/>
</div>
</div>
<div class="row">
<div class="col mb-3">
<label for="verPw" class="form-label">"Verify password"</label>
<input
type="password"
id="verPw"
class="form-control"
name="new_pw[password_ver]"
prop:value={move || empty.get()}
/>
</div>
</div>
</ModalBody>
<ModalFooter>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"
on:click=move |_| {validator.reset(); opener.hide(); empty.update(|e| *e = "".to_string())}>
{trl("Close")}
</button>
<button type="submit" class="btn btn-primary">
{trl("Save changes")}
</button>
</ModalFooter>
</ModalDialog>
</ActionForm>
}
}}
}
}

@ -8,7 +8,7 @@ use crate::pages::company_edit::CompanyEdit;
#[component] #[component]
pub fn CompanyInfo() -> impl IntoView { pub fn CompanyInfo() -> impl IntoView {
let editor = DialogOpener::new(); let editor = DialogOpener::new();
let company = create_resource(move|| editor.visible(), move |_| { get_company() }); let company = create_blocking_resource(move|| editor.visible(), move |_| { get_company() });
let (cmp, set_cmp) = create_signal(Company::default()); let (cmp, set_cmp) = create_signal(Company::default());
view! { view! {

@ -2,49 +2,51 @@ use leptos::*;
use leptos_meta::*; use leptos_meta::*;
use leptos_router::ActionForm; use leptos_router::ActionForm;
use crate::backend::user::Login; use crate::backend::user::Login;
use crate::components::modal_box::DialogOpener;
use crate::components::server_err::ServerErr;
use crate::locales::trl;
#[component] #[component]
pub fn Login() -> impl IntoView { pub fn Login() -> impl IntoView {
let login = create_server_action::<Login>(); let login = create_server_action::<Login>();
let login_val = login.value();
view! { view! {
<Link rel="stylesheet" href="/vendor/css/pages/page-auth.css" /> <Link rel="stylesheet" href="/vendor/css/pages/page-auth.css" />
<div class="authentication-wrapper authentication-basic container-p-y"> <div class="authentication-wrapper authentication-basic container-p-y">
<div class="authentication-inner"> <div class="authentication-inner">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
//<!-- Logo --> //<!-- Logo -->
<div class="app-brand justify-content-center"> <div class="app-brand justify-content-center">
<a href="index.html" class="app-brand-link gap-2"> <a href="index.html" class="app-brand-link gap-2">
<span class="app-brand-logo demo"> <span class="app-brand-logo demo">
<img src="/rezervovator_l.svg" width="200"/>
</span> </span>
//<span class="app-brand-text demo text-body fw-bolder">Sneat</span> //<span class="app-brand-text demo text-body fw-bolder">Sneat</span>
</a> </a>
</div> </div>
//<!-- /Logo --> //<!-- /Logo -->
<h4 class="mb-2">"Welcome to Rezervator 👋"</h4> <p class="mb-4">{trl("Please sign-in to your account")}</p>
<p class="mb-4">"Please sign-in to your account and start the adventure"</p>
<ActionForm action=login> <ActionForm action=login>
<ServerErr result=login_val opener=DialogOpener::new()/>
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">"Username"</label> <label for="username" class="form-label">{trl("Username")}</label>
<input <input
type="text" type="text"
class="form-control" class="form-control"
id="username" id="username"
name="username" name="username"
placeholder="Enter your username" placeholder={trl("Enter your username")}
autofocus autofocus
/> />
</div> </div>
<div class="mb-3 form-password-toggle"> <div class="mb-3 form-password-toggle">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<label class="form-label" for="password">"Password"</label> <label class="form-label" for="password">{trl("Password")}</label>
<a href="auth-forgot-password-basic.html"> /*<a href="auth-forgot-password-basic.html">
<small>"Forgot Password?"</small> <small>"Forgot Password?"</small>
</a> </a>*/
</div> </div>
<div class="input-group input-group-merge"> <div class="input-group input-group-merge">
<input <input
@ -61,7 +63,7 @@ pub fn Login() -> impl IntoView {
</div> </div>
<div class="mb-3"> <div class="mb-3">
<button class="btn btn-primary d-grid w-100" type="submit">"Sign in"</button> <button class="btn btn-primary d-grid w-100" type="submit">{trl("Sign in")}</button>
</div> </div>
</ActionForm> </ActionForm>

@ -4,3 +4,5 @@ pub mod company_info;
mod company_edit; mod company_edit;
pub mod login; pub mod login;
pub mod public; pub mod public;
pub mod profile_edit;
pub mod change_pwd;

@ -0,0 +1,83 @@
use leptos::*;
use leptos_router::*;
use crate::backend::data::User;
use crate::backend::user::UpdateProfile;
use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog, ModalFooter};
use crate::components::server_err::ServerErr;
use crate::components::validation_err::ValidationErr;
use crate::locales::trl;
use crate::validator::Validator;
#[component]
pub fn ProfileEdit(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView {
let update_user = create_server_action::<UpdateProfile>();
let upd_val = update_user.value();
let validator = Validator::new();
view! {
<ActionForm
on:submit=move |ev| {
let act = UpdateProfile::from_event(&ev);
if !act.is_err() {
validator.check(&act.unwrap().user, &ev);
}
}
action=update_user>
<ModalDialog opener=opener title="Edit profile">
<ModalBody>
<ServerErr result={upd_val} opener=opener/>
<ValidationErr validator=validator />
<input type="hidden" value={move || user.get().login} name="user[login]"/>
<div class="row">
<div class="col mb-3">
<label for="name" class="form-label">"Full name"</label>
<input
type="text"
id="name"
class="form-control"
placeholder="Enter Full name"
prop:value={move || user.get().full_name}
name="user[full_name]"
/>
</div>
</div>
<div class="row">
<div class="col mb-3">
<label for="email" class="form-label">"Email"</label>
<input
type="text"
id="name"
class="form-control"
placeholder="Enter email"
prop:value={move || user.get().email.unwrap_or("".to_string())}
name="user[email]"
/>
</div>
</div>
<div class="row">
<div class="col mb-3">
<input
class="form-check-input"
type="checkbox"
id="getMail"
prop:value={move || if user.get().get_emails {"true"} else {"false"}}
prop:checked={move || user.get().get_emails}
name="user[get_emails]"
/>
<label class="form-check-label" for="getMail">"Get emails"</label>
</div>
</div>
</ModalBody>
<ModalFooter>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"
on:click=move |_| {validator.reset(); opener.hide()}>
{trl("Close")}
</button>
<button type="submit" class="btn btn-primary">
{trl("Save changes")}
</button>
</ModalFooter>
</ModalDialog>
</ActionForm>
}
}
Loading…
Cancel
Save