Dwayne Harris 5 years ago
parent
commit
100f73f4c8
  1. 105
      package-lock.json
  2. 3
      package.json
  3. 59
      src/actions/authentication.ts
  4. 15
      src/actions/directory.ts
  5. 35
      src/actions/registration.ts
  6. 10
      src/api/errors.ts
  7. 4
      src/api/fetch.ts
  8. 2
      src/components/app/app.tsx
  9. 2
      src/components/create-group-step/create-group-step.tsx
  10. 10
      src/components/create-group-step/index.ts
  11. 22
      src/components/create-user-step/index.ts
  12. 4
      src/components/forms/password-field/password-field.tsx
  13. 22
      src/components/notification-container/notification-container.tsx
  14. 2
      src/components/page-header/index.tsx
  15. 46
      src/components/pages/login/index.ts
  16. 44
      src/components/pages/login/login.tsx
  17. 4
      src/components/pages/register-group/index.ts
  18. 4
      src/components/pages/register/index.ts
  19. 31
      src/components/pages/self/index.ts
  20. 37
      src/components/pages/self/self.tsx
  21. 16
      src/components/pages/test/index.ts
  22. 26
      src/components/pages/test/test.tsx
  23. 18
      src/components/user-info/index.ts
  24. 9
      src/constants/index.ts
  25. 8
      src/hooks/index.ts
  26. 6
      src/reducers/authentication.ts
  27. 15
      src/selectors/authentication.ts
  28. 6
      src/selectors/directory.ts
  29. 19
      src/selectors/entities.ts
  30. 3
      src/selectors/requests.ts
  31. 2
      src/store/schemas.ts
  32. 5
      src/types/entities.ts
  33. 20
      src/types/store.ts
  34. 7
      src/utils/index.ts
  35. 2
      webpack.config.ts

105
package-lock.json

@ -393,6 +393,15 @@
} }
} }
}, },
"@types/webpack-bundle-analyzer": {
"version": "2.13.3",
"resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.13.3.tgz",
"integrity": "sha512-p8EXyKfq311FFFfRuAR9tOHFFTQ9DqGrjRQYXbjjEMfl9pKGaTtRy1zFJtPMyZHfRoqh5rsYPVSVknkl004M7A==",
"dev": true,
"requires": {
"@types/webpack": "*"
}
},
"@types/webpack-dev-server": { "@types/webpack-dev-server": {
"version": "3.1.7", "version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.1.7.tgz", "resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.1.7.tgz",
@ -641,6 +650,12 @@
"integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==",
"dev": true "dev": true
}, },
"acorn-walk": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==",
"dev": true
},
"ajv": { "ajv": {
"version": "6.10.2", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
@ -1084,6 +1099,18 @@
"tweetnacl": "^0.14.3" "tweetnacl": "^0.14.3"
} }
}, },
"bfj": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz",
"integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
"check-types": "^8.0.3",
"hoopy": "^0.1.4",
"tryer": "^1.0.1"
}
},
"big.js": { "big.js": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@ -1397,6 +1424,12 @@
"supports-color": "^5.3.0" "supports-color": "^5.3.0"
} }
}, },
"check-types": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
"integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==",
"dev": true
},
"chokidar": { "chokidar": {
"version": "2.1.8", "version": "2.1.8",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
@ -2188,6 +2221,12 @@
"domelementtype": "1" "domelementtype": "1"
} }
}, },
"duplexer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
"dev": true
},
"duplexify": { "duplexify": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@ -2216,6 +2255,12 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
"dev": true "dev": true
}, },
"ejs": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.1.tgz",
"integrity": "sha512-kS/gEPzZs3Y1rRsbGX4UOSjtP/CeJP0CxSNZHYxGfVM/VgLcv0ZqM7C45YyTj2DI2g7+P9Dd24C+IMIg6D0nYQ==",
"dev": true
},
"elliptic": { "elliptic": {
"version": "6.5.1", "version": "6.5.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz",
@ -2622,6 +2667,12 @@
"integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
"dev": true "dev": true
}, },
"filesize": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
"integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
"dev": true
},
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -3668,6 +3719,16 @@
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
}, },
"gzip-size": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
"integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==",
"dev": true,
"requires": {
"duplexer": "^0.1.1",
"pify": "^4.0.1"
}
},
"handle-thing": { "handle-thing": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
@ -3853,6 +3914,12 @@
"parse-passwd": "^1.0.0" "parse-passwd": "^1.0.0"
} }
}, },
"hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
"integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==",
"dev": true
},
"hosted-git-info": { "hosted-git-info": {
"version": "2.8.4", "version": "2.8.4",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz",
@ -5029,6 +5096,11 @@
} }
} }
}, },
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"move-concurrently": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -5500,6 +5572,12 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"opener": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz",
"integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==",
"dev": true
},
"opn": { "opn": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
@ -7855,6 +7933,12 @@
"glob": "^7.1.2" "glob": "^7.1.2"
} }
}, },
"tryer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
"dev": true
},
"ts-loader": { "ts-loader": {
"version": "6.1.2", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.1.2.tgz", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.1.2.tgz",
@ -8347,6 +8431,27 @@
} }
} }
}, },
"webpack-bundle-analyzer": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.5.1.tgz",
"integrity": "sha512-CDdaT3TTu4F9X3tcDq6PNJOiNGgREOM0WdN2vVAoUUn+M6NLB5kJ543HImCWbrDwOpbpGARSwU8r+u0Pl367kA==",
"dev": true,
"requires": {
"acorn": "^6.0.7",
"acorn-walk": "^6.1.1",
"bfj": "^6.1.1",
"chalk": "^2.4.1",
"commander": "^2.18.0",
"ejs": "^2.6.1",
"express": "^4.16.3",
"filesize": "^3.6.1",
"gzip-size": "^5.0.0",
"lodash": "^4.17.15",
"mkdirp": "^0.5.1",
"opener": "^1.5.1",
"ws": "^6.0.0"
}
},
"webpack-cli": { "webpack-cli": {
"version": "3.3.9", "version": "3.3.9",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.9.tgz", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.9.tgz",

3
package.json

@ -28,6 +28,7 @@
"@types/redux-logger": "^3.0.7", "@types/redux-logger": "^3.0.7",
"@types/uuid": "^3.4.5", "@types/uuid": "^3.4.5",
"@types/webpack": "^4.39.1", "@types/webpack": "^4.39.1",
"@types/webpack-bundle-analyzer": "^2.13.3",
"@types/webpack-dev-server": "^3.1.7", "@types/webpack-dev-server": "^3.1.7",
"@types/zxcvbn": "^4.4.0", "@types/zxcvbn": "^4.4.0",
"bulma": "^0.7.5", "bulma": "^0.7.5",
@ -42,6 +43,7 @@
"ts-node": "^8.4.1", "ts-node": "^8.4.1",
"typescript": "^3.6.3", "typescript": "^3.6.3",
"webpack": "^4.40.2", "webpack": "^4.40.2",
"webpack-bundle-analyzer": "^3.5.1",
"webpack-cli": "^3.3.9", "webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1" "webpack-dev-server": "^3.8.1"
}, },
@ -53,6 +55,7 @@
"classnames": "^2.2.6", "classnames": "^2.2.6",
"history": "^4.10.1", "history": "^4.10.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"moment": "^2.24.0",
"normalizr": "^3.4.1", "normalizr": "^3.4.1",
"react": "^16.9.0", "react": "^16.9.0",
"react-avatar-editor": "^11.0.7", "react-avatar-editor": "^11.0.7",

59
src/actions/authentication.ts

@ -6,8 +6,13 @@ import { setEntities } from 'src/actions/entities'
import { startRequest, finishRequest } from 'src/actions/requests' import { startRequest, finishRequest } from 'src/actions/requests'
import { userSchema } from 'src/store/schemas' import { userSchema } from 'src/store/schemas'
import { REQUEST_KEYS } from 'src/constants'
import { AppThunkAction, Entity } from 'src/types'
import {
LOCAL_STORAGE_ACCESS_TOKEN_KEY,
LOCAL_STORAGE_REFRESH_TOKEN_KEY,
LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY,
} from 'src/constants'
import { AppThunkAction, Entity, RequestKey } from 'src/types'
export interface SetCheckedAction extends Action { export interface SetCheckedAction extends Action {
type: 'AUTHENTICATION_SET_CHECKED' type: 'AUTHENTICATION_SET_CHECKED'
@ -23,7 +28,11 @@ export interface SetUserAction extends Action {
payload: string payload: string
} }
export type AuthenticationActions = SetCheckedAction | SetAuthenticatedAction | SetUserAction
export interface UnauthenticateAction extends Action {
type: 'AUTHENTICATION_UNAUTHENTICATE'
}
export type AuthenticationActions = SetCheckedAction | SetAuthenticatedAction | SetUserAction | UnauthenticateAction
export const setChecked = (): SetCheckedAction => ({ export const setChecked = (): SetCheckedAction => ({
type: 'AUTHENTICATION_SET_CHECKED', type: 'AUTHENTICATION_SET_CHECKED',
@ -39,8 +48,12 @@ export const setUser = (userId: string): SetUserAction => ({
payload: userId, payload: userId,
}) })
export const unauthenticate = (): UnauthenticateAction => ({
type: 'AUTHENTICATION_UNAUTHENTICATE',
})
export const fetchSelf = (): AppThunkAction => async dispatch => { export const fetchSelf = (): AppThunkAction => async dispatch => {
dispatch(startRequest(REQUEST_KEYS.FETCH_GROUP_AVAILABILITY))
dispatch(startRequest(RequestKey.FetchGroupAvailability))
try { try {
const self = await apiFetch<Entity>({ const self = await apiFetch<Entity>({
@ -53,10 +66,44 @@ export const fetchSelf = (): AppThunkAction => async dispatch => {
dispatch(setUser(self.id)) dispatch(setUser(self.id))
dispatch(setAuthenticated(true)) dispatch(setAuthenticated(true))
dispatch(finishRequest(REQUEST_KEYS.FETCH_GROUP_AVAILABILITY, true))
dispatch(finishRequest(RequestKey.FetchGroupAvailability, true))
} catch (err) { } catch (err) {
dispatch(setAuthenticated(false)) dispatch(setAuthenticated(false))
dispatch(finishRequest(REQUEST_KEYS.FETCH_GROUP_AVAILABILITY, false))
dispatch(finishRequest(RequestKey.FetchGroupAvailability, false))
throw err
}
}
interface AuthenticateResponse {
id: string
access: string
refresh: string
expires: number
}
export const authenticate = (name: string, password: string): AppThunkAction<string> => async dispatch => {
dispatch(startRequest(RequestKey.Authenticate))
try {
const response = await apiFetch<AuthenticateResponse>({
path: '/api/authenticate',
method: 'post',
body: {
id: name,
password,
},
})
localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY, response.access)
localStorage.setItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY, response.refresh)
if (response.expires) localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY, response.expires.toString())
dispatch(finishRequest(RequestKey.Authenticate, true))
await dispatch(fetchSelf())
return response.id
} catch (err) {
dispatch(finishRequest(RequestKey.Authenticate, false))
throw err throw err
} }
} }

15
src/actions/directory.ts

@ -6,9 +6,8 @@ import { setEntity, setEntities } from 'src/actions/entities'
import { startRequest, finishRequest } from 'src/actions/requests' import { startRequest, finishRequest } from 'src/actions/requests'
import { groupSchema } from 'src/store/schemas' import { groupSchema } from 'src/store/schemas'
import { REQUEST_KEYS } from 'src/constants'
import { objectToQuerystring } from 'src/utils' import { objectToQuerystring } from 'src/utils'
import { AppThunkAction, Entity } from 'src/types'
import { AppThunkAction, Entity, RequestKey } from 'src/types'
export interface SetGroupsAction extends Action { export interface SetGroupsAction extends Action {
type: 'DIRECTORY_SET_GROUPS' type: 'DIRECTORY_SET_GROUPS'
@ -44,7 +43,7 @@ export const setContinuation = (continuation: string): SetContinuationAction =>
export const fetchGroup = (id: string): AppThunkAction => { export const fetchGroup = (id: string): AppThunkAction => {
return async dispatch => { return async dispatch => {
dispatch(startRequest(REQUEST_KEYS.FETCH_GROUP))
dispatch(startRequest(RequestKey.FetchGroup))
try { try {
const group = await apiFetch<Entity>({ const group = await apiFetch<Entity>({
@ -52,9 +51,9 @@ export const fetchGroup = (id: string): AppThunkAction => {
}) })
dispatch(setEntity('group', group)) dispatch(setEntity('group', group))
dispatch(finishRequest(REQUEST_KEYS.FETCH_GROUP, true))
dispatch(finishRequest(RequestKey.FetchGroup, true))
} catch (err) { } catch (err) {
dispatch(finishRequest(REQUEST_KEYS.FETCH_GROUP, false))
dispatch(finishRequest(RequestKey.FetchGroup, false))
throw err throw err
} }
} }
@ -66,7 +65,7 @@ interface GroupsResponse {
} }
export const fetchGroups = (sort?: string, continuation?: string): AppThunkAction => async dispatch => { export const fetchGroups = (sort?: string, continuation?: string): AppThunkAction => async dispatch => {
dispatch(startRequest(REQUEST_KEYS.FETCH_GROUPS))
dispatch(startRequest(RequestKey.FetchGroups))
try { try {
const response = await apiFetch<GroupsResponse>({ const response = await apiFetch<GroupsResponse>({
@ -82,9 +81,9 @@ export const fetchGroups = (sort?: string, continuation?: string): AppThunkActio
dispatch(setContinuation(response.continuation)) dispatch(setContinuation(response.continuation))
} }
dispatch(finishRequest(REQUEST_KEYS.FETCH_GROUPS, true))
dispatch(finishRequest(RequestKey.FetchGroups, true))
} catch (err) { } catch (err) {
dispatch(finishRequest(REQUEST_KEYS.FETCH_GROUPS, false))
dispatch(finishRequest(RequestKey.FetchGroups, false))
throw err throw err
} }
} }

35
src/actions/registration.ts

@ -7,10 +7,9 @@ import { startRequest, finishRequest } from 'src/actions/requests'
import { import {
LOCAL_STORAGE_ACCESS_TOKEN_KEY, LOCAL_STORAGE_ACCESS_TOKEN_KEY,
LOCAL_STORAGE_REFRESH_TOKEN_KEY, LOCAL_STORAGE_REFRESH_TOKEN_KEY,
REQUEST_KEYS,
} from 'src/constants' } from 'src/constants'
import { AppThunkAction } from 'src/types'
import { AppThunkAction, NotificationType, RequestKey } from 'src/types'
export interface SetStepAction extends Action { export interface SetStepAction extends Action {
type: 'REGISTRATION_SET_STEP' type: 'REGISTRATION_SET_STEP'
@ -30,7 +29,7 @@ export const setStep = (step: number): SetStepAction => ({
}) })
export const checkGroupAvailability = (name: string): AppThunkAction => async dispatch => { export const checkGroupAvailability = (name: string): AppThunkAction => async dispatch => {
dispatch(startRequest(REQUEST_KEYS.FETCH_GROUP_AVAILABILITY))
dispatch(startRequest(RequestKey.FetchGroupAvailability))
try { try {
const { id, available } = await apiFetch<AvailabilityResponse>({ const { id, available } = await apiFetch<AvailabilityResponse>({
@ -42,20 +41,20 @@ export const checkGroupAvailability = (name: string): AppThunkAction => async di
}) })
if (available) { if (available) {
dispatch(setFieldNotification('group-name', 'success', `${id} is available`))
dispatch(setFieldNotification('group-name', NotificationType.Success, `${id} is available`))
} else { } else {
dispatch(setFieldNotification('group-name', 'error', `${id} isn't available`))
dispatch(setFieldNotification('group-name', NotificationType.Error, `${id} isn't available`))
} }
dispatch(finishRequest(REQUEST_KEYS.FETCH_GROUP_AVAILABILITY, true))
dispatch(finishRequest(RequestKey.FetchGroupAvailability, true))
} catch (err) { } catch (err) {
dispatch(finishRequest(REQUEST_KEYS.FETCH_GROUP_AVAILABILITY, false))
dispatch(finishRequest(RequestKey.FetchGroupAvailability, false))
throw err throw err
} }
} }
export const checkUserAvailability = (name: string): AppThunkAction => async dispatch => { export const checkUserAvailability = (name: string): AppThunkAction => async dispatch => {
dispatch(startRequest(REQUEST_KEYS.FETCH_USER_AVAILABILITY))
dispatch(startRequest(RequestKey.FetchUserAvailability))
try { try {
const { id, available } = await apiFetch<AvailabilityResponse>({ const { id, available } = await apiFetch<AvailabilityResponse>({
@ -67,14 +66,14 @@ export const checkUserAvailability = (name: string): AppThunkAction => async dis
}) })
if (available) { if (available) {
dispatch(setFieldNotification('user-id', 'success', `${id} is available`))
dispatch(setFieldNotification('user-id', NotificationType.Success, `${id} is available`))
} else { } else {
dispatch(setFieldNotification('user-id', 'error', `${id} isn't available`))
dispatch(setFieldNotification('user-id', NotificationType.Error, `${id} isn't available`))
} }
dispatch(finishRequest(REQUEST_KEYS.FETCH_USER_AVAILABILITY, true))
dispatch(finishRequest(RequestKey.FetchUserAvailability, true))
} catch (err) { } catch (err) {
dispatch(finishRequest(REQUEST_KEYS.FETCH_USER_AVAILABILITY, false))
dispatch(finishRequest(RequestKey.FetchUserAvailability, false))
throw err throw err
} }
} }
@ -92,7 +91,7 @@ interface CreateGroupResponse {
export const createGroup = (options: CreateGroupOptions): AppThunkAction<string> => async dispatch => { export const createGroup = (options: CreateGroupOptions): AppThunkAction<string> => async dispatch => {
const { name, registration, about } = options const { name, registration, about } = options
dispatch(startRequest(REQUEST_KEYS.CREATE_GROUP))
dispatch(startRequest(RequestKey.CreateGroup))
try { try {
const { id } = await apiFetch<CreateGroupResponse>({ const { id } = await apiFetch<CreateGroupResponse>({
@ -105,11 +104,11 @@ export const createGroup = (options: CreateGroupOptions): AppThunkAction<string>
}, },
}) })
dispatch(finishRequest(REQUEST_KEYS.CREATE_GROUP, true))
dispatch(finishRequest(RequestKey.CreateGroup, true))
return id return id
} catch (err) { } catch (err) {
dispatch(finishRequest(REQUEST_KEYS.CREATE_GROUP, false))
dispatch(finishRequest(RequestKey.CreateGroup, false))
throw err throw err
} }
} }
@ -131,7 +130,7 @@ interface RegisterResponse {
export const register = (options: RegisterOptions): AppThunkAction<string> => async dispatch => { export const register = (options: RegisterOptions): AppThunkAction<string> => async dispatch => {
const { id, email, password, name, group } = options const { id, email, password, name, group } = options
dispatch(startRequest(REQUEST_KEYS.REGISTER))
dispatch(startRequest(RequestKey.Register))
try { try {
const response = await apiFetch<RegisterResponse>({ const response = await apiFetch<RegisterResponse>({
@ -146,14 +145,14 @@ export const register = (options: RegisterOptions): AppThunkAction<string> => as
}, },
}) })
dispatch(finishRequest(REQUEST_KEYS.REGISTER, true))
dispatch(finishRequest(RequestKey.Register, true))
localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY, response.access) localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY, response.access)
localStorage.setItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY, response.refresh) localStorage.setItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY, response.refresh)
return response.id return response.id
} catch (err) { } catch (err) {
dispatch(finishRequest(REQUEST_KEYS.REGISTER, false))
dispatch(finishRequest(RequestKey.Register, false))
throw err throw err
} }
} }

10
src/api/errors.ts

@ -2,15 +2,15 @@ import { History } from 'history'
import { setFieldNotification } from 'src/actions/forms' import { setFieldNotification } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications' import { showNotification } from 'src/actions/notifications'
import { AppThunkDispatch, FormNotification } from 'src/types'
import { AppThunkDispatch, FormNotification, NotificationType } from 'src/types'
export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, history: History) { export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, history: History) {
if (err instanceof ServerError) { if (err instanceof ServerError) {
dispatch(showNotification('error', 'Server Error'))
dispatch(showNotification(NotificationType.Error, 'Server Error'))
} }
if (err instanceof BadRequestError) { if (err instanceof BadRequestError) {
dispatch(showNotification('error', `Error: ${err.message}`))
dispatch(showNotification(NotificationType.Error, `Error: ${err.message}`))
for (const error of err.errors) { for (const error of err.errors) {
const { field, type, message } = error const { field, type, message } = error
@ -19,12 +19,12 @@ export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, histo
} }
if (err instanceof UnauthorizedError) { if (err instanceof UnauthorizedError) {
dispatch(showNotification('error', 'You need to be logged in.'))
dispatch(showNotification(NotificationType.Error, 'You need to be logged in.'))
history.push('/login') history.push('/login')
} }
if (err instanceof NotFoundError) { if (err instanceof NotFoundError) {
dispatch(showNotification('error', 'Not found.'))
dispatch(showNotification(NotificationType.Error, 'Not found.'))
} }
} }

4
src/api/fetch.ts

@ -11,7 +11,7 @@ import {
LOCAL_STORAGE_REFRESH_TOKEN_KEY, LOCAL_STORAGE_REFRESH_TOKEN_KEY,
} from '../constants' } from '../constants'
import { FetchOptions, FormNotification } from '../types'
import { FetchOptions, FormNotification, NotificationType } from '../types'
import getConfig from '../config' import getConfig from '../config'
interface RefreshResponse { interface RefreshResponse {
@ -38,7 +38,7 @@ const mapErrorsToFormNotifications = (errors?: FormError[]): FormNotification[]
return errors.map(e => ({ return errors.map(e => ({
field: e.field, field: e.field,
type: 'error',
type: NotificationType.Error,
message: e.message, message: e.message,
})) }))
} }

2
src/components/app/app.tsx

@ -17,7 +17,6 @@ import Login from '../pages/login'
import Register from '../pages/register' import Register from '../pages/register'
import RegisterGroup from '../pages/register-group' import RegisterGroup from '../pages/register-group'
import Self from '../pages/self' import Self from '../pages/self'
import Test from '../pages/test'
import './app.scss' import './app.scss'
@ -65,7 +64,6 @@ const App: FC<Props> = ({ collapsed, fetching, fetchSelf, setChecked }) => {
<Route path="/self" component={Self} /> <Route path="/self" component={Self} />
<Route path="/developers" component={Developers} /> <Route path="/developers" component={Developers} />
<Route path="/about" component={About} /> <Route path="/about" component={About} />
<Route path="/test" component={Test} />
</div> </div>
<NotificationContainer /> <NotificationContainer />

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

@ -32,7 +32,7 @@ const CreateGroupStep: FC<Props> = ({
<hr /> <hr />
<nav className="level"> <nav className="level">
<div className="level-left">
<div className="level-left">
<p className="level-item"> <p className="level-item">
<button className="button" onClick={() => previous()}> <button className="button" onClick={() => previous()}>
<span className="icon is-small"> <span className="icon is-small">

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

@ -5,7 +5,7 @@ import { showNotification } from 'src/actions/notifications'
import { setStep } from 'src/actions/registration' import { setStep } from 'src/actions/registration'
import { getFieldValue } from 'src/selectors/forms' import { getFieldValue } from 'src/selectors/forms'
import { MAX_ID_LENGTH } from 'src/constants' import { MAX_ID_LENGTH } from 'src/constants'
import { AppState, AppThunkDispatch } from 'src/types'
import { AppState, AppThunkDispatch, NotificationType } from 'src/types'
import CreateGroupStep, { Props } from './create-group-step' import CreateGroupStep, { Props } from './create-group-step'
@ -23,18 +23,18 @@ const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
let invalid = false let invalid = false
if (!name) { if (!name) {
dispatch(setFieldNotification('group-name', 'error', 'This is required'))
dispatch(setFieldNotification('group-name', NotificationType.Error, 'This is required'))
invalid = true invalid = true
} }
if (name.length > MAX_ID_LENGTH) { if (name.length > MAX_ID_LENGTH) {
dispatch(setFieldNotification('group-name', 'error', `This must be less than ${MAX_ID_LENGTH} characters`))
dispatch(setFieldNotification('group-name', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`))
invalid = true invalid = true
} }
if (!agree) { 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.'))
dispatch(setFieldNotification('group-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue'))
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.'))
invalid = true invalid = true
} }

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

@ -6,9 +6,9 @@ import { showNotification } from 'src/actions/notifications'
import { setStep } from 'src/actions/registration' import { setStep } from 'src/actions/registration'
import { getFieldValue } from 'src/selectors/forms' import { getFieldValue } from 'src/selectors/forms'
import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants' import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants'
import { AppState, AppThunkDispatch } from 'src/types'
import { AppState, AppThunkDispatch, NotificationType } from 'src/types'
import CreateUserStep, { Props } from './create-user-step'
import CreateUserStep from './create-user-step'
const mapStateToProps = (state: AppState) => ({ const mapStateToProps = (state: AppState) => ({
userId: getFieldValue<string>(state, 'user-id', ''), userId: getFieldValue<string>(state, 'user-id', ''),
@ -18,44 +18,44 @@ const mapStateToProps = (state: AppState) => ({
agree: getFieldValue<boolean>(state, 'user-agree', false), agree: getFieldValue<boolean>(state, 'user-agree', false),
}) })
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
next: (userId: string, name: string, email: string, password: string, agree: boolean) => { next: (userId: string, name: string, email: string, password: string, agree: boolean) => {
let invalid = false let invalid = false
if (!userId) { if (!userId) {
dispatch(setFieldNotification('user-id', 'error', 'This is required'))
dispatch(setFieldNotification('user-id', NotificationType.Error, 'This is required'))
invalid = true invalid = true
} }
if (userId.length > MAX_ID_LENGTH) { if (userId.length > MAX_ID_LENGTH) {
dispatch(setFieldNotification('user-id', 'error', `This must be less than ${MAX_ID_LENGTH} characters`))
dispatch(setFieldNotification('user-id', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`))
invalid = true invalid = true
} }
if (name.length > MAX_NAME_LENGTH) { if (name.length > MAX_NAME_LENGTH) {
dispatch(setFieldNotification('user-name', 'error', `This must be less than ${MAX_NAME_LENGTH} characters`))
dispatch(setFieldNotification('user-name', NotificationType.Error, `This must be less than ${MAX_NAME_LENGTH} characters`))
invalid = true invalid = true
} }
if (email === '') { if (email === '') {
dispatch(setFieldNotification('user-email', 'error', 'This is required'))
dispatch(setFieldNotification('user-email', NotificationType.Error, 'This is required'))
invalid = true invalid = true
} }
if (!agree) { 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.'))
dispatch(setFieldNotification('user-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue'))
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.'))
invalid = true invalid = true
} }
if (password === '') { if (password === '') {
dispatch(setFieldNotification('password', 'error', 'This is required'))
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required'))
invalid = true invalid = true
} else { } else {
const { score } = zxcvbn(password) const { score } = zxcvbn(password)
if (score === 0) { if (score === 0) {
dispatch(setFieldNotification('password', 'error', 'Try another password'))
dispatch(setFieldNotification('password', NotificationType.Error, 'Try another password'))
invalid = true invalid = true
} }
} }

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

@ -14,6 +14,7 @@ export interface Props {
userInputs?: string[] userInputs?: string[]
value?: string value?: string
notification?: FormNotification notification?: FormNotification
showStrength?: boolean
setValue?: (value: string) => void setValue?: (value: string) => void
} }
@ -22,6 +23,7 @@ const PasswordField: FC<Props> = ({
userInputs = [], userInputs = [],
value = '', value = '',
notification, notification,
showStrength = true,
setValue = noop, setValue = noop,
}) => { }) => {
const inputClassDictionary: ClassDictionary = { input: true } const inputClassDictionary: ClassDictionary = { input: true }
@ -30,7 +32,7 @@ const PasswordField: FC<Props> = ({
let icon: IconDefinition | undefined let icon: IconDefinition | undefined
let passwordMessage: ReactNode | undefined let passwordMessage: ReactNode | undefined
if (value) {
if (value && showStrength) {
const { score } = zxcvbn(value, userInputs) const { score } = zxcvbn(value, userInputs)
switch (score) { switch (score) {

22
src/components/notification-container/notification-container.tsx

@ -1,6 +1,7 @@
import React, { FC } from 'react' import React, { FC } from 'react'
import { Notification as INotification } from 'src/types' import { Notification as INotification } from 'src/types'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDoorOpen } from '@fortawesome/free-solid-svg-icons'
import Notification from '../notification' import Notification from '../notification'
import './notification-container.scss' import './notification-container.scss'
@ -15,6 +16,23 @@ const NotificationContainer: FC<Props> = ({ notifications, setAuto, dismiss }) =
return ( return (
<div id="notification-container"> <div id="notification-container">
{notifications.map(notification => { {notifications.map(notification => {
const content = () => {
switch (notification.type) {
case 'welcome':
return (
<p>
<span className="icon">
<FontAwesomeIcon icon={faDoorOpen} />
</span>
&nbsp;&nbsp;
<span>{notification.content}</span>
</p>
)
default:
return <span>{notification.content}</span>
}
}
return ( return (
<Notification <Notification
key={notification.id} key={notification.id}
@ -23,7 +41,7 @@ const NotificationContainer: FC<Props> = ({ notifications, setAuto, dismiss }) =
auto={notification.auto} auto={notification.auto}
setAuto={setAuto} setAuto={setAuto}
dismiss={dismiss}> dismiss={dismiss}>
{notification.content}
{content()}
</Notification> </Notification>
) )
})} })}

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

@ -10,7 +10,7 @@ const PageHeader: FC<Props> = ({ title, subtitle }) => (
<div className="hero-body"> <div className="hero-body">
<div className="container"> <div className="container">
<h1 className="title">{title}</h1> <h1 className="title">{title}</h1>
{subtitle && <h2 className="subtitle">{title}</h2>}
{subtitle && <h2 className="subtitle">{subtitle}</h2>}
</div> </div>
</div> </div>
</section> </section>

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

@ -1,21 +1,53 @@
import { connect } from 'react-redux' 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 { handleApiError } from 'src/api/errors'
import { authenticate } from 'src/actions/authentication'
import { showNotification } from 'src/actions/notifications'
import { getChecked, getAuthenticated } from 'src/selectors/authentication'
import { getFieldValue } from 'src/selectors/forms'
import { getIsFetching } from 'src/selectors/requests'
import { initForm, initField, setFieldNotification } from 'src/actions/forms'
import { AppState, AppThunkDispatch, NotificationType, RequestKey } from 'src/types'
import Login from './login'
import Login, { Props } from './login'
const mapStateToProps = (state: AppState) => ({ const mapStateToProps = (state: AppState) => ({
step: getStep(state),
checked: getChecked(state),
authenticated: getAuthenticated(state),
name: getFieldValue(state, 'name', ''),
password: getFieldValue(state, 'password', ''),
authenticating: getIsFetching(state, RequestKey.Authenticate),
}) })
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
initForm: () => { initForm: () => {
dispatch(initForm()) dispatch(initForm())
dispatch(initField('name'))
dispatch(initField('name', 'id'))
dispatch(initField('password')) dispatch(initField('password'))
}, },
authenticate: async (name: string, password: string) => {
let invalid = false
if (!name) {
dispatch(setFieldNotification('name', NotificationType.Error, 'This is required'))
invalid = true
}
if (!password) {
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required'))
invalid = true
}
if (invalid) return
try {
const id = await dispatch(authenticate(name, password))
dispatch(showNotification(NotificationType.Welcome, `Welcome back ${id}!`))
ownProps.history.push('/')
} catch (err) {
handleApiError(err, dispatch, ownProps.history)
}
},
}) })
export default connect( export default connect(

44
src/components/pages/login/login.tsx

@ -1,19 +1,48 @@
import React, { FC, useEffect } from 'react' import React, { FC, useEffect } from 'react'
import { RouteComponentProps } from 'react-router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faIdCard, faKey } from '@fortawesome/free-solid-svg-icons' import { faIdCard, faKey } from '@fortawesome/free-solid-svg-icons'
import classNames from 'classnames'
import PageHeader from 'src/components/page-header' import PageHeader from 'src/components/page-header'
import TextField from 'src/components/forms/text-field' import TextField from 'src/components/forms/text-field'
import PasswordField from 'src/components/forms/password-field' import PasswordField from 'src/components/forms/password-field'
interface Props {
import { ClassDictionary } from 'src/types'
export interface Props extends RouteComponentProps {
checked: boolean
authenticated: boolean
name?: string
password?: string
authenticating?: boolean
initForm: () => void initForm: () => void
authenticate: (name: string, password: string) => void
} }
const Login: FC<Props> = ({ initForm }) => {
const Login: FC<Props> = ({
checked,
authenticated,
name = '',
password = '',
authenticating = false,
initForm,
authenticate,
history
}) => {
useEffect(() => {
if (checked && authenticated) history.push('/self')
}, [checked, authenticated])
useEffect(() => { useEffect(() => {
initForm() initForm()
}, []) }, [])
const buttonClassDictionary: ClassDictionary = {
button: true,
'is-primary': true,
'is-loading': authenticating,
}
return ( return (
<div> <div>
@ -21,17 +50,18 @@ const Login: FC<Props> = ({ initForm }) => {
<div className="main-content"> <div className="main-content">
<div className="centered-content"> <div className="centered-content">
<div className="centered-content-icon">
<span className="icon is-large has-text-primary">
<FontAwesomeIcon icon={faKey} size="lg" />
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faKey} size="2x" />
</span> </span>
</div> </div>
<TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" /> <TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" />
<br /> <br />
<PasswordField placeholder="Your password" />
<PasswordField placeholder="Your password" showStrength={false} />
<br /> <br />
<button className="button is-primary">Log In</button>
<button className={classNames(buttonClassDictionary)} onClick={() => authenticate(name, password)}>Log In</button>
</div> </div>
</div> </div>
</div> </div>

4
src/components/pages/register-group/index.ts

@ -3,12 +3,12 @@ import { connect } from 'react-redux'
import { handleApiError } from 'src/api/errors' import { handleApiError } from 'src/api/errors'
import { fetchGroup } from 'src/actions/directory' import { fetchGroup } from 'src/actions/directory'
import { getEntity } from 'src/selectors/entities' import { getEntity } from 'src/selectors/entities'
import { AppState, AppThunkDispatch } from 'src/types'
import { AppState, AppThunkDispatch, EntityType } from 'src/types'
import RegisterGroup, { Props } from './register-group' import RegisterGroup, { Props } from './register-group'
const mapStateToProps = (state: AppState, ownProps: Props) => ({ const mapStateToProps = (state: AppState, ownProps: Props) => ({
group: getEntity(state, 'groups', ownProps.match.params.id),
group: getEntity(state, EntityType.Group, ownProps.match.params.id),
}) })
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({

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

@ -8,7 +8,7 @@ import { showNotification } from 'src/actions/notifications'
import { createGroup, register } from 'src/actions/registration' import { createGroup, register } from 'src/actions/registration'
import { valueFromForm } from 'src/utils' import { valueFromForm } from 'src/utils'
import { AppState, AppThunkDispatch, Form } from 'src/types'
import { AppState, AppThunkDispatch, Form, NotificationType } from 'src/types'
import Register, { Props } from './register' import Register, { Props } from './register'
@ -36,7 +36,7 @@ const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
const userAgree = valueFromForm<boolean>(form, 'user-agree', false) const userAgree = valueFromForm<boolean>(form, 'user-agree', false)
if (!groupAgree || !userAgree) { if (!groupAgree || !userAgree) {
dispatch(showNotification('error', 'You must agree to both Community and User terms and conditions.'))
dispatch(showNotification(NotificationType.Error, 'You must agree to both Community and User terms and conditions.'))
return return
} }

31
src/components/pages/self/index.ts

@ -1,22 +1,27 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getAuthenticated, getAuthenticatedUserId, getChecked } from 'src/selectors/authentication'
import { getEntity } from 'src/selectors/entities'
import { unauthenticate } from 'src/actions/authentication'
import { getAuthenticated, getChecked, getAuthenticatedUser } from 'src/selectors/authentication'
import { AppState } from 'src/types' import { AppState } from 'src/types'
import Self from './self' import Self from './self'
const mapStateToProps = (state: AppState) => {
const userId = getAuthenticatedUserId(state)
const user = userId ? getEntity(state, 'users', userId) : undefined
const mapStateToProps = (state: AppState) => ({
checked: getChecked(state),
authenticated: getAuthenticated(state),
user: getAuthenticatedUser(state),
})
return {
checked: getChecked(state),
authenticated: getAuthenticated(state),
user,
}
}
const mapDispatchToProps = (dispatch: Dispatch) => ({
logout: async () => {
localStorage.clear()
dispatch(unauthenticate())
window.location.href = '/'
},
})
export default connect( export default connect(
mapStateToProps
mapStateToProps,
mapDispatchToProps
)(Self) )(Self)

37
src/components/pages/self/self.tsx

@ -1,33 +1,52 @@
import React, { FC, useEffect } from 'react' import React, { FC, useEffect } from 'react'
import { RouteComponentProps } from 'react-router-dom' import { RouteComponentProps } from 'react-router-dom'
import { setTitle } from 'src/utils'
import moment from 'moment'
import { useAuthenticationCheck } from 'src/hooks'
import { setTitle } from 'src/utils'
import { Entity } from 'src/types' import { Entity } from 'src/types'
import PageHeader from 'src/components/page-header' import PageHeader from 'src/components/page-header'
interface Props extends RouteComponentProps {
export interface Props extends RouteComponentProps {
checked: boolean checked: boolean
authenticated: boolean authenticated: boolean
user?: Entity user?: Entity
logout: () => void
} }
const Self: FC<Props> = ({ checked, authenticated, user, history }) => {
useEffect(() => {
if (checked && !authenticated) history.push('/login')
}, [checked, authenticated])
const Self: FC<Props> = ({ checked, authenticated, user, logout, history }) => {
useAuthenticationCheck(checked, authenticated, history)
useEffect(() => { useEffect(() => {
if (user) setTitle(`${user.name} (@${user.id})`) if (user) setTitle(`${user.name} (@${user.id})`)
}, [user]) }, [user])
if (!user) {
return (
<div>
<PageHeader title="Self" />
<div className="main-content"></div>
</div>
)
}
return ( return (
<div> <div>
<PageHeader title={user ? user.name as string : '?'} subtitle={user ? user.id : '?'} />
<PageHeader title={user.name as string || user.id as string} subtitle={`@${user.id}`} />
<div className="main-content"> <div className="main-content">
<nav className="level">
<div className="level-item has-text-centered">
<p>
<div className="heading">Joined</div>
<div className="title">{moment(user.created).format('MMMM Do YYYY')}</div>
</p>
</div>
</nav>
<p> <p>
Hello.
<button className="button is-danger" onClick={() => logout()}>Log Out</button>
</p> </p>
</div> </div>
</div> </div>

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

@ -1,16 +0,0 @@
import { connect } from 'react-redux'
import { showNotification } from 'src/actions/notifications'
import { AppThunkDispatch, NotificationType } from 'src/types'
import Test from './test'
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
show: async (type: NotificationType, content: string) => {
dispatch(showNotification(type, content))
},
})
export default connect(
null,
mapDispatchToProps
)(Test)

26
src/components/pages/test/test.tsx

@ -1,26 +0,0 @@
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>
<PageHeader title="Test Page" />
<div className="main-content">
<p>
<button className="button is-success" onClick={() => show('success', 'You did it!!!!!')}>Show Success</button>
<br /><br />
<button className="button is-info" onClick={() => show('info', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.')}>Show Info</button>
<br /><br />
<button className="button is-danger" onClick={() => show('error', 'Fuck')}>Show Error</button>
</p>
</div>
</div>
)
export default Test

18
src/components/user-info/index.ts

@ -1,20 +1,14 @@
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getAuthenticated, getAuthenticatedUserId } from 'src/selectors/authentication'
import { getEntity } from 'src/selectors/entities'
import { AppState, User } from 'src/types'
import { getAuthenticated, getAuthenticatedUser } from 'src/selectors/authentication'
import { AppState } from 'src/types'
import UserInfo from './user-info' import UserInfo from './user-info'
const mapStateToProps = (state: AppState) => {
const userId = getAuthenticatedUserId(state)
const user = userId ? getEntity<User>(state, 'users', userId) : undefined
return {
authenticated: getAuthenticated(state),
user,
}
}
const mapStateToProps = (state: AppState) => ({
authenticated: getAuthenticated(state),
user: getAuthenticatedUser(state),
})
export default connect( export default connect(
mapStateToProps mapStateToProps

9
src/constants/index.ts

@ -4,12 +4,3 @@ export const MAX_NAME_LENGTH = 80
export const LOCAL_STORAGE_ACCESS_TOKEN_KEY = 'FLEXOR_ACCESS_TOKEN' 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_ACCESS_TOKEN_AT_KEY = 'FLEXOR_ACCESS_TOKEN_AT'
export const LOCAL_STORAGE_REFRESH_TOKEN_KEY = 'FLEXOR_REFRESH_TOKEN' export const LOCAL_STORAGE_REFRESH_TOKEN_KEY = 'FLEXOR_REFRESH_TOKEN'
export const REQUEST_KEYS = {
FETCH_GROUP: 'FETCH_GROUP',
FETCH_GROUPS: 'FETCH_GROUPS',
FETCH_GROUP_AVAILABILITY: 'FETCH_GROUP_AVAILABILITY',
FETCH_USER_AVAILABILITY: 'FETCH_USER_AVAILABILITY',
CREATE_GROUP: 'CREATE_GROUP',
REGISTER: 'REGISTER',
}

8
src/hooks/index.ts

@ -0,0 +1,8 @@
import { useEffect } from 'react'
import { History } from 'history'
export const useAuthenticationCheck = (checked: boolean, authenticated: boolean, history: History) => {
useEffect(() => {
if (checked && !authenticated) history.push('/login')
}, [checked, authenticated])
}

6
src/reducers/authentication.ts

@ -27,6 +27,12 @@ const reducer: Reducer<AuthenticationState, AuthenticationActions> = (state = in
...state, ...state,
userId: action.payload, userId: action.payload,
} }
case 'AUTHENTICATION_UNAUTHENTICATE':
return {
...state,
authenticated: false,
userId: undefined,
}
default: default:
return state return state
} }

15
src/selectors/authentication.ts

@ -1,5 +1,18 @@
import { AppState } from '../types'
import { denormalize } from 'normalizr'
import { createSelector } from 'reselect'
import { userSchema } from 'src/store/schemas'
import { getEntityStore } from './entities'
import { AppState, User } from 'src/types'
export const getChecked = (state: AppState) => state.authentication.checked export const getChecked = (state: AppState) => state.authentication.checked
export const getAuthenticated = (state: AppState) => state.authentication.authenticated export const getAuthenticated = (state: AppState) => state.authentication.authenticated
export const getAuthenticatedUserId = (state: AppState) => state.authentication.userId export const getAuthenticatedUserId = (state: AppState) => state.authentication.userId
export const getAuthenticatedUser = createSelector(
[getEntityStore, getAuthenticatedUserId],
(entities, userId) => {
if (!userId) return
return denormalize(userId, userSchema, entities) as User
}
)

6
src/selectors/directory.ts

@ -3,13 +3,13 @@ import { createSelector } from 'reselect'
import { groupSchema } from '../store/schemas' import { groupSchema } from '../store/schemas'
import { getEntityStore } from './entities' import { getEntityStore } from './entities'
import { AppState, Group } from '../types'
import { AppState, Group } from 'src/types'
export const getGroupIds = (state: AppState) => state.directory.groups export const getGroupIds = (state: AppState) => state.directory.groups
export const getGroups = createSelector( export const getGroups = createSelector(
[getEntityStore, getGroupIds], [getEntityStore, getGroupIds],
(store, groups) => {
return denormalize(groups, [groupSchema], store) as Group[]
(entities, groups) => {
return denormalize(groups, [groupSchema], entities) as Group[]
} }
) )

19
src/selectors/entities.ts

@ -1,10 +1,19 @@
import { AppState, Entity, EntityTypes } from '../types'
import { denormalize } from 'normalizr'
import { userSchema, groupSchema } from 'src/store/schemas'
import { AppState, Entity, EntityType } from '../types'
export const getEntityStore = (state: AppState) => state.entities export const getEntityStore = (state: AppState) => state.entities
export const getEntity = <T extends Entity = Entity>(state: AppState, type: EntityTypes, id: string) => {
const store = getEntityStore(state)
const collection = store[type]
export const getEntity = <T extends Entity = Entity>(state: AppState, type: EntityType, id?: string) => {
if (!id) return
const entities = getEntityStore(state)
return collection ? collection[id] as T : undefined
switch (type) {
case EntityType.User:
return denormalize(id, userSchema, entities) as T
case EntityType.Group:
return denormalize(id, groupSchema, entities) as T
default:
return
}
} }

3
src/selectors/requests.ts

@ -0,0 +1,3 @@
import { AppState, RequestKey } from 'src/types'
export const getIsFetching = (state: AppState, key: RequestKey) => state.requests[key] ? state.requests[key].fetching : false

2
src/store/schemas.ts

@ -3,5 +3,5 @@ import { schema } from 'normalizr'
export const groupSchema = new schema.Entity('groups') export const groupSchema = new schema.Entity('groups')
export const userSchema = new schema.Entity('users', { export const userSchema = new schema.Entity('users', {
groupSchema,
group: groupSchema,
}) })

5
src/types/entities.ts

@ -1,4 +1,7 @@
export type EntityTypes = 'users' | 'groups'
export enum EntityType {
User = 'users',
Group = 'groups',
}
export interface Entity { export interface Entity {
[key: string]: string | number | boolean | object | any[] [key: string]: string | number | boolean | object | any[]

20
src/types/store.ts

@ -1,7 +1,23 @@
import { EntityStore } from './entities' import { EntityStore } from './entities'
export type NotificationType = 'info' | 'success' | 'error'
export enum NotificationType {
Info = 'info',
Success = 'success',
Error = 'error',
Welcome = 'welcome',
}
export enum RequestKey {
FetchGroup = 'fetch_group',
FetchGroups = 'fetch_groups',
FetchGroupAvailability = 'fetch_group_availability',
FetchUserAvailability = 'fetch_user_availability',
CreateGroup = 'create_group',
Register = 'register',
Authenticate = 'authenticate',
}
export type FormValue = string | number | boolean export type FormValue = string | number | boolean
export interface FormNotification { export interface FormNotification {
@ -11,7 +27,7 @@ export interface FormNotification {
} }
export interface APIRequest { export interface APIRequest {
id: string
id: RequestKey
fetching: boolean fetching: boolean
started: number started: number
succeeded: boolean succeeded: boolean

7
src/utils/index.ts

@ -2,9 +2,10 @@ import { NotificationType, Form, FormValue } from '../types'
export function notificationTypeToClassName(type: NotificationType): string { export function notificationTypeToClassName(type: NotificationType): string {
switch (type) { switch (type) {
case 'info': return 'is-info'
case 'success': return 'is-success'
case 'error': return 'is-danger'
case NotificationType.Info: return 'is-info'
case NotificationType.Success: return 'is-success'
case NotificationType.Error: return 'is-danger'
case NotificationType.Welcome: return 'is-success'
} }
} }

2
webpack.config.ts

@ -1,6 +1,7 @@
import { Configuration } from 'webpack' import { Configuration } from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin' import HtmlWebpackPlugin from 'html-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import c from './config/config.json' import c from './config/config.json'
@ -71,6 +72,7 @@ const config: Configuration = {
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: '[name].css', filename: '[name].css',
}), }),
// new BundleAnalyzerPlugin(),
], ],
} }

Loading…
Cancel
Save