diff --git a/packages/concordia-app/public/locales/en/translation.json b/packages/concordia-app/public/locales/en/translation.json
index c4834b6..d466c8b 100644
--- a/packages/concordia-app/public/locales/en/translation.json
+++ b/packages/concordia-app/public/locales/en/translation.json
@@ -6,13 +6,25 @@
"post.list.row.post.id": "#{{id}}",
"register.card.header": "Sign Up",
"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.username.field.label": "Username",
- "register.form.username.field.placeholder": "Username",
+ "register.form.personal.information.step.button.skip": "Skip for now",
+ "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:",
"topbar.button.create.topic": "Create topic",
"topbar.button.profile": "Profile",
diff --git a/packages/concordia-app/src/App.jsx b/packages/concordia-app/src/App.jsx
index 7ed70c0..d3b3e3e 100644
--- a/packages/concordia-app/src/App.jsx
+++ b/packages/concordia-app/src/App.jsx
@@ -19,6 +19,7 @@ const App = ({ store }) => (
);
App.propTypes = {
+ // eslint-disable-next-line react/forbid-prop-types
store: PropTypes.object.isRequired,
};
diff --git a/packages/concordia-app/src/constants/RegisterSteps.js b/packages/concordia-app/src/constants/RegisterSteps.js
new file mode 100644
index 0000000..3e39afe
--- /dev/null
+++ b/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';
diff --git a/packages/concordia-app/src/constants/TransactionStatus.js b/packages/concordia-app/src/constants/TransactionStatus.js
new file mode 100644
index 0000000..3ca1dca
--- /dev/null
+++ b/packages/concordia-app/src/constants/TransactionStatus.js
@@ -0,0 +1,2 @@
+export const TRANSACTION_SUCCESS = 'success';
+export const TRANSACTION_ERROR = 'error';
diff --git a/packages/concordia-app/src/constants/UserDatabaseKeys.js b/packages/concordia-app/src/constants/UserDatabaseKeys.js
new file mode 100644
index 0000000..91b7bd7
--- /dev/null
+++ b/packages/concordia-app/src/constants/UserDatabaseKeys.js
@@ -0,0 +1,2 @@
+export const PROFILE_PICTURE = 'profile_picture';
+export const LOCATION = 'location';
diff --git a/packages/concordia-app/src/redux/sagas/orbitSaga.js b/packages/concordia-app/src/redux/sagas/orbitSaga.js
index 543a80a..749f498 100644
--- a/packages/concordia-app/src/redux/sagas/orbitSaga.js
+++ b/packages/concordia-app/src/redux/sagas/orbitSaga.js
@@ -10,7 +10,8 @@ import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider
function* initOrbitDatabases(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() {
diff --git a/packages/concordia-app/src/utils/urlUtils.js b/packages/concordia-app/src/utils/urlUtils.js
new file mode 100644
index 0000000..84351d5
--- /dev/null
+++ b/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;
diff --git a/packages/concordia-app/src/views/Register/PersonalInformationStep/index.jsx b/packages/concordia-app/src/views/Register/PersonalInformationStep/index.jsx
new file mode 100644
index 0000000..fb72557
--- /dev/null
+++ b/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
+ ? (
+
+
+
+ )
+ : 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 (
+ <>
+
+
+
+
+
+
+ {profilePicture}
+
+
+
+
+
+
+
+ {error === true && (
+
+ {errorMessages
+ .map((errorMessage) => (
+
+ ))}
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+PersonalInformationStep.propTypes = {
+ pushNextStep: PropTypes.func.isRequired,
+};
+
+export default PersonalInformationStep;
diff --git a/packages/concordia-app/src/views/Register/PersonalInformationStep/styles.css b/packages/concordia-app/src/views/Register/PersonalInformationStep/styles.css
new file mode 100644
index 0000000..b9ba50c
--- /dev/null
+++ b/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;
+}
diff --git a/packages/concordia-app/src/views/Register/SignUpStep/index.jsx b/packages/concordia-app/src/views/Register/SignUpStep/index.jsx
new file mode 100644
index 0000000..006ce04
--- /dev/null
+++ b/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 (
+ <>
+
+
+
+
+
+
+
+
+
+ {error === true && (
+
+
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+SignUpStep.propTypes = {
+ pushNextStep: PropTypes.func.isRequired,
+};
+
+export default SignUpStep;
diff --git a/packages/concordia-app/src/views/Register/index.jsx b/packages/concordia-app/src/views/Register/index.jsx
index fe9ccd5..ef5e36e 100644
--- a/packages/concordia-app/src/views/Register/index.jsx
+++ b/packages/concordia-app/src/views/Register/index.jsx
@@ -1,184 +1,120 @@
-import React, {
- useCallback, useContext, useEffect, useMemo, useState,
-} from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import {
- Button, Card, Form, Header, Input, Message,
+ Button, Card, Header, Icon, Step,
} 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 AppContext from '../../components/AppContext';
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 { account } = props;
-
- const {
- drizzle: {
- contracts: {
- Forum: {
- methods: { isUserNameTaken, signUp },
- },
- },
- },
- } = useContext(AppContext.Context);
-
+const Register = () => {
+ const [currentStep, setCurrentStep] = useState('signup');
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);
+ const goToHomePage = useCallback(() => history.push('/'), [history]);
- if (checkedUsername && checkedUsername.isTaken) {
- setUsernameIsTaken(true);
- setError(true);
- setErrorMessage(t('register.form.error.username.taken.message', { username: usernameInput }));
- } else {
- setUsernameIsTaken(false);
- setError(false);
- }
+ const pushNextStep = useCallback(() => {
+ if (currentStep === 'signup') {
+ setCurrentStep('profile-information');
}
- }, [isUserNameTakenResults, t, usernameInput]);
- useEffect(() => {
- if (signingUp && transactionStack && transactionStack[registerCacheSendStackId]
- && 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?
- }
+ if (currentStep === 'profile-information') {
+ goToHomePage();
}
- }, [registerCacheSendStackId, signingUp, transactions, transactionStack, history]);
-
- const checkUsernameTaken = useMemo(() => throttle(
- (username) => {
- isUserNameTaken.cacheCall(username);
- }, 200,
- ), [isUserNameTaken]);
+ }, [currentStep, goToHomePage]);
- const handleInputChange = useCallback((event, { value }) => {
- setUsernameInput(value);
-
- if (value.length > 0) {
- checkUsernameTaken(value);
+ const activeStep = useMemo(() => {
+ if (currentStep === REGISTER_STEP_SIGNUP) {
+ return (
+
+ );
}
- }, [checkUsernameTaken]);
- const handleSubmit = useCallback(() => {
- if (user.hasSignedUp) {
- signUp.cacheSend(usernameInput);
- } else {
- setSigningUp(true);
- setRegisterCacheSendStackId(signUp.cacheSend(...[usernameInput], { from: account }));
+ if (currentStep === REGISTER_STEP_PROFILE_INFORMATION) {
+ return (
+
+ );
}
- }, [account, signUp, user.hasSignedUp, usernameInput]);
- const goToHomePage = React.useCallback(() => history.push('/'), [history]);
+ return null;
+ }, [currentStep, pushNextStep]);
return (
- Sign Up
+ {
+ !user.hasSignedUp && (
+
+
+
+
+
+
+ {t('register.form.sign.up.step.title')}
+
+
+ {t('register.form.sign.up.step.description')}
+
+
+
+
+
+
+
+ {t('register.form.profile.information.step.title')}
+
+
+ {t('register.form.profile.information.step.description')}
+
+
+
+
+
+ )
+ }
{t('register.p.account.address')}
{user.address}
- {user.hasSignedUp
- ? (
-
-
- {t('register.form.header.already.member.message')}
-
-
- )
- : (
-
-
-
-
-
- )}
- {error === true && (
-
-
-
- )}
-
- {user.hasSignedUp
- ? (
-
- )
- : (
- <>
+ {user.hasSignedUp
+ ? (
+ <>
+
+
+
+
+ {t('register.form.header.already.member.message')}
+
+
+
+
+
-
- >
- )}
-
+
+ >
+ )
+ : activeStep}
);