mirror of https://gitee.com/answerdev/answer.git
Merge branch 'ui-0.2' into 'main'
Ui 0.2 See merge request opensource/answer!130
This commit is contained in:
commit
810fbe8ae3
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -53,7 +53,7 @@ a {
|
|||
height: 18px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
left: 15px;
|
||||
top: 0;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ const Users: FC = () => {
|
|||
data={user}
|
||||
className="fs-6"
|
||||
avatarSize="24px"
|
||||
avatarSearchStr="s=48"
|
||||
/>
|
||||
</td>
|
||||
<td>{user.rank}</td>
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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');
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue