Dwayne Harris 5 years ago
parent
commit
e9fa0a7e51
  1. 60
      src/actions/registration.ts
  2. 21
      src/api/fetch.ts
  3. 16
      src/api/groups.ts
  4. 20
      src/api/registration.ts
  5. 0
      src/api/users.ts
  6. 13
      src/components/app/app.scss
  7. 35
      src/components/create-group-step/create-group-step.tsx
  8. 25
      src/components/create-group-step/index.ts
  9. 2
      src/components/create-user-form/create-user-form.tsx
  10. 52
      src/components/create-user-step/create-user-step.tsx
  11. 66
      src/components/create-user-step/index.ts
  12. 14
      src/components/forms/password-field/password-field.tsx
  13. 2
      src/components/forms/text-field/text-field.tsx
  14. 19
      src/components/page-header/index.tsx
  15. 35
      src/components/pages/about/index.tsx
  16. 27
      src/components/pages/developers/index.tsx
  17. 22
      src/components/pages/directory/directory.tsx
  18. 24
      src/components/pages/home/index.tsx
  19. 23
      src/components/pages/login/login.tsx
  20. 41
      src/components/pages/register/index.ts
  21. 60
      src/components/pages/register/register.tsx
  22. 10
      src/components/pages/test/test.tsx
  23. 10
      src/constants/index.ts
  24. 1
      src/reducers/forms.ts
  25. 9
      src/selectors/forms.ts
  26. 22
      src/utils/index.ts

60
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<Promise<string>, 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<Promise<string>, 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
}
}
}

21
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()
}

16
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<Entity>({
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<NewGroupResponse>({
path: '/api/group',
method: 'post',
body: {
name,
registration,
about,
},
})
}

20
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<AvailabilityResponse>({
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<RegisterResponse>({
path: '/api/register',
method: 'post',
body: {
id,
email,
password,
name,
group,
},
})
}

0
src/api/users.ts

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

35
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<Props> = ({ name, registration, next = noop }) => (
<div className="columns">
<div className="column is-8 is-offset-2">
<CreateGroupForm />
<br /><hr />
const CreateGroupStep: FC<Props> = ({ name, agree, next = noop }) => (
<div className="centered-content">
<CreateGroupForm />
<hr />
<button className="button is-pulled-right is-success" onClick={() => next(name, registration)}>
<span>Your Account</span>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowRight} />
</span>
</button>
</div>
<nav className="level">
<div className="level-left">
<p className="level-item"></p>
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next(name, agree)}>
<span>Your Account</span>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowRight} />
</span>
</button>
</p>
</div>
</nav>
</div>
)

25
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<string>(state, 'group-name', ''),
registration: getFieldValue<string>(state, 'group-registration', ''),
agree: getFieldValue<boolean>(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))
},
})

2
src/components/create-user-form/create-user-form.tsx

@ -21,7 +21,7 @@ const CreateUserForm: FC<Props> = ({ checkAvailability }) => {
<br />
<PasswordField placeholder="Your new password" />
<br />
<CheckboxField name="group-agree">
<CheckboxField name="user-agree">
I agree to the User <Link to="/terms">terms and conditions</Link>.
</CheckboxField>
</div>

52
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<Props> = ({ previous = noop, next = noop }) => (
<div className="columns">
<div className="column is-8 is-offset-2">
<CreateUserForm />
<br /><hr />
<button className="button is-pulled-left is-outlined" onClick={() => previous()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowLeft} />
</span>
<span>Community</span>
</button>
<button className="button is-pulled-right is-success" onClick={() => next()}>Finish</button>
</div>
const CreateUserStep: FC<Props> = ({
userId = '',
name = '',
email = '',
password = '',
agree = false,
previous = noop,
next = noop,
}) => (
<div className="centered-content">
<CreateUserForm />
<hr />
<nav className="level">
<div className="level-left">
<p className="level-item">
<button className="button" onClick={() => previous()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowLeft} />
</span>
<span>Community</span>
</button>
</p>
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next(userId, name, email, password, agree)}>Finish</button>
</p>
</div>
</nav>
</div>
)

66
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<string>(state, 'user-id', ''),
name: getFieldValue<string>(state, 'user-name', ''),
email: getFieldValue<string>(state, 'user-email', ''),
password: getFieldValue<string>(state, 'password', ''),
agree: getFieldValue<boolean>(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)

14
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<Props> = ({
}) => {
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<Props> = ({
}
}
if (notification) {
const ncn = notificationTypeToClassName(notification.type)
helpClassDictionary[ncn] = true
inputClassDictionary[ncn] = true
}
const helpText = () => {
if (notification) return <p className="help">{notification.message}</p>
if (notification) return <p className={classNames(helpClassDictionary)}>{notification.message}</p>
if (passwordMessage) return <p className="help">{passwordMessage}</p>
}
@ -78,9 +87,6 @@ const PasswordField: FC<Props> = ({
</span>
}
</div>
{notification &&
<p className="help">{notification.message}</p>
}
{helpText()}
</div>
)

2
src/components/forms/text-field/text-field.tsx

@ -37,7 +37,7 @@ const TextField: FC<Props> = ({
const ncn = notificationTypeToClassName(notification.type)
helpClassDictionary['help'] = true
helpClassDictionary[ncn] = true,
helpClassDictionary[ncn] = true
inputClassDictionary[ncn] = true
}

19
src/components/page-header/index.tsx

@ -0,0 +1,19 @@
import React, { FC } from 'react'
interface Props {
title: string
subtitle?: string
}
const PageHeader: FC<Props> = ({ title, subtitle }) => (
<section className="hero is-dark is-bold">
<div className="hero-body">
<div className="container">
<h1 className="title">{title}</h1>
{subtitle && <h2 className="subtitle">{title}</h2>}
</div>
</div>
</section>
)
export default PageHeader

35
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 = () => (
<div>
<section className="hero is-dark is-bold">
<div className="hero-body">
<div className="container">
<h1 className="title">About Flexor</h1>
</div>
</div>
</section>
import PageHeader from 'src/components/page-header'
const About: FC = () => {
useEffect(() => {
setTitle('About Flexor', false)
})
return (
<div>
<PageHeader title="About Flexor" />
<div className="main-content">
<p>
Flexor is a website.
</p>
<div className="main-content">
<p>
Flexor is a website.
</p>
</div>
</div>
</div>
)
)
}
export default About

27
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 = () => (
<div className="main-content">
<h1 className="title">Developers</h1>
import PageHeader from 'src/components/page-header'
<p>
Developer documentation coming soon.
</p>
</div>
)
const Developers: FC = () => {
useEffect(() => {
setTitle('Developers')
})
return (
<div>
<PageHeader title="Developers" />
<div className="main-content">
Developer documentation coming soon.
</div>
</div>
)
}
export default Developers

22
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<Props> = ({ groups, fetchGroups }) => {
fetchGroups()
}, [])
useEffect(() => {
setTitle('Communities')
})
return (
<div className="main-content">
<h1 className="title">Communities</h1>
<div>
<PageHeader title="Communities" />
{groups.length === 0 && <p>No Communities</p>}
<div className="main-content">
{groups.length === 0 && <p>No Communities</p>}
<p className="has-text-centered">
<Link className="has-text-primary" to="/register">Create your own Community</Link>
</p>
<p className="has-text-centered">
<Link className="has-text-primary" to="/register">Create your own Community</Link>
</p>
</div>
</div>
)
}

24
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 = () => (
<div className="main-content">
<h1 className="title">Home</h1>
</div>
)
const Home: FC = () => {
useEffect(() => {
setTitle('Home')
})
return (
<div>
<PageHeader title="Home" />
<div className="main-content">
Hello.
</div>
</div>
)
}
export default Home

23
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<Props> = ({ initForm }) => {
return (
<div>
<section className="hero is-success is-bold">
<div className="hero-body">
<div className="container">
<h1 className="title">Login</h1>
</div>
</div>
</section>
<PageHeader title="Log In" />
<div className="main-content">
<div className="columns">
<div className="column is-8 is-offset-2">
<TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" />
<br />
<PasswordField placeholder="Your password" />
<br />
<button className="button is-primary">Log In</button>
</div>
<div className="centered-content">
<TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" />
<br />
<PasswordField placeholder="Your password" />
<br />
<button className="button is-primary">Log In</button>
</div>
</div>
</div>

41
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<boolean>(form, 'group-agree', false)
const userAgree = valueFromForm<boolean>(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<string>(form, 'group-name', ''),
valueFromForm<string>(form, 'group-registration', '')
))
const userId = await dispatch(register(
valueFromForm<string>(form, 'user-id', ''),
valueFromForm<string>(form, 'user-email', ''),
valueFromForm<string>(form, 'password', ''),
valueFromForm<string>(form, 'user-name', ''),
groupId
))
ownProps.history.push('/self')
}
})
export default connect(

60
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: () => <CreateGroupStep />,
},
{
title: 'Create Your Account',
component: () => <CreateUserStep />,
},
]
export interface Props {
step: number
export interface Props extends RouteComponentProps {
stepIndex: number
form: Form
initForm: () => void
register: (form: Form) => void
}
const Register: FC<Props> = ({ step: index, initForm }) => {
const step = steps[index]
const Component = step.component
const Register: FC<Props> = ({ 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 <CreateGroupStep />
default: return <CreateUserStep register={() => register(form)} />
}
}
useEffect(() => {
initForm()
}, [])
useEffect(() => {
setTitle(title())
}, [stepIndex])
return (
<div>
<section className="hero is-dark is-bold">
<div className="hero-body">
<div className="container">
<h1 className="title">{step.title}</h1>
</div>
</div>
</section>
<PageHeader title={title()} />
<div className="main-content">
<Component />
{component()}
</div>
</div>
)

10
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<Props> = ({ show }) => (
<div>
<section className="hero is-dark is-bold">
<div className="hero-body">
<div className="container">
<h1 className="title">Test Page</h1>
</div>
</div>
</section>
<PageHeader title="Test Page" />
<div className="main-content">
<p>

10
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 = {
}

1
src/reducers/forms.ts

@ -35,6 +35,7 @@ const reducer: Reducer<FormsState, FormsActions> = (state = initialState, action
[action.payload.name]: {
...field,
value: action.payload.value,
notification: undefined,
},
},
}

9
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<T extends FormValue>(state: AppState, name: string): T | undefined
export function getFieldValue<T extends FormValue>(state: AppState, name: string, defaultValue: T): T
export function getFieldValue<T extends FormValue>(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<T>(getForm(state), name, defaultValue)
}
export const getFieldNotification = (state: AppState, name: string) => {

22
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<T extends FormValue>(form: Form, name: string): T | undefined
export function valueFromForm<T extends FormValue>(form: Form, name: string, defaultValue: T): T
export function valueFromForm<T extends FormValue>(form: Form, name: string, defaultValue?: T): T | undefined
export function valueFromForm<T extends FormValue>(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
}
Loading…
Cancel
Save