diff --git a/crates/auth-domain/src/lib.rs b/crates/auth-domain/src/lib.rs index 04de4e8c..5b34d370 100644 --- a/crates/auth-domain/src/lib.rs +++ b/crates/auth-domain/src/lib.rs @@ -185,7 +185,8 @@ impl AuthDomainService { id: user_id, personal_org: org.id, orgs: Vec::new(), - name, + name: name.clone(), + name_abbr: User::abbreviate_name(name), email, auth, active_org_index: 0, diff --git a/crates/models/src/user.rs b/crates/models/src/user.rs index 1aad5707..151b3cc1 100644 --- a/crates/models/src/user.rs +++ b/crates/models/src/user.rs @@ -21,6 +21,8 @@ pub struct User { pub orgs: Vec>, /// The user's name. pub name: HumanName, + /// An abbreviated form of the user's name. + pub name_abbr: HumanName, /// The user's email address. pub email: EmailAddress, /// The user's authentication secrets. @@ -51,6 +53,19 @@ impl User { pub fn belongs_to_org(&self, org: RecordId) -> bool { self.personal_org == org || self.orgs.contains(&org) } + + /// Helper fn that abbreviates a name. + pub fn abbreviate_name(name: HumanName) -> HumanName { + HumanName::try_new( + name + .to_string() + .split_whitespace() + .filter_map(|word| word.chars().next()) + .map(|c| c.to_uppercase().to_string()) + .collect::(), + ) + .expect("failed to create name") + } } /// The unique index selector for [`User`] @@ -139,6 +154,8 @@ pub struct AuthUser { pub orgs: Vec>, /// The user's name. pub name: HumanName, + /// An abbreviated form of the user's name. + pub name_abbr: HumanName, /// The hash of the user's authentication secrets. pub auth_hash_bytes: Box<[u8]>, /// The index of the [`Org`] that the user is currently operating as. @@ -154,6 +171,7 @@ impl From for AuthUser { personal_org: user.personal_org, orgs: user.orgs, name: user.name, + name_abbr: user.name_abbr, auth_hash_bytes, active_org_index: user.active_org_index, } diff --git a/crates/prime-domain/src/migrate.rs b/crates/prime-domain/src/migrate.rs index b6adaf63..9ed2fcfa 100644 --- a/crates/prime-domain/src/migrate.rs +++ b/crates/prime-domain/src/migrate.rs @@ -48,6 +48,9 @@ impl PrimeDomainService { .unwrap(), name: HumanName::try_new("Jean-Luc Picard") .expect("failed to create name"), + name_abbr: User::abbreviate_name( + HumanName::try_new("Jean-Luc Picard").expect("failed to create name"), + ), auth: models::UserAuthCredentials::Password { // hash for password `password` password_hash: models::PasswordHash( diff --git a/crates/site-app/src/components/navbar.rs b/crates/site-app/src/components/navbar.rs index 46cfcd62..70ae064f 100644 --- a/crates/site-app/src/components/navbar.rs +++ b/crates/site-app/src/components/navbar.rs @@ -1,9 +1,10 @@ +mod account_menu; mod org_selector; use leptos::{either::Either, prelude::*}; use models::AuthUser; -use self::org_selector::OrgSelectorPopover; +use self::{account_menu::AccountMenu, org_selector::OrgSelector}; use crate::{hooks::OrgHook, navigation::next_url_encoded_hook}; #[component] @@ -39,7 +40,7 @@ fn NavbarUserArea() -> impl IntoView { let auth_user = use_context::(); match auth_user { - Some(user) => Either::Left(view! { }), + Some(_) => Either::Left(view! { }), None => Either::Right(view! { }), } } @@ -61,13 +62,14 @@ fn LoggedOutUserAuthActions() -> impl IntoView { } #[component] -fn LoggedInUserAuthActions(user: AuthUser) -> impl IntoView { +fn LoggedInUserAuthActions() -> impl IntoView { let active_org_hook = OrgHook::new_active(); let active_org_dashboard_url = active_org_hook.dashboard_url(); view! { - "Dashboard" - "Log Out" + + + // "Log Out" } } diff --git a/crates/site-app/src/components/navbar/account_menu.rs b/crates/site-app/src/components/navbar/account_menu.rs new file mode 100644 index 00000000..276a894f --- /dev/null +++ b/crates/site-app/src/components/navbar/account_menu.rs @@ -0,0 +1,47 @@ +use leptos::prelude::*; +use models::AuthUser; + +use crate::components::{Popover, PopoverContents, PopoverTrigger}; + +#[component] +fn AccountMenuTrigger() -> impl IntoView { + let user = expect_context::(); + + const CLASS: &str = "size-10 flex flex-col justify-center items-center \ + btn-secondary transition-colors rounded-full \ + border-[1.5px] border-base-6 cursor-pointer"; + + view! { +
+ { user.name_abbr.to_string() } +
+ } +} + +#[island] +pub(crate) fn AccountMenu() -> impl IntoView { + view! { + + + + + + + + + } +} + +#[component] +fn AccountMenuMenu() -> impl IntoView { + const POPOVER_CLASS: &str = + "absolute right-0 top-[calc(100%+(var(--spacing)*4))] min-w-56 \ + elevation-lv1 p-2 flex flex-col gap-1 leading-none"; + + view! { + + } +} diff --git a/crates/site-app/src/components/navbar/org_selector.rs b/crates/site-app/src/components/navbar/org_selector.rs index db6a1bb2..9abd9446 100644 --- a/crates/site-app/src/components/navbar/org_selector.rs +++ b/crates/site-app/src/components/navbar/org_selector.rs @@ -10,52 +10,60 @@ use crate::{ navigation::navigate_to, }; -#[island] -pub(super) fn OrgSelectorPopover(user: AuthUser) -> impl IntoView { - const CONTAINER_CLASS: &str = "transition hover:bg-base-3 active:bg-base-4 \ - cursor-pointer px-2 py-1 rounded flex \ - flex-col gap leading-none items-end gap-0"; - +#[component] +fn OrgSelectorTrigger() -> impl IntoView { let active_org_hook = OrgHook::new_active(); let active_org_descriptor = active_org_hook.descriptor(); - let user_name = Signal::stored(user.name.to_string()); + const CLASS: &str = "transition hover:bg-base-3 active:bg-base-4 \ + cursor-pointer px-2 py-1 rounded flex flex-col gap-0.5 \ + text-sm leading-none items-end gap-0"; + + view! { +
+

+ + { move || Suspend::new(active_org_descriptor) } + +

+
+

"Switch Orgs"

+ +
+
+ } +} + +#[island] +pub(super) fn OrgSelector() -> impl IntoView { view! { -
- { user_name } -
- - - { move || Suspend::new(active_org_descriptor) } - - - -
-
+
- +
} } #[component] -fn OrgSelector(user: AuthUser) -> impl IntoView { +fn OrgSelectorMenu() -> impl IntoView { + let auth_user = expect_context::(); + const POPOVER_CLASS: &str = - "absolute left-0 top-[calc(100%+(var(--spacing)*2))] min-w-56 \ + "absolute right-0 top-[calc(100%+(var(--spacing)*4))] min-w-56 \ elevation-lv1 p-2 flex flex-col gap-1 leading-none"; let org_hooks = Signal::stored( - user + auth_user .iter_orgs() .map(|o| (o, OrgHook::new(move || o))) .collect::>(), ); - let active_org = user.active_org(); + let active_org = auth_user.active_org(); let action = ServerAction::::new(); let selected = RwSignal::new(None::>); @@ -97,9 +105,9 @@ fn OrgSelector(user: AuthUser) -> impl IntoView { view! {