From a9be9641039d5fcc736773350f50200accecb5fe Mon Sep 17 00:00:00 2001 From: Aleksei Iakovets Date: Wed, 13 Dec 2017 15:55:14 +0300 Subject: [PATCH 1/2] Home task 2 --- admin-panel/src/components/App.js | 3 + .../src/components/people/NewPersonForm.js | 61 +++++----- .../src/components/people/PeopleList.css | 15 +++ .../src/components/people/PeopleList.jsx | 28 +++++ admin-panel/src/components/routes/Events.jsx | 48 ++++++++ .../src/components/routes/PersonPage.js | 12 +- admin-panel/src/config.js | 6 +- admin-panel/src/ducks/auth.test.js | 113 ++++++++++++++++++ admin-panel/src/ducks/events.js | 111 +++++++++++++++++ admin-panel/src/ducks/people.js | 16 ++- admin-panel/src/ducks/people.test.js | 7 +- admin-panel/src/redux/reducer.js | 4 +- admin-panel/src/redux/saga.js | 4 +- 13 files changed, 385 insertions(+), 43 deletions(-) create mode 100644 admin-panel/src/components/people/PeopleList.css create mode 100644 admin-panel/src/components/people/PeopleList.jsx create mode 100644 admin-panel/src/components/routes/Events.jsx create mode 100644 admin-panel/src/ducks/auth.test.js create mode 100644 admin-panel/src/ducks/events.js diff --git a/admin-panel/src/components/App.js b/admin-panel/src/components/App.js index 6abd045..f2f7f85 100644 --- a/admin-panel/src/components/App.js +++ b/admin-panel/src/components/App.js @@ -4,6 +4,7 @@ import AuthPage from './routes/auth' import AdminPage from './routes/Admin' import ProtectedRoute from './common/ProtectedRoute' import PersonPage from './routes/PersonPage' +import EventsPage from './routes/Events' class App extends Component { static propTypes = { @@ -17,10 +18,12 @@ class App extends Component { + ) } diff --git a/admin-panel/src/components/people/NewPersonForm.js b/admin-panel/src/components/people/NewPersonForm.js index 6707eb7..ea28258 100644 --- a/admin-panel/src/components/people/NewPersonForm.js +++ b/admin-panel/src/components/people/NewPersonForm.js @@ -1,40 +1,47 @@ import React, { Component } from 'react' -import {reduxForm, Field} from 'redux-form' +import { reduxForm, Field } from 'redux-form' import validateEmail from 'email-validator' import ErrorField from '../common/ErrorField' class NewPersonForm extends Component { - static propTypes = { - - }; - - render() { - return ( -
-
- - - -
- -
- -
- ) - } + static propTypes = { + + }; + + submit = (values) => { + return new Promise((resolve) => { + this.props.addPerson(values, resolve) + }) + .then(() => this.props.reset()) + } + + render() { + return ( +
+
+ + + +
+ +
+ +
+ ) + } } -function validate({firstName, email}) { - const errors = {} - if (!firstName) errors.firstName = 'first name is required' +function validate({ firstName, email }) { + const errors = {} + if (!firstName) errors.firstName = 'first name is required' - if (!email) errors.email = 'email is required' - else if (!validateEmail.validate(email)) errors.email = 'email is invalid' + if (!email) errors.email = 'email is required' + else if (!validateEmail.validate(email)) errors.email = 'email is invalid' - return errors + return errors } export default reduxForm({ - form: 'person', - validate + form: 'person', + validate })(NewPersonForm) \ No newline at end of file diff --git a/admin-panel/src/components/people/PeopleList.css b/admin-panel/src/components/people/PeopleList.css new file mode 100644 index 0000000..831ec81 --- /dev/null +++ b/admin-panel/src/components/people/PeopleList.css @@ -0,0 +1,15 @@ +.people-list { + width: 100%; + border: 1px solid black; + border-collapse: collapse; +} + +.people-list caption { + caption-side: top; + font-size: 1.3em; +} + +.people-list th, +.people-list td { + border: 1px solid black; +} diff --git a/admin-panel/src/components/people/PeopleList.jsx b/admin-panel/src/components/people/PeopleList.jsx new file mode 100644 index 0000000..b0975c1 --- /dev/null +++ b/admin-panel/src/components/people/PeopleList.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import './PeopleList.css'; + +export default function PeopleList(props) { + return ( + + + + + + + + + + + + {props.people.map(person => ( + + + + + + ))} + +
People list
FirstnameLastnameEmail
{person.firstName}{person.lastName}{person.email}
+
+ ); +} diff --git a/admin-panel/src/components/routes/Events.jsx b/admin-panel/src/components/routes/Events.jsx new file mode 100644 index 0000000..cb2872d --- /dev/null +++ b/admin-panel/src/components/routes/Events.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import Loader from '../common/Loader'; +import { dataSelector, loadingSelector, errorSelector, eventsRequest } from '../../ducks/events'; + +class Events extends React.Component { + componentDidMount() { + this.props.eventsRequest(); + } + + render() { + if (this.props.loading) return ; + if (this.props.error) return

{this.props.error}

; + + return ( + + + + + + + + + + + + + {this.props.events.map(event => ( + + + + + + + + + ))} + +
MonthSubmission dead lineTitleURLWhenWhere
{event.month}{event.submissionDeadline}{event.title}{event.url}{event.when}{event.where}
+ ); + } +} + +export default connect(state => ({ + events: dataSelector(state), + loading: loadingSelector(state), + error: errorSelector(state), +}), { eventsRequest })(Events); diff --git a/admin-panel/src/components/routes/PersonPage.js b/admin-panel/src/components/routes/PersonPage.js index fd69263..4f36652 100644 --- a/admin-panel/src/components/routes/PersonPage.js +++ b/admin-panel/src/components/routes/PersonPage.js @@ -1,7 +1,8 @@ import React, { Component } from 'react' import {connect} from 'react-redux' -import {addPerson} from '../../ducks/people' +import {addPerson, peopleSelector} from '../../ducks/people' import NewPersonForm from '../people/NewPersonForm' +import PeopleList from '../people/PeopleList' class PersonPage extends Component { static propTypes = { @@ -12,10 +13,15 @@ class PersonPage extends Component { return (

Add new person

- + +
) } } -export default connect(null, {addPerson})(PersonPage) \ No newline at end of file +const mapStateToProps = state => ({ + people: peopleSelector(state) +}); + +export default connect(mapStateToProps, {addPerson})(PersonPage) \ No newline at end of file diff --git a/admin-panel/src/config.js b/admin-panel/src/config.js index 65bba00..9bb9f02 100644 --- a/admin-panel/src/config.js +++ b/admin-panel/src/config.js @@ -1,14 +1,14 @@ import firebase from 'firebase' -export const appName = 'advreact-04-12' +export const appName = 'advreact-386f6' const config = { - apiKey: "AIzaSyCmDWlgYIhtEr1pWjgKYds3iXKWBl9wbjE", + apiKey: "AIzaSyDSPRtistNZnrnNMJXCra5uS9Ugpken3F0", authDomain: `${appName}.firebaseapp.com`, databaseURL: `https://${appName}.firebaseio.com`, projectId: appName, storageBucket: "", - messagingSenderId: "95255462276" + messagingSenderId: "648901552269" } firebase.initializeApp(config) \ No newline at end of file diff --git a/admin-panel/src/ducks/auth.test.js b/admin-panel/src/ducks/auth.test.js new file mode 100644 index 0000000..d4fba5a --- /dev/null +++ b/admin-panel/src/ducks/auth.test.js @@ -0,0 +1,113 @@ +import { take, put, call, apply } from 'redux-saga/effects'; +import firebase from 'firebase'; +import { + signUpSaga, + signInSaga, + SIGN_UP_REQUEST, + SIGN_UP_START, + SIGN_UP_SUCCESS, + SIGN_UP_ERROR, + SIGN_IN_START, + SIGN_IN_SUCCESS, + SIGN_IN_ERROR, +} from './auth'; + +const action = { + type: SIGN_UP_REQUEST, + payload: { + email: 'test@email.com', + password: '8888888', + }, +}; + +const { email, password } = action.payload; + +const auth = firebase.auth(); + +const user = { + userData: 'data', +}; + +const error = new Error('Something went wrong'); + +describe('signUpSaga saga', () => { + test('signup has done successfully', () => { + const generator = signUpSaga(); + + expect(generator.next().value) + .toEqual(take(SIGN_UP_REQUEST)); + + expect(generator.next(action).value) + .toEqual(put({ type: SIGN_UP_START })); + + expect(generator.next().value) + .toEqual(call([auth, auth.createUserWithEmailAndPassword], email, password)); + + expect(generator.next(user).value) + .toEqual(put({ + type: SIGN_UP_SUCCESS, + payload: { user }, + })); + + expect(generator.next().done).toBe(false); + }); + + test('signup has done unsuccessfully', () => { + const generator = signUpSaga(); + + expect(generator.next().value) + .toEqual(take(SIGN_UP_REQUEST)); + + expect(generator.next(action).value) + .toEqual(put({ type: SIGN_UP_START })); + + expect(generator.next().value) + .toEqual(call([auth, auth.createUserWithEmailAndPassword], email, password)); + + expect(generator.throw(error).value) + .toEqual(put({ + type: SIGN_UP_ERROR, + payload: { error }, + })); + + expect(generator.next().done).toBe(false); + }); +}); + +describe('signInSaga saga', () => { + test('signin has done successfully, ', () => { + const generator = signInSaga(action); + + expect(generator.next().value) + .toEqual(put({ type: SIGN_IN_START })); + + expect(generator.next().value) + .toEqual(apply(auth, auth.signInWithEmailAndPassword, [email, password])); + + expect(generator.next(user).value) + .toEqual(put({ + type: SIGN_IN_SUCCESS, + payload: { user }, + })); + + expect(generator.next().done).toBe(true); + }); + + test('signin has done unsuccessfully, ', () => { + const generator = signInSaga(action); + + expect(generator.next().value) + .toEqual(put({ type: SIGN_IN_START })); + + expect(generator.next().value) + .toEqual(apply(auth, auth.signInWithEmailAndPassword, [email, password])); + + expect(generator.throw(error).value) + .toEqual(put({ + type: SIGN_IN_ERROR, + payload: { error }, + })); + + expect(generator.next().done).toBe(true); + }); +}); diff --git a/admin-panel/src/ducks/events.js b/admin-panel/src/ducks/events.js new file mode 100644 index 0000000..5613293 --- /dev/null +++ b/admin-panel/src/ducks/events.js @@ -0,0 +1,111 @@ +import { takeLatest, put, call } from 'redux-saga/effects'; +import { createSelector } from 'reselect'; +import { Record, List } from 'immutable'; +import { appName } from '../config'; + +/** + * Constants + * */ +export const moduleName = 'events'; +const prefix = `${appName}/${moduleName}`; + +export const EVENTS_REQUEST = `${prefix}/EVENTS_REQUEST`; +export const EVENTS_SUCCESS = `${prefix}/EVENTS_SUCCESS`; +export const EVENTS_ERROR = `${prefix}/EVENTS_ERROR`; + +/** + * Reducer + * */ +export const ReducerState = Record({ + data: new List([]), + loading: false, + error: null, +}); + +export const EventRecord = Record({ + id: null, + month: null, + submissionDeadline: null, + title: null, + url: null, + when: null, + where: null, +}); + +export default function reducer(state = new ReducerState(), action) { + const { type, payload } = action; + + switch (type) { + case EVENTS_REQUEST: + return state + .set('loading', true); + + case EVENTS_SUCCESS: + return state + .set('loading', false) + .update('data', (data) => { + const formattedData = Object.keys(payload).map(key => ( + new EventRecord({ + id: key, + ...payload[key], + }) + )); + + return data.merge(formattedData); + }); + + case EVENTS_ERROR: + return state + .set('loading', false) + .set('error', payload.error); + + default: + return state; + } +} + +/** + * Selectors + * */ + +export const stateSelector = state => state[moduleName]; +export const dataSelector = createSelector(stateSelector, state => state.data); +export const errorSelector = createSelector(stateSelector, state => state.error); +export const loadingSelector = createSelector(stateSelector, state => state.loading); + +/** + * Action Creators + * */ + +export function eventsRequest() { + return { + type: EVENTS_REQUEST, + }; +} + +/** + * Sagas + */ + +export function* eventsSaga() { + try { + const response = yield call(fetch, 'https://advreact-386f6.firebaseio.com/events.json'); + const data = yield call([response, 'json']); + + if (!response.ok) throw data; + + yield put({ + type: EVENTS_SUCCESS, + payload: data, + }); + } catch (error) { + yield put({ + type: EVENTS_ERROR, + payload: error, + }); + } +} + +export function* watchEventsSaga() { + yield takeLatest(EVENTS_REQUEST, eventsSaga); +} diff --git a/admin-panel/src/ducks/people.js b/admin-panel/src/ducks/people.js index 52bce70..65dc281 100644 --- a/admin-panel/src/ducks/people.js +++ b/admin-panel/src/ducks/people.js @@ -1,6 +1,7 @@ import {appName} from '../config' import {Record, List} from 'immutable' import {put, call, all, takeEvery} from 'redux-saga/effects' +import {createSelector} from 'reselect' import {generateId} from './utils' /** @@ -30,7 +31,7 @@ export default function reducer(state = new ReducerState(), action) { switch (type) { case ADD_PERSON_SUCCESS: - return state.update('entities', entities => entities.push(new PersonRecord(payload.person))) + return state.update('entities', entities => entities.push(new PersonRecord(payload))) default: return state @@ -41,14 +42,17 @@ export default function reducer(state = new ReducerState(), action) { * Selectors * */ +export const stateSelector = state => state[moduleName] +export const peopleSelector = createSelector(stateSelector, state => state.entities) + /** * Action Creators * */ -export function addPerson(person) { +export function addPerson(person, resolve) { return { type: ADD_PERSON, - payload: { person } + payload: { person, resolve } } } @@ -57,14 +61,16 @@ export function addPerson(person) { */ export const addPersonSaga = function * (action) { - const { person } = action.payload - + const { person, resolve } = action.payload + const id = yield call(generateId) yield put({ type: ADD_PERSON_SUCCESS, payload: {id, ...person} }) + + yield call(resolve) } export const saga = function * () { diff --git a/admin-panel/src/ducks/people.test.js b/admin-panel/src/ducks/people.test.js index e1bf76f..098a2bd 100644 --- a/admin-panel/src/ducks/people.test.js +++ b/admin-panel/src/ducks/people.test.js @@ -7,12 +7,12 @@ describe('people saga', () => { const person = { firstName: 'Roman', lastName: 'Iakobchuk', - email: 'r.iakobchuk@javascript.ru' + email: 'r.iakobchuk@javascript.ru', } const action = { type: ADD_PERSON, - payload: { person } + payload: { person, resolve: Promise.resolve } } const generator = addPersonSaga(action) @@ -26,7 +26,8 @@ describe('people saga', () => { payload: {id, ...person} })) - expect(generator.next().done).toBe(true) + expect(generator.next().value).toEqual(call(Promise.resolve)) + expect(generator.next().done).toBe(true) }); }); \ No newline at end of file diff --git a/admin-panel/src/redux/reducer.js b/admin-panel/src/redux/reducer.js index cc6e0aa..d593bc8 100644 --- a/admin-panel/src/redux/reducer.js +++ b/admin-panel/src/redux/reducer.js @@ -3,9 +3,11 @@ 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 }) \ No newline at end of file diff --git a/admin-panel/src/redux/saga.js b/admin-panel/src/redux/saga.js index 4a6c841..c2d123b 100644 --- a/admin-panel/src/redux/saga.js +++ b/admin-panel/src/redux/saga.js @@ -1,10 +1,12 @@ import {all} from 'redux-saga/effects' import {saga as peopleSaga} from '../ducks/people' import {saga as authSaga} from '../ducks/auth' +import {watchEventsSaga} from '../ducks/events' export default function * rootSaga() { yield all([ peopleSaga(), - authSaga() + authSaga(), + watchEventsSaga() ]) } \ No newline at end of file From fcded6080a4a05dd81a12aca17090f0c1b5d431e Mon Sep 17 00:00:00 2001 From: Aleksei Iakovets Date: Thu, 14 Dec 2017 13:09:52 +0300 Subject: [PATCH 2/2] Reset form has removed from the component NewPersonForm and has placed reset action in the saga addPersonSaga --- admin-panel/src/components/people/NewPersonForm.js | 9 +-------- admin-panel/src/components/routes/PersonPage.js | 2 +- admin-panel/src/ducks/people.js | 9 +++++---- admin-panel/src/ducks/people.test.js | 5 +++-- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/admin-panel/src/components/people/NewPersonForm.js b/admin-panel/src/components/people/NewPersonForm.js index ea28258..d63318c 100644 --- a/admin-panel/src/components/people/NewPersonForm.js +++ b/admin-panel/src/components/people/NewPersonForm.js @@ -8,17 +8,10 @@ class NewPersonForm extends Component { }; - submit = (values) => { - return new Promise((resolve) => { - this.props.addPerson(values, resolve) - }) - .then(() => this.props.reset()) - } - render() { return (
-
+ diff --git a/admin-panel/src/components/routes/PersonPage.js b/admin-panel/src/components/routes/PersonPage.js index 4f36652..935b022 100644 --- a/admin-panel/src/components/routes/PersonPage.js +++ b/admin-panel/src/components/routes/PersonPage.js @@ -13,7 +13,7 @@ class PersonPage extends Component { return (

Add new person

- +
) diff --git a/admin-panel/src/ducks/people.js b/admin-panel/src/ducks/people.js index 65dc281..fb53b8b 100644 --- a/admin-panel/src/ducks/people.js +++ b/admin-panel/src/ducks/people.js @@ -2,6 +2,7 @@ import {appName} from '../config' import {Record, List} from 'immutable' import {put, call, all, takeEvery} from 'redux-saga/effects' import {createSelector} from 'reselect' +import { reset } from 'redux-form' import {generateId} from './utils' /** @@ -49,10 +50,10 @@ export const peopleSelector = createSelector(stateSelector, state => state.entit * Action Creators * */ -export function addPerson(person, resolve) { +export function addPerson(person) { return { type: ADD_PERSON, - payload: { person, resolve } + payload: { person } } } @@ -61,7 +62,7 @@ export function addPerson(person, resolve) { */ export const addPersonSaga = function * (action) { - const { person, resolve } = action.payload + const { person } = action.payload const id = yield call(generateId) @@ -70,7 +71,7 @@ export const addPersonSaga = function * (action) { payload: {id, ...person} }) - yield call(resolve) + yield put(reset('person')) } export const saga = function * () { diff --git a/admin-panel/src/ducks/people.test.js b/admin-panel/src/ducks/people.test.js index 098a2bd..43fe6d1 100644 --- a/admin-panel/src/ducks/people.test.js +++ b/admin-panel/src/ducks/people.test.js @@ -1,4 +1,5 @@ import {call, put} from 'redux-saga/effects' +import { reset } from 'redux-form' import {addPersonSaga, ADD_PERSON, ADD_PERSON_SUCCESS} from './people' import {generateId} from './utils' @@ -12,7 +13,7 @@ describe('people saga', () => { const action = { type: ADD_PERSON, - payload: { person, resolve: Promise.resolve } + payload: { person } } const generator = addPersonSaga(action) @@ -26,7 +27,7 @@ describe('people saga', () => { payload: {id, ...person} })) - expect(generator.next().value).toEqual(call(Promise.resolve)) + expect(generator.next().value).toEqual(put(reset('person'))) expect(generator.next().done).toBe(true) });