0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

feat(ui): add desktop profile page (#2624)

This commit is contained in:
Charles Zhao 2022-12-16 22:20:47 +08:00 committed by GitHub
parent 9ef395f668
commit 858e1190e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 462 additions and 1 deletions

View file

@ -85,6 +85,33 @@ const translation = {
'For added security, please link your email or phone with the account.', // UNTRANSLATED
continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED
},
profile: {
title: 'Account Settings', // UNTRANSLATED
description:
'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED
settings: {
title: 'PROFILE SETTINGS', // UNTRANSLATED
profile_information: 'Profile Information', // UNTRANSLATED
avatar: 'Avatar', // UNTRANSLATED
name: 'Name', // UNTRANSLATED
username: 'Username', // UNTRANSLATED
},
password: {
title: 'PASSWORD', // UNTRANSLATED
reset_password: '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
},
edit: 'Edit', // UNTRANSLATED
change: 'Change', // UNTRANSLATED
link: 'Link', // UNTRANSLATED
unlink: 'Unlink', // UNTRANSLATED
},
error: {
username_password_mismatch: 'Benutzername oder Passwort ist falsch',
username_required: 'Benutzername ist erforderlich',

View file

@ -81,6 +81,33 @@ const translation = {
'For added security, please link your email or phone with the account.',
continue_with_more_information: 'For added security, please complete below account details.',
},
profile: {
title: 'Account Settings',
description:
'Change your account settings and manage your personal information here to ensure your account security.',
settings: {
title: 'PROFILE SETTINGS',
profile_information: 'Profile Information',
avatar: 'Avatar',
name: 'Name',
username: 'Username',
},
password: {
title: 'PASSWORD',
reset_password: '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',
},
edit: 'Edit',
change: 'Change',
link: 'Link',
unlink: 'Unlink',
},
error: {
username_password_mismatch: 'Username and password do not match',
username_required: 'Username is required',

View file

@ -85,6 +85,33 @@ const translation = {
'For added security, please link your email or phone with the account.', // UNTRANSLATED
continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED
},
profile: {
title: 'Account Settings', // UNTRANSLATED
description:
'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED
settings: {
title: 'PROFILE SETTINGS', // UNTRANSLATED
profile_information: 'Profile Information', // UNTRANSLATED
avatar: 'Avatar', // UNTRANSLATED
name: 'Name', // UNTRANSLATED
username: 'Username', // UNTRANSLATED
},
password: {
title: 'PASSWORD', // UNTRANSLATED
reset_password: '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
},
edit: 'Edit', // UNTRANSLATED
change: 'Change', // UNTRANSLATED
link: 'Link', // UNTRANSLATED
unlink: 'Unlink', // UNTRANSLATED
},
error: {
username_password_mismatch: "Le nom d'utilisateur et le mot de passe ne correspondent pas",
username_required: "Le nom d'utilisateur est requis",

View file

@ -81,6 +81,33 @@ const translation = {
'For added security, please link your email or phone with the account.', // UNTRANSLATED
continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED
},
profile: {
title: 'Account Settings', // UNTRANSLATED
description:
'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED
settings: {
title: 'PROFILE SETTINGS', // UNTRANSLATED
profile_information: 'Profile Information', // UNTRANSLATED
avatar: 'Avatar', // UNTRANSLATED
name: 'Name', // UNTRANSLATED
username: 'Username', // UNTRANSLATED
},
password: {
title: 'PASSWORD', // UNTRANSLATED
reset_password: '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
},
edit: 'Edit', // UNTRANSLATED
change: 'Change', // UNTRANSLATED
link: 'Link', // UNTRANSLATED
unlink: 'Unlink', // UNTRANSLATED
},
error: {
username_password_mismatch: '사용자 이름 또는 비밀번호가 일치하지 않아요.',
username_required: '사용자 이름은 필수예요.',

View file

@ -83,6 +83,33 @@ const translation = {
'Para maior segurança, vincule seu e-mail ou telefone à conta.',
continue_with_more_information: 'Para maior segurança, preencha os detalhes da conta abaixo.',
},
profile: {
title: 'Account Settings', // UNTRANSLATED
description:
'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED
settings: {
title: 'PROFILE SETTINGS', // UNTRANSLATED
profile_information: 'Profile Information', // UNTRANSLATED
avatar: 'Avatar', // UNTRANSLATED
name: 'Name', // UNTRANSLATED
username: 'Username', // UNTRANSLATED
},
password: {
title: 'PASSWORD', // UNTRANSLATED
reset_password: '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
},
edit: 'Edit', // UNTRANSLATED
change: 'Change', // UNTRANSLATED
link: 'Link', // UNTRANSLATED
unlink: 'Unlink', // UNTRANSLATED
},
error: {
username_password_mismatch: 'Usuário e senha não correspondem',
username_required: 'Nome de usuário é obrigatório',

View file

@ -81,6 +81,33 @@ const translation = {
'For added security, please link your email or phone with the account.', // UNTRANSLATED
continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED
},
profile: {
title: 'Account Settings', // UNTRANSLATED
description:
'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED
settings: {
title: 'PROFILE SETTINGS', // UNTRANSLATED
profile_information: 'Profile Information', // UNTRANSLATED
avatar: 'Avatar', // UNTRANSLATED
name: 'Name', // UNTRANSLATED
username: 'Username', // UNTRANSLATED
},
password: {
title: 'PASSWORD', // UNTRANSLATED
reset_password: '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
},
edit: 'Edit', // UNTRANSLATED
change: 'Change', // UNTRANSLATED
link: 'Link', // UNTRANSLATED
unlink: 'Unlink', // UNTRANSLATED
},
error: {
username_password_mismatch: 'O Utilizador e a password não correspondem',
username_required: 'Utilizador necessário',

View file

@ -82,6 +82,33 @@ const translation = {
'For added security, please link your email or phone with the account.', // UNTRANSLATED
continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED
},
profile: {
title: 'Account Settings', // UNTRANSLATED
description:
'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED
settings: {
title: 'PROFILE SETTINGS', // UNTRANSLATED
profile_information: 'Profile Information', // UNTRANSLATED
avatar: 'Avatar', // UNTRANSLATED
name: 'Name', // UNTRANSLATED
username: 'Username', // UNTRANSLATED
},
password: {
title: 'PASSWORD', // UNTRANSLATED
reset_password: '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
},
edit: 'Edit', // UNTRANSLATED
change: 'Change', // UNTRANSLATED
link: 'Link', // UNTRANSLATED
unlink: 'Unlink', // UNTRANSLATED
},
error: {
username_password_mismatch: 'Kullanıcı adı ve şifre eşleşmiyor.',
username_required: 'Kullanıcı adı gerekli.',

View file

@ -77,6 +77,33 @@ const translation = {
link_email_or_phone_description: '绑定邮箱或手机号以保障您的账号安全',
continue_with_more_information: '为保障您的账号安全,需要您补充以下信息。',
},
profile: {
title: 'Account Settings', // UNTRANSLATED
description:
'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED
settings: {
title: 'PROFILE SETTINGS', // UNTRANSLATED
profile_information: 'Profile Information', // UNTRANSLATED
avatar: 'Avatar', // UNTRANSLATED
name: 'Name', // UNTRANSLATED
username: 'Username', // UNTRANSLATED
},
password: {
title: 'PASSWORD', // UNTRANSLATED
reset_password: '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
},
edit: 'Edit', // UNTRANSLATED
change: 'Change', // UNTRANSLATED
link: 'Link', // UNTRANSLATED
unlink: 'Unlink', // UNTRANSLATED
},
error: {
username_password_mismatch: '用户名和密码不匹配',
username_required: '用户名必填',

View file

@ -0,0 +1,8 @@
import type { UserInfo } from '@logto/schemas';
import api from './api';
const profileApiPrefix = '/api/profile';
export const getUserProfile = async (): Promise<UserInfo> =>
api.get(profileApiPrefix).json<UserInfo>();

View file

@ -0,0 +1,31 @@
@use '@/scss/underscore' as _;
.container {
padding: _.unit(6) _.unit(8);
display: flex;
margin-top: _.unit(4);
background: var(--color-bg-layer-1);
border-radius: 12px;
}
.title {
width: 405px;
flex-shrink: 0;
color: var(--color-neutral-variant-60);
font: var(--font-subhead-cap);
}
.content {
flex-grow: 1;
}
@media screen and (max-width: 1080px) {
.container {
flex-direction: column;
.content {
margin-top: _.unit(4);
flex-grow: unset;
}
}
}

View file

@ -0,0 +1,23 @@
import type { I18nKey } from '@logto/phrases-ui';
import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import * as styles from './index.module.scss';
type Props = {
title: I18nKey;
children: ReactNode;
};
const FormCard = ({ title, children }: Props) => {
const { t } = useTranslation();
return (
<div className={styles.container}>
<div className={styles.title}>{t(title)}</div>
<div className={styles.content}>{children}</div>
</div>
);
};
export default FormCard;

View file

@ -0,0 +1,30 @@
@use '@/scss/underscore' as _;
.container {
width: 100%;
.title {
font: var(--font-label-2);
margin-bottom: _.unit(1);
}
table {
width: 100%;
border-spacing: 0;
border: 1px solid var(--color-neutral-variant-90);
border-radius: 8px;
td {
padding: _.unit(6);
border-bottom: 1px solid var(--color-neutral-variant-90);
&:first-child {
width: 35%;
}
}
tr:last-child td {
border-bottom: none;
}
}
}

View file

@ -0,0 +1,44 @@
import type { I18nKey } from '@logto/phrases-ui';
import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import * as styles from './index.module.scss';
export type Row = {
label: I18nKey;
value: unknown;
renderer?: (value: unknown) => ReactNode;
};
type Props = {
title: I18nKey;
data: Row[];
};
const defaultRenderer = (value: unknown) => (value ? String(value) : '-');
const Table = ({ title, data }: Props) => {
const { t } = useTranslation();
if (data.length === 0) {
return null;
}
return (
<div className={styles.container}>
<div className={styles.title}>{t(title)}</div>
<table>
<tbody>
{data.map(({ label, value, renderer = defaultRenderer }) => (
<tr key={label}>
<td>{t(label)}</td>
<td>{renderer(value)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default Table;

View file

@ -0,0 +1,42 @@
@use '@/scss/underscore' as _;
.container {
@include _.flex-column(center, normal);
position: absolute;
inset: 0;
overflow-y: auto;
.wrapper {
@include _.flex-column(normal, normal);
width: 100%;
max-width: 1200px;
flex: 1;
padding: _.unit(4);
.header {
margin-top: _.unit(2);
.title {
font: var(--font-title-1);
margin-bottom: _.unit(1);
}
.subtitle {
font: var(--font-body-2);
color: var(--color-type-secondary);
}
}
}
}
:global(body.mobile) {
.container {
background: var(--color-bg-body-base);
}
}
:global(body.desktop) {
.container {
background: var(--color-surface);
}
}

View file

@ -1,5 +1,63 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { getUserProfile } from '@/apis/profile';
import LoadingLayer from '@/components/LoadingLayer';
import useApi from '@/hooks/use-api';
import FormCard from './components/FormCard';
import Table from './components/Table';
import * as styles from './index.module.scss';
const Profile = () => {
return <>Profile works!</>;
const { t } = useTranslation();
const { run: asyncGetProfile, result: profile } = useApi(getUserProfile);
useEffect(() => {
void asyncGetProfile();
}, [asyncGetProfile]);
if (!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>
</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>
</div>
</div>
);
};
export default Profile;

View file

@ -55,6 +55,7 @@
/* Background */
--color-bg-body-base: var(--color-neutral-95);
--color-bg-body: var(--color-neutral-100);
--color-bg-layer-1: var(--color-static-white);
--color-bg-layer-2: var(--color-neutral-95);
--color-bg-body-overlay: var(--color-neutral-100);
--color-bg-float-base: var(--color-neutral-variant-90);
@ -93,6 +94,8 @@
--color-overlay-brand-hover: rgba(93, 52, 242, 8%); // 8% --color-brand-default
--color-overlay-brand-pressed: rgba(93, 52, 242, 12%); // 12% --color-brand-default
--color-overlay-brand-focused: rgba(93, 52, 242, 16%); // 16% --color-brand-default
--color-surface: var(--color-neutral-99);
}
@mixin dark {
@ -156,6 +159,10 @@
--color-bg-body-base: var(--color-neutral-100);
--color-bg-body: var(--color-surface);
--color-bg-body-overlay: var(--color-surface-2);
--color-bg-layer-1:
linear-gradient(0deg, rgba(202, 190, 255, 8%), rgba(202, 190, 255, 8%)),
linear-gradient(0deg, rgba(196, 199, 199, 2%), rgba(196, 199, 199, 2%)),
#191c1d;
--color-bg-layer-2: var(--color-surface-4);
--color-bg-float-base: var(--color-neutral-100);
--color-bg-float: var(--color-surface-2);

View file

@ -24,4 +24,6 @@ $font-family:
--font-body-1: 400 16px/24px #{$font-family};
--font-body-2: 400 14px/20px #{$font-family};
--font-body-3: 400 12px/16px #{$font-family};
--font-subhead-cap: 700 12px/16px #{$font-family};
}