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