"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]); }, [checkSession, router]);
return ( return (
<MenuItem component="div" onClick={handleSignOut} sx={{ justifyContent: 'center' }}> <MenuItem
component="div"
onClick={handleSignOut}
sx={{ justifyContent: 'center' }}
>
Sign out Sign out
</MenuItem> </MenuItem>
); );

View File

@@ -23,8 +23,10 @@ import { CognitoSignOut } from './cognito-sign-out';
import { CustomSignOut } from './custom-sign-out'; import { CustomSignOut } from './custom-sign-out';
import { FirebaseSignOut } from './firebase-sign-out'; import { FirebaseSignOut } from './firebase-sign-out';
import { SupabaseSignOut } from './supabase-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', id: 'USR-000',
name: 'Sofia Rivers', name: 'Sofia Rivers',
avatar: '/assets/avatar.png', avatar: '/assets/avatar.png',
@@ -38,6 +40,23 @@ export interface UserPopoverProps {
} }
export function UserPopover({ anchorEl, onClose, open }: UserPopoverProps): React.JSX.Element { 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 ( return (
<Popover <Popover
anchorEl={anchorEl} anchorEl={anchorEl}
@@ -48,26 +67,41 @@ export function UserPopover({ anchorEl, onClose, open }: UserPopoverProps): Reac
transformOrigin={{ horizontal: 'right', vertical: 'top' }} transformOrigin={{ horizontal: 'right', vertical: 'top' }}
> >
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Typography>{user.name}</Typography> <Typography>{userMeta.name}</Typography>
<Typography color="text.secondary" variant="body2"> <Typography
{user.email} color="text.secondary"
variant="body2"
>
{userMeta.email}
</Typography> </Typography>
</Box> </Box>
<Divider /> <Divider />
<List sx={{ p: 1 }}> <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> <ListItemIcon>
<UserIcon /> <UserIcon />
</ListItemIcon> </ListItemIcon>
Account Account
</MenuItem> </MenuItem>
<MenuItem component={RouterLink} href={paths.dashboard.settings.security} onClick={onClose}> <MenuItem
component={RouterLink}
href={paths.dashboard.settings.security}
onClick={onClose}
>
<ListItemIcon> <ListItemIcon>
<LockKeyIcon /> <LockKeyIcon />
</ListItemIcon> </ListItemIcon>
Security Security
</MenuItem> </MenuItem>
<MenuItem component={RouterLink} href={paths.dashboard.settings.billing} onClick={onClose}> <MenuItem
component={RouterLink}
href={paths.dashboard.settings.billing}
onClick={onClose}
>
<ListItemIcon> <ListItemIcon>
<CreditCardIcon /> <CreditCardIcon />
</ListItemIcon> </ListItemIcon>

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
'use client'; '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'; import type { User } from '@/types/user';
function generateToken(): string { function generateToken(): string {
@@ -8,7 +11,7 @@ function generateToken(): string {
return Array.from(arr, (v) => v.toString(16).padStart(2, '0')).join(''); return Array.from(arr, (v) => v.toString(16).padStart(2, '0')).join('');
} }
const user = { const user_xxx = {
id: 'USR-000', id: 'USR-000',
avatar: '/assets/avatar.png', avatar: '/assets/avatar.png',
firstName: 'Sofia', firstName: 'Sofia',
@@ -54,17 +57,23 @@ class AuthClient {
async signInWithPassword(params: SignInWithPasswordParams): Promise<{ error?: string }> { async signInWithPassword(params: SignInWithPasswordParams): Promise<{ error?: string }> {
const { email, password } = params; 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. // // 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') { // 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' }; return { error: 'Invalid credentials' };
} }
const token = generateToken();
localStorage.setItem('custom-auth-token', token);
return {};
} }
async resetPassword(_: ResetPasswordParams): Promise<{ error?: string }> { async resetPassword(_: ResetPasswordParams): Promise<{ error?: string }> {
@@ -79,16 +88,28 @@ class AuthClient {
// Make API request // Make API request
// We do not handle the API, so just check if we have a token in localStorage. // 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 }; return { data: null };
} catch (error) {
return { error: 'sorry cannot get user meta' };
} }
return { data: user };
} }
async signOut(): Promise<{ error?: string }> { async signOut(): Promise<{ error?: string }> {
pb.authStore.clear();
localStorage.removeItem('custom-auth-token'); localStorage.removeItem('custom-auth-token');
return {}; return {};