Dwayne Harris
5 years ago
20 changed files with 335 additions and 134 deletions
-
5package-lock.json
-
1package.json
-
73src/actions/directory.ts
-
8src/actions/entities.ts
-
46src/actions/groups.ts
-
4src/api/groups.ts
-
9src/components/app/app.tsx
-
2src/components/pages/directory/directory.tsx
-
5src/components/pages/directory/index.ts
-
13src/components/pages/register/register.tsx
-
17src/components/spinner/index.tsx
-
62src/components/spinner/spinner.scss
-
6src/components/user-info/user-info.tsx
-
36src/reducers/directory.ts
-
3src/reducers/entities.ts
-
1src/reducers/requests.ts
-
7src/selectors/index.ts
-
12src/store/schemas.ts
-
88src/types/index.ts
-
71src/types/store.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, |
|||
} |
@ -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, |
|||
} |
@ -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> |
|||
) |
|||
} |
|||
|
|||
|
@ -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 |
@ -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); |
|||
} |
|||
} |
@ -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 |
@ -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) |
|||
} |
@ -0,0 +1,12 @@ |
|||
import { schema } from 'normalizr' |
|||
|
|||
const group = new schema.Entity('groups') |
|||
|
|||
const user = new schema.Entity('users', { |
|||
group, |
|||
}) |
|||
|
|||
export { |
|||
group, |
|||
user, |
|||
} |
@ -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 |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue