diff --git a/admin/src/App.js b/admin/src/App.js
index 89f2dc0..8aa2b40 100644
--- a/admin/src/App.js
+++ b/admin/src/App.js
@@ -4,20 +4,28 @@ import ProtectedRoute from './components/common/protected-route'
import AuthPage from './routes/auth'
import AdminPage from './routes/admin'
+const activeStyle = { color: 'red' }
+
class App extends Component {
render() {
return (
-
+
+ events
+
+
+
+
people
-
+
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..a6ca8a4
--- /dev/null
+++ b/admin/src/components/events/events-list.js
@@ -0,0 +1,48 @@
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+import { eventsSelector, getEventsList } from '../../ducks/events'
+import { fieldsConfig as eventFields } from './new-event-form'
+
+class EventList extends Component {
+ static propTypes = {}
+
+ componentDidMount() {
+ this.props.dispatch(getEventsList())
+ }
+
+ render() {
+ const { events } = this.props
+
+ return (
+
+ {events.length ? (
+
+
+
+ {eventFields.map((field, i) => | {field.name} | )}
+
+
+
+ {events.map((event, i) => (
+
+ | {event.title} |
+ {event.url} |
+ {event.where} |
+ {event.month} |
+ {event.when} |
+ {event.submissionDeadline} |
+
+ ))}
+
+
+ ) : (
+
The events have not yet been created
+ )}
+
+ )
+ }
+}
+
+export default connect((state) => ({
+ events: eventsSelector(state)
+}))(EventList)
diff --git a/admin/src/components/events/new-event-form.js b/admin/src/components/events/new-event-form.js
new file mode 100644
index 0000000..bfbb518
--- /dev/null
+++ b/admin/src/components/events/new-event-form.js
@@ -0,0 +1,68 @@
+import React, { Component } from 'react'
+import { reduxForm, Field } from 'redux-form'
+import ErrorField from '../common/error-field'
+
+export const fieldsConfig = [
+ {
+ name: 'title',
+ type: 'text'
+ },
+ {
+ name: 'url',
+ type: 'text'
+ },
+ {
+ name: 'where',
+ type: 'text'
+ },
+ {
+ name: 'month',
+ type: 'month'
+ },
+ {
+ name: 'when',
+ type: 'date'
+ },
+ {
+ name: 'submissionDeadline',
+ type: 'date'
+ }
+]
+
+class NewEventForm extends Component {
+ static propTypes = {}
+
+ render() {
+ return (
+
+ )
+ }
+}
+
+function validate({ title }) {
+ const errors = {}
+
+ if (!title) errors.title = 'title is required'
+
+ return errors
+}
+
+export default reduxForm({
+ form: 'event',
+ validate
+})(NewEventForm)
diff --git a/admin/src/config.js b/admin/src/config.js
deleted file mode 100644
index aa22b3b..0000000
--- a/admin/src/config.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import firebase from 'firebase/app'
-import 'firebase/auth'
-import 'firebase/database'
-
-export const appName = 'advreact-10-05'
-
-export const config = {
- apiKey: 'AIzaSyCbMQM0eQUSQ0SuLVAu9ZNPUcm4rdbiB8U',
- authDomain: `${appName}.firebaseapp.com`,
- databaseURL: `https://${appName}.firebaseio.com`,
- projectId: appName,
- storageBucket: '',
- messagingSenderId: '1094825197832'
-}
-
-firebase.initializeApp(config)
diff --git a/admin/src/ducks/auth.js b/admin/src/ducks/auth.js
index 55ae8d0..a2aaf93 100644
--- a/admin/src/ducks/auth.js
+++ b/admin/src/ducks/auth.js
@@ -78,6 +78,7 @@ export function signIn(email, password) {
export function* signUpSaga({ payload: { email, password } }) {
const auth = firebase.auth()
+
try {
const user = yield call(
[auth, auth.createUserWithEmailAndPassword],
diff --git a/admin/src/ducks/auth.test.js b/admin/src/ducks/auth.test.js
new file mode 100644
index 0000000..21da067
--- /dev/null
+++ b/admin/src/ducks/auth.test.js
@@ -0,0 +1,88 @@
+import { call, put, take, apply } from 'redux-saga/effects'
+import { reset } from 'redux-form'
+import firebase from 'firebase/app'
+import {
+ signIn,
+ signUp,
+ signUpSaga,
+ signInSaga,
+ SIGN_UP_REQUEST,
+ SIGN_UP_SUCCESS,
+ SIGN_UP_ERROR,
+ SIGN_IN_REQUEST,
+ SIGN_IN_SUCCESS,
+ SIGN_IN_ERROR,
+ SIGN_IN_REQUESTS_LIMIT
+} from './auth'
+import { user } from '../mocks/user'
+
+const auth = firebase.auth()
+const { email, password } = user
+
+describe('auth duck', () => {
+ it('should sign up success', () => {
+ const sagaProcess = signUpSaga(signUp(email, password))
+
+ expect(sagaProcess.next().value).toEqual(
+ call([auth, auth.createUserWithEmailAndPassword], email, password)
+ )
+
+ expect(sagaProcess.next(user).value).toEqual(
+ put({ type: SIGN_UP_SUCCESS, payload: { user } })
+ )
+
+ expect(sagaProcess.next().done).toEqual(true)
+ })
+ it('should sign up with error', () => {
+ const error = new Error('sign up with error')
+ const sagaProcess = signUpSaga(signUp(email, password))
+
+ expect(sagaProcess.next().value).toEqual(
+ call([auth, auth.createUserWithEmailAndPassword], email, password)
+ )
+
+ expect(sagaProcess.throw(error).value).toEqual(
+ put({ type: SIGN_UP_ERROR, error })
+ )
+
+ expect(sagaProcess.next().done).toEqual(true)
+ })
+
+ it('should sign in success', () => {
+ const sagaProcess = signInSaga(signIn(email, password))
+
+ expect(sagaProcess.next().value).toEqual(take(SIGN_IN_REQUEST))
+
+ expect(sagaProcess.next({ payload: { email, password } }).value).toEqual(
+ apply(auth, auth.signInWithEmailAndPassword, [email, password])
+ )
+
+ expect(sagaProcess.next(user).value).toEqual(
+ put({ type: SIGN_IN_SUCCESS, payload: { user } })
+ )
+
+ // without counting attempts
+ })
+ it('should limit the count of attempts to 3 and throw error', () => {
+ const error = new Error('sign in with error')
+ const sagaProcess = signInSaga(signIn(email, password))
+
+ for (let i = 0; i < 3; i++) {
+ expect(sagaProcess.next().value).toEqual(take(SIGN_IN_REQUEST))
+
+ expect(sagaProcess.next({ payload: { email, password } }).value).toEqual(
+ apply(auth, auth.signInWithEmailAndPassword, [email, password])
+ )
+
+ expect(sagaProcess.throw(error).value).toEqual(
+ put({ type: SIGN_IN_ERROR, error })
+ )
+ }
+
+ expect(sagaProcess.next().value).toEqual(
+ put({ type: SIGN_IN_REQUESTS_LIMIT })
+ )
+
+ expect(sagaProcess.next().done).toEqual(true)
+ })
+})
diff --git a/admin/src/ducks/events.js b/admin/src/ducks/events.js
new file mode 100644
index 0000000..96f37fe
--- /dev/null
+++ b/admin/src/ducks/events.js
@@ -0,0 +1,116 @@
+import { appName } from '../config'
+import { Record, List } from 'immutable'
+import { reset } from 'redux-form'
+import firebase from 'firebase/app'
+import { createSelector } from 'reselect'
+import { takeEvery, all, put, call, apply } from 'redux-saga/effects'
+import { generateId } from './utils'
+
+/**
+ * Constants
+ * */
+export const moduleName = 'events'
+const prefix = `${appName}/${moduleName}`
+export const ADD_EVENT_REQUEST = `${prefix}/ADD_EVENT_REQUEST`
+export const ADD_EVENT = `${prefix}/ADD_EVENT`
+
+export const GET_EVENT_REQUEST = `${prefix}/GET_EVENT_REQUEST`
+export const GET_EVENT_SUCCESS = `${prefix}/GET_EVENT_SUCCESS`
+export const GET_EVENT_ERROR = `${prefix}/GET_EVENT_ERROR`
+
+/**
+ * Reducer
+ * */
+const ReducerState = Record({
+ entities: new List([])
+})
+
+const EventRecord = Record({
+ id: null,
+ title: null,
+ url: null,
+ where: null,
+ month: null,
+ when: null,
+ submissionDeadline: null
+})
+
+export default function reducer(state = new ReducerState(), action) {
+ const { type, payload } = action
+
+ switch (type) {
+ case ADD_EVENT:
+ return state.update('entities', (entities) =>
+ entities.unshift(new EventRecord(payload))
+ )
+ case GET_EVENT_SUCCESS:
+ const eventList = Object.keys(payload).map((key) => ({
+ id: key,
+ ...payload[key]
+ }))
+ return state.set(
+ 'entities',
+ new List(eventList.map((item) => new EventRecord(item)))
+ )
+
+ default:
+ return state
+ }
+}
+/**
+ * Selectors
+ * */
+
+export const stateSelector = (state) => state[moduleName]
+export const eventsSelector = createSelector(stateSelector, (state) =>
+ state.entities.valueSeq().toArray()
+)
+
+/**
+ * Action Creators
+ * */
+
+export function addEvent(event) {
+ return {
+ type: ADD_EVENT_REQUEST,
+ payload: { event }
+ }
+}
+export function getEventsList() {
+ return {
+ type: GET_EVENT_REQUEST
+ }
+}
+
+/**
+ * Sagas
+ * */
+
+export function* addEventSaga({ payload: { event } }) {
+ const id = yield call(generateId)
+
+ yield put({ type: ADD_EVENT, payload: { id, ...event } })
+ yield put(reset('event'))
+}
+
+export function* getEventListSaga() {
+ const table = firebase.database().ref('events/')
+
+ try {
+ const snapshot = yield apply(table, table.once, ['value'])
+
+ yield put({
+ type: GET_EVENT_SUCCESS,
+ payload: snapshot.val()
+ })
+ } catch (error) {
+ yield put({ type: GET_EVENT_ERROR, error })
+ }
+}
+
+export function* saga() {
+ yield all([
+ takeEvery(ADD_EVENT_REQUEST, addEventSaga),
+ takeEvery(GET_EVENT_REQUEST, getEventListSaga)
+ ])
+}
diff --git a/admin/src/ducks/utils.js b/admin/src/ducks/utils.js
index 8006db7..e15ceb8 100644
--- a/admin/src/ducks/utils.js
+++ b/admin/src/ducks/utils.js
@@ -1,3 +1,9 @@
export function generateId() {
return Date.now() + Math.random()
}
+
+export function generatePassword() {
+ return Math.random()
+ .toString(36)
+ .slice(-8)
+}
diff --git a/admin/src/mocks/user.js b/admin/src/mocks/user.js
new file mode 100644
index 0000000..5c9a0aa
--- /dev/null
+++ b/admin/src/mocks/user.js
@@ -0,0 +1,6 @@
+import { generatePassword } from '../ducks/utils'
+
+export const user = {
+ email: 'test@test.ru',
+ password: generatePassword()
+}
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..57d120e 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 EventPage from './event-page'
class AdminPage extends Component {
static propTypes = {}
@@ -8,8 +9,9 @@ class AdminPage extends Component {
render() {
return (
-
Admin Page
+ Admin Pages
+
)
}
diff --git a/admin/src/routes/event-page.js b/admin/src/routes/event-page.js
new file mode 100644
index 0000000..21059c7
--- /dev/null
+++ b/admin/src/routes/event-page.js
@@ -0,0 +1,23 @@
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+import { addEvent } from '../ducks/events'
+import NewPersonForm from '../components/events/new-event-form'
+import EventsList from '../components/events/events-list'
+
+class EventPage extends Component {
+ static propTypes = {}
+
+ render() {
+ return (
+
+
Add new event
+
+
+ Events list
+
+
+ )
+ }
+}
+
+export default connect(null, { addEvent })(EventPage)