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..d63318c 100644
--- a/admin-panel/src/components/people/NewPersonForm.js
+++ b/admin-panel/src/components/people/NewPersonForm.js
@@ -1,40 +1,40 @@
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 = {
+ static propTypes = {
- };
+ };
- render() {
- return (
-
- )
- }
+ 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 (
+
+
+ People list
+
+
+ | Firstname |
+ Lastname |
+ Email |
+
+
+
+ {props.people.map(person => (
+
+ | {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 (
+
+
+
+ | Month |
+ Submission dead line |
+ Title |
+ URL |
+ When |
+ Where |
+
+
+
+ {this.props.events.map(event => (
+
+ | {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..935b022 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 = {
@@ -13,9 +14,14 @@ class PersonPage extends Component {
)
}
}
-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..fb53b8b 100644
--- a/admin-panel/src/ducks/people.js
+++ b/admin-panel/src/ducks/people.js
@@ -1,6 +1,8 @@
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'
/**
@@ -30,7 +32,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,6 +43,9 @@ 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
* */
@@ -58,13 +63,15 @@ export function addPerson(person) {
export const addPersonSaga = function * (action) {
const { person } = action.payload
-
+
const id = yield call(generateId)
yield put({
type: ADD_PERSON_SUCCESS,
payload: {id, ...person}
})
+
+ 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 e1bf76f..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'
@@ -7,7 +8,7 @@ describe('people saga', () => {
const person = {
firstName: 'Roman',
lastName: 'Iakobchuk',
- email: 'r.iakobchuk@javascript.ru'
+ email: 'r.iakobchuk@javascript.ru',
}
const action = {
@@ -26,7 +27,8 @@ describe('people saga', () => {
payload: {id, ...person}
}))
- expect(generator.next().done).toBe(true)
+ expect(generator.next().value).toEqual(put(reset('person')))
+ 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