-
{t('common.email')}
+
{t('common.emailOrUsername')}
{
}
switch (error.message) {
- case 'User is already exist':
+ case 'Email already in use':
return {
type: 'error',
- content: 'common.userIsAlreadyExist',
+ content: 'common.emailAlreadyInUse',
};
- case 'Current password is not valid':
+ case 'Invalid current password':
return {
type: 'error',
content: 'common.invalidCurrentPassword',
@@ -83,11 +83,11 @@ const EditEmailStep = React.memo(
if (wasSubmitting && !isSubmitting) {
if (error) {
switch (error.message) {
- case 'User is already exist':
+ case 'Email already in use':
emailField.current.select();
break;
- case 'Current password is not valid':
+ case 'Invalid current password':
setData((prevData) => ({
...prevData,
currentPassword: '',
diff --git a/client/src/components/UserPopup/EditPasswordStep.jsx b/client/src/components/UserPopup/EditPasswordStep.jsx
index 50f46cb6..ea1d622a 100644
--- a/client/src/components/UserPopup/EditPasswordStep.jsx
+++ b/client/src/components/UserPopup/EditPasswordStep.jsx
@@ -15,7 +15,7 @@ const createMessage = (error) => {
}
switch (error.message) {
- case 'Current password is not valid':
+ case 'Invalid current password':
return {
type: 'error',
content: 'common.invalidCurrentPassword',
@@ -67,7 +67,7 @@ const EditPasswordStep = React.memo(
if (wasSubmitting && !isSubmitting) {
if (!error) {
onClose();
- } else if (error.message === 'Current password is not valid') {
+ } else if (error.message === 'Invalid current password') {
setData((prevData) => ({
...prevData,
currentPassword: '',
diff --git a/client/src/components/UserPopup/EditUsernameStep.jsx b/client/src/components/UserPopup/EditUsernameStep.jsx
new file mode 100644
index 00000000..00c8709f
--- /dev/null
+++ b/client/src/components/UserPopup/EditUsernameStep.jsx
@@ -0,0 +1,182 @@
+import React, { useCallback, useEffect, useMemo, useRef } from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Message } from 'semantic-ui-react';
+import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
+import { Input, Popup } from '../../lib/custom-ui';
+
+import { useForm } from '../../hooks';
+import { isUsername } from '../../utils/validator';
+
+import styles from './EditUsernameStep.module.css';
+
+const createMessage = (error) => {
+ if (!error) {
+ return error;
+ }
+
+ switch (error.message) {
+ case 'Username already in use':
+ return {
+ type: 'error',
+ content: 'common.usernameAlreadyInUse',
+ };
+ case 'Invalid current password':
+ return {
+ type: 'error',
+ content: 'common.invalidCurrentPassword',
+ };
+ default:
+ return {
+ type: 'warning',
+ content: 'common.unknownError',
+ };
+ }
+};
+
+const EditUsernameStep = React.memo(
+ ({ defaultData, username, isSubmitting, error, onUpdate, onMessageDismiss, onBack, onClose }) => {
+ const [t] = useTranslation();
+ const wasSubmitting = usePrevious(isSubmitting);
+
+ const [data, handleFieldChange, setData] = useForm({
+ username: '',
+ currentPassword: '',
+ ...defaultData,
+ });
+
+ const message = useMemo(() => createMessage(error), [error]);
+ const [focusCurrentPasswordFieldState, focusCurrentPasswordField] = useToggle();
+
+ const usernameField = useRef(null);
+ const currentPasswordField = useRef(null);
+
+ const handleSubmit = useCallback(() => {
+ const cleanData = {
+ ...data,
+ username: data.username.trim() || null,
+ };
+
+ if (cleanData.username && !isUsername(cleanData.username)) {
+ usernameField.current.select();
+ return;
+ }
+
+ if (cleanData.username === username) {
+ onClose();
+ return;
+ }
+
+ if (!cleanData.currentPassword) {
+ currentPasswordField.current.focus();
+ return;
+ }
+
+ onUpdate(cleanData);
+ }, [username, onUpdate, onClose, data]);
+
+ useEffect(() => {
+ usernameField.current.select();
+ }, []);
+
+ useEffect(() => {
+ if (wasSubmitting && !isSubmitting) {
+ if (error) {
+ switch (error.message) {
+ case 'Username already in use':
+ usernameField.current.select();
+
+ break;
+ case 'Invalid current password':
+ setData((prevData) => ({
+ ...prevData,
+ currentPassword: '',
+ }));
+ focusCurrentPasswordField();
+
+ break;
+ default:
+ }
+ } else {
+ onClose();
+ }
+ }
+ }, [isSubmitting, wasSubmitting, error, onClose, setData, focusCurrentPasswordField]);
+
+ useDidUpdate(() => {
+ currentPasswordField.current.focus();
+ }, [focusCurrentPasswordFieldState]);
+
+ return (
+ <>
+
+ {t('common.editUsername', {
+ context: 'title',
+ })}
+
+
+ {message && (
+
+ )}
+
+
+ >
+ );
+ },
+);
+
+EditUsernameStep.propTypes = {
+ defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+ username: PropTypes.string,
+ isSubmitting: PropTypes.bool.isRequired,
+ error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+ onUpdate: PropTypes.func.isRequired,
+ onMessageDismiss: PropTypes.func.isRequired,
+ onBack: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+EditUsernameStep.defaultProps = {
+ username: undefined,
+ error: undefined,
+};
+
+export default EditUsernameStep;
diff --git a/client/src/components/UserPopup/EditUsernameStep.module.css b/client/src/components/UserPopup/EditUsernameStep.module.css
new file mode 100644
index 00000000..72114a1d
--- /dev/null
+++ b/client/src/components/UserPopup/EditUsernameStep.module.css
@@ -0,0 +1,10 @@
+.field {
+ margin-bottom: 8px;
+}
+
+.text {
+ color: #444444;
+ font-size: 12px;
+ font-weight: bold;
+ padding-bottom: 6px;
+}
diff --git a/client/src/components/UserPopup/UserPopup.jsx b/client/src/components/UserPopup/UserPopup.jsx
index 34db3a38..bd624355 100755
--- a/client/src/components/UserPopup/UserPopup.jsx
+++ b/client/src/components/UserPopup/UserPopup.jsx
@@ -7,6 +7,7 @@ import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import EditNameStep from './EditNameStep';
+import EditUsernameStep from './EditUsernameStep';
import EditAvatarStep from './EditAvatarStep';
import EditEmailStep from './EditEmailStep';
import EditPasswordStep from './EditPasswordStep';
@@ -15,6 +16,7 @@ import styles from './UserPopup.module.css';
const StepTypes = {
EDIT_NAME: 'EDIT_NAME',
+ EDIT_USERNAME: 'EDIT_USERNAME',
EDIT_AVATAR: 'EDIT_AVATAR',
EDIT_EMAIL: 'EDIT_EMAIL',
EDIT_PASSWORD: 'EDIT_PASSWORD',
@@ -24,12 +26,16 @@ const UserStep = React.memo(
({
email,
name,
+ username,
avatar,
isAvatarUploading,
+ usernameUpdateForm,
emailUpdateForm,
passwordUpdateForm,
onUpdate,
onAvatarUpload,
+ onUsernameUpdate,
+ onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
@@ -48,6 +54,10 @@ const UserStep = React.memo(
openStep(StepTypes.EDIT_AVATAR);
}, [openStep]);
+ const handleUsernameEditClick = useCallback(() => {
+ openStep(StepTypes.EDIT_USERNAME);
+ }, [openStep]);
+
const handleEmailEditClick = useCallback(() => {
openStep(StepTypes.EDIT_EMAIL);
}, [openStep]);
@@ -93,6 +103,19 @@ const UserStep = React.memo(
onBack={handleBack}
/>
);
+ case StepTypes.EDIT_USERNAME:
+ return (
+
+ );
case StepTypes.EDIT_EMAIL:
return (
- {name}
+
+ {t('common.userActions', {
+ context: 'title',
+ })}
+