mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
refactor: add desktop and mobile views to render profile on different platforms (#2642)
This commit is contained in:
parent
858e1190e6
commit
c2bf25bd0b
22 changed files with 332 additions and 63 deletions
|
@ -15,6 +15,7 @@ import { createRequester } from '#src/utils/test-utils.js';
|
|||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const mockUserProfileResponse = { ...mockUserResponse, hasPasswordSet: true };
|
||||
const getLogtoConnectorById = jest.fn(async () => ({
|
||||
dbEntry: { enabled: true },
|
||||
metadata: { id: 'connectorId', target: 'mock_social' },
|
||||
|
@ -105,7 +106,7 @@ describe('session -> profileRoutes', () => {
|
|||
it('should return current user data', async () => {
|
||||
const response = await sessionRequest.get(profileRoute);
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toEqual(mockUserResponse);
|
||||
expect(response.body).toEqual(mockUserProfileResponse);
|
||||
});
|
||||
|
||||
it('should throw when the user is not authenticated', async () => {
|
||||
|
@ -170,8 +171,7 @@ describe('session -> profileRoutes', () => {
|
|||
.patch(`${profileRoute}/username`)
|
||||
.send({ username: newUsername });
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toEqual({ ...mockUserResponse, username: newUsername });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
});
|
||||
|
||||
it('should throw when username is already in use', async () => {
|
||||
|
|
|
@ -28,7 +28,11 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
|
||||
const user = await findUserById(userId);
|
||||
|
||||
ctx.body = pick(user, ...userInfoSelectFields);
|
||||
ctx.body = {
|
||||
...pick(user, ...userInfoSelectFields),
|
||||
hasPasswordSet: Boolean(user.passwordEncrypted),
|
||||
};
|
||||
|
||||
ctx.status = 200;
|
||||
|
||||
return next();
|
||||
|
@ -69,9 +73,9 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
|
||||
const { username } = ctx.guard.body;
|
||||
await checkIdentifierCollision({ username }, userId);
|
||||
await updateUserById(userId, { username }, 'replace');
|
||||
|
||||
const user = await updateUserById(userId, { username }, 'replace');
|
||||
ctx.body = pick(user, ...userInfoSelectFields);
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
|
|
@ -99,14 +99,18 @@ const translation = {
|
|||
password: {
|
||||
title: 'PASSWORD', // UNTRANSLATED
|
||||
reset_password: 'Reset Password', // UNTRANSLATED
|
||||
reset_password_sc: 'Reset password', // UNTRANSLATED
|
||||
},
|
||||
link_account: {
|
||||
title: 'LINK ACCOUNT', // UNTRANSLATED
|
||||
email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED
|
||||
email: 'Email', // UNTRANSLATED
|
||||
phone: 'Phone', // UNTRANSLATED
|
||||
social_sign_in: 'Social Sign-In', // UNTRANSLATED
|
||||
phone_sc: 'Phone number', // UNTRANSLATED
|
||||
social: 'Social Sign-In', // UNTRANSLATED
|
||||
social_sc: 'Social accounts', // UNTRANSLATED
|
||||
},
|
||||
not_set: 'Not set', // UNTRANSLATED
|
||||
edit: 'Edit', // UNTRANSLATED
|
||||
change: 'Change', // UNTRANSLATED
|
||||
link: 'Link', // UNTRANSLATED
|
||||
|
|
|
@ -95,14 +95,18 @@ const translation = {
|
|||
password: {
|
||||
title: 'PASSWORD',
|
||||
reset_password: 'Reset Password',
|
||||
reset_password_sc: 'Reset password',
|
||||
},
|
||||
link_account: {
|
||||
title: 'LINK ACCOUNT',
|
||||
email_phone_sign_in: 'Email / Phone Sign-In',
|
||||
email: 'Email',
|
||||
phone: 'Phone',
|
||||
social_sign_in: 'Social Sign-In',
|
||||
phone_sc: 'Phone number',
|
||||
social: 'Social Sign-In',
|
||||
social_sc: 'Social accounts',
|
||||
},
|
||||
not_set: 'Not set',
|
||||
edit: 'Edit',
|
||||
change: 'Change',
|
||||
link: 'Link',
|
||||
|
|
|
@ -99,14 +99,18 @@ const translation = {
|
|||
password: {
|
||||
title: 'PASSWORD', // UNTRANSLATED
|
||||
reset_password: 'Reset Password', // UNTRANSLATED
|
||||
reset_password_sc: 'Reset password', // UNTRANSLATED
|
||||
},
|
||||
link_account: {
|
||||
title: 'LINK ACCOUNT', // UNTRANSLATED
|
||||
email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED
|
||||
email: 'Email', // UNTRANSLATED
|
||||
phone: 'Phone', // UNTRANSLATED
|
||||
social_sign_in: 'Social Sign-In', // UNTRANSLATED
|
||||
phone_sc: 'Phone number', // UNTRANSLATED
|
||||
social: 'Social Sign-In', // UNTRANSLATED
|
||||
social_sc: 'Social accounts', // UNTRANSLATED
|
||||
},
|
||||
not_set: 'Not set', // UNTRANSLATED
|
||||
edit: 'Edit', // UNTRANSLATED
|
||||
change: 'Change', // UNTRANSLATED
|
||||
link: 'Link', // UNTRANSLATED
|
||||
|
|
|
@ -95,14 +95,18 @@ const translation = {
|
|||
password: {
|
||||
title: 'PASSWORD', // UNTRANSLATED
|
||||
reset_password: 'Reset Password', // UNTRANSLATED
|
||||
reset_password_sc: 'Reset password', // UNTRANSLATED
|
||||
},
|
||||
link_account: {
|
||||
title: 'LINK ACCOUNT', // UNTRANSLATED
|
||||
email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED
|
||||
email: 'Email', // UNTRANSLATED
|
||||
phone: 'Phone', // UNTRANSLATED
|
||||
social_sign_in: 'Social Sign-In', // UNTRANSLATED
|
||||
phone_sc: 'Phone number', // UNTRANSLATED
|
||||
social: 'Social Sign-In', // UNTRANSLATED
|
||||
social_sc: 'Social accounts', // UNTRANSLATED
|
||||
},
|
||||
not_set: 'Not set', // UNTRANSLATED
|
||||
edit: 'Edit', // UNTRANSLATED
|
||||
change: 'Change', // UNTRANSLATED
|
||||
link: 'Link', // UNTRANSLATED
|
||||
|
|
|
@ -97,14 +97,18 @@ const translation = {
|
|||
password: {
|
||||
title: 'PASSWORD', // UNTRANSLATED
|
||||
reset_password: 'Reset Password', // UNTRANSLATED
|
||||
reset_password_sc: 'Reset password', // UNTRANSLATED
|
||||
},
|
||||
link_account: {
|
||||
title: 'LINK ACCOUNT', // UNTRANSLATED
|
||||
email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED
|
||||
email: 'Email', // UNTRANSLATED
|
||||
phone: 'Phone', // UNTRANSLATED
|
||||
social_sign_in: 'Social Sign-In', // UNTRANSLATED
|
||||
phone_sc: 'Phone number', // UNTRANSLATED
|
||||
social: 'Social Sign-In', // UNTRANSLATED
|
||||
social_sc: 'Social accounts', // UNTRANSLATED
|
||||
},
|
||||
not_set: 'Not set', // UNTRANSLATED
|
||||
edit: 'Edit', // UNTRANSLATED
|
||||
change: 'Change', // UNTRANSLATED
|
||||
link: 'Link', // UNTRANSLATED
|
||||
|
|
|
@ -95,14 +95,18 @@ const translation = {
|
|||
password: {
|
||||
title: 'PASSWORD', // UNTRANSLATED
|
||||
reset_password: 'Reset Password', // UNTRANSLATED
|
||||
reset_password_sc: 'Reset password', // UNTRANSLATED
|
||||
},
|
||||
link_account: {
|
||||
title: 'LINK ACCOUNT', // UNTRANSLATED
|
||||
email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED
|
||||
email: 'Email', // UNTRANSLATED
|
||||
phone: 'Phone', // UNTRANSLATED
|
||||
social_sign_in: 'Social Sign-In', // UNTRANSLATED
|
||||
phone_sc: 'Phone number', // UNTRANSLATED
|
||||
social: 'Social Sign-In', // UNTRANSLATED
|
||||
social_sc: 'Social accounts', // UNTRANSLATED
|
||||
},
|
||||
not_set: 'Not set', // UNTRANSLATED
|
||||
edit: 'Edit', // UNTRANSLATED
|
||||
change: 'Change', // UNTRANSLATED
|
||||
link: 'Link', // UNTRANSLATED
|
||||
|
|
|
@ -96,14 +96,18 @@ const translation = {
|
|||
password: {
|
||||
title: 'PASSWORD', // UNTRANSLATED
|
||||
reset_password: 'Reset Password', // UNTRANSLATED
|
||||
reset_password_sc: 'Reset password', // UNTRANSLATED
|
||||
},
|
||||
link_account: {
|
||||
title: 'LINK ACCOUNT', // UNTRANSLATED
|
||||
email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED
|
||||
email: 'Email', // UNTRANSLATED
|
||||
phone: 'Phone', // UNTRANSLATED
|
||||
social_sign_in: 'Social Sign-In', // UNTRANSLATED
|
||||
phone_sc: 'Phone number', // UNTRANSLATED
|
||||
social: 'Social Sign-In', // UNTRANSLATED
|
||||
social_sc: 'Social accounts', // UNTRANSLATED
|
||||
},
|
||||
not_set: 'Not set', // UNTRANSLATED
|
||||
edit: 'Edit', // UNTRANSLATED
|
||||
change: 'Change', // UNTRANSLATED
|
||||
link: 'Link', // UNTRANSLATED
|
||||
|
|
|
@ -91,14 +91,18 @@ const translation = {
|
|||
password: {
|
||||
title: 'PASSWORD', // UNTRANSLATED
|
||||
reset_password: 'Reset Password', // UNTRANSLATED
|
||||
reset_password_sc: 'Reset password', // UNTRANSLATED
|
||||
},
|
||||
link_account: {
|
||||
title: 'LINK ACCOUNT', // UNTRANSLATED
|
||||
email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED
|
||||
email: 'Email', // UNTRANSLATED
|
||||
phone: 'Phone', // UNTRANSLATED
|
||||
social_sign_in: 'Social Sign-In', // UNTRANSLATED
|
||||
phone_sc: 'Phone number', // UNTRANSLATED
|
||||
social: 'Social Sign-In', // UNTRANSLATED
|
||||
social_sc: 'Social accounts', // UNTRANSLATED
|
||||
},
|
||||
not_set: 'Not set', // UNTRANSLATED
|
||||
edit: 'Edit', // UNTRANSLATED
|
||||
change: 'Change', // UNTRANSLATED
|
||||
link: 'Link', // UNTRANSLATED
|
||||
|
|
|
@ -21,6 +21,8 @@ export type UserInfo<Keys extends keyof CreateUser = typeof userInfoSelectFields
|
|||
Keys
|
||||
>;
|
||||
|
||||
export type UserProfileResponse = UserInfo & { hasPasswordSet: boolean };
|
||||
|
||||
export enum UserRole {
|
||||
Admin = 'admin',
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { UserInfo } from '@logto/schemas';
|
||||
import type { UserProfileResponse } from '@logto/schemas';
|
||||
|
||||
import api from './api';
|
||||
|
||||
const profileApiPrefix = '/api/profile';
|
||||
|
||||
export const getUserProfile = async (): Promise<UserInfo> =>
|
||||
api.get(profileApiPrefix).json<UserInfo>();
|
||||
export const getUserProfile = async (): Promise<UserProfileResponse> =>
|
||||
api.get(profileApiPrefix).json<UserProfileResponse>();
|
||||
|
|
3
packages/ui/src/assets/icons/arrow-next.svg
Normal file
3
packages/ui/src/assets/icons/arrow-next.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.2448 10.3488L9.05651 5.16959C8.97129 5.08367 8.86991 5.01547 8.7582 4.96894C8.6465 4.9224 8.52669 4.89844 8.40567 4.89844C8.28466 4.89844 8.16485 4.9224 8.05315 4.96894C7.94144 5.01547 7.84006 5.08367 7.75484 5.16959C7.58411 5.34133 7.48828 5.57367 7.48828 5.81584C7.48828 6.05801 7.58411 6.29034 7.75484 6.46209L12.2923 11.0454L7.75484 15.5829C7.58411 15.7547 7.48828 15.987 7.48828 16.2292C7.48828 16.4713 7.58411 16.7037 7.75484 16.8754C7.83974 16.962 7.94098 17.0309 8.0527 17.0781C8.16442 17.1253 8.28439 17.1499 8.40567 17.1504C8.52696 17.1499 8.64693 17.1253 8.75865 17.0781C8.87037 17.0309 8.97161 16.962 9.05651 16.8754L14.2448 11.6963C14.3379 11.6104 14.4121 11.5062 14.4629 11.3903C14.5137 11.2743 14.5399 11.1491 14.5399 11.0225C14.5399 10.8959 14.5137 10.7707 14.4629 10.6547C14.4121 10.5388 14.3379 10.4346 14.2448 10.3488Z" fill="currentcolor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 977 B |
3
packages/ui/src/assets/icons/nav-close.svg
Normal file
3
packages/ui/src/assets/icons/nav-close.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.4099 12.0002L17.7099 7.71019C17.8982 7.52188 18.004 7.26649 18.004 7.00019C18.004 6.73388 17.8982 6.47849 17.7099 6.29019C17.5216 6.10188 17.2662 5.99609 16.9999 5.99609C16.7336 5.99609 16.4782 6.10188 16.2899 6.29019L11.9999 10.5902L7.70994 6.29019C7.52164 6.10188 7.26624 5.99609 6.99994 5.99609C6.73364 5.99609 6.47824 6.10188 6.28994 6.29019C6.10164 6.47849 5.99585 6.73388 5.99585 7.00019C5.99585 7.26649 6.10164 7.52188 6.28994 7.71019L10.5899 12.0002L6.28994 16.2902C6.19621 16.3831 6.12182 16.4937 6.07105 16.6156C6.02028 16.7375 5.99414 16.8682 5.99414 17.0002C5.99414 17.1322 6.02028 17.2629 6.07105 17.3848C6.12182 17.5066 6.19621 17.6172 6.28994 17.7102C6.3829 17.8039 6.4935 17.8783 6.61536 17.9291C6.73722 17.9798 6.86793 18.006 6.99994 18.006C7.13195 18.006 7.26266 17.9798 7.38452 17.9291C7.50638 17.8783 7.61698 17.8039 7.70994 17.7102L11.9999 13.4102L16.2899 17.7102C16.3829 17.8039 16.4935 17.8783 16.6154 17.9291C16.7372 17.9798 16.8679 18.006 16.9999 18.006C17.132 18.006 17.2627 17.9798 17.3845 17.9291C17.5064 17.8783 17.617 17.8039 17.7099 17.7102C17.8037 17.6172 17.8781 17.5066 17.9288 17.3848C17.9796 17.2629 18.0057 17.1322 18.0057 17.0002C18.0057 16.8682 17.9796 16.7375 17.9288 16.6156C17.8781 16.4937 17.8037 16.3831 17.7099 16.2902L13.4099 12.0002Z" fill="currentcolor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -18,9 +18,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.backButton {
|
||||
.navButton {
|
||||
position: absolute;
|
||||
left: _.unit(-2);
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font: var(--font-label-2);
|
||||
|
@ -29,13 +29,13 @@
|
|||
}
|
||||
|
||||
:global(body.mobile) {
|
||||
.backButton > span {
|
||||
.navButton > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global(body.desktop) {
|
||||
.backButton {
|
||||
.navButton {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
@ -2,35 +2,41 @@ import { useTranslation } from 'react-i18next';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import ArrowPrev from '@/assets/icons/arrow-prev.svg';
|
||||
import NavClose from '@/assets/icons/nav-close.svg';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
type?: 'back' | 'close';
|
||||
};
|
||||
|
||||
const NavBar = ({ title }: Props) => {
|
||||
const NavBar = ({ title, type = 'back' }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const isClosable = type === 'close';
|
||||
|
||||
const clickHandler = () => {
|
||||
if (isClosable) {
|
||||
window.close();
|
||||
}
|
||||
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.navBar}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.backButton}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
navigate(-1);
|
||||
})}
|
||||
onClick={() => {
|
||||
navigate(-1);
|
||||
}}
|
||||
className={styles.navButton}
|
||||
onKeyDown={onKeyDownHandler(clickHandler)}
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<ArrowPrev />
|
||||
<span>{t('action.nav_back')}</span>
|
||||
{isClosable ? <NavClose /> : <ArrowPrev />}
|
||||
{!isClosable && <span>{t('action.nav_back')}</span>}
|
||||
</div>
|
||||
|
||||
{title && <div className={styles.title}>{title}</div>}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-top: _.unit(3);
|
||||
background: var(--color-bg-layer-1);
|
||||
|
||||
.item {
|
||||
padding-left: _.unit(5);
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding: _.unit(3) 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--color-overlay-neutral-pressed);
|
||||
}
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
.wrapper {
|
||||
border-top: 1px solid var(--color-line-divider);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.label {
|
||||
font: var(--font-body-1);
|
||||
}
|
||||
|
||||
.value {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-type-secondary);
|
||||
margin-top: _.unit(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 _.unit(4) 0 _.unit(3);
|
||||
color: var(--color-type-secondary);
|
||||
}
|
||||
}
|
51
packages/ui/src/pages/Profile/components/NavItem/index.tsx
Normal file
51
packages/ui/src/pages/Profile/components/NavItem/index.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import type { I18nKey } from '@logto/phrases-ui';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ArrowNext from '@/assets/icons/arrow-next.svg';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Item = {
|
||||
label: I18nKey;
|
||||
value?: Nullable<string>;
|
||||
onTap: () => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
data: Item[];
|
||||
};
|
||||
|
||||
const NavItem = ({ data }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{data.map(({ label, value, onTap }) => (
|
||||
<div
|
||||
key={label}
|
||||
className={styles.item}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onTap}
|
||||
onKeyDown={onKeyDownHandler({
|
||||
Enter: onTap,
|
||||
})}
|
||||
>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.label}>{t(label)}</div>
|
||||
{value && <div className={styles.value}>{value}</div>}
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<ArrowNext />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavItem;
|
|
@ -0,0 +1,51 @@
|
|||
import type { UserProfileResponse } from '@logto/schemas';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FormCard from '../../components/FormCard';
|
||||
import Table from '../../components/Table';
|
||||
|
||||
type Props = {
|
||||
profile: UserProfileResponse;
|
||||
};
|
||||
|
||||
const DesktopView = ({ profile }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { avatar, name, username, primaryEmail, primaryPhone, hasPasswordSet } = profile;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormCard title="profile.settings.title">
|
||||
<Table
|
||||
title="profile.settings.profile_information"
|
||||
data={[
|
||||
{ label: 'profile.settings.avatar', value: avatar },
|
||||
{ label: 'profile.settings.name', value: name },
|
||||
{ label: 'profile.settings.username', value: username },
|
||||
]}
|
||||
/>
|
||||
</FormCard>
|
||||
<FormCard title="profile.password.title">
|
||||
<Table
|
||||
title="profile.password.reset_password"
|
||||
data={[
|
||||
{
|
||||
label: 'profile.password.reset_password',
|
||||
value: hasPasswordSet ? '******' : t('profile.not_set'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</FormCard>
|
||||
<FormCard title="profile.link_account.title">
|
||||
<Table
|
||||
title="profile.link_account.email_phone_sign_in"
|
||||
data={[
|
||||
{ label: 'profile.link_account.email', value: primaryEmail },
|
||||
{ label: 'profile.link_account.phone', value: primaryPhone },
|
||||
]}
|
||||
/>
|
||||
</FormCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesktopView;
|
|
@ -0,0 +1,71 @@
|
|||
import type { UserProfileResponse } from '@logto/schemas';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import NavItem from '../../components/NavItem';
|
||||
|
||||
type Props = {
|
||||
profile: UserProfileResponse;
|
||||
};
|
||||
|
||||
const MobileView = ({ profile }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { username, primaryEmail, primaryPhone, hasPasswordSet, identities } = profile;
|
||||
const socialConnectorNames = identities?.length
|
||||
? Object.keys(identities).join(', ')
|
||||
: t('profile.not_set');
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavItem
|
||||
data={[
|
||||
{
|
||||
label: 'profile.settings.username',
|
||||
value: username ?? t('profile.not_set'),
|
||||
onTap: () => {
|
||||
console.log('username');
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<NavItem
|
||||
data={[
|
||||
{
|
||||
label: 'profile.password.reset_password_sc',
|
||||
value: hasPasswordSet ? '******' : t('profile.not_set'),
|
||||
onTap: () => {
|
||||
console.log('password');
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<NavItem
|
||||
data={[
|
||||
{
|
||||
label: 'profile.link_account.email',
|
||||
value: primaryEmail ?? t('profile.not_set'),
|
||||
onTap: () => {
|
||||
console.log('email');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'profile.link_account.phone_sc',
|
||||
value: primaryPhone ?? t('profile.not_set'),
|
||||
onTap: () => {
|
||||
console.log('phone');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'profile.link_account.social_sc',
|
||||
value: socialConnectorNames,
|
||||
onTap: () => {
|
||||
console.log('social accounts');
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileView;
|
|
@ -32,6 +32,16 @@
|
|||
:global(body.mobile) {
|
||||
.container {
|
||||
background: var(--color-bg-body-base);
|
||||
|
||||
.wrapper {
|
||||
padding: 0;
|
||||
|
||||
.header {
|
||||
margin: 0;
|
||||
padding: 0 _.unit(4);
|
||||
background: var(--color-bg-layer-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,15 +3,19 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { getUserProfile } from '@/apis/profile';
|
||||
import LoadingLayer from '@/components/LoadingLayer';
|
||||
import NavBar from '@/components/NavBar';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import usePlatform from '@/hooks/use-platform';
|
||||
|
||||
import FormCard from './components/FormCard';
|
||||
import Table from './components/Table';
|
||||
import DesktopView from './containers/DesktopView';
|
||||
import MobileView from './containers/MobileView';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const Profile = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = usePlatform();
|
||||
const { run: asyncGetProfile, result: profile } = useApi(getUserProfile);
|
||||
const ContainerView = isMobile ? MobileView : DesktopView;
|
||||
|
||||
useEffect(() => {
|
||||
void asyncGetProfile();
|
||||
|
@ -21,40 +25,19 @@ const Profile = () => {
|
|||
return <LoadingLayer />;
|
||||
}
|
||||
|
||||
const { avatar, name, username, primaryEmail, primaryPhone } = profile;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{t('profile.title')}</div>
|
||||
<div className={styles.subtitle}>{t('profile.description')}</div>
|
||||
{isMobile && <NavBar type="close" title={t('profile.title')} />}
|
||||
{!isMobile && (
|
||||
<>
|
||||
<div className={styles.title}>{t('profile.title')}</div>
|
||||
<div className={styles.subtitle}>{t('profile.description')}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<FormCard title="profile.settings.title">
|
||||
<Table
|
||||
title="profile.settings.profile_information"
|
||||
data={[
|
||||
{ label: 'profile.settings.avatar', value: avatar },
|
||||
{ label: 'profile.settings.name', value: name },
|
||||
{ label: 'profile.settings.username', value: username },
|
||||
]}
|
||||
/>
|
||||
</FormCard>
|
||||
<FormCard title="profile.password.title">
|
||||
<Table
|
||||
title="profile.password.reset_password"
|
||||
data={[{ label: 'profile.password.reset_password', value: '******' }]}
|
||||
/>
|
||||
</FormCard>
|
||||
<FormCard title="profile.link_account.title">
|
||||
<Table
|
||||
title="profile.link_account.email_phone_sign_in"
|
||||
data={[
|
||||
{ label: 'profile.link_account.email', value: primaryEmail },
|
||||
{ label: 'profile.link_account.phone', value: primaryPhone },
|
||||
]}
|
||||
/>
|
||||
</FormCard>
|
||||
<ContainerView profile={profile} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue