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/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 })
+ )
+ })
+})
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