Skip to content
Merged
3 changes: 2 additions & 1 deletion crates/auth-domain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions crates/models/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub struct User {
pub orgs: Vec<RecordId<Org>>,
/// 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.
Expand Down Expand Up @@ -51,6 +53,19 @@ impl User {
pub fn belongs_to_org(&self, org: RecordId<Org>) -> 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::<String>(),
)
.expect("failed to create name")
}
}

/// The unique index selector for [`User`]
Expand Down Expand Up @@ -139,6 +154,8 @@ pub struct AuthUser {
pub orgs: Vec<RecordId<Org>>,
/// 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.
Expand All @@ -154,6 +171,7 @@ impl From<User> 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,
}
Expand Down
3 changes: 3 additions & 0 deletions crates/prime-domain/src/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 7 additions & 5 deletions crates/site-app/src/components/navbar.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -39,7 +40,7 @@ fn NavbarUserArea() -> impl IntoView {
let auth_user = use_context::<AuthUser>();

match auth_user {
Some(user) => Either::Left(view! { <LoggedInUserAuthActions user=user /> }),
Some(_) => Either::Left(view! { <LoggedInUserAuthActions /> }),
None => Either::Right(view! { <LoggedOutUserAuthActions /> }),
}
}
Expand All @@ -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! {
<OrgSelectorPopover user=user />
<a href=active_org_dashboard_url class="btn-link btn-link-primary">"Dashboard"</a>
<a href="/auth/logout" class="btn-link btn-link-secondary">"Log Out"</a>
<OrgSelector />
<AccountMenu />
// <a href="/auth/logout" class="btn-link btn-link-secondary">"Log Out"</a>
}
}
47 changes: 47 additions & 0 deletions crates/site-app/src/components/navbar/account_menu.rs
Original file line number Diff line number Diff line change
@@ -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::<AuthUser>();

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! {
<div class=CLASS>
{ user.name_abbr.to_string() }
</div>
}
}

#[island]
pub(crate) fn AccountMenu() -> impl IntoView {
view! {
<Popover>
<PopoverTrigger slot>
<AccountMenuTrigger />
</PopoverTrigger>
<PopoverContents slot>
<AccountMenuMenu />
</PopoverContents>
</Popover>
}
}

#[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! {
<div class=POPOVER_CLASS>
<a class="btn-link btn-link-secondary">"Account Settings"</a>
<a href="/auth/logout" class="btn btn-critical-subtle">"Log Out"</a>
</div>
}
}
63 changes: 36 additions & 27 deletions crates/site-app/src/components/navbar/org_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
<div class=CLASS>
<p class="text-base/[1] text-base-12">
<Suspense fallback=|| "[loading]">
{ move || Suspend::new(active_org_descriptor) }
</Suspense>
</p>
<div class="flex flex-row items-end gap-0.5">
<p>"Switch Orgs"</p>
<ChevronDownHeroIcon {..} class="size-3 stroke-[3.0] stroke-base-11" />
</div>
</div>
}
}

#[island]
pub(super) fn OrgSelector() -> impl IntoView {
view! {
<Popover>
<PopoverTrigger slot>
<div class=CONTAINER_CLASS>
<span class="text-base-12 text-sm">{ user_name }</span>
<div class="flex flex-row items-center gap-0">
<span class="text-sm">
<Suspense fallback=|| "[loading]">
{ move || Suspend::new(active_org_descriptor) }
</Suspense>
</span>
<ChevronDownHeroIcon {..} class="size-3 stroke-[3.0] stroke-base-11" />
</div>
</div>
<OrgSelectorTrigger />
</PopoverTrigger>

<PopoverContents slot>
<OrgSelector user=user />
<OrgSelectorMenu />
</PopoverContents>
</Popover>
}
}

#[component]
fn OrgSelector(user: AuthUser) -> impl IntoView {
fn OrgSelectorMenu() -> impl IntoView {
let auth_user = expect_context::<AuthUser>();

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::<Vec<_>>(),
);
let active_org = user.active_org();
let active_org = auth_user.active_org();

let action = ServerAction::<SwitchActiveOrg>::new();
let selected = RwSignal::new(None::<RecordId<Org>>);
Expand Down Expand Up @@ -97,9 +105,9 @@ fn OrgSelector(user: AuthUser) -> impl IntoView {

view! {
<div
class="rounded p-2 flex flex-row gap-2 items-center"
class=("text-base-12 font-semibold", id == active_org)
class=("cursor-pointer hover:bg-base-3 active:bg-base-4", id != active_org)
class="rounded p-2 flex flex-row gap-2 items-center transition-colors text-base-12"
class=("font-bold", id == active_org)
class=("cursor-pointer btn-link-secondary", id != active_org)
on:click=handler
>
{ icon_element }
Expand Down Expand Up @@ -128,7 +136,8 @@ fn OrgSelector(user: AuthUser) -> impl IntoView {
#[component]
fn CreateOrgRow() -> impl IntoView {
const CLASS: &str = "rounded p-2 flex flex-row gap-2 items-center \
cursor-pointer hover:bg-base-3 active-bg-base-4";
cursor-pointer btn-link-secondary transition-colors \
text-base-12";

view! {
<a href="/org/create_org" class=CLASS>
Expand Down
2 changes: 1 addition & 1 deletion crates/site-app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fn LeptosFetchDevtools() -> impl IntoView {
#[component]
fn PageContainer(children: Children) -> impl IntoView {
view! {
<main class="elevation-suppressed text-base-11 font-normal text-base/[1.2]">
<main class="elevation-suppressed text-base-11 font-medium text-base/[1.2]">
<div class="page-container flex flex-col min-h-svh pb-8">
<self::components::Navbar />
{ children() }
Expand Down
4 changes: 3 additions & 1 deletion crates/site-app/style/src/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,17 @@
--form-grid-cols: calc(48 * var(--spacing)) minmax(0, 1fr)
}

/* reduce `rem` on small screen sizes */
:root {
font-size: 85%;
}

@media (min-width: 40rem) {
:root {
font-size: inherit;
}
}

/* custom grid template columns */
@utility grid-cols-form {
grid-template-columns: var(--form-grid-cols);
}
Expand All @@ -85,6 +86,7 @@
}
}

/* disregard leptos internal components */
@layer base {
leptos-island {
display: contents;
Expand Down