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 React, { FC } from 'react' |
||||
|
|
||||
import { Entity } from '../../../types/entities' |
|
||||
|
import { Entity } from '../../../types' |
||||
|
|
||||
interface Props { |
interface Props { |
||||
groups: Entity[] |
|
||||
fetchGroups: () => void |
|
||||
|
group?: Entity |
||||
} |
} |
||||
|
|
||||
const Register: FC<Props> = ({ groups, fetchGroups }) => { |
|
||||
|
const Register: FC<Props> = ({ group }) => { |
||||
return ( |
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 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 getMenuCollapsed = (state: AppState) => state.menu.collapsed |
||||
export const getAuthenticated = (state: AppState) => state.authentication.authenticated |
export const getAuthenticated = (state: AppState) => state.authentication.authenticated |
||||
export const getNotifications = (state: AppState) => state.notifications |
export const getNotifications = (state: AppState) => state.notifications |
||||
|
|
||||
export const getFetching = (state: AppState) => { |
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