"update user popover with dynamic user metadata loading and improved UI consistency,"
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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
|
||||
? {
|
||||
|
@@ -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 {};
|
||||
|
Reference in New Issue
Block a user