B2B-31: refactor profile menu, header

This commit is contained in:
devmc-ee
2025-06-29 19:25:50 +03:00
parent a8dbc98b62
commit fbbc2f8760
14 changed files with 132 additions and 69 deletions

View File

@@ -1,38 +1,41 @@
import { If } from '@kit/ui/if';
import { AppLogo } from '~/components/app-logo'; import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
import featuresFlagConfig from '~/config/feature-flags.config'; import { Trans } from '@kit/ui/trans';
// home imports // home imports
import { HomeAccountSelector } from '../_components/home-account-selector';
import { UserNotifications } from '../_components/user-notifications'; import { UserNotifications } from '../_components/user-notifications';
import { type UserWorkspace } from '../_lib/server/load-user-workspace'; import { type UserWorkspace } from '../_lib/server/load-user-workspace';
import { Button } from '@kit/ui/button';
import { ShoppingCart } from 'lucide-react';
export function HomeMenuNavigation(props: { workspace: UserWorkspace }) { export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
const { workspace, user, accounts } = props.workspace; const { workspace, user, accounts } = props.workspace;
console.log('HomeMenuNavigation', accounts)
return ( return (
<div className={'flex w-full flex-1 justify-between'}> <div className={'flex w-full flex-1 justify-between'}>
<div className={'flex items-center space-x-8'}> <div className={'flex items-center space-x-8'}>
<AppLogo /> <AppLogo />
</div> </div>
{/* searbar */}
<div className={'flex justify-end space-x-2.5 gap-2 items-center'}>
{/* TODO: implement account budget */}
<Button className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' variant={'ghost'}>
<span className='flex items-center text-nowrap'> 231,89</span>
</Button>
{/* TODO: implement cart */}
<Button className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer gap-2' variant={'ghost'}>
<ShoppingCart size={16} />
<div className={'flex justify-end space-x-2.5'}> <Trans i18nKey="billing:cart.label" values={{ items: 0 }}/>
</Button>
<UserNotifications userId={user.id} /> <UserNotifications userId={user.id} />
<ProfileAccountDropdownContainer
<If condition={featuresFlagConfig.enableTeamAccounts && accounts.length}> user={user}
<HomeAccountSelector userId={user.id} accounts={accounts} /> account={workspace}
</If> accounts={accounts}
showProfileName={true}
<div> />
<ProfileAccountDropdownContainer
user={user}
account={workspace}
showProfileName={false}
/>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -6,16 +6,22 @@ import {
import { AppLogo } from '~/components/app-logo'; import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config'; import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config';
import { TeamAccountAccountsSelector } from '~/home/[account]/_components/team-account-accounts-selector';
// local imports // local imports
import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader'; import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader';
import { TeamAccountNotifications } from './team-account-notifications'; import { TeamAccountNotifications } from './team-account-notifications';
import { useMemo } from 'react';
export function TeamAccountNavigationMenu(props: { export function TeamAccountNavigationMenu(props: {
workspace: TeamAccountWorkspace; workspace: TeamAccountWorkspace;
}) { }) {
const { account, user, accounts } = props.workspace; const { account, user, accounts: rawAccounts } = props.workspace;
const accounts = useMemo(() => rawAccounts.map((account) => ({
label: account.name,
value: account.slug,
image: account.picture_url,
})),[rawAccounts])
const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce< const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce<
Array<{ Array<{
@@ -40,7 +46,6 @@ export function TeamAccountNavigationMenu(props: {
<div className={'flex w-full flex-1 justify-between'}> <div className={'flex w-full flex-1 justify-between'}>
<div className={'flex items-center space-x-8'}> <div className={'flex items-center space-x-8'}>
<AppLogo /> <AppLogo />
<BorderedNavigationMenu> <BorderedNavigationMenu>
{routes.map((route) => ( {routes.map((route) => (
<BorderedNavigationMenuItem {...route} key={route.path} /> <BorderedNavigationMenuItem {...route} key={route.path} />
@@ -48,26 +53,14 @@ export function TeamAccountNavigationMenu(props: {
</BorderedNavigationMenu> </BorderedNavigationMenu>
</div> </div>
<div className={'flex items-center justify-end space-x-2.5'}> <div className={'flex items-center justify-end space-x-2.5 gap-2'}>
<TeamAccountNotifications accountId={account.id} userId={user.id} /> <TeamAccountNotifications accountId={account.id} userId={user.id} />
<ProfileAccountDropdownContainer
<TeamAccountAccountsSelector user={user}
userId={user.id} account={account}
selectedAccount={account.slug} showProfileName={true}
accounts={accounts.map((account) => ({ accounts={accounts}
label: account.name,
value: account.slug,
image: account.picture_url,
}))}
/> />
<div>
<ProfileAccountDropdownContainer
user={user}
account={account}
showProfileName={false}
/>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -28,12 +28,8 @@ export const loadTeamWorkspace = cache(workspaceLoader);
async function workspaceLoader(accountSlug: string) { async function workspaceLoader(accountSlug: string) {
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createTeamAccountsApi(client); const api = createTeamAccountsApi(client);
const user = await requireUserInServerComponent();
const [workspace, user] = await Promise.all([ const workspace = await api.getAccountWorkspace(accountSlug, user.id);
api.getAccountWorkspace(accountSlug),
requireUserInServerComponent(),
]);
// we cannot find any record for the selected account // we cannot find any record for the selected account
// so we redirect the user to the home page // so we redirect the user to the home page
if (!workspace.data?.account) { if (!workspace.data?.account) {
@@ -42,6 +38,7 @@ async function workspaceLoader(accountSlug: string) {
return { return {
...workspace.data, ...workspace.data,
accounts: workspace.data.accounts.map(({ user_accounts }) => ({...user_accounts})),
user, user,
}; };
} }

View File

@@ -30,7 +30,7 @@ function TeamWorkspaceLayout({ children, params }: TeamWorkspaceLayoutProps) {
return <SidebarLayout account={account}>{children}</SidebarLayout>; return <SidebarLayout account={account}>{children}</SidebarLayout>;
} }
return <HeaderLayout account={account}>{children}</HeaderLayout>; return <HeaderLayout account={account}>{children}</HeaderLayout>;
} }
function SidebarLayout({ function SidebarLayout({

View File

@@ -26,6 +26,11 @@ export function ProfileAccountDropdownContainer(props: {
name: string | null; name: string | null;
picture_url: string | null; picture_url: string | null;
}; };
accounts: {
label: string | null;
value: string | null;
image?: string | null;
}[]
}) { }) {
const signOut = useSignOut(); const signOut = useSignOut();
const user = useUser(props.user); const user = useUser(props.user);
@@ -34,7 +39,7 @@ export function ProfileAccountDropdownContainer(props: {
if (!userData) { if (!userData) {
return null; return null;
} }
console.log(props.accounts)
return ( return (
<PersonalAccountDropdown <PersonalAccountDropdown
className={'w-full'} className={'w-full'}
@@ -42,6 +47,7 @@ export function ProfileAccountDropdownContainer(props: {
features={features} features={features}
user={userData} user={userData}
account={props.account} account={props.account}
accounts={props.accounts}
signOutRequested={() => signOut.mutateAsync()} signOutRequested={() => signOut.mutateAsync()}
showProfileName={props.showProfileName} showProfileName={props.showProfileName}
/> />

View File

@@ -112,10 +112,10 @@ export function AccountSelector({
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className={cn( className={cn(
'dark:shadow-primary/10 group w-full min-w-0 px-2 lg:w-auto lg:max-w-fit', 'dark:shadow-primary/10 group w-full min-w-0 px-4 py-2 h-10 border-1 lg:w-auto lg:max-w-fit',
{ {
'justify-start': !collapsed, 'justify-start': !collapsed,
'm-auto justify-center px-2 lg:w-full': collapsed, 'm-auto justify-center px-4 lg:w-full': collapsed,
}, },
className, className,
)} )}
@@ -124,7 +124,7 @@ export function AccountSelector({
condition={selected} condition={selected}
fallback={ fallback={
<span <span
className={cn('flex max-w-full items-center', { className={cn('flex max-w-full items-center w-4 h-4', {
'justify-center gap-x-0': collapsed, 'justify-center gap-x-0': collapsed,
'gap-x-4': !collapsed, 'gap-x-4': !collapsed,
})} })}
@@ -148,7 +148,7 @@ export function AccountSelector({
'gap-x-4': !collapsed, 'gap-x-4': !collapsed,
})} })}
> >
<Avatar className={'rounded-xs h-6 w-6'}> <Avatar className={'rounded-md h-6 w-6'}>
<AvatarImage src={account.image ?? undefined} /> <AvatarImage src={account.image ?? undefined} />
<AvatarFallback <AvatarFallback
@@ -297,7 +297,7 @@ export function AccountSelector({
function UserAvatar(props: { pictureUrl?: string }) { function UserAvatar(props: { pictureUrl?: string }) {
return ( return (
<Avatar className={'rounded-xs h-6 w-6'}> <Avatar className={'rounded-md h-6 w-6'}>
<AvatarImage src={props.pictureUrl} /> <AvatarImage src={props.pictureUrl} />
</Avatar> </Avatar>
); );

View File

@@ -28,6 +28,9 @@ import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import { usePersonalAccountData } from '../hooks/use-personal-account-data'; import { usePersonalAccountData } from '../hooks/use-personal-account-data';
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
const PERSONAL_ACCOUNT_SLUG = 'personal';
export function PersonalAccountDropdown({ export function PersonalAccountDropdown({
className, className,
@@ -37,6 +40,7 @@ export function PersonalAccountDropdown({
paths, paths,
features, features,
account, account,
accounts
}: { }: {
user: User; user: User;
@@ -45,7 +49,11 @@ export function PersonalAccountDropdown({
name: string | null; name: string | null;
picture_url: string | null; picture_url: string | null;
}; };
accounts: {
label: string | null;
value: string | null;
image?: string | null;
}[];
signOutRequested: () => unknown; signOutRequested: () => unknown;
paths: { paths: {
@@ -95,7 +103,7 @@ export function PersonalAccountDropdown({
className ?? '', className ?? '',
{ {
['active:bg-secondary/50 items-center gap-4 rounded-md' + ['active:bg-secondary/50 items-center gap-4 rounded-md' +
' hover:bg-secondary p-2 transition-colors']: showProfileName, ' hover:bg-secondary p-0 m-0 transition-colors border-1 rounded-md px-4 py-1 h-10']: showProfileName,
}, },
)} )}
> >
@@ -119,12 +127,6 @@ export function PersonalAccountDropdown({
{displayName} {displayName}
</span> </span>
<span
data-test={'account-dropdown-email'}
className={'text-muted-foreground truncate text-xs'}
>
{signedInAsLabel}
</span>
</div> </div>
<ChevronsUpDown <ChevronsUpDown
@@ -167,6 +169,46 @@ export function PersonalAccountDropdown({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<If condition={(accounts ?? []).length > 0}>
<span className='px-2 text-muted-foreground text-xs'>
<Trans
i18nKey={'teams:yourTeams'}
values={{ teamsCount: (accounts ?? []).length }}
/>
</span>
{(accounts ?? []).map((account) => (
<DropdownMenuItem key={account.value} asChild>
<Link
className={'s-full flex cursor-pointer items-center space-x-2'}
href={`/home/${account.value}`}
>
<div className={'flex items-center'}>
<Avatar className={'rounded-xs h-5 w-5'}>
<AvatarImage src={account.image ?? undefined} />
<AvatarFallback
className={cn('rounded-md', {
['bg-background']: PERSONAL_ACCOUNT_SLUG === account.value,
['group-hover:bg-background']:
PERSONAL_ACCOUNT_SLUG !== account.value,
})}
>
{account.label ? account.label[0] : ''}
</AvatarFallback>
</Avatar>
<span className={'pl-3'}>
{account.label}
</span>
</div>
</Link>
</DropdownMenuItem>
))}
</If>
<DropdownMenuSeparator />
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link <Link
className={'s-full flex cursor-pointer items-center space-x-2'} className={'s-full flex cursor-pointer items-center space-x-2'}

View File

@@ -121,8 +121,8 @@ export function NotificationsPopover(params: {
return ( return (
<Popover modal open={open} onOpenChange={setOpen}> <Popover modal open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button className={'relative h-9 w-9'} variant={'ghost'}> <Button className={'relative px-4 py-2 h-10 border-1 mr-0'} variant={'ghost'}>
<Bell className={'min-h-4 min-w-4'} /> <Bell className={'h-4 w-4'} />
<span <span
className={cn( className={cn(

View File

@@ -90,18 +90,31 @@ export class TeamAccountsApi {
* @description Get the account workspace data. * @description Get the account workspace data.
* @param slug * @param slug
*/ */
async getAccountWorkspace(slug: string) { async getAccountWorkspace(slug: string, userId: string) {
const accountPromise = this.client.rpc('team_account_workspace', { const accountPromise = this.client.rpc('team_account_workspace', {
account_slug: slug, account_slug: slug,
}); });
const accountsPromise = this.client.from('user_accounts').select('*'); const accountsPromise = this.client
.from('accounts_memberships')
.select(`
account_id,
user_accounts (
id,
role,
name,
slug,
picture_url
)
`)
.eq('user_id', userId)
.eq('account_role', 'owner');
const [accountResult, accountsResult] = await Promise.all([ const [accountResult, accountsResult] = await Promise.all([
accountPromise, accountPromise,
accountsPromise, accountsPromise,
]); ]);
if (accountResult.error) { if (accountResult.error) {
return { return {
error: accountResult.error, error: accountResult.error,

View File

@@ -87,7 +87,7 @@ function PageWithHeader(props: PageProps) {
> >
<div <div
className={cn( className={cn(
'bg-muted/40 dark:border-border dark:shadow-primary/10 flex h-14 items-center justify-between px-4 lg:justify-start lg:shadow-xs', 'bg-bg-background border-1 light:border-border dark:border-border dark:shadow-primary/10 flex h-15 items-center justify-between px-4 py-1 lg:justify-start lg:shadow-xs',
{ {
'sticky top-0 z-10 backdrop-blur-md': props.sticky ?? true, 'sticky top-0 z-10 backdrop-blur-md': props.sticky ?? true,
}, },

View File

@@ -18,7 +18,7 @@ type ProfileAvatarProps = (SessionProps | TextProps) & {
export function ProfileAvatar(props: ProfileAvatarProps) { export function ProfileAvatar(props: ProfileAvatarProps) {
const avatarClassName = cn( const avatarClassName = cn(
props.className, props.className,
'mx-auto h-9 w-9 group-focus:ring-2', 'mx-auto h-6 w-6 group-focus:ring-2',
); );
if ('text' in props) { if ('text' in props) {

View File

@@ -116,5 +116,8 @@
"heading": "Your payment failed", "heading": "Your payment failed",
"description": "Your payment failed. Please update your payment method." "description": "Your payment failed. Please update your payment method."
} }
},
"cart": {
"label": "Cart ({{ items }})"
} }
} }

View File

@@ -116,5 +116,8 @@
"heading": "Your payment failed", "heading": "Your payment failed",
"description": "Your payment failed. Please update your payment method." "description": "Your payment failed. Please update your payment method."
} }
},
"cart": {
"label": "Cart ({{ items }})"
} }
} }

View File

@@ -116,5 +116,8 @@
"heading": "Your payment failed", "heading": "Your payment failed",
"description": "Your payment failed. Please update your payment method." "description": "Your payment failed. Please update your payment method."
} }
},
"cart": {
"label": "Cart ({{ items }})"
} }
} }