Skip to content

Commit

Permalink
feat: design && impl profile page (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
wiseaidev authored Nov 27, 2024
1 parent 5c5e665 commit bce6b53
Show file tree
Hide file tree
Showing 18 changed files with 547 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/components/common/logo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pub fn Logo() -> Element {
rsx! {
div { class: "flex items-center",
img {
src: "./logo.webp",
src: "https://aibook-8syx.onrender.com/logo.webp",
alt: "AI Book Logo",
class: "w-24 h-24 object-contain"
}
Expand Down
39 changes: 37 additions & 2 deletions src/components/dashboard/navbar.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::components::spinner::Spinner;
use crate::components::spinner::SpinnerSize;
use crate::server::auth::controller::about_me;
use crate::server::auth::model::User;
use crate::theme::Theme;
use crate::theme::ThemeToggle;
use dioxus::prelude::*;
Expand All @@ -14,6 +16,26 @@ pub fn Navbar() -> Element {
let dark_mode = theme() == Theme::Dark;
let navigator = use_navigator();

let mut user_data = use_signal(|| None::<User>);

use_effect(move || {
spawn(async move {
let token: String = SessionStorage::get("jwt").unwrap_or_default();
if token.is_empty() {
navigator.push("/login");
} else {
match about_me(token.clone()).await {
Ok(res) => {
user_data.set(Some(res.data.user));
}
Err(_) => {
navigator.push("/login");
}
}
}
});
});

let handle_logout = move |e: Event<MouseData>| {
e.stop_propagation();
loading.set(false);
Expand All @@ -23,6 +45,15 @@ pub fn Navbar() -> Element {
navigator.push("/login");
};

let handle_profile = move |e: Event<MouseData>| {
e.stop_propagation();
loading.set(false);

if user_data().is_some() {
navigator.push(format!("/dashboard/profile/{}", user_data().unwrap().id));
}
};

rsx! {
div { class: format!("flex justify-between items-center mb-4 border-b shadow-sm p-2 {}", if dark_mode { "dark:border-gray-700" } else { "" }),
h1 { class: "text-2xl font-semibold", "Dashboard" }
Expand All @@ -35,14 +66,18 @@ pub fn Navbar() -> Element {
class: format!("p-2 rounded-full flex items-center justify-center {}", if dark_mode { "bg-gray-700" } else { "bg-gray-200" }),
onclick: move |_| show_dropdown.set(!show_dropdown()),
img {
src: "./features.webp",
src: "https://rustacean.net/assets/rustacean-orig-noshadow.svg",
alt: "User profile image",
class: "w-8 h-8 rounded-full"
}
}
if show_dropdown() {
div { class: format!("absolute right-0 mt-2 w-48 shadow-lg rounded-lg {}", if dark_mode { "bg-gray-800" } else { "bg-white" }),
button { class: format!("w-full text-left px-4 py-2 hover:bg-gray-100 {}", if dark_mode { "hover:bg-gray-700" } else { "" }), "Profile" }
button {
class: format!("w-full text-left px-4 py-2 hover:bg-gray-100 {}", if dark_mode { "hover:bg-gray-700" } else { "" }),
onclick: handle_profile,
"Profile"
}
button {
class: "w-full text-left px-4 py-2 hover:bg-gray-100",
onclick: handle_logout,
Expand Down
61 changes: 58 additions & 3 deletions src/components/dashboard/profile.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,69 @@
pub(crate) mod edit;
pub(crate) mod view;

use crate::components::dashboard::profile::edit::ProfileForm;
use crate::components::dashboard::profile::view::ProfileDetails;
use crate::server::auth::controller::about_me;
use crate::server::auth::model::User;
use crate::theme::Theme;

use dioxus::prelude::*;
use gloo_storage::SessionStorage;
use gloo_storage::Storage;

#[component]
pub fn EditProfilePanel() -> Element {
pub fn ProfilePagePanel() -> Element {
let theme = use_context::<Signal<Theme>>();
let dark_mode = theme() == Theme::Dark;
let mut user_token = use_signal(|| "".to_string());
let mut user_data = use_signal(|| None::<User>);
let mut edit_mode = use_signal(|| false);
let navigator = use_navigator();

use_effect(move || {
spawn(async move {
let token: String = SessionStorage::get("jwt").unwrap_or_default();
if token.is_empty() {
navigator.push("/login");
} else {
match about_me(token.clone()).await {
Ok(res) => {
user_data.set(Some(res.data.user));
user_token.set(token.clone());
}
Err(_) => {
navigator.push("/login");
}
}
}
});
});

rsx! {
div { class: format!("p-4 {}", if dark_mode { "bg-gray-800 text-white" } else { "bg-white text-gray-900" }),
h2 { class: "text-xl font-semibold mb-4", "Edit Profile" }
p { "TODO" }
h2 { class: "text-xl font-semibold mb-4", "Profile" }
div { class: "container mx-auto p-4",
div { class: "flex items-center justify-between",
button {
class: format!("py-2 px-4 rounded {}", if dark_mode { "bg-blue-600" } else { "bg-blue-500 text-white" }),
onclick: move |_| edit_mode.set(!edit_mode()),
if edit_mode() { "Cancel" } else { "Edit" }
}
}

div { class: format!("mt-6 space-y-4 bg-white shadow-md p-4 rounded-md {}", if dark_mode { "bg-gray-800" } else { "bg-white" }),
match user_data.as_ref() {
Some(user) => rsx! {
if edit_mode() {
ProfileForm { user: user.clone(), dark_mode, user_token }
} else {
ProfileDetails { user: user.clone(), dark_mode, user_token }
}
},
None => rsx!(p { "Loading..." })
}
}
}
}
}
}
197 changes: 197 additions & 0 deletions src/components/dashboard/profile/edit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use crate::components::dashboard::fields::input::InputField;
use crate::components::dashboard::profile::view::ProfileDetailsProps;
use crate::components::toast::manager::{ToastManager, ToastType};
use crate::server::auth::controller::edit_profile;
use crate::server::auth::request::EditUserSchema;
use chrono::Duration;
use dioxus::prelude::*;

#[component]
pub fn ProfileForm(props: ProfileDetailsProps) -> Element {
let user = &props.user;
let dark_mode = props.dark_mode;
let user_token = props.user_token;

let name = use_signal(|| user.name.clone());
// default to lord Ferris
let photo =
use_signal(|| "https://rustacean.net/assets/rustacean-orig-noshadow.svg".to_string());
let email = use_signal(|| user.email.clone());
let old_password = use_signal(|| String::new());
let new_password = use_signal(|| String::new());
let confirm_password = use_signal(|| String::new());

let mut name_valid = use_signal(|| true);
let validate_name = |name: &str| !name.is_empty();
let mut email_valid = use_signal(|| true);
let validate_email = |email: &str| email.contains("@") && email.contains(".");

let photo_valid = use_signal(|| true);
let validate_photo = |photo: &str| !photo.is_empty();

let mut old_password_valid = use_signal(|| true);
let validate_old_password = |password: &str| !password.is_empty();
let mut new_password_valid = use_signal(|| true);
let validate_new_password = |password: &str| password.len() >= 8;
let mut confirm_password_valid = use_signal(|| true);
let validate_confirm_password = |confirm: &str, new: &str| confirm == new;

let navigator = use_navigator();
let mut toasts_manager = use_context::<Signal<ToastManager>>();

let handle_submit = move |evt: Event<FormData>| {
evt.stop_propagation();
let user_token = user_token.clone();

let mut all_valid = true;

if !validate_name(&name()) {
name_valid.set(false);
all_valid = false;
} else {
name_valid.set(true);
}

if !validate_email(&email()) {
email_valid.set(false);
all_valid = false;
} else {
email_valid.set(true);
}

if !validate_old_password(&old_password()) {
old_password_valid.set(false);
all_valid = false;
} else {
old_password_valid.set(true);
}

if !validate_new_password(&new_password()) {
new_password_valid.set(false);
all_valid = false;
} else {
new_password_valid.set(true);
}

if !validate_confirm_password(&confirm_password(), &new_password()) {
confirm_password_valid.set(false);
all_valid = false;
} else {
confirm_password_valid.set(true);
}

if all_valid {
spawn({
async move {
match edit_profile(EditUserSchema {
token: user_token,
name: Some(name()),
email: Some(email()),
photo: Some(photo()),
old_password: Some(old_password()),
new_password: Some(new_password()),
confirm_password: Some(confirm_password()),
})
.await
{
Ok(_) => {
toasts_manager.set(
toasts_manager()
.add_toast(
"Success".into(),
"Profile updated successfully.".into(),
ToastType::Success,
Some(Duration::seconds(5)),
)
.clone(),
);
navigator.push("/dashboard");
}
Err(e) => {
let msg = e.to_string();
let error_message = msg
.splitn(2, "error running server function:")
.nth(1)
.unwrap_or("An error occurred")
.trim();
toasts_manager.set(
toasts_manager()
.add_toast(
"Error".into(),
error_message.into(),
ToastType::Error,
Some(Duration::seconds(5)),
)
.clone(),
);
}
}
}
});
} else {
toasts_manager.set(
toasts_manager()
.add_toast(
"Error".into(),
"Please ensure all fields are valid.".into(),
ToastType::Error,
Some(Duration::seconds(5)),
)
.clone(),
);
}
};

rsx!(
form { class: "space-y-4",
onsubmit: handle_submit,
InputField {
label: "Name",
value: name,
is_valid: name_valid,
validate: validate_name,
required: true
},
InputField {
label: "Image",
value: photo,
is_valid: photo_valid,
validate: validate_photo,
required: true
},
InputField {
label: "Email",
value: email,
is_valid: email_valid,
validate: validate_email,
required: true
},
InputField {
label: "Old Password",
value: old_password,
is_valid: old_password_valid,
validate: validate_old_password,
required: true
},
InputField {
label: "New Password",
value: new_password,
is_valid: new_password_valid,
validate: validate_new_password,
required: true
},
InputField {
label: "Confirm Password",
value: confirm_password,
is_valid: confirm_password_valid,
validate: validate_new_password,
required: true
},
button {
class: format!("py-2 px-4 rounded-md {}", if dark_mode { "bg-blue-600" } else { "bg-blue-500 text-white" }),
r#type: "submit",
"Save"
}
}
)
}
41 changes: 41 additions & 0 deletions src/components/dashboard/profile/view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use crate::server::auth::model::User;
use dioxus::prelude::*;

#[derive(Props, Clone, PartialEq)]
pub struct ProfileDetailsProps {
pub user: User,
pub dark_mode: bool,
pub user_token: String,
}

#[component]
pub fn ProfileDetails(props: ProfileDetailsProps) -> Element {
rsx!(
div { class: "grid grid-cols-2 gap-4 md:grid-cols-3",
div { class: "flex items-center space-x-2",
span { class: "font-bold", "User ID:" }
span { "{props.user.id}" }
}
div { class: "flex items-center space-x-2",
span { class: "font-bold", "Name:" }
span { "{props.user.name}" }
}
div { class: "flex items-center space-x-2",
span { class: "font-bold", "Email:" }
span { "{props.user.email}" }
}
div { class: "flex items-center space-x-2",
span { class: "font-bold", "Role:" }
span { "{props.user.role}" }
}
div { class: "flex items-center space-x-2",
span { class: "font-bold", "Verified:" }
span { "{props.user.verified}" }
}
div { class: "flex items-center space-x-2",
span { class: "font-bold", "Registered At:" }
span { "{props.user.created_at.format(\"%B %d, %Y\")}" }
}
}
)
}
Loading

0 comments on commit bce6b53

Please sign in to comment.