"refactor settings pages, add translation support and implement side navigation component"
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
// import type { Metadata } from 'next';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { AccountDetails } from '@/components/dashboard/settings/account-details';
|
||||
@@ -9,13 +12,15 @@ import { DeleteAccount } from '@/components/dashboard/settings/delete-account';
|
||||
import { Privacy } from '@/components/dashboard/settings/privacy';
|
||||
import { ThemeSwitch } from '@/components/dashboard/settings/theme-switch';
|
||||
|
||||
export const metadata = { title: `Account | Settings | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
// export const metadata = { title: `Account | Settings | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Typography variant="h4">Account</Typography>
|
||||
<Typography variant="h4">{t('account')}</Typography>
|
||||
</div>
|
||||
<Stack spacing={4}>
|
||||
<AccountDetails />
|
||||
|
@@ -18,7 +18,11 @@ export default function Layout({ children }: LayoutProps): React.JSX.Element {
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={4} sx={{ position: 'relative' }}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
spacing={4}
|
||||
sx={{ position: 'relative' }}
|
||||
>
|
||||
<SideNav />
|
||||
<Box sx={{ flex: '1 1 auto', minWidth: 0 }}>{children}</Box>
|
||||
</Stack>
|
||||
|
@@ -14,10 +14,11 @@ import { PhoneNotifications } from '@/components/dashboard/settings/phone-notifi
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Typography variant="h4">{t('Notifications')}</Typography>
|
||||
<Typography variant="h4">{t('notifications')}</Typography>
|
||||
</div>
|
||||
<Stack spacing={4}>
|
||||
<EmailNotifications />
|
||||
|
@@ -1,7 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
@@ -9,13 +12,15 @@ import { LoginHistory } from '@/components/dashboard/settings/login-history';
|
||||
import { MultiFactor } from '@/components/dashboard/settings/multi-factor';
|
||||
import { PasswordForm } from '@/components/dashboard/settings/password-form';
|
||||
|
||||
export const metadata = { title: `Security | Settings | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
// export const metadata = { title: `Security | Settings | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Typography variant="h4">Security</Typography>
|
||||
<Typography variant="h4">{t('security')}</Typography>
|
||||
</div>
|
||||
<Stack spacing={4}>
|
||||
<PasswordForm />
|
||||
|
@@ -1,33 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
// import type { Metadata } from 'next';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { Members } from '@/components/dashboard/settings/members';
|
||||
|
||||
export const metadata = { title: `Team | Settings | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
// export const metadata = { title: `Team | Settings | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Typography variant="h4">Team</Typography>
|
||||
<Typography variant="h4">{t('team')}</Typography>
|
||||
</div>
|
||||
<Members
|
||||
members={[
|
||||
{
|
||||
id: 'USR-000',
|
||||
name: 'Sofia Rivers',
|
||||
name: 'team member1',
|
||||
avatar: '/assets/avatar.png',
|
||||
email: 'sofia@devias.io',
|
||||
email: 'teamMember1@devias.io',
|
||||
role: 'Owner',
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
name: 'team member2',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
email: 'teamMember2@domain.com',
|
||||
role: 'Standard',
|
||||
},
|
||||
]}
|
||||
|
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { COL_USER_METAS } from '@/constants';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { logger } from '@/lib/default-logger';
|
||||
import { fileToBase64 } from '@/lib/file-to-base64';
|
||||
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { useUser } from '@/hooks/use-user';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
import ErrorDisplay from '../../error';
|
||||
import { NavItem } from './nav-item';
|
||||
import { navItems } from './navItems';
|
||||
|
||||
export function SideNav(): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { user, isLoading } = useUser();
|
||||
const pathname = usePathname();
|
||||
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
|
||||
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(false);
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
|
||||
if (showLoading) return <FormLoading />;
|
||||
if (!user) return <>loading</>;
|
||||
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code="500"
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stack
|
||||
spacing={3}
|
||||
sx={{
|
||||
flex: '0 0 auto',
|
||||
flexDirection: { xs: 'column-reverse', md: 'column' },
|
||||
position: { md: 'sticky' },
|
||||
top: '64px',
|
||||
width: { xs: '100%', md: '240px' },
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="ul"
|
||||
spacing={3}
|
||||
sx={{ listStyle: 'none', m: 0, p: 0 }}
|
||||
>
|
||||
{navItems.map((group) => (
|
||||
<Stack
|
||||
component="li"
|
||||
key={group.key}
|
||||
spacing={2}
|
||||
>
|
||||
{group.title ? (
|
||||
<div>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="caption"
|
||||
>
|
||||
{group.title}
|
||||
</Typography>
|
||||
</div>
|
||||
) : null}
|
||||
<Stack
|
||||
component="ul"
|
||||
spacing={1}
|
||||
sx={{ listStyle: 'none', m: 0, p: 0 }}
|
||||
>
|
||||
{group.items.map((item) => (
|
||||
<NavItem
|
||||
{...item}
|
||||
key={item.key}
|
||||
pathname={pathname}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center' }}
|
||||
>
|
||||
<Avatar src={getImageUrlFromFile(user.collectionId, user.id, user.avatar)}>{user.name}</Avatar>
|
||||
<div>
|
||||
<Typography variant="subtitle1">{user.name}</Typography>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="caption"
|
||||
>
|
||||
{user.email}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -16,36 +16,9 @@ import { UserCircle as UserCircleIcon } from '@phosphor-icons/react/dist/ssr/Use
|
||||
import { UsersThree as UsersThreeIcon } from '@phosphor-icons/react/dist/ssr/UsersThree';
|
||||
|
||||
import type { NavItemConfig } from '@/types/nav';
|
||||
import { paths } from '@/paths';
|
||||
import { isNavItemActive } from '@/lib/is-nav-item-active';
|
||||
|
||||
// NOTE: First level elements are groups.
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
key: 'personal',
|
||||
title: 'Personal',
|
||||
items: [
|
||||
{ key: 'account', title: 'Account', href: paths.dashboard.settings.account, icon: 'user-circle' },
|
||||
{ key: 'notifications', title: 'Notifications', href: paths.dashboard.settings.notifications, icon: 'bell' },
|
||||
{ key: 'security', title: 'Security', href: paths.dashboard.settings.security, icon: 'lock-key' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'organization',
|
||||
title: 'Organization',
|
||||
items: [
|
||||
{ key: 'billing', title: 'Billing & plans', href: paths.dashboard.settings.billing, icon: 'credit-card' },
|
||||
{ key: 'team', title: 'Team', href: paths.dashboard.settings.team, icon: 'users-three' },
|
||||
{
|
||||
key: 'integrations',
|
||||
title: 'Integrations',
|
||||
href: paths.dashboard.settings.integrations,
|
||||
icon: 'plugs-connected',
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies NavItemConfig[];
|
||||
import { navItems } from './navItems';
|
||||
|
||||
const icons = {
|
||||
'credit-card': CreditCardIcon,
|
||||
@@ -56,84 +29,11 @@ const icons = {
|
||||
bell: BellIcon,
|
||||
} as Record<string, Icon>;
|
||||
|
||||
export function SideNav(): React.JSX.Element {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stack
|
||||
spacing={3}
|
||||
sx={{
|
||||
flex: '0 0 auto',
|
||||
flexDirection: { xs: 'column-reverse', md: 'column' },
|
||||
position: { md: 'sticky' },
|
||||
top: '64px',
|
||||
width: { xs: '100%', md: '240px' },
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="ul"
|
||||
spacing={3}
|
||||
sx={{ listStyle: 'none', m: 0, p: 0 }}
|
||||
>
|
||||
{navItems.map((group) => (
|
||||
<Stack
|
||||
component="li"
|
||||
key={group.key}
|
||||
spacing={2}
|
||||
>
|
||||
{group.title ? (
|
||||
<div>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="caption"
|
||||
>
|
||||
{group.title}
|
||||
</Typography>
|
||||
</div>
|
||||
) : null}
|
||||
<Stack
|
||||
component="ul"
|
||||
spacing={1}
|
||||
sx={{ listStyle: 'none', m: 0, p: 0 }}
|
||||
>
|
||||
{group.items.map((item) => (
|
||||
<NavItem
|
||||
{...item}
|
||||
key={item.key}
|
||||
pathname={pathname}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
<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"
|
||||
>
|
||||
sofia@devias.io
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface NavItemProps extends NavItemConfig {
|
||||
pathname: string;
|
||||
}
|
||||
|
||||
function NavItem({ disabled, external, href, icon, pathname, title }: NavItemProps): React.JSX.Element {
|
||||
export function NavItem({ disabled, external, href, icon, pathname, title }: NavItemProps): React.JSX.Element {
|
||||
const active = isNavItemActive({ disabled, external, href, pathname });
|
||||
const Icon = icon ? icons[icon] : null;
|
||||
|
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import type { NavItemConfig } from '@/types/nav';
|
||||
import { paths } from '@/paths';
|
||||
|
||||
// NOTE: First level elements are groups.
|
||||
const navItems = [
|
||||
{
|
||||
key: 'personal',
|
||||
title: 'Personal',
|
||||
items: [
|
||||
{ key: 'account', title: 'Account', href: paths.dashboard.settings.account, icon: 'user-circle' },
|
||||
{ key: 'notifications', title: 'Notifications', href: paths.dashboard.settings.notifications, icon: 'bell' },
|
||||
{ key: 'security', title: 'Security', href: paths.dashboard.settings.security, icon: 'lock-key' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'organization',
|
||||
title: 'Organization',
|
||||
items: [
|
||||
{ key: 'billing', title: 'Billing & plans', href: paths.dashboard.settings.billing, icon: 'credit-card' },
|
||||
{ key: 'team', title: 'Team', href: paths.dashboard.settings.team, icon: 'users-three' },
|
||||
{
|
||||
key: 'integrations',
|
||||
title: 'Integrations',
|
||||
href: paths.dashboard.settings.integrations,
|
||||
icon: 'plugs-connected',
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies NavItemConfig[];
|
||||
|
||||
export { navItems };
|
Reference in New Issue
Block a user