"update user popover with dynamic user metadata loading and improved UI consistency,"

This commit is contained in:
louiscklaw
2025-05-11 07:55:16 +08:00
parent 9a8fd1c073
commit b5e9c8ba34
5 changed files with 164 additions and 39 deletions

View File

@@ -37,7 +37,11 @@ export function CustomSignOut(): React.JSX.Element {
}, [checkSession, router]);
return (
<MenuItem component="div" onClick={handleSignOut} sx={{ justifyContent: 'center' }}>
<MenuItem
component="div"
onClick={handleSignOut}
sx={{ justifyContent: 'center' }}
>
Sign out
</MenuItem>
);

View File

@@ -23,8 +23,10 @@ import { CognitoSignOut } from './cognito-sign-out';
import { CustomSignOut } from './custom-sign-out';
import { FirebaseSignOut } from './firebase-sign-out';
import { SupabaseSignOut } from './supabase-sign-out';
import { authClient } from '@/lib/auth/custom/client';
import { logger } from '@/lib/default-logger';
const user = {
const defaultUser = {
id: 'USR-000',
name: 'Sofia Rivers',
avatar: '/assets/avatar.png',
@@ -38,6 +40,23 @@ export interface UserPopoverProps {
}
export function UserPopover({ anchorEl, onClose, open }: UserPopoverProps): React.JSX.Element {
const [userMeta, setUserMeta] = React.useState<User>(defaultUser);
async function loadUserMeta(): Promise<void> {
try {
const tempUserMeta = await authClient.getUser();
if (tempUserMeta.error) throw new Error(tempUserMeta.error);
setUserMeta(tempUserMeta.data as unknown as User);
} catch (error) {
logger.error(error);
}
}
React.useEffect(() => {
void loadUserMeta();
}, []);
if (!userMeta) return <>loading</>;
return (
<Popover
anchorEl={anchorEl}
@@ -48,26 +67,41 @@ export function UserPopover({ anchorEl, onClose, open }: UserPopoverProps): Reac
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
>
<Box sx={{ p: 2 }}>
<Typography>{user.name}</Typography>
<Typography color="text.secondary" variant="body2">
{user.email}
<Typography>{userMeta.name}</Typography>
<Typography
color="text.secondary"
variant="body2"
>
{userMeta.email}
</Typography>
</Box>
<Divider />
<List sx={{ p: 1 }}>
<MenuItem component={RouterLink} href={paths.dashboard.settings.account} onClick={onClose}>
<MenuItem
component={RouterLink}
href={paths.dashboard.settings.account}
onClick={onClose}
>
<ListItemIcon>
<UserIcon />
</ListItemIcon>
Account
</MenuItem>
<MenuItem component={RouterLink} href={paths.dashboard.settings.security} onClick={onClose}>
<MenuItem
component={RouterLink}
href={paths.dashboard.settings.security}
onClick={onClose}
>
<ListItemIcon>
<LockKeyIcon />
</ListItemIcon>
Security
</MenuItem>
<MenuItem component={RouterLink} href={paths.dashboard.settings.billing} onClick={onClose}>
<MenuItem
component={RouterLink}
href={paths.dashboard.settings.billing}
onClick={onClose}
>
<ListItemIcon>
<CreditCardIcon />
</ListItemIcon>

View File

@@ -60,7 +60,11 @@ export function MainNav({ items }: MainNavProps): React.JSX.Element {
py: 1,
}}
>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto' }}
>
<IconButton
onClick={(): void => {
setOpenNav(true);
@@ -105,11 +109,17 @@ function SearchButton(): React.JSX.Element {
return (
<React.Fragment>
<Tooltip title="Search">
<IconButton onClick={dialog.handleOpen} sx={{ display: { xs: 'none', lg: 'inline-flex' } }}>
<IconButton
onClick={dialog.handleOpen}
sx={{ display: { xs: 'none', lg: 'inline-flex' } }}
>
<MagnifyingGlassIcon />
</IconButton>
</Tooltip>
<SearchDialog onClose={dialog.handleClose} open={dialog.open} />
<SearchDialog
onClose={dialog.handleClose}
open={dialog.open}
/>
</React.Fragment>
);
}
@@ -120,11 +130,18 @@ function ContactsButton(): React.JSX.Element {
return (
<React.Fragment>
<Tooltip title="Contacts">
<IconButton onClick={popover.handleOpen} ref={popover.anchorRef}>
<IconButton
onClick={popover.handleOpen}
ref={popover.anchorRef}
>
<UsersIcon />
</IconButton>
</Tooltip>
<ContactsPopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
<ContactsPopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}
@@ -140,12 +157,19 @@ function NotificationsButton(): React.JSX.Element {
sx={{ '& .MuiBadge-dot': { borderRadius: '50%', height: '10px', right: '6px', top: '6px', width: '10px' } }}
variant="dot"
>
<IconButton onClick={popover.handleOpen} ref={popover.anchorRef}>
<IconButton
onClick={popover.handleOpen}
ref={popover.anchorRef}
>
<BellIcon />
</IconButton>
</Badge>
</Tooltip>
<NotificationsPopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
<NotificationsPopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}
@@ -165,11 +189,20 @@ function LanguageSwitch(): React.JSX.Element {
sx={{ display: { xs: 'none', lg: 'inline-flex' } }}
>
<Box sx={{ height: '24px', width: '24px' }}>
<Box alt={language} component="img" src={flag} sx={{ height: 'auto', width: '100%' }} />
<Box
alt={language}
component="img"
src={flag}
sx={{ height: 'auto', width: '100%' }}
/>
</Box>
</IconButton>
</Tooltip>
<LanguagePopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
<LanguagePopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}
@@ -210,7 +243,11 @@ function UserButton(): React.JSX.Element {
<Avatar src={user.avatar} />
</Badge>
</Box>
<UserPopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
<UserPopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}

View File

@@ -71,29 +71,55 @@ export function SideNav(): React.JSX.Element {
width: { xs: '100%', md: '240px' },
}}
>
<Stack component="ul" spacing={3} sx={{ listStyle: 'none', m: 0, p: 0 }}>
<Stack
component="ul"
spacing={3}
sx={{ listStyle: 'none', m: 0, p: 0 }}
>
{navItems.map((group) => (
<Stack component="li" key={group.key} spacing={2}>
<Stack
component="li"
key={group.key}
spacing={2}
>
{group.title ? (
<div>
<Typography color="text.secondary" variant="caption">
<Typography
color="text.secondary"
variant="caption"
>
{group.title}
</Typography>
</div>
) : null}
<Stack component="ul" spacing={1} sx={{ listStyle: 'none', m: 0, p: 0 }}>
<Stack
component="ul"
spacing={1}
sx={{ listStyle: 'none', m: 0, p: 0 }}
>
{group.items.map((item) => (
<NavItem {...item} key={item.key} pathname={pathname} />
<NavItem
{...item}
key={item.key}
pathname={pathname}
/>
))}
</Stack>
</Stack>
))}
</Stack>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center' }}
>
<Avatar src="/assets/avatar.png">AV</Avatar>
<div>
<Typography variant="subtitle1">Sofia Rivers</Typography>
<Typography color="text.secondary" variant="caption">
<Typography
color="text.secondary"
variant="caption"
>
sofia@devias.io
</Typography>
</div>
@@ -112,7 +138,10 @@ function NavItem({ disabled, external, href, icon, pathname, title }: NavItemPro
const Icon = icon ? icons[icon] : null;
return (
<Box component="li" sx={{ userSelect: 'none' }}>
<Box
component="li"
sx={{ userSelect: 'none' }}
>
<Box
{...(href
? {

View File

@@ -1,5 +1,8 @@
'use client';
import { getUserMetaById } from '@/db/UserMetas/GetById';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import type { User } from '@/types/user';
function generateToken(): string {
@@ -8,7 +11,7 @@ function generateToken(): string {
return Array.from(arr, (v) => v.toString(16).padStart(2, '0')).join('');
}
const user = {
const user_xxx = {
id: 'USR-000',
avatar: '/assets/avatar.png',
firstName: 'Sofia',
@@ -54,17 +57,23 @@ class AuthClient {
async signInWithPassword(params: SignInWithPasswordParams): Promise<{ error?: string }> {
const { email, password } = params;
// Make API request
try {
// Make API request
await pb.collection('users').authWithPassword(email, password);
// We do not handle the API, so we'll check if the credentials match with the hardcoded ones.
if (email !== 'sofia@devias.io' || password !== 'Secret1') {
// // We do not handle the API, so we'll check if the credentials match with the hardcoded ones.
// if (email !== 'sofia@devias.io' || password !== 'Secret1') {
// return { error: 'Invalid credentials' };
// }
// const token = generateToken();
localStorage.setItem('custom-auth-token', pb.authStore.token);
return {};
} catch (error) {
logger.error(error);
return { error: 'Invalid credentials' };
}
const token = generateToken();
localStorage.setItem('custom-auth-token', token);
return {};
}
async resetPassword(_: ResetPasswordParams): Promise<{ error?: string }> {
@@ -79,16 +88,28 @@ class AuthClient {
// Make API request
// We do not handle the API, so just check if we have a token in localStorage.
const token = localStorage.getItem('custom-auth-token');
// const token = localStorage.getItem('custom-auth-token');
// if (!token) {
// return { data: null };
// }
try {
logger.debug(JSON.stringify(`getUser: ${pb.authStore.record?.id}`));
//
if (pb.authStore.record?.id !== undefined) {
const userMeta = await getUserMetaById(pb.authStore.record?.id);
logger.debug({ userMeta });
return { data: userMeta as unknown as User };
}
if (!token) {
return { data: null };
} catch (error) {
return { error: 'sorry cannot get user meta' };
}
return { data: user };
}
async signOut(): Promise<{ error?: string }> {
pb.authStore.clear();
localStorage.removeItem('custom-auth-token');
return {};