Dwayne Harris 5 years ago
parent
commit
738ed64a2c
  1. 5
      package-lock.json
  2. 1
      package.json
  3. 73
      src/actions/directory.ts
  4. 8
      src/actions/entities.ts
  5. 46
      src/actions/groups.ts
  6. 4
      src/api/groups.ts
  7. 9
      src/components/app/app.tsx
  8. 2
      src/components/pages/directory/directory.tsx
  9. 5
      src/components/pages/directory/index.ts
  10. 13
      src/components/pages/register/register.tsx
  11. 17
      src/components/spinner/index.tsx
  12. 62
      src/components/spinner/spinner.scss
  13. 6
      src/components/user-info/user-info.tsx
  14. 36
      src/reducers/directory.ts
  15. 3
      src/reducers/entities.ts
  16. 1
      src/reducers/requests.ts
  17. 7
      src/selectors/index.ts
  18. 12
      src/store/schemas.ts
  19. 88
      src/types/index.ts
  20. 71
      src/types/store.ts

5
package-lock.json

@ -5295,6 +5295,11 @@
"sort-keys": "^1.0.0"
}
},
"normalizr": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/normalizr/-/normalizr-3.4.1.tgz",
"integrity": "sha512-gei+tJucERU8vYN6TFQL2k5YMLX2Yh7nlylKMJC65+Uu/LS3xQCDJc8cies72aHouycKYyVgcnyLRbaJsigXKw=="
},
"npm-run-all": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",

1
package.json

@ -44,6 +44,7 @@
"@fortawesome/free-solid-svg-icons": "^5.10.2",
"@fortawesome/react-fontawesome": "^0.1.4",
"lodash": "^4.17.15",
"normalizr": "^3.4.1",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-redux": "^7.1.1",

73
src/actions/directory.ts

@ -0,0 +1,73 @@
import { Action, AnyAction } from 'redux'
import { ThunkAction, ThunkDispatch } from 'redux-thunk'
import { normalize } from 'normalizr'
import { getGroups } from '../api/groups'
import { setEntities } from '../actions/entities'
import { startRequest, finishRequest } from '../actions/requests'
import { group } from '../store/schemas'
import { AppState } from '../types'
const FETCH_ID = 'groups'
export interface SetGroupsAction extends Action {
type: 'DIRECTORY_SET_GROUPS'
payload: string[]
}
export interface AppendGroupsAction extends Action {
type: 'DIRECTORY_APPEND_GROUPS',
payload: string[]
}
export interface SetContinuationAction extends Action {
type: 'DIRECTORY_SET_CONTINUATION'
payload: string
}
export type DirectoryActions = SetGroupsAction | AppendGroupsAction | SetContinuationAction
const setGroups = (groups: string[]): SetGroupsAction => ({
type: 'DIRECTORY_SET_GROUPS',
payload: groups,
})
const appendGroups = (groups: string[]): AppendGroupsAction => ({
type: 'DIRECTORY_APPEND_GROUPS',
payload: groups,
})
const setContinuation = (continuation: string): SetContinuationAction => ({
type: 'DIRECTORY_SET_CONTINUATION',
payload: continuation,
})
const fetchGroups = (sort?: string, continuation?: string): ThunkAction<Promise<void>, AppState, void, AnyAction> => {
return async (dispatch: ThunkDispatch<AppState, void, AnyAction>) => {
dispatch(startRequest(FETCH_ID))
try {
const response = await getGroups(sort, continuation)
const groups = normalize(response.groups, group)
dispatch(setEntities(groups.entities))
dispatch(setGroups(groups.result))
if (response.continuation) {
dispatch(setContinuation(response.continuation))
}
dispatch(finishRequest(FETCH_ID, true))
} catch (err) {
console.error(err)
dispatch(finishRequest(FETCH_ID, false))
}
}
}
export {
setGroups,
appendGroups,
setContinuation,
fetchGroups,
}

8
src/actions/entities.ts

@ -1,5 +1,5 @@
import { Action } from 'redux'
import { Entity } from '../types/entities'
import { Entity, EntityStore } from '../types'
export interface SetEntityAction extends Action {
type: 'ENTITIES_SET_ENTITY'
@ -12,8 +12,7 @@ export interface SetEntityAction extends Action {
export interface SetEntitiesAction extends Action {
type: 'ENTITIES_SET_ENTITIES'
payload: {
type: string
entities: Entity[]
entities: EntityStore
}
}
@ -27,10 +26,9 @@ const setEntity = (type: string, entity: Entity): SetEntityAction => ({
}
})
const setEntities = (type: string, entities: Entity[]): SetEntitiesAction => ({
const setEntities = (entities: EntityStore): SetEntitiesAction => ({
type: 'ENTITIES_SET_ENTITIES',
payload: {
type,
entities,
}
})

46
src/actions/groups.ts

@ -1,46 +0,0 @@
import { ThunkAction, ThunkDispatch } from 'redux-thunk'
import { Action, AnyAction } from 'redux'
import { getGroups } from '../api/groups'
import { startRequest, finishRequest } from '../actions/requests'
import { AppState } from '../types'
import { Entity } from '../types/entities'
const FETCH_ID = 'groups'
export interface FetchGroupsAction extends Action {
type: 'GROUPS_FETCH'
payload: {
groups: Entity[]
continuation?: string
}
}
const setGroups = (groups: Entity[], continuation?: string): FetchGroupsAction => ({
type: 'GROUPS_FETCH',
payload: {
groups,
continuation,
},
})
const fetchGroups = (sort?: string, continuation?: string): ThunkAction<Promise<void>, AppState, void, AnyAction> => {
return async (dispatch: ThunkDispatch<AppState, void, AnyAction>) => {
dispatch(startRequest(FETCH_ID))
try {
const response = await getGroups(sort, continuation)
dispatch(setGroups(response.groups, response.continuation))
dispatch(finishRequest(FETCH_ID, true))
} catch (err) {
console.error(err)
dispatch(finishRequest(FETCH_ID, false))
}
}
}
export {
fetchGroups,
}

4
src/api/groups.ts

@ -1,5 +1,5 @@
import { fetch } from './fetch'
import { Entity } from '../types/entities'
import { Entity } from '../types'
interface GroupsResponse {
groups: Entity[]
@ -13,7 +13,7 @@ export async function getGroups(sort: string = 'members', continuation?: string)
}
const querystring = Object.entries(params).filter(([name, value]) => value !== undefined).map(([name, value]) => `${name}=${value}`).join('&')
return await fetch<GroupsResponse>({
path: `/api/groups?${querystring}`
})

9
src/components/app/app.tsx

@ -1,8 +1,12 @@
import React, { FC } from 'react'
import { HashRouter as Router, Route, Link } from 'react-router-dom'
import Spinner from '../spinner'
import UserInfo from '../user-info'
import Home from '../pages/home'
import Register from '../pages/register'
import Directory from '../pages/directory'
import './app.scss'
@ -30,6 +34,8 @@ const App: FC<Props> = ({ menuCollapsed, fetching }) => {
</p>
</div>
{fetching && <Spinner />}
<UserInfo />
<footer>
@ -46,7 +52,8 @@ const App: FC<Props> = ({ menuCollapsed, fetching }) => {
<div id="main-column" style={{ marginLeft: mainColumnLeftMargin }}>
<Route exact path="/" component={Home} />
<Route path="/login" component={Home} />
<Route path="/signup" component={Home} />
<Route path="/register/:group" component={Register} />
<Route path="/directory" component={Directory} />
</div>
</div>
</Router>

2
src/components/pages/directory/directory.tsx

@ -1,6 +1,6 @@
import React, { FC } from 'react'
import { Entity } from '../../../types/entities'
import { Entity } from '../../../types'
interface Props {
groups: Entity[]

5
src/components/pages/directory/index.ts

@ -1,7 +1,8 @@
import { connect } from 'react-redux'
import { ThunkDispatch } from 'redux-thunk'
import { AnyAction } from 'redux'
import { fetchGroups, FetchGroupsAction } from '../../../actions/groups'
import { fetchGroups } from '../../../actions/directory'
import { AppState } from '../../../types'
import Directory from './directory'
@ -9,7 +10,7 @@ import Directory from './directory'
const mapStateToProps = () => {
}
const mapDispatchToProps = (dispatch: ThunkDispatch<AppState, void, FetchGroupsAction>) => ({
const mapDispatchToProps = (dispatch: ThunkDispatch<AppState, void, AnyAction>) => ({
fetchGroups: () => {
dispatch(fetchGroups())
}

13
src/components/pages/register/register.tsx

@ -1,19 +1,14 @@
import React, { FC } from 'react'
import { Entity } from '../../../types/entities'
import { Entity } from '../../../types'
interface Props {
groups: Entity[]
fetchGroups: () => void
group?: Entity
}
const Register: FC<Props> = ({ groups, fetchGroups }) => {
const Register: FC<Props> = ({ group }) => {
return (
<div>
<h1 className="title">Communities</h1>
{groups.length === 0 && <p>No Groups</p>}
</div>
<div></div>
)
}

17
src/components/spinner/index.tsx

@ -0,0 +1,17 @@
import React, { FC } from 'react'
const Spinner: FC = () => (
<div className="sk-cube-grid">
<div className="sk-cube sk-cube1"></div>
<div className="sk-cube sk-cube2"></div>
<div className="sk-cube sk-cube3"></div>
<div className="sk-cube sk-cube4"></div>
<div className="sk-cube sk-cube5"></div>
<div className="sk-cube sk-cube6"></div>
<div className="sk-cube sk-cube7"></div>
<div className="sk-cube sk-cube8"></div>
<div className="sk-cube sk-cube9"></div>
</div>
)
export default Spinner

62
src/components/spinner/spinner.scss

@ -0,0 +1,62 @@
.sk-cube-grid {
width: 40px;
height: 40px;
margin: 100px auto;
}
.sk-cube-grid .sk-cube {
width: 33%;
height: 33%;
background-color: #333;
float: left;
-webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
}
.sk-cube-grid .sk-cube1 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s; }
.sk-cube-grid .sk-cube2 {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s; }
.sk-cube-grid .sk-cube3 {
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s; }
.sk-cube-grid .sk-cube4 {
-webkit-animation-delay: 0.1s;
animation-delay: 0.1s; }
.sk-cube-grid .sk-cube5 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s; }
.sk-cube-grid .sk-cube6 {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s; }
.sk-cube-grid .sk-cube7 {
-webkit-animation-delay: 0s;
animation-delay: 0s; }
.sk-cube-grid .sk-cube8 {
-webkit-animation-delay: 0.1s;
animation-delay: 0.1s; }
.sk-cube-grid .sk-cube9 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s; }
@-webkit-keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
} 35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}
@keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
} 35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}

6
src/components/user-info/user-info.tsx

@ -1,13 +1,13 @@
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
import { IUser } from '../../types/entities'
import { User } from '../../types'
import './user-info.scss'
interface Props {
authenticated: boolean
user?: IUser
user?: User
}
const UserInfo: FC<Props> = ({ authenticated, user }) => {
@ -32,7 +32,7 @@ const UserInfo: FC<Props> = ({ authenticated, user }) => {
<p>
<Link to="/login" className="is-size-5 has-text-white has-text-weight-bold">Log In</Link>
<br />
<Link to="/signup" className="is-size-7 has-text-primary">Sign Up</Link>
<Link to="/directory" className="is-size-7 has-text-primary">Sign Up</Link>
</p>
</div>
</div>

36
src/reducers/directory.ts

@ -0,0 +1,36 @@
import { Reducer } from 'redux'
import { DirectoryActions } from '../actions/directory'
import { DirectoryState } from '../types'
const initialState: DirectoryState = {
groups: [],
continuation: undefined,
}
const reducer: Reducer<DirectoryState, DirectoryActions> = (state = initialState, action) => {
switch (action.type) {
case 'DIRECTORY_SET_GROUPS':
return {
...state,
groups: action.payload,
}
case 'DIRECTORY_APPEND_GROUPS':
return {
...state,
groups: [
...state.groups,
...action.payload,
],
}
case 'DIRECTORY_SET_CONTINUATION':
return {
...state,
continuation: action.payload,
}
default:
return state
}
}
export default reducer

3
src/reducers/entities.ts

@ -21,6 +21,9 @@ const reducer: Reducer<EntitiesState, EntitiesActions> = (state = initialState,
},
}
}
case 'ENTITIES_SET_ENTITIES': {
return merge(state, action.payload.entities)
}
default:
return state
}

1
src/reducers/requests.ts

@ -16,6 +16,7 @@ const reducer: Reducer<RequestsState, RequestsActions> = (state = initialState,
...request,
fetching: true,
succeeded: false,
started: Date.now(),
},
}
}

7
src/selectors/index.ts

@ -1,10 +1,13 @@
import values from 'lodash/values'
import { AppState } from '../types'
import { AppState, APIRequest } from '../types'
const REQUEST_LOADING_MIN = 500
export const getMenuCollapsed = (state: AppState) => state.menu.collapsed
export const getAuthenticated = (state: AppState) => state.authentication.authenticated
export const getNotifications = (state: AppState) => state.notifications
export const getFetching = (state: AppState) => {
return values(state.requests).reduce((fetching, request) => fetching || request.fetching, false)
const isFetching = (request: APIRequest) => request.fetching && (Date.now() - request.started > REQUEST_LOADING_MIN)
return values(state.requests).reduce((fetching, request) => fetching || isFetching(request) , false)
}

12
src/store/schemas.ts

@ -0,0 +1,12 @@
import { schema } from 'normalizr'
const group = new schema.Entity('groups')
const user = new schema.Entity('users', {
group,
})
export {
group,
user,
}

88
src/types/index.ts

@ -1,6 +1,3 @@
import { EntityStore } from './entities'
export type NotificationType = 'info' | 'success' | 'error'
export type FetchMethods = 'get' | 'post' | 'put'
export interface FetchOptions {
@ -10,63 +7,28 @@ export interface FetchOptions {
headers?: HeadersInit
}
export interface FormNotification {
field?: string
type: NotificationType
message: string
}
export interface APIRequest {
readonly id: string
readonly fetching: boolean
readonly succeeded: boolean
}
export interface APIRequestCollection {
[id: string]: APIRequest
}
export interface Notification {
readonly id: string
readonly type: NotificationType
readonly content: string
readonly auto: boolean
readonly expiration: number
}
export interface AuthenticationState {
readonly authenticated: boolean
readonly userId?: string
}
export interface MenuState {
readonly collapsed: boolean
}
export interface FormField {
readonly name: string
readonly value?: string
readonly notification?: FormNotification
}
export interface Form {
[name: string]: FormField
}
export interface FormsState {
readonly form: Form
readonly notification?: FormNotification
}
export type RequestsState = APIRequestCollection
export type NotificationsState = readonly Notification[]
export type EntitiesState = EntityStore
export interface AppState {
authentication: AuthenticationState
entities: EntitiesState
forms: FormsState
menu: MenuState
notifications: NotificationsState
requests: RequestsState
}
export {
Entity,
Group,
User,
EntityCollection,
EntityStore,
} from './entities'
export {
NotificationType,
FormNotification,
APIRequest,
APIRequestCollection,
Notification,
AuthenticationState,
MenuState,
FormField,
Form,
FormsState,
DirectoryState,
RequestsState,
NotificationsState,
EntitiesState,
AppState,
} from './store'

71
src/types/store.ts

@ -0,0 +1,71 @@
import { EntityStore } from './entities'
export type NotificationType = 'info' | 'success' | 'error'
export interface FormNotification {
field?: string
type: NotificationType
message: string
}
export interface APIRequest {
readonly id: string
readonly fetching: boolean
readonly started: number
readonly succeeded: boolean
}
export interface APIRequestCollection {
[id: string]: APIRequest
}
export interface Notification {
readonly id: string
readonly type: NotificationType
readonly content: string
readonly auto: boolean
readonly expiration: number
}
export interface AuthenticationState {
readonly authenticated: boolean
readonly userId?: string
}
export interface MenuState {
readonly collapsed: boolean
}
export interface FormField {
readonly name: string
readonly value?: string
readonly notification?: FormNotification
}
export interface Form {
[name: string]: FormField
}
export interface FormsState {
readonly form: Form
readonly notification?: FormNotification
}
export interface DirectoryState {
readonly groups: string[]
readonly continuation?: string
}
export type RequestsState = APIRequestCollection
export type NotificationsState = readonly Notification[]
export type EntitiesState = EntityStore
export interface AppState {
authentication: AuthenticationState
entities: EntitiesState
forms: FormsState
menu: MenuState
notifications: NotificationsState
requests: RequestsState
}
Loading…
Cancel
Save