diff --git a/packages/concordia-app/public/locales/en/translation.json b/packages/concordia-app/public/locales/en/translation.json index 443ce20..61d79a1 100644 --- a/packages/concordia-app/public/locales/en/translation.json +++ b/packages/concordia-app/public/locales/en/translation.json @@ -3,17 +3,28 @@ "board.sub.header.no.topics.guest": "Sign up and be the first to post.", "board.sub.header.no.topics.user": "Be the first to post.", "custom.loading.tab.pane.default.generic.message": "Magic in the background", + "edit.information.modal.form.cancel.button": "Cancel", + "edit.information.modal.form.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.", + "edit.information.modal.form.error.message.header": "Form contains errors", + "edit.information.modal.form.location.field.label": "Location", + "edit.information.modal.form.location.field.placeholder": "Location", + "edit.information.modal.form.profile.picture.field.label": "Profile picture URL", + "edit.information.modal.form.profile.picture.field.placeholder": "URL", + "edit.information.modal.form.submit.button": "Submit", + "edit.information.modal.title": "Edit profile information", "post.create.form.send.button": "Post", "post.form.content.field.placeholder": "Message", "post.form.subject.field.placeholder": "Subject", "post.list.row.post.id": "#{{id}}", "profile.general.tab.address.row.title": "Account address:", + "profile.general.tab.edit.info.button.title": "Edit information", "profile.general.tab.location.row.not.set": "Not set", "profile.general.tab.location.row.title": "Location:", "profile.general.tab.number.of.posts.row.title": "Number of posts:", "profile.general.tab.number.of.topics.row.title": "Number of topics created:", "profile.general.tab.posts.db.address.row.title": "PostsDB:", "profile.general.tab.registration.date.row.title": "Member since:", + "profile.general.tab.save.info.button.title": "Save information", "profile.general.tab.title": "General", "profile.general.tab.topics.db.address.row.title": "TopicsDB:", "profile.general.tab.user.db.address.row.title": "UserDB:", @@ -39,10 +50,7 @@ "register.form.sign.up.step.button.submit": "Sign Up", "register.form.sign.up.step.description": "Create a Concordia account", "register.form.sign.up.step.error.message.header": "Form contains errors", - "register.form.sign.up.step.error.username.taken.message": "The username {{username}} is already taken.", "register.form.sign.up.step.title": "Sign Up", - "register.form.sign.up.step.username.field.label": "Username", - "register.form.sign.up.step.username.field.placeholder": "Username", "register.p.account.address": "Account address:", "topbar.button.create.topic": "Create topic", "topbar.button.profile": "Profile", @@ -54,5 +62,9 @@ "topic.create.form.subject.field.placeholder": "Subject", "topic.list.row.author": "by {{author}}", "topic.list.row.number.of.replies": "{{numberOfReplies}} replies", - "topic.list.row.topic.id": "#{{id}}" -} + "topic.list.row.topic.id": "#{{id}}", + "username.selector.error.username.empty.message": "Username is required", + "username.selector.error.username.taken.message": "The username {{username}} is already taken.", + "username.selector.username.field.label": "Username", + "username.selector.username.field.placeholder": "Username" +} \ No newline at end of file diff --git a/packages/concordia-app/src/components/UsernameSelector.jsx b/packages/concordia-app/src/components/UsernameSelector.jsx new file mode 100644 index 0000000..530cd7a --- /dev/null +++ b/packages/concordia-app/src/components/UsernameSelector.jsx @@ -0,0 +1,104 @@ +import React, { useCallback, useEffect, useMemo } from 'react'; +import { + Form, Input, +} from 'semantic-ui-react'; +import throttle from 'lodash/throttle'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { drizzle } from '../redux/store'; +import { FORUM_CONTRACT } from '../constants/contracts/ContractNames'; + +const { contracts: { [FORUM_CONTRACT]: { methods: { isUserNameTaken } } } } = drizzle; + +const UsernameSelector = (props) => { + const { + initialUsername, username, onChangeCallback, onErrorChangeCallback, + } = props; + const isUserNameTakenResults = useSelector((state) => state.contracts[FORUM_CONTRACT].isUserNameTaken); + const { t } = useTranslation(); + + useEffect(() => { + if (username.length > 0) { + const checkedUsernames = Object + .values(isUserNameTakenResults) + .map((callCompleted) => ({ + checkedUsername: callCompleted.args[0], + isTaken: callCompleted.value, + })); + + const checkedUsername = checkedUsernames + .find((callCompleted) => callCompleted.checkedUsername === username); + + if (checkedUsername && checkedUsername.isTaken && username !== initialUsername) { + onErrorChangeCallback({ + usernameChecked: true, + error: true, + errorMessage: t('username.selector.error.username.taken.message', { username }), + }); + } else { + onErrorChangeCallback({ + usernameChecked: checkedUsername !== undefined, + error: false, + errorMessage: null, + }); + } + + return; + } + + // Username input is empty + if (initialUsername && initialUsername !== '') { + onErrorChangeCallback({ + usernameChecked: true, + error: true, + errorMessage: t('username.selector.error.username.empty.message'), + }); + } else { + onErrorChangeCallback({ + usernameChecked: true, + error: false, + errorMessage: null, + }); + } + }, [initialUsername, isUserNameTakenResults, onErrorChangeCallback, t, username, username.length]); + + const checkUsernameTaken = useMemo(() => throttle( + (usernameToCheck) => { + isUserNameTaken.cacheCall(usernameToCheck); + }, 200, + ), []); + + const handleInputChange = useCallback((event, { value }) => { + onChangeCallback(value); + + if (value.length > 0) { + checkUsernameTaken(value); + } + }, [checkUsernameTaken, onChangeCallback]); + + return ( + + + + + ); +}; + +UsernameSelector.propTypes = { + initialUsername: PropTypes.string, + username: PropTypes.string.isRequired, + onChangeCallback: PropTypes.func.isRequired, + onErrorChangeCallback: PropTypes.func.isRequired, +}; + +export default UsernameSelector; diff --git a/packages/concordia-app/src/views/Profile/GeneralTab/EditInformationModal/index.jsx b/packages/concordia-app/src/views/Profile/GeneralTab/EditInformationModal/index.jsx new file mode 100644 index 0000000..1290c7e --- /dev/null +++ b/packages/concordia-app/src/views/Profile/GeneralTab/EditInformationModal/index.jsx @@ -0,0 +1,235 @@ +import React, { + useCallback, useEffect, useMemo, useState, +} from 'react'; +import { + Button, Form, Icon, Image, Input, Message, Modal, +} from 'semantic-ui-react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import checkUrlValid from '../../../../utils/urlUtils'; +import { USER_LOCATION, USER_PROFILE_PICTURE } from '../../../../constants/orbit/UserDatabaseKeys'; +import { USER_DATABASE } from '../../../../constants/orbit/OrbitDatabases'; +import { breeze, drizzle } from '../../../../redux/store'; +import UsernameSelector from '../../../../components/UsernameSelector'; +import { FORUM_CONTRACT } from '../../../../constants/contracts/ContractNames'; + +const { orbit: { stores } } = breeze; +const { contracts: { [FORUM_CONTRACT]: { methods: { updateUsername } } } } = drizzle; + +const EditInformationModal = (props) => { + const { + initialUsername, initialAuthorAvatar, initialUserLocation, open, onSubmit, onCancel, + } = props; + const [usernameInput, setUsernameInput] = useState(initialUsername); + const [usernameChecked, setUsernameChecked] = useState(true); + const [profilePictureInput, setProfilePictureInput] = useState(''); + const [profilePictureUrlValid, setProfilePictureUrlValid] = useState(true); + const [locationInput, setLocationInput] = useState(''); + const [error, setError] = useState(false); + const [errorMessages, setErrorMessages] = useState([]); + const [usernameError, setUsernameError] = useState(false); + const [usernameErrorMessage, setUsernameErrorMessage] = useState(''); + const { t } = useTranslation(); + + useEffect(() => { + setLocationInput(initialUserLocation || ''); + }, [initialUserLocation]); + + useEffect(() => { + setProfilePictureInput(initialAuthorAvatar || ''); + setProfilePictureUrlValid(initialAuthorAvatar ? checkUrlValid(initialAuthorAvatar) : true); + }, [initialAuthorAvatar]); + + useEffect(() => { + let formHasError = false; + const formErrors = []; + + if (!profilePictureUrlValid) { + formHasError = true; + formErrors.push(t('edit.information.modal.form.error.invalid.profile.picture.url.message')); + } + + setError(formHasError); + setErrorMessages(formErrors); + }, [profilePictureUrlValid, t]); + + const handleUsernameChange = (modifiedUsername) => { + setUsernameInput(modifiedUsername); + }; + + const handleUsernameErrorChange = useCallback(({ + usernameChecked: isUsernameChecked, + error: hasUsernameError, + errorMessage, + }) => { + setUsernameChecked(isUsernameChecked); + + if (hasUsernameError) { + setUsernameError(true); + setUsernameErrorMessage(errorMessage); + } else { + setUsernameError(false); + } + }, []); + + const handleInputChange = useCallback((event, { name, value }) => { + if (name === 'profilePictureInput') { + setProfilePictureInput(value); + + if (value.length > 0) { + setProfilePictureUrlValid(checkUrlValid(value)); + } else { + setProfilePictureUrlValid(true); + } + } + + if (name === 'locationInput') { + setLocationInput(value); + } + }, []); + + const profilePicture = useMemo(() => (profilePictureInput.length > 0 && profilePictureUrlValid + ? () + : () + ), [profilePictureInput, profilePictureUrlValid]); + + const handleSubmit = useCallback(() => { + const keyValuesToStore = []; + + keyValuesToStore.push({ + key: USER_PROFILE_PICTURE, + value: profilePictureInput, + }); + + keyValuesToStore.push({ + key: USER_LOCATION, + value: locationInput, + }); + + const userDb = Object.values(stores).find((store) => store.dbname === USER_DATABASE); + + const promiseArray = keyValuesToStore + .map((keyValueToStore) => { + if (keyValueToStore.value !== '') { + return userDb + .put(keyValueToStore.key, keyValueToStore.value, { pin: true }); + } + + return userDb.del(keyValueToStore.key); + }); + + Promise + .all(promiseArray) + .then(() => { + // TODO: display a message + }) + .catch((reason) => { + console.log(reason); + }); + + if (usernameInput !== initialUsername) { + updateUsername.cacheSend(usernameInput); + } + + onSubmit(); + }, [initialUsername, locationInput, onSubmit, profilePictureInput, usernameInput]); + + return useMemo(() => ( + + {t('edit.information.modal.title')} + + {profilePicture} + +
+ + + + + + + + + + + {error === true && ( + errorMessages + .map((errorMessage) => ( + + )) + )} + {usernameError === true && ( + + )} +
+
+ + +