From e9fa0a7e519647137b3be2ba2c22fa932e238420 Mon Sep 17 00:00:00 2001 From: Dwayne Harris Date: Thu, 19 Sep 2019 00:08:00 -0400 Subject: [PATCH] WIP --- src/actions/registration.ts | 60 +++++++++++++++-- src/api/fetch.ts | 21 ++++-- src/api/groups.ts | 16 +++++ src/api/registration.ts | 20 ++++++ src/api/users.ts | 0 src/components/app/app.scss | 13 +++- .../create-group-step/create-group-step.tsx | 35 ++++++---- src/components/create-group-step/index.ts | 25 +++++-- .../create-user-form/create-user-form.tsx | 2 +- .../create-user-step/create-user-step.tsx | 52 +++++++++++---- src/components/create-user-step/index.ts | 66 +++++++++++++++++-- .../forms/password-field/password-field.tsx | 14 ++-- .../forms/text-field/text-field.tsx | 2 +- src/components/page-header/index.tsx | 19 ++++++ src/components/pages/about/index.tsx | 35 +++++----- src/components/pages/developers/index.tsx | 27 +++++--- src/components/pages/directory/directory.tsx | 22 +++++-- src/components/pages/home/index.tsx | 24 +++++-- src/components/pages/login/login.tsx | 23 +++---- src/components/pages/register/index.ts | 41 ++++++++++-- src/components/pages/register/register.tsx | 60 ++++++++--------- src/components/pages/test/test.tsx | 10 +-- src/constants/index.ts | 10 +++ src/reducers/forms.ts | 1 + src/selectors/forms.ts | 9 +-- src/utils/index.ts | 22 ++++++- 26 files changed, 468 insertions(+), 161 deletions(-) create mode 100644 src/api/users.ts create mode 100644 src/components/page-header/index.tsx create mode 100644 src/constants/index.ts diff --git a/src/actions/registration.ts b/src/actions/registration.ts index 7d108d0..330472c 100644 --- a/src/actions/registration.ts +++ b/src/actions/registration.ts @@ -1,11 +1,22 @@ -import { Action } from 'redux' +import { Action, AnyAction } from 'redux' +import { ThunkAction } from 'redux-thunk' + import { setFieldNotification } from 'src/actions/forms' import { startRequest, finishRequest } from 'src/actions/requests' -import { fetchGroupAvailability } from 'src/api/registration' -import { AppThunkAction } from 'src/types' +import { createGroup as fetchCreateGroup } from 'src/api/groups' +import { fetchGroupAvailability, register as fetchRegister } from 'src/api/registration' + +import { + LOCAL_STORAGE_ACCESS_TOKEN_KEY, + LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY, + LOCAL_STORAGE_REFRESH_TOKEN_KEY, +} from 'src/constants' +import { AppState, AppThunkAction } from 'src/types' const FETCH_GROUP_AVAILABILITY_ID = 'FETCH_GROUP_AVAILABILITY' const FETCH_USER_AVAILABILITY_ID = 'FETCH_USER_AVAILABILITY' +const CREATE_GROUP_ID = 'CREATE_GROUP' +const REGISTER_ID = 'REGISTER' export interface SetStepAction extends Action { type: 'REGISTRATION_SET_STEP' @@ -45,12 +56,12 @@ export const checkUserAvailability = (name: string): AppThunkAction => { dispatch(startRequest(FETCH_USER_AVAILABILITY_ID)) try { - const response = await fetchGroupAvailability(name) + const { id, available } = await fetchGroupAvailability(name) - if (response.available) { - dispatch(setFieldNotification('user-id', 'success', `${response.id} is available`)) + if (available) { + dispatch(setFieldNotification('user-id', 'success', `${id} is available`)) } else { - dispatch(setFieldNotification('user-id', 'error', `${response.id} isn't available`)) + dispatch(setFieldNotification('user-id', 'error', `${id} isn't available`)) } dispatch(finishRequest(FETCH_USER_AVAILABILITY_ID, true)) @@ -60,3 +71,38 @@ export const checkUserAvailability = (name: string): AppThunkAction => { } } } + +export const createGroup = (name: string, registration: string, about?: string): ThunkAction, AppState, void, AnyAction> => { + return async dispatch => { + dispatch(startRequest(CREATE_GROUP_ID)) + + try { + const { id } = await fetchCreateGroup(name, registration, about) + dispatch(finishRequest(CREATE_GROUP_ID, true)) + + return id + } catch (err) { + dispatch(finishRequest(CREATE_GROUP_ID, false)) + throw err + } + } +} + +export const register = (id: string, email: string, password: string, name?: string, group?: string): ThunkAction, AppState, void, AnyAction> => { + return async dispatch => { + dispatch(startRequest(REGISTER_ID)) + + try { + const response = await fetchRegister(id, email, password, name, group) + dispatch(finishRequest(REGISTER_ID, true)) + + localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY, response.access) + localStorage.setItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY, response.refresh) + + return response.id + } catch (err) { + dispatch(finishRequest(REGISTER_ID, false)) + throw err + } + } +} diff --git a/src/api/fetch.ts b/src/api/fetch.ts index ac5f9b1..bc046a8 100644 --- a/src/api/fetch.ts +++ b/src/api/fetch.ts @@ -4,6 +4,13 @@ import { NotFoundError, ServerError, } from './errors' + +import { + LOCAL_STORAGE_ACCESS_TOKEN_KEY, + LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY, + LOCAL_STORAGE_REFRESH_TOKEN_KEY, +} from '../constants' + import { FetchOptions, FormNotification } from '../types' import getConfig from '../config' @@ -73,7 +80,7 @@ const apiFetch: APIFetch = async (options: FetchOptions) => { 'Accept': contentType, }) - const accessToken = localStorage.getItem('accessToken') + const accessToken = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY) if (accessToken) headers.append('Authorization', `Bearer ${accessToken}`) return await fetch(`${config.apiUrl}${path}`, { @@ -84,8 +91,8 @@ const apiFetch: APIFetch = async (options: FetchOptions) => { } const doRefresh = async () => { - const accessToken = localStorage.getItem('accessToken') - const refreshToken = localStorage.getItem('refreshToken') + const accessToken = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY) + const refreshToken = localStorage.getItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY) if (accessToken && refreshToken) { const refreshResponse = await fetch('/api/refresh', { @@ -105,9 +112,9 @@ const apiFetch: APIFetch = async (options: FetchOptions) => { const data = await getResponseData(refreshResponse) as RefreshResponse - localStorage.setItem('accessToken', data.access) - localStorage.setItem('accessTokenExpiresAt', data.expires.toString()) - localStorage.setItem('refeshToken', data.refresh) + localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY, data.access) + localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY, data.expires.toString()) + localStorage.setItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY, data.refresh) const secondResponse = await doFetch() if (secondResponse.ok) { @@ -118,7 +125,7 @@ const apiFetch: APIFetch = async (options: FetchOptions) => { } } - const accessTokenExpiresAt = localStorage.getItem('accessTokenExpiresAt') + const accessTokenExpiresAt = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY) if (accessTokenExpiresAt && Date.now() >= parseInt(accessTokenExpiresAt, 10)) { return await doRefresh() } diff --git a/src/api/groups.ts b/src/api/groups.ts index 462823e..b5169d7 100644 --- a/src/api/groups.ts +++ b/src/api/groups.ts @@ -6,6 +6,10 @@ interface GroupsResponse { continuation?: string } +interface NewGroupResponse { + id: string +} + export async function getGroup(id: string) { return await fetch({ path: `/api/group/${id}` @@ -24,3 +28,15 @@ export async function getGroups(sort: string = 'members', continuation?: string) path: `/api/groups?${querystring}` }) } + +export async function createGroup(name: string, registration: string, about?: string) { + return await fetch({ + path: '/api/group', + method: 'post', + body: { + name, + registration, + about, + }, + }) +} diff --git a/src/api/registration.ts b/src/api/registration.ts index 863f4de..5eaffdc 100644 --- a/src/api/registration.ts +++ b/src/api/registration.ts @@ -5,6 +5,12 @@ interface AvailabilityResponse { available: boolean } +interface RegisterResponse { + id: string + access: string + refresh: string +} + export async function fetchGroupAvailability(name: string) { return await fetch({ path: '/api/group/available', @@ -24,3 +30,17 @@ export async function fetchUserAvailability(name: string) { }, }) } + +export async function register(id: string, email: string, password: string, name?: string, group?: string) { + return await fetch({ + path: '/api/register', + method: 'post', + body: { + id, + email, + password, + name, + group, + }, + }) +} diff --git a/src/api/users.ts b/src/api/users.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/components/app/app.scss b/src/components/app/app.scss index 507f38f..d666bd0 100644 --- a/src/components/app/app.scss +++ b/src/components/app/app.scss @@ -10,11 +10,13 @@ $turquoise: hsl(171, 100%, 41%); $cyan: hsl(204, 86%, 53%); $blue: hsl(217, 72%, 30%); $purple: hsl(271, 63%, 32%); -$red: hsl(348, 82%, 30%); +$red: hsl(348, 71%, 42%); +$white-ter: hsl(0, 0%, 96%); $family-sans-serif: "Open Sans", sans-serif; $primary: $blue; $title-weight: 400; +$body-background-color: $white-ter; @import "../../../node_modules/bulma/sass/utilities/_all.sass"; @import "../../../node_modules/bulma/sass/base/_all.sass"; @@ -26,6 +28,7 @@ $title-weight: 400; @import "../../../node_modules/bulma/sass/elements/other.sass"; @import "../../../node_modules/bulma/sass/elements/title.sass"; @import "../../../node_modules/bulma/sass/layout/hero.sass"; +@import "../../../node_modules/bulma/sass/components/level.sass"; @import "../../../node_modules/bulma/sass/components/media.sass"; div#main-menu { @@ -46,6 +49,14 @@ div.main-content { padding: $size-normal; } +div.centered-content { + background-color: $white; + border-radius: $radius; + margin: 1rem auto; + padding: 2rem; + width: 80%; +} + div#navigation { flex-grow: 1; diff --git a/src/components/create-group-step/create-group-step.tsx b/src/components/create-group-step/create-group-step.tsx index 4bd3b78..7770edc 100644 --- a/src/components/create-group-step/create-group-step.tsx +++ b/src/components/create-group-step/create-group-step.tsx @@ -8,22 +8,31 @@ import CreateGroupForm from '../create-group-form' export interface Props { name: string registration: string - next?: (name: string, registration: string) => void + agree: boolean + next?: (name: string, agree: boolean) => void } -const CreateGroupStep: FC = ({ name, registration, next = noop }) => ( -
-
- -

+const CreateGroupStep: FC = ({ name, agree, next = noop }) => ( +
+ +
- -
+
) diff --git a/src/components/create-group-step/index.ts b/src/components/create-group-step/index.ts index 3878889..6edfba5 100644 --- a/src/components/create-group-step/index.ts +++ b/src/components/create-group-step/index.ts @@ -2,31 +2,42 @@ import { Dispatch } from 'redux' import { connect } from 'react-redux' import { setFieldNotification } from 'src/actions/forms' +import { showNotification } from 'src/actions/notifications' import { setStep } from 'src/actions/registration' import { getFieldValue } from 'src/selectors/forms' -import { AppState } from 'src/types' +import { MAX_ID_LENGTH } from 'src/constants' +import { AppState, AppThunkDispatch } from 'src/types' import CreateGroupStep from './create-group-step' -const MAX_ID_LENGTH = 40 - const mapStateToProps = (state: AppState) => ({ name: getFieldValue(state, 'group-name', ''), registration: getFieldValue(state, 'group-registration', ''), + agree: getFieldValue(state, 'group-agree', false), }) -const mapDispatchToProps = (dispatch: Dispatch) => ({ - next: (name: string) => { +const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({ + next: (name: string, agree: boolean) => { + let invalid = false + if (!name) { dispatch(setFieldNotification('group-name', 'error', 'This is required')) - return + invalid = true } if (name.length > MAX_ID_LENGTH) { dispatch(setFieldNotification('group-name', 'error', `This must be less than ${MAX_ID_LENGTH} characters`)) - return + invalid = true } + if (!agree) { + dispatch(setFieldNotification('group-agree', 'error', 'You must agree to the terms and conditions to continue')) + dispatch(showNotification('error', 'You must agree to the terms and conditions to continue.')) + invalid = true + } + + if (invalid) return + dispatch(setStep(1)) }, }) diff --git a/src/components/create-user-form/create-user-form.tsx b/src/components/create-user-form/create-user-form.tsx index dd0f515..b251480 100644 --- a/src/components/create-user-form/create-user-form.tsx +++ b/src/components/create-user-form/create-user-form.tsx @@ -21,7 +21,7 @@ const CreateUserForm: FC = ({ checkAvailability }) => {

- + I agree to the User terms and conditions.
diff --git a/src/components/create-user-step/create-user-step.tsx b/src/components/create-user-step/create-user-step.tsx index 96d9cb1..c91f4a2 100644 --- a/src/components/create-user-step/create-user-step.tsx +++ b/src/components/create-user-step/create-user-step.tsx @@ -6,23 +6,47 @@ import { faArrowLeft } from '@fortawesome/free-solid-svg-icons' import CreateUserForm from '../create-user-form' export interface Props { + userId?: string + name?: string + email?: string + password?: string + agree?: boolean previous?: () => void - next?: () => void + next?: (userId: string, name: string, email: string, password: string, agree: boolean) => void + register: () => void } -const CreateUserStep: FC = ({ previous = noop, next = noop }) => ( -
-
- -

- - -
+const CreateUserStep: FC = ({ + userId = '', + name = '', + email = '', + password = '', + agree = false, + previous = noop, + next = noop, +}) => ( +
+ +
+ +
) diff --git a/src/components/create-user-step/index.ts b/src/components/create-user-step/index.ts index ee1d4b5..edca92e 100644 --- a/src/components/create-user-step/index.ts +++ b/src/components/create-user-step/index.ts @@ -1,20 +1,74 @@ -import { Dispatch } from 'redux' import { connect } from 'react-redux' +import zxcvbn from 'zxcvbn' +import { setFieldNotification } from 'src/actions/forms' +import { showNotification } from 'src/actions/notifications' import { setStep } from 'src/actions/registration' +import { getFieldValue } from 'src/selectors/forms' +import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants' +import { AppState, AppThunkDispatch } from 'src/types' -import CreateUserStep from './create-user-step' +import CreateUserStep, { Props } from './create-user-step' -const mapDispatchToProps = (dispatch: Dispatch) => ({ +const mapStateToProps = (state: AppState) => ({ + userId: getFieldValue(state, 'user-id', ''), + name: getFieldValue(state, 'user-name', ''), + email: getFieldValue(state, 'user-email', ''), + password: getFieldValue(state, 'password', ''), + agree: getFieldValue(state, 'user-agree', false), +}) + +const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ previous: () => { dispatch(setStep(0)) }, - next: () => { - + next: (userId: string, name: string, email: string, password: string, agree: boolean) => { + let invalid = false + + if (!userId) { + dispatch(setFieldNotification('user-id', 'error', 'This is required')) + invalid = true + } + + if (userId.length > MAX_ID_LENGTH) { + dispatch(setFieldNotification('user-id', 'error', `This must be less than ${MAX_ID_LENGTH} characters`)) + invalid = true + } + + if (name.length > MAX_NAME_LENGTH) { + dispatch(setFieldNotification('user-name', 'error', `This must be less than ${MAX_NAME_LENGTH} characters`)) + invalid = true + } + + if (email === '') { + dispatch(setFieldNotification('user-email', 'error', 'This is required')) + invalid = true + } + + if (!agree) { + dispatch(setFieldNotification('user-agree', 'error', 'You must agree to the terms and conditions to continue')) + dispatch(showNotification('error', 'You must agree to the terms and conditions to continue.')) + invalid = true + } + + if (password === '') { + dispatch(setFieldNotification('password', 'error', 'This is required')) + invalid = true + } else { + const { score } = zxcvbn(password) + + if (score === 0) { + dispatch(setFieldNotification('password', 'error', 'Try another password')) + invalid = true + } + } + + if (invalid) return + if (ownProps.register) ownProps.register() }, }) export default connect( - null, + mapStateToProps, mapDispatchToProps )(CreateUserStep) diff --git a/src/components/forms/password-field/password-field.tsx b/src/components/forms/password-field/password-field.tsx index aa91650..365c676 100644 --- a/src/components/forms/password-field/password-field.tsx +++ b/src/components/forms/password-field/password-field.tsx @@ -6,6 +6,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { IconDefinition } from '@fortawesome/fontawesome-common-types' import { faKey, faExclamationTriangle, faCheckCircle } from '@fortawesome/free-solid-svg-icons' +import { notificationTypeToClassName } from 'src/utils' import { FormNotification, ClassDictionary } from 'src/types' export interface Props { @@ -25,6 +26,7 @@ const PasswordField: FC = ({ }) => { const inputClassDictionary: ClassDictionary = { input: true } const controlClassDictionary: ClassDictionary = { control: true, 'has-icons-left': true } + const helpClassDictionary: ClassDictionary = { help: true } let icon: IconDefinition | undefined let passwordMessage: ReactNode | undefined @@ -59,8 +61,15 @@ const PasswordField: FC = ({ } } + if (notification) { + const ncn = notificationTypeToClassName(notification.type) + + helpClassDictionary[ncn] = true + inputClassDictionary[ncn] = true + } + const helpText = () => { - if (notification) return

{notification.message}

+ if (notification) return

{notification.message}

if (passwordMessage) return

{passwordMessage}

} @@ -78,9 +87,6 @@ const PasswordField: FC = ({ }
- {notification && -

{notification.message}

- } {helpText()} ) diff --git a/src/components/forms/text-field/text-field.tsx b/src/components/forms/text-field/text-field.tsx index 63a7e10..ed0b99f 100644 --- a/src/components/forms/text-field/text-field.tsx +++ b/src/components/forms/text-field/text-field.tsx @@ -37,7 +37,7 @@ const TextField: FC = ({ const ncn = notificationTypeToClassName(notification.type) helpClassDictionary['help'] = true - helpClassDictionary[ncn] = true, + helpClassDictionary[ncn] = true inputClassDictionary[ncn] = true } diff --git a/src/components/page-header/index.tsx b/src/components/page-header/index.tsx new file mode 100644 index 0000000..b593a02 --- /dev/null +++ b/src/components/page-header/index.tsx @@ -0,0 +1,19 @@ +import React, { FC } from 'react' + +interface Props { + title: string + subtitle?: string +} + +const PageHeader: FC = ({ title, subtitle }) => ( +
+
+
+

{title}

+ {subtitle &&

{title}

} +
+
+
+) + +export default PageHeader diff --git a/src/components/pages/about/index.tsx b/src/components/pages/about/index.tsx index b600dd1..4b05aa0 100644 --- a/src/components/pages/about/index.tsx +++ b/src/components/pages/about/index.tsx @@ -1,21 +1,24 @@ -import React, { FC } from 'react' +import React, { FC, useEffect } from 'react' +import { setTitle } from 'src/utils' -const About: FC = () => ( -
-
-
-
-

About Flexor

-
-
-
+import PageHeader from 'src/components/page-header' + +const About: FC = () => { + useEffect(() => { + setTitle('About Flexor', false) + }) + + return ( +
+ -
-

- Flexor is a website. -

+
+

+ Flexor is a website. +

+
-
-) + ) +} export default About diff --git a/src/components/pages/developers/index.tsx b/src/components/pages/developers/index.tsx index e44b765..df2c5b1 100644 --- a/src/components/pages/developers/index.tsx +++ b/src/components/pages/developers/index.tsx @@ -1,13 +1,22 @@ -import React, { FC } from 'react' +import React, { FC, useEffect } from 'react' +import { setTitle } from 'src/utils' -const Developers: FC = () => ( -
-

Developers

+import PageHeader from 'src/components/page-header' -

- Developer documentation coming soon. -

-
-) +const Developers: FC = () => { + useEffect(() => { + setTitle('Developers') + }) + + return ( +
+ + +
+ Developer documentation coming soon. +
+
+ ) +} export default Developers diff --git a/src/components/pages/directory/directory.tsx b/src/components/pages/directory/directory.tsx index 03d7fbe..9ab981c 100644 --- a/src/components/pages/directory/directory.tsx +++ b/src/components/pages/directory/directory.tsx @@ -1,7 +1,11 @@ import React, { FC, useEffect } from 'react' import { Link } from 'react-router-dom' + +import { setTitle } from 'src/utils' import { Entity } from 'src/types' +import PageHeader from 'src/components/page-header' + interface Props { groups: Entity[] fetchGroups: () => void @@ -12,15 +16,21 @@ const Directory: FC = ({ groups, fetchGroups }) => { fetchGroups() }, []) + useEffect(() => { + setTitle('Communities') + }) + return ( -
-

Communities

+
+ - {groups.length === 0 &&

No Communities

} +
+ {groups.length === 0 &&

No Communities

} -

- Create your own Community -

+

+ Create your own Community +

+
) } diff --git a/src/components/pages/home/index.tsx b/src/components/pages/home/index.tsx index d3a8776..37b2d96 100644 --- a/src/components/pages/home/index.tsx +++ b/src/components/pages/home/index.tsx @@ -1,9 +1,21 @@ -import React, { FC } from 'react' +import React, { FC, useEffect } from 'react' +import { setTitle } from 'src/utils' +import PageHeader from 'src/components/page-header' -const Home: FC = () => ( -
-

Home

-
-) +const Home: FC = () => { + useEffect(() => { + setTitle('Home') + }) + + return ( +
+ + +
+ Hello. +
+
+ ) +} export default Home diff --git a/src/components/pages/login/login.tsx b/src/components/pages/login/login.tsx index b79d429..68d966d 100644 --- a/src/components/pages/login/login.tsx +++ b/src/components/pages/login/login.tsx @@ -1,6 +1,7 @@ import React, { FC, useEffect } from 'react' import { faIdCard } from '@fortawesome/free-solid-svg-icons' +import PageHeader from 'src/components/page-header' import TextField from 'src/components/forms/text-field' import PasswordField from 'src/components/forms/password-field' @@ -15,23 +16,15 @@ const Login: FC = ({ initForm }) => { return (
-
-
-
-

Login

-
-
-
+
-
-
- -
- -
- -
+
+ +
+ +
+
diff --git a/src/components/pages/register/index.ts b/src/components/pages/register/index.ts index 9cdcc2d..e69d4bb 100644 --- a/src/components/pages/register/index.ts +++ b/src/components/pages/register/index.ts @@ -1,24 +1,57 @@ import { connect } from 'react-redux' + +import { getForm } from 'src/selectors/forms' import { getStep } from 'src/selectors/registration' import { initForm, initField } from 'src/actions/forms' -import { AppState, AppThunkDispatch } from 'src/types' +import { showNotification } from 'src/actions/notifications' +import { createGroup, register } from 'src/actions/registration' + +import { valueFromForm } from 'src/utils' +import { AppState, AppThunkDispatch, Form } from 'src/types' -import Register from './register' +import Register, { Props } from './register' const mapStateToProps = (state: AppState) => ({ - step: getStep(state), + stepIndex: getStep(state), + form: getForm(state), }) -const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({ +const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ initForm: () => { dispatch(initForm()) dispatch(initField('group-name')) dispatch(initField('group-registration')) + dispatch(initField('group-agree')) dispatch(initField('user-id')) dispatch(initField('user-name')) dispatch(initField('user-email')) dispatch(initField('password')) + dispatch(initField('user-agree')) }, + register: async (form: Form) => { + const groupAgree = valueFromForm(form, 'group-agree', false) + const userAgree = valueFromForm(form, 'user-agree', false) + + if (!groupAgree || !userAgree) { + dispatch(showNotification('error', 'You must agree to both Community and User terms and conditions.')) + return + } + + const groupId = await dispatch(createGroup( + valueFromForm(form, 'group-name', ''), + valueFromForm(form, 'group-registration', '') + )) + + const userId = await dispatch(register( + valueFromForm(form, 'user-id', ''), + valueFromForm(form, 'user-email', ''), + valueFromForm(form, 'password', ''), + valueFromForm(form, 'user-name', ''), + groupId + )) + + ownProps.history.push('/self') + } }) export default connect( diff --git a/src/components/pages/register/register.tsx b/src/components/pages/register/register.tsx index eff5ff2..945fd64 100644 --- a/src/components/pages/register/register.tsx +++ b/src/components/pages/register/register.tsx @@ -1,49 +1,49 @@ import React, { FC, useEffect } from 'react' +import { RouteComponentProps } from 'react-router' -import CreateGroupStep from '../../create-group-step' -import CreateUserStep from '../../create-user-step' +import PageHeader from 'src/components/page-header' +import CreateGroupStep from 'src/components/create-group-step' +import CreateUserStep from 'src/components/create-user-step' -interface Step { - title: string - component: FC -} +import { setTitle } from 'src/utils' +import { Form } from 'src/types' -const steps: Step[] = [ - { - title: 'Create a Community', - component: () => , - }, - { - title: 'Create Your Account', - component: () => , - }, -] - -export interface Props { - step: number +export interface Props extends RouteComponentProps { + stepIndex: number + form: Form initForm: () => void + register: (form: Form) => void } -const Register: FC = ({ step: index, initForm }) => { - const step = steps[index] - const Component = step.component +const Register: FC = ({ stepIndex, form, initForm, register }) => { + const title = () => { + switch (stepIndex) { + case 0: return 'Create a Community' + default: return 'Create Your Account' + } + } + + const component = () => { + switch (stepIndex) { + case 0: return + default: return register(form)} /> + } + } useEffect(() => { initForm() }, []) + useEffect(() => { + setTitle(title()) + }, [stepIndex]) + return (
-
-
-
-

{step.title}

-
-
-
+
- + {component()}
) diff --git a/src/components/pages/test/test.tsx b/src/components/pages/test/test.tsx index a54ee9d..c186c50 100644 --- a/src/components/pages/test/test.tsx +++ b/src/components/pages/test/test.tsx @@ -1,19 +1,15 @@ import React, { FC } from 'react' import { NotificationType } from 'src/types' +import PageHeader from 'src/components/page-header' + interface Props { show: (type: NotificationType, content: string) => void } const Test: FC = ({ show }) => (
-
-
-
-

Test Page

-
-
-
+

diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..cc8b9bf --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,10 @@ +export const MAX_ID_LENGTH = 40 +export const MAX_NAME_LENGTH = 80 + +export const LOCAL_STORAGE_ACCESS_TOKEN_KEY = 'FLEXOR_ACCESS_TOKEN' +export const LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY = 'FLEXOR_ACCESS_TOKEN_AT' +export const LOCAL_STORAGE_REFRESH_TOKEN_KEY = 'FLEXOR_REFRESH_TOKEN' + +export const REQUEST_KEYS = { + +} diff --git a/src/reducers/forms.ts b/src/reducers/forms.ts index 82c3428..04bd48f 100644 --- a/src/reducers/forms.ts +++ b/src/reducers/forms.ts @@ -35,6 +35,7 @@ const reducer: Reducer = (state = initialState, action [action.payload.name]: { ...field, value: action.payload.value, + notification: undefined, }, }, } diff --git a/src/selectors/forms.ts b/src/selectors/forms.ts index 5aac3fe..74cab96 100644 --- a/src/selectors/forms.ts +++ b/src/selectors/forms.ts @@ -1,15 +1,12 @@ import { AppState, FormValue } from '../types' +import { valueFromForm } from '../utils' export const getForm = (state: AppState) => state.forms.form +export function getFieldValue(state: AppState, name: string): T | undefined export function getFieldValue(state: AppState, name: string, defaultValue: T): T export function getFieldValue(state: AppState, name: string, defaultValue?: T): T | undefined { - const field = getForm(state)[name] - - if (!field) return defaultValue - if (field.value === undefined) return defaultValue - - return field.value as T + return valueFromForm(getForm(state), name, defaultValue) } export const getFieldNotification = (state: AppState, name: string) => { diff --git a/src/utils/index.ts b/src/utils/index.ts index 21e2dc8..9d1aabd 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { NotificationType } from '../types' +import { NotificationType, Form, FormValue } from '../types' export function notificationTypeToClassName(type: NotificationType): string { switch (type) { @@ -7,3 +7,23 @@ export function notificationTypeToClassName(type: NotificationType): string { case 'error': return 'is-danger' } } + +export function setTitle(title: string, decorate: boolean = true) { + if (decorate) { + document.title = `${title} | Flexor` + } else { + document.title = title + } +} + +export function valueFromForm(form: Form, name: string): T | undefined +export function valueFromForm(form: Form, name: string, defaultValue: T): T +export function valueFromForm(form: Form, name: string, defaultValue?: T): T | undefined +export function valueFromForm(form: Form, name: string, defaultValue?: T): T | undefined { + const field = form[name] + + if (!field) return defaultValue + if (field.value === undefined) return defaultValue + + return field.value as T +}