Browse Source

Add steps to register, Add personal information to user database

develop
Apostolos Fanakis 4 years ago
parent
commit
1a5b629522
  1. 24
      packages/concordia-app/public/locales/en/translation.json
  2. 1
      packages/concordia-app/src/App.jsx
  3. 2
      packages/concordia-app/src/constants/RegisterSteps.js
  4. 2
      packages/concordia-app/src/constants/TransactionStatus.js
  5. 2
      packages/concordia-app/src/constants/UserDatabaseKeys.js
  6. 3
      packages/concordia-app/src/redux/sagas/orbitSaga.js
  7. 12
      packages/concordia-app/src/utils/urlUtils.js
  8. 175
      packages/concordia-app/src/views/Register/PersonalInformationStep/index.jsx
  9. 7
      packages/concordia-app/src/views/Register/PersonalInformationStep/styles.css
  10. 152
      packages/concordia-app/src/views/Register/SignUpStep/index.jsx
  11. 202
      packages/concordia-app/src/views/Register/index.jsx

24
packages/concordia-app/public/locales/en/translation.json

@ -6,13 +6,25 @@
"post.list.row.post.id": "#{{id}}", "post.list.row.post.id": "#{{id}}",
"register.card.header": "Sign Up", "register.card.header": "Sign Up",
"register.form.button.back": "Back", "register.form.button.back": "Back",
"register.form.button.guest": "Continue as guest",
"register.form.button.submit": "Sign Up",
"register.form.error.message.header": "Form contains errors",
"register.form.error.username.taken.message": "The username {{username}} is already taken.",
"register.form.header.already.member.message": "There is already an account for this address.\nIf you want to create another account please change your address.", "register.form.header.already.member.message": "There is already an account for this address.\nIf you want to create another account please change your address.",
"register.form.username.field.label": "Username", "register.form.personal.information.step.button.skip": "Skip for now",
"register.form.username.field.placeholder": "Username", "register.form.personal.information.step.button.submit": "Submit",
"register.form.personal.information.step.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.",
"register.form.personal.information.step.error.message.header": "Form contains errors",
"register.form.personal.information.step.location.field.label": "Location",
"register.form.personal.information.step.location.field.placeholder": "Location",
"register.form.personal.information.step.profile.picture.field.label": "Profile picture URL",
"register.form.personal.information.step.profile.picture.field.placeholder": "URL",
"register.form.profile.information.step.description": "Give a hint about who you are",
"register.form.profile.information.step.title": "Profile Information",
"register.form.sign.up.step.button.guest": "Continue as guest",
"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:", "register.p.account.address": "Account address:",
"topbar.button.create.topic": "Create topic", "topbar.button.create.topic": "Create topic",
"topbar.button.profile": "Profile", "topbar.button.profile": "Profile",

1
packages/concordia-app/src/App.jsx

@ -19,6 +19,7 @@ const App = ({ store }) => (
); );
App.propTypes = { App.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
}; };

2
packages/concordia-app/src/constants/RegisterSteps.js

@ -0,0 +1,2 @@
export const REGISTER_STEP_SIGNUP = 'signup';
export const REGISTER_STEP_PROFILE_INFORMATION = 'profile-information';

2
packages/concordia-app/src/constants/TransactionStatus.js

@ -0,0 +1,2 @@
export const TRANSACTION_SUCCESS = 'success';
export const TRANSACTION_ERROR = 'error';

2
packages/concordia-app/src/constants/UserDatabaseKeys.js

@ -0,0 +1,2 @@
export const PROFILE_PICTURE = 'profile_picture';
export const LOCATION = 'location';

3
packages/concordia-app/src/redux/sagas/orbitSaga.js

@ -10,7 +10,8 @@ import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider
function* initOrbitDatabases(action) { function* initOrbitDatabases(action) {
const { account, breeze } = action; const { account, breeze } = action;
yield put(breezeActions.orbit.orbitInit(breeze, account + EthereumContractIdentityProvider.contractAddress)); // same as breeze.initOrbit(account); // same as breeze.initOrbit(account);
yield put(breezeActions.orbit.orbitInit(breeze, account + EthereumContractIdentityProvider.contractAddress));
} }
function* orbitSaga() { function* orbitSaga() {

12
packages/concordia-app/src/utils/urlUtils.js

@ -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;

175
packages/concordia-app/src/views/Register/PersonalInformationStep/index.jsx

@ -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;

7
packages/concordia-app/src/views/Register/PersonalInformationStep/styles.css

@ -0,0 +1,7 @@
.register-form-profile-picture-wrapper {
text-align: center;
}
.register-form-profile-picture {
max-height: 30vh;
}

152
packages/concordia-app/src/views/Register/SignUpStep/index.jsx

@ -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;

202
packages/concordia-app/src/views/Register/index.jsx

@ -1,184 +1,120 @@
import React, { import React, { useCallback, useMemo, useState } from 'react';
useCallback, useContext, useEffect, useMemo, useState,
} from 'react';
import { import {
Button, Card, Form, Header, Input, Message, Button, Card, Header, Icon, Step,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import throttle from 'lodash/throttle';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import AppContext from '../../components/AppContext';
import './styles.css'; import './styles.css';
import SignUpStep from './SignUpStep';
import PersonalInformationStep from './PersonalInformationStep';
import { REGISTER_STEP_PROFILE_INFORMATION, REGISTER_STEP_SIGNUP } from '../../constants/RegisterSteps';
const Register = (props) => { const Register = () => {
const { account } = props; const [currentStep, setCurrentStep] = useState('signup');
const {
drizzle: {
contracts: {
Forum: {
methods: { isUserNameTaken, signUp },
},
},
},
} = useContext(AppContext.Context);
const user = useSelector((state) => state.user); 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 history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { const goToHomePage = useCallback(() => history.push('/'), [history]);
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) { const pushNextStep = useCallback(() => {
setUsernameIsTaken(true); if (currentStep === 'signup') {
setError(true); setCurrentStep('profile-information');
setErrorMessage(t('register.form.error.username.taken.message', { username: usernameInput }));
} else {
setUsernameIsTaken(false);
setError(false);
} }
}
}, [isUserNameTakenResults, t, usernameInput]);
useEffect(() => { if (currentStep === 'profile-information') {
if (signingUp && transactionStack && transactionStack[registerCacheSendStackId] goToHomePage();
&& transactions[transactionStack[registerCacheSendStackId]]) {
if (transactions[transactionStack[registerCacheSendStackId]].status === 'error') {
setSigningUp(false);
} else if (transactions[transactionStack[registerCacheSendStackId]].status === 'success') {
history.push('/');
// TODO: display a welcome message?
}
} }
}, [registerCacheSendStackId, signingUp, transactions, transactionStack, history]); }, [currentStep, goToHomePage]);
const checkUsernameTaken = useMemo(() => throttle(
(username) => {
isUserNameTaken.cacheCall(username);
}, 200,
), [isUserNameTaken]);
const handleInputChange = useCallback((event, { value }) => { const activeStep = useMemo(() => {
setUsernameInput(value); if (currentStep === REGISTER_STEP_SIGNUP) {
return (
if (value.length > 0) { <SignUpStep pushNextStep={pushNextStep} />
checkUsernameTaken(value); );
} }
}, [checkUsernameTaken]);
const handleSubmit = useCallback(() => { if (currentStep === REGISTER_STEP_PROFILE_INFORMATION) {
if (user.hasSignedUp) { return (
signUp.cacheSend(usernameInput); <PersonalInformationStep pushNextStep={pushNextStep} />
} else { );
setSigningUp(true);
setRegisterCacheSendStackId(signUp.cacheSend(...[usernameInput], { from: account }));
} }
}, [account, signUp, user.hasSignedUp, usernameInput]);
const goToHomePage = React.useCallback(() => history.push('/'), [history]); return null;
}, [currentStep, pushNextStep]);
return ( return (
<div className="centered form-card-container"> <div className="centered form-card-container">
<Card fluid> <Card fluid>
<Card.Content> <Card.Content>
<Card.Header>Sign Up</Card.Header> {
!user.hasSignedUp && (
<Card.Header>
<Step.Group>
<Step
key="register-form-step-signup"
active={currentStep === REGISTER_STEP_SIGNUP}
>
<Icon name="signup" />
<Step.Content>
<Step.Title>
{t('register.form.sign.up.step.title')}
</Step.Title>
<Step.Description>
{t('register.form.sign.up.step.description')}
</Step.Description>
</Step.Content>
</Step>
<Step
key="register-form-step-profile-information"
active={currentStep === REGISTER_STEP_PROFILE_INFORMATION}
>
<Icon name="user circle" />
<Step.Content>
<Step.Title>
{t('register.form.profile.information.step.title')}
</Step.Title>
<Step.Description>
{t('register.form.profile.information.step.description')}
</Step.Description>
</Step.Content>
</Step>
</Step.Group>
</Card.Header>
)
}
<Card.Description> <Card.Description>
<p> <p>
<strong>{t('register.p.account.address')}</strong> <strong>{t('register.p.account.address')}</strong>
&nbsp; &nbsp;
{user.address} {user.address}
</p> </p>
</Card.Description>
</Card.Content>
{user.hasSignedUp {user.hasSignedUp
? ( ? (
<>
<Card.Content>
<Card.Description>
<div> <div>
<Header as="h4" className="i18next-newlines"> <Header as="h4" className="i18next-newlines">
{t('register.form.header.already.member.message')} {t('register.form.header.already.member.message')}
</Header> </Header>
</div> </div>
)
: (
<Form loading={signingUp}>
<Form.Field required>
<label htmlFor="form-register-field-username">
{t('register.form.username.field.label')}
</label>
<Input
id="form-register-field-username"
placeholder={t('register.form.username.field.placeholder')}
name="usernameInput"
className="form-input"
value={usernameInput}
onChange={handleInputChange}
/>
</Form.Field>
</Form>
)}
</Card.Description> </Card.Description>
</Card.Content> </Card.Content>
{error === true && (
<Card.Content extra>
<Message
error
header={t('register.form.error.message.header')}
content={errorMessage}
/>
</Card.Content>
)}
<Card.Content extra> <Card.Content extra>
{user.hasSignedUp
? (
<Button <Button
color="black" color="black"
floated="right" floated="right"
content={t('register.form.button.back')} content={t('register.form.button.back')}
onClick={goToHomePage} onClick={goToHomePage}
/> />
)
: (
<>
<Button
color="green"
floated="right"
content={t('register.form.button.submit')}
onClick={handleSubmit}
disabled={usernameIsTaken || signingUp}
loading={!usernameIsChecked}
/>
<Button
color="violet"
floated="right"
basic
content={t('register.form.button.guest')}
onClick={goToHomePage}
disabled={signingUp}
/>
</>
)}
</Card.Content> </Card.Content>
</>
)
: activeStep}
</Card> </Card>
</div> </div>
); );

Loading…
Cancel
Save