B2B-31: refactor profile menu, header
B2B-31: refactor profile menu, header
This commit is contained in:
@@ -1,38 +1,40 @@
|
|||||||
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;
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 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 ' + account.image}>
|
||||||
|
<AvatarImage {...(account.image && { src: account.image })} />
|
||||||
|
|
||||||
|
<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'}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 }})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 }})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user