Dwayne Harris 5 years ago
parent
commit
4ee80cc987
  1. 12
      package-lock.json
  2. 4
      package.json
  3. 9
      src/actions/authentication.ts
  4. 43
      src/actions/directory.ts
  5. 9
      src/actions/entities.ts
  6. 18
      src/actions/forms.ts
  7. 0
      src/actions/index.ts
  8. 6
      src/actions/menu.ts
  9. 70
      src/actions/notifications.ts
  10. 9
      src/actions/requests.ts
  11. 6
      src/api/groups.ts
  12. 7
      src/components/app/app.tsx
  13. 6
      src/components/notification-container/index.ts
  14. 11
      src/components/notification-container/notification-container.scss
  15. 1
      src/components/notification-container/notification-container.tsx
  16. 9
      src/components/notification/index.tsx
  17. 2
      src/components/pages/directory/index.ts
  18. 15
      src/components/pages/register/register.tsx
  19. 2
      src/components/user-info/user-info.tsx
  20. 23
      src/reducers/notifications.ts
  21. 2
      src/store/index.ts
  22. 9
      src/store/schemas.ts
  23. 1
      src/types/store.ts

12
package-lock.json

@ -347,6 +347,15 @@
} }
} }
}, },
"@types/uuid": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.5.tgz",
"integrity": "sha512-MNL15wC3EKyw1VLF+RoVO4hJJdk9t/Hlv3rt1OL65Qvuadm4BYo6g9ZJQqoq7X8NBFSsQXgAujWciovh2lpVjA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/webpack": { "@types/webpack": {
"version": "4.39.1", "version": "4.39.1",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.39.1.tgz", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.39.1.tgz",
@ -8120,8 +8129,7 @@
"uuid": { "uuid": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==",
"dev": true
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
}, },
"v8-compile-cache": { "v8-compile-cache": {
"version": "2.0.3", "version": "2.0.3",

4
package.json

@ -22,6 +22,7 @@
"@types/react-redux": "^7.1.2", "@types/react-redux": "^7.1.2",
"@types/react-router-dom": "^4.3.5", "@types/react-router-dom": "^4.3.5",
"@types/redux-logger": "^3.0.7", "@types/redux-logger": "^3.0.7",
"@types/uuid": "^3.4.5",
"@types/webpack": "^4.39.1", "@types/webpack": "^4.39.1",
"@types/webpack-dev-server": "^3.1.7", "@types/webpack-dev-server": "^3.1.7",
"bulma": "^0.7.5", "bulma": "^0.7.5",
@ -52,6 +53,7 @@
"redux": "^4.0.4", "redux": "^4.0.4",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"reselect": "^4.0.0"
"reselect": "^4.0.0",
"uuid": "^3.3.3"
} }
} }

9
src/actions/authentication.ts

@ -12,17 +12,12 @@ export interface SetUserAction extends Action {
export type AuthenticationActions = SetAuthenticatedAction | SetUserAction export type AuthenticationActions = SetAuthenticatedAction | SetUserAction
const setAuthenticated = (authenticated: boolean): SetAuthenticatedAction => ({
export const setAuthenticated = (authenticated: boolean): SetAuthenticatedAction => ({
type: 'AUTHENTICATION_SET_AUTHENTICATED', type: 'AUTHENTICATION_SET_AUTHENTICATED',
payload: authenticated, payload: authenticated,
}) })
const setUser = (userId: string): SetUserAction => ({
export const setUser = (userId: string): SetUserAction => ({
type: 'AUTHENTICATION_SET_USER', type: 'AUTHENTICATION_SET_USER',
payload: userId, payload: userId,
}) })
export {
setAuthenticated,
setUser,
}

43
src/actions/directory.ts

@ -2,13 +2,14 @@ import { Action, AnyAction } from 'redux'
import { ThunkAction, ThunkDispatch } from 'redux-thunk' import { ThunkAction, ThunkDispatch } from 'redux-thunk'
import { normalize } from 'normalizr' import { normalize } from 'normalizr'
import { getGroups } from '../api/groups'
import { setEntities } from '../actions/entities'
import { getGroup, getGroups } from '../api/groups'
import { setEntity, setEntities } from '../actions/entities'
import { startRequest, finishRequest } from '../actions/requests' import { startRequest, finishRequest } from '../actions/requests'
import { groupSchema } from '../store/schemas' import { groupSchema } from '../store/schemas'
import { AppState } from '../types' import { AppState } from '../types'
const FETCH_ID = 'groups'
const FETCH_GROUP_ID = 'FETCH_GROUP'
const FETCH_GROUPS_ID = 'FETCH_GROUPS'
export interface SetGroupsAction extends Action { export interface SetGroupsAction extends Action {
type: 'DIRECTORY_SET_GROUPS' type: 'DIRECTORY_SET_GROUPS'
@ -27,24 +28,39 @@ export interface SetContinuationAction extends Action {
export type DirectoryActions = SetGroupsAction | AppendGroupsAction | SetContinuationAction export type DirectoryActions = SetGroupsAction | AppendGroupsAction | SetContinuationAction
const setGroups = (groups: string[]): SetGroupsAction => ({
export const setGroups = (groups: string[]): SetGroupsAction => ({
type: 'DIRECTORY_SET_GROUPS', type: 'DIRECTORY_SET_GROUPS',
payload: groups, payload: groups,
}) })
const appendGroups = (groups: string[]): AppendGroupsAction => ({
export const appendGroups = (groups: string[]): AppendGroupsAction => ({
type: 'DIRECTORY_APPEND_GROUPS', type: 'DIRECTORY_APPEND_GROUPS',
payload: groups, payload: groups,
}) })
const setContinuation = (continuation: string): SetContinuationAction => ({
export const setContinuation = (continuation: string): SetContinuationAction => ({
type: 'DIRECTORY_SET_CONTINUATION', type: 'DIRECTORY_SET_CONTINUATION',
payload: continuation, payload: continuation,
}) })
const fetchGroups = (sort?: string, continuation?: string): ThunkAction<Promise<void>, AppState, void, AnyAction> => {
export const fetchGroup = (id: string) => {
return async (dispatch: ThunkDispatch<AppState, void, AnyAction>) => { return async (dispatch: ThunkDispatch<AppState, void, AnyAction>) => {
dispatch(startRequest(FETCH_ID))
dispatch(startRequest(FETCH_GROUP_ID))
try {
const group = await getGroup(id)
dispatch(setEntity('group', group))
dispatch(finishRequest(FETCH_GROUP_ID, true))
} catch (err) {
console.error(err)
dispatch(finishRequest(FETCH_GROUP_ID, false))
}
}
}
export const fetchGroups = (sort?: string, continuation?: string): ThunkAction<Promise<void>, AppState, void, AnyAction> => {
return async (dispatch: ThunkDispatch<AppState, void, AnyAction>) => {
dispatch(startRequest(FETCH_GROUPS_ID))
try { try {
const response = await getGroups(sort, continuation) const response = await getGroups(sort, continuation)
@ -57,17 +73,10 @@ const fetchGroups = (sort?: string, continuation?: string): ThunkAction<Promise<
dispatch(setContinuation(response.continuation)) dispatch(setContinuation(response.continuation))
} }
dispatch(finishRequest(FETCH_ID, true))
dispatch(finishRequest(FETCH_GROUPS_ID, true))
} catch (err) { } catch (err) {
console.error(err) console.error(err)
dispatch(finishRequest(FETCH_ID, false))
dispatch(finishRequest(FETCH_GROUPS_ID, false))
} }
} }
} }
export {
setGroups,
appendGroups,
setContinuation,
fetchGroups,
}

9
src/actions/entities.ts

@ -18,7 +18,7 @@ export interface SetEntitiesAction extends Action {
export type EntitiesActions = SetEntityAction | SetEntitiesAction export type EntitiesActions = SetEntityAction | SetEntitiesAction
const setEntity = (type: string, entity: Entity): SetEntityAction => ({
export const setEntity = (type: string, entity: Entity): SetEntityAction => ({
type: 'ENTITIES_SET_ENTITY', type: 'ENTITIES_SET_ENTITY',
payload: { payload: {
type, type,
@ -26,14 +26,9 @@ const setEntity = (type: string, entity: Entity): SetEntityAction => ({
} }
}) })
const setEntities = (entities: EntityStore): SetEntitiesAction => ({
export const setEntities = (entities: EntityStore): SetEntitiesAction => ({
type: 'ENTITIES_SET_ENTITIES', type: 'ENTITIES_SET_ENTITIES',
payload: { payload: {
entities, entities,
} }
}) })
export {
setEntity,
setEntities,
}

18
src/actions/forms.ts

@ -37,16 +37,16 @@ export interface SetFieldNotificationAction extends Action {
export type FormsActions = InitFormAction | InitFieldAction | SetFieldValueAction | SetFormNotificationAction | SetFieldNotificationAction export type FormsActions = InitFormAction | InitFieldAction | SetFieldValueAction | SetFormNotificationAction | SetFieldNotificationAction
const initForm = (): InitFormAction => ({
export const initForm = (): InitFormAction => ({
type: 'FORMS_INIT', type: 'FORMS_INIT',
}) })
const initField = (name: string): InitFieldAction => ({
export const initField = (name: string): InitFieldAction => ({
type: 'FORMS_INIT_FIELD', type: 'FORMS_INIT_FIELD',
payload: name, payload: name,
}) })
const setFieldValue = (name: string, value: any): SetFieldValueAction => ({
export const setFieldValue = (name: string, value: any): SetFieldValueAction => ({
type: 'FORMS_SET_FIELD_VALUE', type: 'FORMS_SET_FIELD_VALUE',
payload: { payload: {
name, name,
@ -54,7 +54,7 @@ const setFieldValue = (name: string, value: any): SetFieldValueAction => ({
}, },
}) })
const setFormNotification = (type: NotificationType, message: string): SetFormNotificationAction => ({
export const setFormNotification = (type: NotificationType, message: string): SetFormNotificationAction => ({
type: 'FORMS_SET_FORM_NOTIFICATION', type: 'FORMS_SET_FORM_NOTIFICATION',
payload: { payload: {
type, type,
@ -62,7 +62,7 @@ const setFormNotification = (type: NotificationType, message: string): SetFormNo
} }
}) })
const setFieldNotification = (name: string, type: NotificationType, message: string): SetFieldNotificationAction => ({
export const setFieldNotification = (name: string, type: NotificationType, message: string): SetFieldNotificationAction => ({
type: 'FORMS_SET_FIELD_NOTIFICATION', type: 'FORMS_SET_FIELD_NOTIFICATION',
payload: { payload: {
name, name,
@ -70,11 +70,3 @@ const setFieldNotification = (name: string, type: NotificationType, message: str
message, message,
} }
}) })
export {
initForm,
initField,
setFieldValue,
setFormNotification,
setFieldNotification,
}

0
src/actions/index.ts

6
src/actions/menu.ts

@ -7,11 +7,7 @@ export interface SetCollapsedAction extends Action {
export type MenuActions = SetCollapsedAction export type MenuActions = SetCollapsedAction
const setCollapsed = (collapsed: boolean): SetCollapsedAction => ({
export const setCollapsed = (collapsed: boolean): SetCollapsedAction => ({
type: 'MENU_SET_COLLAPSED', type: 'MENU_SET_COLLAPSED',
payload: collapsed payload: collapsed
}) })
export {
setCollapsed,
}

70
src/actions/notifications.ts

@ -1,36 +1,66 @@
import { Action } from 'redux'
import { Action, AnyAction } from 'redux'
import { ThunkAction, ThunkDispatch } from 'redux-thunk'
import { v1 } from 'uuid'
export interface SetAutoAction extends Action {
type: 'NOTIFICATIONS_SET_AUTO'
import { AppState, NotificationType } from '../types'
export interface AddNotificationAction extends Action {
type: 'NOTIFICATIONS_ADD_NOTIFICATION'
payload: { payload: {
id: string id: string
type: NotificationType
content: string
} }
} }
export interface DismissAction extends Action {
type: 'NOTIFICATIONS_DISMISS'
payload: {
id: string
}
export interface RemoveNotificationAction extends Action {
type: 'NOTIFICATIONS_REMOVE_NOTIFICATION'
payload: string
} }
export type NotificationActions = SetAutoAction | DismissAction
export interface ClearNotificationAction extends Action {
type: 'NOTIFICATIONS_CLEAR_NOTIFICATION'
payload: string
}
const setAuto = (id: string): SetAutoAction => ({
type: 'NOTIFICATIONS_SET_AUTO',
export interface SetNotificationAutoAction extends Action {
type: 'NOTIFICATIONS_SET_AUTO'
payload: string
}
export type NotificationActions = AddNotificationAction | RemoveNotificationAction | ClearNotificationAction | SetNotificationAutoAction
export const addNotification = (id: string, type: NotificationType, content: string): AddNotificationAction => ({
type: 'NOTIFICATIONS_ADD_NOTIFICATION',
payload: { payload: {
id, id,
},
type,
content,
}
}) })
const dismiss = (id: string): DismissAction => ({
type: 'NOTIFICATIONS_DISMISS',
payload: {
id,
},
export const removeNotification = (id: string): RemoveNotificationAction => ({
type: 'NOTIFICATIONS_REMOVE_NOTIFICATION',
payload: id,
})
export const clearNotification = (id: string): ClearNotificationAction => ({
type: 'NOTIFICATIONS_CLEAR_NOTIFICATION',
payload: id,
}) })
export {
setAuto,
dismiss,
export const setNotificationAuto = (id: string): SetNotificationAutoAction => ({
type: 'NOTIFICATIONS_SET_AUTO',
payload: id,
})
export const showNotification = (type: NotificationType, content: string, expiration: number = 5000): ThunkAction<Promise<void>, AppState, void, AnyAction> => {
return async (dispatch: ThunkDispatch<AppState, void, AnyAction>) => {
const id = v1()
dispatch(addNotification(id, type, content))
setTimeout(() => {
dispatch(clearNotification(id))
}, expiration)
}
} }

9
src/actions/requests.ts

@ -17,22 +17,17 @@ export interface FinishRequestAction extends Action {
export type RequestsActions = StartRequestAction | FinishRequestAction export type RequestsActions = StartRequestAction | FinishRequestAction
const startRequest = (id: string): StartRequestAction => ({
export const startRequest = (id: string): StartRequestAction => ({
type: 'REQUESTS_START_REQUEST', type: 'REQUESTS_START_REQUEST',
payload: { payload: {
id, id,
}, },
}) })
const finishRequest = (id: string, succeeded: boolean): FinishRequestAction => ({
export const finishRequest = (id: string, succeeded: boolean): FinishRequestAction => ({
type: 'REQUESTS_FINISH_REQUEST', type: 'REQUESTS_FINISH_REQUEST',
payload: { payload: {
id, id,
succeeded, succeeded,
}, },
}) })
export {
startRequest,
finishRequest,
}

6
src/api/groups.ts

@ -6,6 +6,12 @@ interface GroupsResponse {
continuation?: string continuation?: string
} }
export async function getGroup(id: string) {
return await fetch<Entity>({
path: `/api/group/${id}`
})
}
export async function getGroups(sort: string = 'members', continuation?: string) { export async function getGroups(sort: string = 'members', continuation?: string) {
const params = { const params = {
sort, sort,

7
src/components/app/app.tsx

@ -1,6 +1,7 @@
import React, { FC } from 'react' import React, { FC } from 'react'
import { HashRouter as Router, Route, Link } from 'react-router-dom' import { HashRouter as Router, Route, Link } from 'react-router-dom'
import NotificationContainer from '../notification-container'
import Spinner from '../spinner' import Spinner from '../spinner'
import UserInfo from '../user-info' import UserInfo from '../user-info'
@ -52,9 +53,11 @@ const App: FC<Props> = ({ menuCollapsed, fetching }) => {
<div id="main-column" style={{ marginLeft: mainColumnLeftMargin }}> <div id="main-column" style={{ marginLeft: mainColumnLeftMargin }}>
<Route exact path="/" component={Home} /> <Route exact path="/" component={Home} />
<Route path="/login" component={Home} /> <Route path="/login" component={Home} />
<Route path="/register/:group" component={Register} />
<Route path="/directory" component={Directory} />
<Route path="/c/:group/register" component={Register} />
<Route path="/communities" component={Directory} />
</div> </div>
<NotificationContainer />
</div> </div>
</Router> </Router>
) )

6
src/components/notification-container/index.ts

@ -1,7 +1,7 @@
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { setAuto, dismiss } from '../../actions/notifications'
import { setNotificationAuto, removeNotification } from '../../actions/notifications'
import { getNotifications } from '../../selectors' import { getNotifications } from '../../selectors'
import { AppState } from '../../types' import { AppState } from '../../types'
@ -13,10 +13,10 @@ const mapStateToProps = (state: AppState) => ({
const mapDispatchToProps = (dispatch: Dispatch) => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
setAuto: (id: string) => { setAuto: (id: string) => {
dispatch(setAuto(id))
dispatch(setNotificationAuto(id))
}, },
dismiss: (id: string) => { dismiss: (id: string) => {
dispatch(dismiss(id))
dispatch(removeNotification(id))
} }
}) })

11
src/components/notification-container/notification-container.scss

@ -1,6 +1,11 @@
@import "../../../node_modules/bulma/sass/utilities/_all.sass";
@import "../../../node_modules/bulma/sass/base/_all.sass";
@import "../../../node_modules/bulma/sass/elements/button.sass";
@import "../../../node_modules/bulma/sass/elements/notification.sass";
div#notification-container { div#notification-container {
bottom: 0;
bottom: 10px;
position: absolute; position: absolute;
right: 0;
width: 25%;
right: 10px;
width: 40%;
} }

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

@ -17,6 +17,7 @@ const NotificationContainer: FC<Props> = ({ notifications, setAuto, dismiss }) =
{notifications.map(notification => { {notifications.map(notification => {
return ( return (
<Notification <Notification
key={notification.id}
id={notification.id} id={notification.id}
type={notification.type} type={notification.type}
auto={notification.auto} auto={notification.auto}

9
src/components/notification/index.tsx

@ -1,4 +1,4 @@
import React, { FC } from 'react'
import React, { FC, MouseEventHandler } from 'react'
import { NotificationType } from '../../types' import { NotificationType } from '../../types'
interface Props { interface Props {
@ -23,9 +23,14 @@ const Notification: FC<Props> = ({ id, type, auto, setAuto, dismiss, children })
getClassName(type), getClassName(type),
].join(' ') ].join(' ')
const handleDismiss: MouseEventHandler = e => {
e.stopPropagation()
dismiss(id)
}
return ( return (
<div className={classnames} onClick={() => setAuto(id)}> <div className={classnames} onClick={() => setAuto(id)}>
{!auto && <button className="delete" onClick={() => dismiss(id)}></button>}
{!auto && <button className="delete" onClick={handleDismiss}></button>}
{children} {children}
</div> </div>
) )

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

@ -15,7 +15,7 @@ const mapStateToProps = (state: AppState) => ({
const mapDispatchToProps = (dispatch: ThunkDispatch<AppState, void, AnyAction>) => ({ const mapDispatchToProps = (dispatch: ThunkDispatch<AppState, void, AnyAction>) => ({
fetchGroups: () => { fetchGroups: () => {
dispatch(fetchGroups()) dispatch(fetchGroups())
}
},
}) })
export default connect( export default connect(

15
src/components/pages/register/register.tsx

@ -1,12 +1,21 @@
import React, { FC } from 'react'
import React, { FC, useEffect } from 'react'
import { RouteComponentProps } from 'react-router'
import { Entity } from '../../../types' import { Entity } from '../../../types'
interface Props {
interface Params {
group: string
}
type Props = RouteComponentProps<Params> & {
group?: Entity group?: Entity
} }
const Register: FC<Props> = ({ group }) => {
const Register: FC<Props> = ({ group, match }) => {
useEffect(() => {
})
return ( return (
<div> <div>
<h1 className="title">Create a new Account</h1> <h1 className="title">Create a new Account</h1>

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

@ -32,7 +32,7 @@ const UserInfo: FC<Props> = ({ authenticated, user }) => {
<p> <p>
<Link to="/login" className="is-size-5 has-text-white has-text-weight-bold">Log In</Link> <Link to="/login" className="is-size-5 has-text-white has-text-weight-bold">Log In</Link>
<br /> <br />
<Link to="/directory" className="is-size-7 has-text-primary">Sign Up</Link>
<Link to="/communities" className="is-size-7 has-text-primary">Sign Up</Link>
</p> </p>
</div> </div>
</div> </div>

23
src/reducers/notifications.ts

@ -7,9 +7,28 @@ const initialState: NotificationsState = []
const reducer: Reducer<NotificationsState, NotificationActions> = (state = initialState, action) => { const reducer: Reducer<NotificationsState, NotificationActions> = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case 'NOTIFICATIONS_ADD_NOTIFICATION':
const { id, type, content } = action.payload
return [
...state,
{
id,
type,
content,
auto: true,
},
]
case 'NOTIFICATIONS_REMOVE_NOTIFICATION':
return state.filter(notification => notification.id !== action.payload)
case 'NOTIFICATIONS_CLEAR_NOTIFICATION':
return state.filter(notification => {
if (notification.id === action.payload) return !notification.auto
return true
})
case 'NOTIFICATIONS_SET_AUTO': case 'NOTIFICATIONS_SET_AUTO':
return state.map(notification => { return state.map(notification => {
if (notification.id === action.payload.id) {
if (notification.id === action.payload) {
return { return {
...notification, ...notification,
auto: false, auto: false,
@ -18,8 +37,6 @@ const reducer: Reducer<NotificationsState, NotificationActions> = (state = initi
return notification return notification
}) })
case 'NOTIFICATIONS_DISMISS':
return state.filter(notification => notification.id !== action.payload.id)
default: default:
return state return state
} }

2
src/store/index.ts

@ -22,7 +22,7 @@ const store = createStore(
notifications, notifications,
requests, requests,
}), }),
applyMiddleware(logger, thunk)
applyMiddleware(thunk, logger)
) )
export default store export default store

9
src/store/schemas.ts

@ -1,12 +1,7 @@
import { schema } from 'normalizr' import { schema } from 'normalizr'
const groupSchema = new schema.Entity('groups')
export const groupSchema = new schema.Entity('groups')
const userSchema = new schema.Entity('users', {
export const userSchema = new schema.Entity('users', {
groupSchema, groupSchema,
}) })
export {
groupSchema,
userSchema,
}

1
src/types/store.ts

@ -25,7 +25,6 @@ export interface Notification {
type: NotificationType type: NotificationType
content: string content: string
auto: boolean auto: boolean
expiration: number
} }
export interface AuthenticationState { export interface AuthenticationState {

Loading…
Cancel
Save