Merge branch 'ui-0.2' into 'main'

Ui 0.2

See merge request opensource/answer!130
This commit is contained in:
Li Shuailing 2022-10-31 03:13:55 +00:00
commit 810fbe8ae3
18 changed files with 479 additions and 60 deletions

View File

@ -25,8 +25,7 @@ module.exports = {
const config = configFunction(proxy, allowedHost);
config.proxy = {
'/answer': {
target: "http://10.0.20.84:8080",
// target: 'http://10.0.10.98:2060',
target: 'http://10.0.10.98:2060',
changeOrigin: true,
secure: false,
},

View File

@ -40,6 +40,8 @@
"katex": "^0.16.2",
"lodash": "^4.17.21",
"marked": "^4.0.19",
"md5": "^2.3.0",
"md5.js": "^1.3.5",
"mermaid": "^9.1.7",
"next-share": "^0.18.1",
"qs": "^6.11.0",

View File

@ -57,6 +57,8 @@ specifiers:
lint-staged: ^13.0.3
lodash: ^4.17.21
marked: ^4.0.19
md5: ^2.3.0
md5.js: ^1.3.5
mermaid: ^9.1.7
next-share: ^0.18.1
postcss: ^8.0.0
@ -96,6 +98,8 @@ dependencies:
katex: 0.16.2
lodash: 4.17.21
marked: 4.1.0
md5: 2.3.0
md5.js: 1.3.5
mermaid: 9.1.7
next-share: 0.18.1_lbqamd2wfmenkveygahn4wdfcq
qs: 6.11.0
@ -152,7 +156,7 @@ devDependencies:
prettier: 2.7.1
purgecss-webpack-plugin: 4.1.3
react-app-rewired: 2.2.1_react-scripts@5.0.1
react-scripts: 5.0.1_r727nmttzgvwuocpb6eyxi2m5i
react-scripts: 5.0.1_vcopaw66ubzwqe5wj7m4edgwnq
sass: 1.54.9
tsconfig-paths-webpack-plugin: 4.0.0
typescript: 4.8.3
@ -3660,6 +3664,10 @@ packages:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true
/charenc/0.0.2:
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
dev: false
/check-types/11.1.2:
resolution: {integrity: sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==}
@ -4060,8 +4068,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
dependencies:
is-text-path: 1.0.1
JSONStream: 1.3.5
is-text-path: 1.0.1
lodash: 4.17.21
meow: 8.1.2
split2: 3.2.2
@ -4157,6 +4165,10 @@ packages:
shebang-command: 2.0.0
which: 2.0.2
/crypt/0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
dev: false
/crypto-random-string/2.0.0:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
@ -6449,6 +6461,15 @@ packages:
dependencies:
function-bind: 1.1.1
/hash-base/3.1.0:
resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==}
engines: {node: '>=4'}
dependencies:
inherits: 2.0.4
readable-stream: 3.6.0
safe-buffer: 5.2.1
dev: false
/he/1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
@ -6807,6 +6828,10 @@ packages:
call-bind: 1.0.2
has-tostringtag: 1.0.0
/is-buffer/1.1.6:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
dev: false
/is-callable/1.2.6:
resolution: {integrity: sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q==}
engines: {node: '>= 0.4'}
@ -7966,6 +7991,22 @@ packages:
hasBin: true
dev: false
/md5.js/1.3.5:
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
dependencies:
hash-base: 3.1.0
inherits: 2.0.4
safe-buffer: 5.2.1
dev: false
/md5/2.3.0:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
dependencies:
charenc: 0.0.2
crypt: 0.0.2
is-buffer: 1.1.6
dev: false
/mdn-data/2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
@ -8167,7 +8208,7 @@ packages:
jsonp: 0.2.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-scripts: 5.0.1_r727nmttzgvwuocpb6eyxi2m5i
react-scripts: 5.0.1_vcopaw66ubzwqe5wj7m4edgwnq
transitivePeerDependencies:
- supports-color
dev: false
@ -9497,7 +9538,7 @@ packages:
peerDependencies:
react-scripts: '>=2.1.3'
dependencies:
react-scripts: 5.0.1_r727nmttzgvwuocpb6eyxi2m5i
react-scripts: 5.0.1_vcopaw66ubzwqe5wj7m4edgwnq
semver: 5.7.1
dev: true
@ -9531,6 +9572,12 @@ packages:
/react-dev-utils/12.0.1_npfwkgbcmgrbevrxnqgustqabe:
resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==}
engines: {node: '>=14'}
peerDependencies:
typescript: '>=2.7'
webpack: '>=4'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@babel/code-frame': 7.18.6
address: 1.2.1
@ -9561,9 +9608,7 @@ packages:
transitivePeerDependencies:
- eslint
- supports-color
- typescript
- vue-template-compiler
- webpack
/react-dom/18.2.0_react@18.2.0:
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
@ -9655,11 +9700,12 @@ packages:
react: 18.2.0
dev: false
/react-scripts/5.0.1_r727nmttzgvwuocpb6eyxi2m5i:
/react-scripts/5.0.1_vcopaw66ubzwqe5wj7m4edgwnq:
resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==}
engines: {node: '>=14.0.0'}
hasBin: true
peerDependencies:
eslint: '*'
react: '>= 16'
typescript: ^3.2.1 || ^4
peerDependenciesMeta:
@ -9708,7 +9754,7 @@ packages:
semver: 7.3.7
source-map-loader: 3.0.1_webpack@5.74.0
style-loader: 3.3.1_webpack@5.74.0
tailwindcss: 3.1.8
tailwindcss: 3.1.8_postcss@8.4.16
terser-webpack-plugin: 5.3.6_webpack@5.74.0
typescript: 4.8.3
webpack: 5.74.0
@ -10686,10 +10732,12 @@ packages:
/symbol-tree/3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
/tailwindcss/3.1.8:
/tailwindcss/3.1.8_postcss@8.4.16:
resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==}
engines: {node: '>=12.13.0'}
hasBin: true
peerDependencies:
postcss: ^8.0.9
dependencies:
arg: 5.0.2
chokidar: 3.5.3

View File

@ -2,6 +2,7 @@ export interface FormValue<T = any> {
value: T;
isInvalid: boolean;
errorMsg: string;
[prop: string]: any;
}
export interface FormDataType {
@ -89,7 +90,7 @@ export interface ModifyPasswordReq {
export interface ModifyUserReq {
display_name: string;
username?: string;
avatar: string;
avatar: any;
bio: string;
bio_html?: string;
location: string;
@ -97,7 +98,7 @@ export interface ModifyUserReq {
}
export interface UserInfoBase {
avatar: string;
avatar: any;
username: string;
display_name: string;
rank: number;

View File

@ -6,15 +6,25 @@ import DefaultAvatar from '@/assets/images/default-avatar.svg';
interface IProps {
/** avatar url */
avatar: string;
avatar: string | { type: string; gravatar: string; custom: string };
size: string;
searchStr?: string;
className?: string;
}
const Index: FC<IProps> = ({ avatar, size, className }) => {
const Index: FC<IProps> = ({ avatar, size, className, searchStr = '' }) => {
let url = '';
if (typeof avatar === 'string') {
if (avatar.length > 1) {
url = `${avatar}?${searchStr}`;
}
} else if (avatar?.type !== 'default') {
url = `${avatar[avatar.type]}?${searchStr}`;
}
return (
<img
src={avatar || DefaultAvatar}
src={url || DefaultAvatar}
width={size}
height={size}
className={classNames('rounded', className)}

View File

@ -9,6 +9,7 @@ interface Props {
data: any;
showAvatar?: boolean;
avatarSize?: string;
avatarSearchStr?: string;
className?: string;
}
@ -17,20 +18,31 @@ const Index: FC<Props> = ({
showAvatar = true,
avatarSize = '20px',
className = 'fs-14',
avatarSearchStr = 's=48',
}) => {
return (
<div className={`text-secondary ${className}`}>
{data?.status !== 'deleted' ? (
<Link to={`/users/${data?.username}`}>
{showAvatar && (
<Avatar avatar={data?.avatar} size={avatarSize} className="me-1" />
<Avatar
avatar={data?.avatar}
size={avatarSize}
className="me-1"
searchStr={avatarSearchStr}
/>
)}
<span className="me-1 text-break">{data?.display_name}</span>
</Link>
) : (
<>
{showAvatar && (
<Avatar avatar={data?.avatar} size={avatarSize} className="me-1" />
<Avatar
avatar={data?.avatar}
size={avatarSize}
className="me-1"
searchStr={avatarSearchStr}
/>
)}
<span className="me-1 text-break">{data?.display_name}</span>
</>

View File

@ -43,7 +43,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
id="dropdown-basic"
as="a"
className="no-toggle pointer">
<Avatar size="36px" avatar={userInfo?.avatar} />
<Avatar size="36px" avatar={userInfo?.avatar} searchStr="s=48" />
</Dropdown.Toggle>
<Dropdown.Menu>

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Button, Col } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { resendEmail, checkImgCode } from '@answer/api';
import { PicAuthCodeModal } from '@answer/components/Modal';
@ -120,6 +121,9 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
<Button variant="link" onClick={onSentEmail}>
{t('btn_name')}
</Button>
<Link to="/users/change-email" replace className="btn btn-link ms-2">
{t('change_btn_name')}
</Link>
</>
)}

View File

@ -23,16 +23,23 @@ const Index: FC<Props> = ({ data, time, preFix, className = '' }) => {
avatar={data?.avatar}
size="40px"
className="me-2 d-none d-md-block"
searchStr="s=48"
/>
<Avatar
avatar={data?.avatar}
size="24px"
className="me-2 d-block d-md-none"
searchStr="s=48"
/>
</Link>
) : (
<Avatar avatar={data?.avatar} size="40px" className="me-2" />
<Avatar
avatar={data?.avatar}
size="40px"
className="me-2"
searchStr="s=48"
/>
)}
<div className="fs-14 text-secondary d-flex flex-row flex-md-column align-items-center align-items-md-start">
<div className="me-1 me-md-0">

View File

@ -27,7 +27,8 @@
"account_activation": "Account Activation",
"confirm_email": "Confirm Email",
"account_suspended": "Account Suspended",
"admin": "Admin"
"admin": "Admin",
"change_email": "Modify Email"
},
"notifications": {
"title": "Notifications",
@ -421,6 +422,7 @@
"info": "If it doesn't arrive, check your spam folder.",
"another": "We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.",
"btn_name": "Resend activation email",
"change_btn_name": "Change email",
"msg": {
"empty": "Cannot be empty."
}
@ -462,6 +464,18 @@
}
}
},
"change_email": {
"page_title": "Welcome to Answer",
"btn_cancel": "Cancel",
"btn_update": "Update email address",
"send_success": "If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.",
"email": {
"label": "New Email",
"msg": {
"empty": "Email cannot be empty."
}
}
},
"password_reset": {
"page_title": "Password Reset",
"btn_name": "Reset my password",
@ -504,7 +518,12 @@
},
"avatar": {
"label": "Profile Image",
"text": "You can upload your image or <1>reset</1> it to"
"gravatar": "Gravatar",
"gravatar_text": "You can change image on <1>gravatar.com</1>",
"custom": "Custom",
"btn_refresh": "Refresh",
"custom_text": "You can upload your image.",
"default": "Default"
},
"bio": {
"label": "About Me (optional)"

View File

@ -53,7 +53,7 @@ a {
height: 18px;
border-radius: 50%;
position: absolute;
left: 20px;
left: 15px;
top: 0;
border: 1px solid #fff;
}

View File

@ -107,6 +107,7 @@ const Users: FC = () => {
data={user}
className="fs-6"
avatarSize="24px"
avatarSearchStr="s=48"
/>
</td>
<td>{user.rank}</td>

View File

@ -0,0 +1,176 @@
import { FC, memo, useEffect, useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { changeEmail, checkImgCode } from '@answer/api';
import type {
ImgCodeRes,
PasswordResetReq,
FormDataType,
} from '@answer/common/interface';
import { userInfoStore } from '@answer/stores';
import { PicAuthCodeModal } from '@/components/Modal';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
const [formData, setFormData] = useState<FormDataType>({
e_mail: {
value: '',
isInvalid: false,
errorMsg: '',
},
captcha_code: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
const [imgCode, setImgCode] = useState<ImgCodeRes>({
captcha_id: '',
captcha_img: '',
verify: false,
});
const [showModal, setModalState] = useState(false);
const navigate = useNavigate();
const { user: userInfo, update: updateUser } = userInfoStore();
const getImgCode = () => {
checkImgCode({
action: 'e_mail',
}).then((res) => {
setImgCode(res);
});
};
const handleChange = (params: FormDataType) => {
setFormData({ ...formData, ...params });
};
const checkValidated = (): boolean => {
let bol = true;
if (!formData.e_mail.value) {
bol = false;
formData.e_mail = {
value: '',
isInvalid: true,
errorMsg: t('email.msg.empty'),
};
}
setFormData({
...formData,
});
return bol;
};
const sendEmail = (e?: any) => {
if (e) {
e.preventDefault();
}
const params: PasswordResetReq = {
e_mail: formData.e_mail.value,
};
if (imgCode.verify) {
params.captcha_code = formData.captcha_code.value;
params.captcha_id = imgCode.captcha_id;
}
changeEmail(params)
.then(() => {
userInfo.e_mail = formData.e_mail.value;
updateUser(userInfo);
navigate('/users/login', { replace: true });
setModalState(false);
})
.catch((err) => {
if (err.isError && err.key) {
formData[err.key].isInvalid = true;
formData[err.key].errorMsg = err.value;
if (err.key.indexOf('captcha') < 0) {
setModalState(false);
}
}
setFormData({ ...formData });
})
.finally(() => {
getImgCode();
});
};
const handleSubmit = async (event: any) => {
event.preventDefault();
event.stopPropagation();
if (!checkValidated()) {
return;
}
if (imgCode.verify) {
setModalState(true);
return;
}
sendEmail();
};
const goBack = () => {
navigate('/users/login?status=inactive', { replace: true });
};
useEffect(() => {
getImgCode();
}, []);
return (
<>
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('email.label')}</Form.Label>
<Form.Control
required
type="email"
value={formData.e_mail.value}
isInvalid={formData.e_mail.isInvalid}
onChange={(e) => {
handleChange({
e_mail: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.e_mail.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<div className="d-grid mb-3">
<Button variant="primary" type="submit">
{t('btn_update')}
</Button>
<Button variant="link" className="mt-2 d-block" onClick={goBack}>
{t('btn_cancel')}
</Button>
</div>
</Form>
<PicAuthCodeModal
visible={showModal}
data={{
captcha: formData.captcha_code,
imgCode,
}}
handleCaptcha={handleChange}
clickSubmit={sendEmail}
refreshImgCode={getImgCode}
onClose={() => setModalState(false)}
/>
</>
);
};
export default memo(Index);

View File

@ -0,0 +1,25 @@
import { FC, memo } from 'react';
import { Container, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import SendEmail from './components/sendEmail';
import { PageTitle } from '@/components';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
return (
<>
<PageTitle title={t('change_email', { keyPrefix: 'page_title' })} />
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3>
<Col className="mx-auto" md={3}>
<SendEmail />
</Col>
</Container>
</>
);
};
export default memo(Index);

View File

@ -3,7 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { changeEmailVerify } from '@answer/api';
import { changeEmailVerify, getUserInfo } from '@answer/api';
import { userInfoStore } from '@answer/stores';
import { PageTitle } from '@/components';
@ -12,6 +13,8 @@ const Index: FC = () => {
const [searchParams] = useSearchParams();
const [step, setStep] = useState('loading');
const updateUser = userInfoStore((state) => state.update);
useEffect(() => {
const code = searchParams.get('code');
if (code) {
@ -19,6 +22,10 @@ const Index: FC = () => {
changeEmailVerify({ code })
.then(() => {
setStep('success');
getUserInfo().then((res) => {
// update user info
updateUser(res);
});
})
.catch(() => {
setStep('invalid');

View File

@ -19,10 +19,10 @@ const Index: FC<Props> = ({ data }) => {
<div className="d-flex mb-4">
{data?.status !== 'deleted' ? (
<Link to={`/users/${data.username}`} reloadDocument>
<Avatar avatar={data.avatar} size="160px" />
<Avatar avatar={data.avatar} size="160px" searchStr="s=128" />
</Link>
) : (
<Avatar avatar={data.avatar} size="160px" />
<Avatar avatar={data.avatar} size="160px" searchStr="s=128" />
)}
<div className="ms-4">

View File

@ -3,6 +3,7 @@ import { Form, Button } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next';
import { marked } from 'marked';
import MD5 from 'md5';
import { modifyUserInfo, uploadAvatar, getUserInfo } from '@answer/api';
import type { FormDataType } from '@answer/common/interface';
@ -16,6 +17,9 @@ const Index: React.FC = () => {
});
const toast = useToast();
const { user, update } = userInfoStore();
const [mailHash, setMailHash] = useState('');
const [count, setCount] = useState(0);
const [formData, setFormData] = useState<FormDataType>({
display_name: {
value: '',
@ -28,6 +32,9 @@ const Index: React.FC = () => {
errorMsg: '',
},
avatar: {
type: 'default',
gravatar: '',
custom: '',
value: '',
isInvalid: false,
errorMsg: '',
@ -59,7 +66,9 @@ const Index: React.FC = () => {
setFormData({
...formData,
avatar: {
value: res,
...formData.avatar,
type: 'custom',
custom: res,
isInvalid: false,
errorMsg: '',
},
@ -136,7 +145,11 @@ const Index: React.FC = () => {
const params = {
display_name: formData.display_name.value,
username: formData.username.value,
avatar: formData.avatar.value,
avatar: {
type: formData.avatar.type,
gravatar: formData.avatar.gravatar,
custom: formData.avatar.custom,
},
bio: formData.bio.value,
website: formData.website.value,
location: formData.location.value,
@ -168,13 +181,25 @@ const Index: React.FC = () => {
formData.display_name.value = res.display_name;
formData.username.value = res.username;
formData.bio.value = res.bio;
formData.avatar.value = res.avatar;
formData.avatar.type = res.avatar.type || 'default';
formData.avatar.gravatar = res.avatar.gravatar;
formData.avatar.custom = res.avatar.custom;
formData.location.value = res.location;
formData.website.value = res.website;
setFormData({ ...formData });
if (res.e_mail) {
const str = res.e_mail.toLowerCase().trim();
const hash = MD5(str);
console.log(str, hash, mailHash);
setMailHash(hash);
}
});
};
const refreshGravatar = () => {
setCount((pre) => pre + 1);
};
useEffect(() => {
getProfile();
}, []);
@ -227,39 +252,118 @@ const Index: React.FC = () => {
<Form.Group className="mb-3">
<Form.Label>{t('avatar.label')}</Form.Label>
<div className="d-flex align-items-center">
<Avatar
size="128px"
avatar={formData.avatar.value}
className="me-3 rounded"
<div className="mb-2">
<Form.Check
inline
type="radio"
label={t('avatar.gravatar')}
className="mb-0"
checked={formData.avatar.type === 'gravatar'}
onChange={() =>
handleChange({
avatar: {
...formData.avatar,
type: 'gravatar',
gravatar: `https://www.gravatar.com/avatar/${mailHash}`,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Check
inline
type="radio"
label={t('avatar.custom')}
className="mb-0"
checked={formData.avatar.type === 'custom'}
onChange={() =>
handleChange({
avatar: {
...formData.avatar,
type: 'custom',
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Check
inline
type="radio"
label={t('avatar.default')}
className="mb-0"
checked={formData.avatar.type === 'default'}
onChange={() =>
handleChange({
avatar: {
...formData.avatar,
type: 'default',
isInvalid: false,
errorMsg: '',
},
})
}
/>
</div>
<div className="d-flex align-items-center">
{formData.avatar.type === 'gravatar' && (
<>
<Avatar
size="128px"
avatar={formData.avatar.gravatar}
searchStr={`s=128&d=identicon&t=${
new Date().valueOf() + count
}`}
className="me-3 rounded"
/>
<div>
<Button
variant="outline-secondary"
className="mb-2"
onClick={refreshGravatar}>
{t('avatar.btn_refresh')}
</Button>
<div>
<Form.Text className="text-muted mt-0">
<Trans i18nKey="settings.profile.gravatar_text">
You can change your image on{' '}
<a
href="https://gravatar.com"
target="_blank"
rel="noreferrer">
gravatar.com
</a>
</Trans>
</Form.Text>
</div>
</div>
</>
)}
<div>
<UploadImg type="avatar" upload={avatarUpload} />
<div>
<Form.Text className="text-muted mt-0">
<Trans i18nKey="settings.profile.avatar.text">
You can upload your image or
<a
href="@/pages/Users/Settings/Profile/index##"
onClick={(e) => {
e.preventDefault();
handleChange({
avatar: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
}}>
reset
</a>
it to
</Trans>
<a href="https://gravatar.com"> gravatar.com</a>
</Form.Text>
</div>
</div>
{formData.avatar.type === 'custom' && (
<>
<Avatar
size="128px"
searchStr="s=128"
avatar={formData.avatar.custom}
className="me-3 rounded"
/>
<div>
<UploadImg type="avatar" upload={avatarUpload} />
<div>
<Form.Text className="text-muted mt-0">
<Trans i18nKey="settings.profile.avatar.text">
You can upload your image.
</Trans>
</Form.Text>
</div>
</div>
</>
)}
{formData.avatar.type === 'default' && (
<Avatar size="128px" avatar="" className="me-3 rounded" />
)}
</div>
</Form.Group>

View File

@ -114,6 +114,10 @@ const routeConfig: RouteNode[] = [
path: 'users/account-recovery',
page: 'pages/Users/AccountForgot',
},
{
path: 'users/change-email',
page: 'pages/Users/ChangeEmail',
},
{
path: 'users/password-reset',
page: 'pages/Users/PasswordReset',