diff --git a/client/src/connectors/InviteForm.connect.js b/client/src/connectors/InviteForm.connect.js new file mode 100644 index 000000000..83c5b7699 --- /dev/null +++ b/client/src/connectors/InviteForm.connect.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { submitInvite, submitSendInvite } from 'store/Invite/invite.action'; + +export const mapStateToProps = (state, props) => { + return {}; +}; + +export const mapDispatchToProps = (dispatch) => ({ + requestSubmitInvite: (form, token) => dispatch(submitInvite({ form, token })), + requestSendInvite: (form) => dispatch(submitSendInvite({ form })), +}); + +export default connect(mapStateToProps, mapDispatchToProps); diff --git a/client/src/connectors/RegisterForm.connect.js b/client/src/connectors/RegisterForm.connect.js new file mode 100644 index 000000000..a82e601ac --- /dev/null +++ b/client/src/connectors/RegisterForm.connect.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import { submitRegister } from 'store/registers/register.action'; + +export const mapStateToProps = (state, props) => { + return {}; +}; + +export const mapDispatchToProps = (dispatch) => ({ + requestSubmitRegister: (form) => dispatch(submitRegister({ form })), +}); + +export default connect(mapStateToProps, mapDispatchToProps); diff --git a/client/src/connectors/ResetPassword.connect.js b/client/src/connectors/ResetPassword.connect.js new file mode 100644 index 000000000..5dc209556 --- /dev/null +++ b/client/src/connectors/ResetPassword.connect.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { submitResetPassword } from 'store/resetPassword/resetPassword.action'; + +export const mapStateToProps = (state, props) => { + return {}; +}; + +export const mapDispatchToProps = (dispatch) => ({ + requestResetPassword: (password) => + dispatch(submitResetPassword({password})), +}); + +export default connect(mapStateToProps, mapDispatchToProps); diff --git a/client/src/containers/Authentication/Invite.js b/client/src/containers/Authentication/Invite.js new file mode 100644 index 000000000..7837881e9 --- /dev/null +++ b/client/src/containers/Authentication/Invite.js @@ -0,0 +1,183 @@ +import React, { useEffect, useMemo } from 'react'; +import * as Yup from 'yup'; +import { useFormik } from 'formik'; +import { useIntl } from 'react-intl'; +import ErrorMessage from 'components/ErrorMessage'; +import AppToaster from 'components/AppToaster'; +import { compose } from 'utils'; +import InviteFormConnect from 'connectors/InviteForm.connect'; +import { useParams } from 'react-router-dom'; +import { + Button, + InputGroup, + Intent, + FormGroup, + HTMLSelect, +} from '@blueprintjs/core'; + +function Invite({ requestSubmitInvite }) { + const intl = useIntl(); + let params = useParams('accept/:token'); + + const { token } = params; + + const phoneRegExp = /^((\\+[1-9]{1,4}[ \\-]*)|(\\([0-9]{2,3}\\)[ \\-]*)|([0-9]{2,4})[ \\-]*)*?[0-9]{3,4}?[ \\-]*[0-9]{3,4}?$/; + + const language = useMemo( + () => [ + { value: null, label: 'Select Country' }, + { value: 'Arabic', label: 'Arabic' }, + { value: 'English', label: 'English' }, + ], + [] + ); + const ValidationSchema = Yup.object().shape({ + first_name: Yup.string().required(intl.formatMessage({ id: 'required' })), + + last_name: Yup.string().required(intl.formatMessage({ id: 'required' })), + + email: Yup.string() + .email() + .required(intl.formatMessage({ id: 'required' })), + phone_number: Yup.string() + .matches(phoneRegExp) + .required(intl.formatMessage({ id: 'required' })), + language: Yup.string().required( + intl.formatMessage({ + id: 'required', + }) + ), + password: Yup.string() + .min(4, 'Password has to be longer than 4 characters!') + .required('Password is required!'), + }); + + const initialValues = useMemo( + () => ({ + first_name: '', + last_name: '', + email: '', + phone_number: '', + language: '', + password: '', + }), + [] + ); + const formik = useFormik({ + enableReinitialize: true, + validationSchema: ValidationSchema, + initialValues: { + ...initialValues, + }, + onSubmit: (values, { setSubmitting }) => { + requestSubmitInvite(values, token) + .then((response) => { + AppToaster.show({ + message: 'success', + }); + setSubmitting(false); + }) + .catch((error) => { + setSubmitting(false); + }); + }, + }); + + const { errors, values, touched } = useMemo(() => formik, [formik]); + const requiredSpan = useMemo(() => *, []); + + return ( +
+
+ } + > + + + } + > + + + + } + > + + + + } + > + + + + } + > + + + } + > + + + + +
+
+ ); +} + +export default compose(InviteFormConnect)(Invite); diff --git a/client/src/containers/Authentication/Register.js b/client/src/containers/Authentication/Register.js new file mode 100644 index 000000000..3189db91a --- /dev/null +++ b/client/src/containers/Authentication/Register.js @@ -0,0 +1,201 @@ +import React, { useEffect, useMemo } from 'react'; +import * as Yup from 'yup'; +import { useFormik } from 'formik'; +import { useIntl } from 'react-intl'; +import { + Button, + InputGroup, + Intent, + FormGroup, + HTMLSelect, +} from '@blueprintjs/core'; + +import RegisterFromConnect from 'connectors/RegisterForm.connect'; +import ErrorMessage from 'components/ErrorMessage'; +import AppToaster from 'components/AppToaster'; +import { compose } from 'utils'; + +function Register({ requestSubmitRegister }) { + const intl = useIntl(); + + const phoneRegExp = /^((\\+[1-9]{1,4}[ \\-]*)|(\\([0-9]{2,3}\\)[ \\-]*)|([0-9]{2,4})[ \\-]*)*?[0-9]{3,4}?[ \\-]*[0-9]{3,4}?$/; + + const Country = useMemo( + () => [ + { value: null, label: 'Select Country' }, + { value: 'libya', label: 'Libya' }, + ], + [] + ); + + const ValidationSchema = Yup.object().shape({ + organization_name: Yup.string().required( + intl.formatMessage({ id: 'required' }) + ), + first_name: Yup.string().required(intl.formatMessage({ id: 'required' })), + + last_name: Yup.string().required(intl.formatMessage({ id: 'required' })), + email: Yup.string() + .email() + .required(intl.formatMessage({ id: 'required' })), + phone_number: Yup.string() + .matches(phoneRegExp) + .required(intl.formatMessage({ id: 'required' })), + password: Yup.string() + .min(4, 'Password has to be longer than 8 characters!') + .required('Password is required!'), + country: Yup.string().required(intl.formatMessage({ id: 'required' })), + }); + + const initialValues = useMemo( + () => ({ + organization_name: '', + first_name: '', + last_name: '', + email: '', + phone_number: '', + password: '', + country: '', + }), + [] + ); + + const formik = useFormik({ + enableReinitialize: true, + validationSchema: ValidationSchema, + initialValues: { + ...initialValues, + }, + onSubmit: (values, { setSubmitting }) => { + requestSubmitRegister(values) + .then((response) => { + AppToaster.show({ + message: 'success', + }); + setSubmitting(false); + }) + .catch((error) => { + setSubmitting(false); + }); + }, + }); + + const { errors, values, touched } = useMemo(() => formik, [formik]); + const requiredSpan = useMemo(() => *, []); + + return ( +
+
+ } + > + + + + } + > + + + } + > + + + + } + > + + + + } + > + + + } + > + + + + } + > + + + + +
+
+ ); +} + +export default compose(RegisterFromConnect)(Register); diff --git a/client/src/containers/Authentication/ResetPassword.js b/client/src/containers/Authentication/ResetPassword.js index 7abe691b6..421143528 100644 --- a/client/src/containers/Authentication/ResetPassword.js +++ b/client/src/containers/Authentication/ResetPassword.js @@ -1,29 +1,106 @@ -import * as React from "react"; -import { Link } from 'react-router-dom'; -import {Button, InputGroup} from "@blueprintjs/core"; -import { FormattedMessage } from 'react-intl'; +import React, { useEffect, useMemo } from 'react'; +import * as Yup from 'yup'; +import { useFormik } from 'formik'; +import { useIntl } from 'react-intl'; +import { + Button, + InputGroup, + Intent, + FormGroup, + HTMLSelect, +} from '@blueprintjs/core'; +import ErrorMessage from 'components/ErrorMessage'; +import AppToaster from 'components/AppToaster'; +import { compose } from 'utils'; +import SendResetPasswordConnect from 'connectors/ResetPassword.connect'; + +function ResetPassword({ requestSendResetPassword }) { + const intl = useIntl(); + const ValidationSchema = Yup.object().shape({ + password: Yup.string() + .min(4, 'Password has to be longer than 4 characters!') + .required('Password is required!'), + + confirm_password: Yup.string() + .oneOf([Yup.ref('password'), null], 'Passwords must match') + .required('Confirm Password is required'), + }); + + const initialValues = useMemo( + () => ({ + password: '', + confirm_password: '', + }), + [] + ); + const formik = useFormik({ + enableReinitialize: true, + validationSchema: ValidationSchema, + initialValues: { + ...initialValues, + }, + onSubmit: (values, { setSubmitting }) => { + requestSendResetPassword(values.password) + .then((response) => { + AppToaster.show({ + message: 'success', + }); + setSubmitting(false); + }) + .catch((error) => { + setSubmitting(false); + }); + }, + }); + + const { errors, values, touched } = useMemo(() => formik, [formik]); + const requiredSpan = useMemo(() => *, []); -export default function Login() { return ( -
-
- } - large={true} - className="input-group--email" - /> - - +
+ + } + > + + + } + > + + + - -
- ) -} \ No newline at end of file + ); +} + +export default compose(SendResetPasswordConnect)(ResetPassword); diff --git a/client/src/containers/Authentication/SendInvite.js b/client/src/containers/Authentication/SendInvite.js new file mode 100644 index 000000000..f64ea237f --- /dev/null +++ b/client/src/containers/Authentication/SendInvite.js @@ -0,0 +1,81 @@ +import React, { useEffect, useMemo } from 'react'; +import * as Yup from 'yup'; +import { useFormik } from 'formik'; +import { useIntl } from 'react-intl'; +import ErrorMessage from 'components/ErrorMessage'; +import AppToaster from 'components/AppToaster'; +import InviteFormConnect from 'connectors/InviteForm.connect'; + +import { compose } from 'utils'; +import { + Button, + InputGroup, + Intent, + FormGroup, + HTMLSelect, +} from '@blueprintjs/core'; + +function SendInvite({ requestSendInvite }) { + const intl = useIntl(); + const ValidationSchema = Yup.object().shape({ + email: Yup.string() + .email() + .required(intl.formatMessage({ id: 'required' })), + }); + + const initialValues = useMemo( + () => ({ + email: '', + }), + [] + ); + + const formik = useFormik({ + enableReinitialize: true, + validationSchema: ValidationSchema, + initialValues: { + ...initialValues, + }, + onSubmit: (values, { setSubmitting }) => { + requestSendInvite(values) + .then((response) => { + AppToaster.show({ + message: 'success', + }); + setSubmitting(false); + }) + .catch((error) => { + setSubmitting(false); + }); + }, + }); + + const { errors, values, touched } = useMemo(() => formik, [formik]); + const requiredSpan = useMemo(() => *, []); + + return ( +
+
+ } + > + + + +
+
+ ); +} + +export default compose(InviteFormConnect)(SendInvite); diff --git a/client/src/containers/Authentication/SendResetPassword.js b/client/src/containers/Authentication/SendResetPassword.js new file mode 100644 index 000000000..d4b78f2f0 --- /dev/null +++ b/client/src/containers/Authentication/SendResetPassword.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { Button, InputGroup } from '@blueprintjs/core'; +import { FormattedMessage } from 'react-intl'; + +export default function SendResetPassword() { + return ( + + ); +} diff --git a/client/src/routes/authentication.js b/client/src/routes/authentication.js index eb8d2a367..9972fe0f8 100644 --- a/client/src/routes/authentication.js +++ b/client/src/routes/authentication.js @@ -7,14 +7,44 @@ export default [ path: `${BASE_URL}/login`, name: 'auth.login', component: LazyLoader({ - loader: () => import('containers/Authentication/Login') + loader: () => import('containers/Authentication/Login'), }), }, { - path: `${BASE_URL}/reset_password`, + path: `${BASE_URL}/register`, + name: 'auth.register', + component: LazyLoader({ + loader: () => import('containers/Authentication/Register'), + }), + }, + + { + path: `${BASE_URL}/send_reset_password`, name: 'auth.reset_password', component: LazyLoader({ - loader: () => import('containers/Authentication/ResetPassword') + loader: () => import('containers/Authentication/SendResetPassword'), }), - } -]; \ No newline at end of file + }, + + { + path: `${BASE_URL}/reset_password`, + name: 'auth.send.reset_password', + component: LazyLoader({ + loader: () => import('containers/Authentication/ResetPassword'), + }), + }, + { + path: `${BASE_URL}/send_invite`, + name: 'auth.send_invite', + component: LazyLoader({ + loader: () => import('containers/Authentication/SendInvite'), + }), + }, + { + path: `${BASE_URL}/invite/:token`, + name: 'auth.invite', + component: LazyLoader({ + loader: () => import('containers/Authentication/Invite'), + }), + }, +]; diff --git a/client/src/store/Invite/invite.action.js b/client/src/store/Invite/invite.action.js new file mode 100644 index 000000000..d0bae2d6f --- /dev/null +++ b/client/src/store/Invite/invite.action.js @@ -0,0 +1,14 @@ + +import ApiService from 'services/ApiService'; + +export const submitInvite = ({ form, token }) => { + return (dispatch) => { + return ApiService.post(`/invite/accept/${token}`, { ...form }); + }; +}; + +export const submitSendInvite = ({ form }) => { + return (dispatch) => { + return ApiService.post('invite', { form }); + }; +}; diff --git a/client/src/store/registers/register.action.js b/client/src/store/registers/register.action.js new file mode 100644 index 000000000..251ab02bd --- /dev/null +++ b/client/src/store/registers/register.action.js @@ -0,0 +1,7 @@ +import ApiService from 'services/ApiService'; + +export const submitRegister = ({ form }) => { + return (dispatch) => { + return ApiService.post('auth/register', { ...form }); + }; +}; diff --git a/client/src/store/registers/register.reducer.js b/client/src/store/registers/register.reducer.js new file mode 100644 index 000000000..89e08bca3 --- /dev/null +++ b/client/src/store/registers/register.reducer.js @@ -0,0 +1,20 @@ +import { createReducer } from '@reduxjs/toolkit'; +import t from 'store/types'; + +const initialState = { + registers: {}, +}; + +export default createReducer(initialState, { + [t.REGISTER_SET]: (state, action) => { + const _registers = {}; + + action.registers.forEach((register) => { + _registers[register.id] = register; + }); + state.registers = { + ...state.registers, + ..._registers, + }; + }, +}); diff --git a/client/src/store/registers/register.type.js b/client/src/store/registers/register.type.js new file mode 100644 index 000000000..17a53e1c0 --- /dev/null +++ b/client/src/store/registers/register.type.js @@ -0,0 +1,4 @@ +export default { + REGISTER_SET: 'REGISTER_SUCCESS', + REGISTER_CLEAR_ERRORS: 'REGISTER_CLEAR_ERRORS', +}; diff --git a/client/src/store/resetPassword/resetPassword.action.js b/client/src/store/resetPassword/resetPassword.action.js new file mode 100644 index 000000000..e55cbdd9d --- /dev/null +++ b/client/src/store/resetPassword/resetPassword.action.js @@ -0,0 +1,7 @@ +import ApiService from 'services/ApiService'; + +export const submitResetPassword = (password) => { + return (dispatch) => { + return ApiService.post('auth/reset_password', password); + }; +}; diff --git a/client/src/store/types.js b/client/src/store/types.js index ef00f5c29..ed4bbad1b 100644 --- a/client/src/store/types.js +++ b/client/src/store/types.js @@ -14,6 +14,7 @@ import financialStatements from './financialStatement/financialStatements.types' import itemCategories from './itemCategories/itemsCategory.type'; import settings from './settings/settings.type'; import search from './search/search.type'; +import register from './registers/register.type'; export default { ...authentication, @@ -32,4 +33,5 @@ export default { ...settings, ...accounting, ...search, + ...register, };