From c531c1dcac24e354fa4ff0b680c8ff80c6126b2a Mon Sep 17 00:00:00 2001 From: Denis Date: Sun, 20 May 2018 16:25:21 +0000 Subject: [PATCH 1/2] add tests for auth duck --- admin/src/ducks/auth.test.js | 110 +++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 admin/src/ducks/auth.test.js diff --git a/admin/src/ducks/auth.test.js b/admin/src/ducks/auth.test.js new file mode 100644 index 0000000..f50e85b --- /dev/null +++ b/admin/src/ducks/auth.test.js @@ -0,0 +1,110 @@ +import { apply, call, put, take } from 'redux-saga/effects' +import firebase from 'firebase/app' +import reducer, { + ReducerRecord, + signUpSaga, + SIGN_UP_REQUEST, + SIGN_UP_SUCCESS, + SIGN_UP_ERROR, + signInSaga, + SIGN_IN_REQUEST, + SIGN_IN_SUCCESS, + SIGN_IN_ERROR, + SIGN_IN_REQUESTS_LIMIT +} from './auth' + +describe('auth duck', () => { + /** + * Reducer + * */ + it('should sign in', () => { + const state = ReducerRecord() + const user = { + email: 'test@test.com' + } + + const newState = reducer(state, { + type: SIGN_IN_SUCCESS, + payload: { user } + }) + + expect(newState).toEqual(ReducerRecord({ user })) + }) + + /** + * Sagas + */ + it('should sign up', () => { + const email = 'test@test.com' + const password = 'password' + const user = { + email + } + + const sagaProcess = signUpSaga({ + type: SIGN_UP_REQUEST, + payload: { email, password } + }) + + const auth = firebase.auth() + + expect(sagaProcess.next().value).toEqual( + call([auth, auth.createUserWithEmailAndPassword], email, password) + ) + + expect(sagaProcess.next(user).value).toEqual( + put({ type: SIGN_UP_SUCCESS, payload: { user } }) + ) + + const error = new Error() + + expect(sagaProcess.throw(error).value).toEqual( + put({ type: SIGN_UP_ERROR, error }) + ) + }) + + it('should sign in', () => { + const email = 'test@test.com' + const password = 'password' + const user = { + email + } + + const sagaProcess = signInSaga() + + expect(sagaProcess.next().value).toEqual(take(SIGN_IN_REQUEST)) + + const auth = firebase.auth() + + expect( + sagaProcess.next({ type: SIGN_IN_REQUEST, payload: { email, password } }) + .value + ).toEqual(apply(auth, auth.signInWithEmailAndPassword, [email, password])) + + expect(sagaProcess.next(user).value).toEqual( + put({ type: SIGN_IN_SUCCESS, payload: { user } }) + ) + }) + + it('should have limit for sign in', () => { + const email = 'test@test.com' + const password = 'password' + + const sagaProcess = signInSaga() + + for (let i = 0; i < 3; i++) { + sagaProcess.next() + sagaProcess.next({ type: SIGN_IN_REQUEST, payload: { email, password } }) + + const error = new Error() + + expect(sagaProcess.throw(error).value).toEqual( + put({ type: SIGN_IN_ERROR, error }) + ) + } + + expect(sagaProcess.next().value).toEqual( + put({ type: SIGN_IN_REQUESTS_LIMIT }) + ) + }) +}) From 3005b5b20a4f053856db6490c5bb36041106245c Mon Sep 17 00:00:00 2001 From: Denis Date: Sun, 20 May 2018 16:26:32 +0000 Subject: [PATCH 2/2] add events --- admin/src/App.js | 5 ++ admin/src/components/events/events-list.js | 43 ++++++++++ admin/src/ducks/events.js | 98 ++++++++++++++++++++++ admin/src/ducks/utils.js | 8 ++ admin/src/redux/reducer.js | 4 +- admin/src/redux/saga.js | 3 +- admin/src/routes/admin.js | 2 + admin/src/routes/events-page.js | 15 ++++ 8 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 admin/src/components/events/events-list.js create mode 100644 admin/src/ducks/events.js create mode 100644 admin/src/routes/events-page.js diff --git a/admin/src/App.js b/admin/src/App.js index 89f2dc0..160980f 100644 --- a/admin/src/App.js +++ b/admin/src/App.js @@ -13,6 +13,11 @@ class App extends Component { people +
+ + events + +
auth diff --git a/admin/src/components/events/events-list.js b/admin/src/components/events/events-list.js new file mode 100644 index 0000000..1aa84b6 --- /dev/null +++ b/admin/src/components/events/events-list.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { + eventsSelector, + loadingSelector, + loadedSelector, + fetchEvents +} from '../../ducks/events' + +class EventsList extends Component { + componentDidMount() { + this.fetchEvents() + } + + fetchEvents = () => { + if (this.props.loading || this.props.loaded) return + + this.props.fetchEvents() + } + + render() { + if (this.props.loading) return 'Loading...' + + return ( +
    + {this.props.events.map(({ id, title, url, where, when }) => ( +
  • + {title} in {where} at {when} +
  • + ))} +
+ ) + } +} + +export default connect( + (state) => ({ + events: eventsSelector(state), + loading: loadingSelector(state), + loaded: loadedSelector(state) + }), + { fetchEvents } +)(EventsList) diff --git a/admin/src/ducks/events.js b/admin/src/ducks/events.js new file mode 100644 index 0000000..88a9cd9 --- /dev/null +++ b/admin/src/ducks/events.js @@ -0,0 +1,98 @@ +import { appName } from '../config' +import { Record, OrderedMap } from 'immutable' +import firebase from 'firebase/app' +import { createSelector } from 'reselect' +import { takeEvery, all, put, call } from 'redux-saga/effects' +import { objectToOrderedMap } from './utils' + +/** + * Constants + * */ +export const moduleName = 'events' +const prefix = `${appName}/${moduleName}` +export const FETCH_EVENTS_REQUEST = `${prefix}/FETCH_EVENTS_REQUEST` +export const FETCH_EVENTS_SUCCESS = `${prefix}/FETCH_EVENTS_SUCCESS` +export const FETCH_EVENTS_ERROR = `${prefix}/FETCH_EVENTS_ERROR` + +/** + * Reducer + * */ +const ReducerState = Record({ + entities: OrderedMap(), + loading: false, + loaded: false +}) + +const EventRecord = Record({ + id: null, + title: null, + url: null, + where: null, + when: null, + month: null, + submissionDeadline: null +}) + +export default function reducer(state = ReducerState(), action) { + const { type, payload } = action + + switch (type) { + case FETCH_EVENTS_REQUEST: + return state.set('loading', true) + + case FETCH_EVENTS_SUCCESS: + return state + .set('loading', false) + .set('loaded', true) + .set('entities', objectToOrderedMap(payload, EventRecord)) + + case FETCH_EVENTS_ERROR: + return state.set('loading', false) + + default: + return state + } +} + +/** + * Selectors + * */ +export const stateSelector = (state) => state[moduleName] +export const loadingSelector = createSelector( + stateSelector, + (state) => state.loading +) +export const loadedSelector = createSelector( + stateSelector, + (state) => state.loaded +) +export const eventsSelector = createSelector(stateSelector, (state) => + state.entities.valueSeq().toArray() +) + +/** + * Action Creators + * */ +export function fetchEvents() { + return { + type: FETCH_EVENTS_REQUEST + } +} + +/** + * Sagas + * */ +export function* fetchEventsSaga() { + const ref = firebase.database().ref('/events') + + try { + const snapshot = yield call([ref, ref.once], 'value') + yield put({ type: FETCH_EVENTS_SUCCESS, payload: snapshot.val() }) + } catch (error) { + yield put({ type: FETCH_EVENTS_ERROR, payload: error }) + } +} + +export function* saga() { + yield all([takeEvery(FETCH_EVENTS_REQUEST, fetchEventsSaga)]) +} diff --git a/admin/src/ducks/utils.js b/admin/src/ducks/utils.js index 8006db7..7f3fd7f 100644 --- a/admin/src/ducks/utils.js +++ b/admin/src/ducks/utils.js @@ -1,3 +1,11 @@ +import { OrderedMap } from 'immutable' + export function generateId() { return Date.now() + Math.random() } + +export const objectToOrderedMap = (obj, ItemRecord) => + Object.entries(obj).reduce( + (acc, [id, value]) => acc.set(id, ItemRecord({ id, ...value })), + OrderedMap() + ) diff --git a/admin/src/redux/reducer.js b/admin/src/redux/reducer.js index f9c1879..7e88eca 100644 --- a/admin/src/redux/reducer.js +++ b/admin/src/redux/reducer.js @@ -3,10 +3,12 @@ import { routerReducer as router } from 'react-router-redux' import { reducer as form } from 'redux-form' import authReducer, { moduleName as authModule } from '../ducks/auth' import peopleReducer, { moduleName as peopleModule } from '../ducks/people' +import eventsReducer, { moduleName as eventsModule } from '../ducks/events' export default combineReducers({ router, form, [authModule]: authReducer, - [peopleModule]: peopleReducer + [peopleModule]: peopleReducer, + [eventsModule]: eventsReducer }) diff --git a/admin/src/redux/saga.js b/admin/src/redux/saga.js index 8f1e0cd..9d0ed64 100644 --- a/admin/src/redux/saga.js +++ b/admin/src/redux/saga.js @@ -1,7 +1,8 @@ import { all } from 'redux-saga/effects' import { saga as authSaga } from '../ducks/auth' import { saga as peopleSaga } from '../ducks/people' +import { saga as eventsSaga } from '../ducks/events' export default function*() { - yield all([authSaga(), peopleSaga()]) + yield all([authSaga(), peopleSaga(), eventsSaga()]) } diff --git a/admin/src/routes/admin.js b/admin/src/routes/admin.js index aa30167..f0edbd1 100644 --- a/admin/src/routes/admin.js +++ b/admin/src/routes/admin.js @@ -1,6 +1,7 @@ import React, { Component } from 'react' import { Route } from 'react-router-dom' import PersonPage from './person-page' +import EventsPage from './events-page' class AdminPage extends Component { static propTypes = {} @@ -10,6 +11,7 @@ class AdminPage extends Component {

Admin Page

+
) } diff --git a/admin/src/routes/events-page.js b/admin/src/routes/events-page.js new file mode 100644 index 0000000..31d76f1 --- /dev/null +++ b/admin/src/routes/events-page.js @@ -0,0 +1,15 @@ +import React, { Component } from 'react' +import EventsList from '../components/events/events-list' + +class EventsPage extends Component { + render() { + return ( +
+

Events

+ +
+ ) + } +} + +export default EventsPage