mirror of https://gitlab.com/ecentrics/concordia
Apostolos Fanakis
4 years ago
11 changed files with 453 additions and 151 deletions
@ -0,0 +1,2 @@ |
|||
export const REGISTER_STEP_SIGNUP = 'signup'; |
|||
export const REGISTER_STEP_PROFILE_INFORMATION = 'profile-information'; |
@ -0,0 +1,2 @@ |
|||
export const TRANSACTION_SUCCESS = 'success'; |
|||
export const TRANSACTION_ERROR = 'error'; |
@ -0,0 +1,2 @@ |
|||
export const PROFILE_PICTURE = 'profile_picture'; |
|||
export const LOCATION = 'location'; |
@ -0,0 +1,12 @@ |
|||
const checkUrlValid = (url) => { |
|||
const pattern = new RegExp('^(https?:\\/\\/)?' // protocol
|
|||
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' // domain name
|
|||
+ '((\\d{1,3}\\.){3}\\d{1,3}))' // OR ip (v4) address
|
|||
+ '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' // port and path
|
|||
+ '(\\?[;&a-z\\d%_.~+=-]*)?' // query string
|
|||
+ '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator
|
|||
|
|||
return !!pattern.test(url); |
|||
}; |
|||
|
|||
export default checkUrlValid; |
@ -0,0 +1,175 @@ |
|||
import React, { |
|||
useCallback, useEffect, useMemo, useState, |
|||
} from 'react'; |
|||
import { |
|||
Button, Card, Form, Image, Input, Message, |
|||
} from 'semantic-ui-react'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useHistory } from 'react-router'; |
|||
import checkUrlValid from '../../../utils/urlUtils'; |
|||
import { breeze } from '../../../redux/store'; |
|||
import './styles.css'; |
|||
import { USER_DATABASE } from '../../../constants/OrbitDatabases'; |
|||
import { LOCATION, PROFILE_PICTURE } from '../../../constants/UserDatabaseKeys'; |
|||
|
|||
const { orbit: { stores } } = breeze; |
|||
|
|||
const PersonalInformationStep = (props) => { |
|||
const { pushNextStep } = props; |
|||
const [profilePictureInput, setProfilePictureInput] = useState(''); |
|||
const [profilePictureUrlValid, setProfilePictureUrlValid] = useState(true); |
|||
const [locationInput, setLocationInput] = useState(''); |
|||
const [error, setError] = useState(false); |
|||
const [errorMessages, setErrorMessages] = useState([]); |
|||
const history = useHistory(); |
|||
const { t } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
let formHasError = false; |
|||
const formErrors = []; |
|||
|
|||
if (!profilePictureUrlValid) { |
|||
formHasError = true; |
|||
formErrors.push(t('register.form.personal.information.step.error.invalid.profile.picture.url.message')); |
|||
} |
|||
|
|||
setError(formHasError); |
|||
setErrorMessages(formErrors); |
|||
}, [profilePictureUrlValid, t]); |
|||
|
|||
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 |
|||
? ( |
|||
<div className="register-form-profile-picture-wrapper"> |
|||
<Image rounded src={profilePictureInput} className="register-form-profile-picture" /> |
|||
</div> |
|||
) |
|||
: null |
|||
), [profilePictureInput, profilePictureUrlValid]); |
|||
|
|||
const handleSubmit = useCallback(() => { |
|||
if (error) { |
|||
return; |
|||
} |
|||
|
|||
const keyValuesToStore = []; |
|||
|
|||
if (profilePictureInput.length > 0) { |
|||
keyValuesToStore.push({ |
|||
key: PROFILE_PICTURE, |
|||
value: profilePictureInput, |
|||
}); |
|||
} |
|||
|
|||
if (locationInput.length > 0) { |
|||
keyValuesToStore.push({ |
|||
key: LOCATION, |
|||
value: locationInput, |
|||
}); |
|||
} |
|||
|
|||
if (keyValuesToStore.length > 0) { |
|||
const userDb = Object.values(stores).find((store) => store.dbname === USER_DATABASE); |
|||
|
|||
keyValuesToStore |
|||
.reduce((acc, keyValueToStore) => acc |
|||
.then(() => userDb |
|||
.put(keyValueToStore.key, keyValueToStore.value, { pin: true })), |
|||
Promise.resolve()) |
|||
.then(() => pushNextStep()) |
|||
.catch((reason) => { |
|||
console.log(reason); |
|||
}); |
|||
} |
|||
}, [error, locationInput, profilePictureInput, pushNextStep]); |
|||
|
|||
const goToHomePage = () => history.push('/'); |
|||
|
|||
return ( |
|||
<> |
|||
<Card.Content> |
|||
<Card.Description> |
|||
<Form> |
|||
<Form.Field> |
|||
<label htmlFor="form-register-field-profile-picture"> |
|||
{t('register.form.personal.information.step.profile.picture.field.label')} |
|||
</label> |
|||
<Input |
|||
id="form-register-field-profile-picture" |
|||
placeholder={t('register.form.personal.information.step.profile.picture.field.placeholder')} |
|||
name="profilePictureInput" |
|||
className="form-input" |
|||
value={profilePictureInput} |
|||
onChange={handleInputChange} |
|||
/> |
|||
</Form.Field> |
|||
{profilePicture} |
|||
<Form.Field> |
|||
<label htmlFor="form-register-field-location"> |
|||
{t('register.form.personal.information.step.location.field.label')} |
|||
</label> |
|||
<Input |
|||
id="form-register-field-location" |
|||
placeholder={t('register.form.personal.information.step.location.field.placeholder')} |
|||
name="locationInput" |
|||
className="form-input" |
|||
value={locationInput} |
|||
onChange={handleInputChange} |
|||
/> |
|||
</Form.Field> |
|||
</Form> |
|||
</Card.Description> |
|||
</Card.Content> |
|||
{error === true && ( |
|||
<Card.Content extra> |
|||
{errorMessages |
|||
.map((errorMessage) => ( |
|||
<Message |
|||
error |
|||
header={t('register.form.personal.information.step.error.message.header')} |
|||
content={errorMessage} |
|||
/> |
|||
))} |
|||
</Card.Content> |
|||
)} |
|||
<Card.Content extra> |
|||
<Button |
|||
color="green" |
|||
floated="right" |
|||
content={t('register.form.personal.information.step.button.submit')} |
|||
onClick={handleSubmit} |
|||
disabled={!profilePictureUrlValid} |
|||
/> |
|||
<Button |
|||
color="violet" |
|||
floated="right" |
|||
basic |
|||
content={t('register.form.personal.information.step.button.skip')} |
|||
onClick={goToHomePage} |
|||
/> |
|||
</Card.Content> |
|||
</> |
|||
); |
|||
}; |
|||
|
|||
PersonalInformationStep.propTypes = { |
|||
pushNextStep: PropTypes.func.isRequired, |
|||
}; |
|||
|
|||
export default PersonalInformationStep; |
@ -0,0 +1,7 @@ |
|||
.register-form-profile-picture-wrapper { |
|||
text-align: center; |
|||
} |
|||
|
|||
.register-form-profile-picture { |
|||
max-height: 30vh; |
|||
} |
@ -0,0 +1,152 @@ |
|||
import React, { |
|||
useCallback, useEffect, useMemo, useState, |
|||
} from 'react'; |
|||
import { |
|||
Button, Card, Form, Input, Message, |
|||
} from 'semantic-ui-react'; |
|||
import throttle from 'lodash/throttle'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { useSelector } from 'react-redux'; |
|||
import { useHistory } from 'react-router'; |
|||
import PropTypes from 'prop-types'; |
|||
import { drizzle } from '../../../redux/store'; |
|||
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../../constants/TransactionStatus'; |
|||
|
|||
const { contracts: { Forum: { methods: { isUserNameTaken, signUp } } } } = drizzle; |
|||
|
|||
const SignUpStep = (props) => { |
|||
const { pushNextStep, account } = props; |
|||
const user = useSelector((state) => state.user); |
|||
const isUserNameTakenResults = useSelector((state) => state.contracts.Forum.isUserNameTaken); |
|||
const transactionStack = useSelector((state) => state.transactionStack); |
|||
const transactions = useSelector((state) => state.transactions); |
|||
const [usernameInput, setUsernameInput] = useState(''); |
|||
const [usernameIsChecked, setUsernameIsChecked] = useState(true); |
|||
const [usernameIsTaken, setUsernameIsTaken] = useState(true); |
|||
const [error, setError] = useState(false); |
|||
const [errorMessage, setErrorMessage] = useState(''); |
|||
const [signingUp, setSigningUp] = useState(false); |
|||
const [registerCacheSendStackId, setRegisterCacheSendStackId] = useState(''); |
|||
|
|||
const history = useHistory(); |
|||
const { t } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
if (usernameInput.length > 0) { |
|||
const checkedUsernames = Object |
|||
.values(isUserNameTakenResults) |
|||
.map((callCompleted) => ({ |
|||
checkedUsername: callCompleted.args[0], |
|||
isTaken: callCompleted.value, |
|||
})); |
|||
|
|||
const checkedUsername = checkedUsernames |
|||
.find((callCompleted) => callCompleted.checkedUsername === usernameInput); |
|||
|
|||
setUsernameIsChecked(checkedUsername !== undefined); |
|||
|
|||
if (checkedUsername && checkedUsername.isTaken) { |
|||
setUsernameIsTaken(true); |
|||
setError(true); |
|||
setErrorMessage(t('register.form.sign.up.step.error.username.taken.message', { username: usernameInput })); |
|||
} else { |
|||
setUsernameIsTaken(false); |
|||
setError(false); |
|||
} |
|||
} |
|||
}, [isUserNameTakenResults, t, usernameInput]); |
|||
|
|||
useEffect(() => { |
|||
if (signingUp && transactionStack && transactionStack[registerCacheSendStackId] |
|||
&& transactions[transactionStack[registerCacheSendStackId]]) { |
|||
if (transactions[transactionStack[registerCacheSendStackId]].status === TRANSACTION_ERROR) { |
|||
setSigningUp(false); |
|||
} else if (transactions[transactionStack[registerCacheSendStackId]].status === TRANSACTION_SUCCESS) { |
|||
pushNextStep(); |
|||
// TODO: display a welcome message? |
|||
} |
|||
} |
|||
}, [pushNextStep, registerCacheSendStackId, signingUp, transactionStack, transactions]); |
|||
|
|||
const checkUsernameTaken = useMemo(() => throttle( |
|||
(username) => { |
|||
isUserNameTaken.cacheCall(username); |
|||
}, 200, |
|||
), []); |
|||
|
|||
const handleInputChange = useCallback((event, { value }) => { |
|||
setUsernameInput(value); |
|||
|
|||
if (value.length > 0) { |
|||
checkUsernameTaken(value); |
|||
} |
|||
}, [checkUsernameTaken]); |
|||
|
|||
const handleSubmit = useCallback(() => { |
|||
if (user.hasSignedUp) { |
|||
signUp.cacheSend(usernameInput); |
|||
} else { |
|||
setSigningUp(true); |
|||
setRegisterCacheSendStackId(signUp.cacheSend(...[usernameInput], { from: account })); |
|||
} |
|||
}, [account, user.hasSignedUp, usernameInput]); |
|||
|
|||
const goToHomePage = () => history.push('/'); |
|||
|
|||
return ( |
|||
<> |
|||
<Card.Content> |
|||
<Card.Description> |
|||
<Form loading={signingUp}> |
|||
<Form.Field required> |
|||
<label htmlFor="form-register-field-username"> |
|||
{t('register.form.sign.up.step.username.field.label')} |
|||
</label> |
|||
<Input |
|||
id="form-register-field-username" |
|||
placeholder={t('register.form.sign.up.step.username.field.placeholder')} |
|||
name="usernameInput" |
|||
className="form-input" |
|||
value={usernameInput} |
|||
onChange={handleInputChange} |
|||
/> |
|||
</Form.Field> |
|||
</Form> |
|||
</Card.Description> |
|||
</Card.Content> |
|||
{error === true && ( |
|||
<Card.Content extra> |
|||
<Message |
|||
error |
|||
header={t('register.form.sign.up.step.error.message.header')} |
|||
content={errorMessage} |
|||
/> |
|||
</Card.Content> |
|||
)} |
|||
<Card.Content extra> |
|||
<Button |
|||
color="green" |
|||
floated="right" |
|||
content={t('register.form.sign.up.step.button.submit')} |
|||
onClick={handleSubmit} |
|||
disabled={usernameIsTaken || signingUp} |
|||
loading={!usernameIsChecked} |
|||
/> |
|||
<Button |
|||
color="violet" |
|||
floated="right" |
|||
basic |
|||
content={t('register.form.sign.up.step.button.guest')} |
|||
onClick={goToHomePage} |
|||
disabled={signingUp} |
|||
/> |
|||
</Card.Content> |
|||
</> |
|||
); |
|||
}; |
|||
|
|||
SignUpStep.propTypes = { |
|||
pushNextStep: PropTypes.func.isRequired, |
|||
}; |
|||
|
|||
export default SignUpStep; |
Loading…
Reference in new issue