mirror of https://gitlab.com/ecentrics/concordia
Apostolos Fanakis
4 years ago
6 changed files with 515 additions and 135 deletions
@ -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 ( |
||||
|
<Form.Field required> |
||||
|
<label htmlFor="form-field-username-selector"> |
||||
|
{t('username.selector.username.field.label')} |
||||
|
</label> |
||||
|
<Input |
||||
|
id="form-field-username-selector" |
||||
|
placeholder={t('username.selector.username.field.placeholder')} |
||||
|
name="usernameInput" |
||||
|
className="form-input" |
||||
|
value={username} |
||||
|
onChange={handleInputChange} |
||||
|
/> |
||||
|
</Form.Field> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
UsernameSelector.propTypes = { |
||||
|
initialUsername: PropTypes.string, |
||||
|
username: PropTypes.string.isRequired, |
||||
|
onChangeCallback: PropTypes.func.isRequired, |
||||
|
onErrorChangeCallback: PropTypes.func.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default UsernameSelector; |
@ -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 |
||||
|
? (<Image size="medium" src={profilePictureInput} wrapped />) |
||||
|
: (<Icon name="user circle" size="massive" inverted color="black" />) |
||||
|
), [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(() => ( |
||||
|
<Modal |
||||
|
onClose={onCancel} |
||||
|
open={open} |
||||
|
> |
||||
|
<Modal.Header>{t('edit.information.modal.title')}</Modal.Header> |
||||
|
<Modal.Content image> |
||||
|
{profilePicture} |
||||
|
<Modal.Description> |
||||
|
<Form> |
||||
|
<UsernameSelector |
||||
|
initialUsername={initialUsername} |
||||
|
username={usernameInput} |
||||
|
onChangeCallback={handleUsernameChange} |
||||
|
onErrorChangeCallback={handleUsernameErrorChange} |
||||
|
/> |
||||
|
<Form.Field> |
||||
|
<label htmlFor="form-edit-information-field-profile-picture"> |
||||
|
{t('edit.information.modal.form.profile.picture.field.label')} |
||||
|
</label> |
||||
|
<Input |
||||
|
id="form-edit-information-field-profile-picture" |
||||
|
placeholder={t('edit.information.modal.form.profile.picture.field.placeholder')} |
||||
|
name="profilePictureInput" |
||||
|
className="form-input" |
||||
|
value={profilePictureInput} |
||||
|
onChange={handleInputChange} |
||||
|
/> |
||||
|
</Form.Field> |
||||
|
<Form.Field> |
||||
|
<label htmlFor="form-edit-information-field-location"> |
||||
|
{t('edit.information.modal.form.location.field.label')} |
||||
|
</label> |
||||
|
<Input |
||||
|
id="form-edit-information-field-location" |
||||
|
placeholder={t('edit.information.modal.form.location.field.placeholder')} |
||||
|
name="locationInput" |
||||
|
className="form-input" |
||||
|
value={locationInput} |
||||
|
onChange={handleInputChange} |
||||
|
/> |
||||
|
</Form.Field> |
||||
|
</Form> |
||||
|
{error === true && ( |
||||
|
errorMessages |
||||
|
.map((errorMessage) => ( |
||||
|
<Message |
||||
|
error |
||||
|
header={t('edit.information.modal.form.error.message.header')} |
||||
|
content={errorMessage} |
||||
|
/> |
||||
|
)) |
||||
|
)} |
||||
|
{usernameError === true && ( |
||||
|
<Message |
||||
|
error |
||||
|
header={t('edit.information.modal.form.error.message.header')} |
||||
|
content={usernameErrorMessage} |
||||
|
/> |
||||
|
)} |
||||
|
</Modal.Description> |
||||
|
</Modal.Content> |
||||
|
<Modal.Actions> |
||||
|
<Button color="black" onClick={onCancel}> |
||||
|
{t('edit.information.modal.form.cancel.button')} |
||||
|
</Button> |
||||
|
<Button |
||||
|
content={t('edit.information.modal.form.submit.button')} |
||||
|
labelPosition="right" |
||||
|
icon="checkmark" |
||||
|
onClick={handleSubmit} |
||||
|
positive |
||||
|
loading={!usernameChecked} |
||||
|
disabled={!usernameChecked || error || usernameError} |
||||
|
/> |
||||
|
</Modal.Actions> |
||||
|
</Modal> |
||||
|
), [ |
||||
|
error, errorMessages, handleInputChange, handleSubmit, handleUsernameErrorChange, initialUsername, locationInput, |
||||
|
onCancel, open, profilePicture, profilePictureInput, t, usernameChecked, usernameError, usernameErrorMessage, |
||||
|
usernameInput, |
||||
|
]); |
||||
|
}; |
||||
|
|
||||
|
EditInformationModal.defaultProps = { |
||||
|
open: false, |
||||
|
}; |
||||
|
|
||||
|
EditInformationModal.propTypes = { |
||||
|
profileAddress: PropTypes.string.isRequired, |
||||
|
initialUsername: PropTypes.string.isRequired, |
||||
|
initialAuthorAvatar: PropTypes.string, |
||||
|
initialUserLocation: PropTypes.string, |
||||
|
open: PropTypes.bool, |
||||
|
onSubmit: PropTypes.func.isRequired, |
||||
|
onCancel: PropTypes.func.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default EditInformationModal; |
Loading…
Reference in new issue