"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]);
|
}, [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>
|
||||||
);
|
);
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
? {
|
? {
|
||||||
|
@@ -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 {};
|
||||||
|
Reference in New Issue
Block a user