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) => )} + + + + {events.map((event, i) => ( + + + + + + + + + ))} + +
{field.name}
{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 ( +
+
+ {fieldsConfig.map((field, i) => ( + + ))} +
+ +
+ +
+ ) + } +} + +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)