Dwayne Harris 5 years ago
parent
commit
294bc47d8a
  1. 28
      package-lock.json
  2. 10
      package.json
  3. 61
      src/actions/registration.ts
  4. 26
      src/api/registration.ts
  5. 6
      src/components/app/app.scss
  6. 26
      src/components/app/app.tsx
  7. 55
      src/components/create-group-form/create-group-form.tsx
  8. 25
      src/components/create-group-form/index.ts
  9. 30
      src/components/create-group-step/create-group-step.tsx
  10. 37
      src/components/create-group-step/index.ts
  11. 31
      src/components/create-user-form/create-user-form.tsx
  12. 27
      src/components/create-user-form/index.ts
  13. 29
      src/components/create-user-step/create-user-step.tsx
  14. 20
      src/components/create-user-step/index.ts
  15. 20
      src/components/footer/index.tsx
  16. 24
      src/components/forms/checkbox-field/checkbox-field.tsx
  17. 23
      src/components/forms/checkbox-field/index.ts
  18. 24
      src/components/forms/password-field/index.ts
  19. 89
      src/components/forms/password-field/password-field.tsx
  20. 24
      src/components/forms/select-field/index.ts
  21. 39
      src/components/forms/select-field/select-field.tsx
  22. 13
      src/components/forms/text-field/index.ts
  23. 31
      src/components/forms/text-field/text-field.tsx
  24. 26
      src/components/navigation-menu/index.tsx
  25. 24
      src/components/pages/login/index.ts
  26. 16
      src/components/pages/login/index.tsx
  27. 41
      src/components/pages/login/login.tsx
  28. 18
      src/components/pages/register/index.ts
  29. 61
      src/components/pages/register/register.tsx
  30. 4
      src/components/pages/test/index.ts
  31. 5
      src/reducers/forms.ts
  32. 6
      src/reducers/registration.ts
  33. 11
      src/selectors/forms.ts
  34. 2
      src/selectors/registration.ts
  35. 5
      src/types/index.ts
  36. 5
      src/types/store.ts
  37. 5
      webpack.config.ts

28
package-lock.json

@ -243,6 +243,15 @@
"csstype": "^2.2.0"
}
},
"@types/react-avatar-editor": {
"version": "10.3.4",
"resolved": "https://registry.npmjs.org/@types/react-avatar-editor/-/react-avatar-editor-10.3.4.tgz",
"integrity": "sha512-yulle6pZw+7jCxq156WbIqT0qwuLtzxt53fCIm1op0IFUotChAYYSZmKQR2+4jjhToHrX53nR83Slvr8H6ar5Q==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-dom": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.0.tgz",
@ -416,6 +425,12 @@
}
}
},
"@types/zxcvbn": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.0.tgz",
"integrity": "sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==",
"dev": true
},
"@webassemblyjs/ast": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@ -6110,6 +6125,14 @@
"prop-types": "^15.6.2"
}
},
"react-avatar-editor": {
"version": "11.0.7",
"resolved": "https://registry.npmjs.org/react-avatar-editor/-/react-avatar-editor-11.0.7.tgz",
"integrity": "sha512-GbNYBd1/L1QyuU9VRvOW0hSkW1R0XSneOWZFgqI5phQf6dX+dF/G3/AjiJ0hv3JWh2irMQ7DL0oYDKzwtTnNBQ==",
"requires": {
"prop-types": "^15.5.8"
}
},
"react-dom": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz",
@ -8786,6 +8809,11 @@
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
},
"zxcvbn": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
"integrity": "sha1-KOwXzwl0PtyrBW3dixsGJizHPDA="
}
}
}

10
package.json

@ -5,7 +5,9 @@
"private": true,
"scripts": {
"start": "webpack-dev-server --config webpack.config.ts",
"build": "webpack --config webpack.config.ts",
"build": "npm run build:dev",
"build:dev": "webpack --config webpack.config.ts",
"build:prod": "webpack --mode production --config webpack.config.ts",
"deploy:batch:dev": "az storage blob upload-batch -s ./dist/ -d $web --account-name flexordev",
"deploy:batch:prod": "az storage blob upload-batch -s ./dist/ -d $web --account-name flexor",
"deploy:config:dev": "az storage blob upload -f ./config/dev.json -c flexordev -n config.json",
@ -19,6 +21,7 @@
"@types/lodash": "^4.14.138",
"@types/mini-css-extract-plugin": "^0.8.0",
"@types/react": "^16.9.2",
"@types/react-avatar-editor": "^10.3.4",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.2",
"@types/react-router-dom": "^4.3.5",
@ -26,6 +29,7 @@
"@types/uuid": "^3.4.5",
"@types/webpack": "^4.39.1",
"@types/webpack-dev-server": "^3.1.7",
"@types/zxcvbn": "^4.4.0",
"bulma": "^0.7.5",
"css-loader": "^3.2.0",
"html-webpack-plugin": "^3.2.0",
@ -51,6 +55,7 @@
"lodash": "^4.17.15",
"normalizr": "^3.4.1",
"react": "^16.9.0",
"react-avatar-editor": "^11.0.7",
"react-dom": "^16.9.0",
"react-redux": "^7.1.1",
"react-router-dom": "^5.0.1",
@ -58,6 +63,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"uuid": "^3.3.3"
"uuid": "^3.3.3",
"zxcvbn": "^4.4.2"
}
}

61
src/actions/registration.ts

@ -1,13 +1,62 @@
import { Action } from 'redux'
import { setFieldNotification } from 'src/actions/forms'
import { startRequest, finishRequest } from 'src/actions/requests'
import { fetchGroupAvailability } from 'src/api/registration'
import { AppThunkAction } from 'src/types'
export interface SetPageAction extends Action {
type: 'REGISTRATION_SET_PAGE'
const FETCH_GROUP_AVAILABILITY_ID = 'FETCH_GROUP_AVAILABILITY'
const FETCH_USER_AVAILABILITY_ID = 'FETCH_USER_AVAILABILITY'
export interface SetStepAction extends Action {
type: 'REGISTRATION_SET_STEP'
payload: number
}
export type RegistrationActions = SetPageAction
export type RegistrationActions = SetStepAction
export const setPage = (page: number): SetPageAction => ({
type: 'REGISTRATION_SET_PAGE',
payload: page,
export const setStep = (step: number): SetStepAction => ({
type: 'REGISTRATION_SET_STEP',
payload: step,
})
export const checkGroupAvailability = (name: string): AppThunkAction => {
return async dispatch => {
dispatch(startRequest(FETCH_GROUP_AVAILABILITY_ID))
try {
const response = await fetchGroupAvailability(name)
if (response.available) {
dispatch(setFieldNotification('group-name', 'success', `${response.id} is available`))
} else {
dispatch(setFieldNotification('group-name', 'error', `${response.id} isn't available`))
}
dispatch(finishRequest(FETCH_GROUP_AVAILABILITY_ID, true))
} catch (err) {
dispatch(finishRequest(FETCH_GROUP_AVAILABILITY_ID, false))
throw err
}
}
}
export const checkUserAvailability = (name: string): AppThunkAction => {
return async dispatch => {
dispatch(startRequest(FETCH_USER_AVAILABILITY_ID))
try {
const response = await fetchGroupAvailability(name)
if (response.available) {
dispatch(setFieldNotification('user-id', 'success', `${response.id} is available`))
} else {
dispatch(setFieldNotification('user-id', 'error', `${response.id} isn't available`))
}
dispatch(finishRequest(FETCH_USER_AVAILABILITY_ID, true))
} catch (err) {
dispatch(finishRequest(FETCH_USER_AVAILABILITY_ID, false))
throw err
}
}
}

26
src/api/registration.ts

@ -0,0 +1,26 @@
import { fetch } from './fetch'
interface AvailabilityResponse {
id: string
available: boolean
}
export async function fetchGroupAvailability(name: string) {
return await fetch<AvailabilityResponse>({
path: '/api/group/available',
method: 'post',
body: {
name,
},
})
}
export async function fetchUserAvailability(name: string) {
return await fetch<AvailabilityResponse>({
path: '/api/user/available',
method: 'post',
body: {
name,
},
})
}

6
src/components/app/app.scss

@ -20,8 +20,8 @@ $title-weight: 400;
@import "../../../node_modules/bulma/sass/base/_all.sass";
@import "../../../node_modules/bulma/sass/form/_all.sass";
@import "../../../node_modules/bulma/sass/grid/columns.sass";
// @import "../../../node_modules/bulma/sass/grid/tiles.sass";
@import "../../../node_modules/bulma/sass/elements/button.sass";
@import "../../../node_modules/bulma/sass/elements/icon.sass";
@import "../../../node_modules/bulma/sass/elements/notification.sass";
@import "../../../node_modules/bulma/sass/elements/other.sass";
@import "../../../node_modules/bulma/sass/elements/title.sass";
@ -48,6 +48,10 @@ div.main-content {
div#navigation {
flex-grow: 1;
div {
margin: 1rem 0px;
}
}
footer {

26
src/components/app/app.tsx

@ -1,6 +1,8 @@
import React, { FC } from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
import Footer from '../footer'
import NavigationMenu from '../navigation-menu'
import NotificationContainer from '../notification-container'
import Spinner from '../spinner'
import UserInfo from '../user-info'
@ -21,10 +23,8 @@ interface Props {
fetching: boolean
}
const Divider: FC = () => <>&nbsp;&nbsp;&#9900;&nbsp;&nbsp;</>
const App: FC<Props> = ({ collapsed, fetching }) => {
const mainMenuWidth = 300
const mainMenuWidth = 250
const mainColumnLeftMargin = collapsed ? 0 : mainMenuWidth
return (
@ -36,27 +36,11 @@ const App: FC<Props> = ({ collapsed, fetching }) => {
<hr className="has-background-grey-lighter" />
</div>
<div id="navigation">
<p>
<Link className="has-text-white" to="/">Timeline</Link>
</p>
</div>
<NavigationMenu />
{fetching && <Spinner />}
<UserInfo />
<footer>
<div className="content has-text-centered has-text-white is-size-7">
<Link className="has-text-white is-inline-block" to="/">Home</Link>
<Divider />
<Link className="has-text-white is-inline-block" to="/developers">Developers</Link>
<Divider />
<Link className="has-text-white is-inline-block" to="/about">About</Link>
<p>&copy; 2019 Flexor.cc</p>
</div>
</footer>
<Footer />
</div>
<div id="main-column" style={{ marginLeft: mainColumnLeftMargin }}>

55
src/components/create-group-form/create-group-form.tsx

@ -1,28 +1,53 @@
import React, { FC, useEffect } from 'react'
import TextField from '../text-field'
import React, { FC, FocusEventHandler } from 'react'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload } from '@fortawesome/free-solid-svg-icons'
import CheckboxField from '../forms/checkbox-field'
import TextField from '../forms/text-field'
import SelectField from '../forms/select-field'
interface Props {
initForm: () => void
checkAvailability: FocusEventHandler<HTMLInputElement>
}
const CreateGroupForm: FC<Props> = ({ initForm }) => {
useEffect(() => {
initForm()
}, [])
const CreateGroupForm: FC<Props> = ({ checkAvailability }) => {
const registrationOptions = {
open: 'Anyone can join',
approval: 'Users must be approved',
closed: 'Registration closed',
}
return (
<div className="container">
<TextField name="name" label="Community Name" />
<TextField name="group-name" label="Community Name" onBlur={checkAvailability} />
<br />
<SelectField name="group-registration" label="Registration" options={registrationOptions} />
<br />
<div className="field">
<label className="label">Registration Type</label>
<div className="select is-small">
<select>
<option>Anyone can join</option>
<option>Invite only</option>
</select>
<label className="label">Community Image</label>
<div className="file is-primary is-small">
<label className="file-label">
<input className="file-input" type="file" name="image" />
<span className="file-cta">
<span className="file-icon">
<FontAwesomeIcon icon={faUpload} />
</span>
<span className="file-label">
Choose an image...
</span>
</span>
</label>
</div>
<p className="help">Image must be smaller than 5 MBs.</p>
</div>
<br />
<CheckboxField name="group-agree">
I agree to the Community <Link to="/terms">terms and conditions</Link>.
</CheckboxField>
</div>
)
}

25
src/components/create-group-form/index.ts

@ -1,21 +1,26 @@
import { Dispatch } from 'redux'
import { FocusEventHandler } from 'react'
import { connect } from 'react-redux'
import { initField } from 'src/actions/forms'
import { AppState } from 'src/types'
import { checkGroupAvailability } from 'src/actions/registration'
import { AppState, AppThunkDispatch } from 'src/types'
import CreateGroupForm from './create-group-form'
const mapStateToProps = (state: AppState) => ({
})
const mapDispatchToProps = (dispatch: AppThunkDispatch) => {
const checkAvailability: FocusEventHandler<HTMLInputElement> = event => {
const value = event.target.value
const mapDispatchToProps = (dispatch: Dispatch) => ({
initForm: () => {
dispatch(initField('name'))
if (value.length > 3) {
dispatch(checkGroupAvailability(event.target.value))
}
}
})
return {
checkAvailability,
}
}
export default connect(
mapStateToProps,
null,
mapDispatchToProps
)(CreateGroupForm)

30
src/components/create-group-step/create-group-step.tsx

@ -0,0 +1,30 @@
import React, { FC } from 'react'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
import CreateGroupForm from '../create-group-form'
export interface Props {
name: string
registration: string
next?: (name: string, registration: string) => void
}
const CreateGroupStep: FC<Props> = ({ name, registration, next = noop }) => (
<div className="columns">
<div className="column is-8 is-offset-2">
<CreateGroupForm />
<br /><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>
</div>
)
export default CreateGroupStep

37
src/components/create-group-step/index.ts

@ -0,0 +1,37 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldNotification } from 'src/actions/forms'
import { setStep } from 'src/actions/registration'
import { getFieldValue } from 'src/selectors/forms'
import { AppState } 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', ''),
})
const mapDispatchToProps = (dispatch: Dispatch) => ({
next: (name: string) => {
if (!name) {
dispatch(setFieldNotification('group-name', 'error', 'This is required'))
return
}
if (name.length > MAX_ID_LENGTH) {
dispatch(setFieldNotification('group-name', 'error', `This must be less than ${MAX_ID_LENGTH} characters`))
return
}
dispatch(setStep(1))
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(CreateGroupStep)

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

@ -1,20 +1,29 @@
import React, { FC, useEffect } from 'react'
import TextField from '../text-field'
import React, { FC, FocusEventHandler } from 'react'
import { Link } from 'react-router-dom'
import { faEnvelope, faIdCard } from '@fortawesome/free-solid-svg-icons'
import CheckboxField from '../forms/checkbox-field'
import TextField from '../forms/text-field'
import PasswordField from '../forms/password-field'
interface Props {
initForm: () => void
checkAvailability: FocusEventHandler<HTMLInputElement>
}
const CreateUserForm: FC<Props> = ({ initForm }) => {
useEffect(() => {
initForm()
}, [])
const CreateUserForm: FC<Props> = ({ checkAvailability }) => {
return (
<div className="container">
<TextField name="username" label="Username" placeholder="Your Username/ID" />
<TextField name="name" label="Display Name" placeholder="Whatever you want to go by" />
<TextField name="email" label="Email Address" placeholder="Your email address" />
<TextField icon={faIdCard} name="user-id" label="Username" placeholder="Your Username/ID" onBlur={checkAvailability} />
<br />
<TextField name="user-name" label="Display Name" placeholder="Whatever you want to go by" />
<br />
<TextField type="email" icon={faEnvelope} name="user-email" label="Email Address" placeholder="Your email address" />
<br />
<PasswordField placeholder="Your new password" />
<br />
<CheckboxField name="group-agree">
I agree to the User <Link to="/terms">terms and conditions</Link>.
</CheckboxField>
</div>
)
}

27
src/components/create-user-form/index.ts

@ -1,23 +1,26 @@
import { Dispatch } from 'redux'
import { FocusEventHandler } from 'react'
import { connect } from 'react-redux'
import { initField } from 'src/actions/forms'
import { AppState } from 'src/types'
import { checkUserAvailability } from 'src/actions/registration'
import { AppThunkDispatch } from 'src/types'
import CreateUserForm from './create-user-form'
const mapStateToProps = (state: AppState) => ({
})
const mapDispatchToProps = (dispatch: AppThunkDispatch) => {
const checkAvailability: FocusEventHandler<HTMLInputElement> = event => {
const value = event.target.value
const mapDispatchToProps = (dispatch: Dispatch) => ({
initForm: () => {
dispatch(initField('username'))
dispatch(initField('name'))
dispatch(initField('email'))
if (value.length > 3) {
dispatch(checkUserAvailability(event.target.value))
}
}
})
return {
checkAvailability,
}
}
export default connect(
mapStateToProps,
null,
mapDispatchToProps
)(CreateUserForm)

29
src/components/create-user-step/create-user-step.tsx

@ -0,0 +1,29 @@
import React, { FC } from 'react'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'
import CreateUserForm from '../create-user-form'
export interface Props {
previous?: () => void
next?: () => 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>
</div>
)
export default CreateUserStep

20
src/components/create-user-step/index.ts

@ -0,0 +1,20 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setStep } from 'src/actions/registration'
import CreateUserStep from './create-user-step'
const mapDispatchToProps = (dispatch: Dispatch) => ({
previous: () => {
dispatch(setStep(0))
},
next: () => {
},
})
export default connect(
null,
mapDispatchToProps
)(CreateUserStep)

20
src/components/footer/index.tsx

@ -0,0 +1,20 @@
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
const Divider: FC = () => <>&nbsp;&nbsp;&#9900;&nbsp;&nbsp;</>
const Footer: FC = () => (
<footer>
<div className="content has-text-centered has-text-white is-size-7">
<Link className="has-text-white is-inline-block" to="/">Home</Link>
<Divider />
<Link className="has-text-white is-inline-block" to="/developers">Developers</Link>
<Divider />
<Link className="has-text-white is-inline-block" to="/about">About</Link>
<p>&copy; 2019 Flexor.cc</p>
</div>
</footer>
)
export default Footer

24
src/components/forms/checkbox-field/checkbox-field.tsx

@ -0,0 +1,24 @@
import React, { FC } from 'react'
import noop from 'lodash/noop'
export interface Props {
name: string
value?: boolean
setValue?: (value: boolean) => void
}
const PasswordField: FC<Props> = ({
value = false,
setValue = noop,
children,
}) => {
return (
<label className="checkbox">
<input type="checkbox" checked={value} onChange={(e) => setValue(e.target.checked)} />
&nbsp;&nbsp;
{children}
</label>
)
}
export default PasswordField

23
src/components/forms/checkbox-field/index.ts

@ -0,0 +1,23 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue } from 'src/selectors/forms'
import { AppState } from 'src/types'
import CheckboxField, { Props } from './checkbox-field'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
value: getFieldValue<boolean>(state, ownProps.name, false),
})
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({
setValue: (value: boolean) => {
dispatch(setFieldValue(ownProps.name, value))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(CheckboxField)

24
src/components/forms/password-field/index.ts

@ -0,0 +1,24 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState } from 'src/types'
import PasswordField from './password-field'
const mapStateToProps = (state: AppState) => ({
value: getFieldValue<string>(state, 'password', ''),
notification: getFieldNotification(state, 'password'),
})
const mapDispatchToProps = (dispatch: Dispatch) => ({
setValue: (value: string) => {
dispatch(setFieldValue('password', value))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(PasswordField)

89
src/components/forms/password-field/password-field.tsx

@ -0,0 +1,89 @@
import React, { FC, ReactNode } from 'react'
import classNames from 'classnames'
import noop from 'lodash/noop'
import zxcvbn from 'zxcvbn'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faKey, faExclamationTriangle, faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { FormNotification, ClassDictionary } from 'src/types'
export interface Props {
placeholder?: string
userInputs?: string[]
value?: string
notification?: FormNotification
setValue?: (value: string) => void
}
const PasswordField: FC<Props> = ({
placeholder,
userInputs = [],
value = '',
notification,
setValue = noop,
}) => {
const inputClassDictionary: ClassDictionary = { input: true }
const controlClassDictionary: ClassDictionary = { control: true, 'has-icons-left': true }
let icon: IconDefinition | undefined
let passwordMessage: ReactNode | undefined
if (value) {
const { score } = zxcvbn(value, userInputs)
switch (score) {
case 0:
inputClassDictionary['is-danger'] = true
controlClassDictionary['has-icons-right'] = true
icon = faExclamationTriangle
passwordMessage = <span>Strength: <span className="has-text-danger">Unusable</span></span>
break
case 1:
inputClassDictionary['is-danger'] = true
passwordMessage = <span>Strength: <span className="has-text-danger">Not good</span></span>
break
case 2:
inputClassDictionary['is-warning'] = true
passwordMessage = <span>Strength: <span className="has-text-warning">OK</span></span>
break
case 3:
inputClassDictionary['is-success'] = true
passwordMessage = <span>Strength: <span className="has-text-success">Good!</span></span>
break
case 4:
inputClassDictionary['is-success'] = true
controlClassDictionary['has-icons-right'] = true
icon = faCheckCircle
passwordMessage = <span>Strength: <span className="has-text-success">🔥</span></span>
break
}
}
const helpText = () => {
if (notification) return <p className="help">{notification.message}</p>
if (passwordMessage) return <p className="help">{passwordMessage}</p>
}
return (
<div className="field">
<label className="label">Password</label>
<div className={classNames(controlClassDictionary)}>
<input className={classNames(inputClassDictionary)} type="password" placeholder={placeholder} value={value} onChange={(e) => setValue(e.target.value)} />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faKey} />
</span>
{icon &&
<span className="icon is-small is-right">
<FontAwesomeIcon icon={icon} />
</span>
}
</div>
{notification &&
<p className="help">{notification.message}</p>
}
{helpText()}
</div>
)
}
export default PasswordField

24
src/components/forms/select-field/index.ts

@ -0,0 +1,24 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState } from 'src/types'
import SelectField, { Props } from './select-field'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
value: getFieldValue<string>(state, ownProps.name, ''),
notification: getFieldNotification(state, ownProps.name),
})
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({
setValue: (value: string) => {
dispatch(setFieldValue(ownProps.name, value))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(SelectField)

39
src/components/forms/select-field/select-field.tsx

@ -0,0 +1,39 @@
import React, { FC } from 'react'
import noop from 'lodash/noop'
import { FormNotification } from 'src/types'
interface SelectOptions {
[value: string]: string
}
export interface Props {
name: string
label: string
options: SelectOptions
value?: string
notification?: FormNotification
setValue?: (value: string) => void
}
const SelectField: FC<Props> = ({
label,
options = {},
value = '',
setValue = noop,
}) => {
const opts = Object.entries(options)
return (
<div className="field">
<label className="label">{label}</label>
<div className="select is-small">
<select value={value} onChange={(e) => setValue(e.target.value)}>
{opts.map(([key, value]) => <option key={key} value={key}>{value}</option>)}
</select>
</div>
</div>
)
}
export default SelectField

13
src/components/text-field/index.ts → src/components/forms/text-field/index.ts

@ -1,15 +1,24 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState } from 'src/types'
import TextField, { Props } from './text-field'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
value: getFieldValue(state, ownProps.name),
value: getFieldValue<string>(state, ownProps.name, ''),
notification: getFieldNotification(state, ownProps.name),
})
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({
setValue: (value: string) => {
dispatch(setFieldValue(ownProps.name, value))
}
})
export default connect(
mapStateToProps
mapStateToProps,
mapDispatchToProps
)(TextField)

31
src/components/text-field/text-field.tsx → src/components/forms/text-field/text-field.tsx

@ -1,10 +1,11 @@
import React, { FC } from 'react'
import React, { FC, FocusEventHandler } from 'react'
import classNames from 'classnames'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { notificationTypeToClassName } from 'src/utils'
import { FormNotification } from 'src/types'
import { FormNotification, ClassDictionary } from 'src/types'
export interface Props {
name: string
@ -14,31 +15,37 @@ export interface Props {
icon?: IconDefinition
value?: string
notification?: FormNotification
setValue?: (value: string) => void
onBlur?: FocusEventHandler
}
const RegisterForm: FC<Props> = ({
const TextField: FC<Props> = ({
label,
type = 'text',
placeholder,
icon,
value,
value = '',
notification,
setValue = noop,
onBlur = noop,
}) => {
const controlClassNames = classNames({ control: true, 'has-icons-left': !!icon })
let helpClassNames: string | undefined
const helpClassDictionary: ClassDictionary = {}
const inputClassDictionary: ClassDictionary = { input: true }
if (notification) {
helpClassNames = classNames({
help: true,
[notificationTypeToClassName(notification.type)]: true,
})
const ncn = notificationTypeToClassName(notification.type)
helpClassDictionary['help'] = true
helpClassDictionary[ncn] = true,
inputClassDictionary[ncn] = true
}
return (
<div className="field">
<label className="label">{label}</label>
<div className={controlClassNames}>
<input className="input" type={type} placeholder={placeholder} value={value} />
<input className={classNames(inputClassDictionary)} type={type} placeholder={placeholder} value={value} onChange={(e) => setValue(e.target.value)} onBlur={onBlur} />
{icon &&
<span className="icon is-small is-left">
<FontAwesomeIcon icon={icon} />
@ -46,10 +53,10 @@ const RegisterForm: FC<Props> = ({
}
</div>
{notification &&
<p className={helpClassNames}>{notification.message}</p>
<p className={classNames(helpClassDictionary)}>{notification.message}</p>
}
</div>
)
}
export default RegisterForm
export default TextField

26
src/components/navigation-menu/index.tsx

@ -0,0 +1,26 @@
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faStream, faPaperPlane } from '@fortawesome/free-solid-svg-icons'
const NavigationMenu: FC = () => (
<div id="navigation">
<div>
<span className="icon has-text-white">
<FontAwesomeIcon icon={faStream} />
</span>
&nbsp;
<Link className="has-text-white" to="/">Timeline</Link>
</div>
<div>
<span className="icon has-text-white">
<FontAwesomeIcon icon={faPaperPlane} />
</span>
&nbsp;
<Link className="has-text-white" to="/">Apps</Link>
</div>
</div>
)
export default NavigationMenu

24
src/components/pages/login/index.ts

@ -0,0 +1,24 @@
import { connect } from 'react-redux'
import { getStep } from 'src/selectors/registration'
import { initForm, initField } from 'src/actions/forms'
import { AppState, AppThunkDispatch } from 'src/types'
import Login from './login'
const mapStateToProps = (state: AppState) => ({
step: getStep(state),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
initForm: () => {
dispatch(initForm())
dispatch(initField('name'))
dispatch(initField('password'))
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Login)

16
src/components/pages/login/index.tsx

@ -1,16 +0,0 @@
import React, { FC } from 'react'
import TextField from 'src/components/text-field'
const Login: FC = () => {
return (
<div className="main-content">
<h1 className="title">Login</h1>
<div>
<TextField name="username" label="Username" placeholder="Your Username/ID" />
</div>
</div>
)
}
export default Login

41
src/components/pages/login/login.tsx

@ -0,0 +1,41 @@
import React, { FC, useEffect } from 'react'
import { faIdCard } from '@fortawesome/free-solid-svg-icons'
import TextField from 'src/components/forms/text-field'
import PasswordField from 'src/components/forms/password-field'
interface Props {
initForm: () => void
}
const Login: FC<Props> = ({ initForm }) => {
useEffect(() => {
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>
<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>
</div>
</div>
)
}
export default Login

18
src/components/pages/register/index.ts

@ -1,18 +1,24 @@
import { connect } from 'react-redux'
import { getPage } from 'src/selectors/registration'
import { setPage } from 'src/actions/registration'
import { getStep } from 'src/selectors/registration'
import { initForm, initField } from 'src/actions/forms'
import { AppState, AppThunkDispatch } from 'src/types'
import Register from './register'
const mapStateToProps = (state: AppState) => ({
page: getPage(state),
step: getStep(state),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
setPage: (page: number) => {
dispatch(setPage(page))
}
initForm: () => {
dispatch(initForm())
dispatch(initField('group-name'))
dispatch(initField('group-registration'))
dispatch(initField('user-id'))
dispatch(initField('user-name'))
dispatch(initField('user-email'))
dispatch(initField('password'))
},
})
export default connect(

61
src/components/pages/register/register.tsx

@ -1,70 +1,49 @@
import React, { FC } from 'react'
import React, { FC, useEffect } from 'react'
import CreateGroupForm from '../../create-group-form'
import CreateUserForm from '../../create-user-form'
import CreateGroupStep from '../../create-group-step'
import CreateUserStep from '../../create-user-step'
interface PageProps {
setPage: (page: number) => void
}
interface Page {
interface Step {
title: string
component: FC<PageProps>
component: FC
}
const pages: Page[] = [
const steps: Step[] = [
{
title: 'Create a Community',
component: ({ setPage }) => {
return (
<div className="columns">
<div className="column is-10 is-offset-1">
<CreateGroupForm />
<br />
<button className="button is-pulled-right is-primary" onClick={() => setPage(1)}>Next: Your Account</button>
</div>
</div>
)
},
component: () => <CreateGroupStep />,
},
{
title: 'Create an Account',
component: ({ setPage }) => {
return (
<div className="columns">
<div className="column is-10 is-offset-1">
<CreateUserForm />
<br />
<button className="button is-pulled-left is-warning" onClick={() => setPage(0)}>Back: Community</button>
<button className="button is-pulled-right is-primary">Finish</button>
</div>
</div>
)
},
title: 'Create Your Account',
component: () => <CreateUserStep />,
},
]
export interface Props {
page: number
setPage: (page: number) => void
step: number
initForm: () => void
}
const Register: FC<Props> = ({ page: index, setPage }) => {
const page = pages[index]
const Component = page.component
const Register: FC<Props> = ({ step: index, initForm }) => {
const step = steps[index]
const Component = step.component
useEffect(() => {
initForm()
}, [])
return (
<div>
<section className="hero is-dark is-bold">
<div className="hero-body">
<div className="container">
<h1 className="title">{page.title}</h1>
<h1 className="title">{step.title}</h1>
</div>
</div>
</section>
<div className="main-content">
<Component setPage={setPage} />
<Component />
</div>
</div>
)

4
src/components/pages/test/index.ts

@ -4,8 +4,6 @@ import { AppThunkDispatch, NotificationType } from 'src/types'
import Test from './test'
const mapStateToProps = () => ({})
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
show: async (type: NotificationType, content: string) => {
dispatch(showNotification(type, content))
@ -13,6 +11,6 @@ const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
})
export default connect(
mapStateToProps,
null,
mapDispatchToProps
)(Test)

5
src/reducers/forms.ts

@ -50,13 +50,14 @@ const reducer: Reducer<FormsState, FormsActions> = (state = initialState, action
},
}
case 'FORMS_SET_FIELD_NOTIFICATION': {
const field = state.form[action.payload.name]
const { name, type, message } = action.payload
const field = state.form[name]
return {
...state,
form: {
...state.form,
[action.payload.name]: {
[name]: {
...field,
notification: {
type,

6
src/reducers/registration.ts

@ -4,15 +4,15 @@ import { RegistrationActions } from '../actions/registration'
import { RegistrationState } from '../types'
const initialState: RegistrationState = {
page: 0,
step: 0,
}
const reducer: Reducer<RegistrationState, RegistrationActions> = (state = initialState, action) => {
switch (action.type) {
case 'REGISTRATION_SET_PAGE':
case 'REGISTRATION_SET_STEP':
return {
...state,
page: action.payload,
step: action.payload,
}
default:
return state

11
src/selectors/forms.ts

@ -1,10 +1,15 @@
import { AppState } from '../types'
import { AppState, FormValue } from '../types'
export const getForm = (state: AppState) => state.forms.form
export const getFieldValue = (state: AppState, name: string) => {
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]
return field ? field.value : undefined
if (!field) return defaultValue
if (field.value === undefined) return defaultValue
return field.value as T
}
export const getFieldNotification = (state: AppState, name: string) => {

2
src/selectors/registration.ts

@ -1,3 +1,3 @@
import { AppState } from 'src/types'
export const getPage = (state: AppState) => state.registration.page
export const getStep = (state: AppState) => state.registration.step

5
src/types/index.ts

@ -11,6 +11,10 @@ export interface FetchOptions {
headers?: HeadersInit
}
export interface ClassDictionary {
[name: string]: boolean
}
export {
Entity,
Group,
@ -21,6 +25,7 @@ export {
export {
NotificationType,
FormValue,
FormNotification,
APIRequest,
APIRequestCollection,

5
src/types/store.ts

@ -2,6 +2,7 @@
import { EntityStore } from './entities'
export type NotificationType = 'info' | 'success' | 'error'
export type FormValue = string | number | boolean
export interface FormNotification {
field?: string
@ -38,7 +39,7 @@ export interface MenuState {
export interface FormField {
name: string
value?: string
value?: FormValue
notification?: FormNotification
}
@ -57,7 +58,7 @@ export interface DirectoryState {
}
export interface RegistrationState {
page: number
step: number
}
export type RequestsState = APIRequestCollection

5
webpack.config.ts

@ -15,6 +15,11 @@ const config: Configuration = {
publicPath: '/',
filename: '[name].js',
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
devServer: {
contentBase: `${__dirname}/dist`,
historyApiFallback: true,

Loading…
Cancel
Save