diff --git a/admin-panel/package.json b/admin-panel/package.json index d61a23f..db74ee9 100644 --- a/admin-panel/package.json +++ b/admin-panel/package.json @@ -8,6 +8,7 @@ "history": "^4.7.2", "immutable": "^3.8.2", "logger": "^0.0.1", + "prop-types": "^15.6.0", "react": "^16.2.0", "react-dom": "^16.2.0", "react-redux": "^5.0.6", diff --git a/admin-panel/src/components/App.js b/admin-panel/src/components/App.js index 207fb54..2c4a3b9 100644 --- a/admin-panel/src/components/App.js +++ b/admin-panel/src/components/App.js @@ -18,6 +18,7 @@ class App extends Component { diff --git a/admin-panel/src/components/events/EventList.js b/admin-panel/src/components/events/EventList.js new file mode 100644 index 0000000..eac0d12 --- /dev/null +++ b/admin-panel/src/components/events/EventList.js @@ -0,0 +1,72 @@ +import React, {Component} from 'react' +import firebase from 'firebase/index' + +class EventList extends Component { + static propTypes = { + + }; + + state = { + events: null + } + + componentDidMount = () => { + const eventsRef = firebase.database().ref('events/'); + + eventsRef.on('value', (snapshot) => { + let events = [] + + snapshot.forEach((childSnapshot) => { + events.push({ + id: childSnapshot.key, + ...childSnapshot.val() + }) + }); + + this.setState({events: events}); + }); + } + + getEventList = (events) => { + return ( + + + + + + + + + + + + + { + events.map(event => + + + + + + + + + ) + } + +
MonthDeadlineTitleURLWhenWhere
{event.month}{event.submissionDeadline}{event.title}{event.url}{event.when}{event.where}
+ ) + } + + render() { + const {events} = this.state + + return ( +
+ {!events ?

List is empty

: this.getEventList(events)} +
+ ) + } +} + +export default EventList \ No newline at end of file diff --git a/admin-panel/src/components/events/EventsInfiniteLoaderVirtualized.js b/admin-panel/src/components/events/EventsInfiniteLoaderVirtualized.js new file mode 100644 index 0000000..6dce520 --- /dev/null +++ b/admin-panel/src/components/events/EventsInfiniteLoaderVirtualized.js @@ -0,0 +1,78 @@ +import React, {Component} from 'react' +import {connect} from 'react-redux' +import { + fetchLimitEvents, + selectEvent, + fetchAllEvents, + eventLimitListSelector, + eventListSelector +} from '../../ducks/events' +import {InfiniteLoader, List} from 'react-virtualized' +import 'react-virtualized/styles.css' + +export class EventsInfiniteLoaderVirtualized extends Component { + static propTypes = {}; + + componentDidMount() { + this.props.fetchAllEvents() + this.loadMoreRows({startIndex: 0, stopIndex: 10}) + } + + loadMoreRows = ({startIndex, stopIndex}) => { + this.props.fetchLimitEvents(startIndex, stopIndex) + } + + isRowLoaded = ({index}) => { + return !!this.props.events[index]; + } + + rowRenderer = ({key, index, style}) => { + const {events} = this.props + + return ( +
+ {events[index] && this.getRow(events[index])} +
+ ) + } + + getRow = (event) => ( +
+ {event.title} + {event.when} + {event.where} +
+ ) + + render() { + const {eventsTotal, loading} = this.props + + return ( + + {({onRowsRendered, registerChild}) => ( + + )} + + ) + } +} + +export default connect((state) => ({ + events: eventLimitListSelector(state), + eventsTotal: eventListSelector(state).length, +}), {fetchLimitEvents, selectEvent, fetchAllEvents})(EventsInfiniteLoaderVirtualized) \ No newline at end of file diff --git a/admin-panel/src/components/events/EventsTableVirtualized.js b/admin-panel/src/components/events/EventsTableVirtualized.js index c4f8c3e..662fa6d 100644 --- a/admin-panel/src/components/events/EventsTableVirtualized.js +++ b/admin-panel/src/components/events/EventsTableVirtualized.js @@ -16,6 +16,7 @@ export class EventsTableVirtualized extends Component { render() { if (this.props.loading) return + return ( - + + {submitting && } + {submitSuccess &&

Person was added successfully

} + {submitError &&

{submitError}

}
- +
diff --git a/admin-panel/src/components/people/PeopleList.js b/admin-panel/src/components/people/PeopleList.js index 1fe2549..59549a4 100644 --- a/admin-panel/src/components/people/PeopleList.js +++ b/admin-panel/src/components/people/PeopleList.js @@ -1,7 +1,8 @@ import React, { Component } from 'react' import {connect} from 'react-redux' -import {peopleListSelector} from '../../ducks/people' +import {peopleListSelector, fetchAllPeople, loadingSelector} from '../../ducks/people' import {List} from 'react-virtualized' +import Loader from '../common/Loader' import 'react-virtualized/styles.css' class PeopleList extends Component { @@ -9,10 +10,18 @@ class PeopleList extends Component { }; + componentDidMount() { + this.props.fetchAllPeople() + } + render() { + const {people, loading} = this.props + + if (loading) return + return ({ - people: peopleListSelector(state) -}))(PeopleList) \ No newline at end of file + people: peopleListSelector(state), + loading: loadingSelector(state), +}), {fetchAllPeople})(PeopleList) \ No newline at end of file diff --git a/admin-panel/src/components/routes/EventsPage.js b/admin-panel/src/components/routes/EventsPage.js index eedf79d..9232c29 100644 --- a/admin-panel/src/components/routes/EventsPage.js +++ b/admin-panel/src/components/routes/EventsPage.js @@ -1,5 +1,6 @@ import React, { Component } from 'react' import EventsTableVirtualized from '../events/EventsTableVirtualized' +import EventsInfiniteLoaderVirtualized from '../events/EventsInfiniteLoaderVirtualized' class EventsPage extends Component { static propTypes = { @@ -9,7 +10,7 @@ class EventsPage extends Component { render() { return (
- +
) } diff --git a/admin-panel/src/components/routes/auth/index.js b/admin-panel/src/components/routes/auth/index.js index 91c9bab..f75ee3c 100644 --- a/admin-panel/src/components/routes/auth/index.js +++ b/admin-panel/src/components/routes/auth/index.js @@ -1,16 +1,22 @@ import React, { Component } from 'react' +import PropTypes from 'prop-types' import {Route, NavLink} from 'react-router-dom' import {connect} from 'react-redux' + import {signIn, signUp} from '../../../ducks/auth' +import {errorSelector, loadingSelector} from '../../../ducks/auth' import SignInForm from '../../auth/SignInForm' import SignUpForm from '../../auth/SignUpForm' class Auth extends Component { static propTypes = { - + authError: PropTypes.object, + authLoading: PropTypes.bool.isRequired }; render() { + const {authLoading, authError} = this.props; + return (

Auth page

@@ -18,7 +24,7 @@ class Auth extends Component {
  • Sign In
  • Sign Up
  • - } /> + } /> } />
    ) @@ -29,4 +35,7 @@ class Auth extends Component { } -export default connect(null, { signIn, signUp })(Auth) \ No newline at end of file +export default connect(state => ({ + authError: errorSelector(state), + authLoading: loadingSelector(state) +}), { signIn, signUp })(Auth) \ No newline at end of file diff --git a/admin-panel/src/components/user/NewUserForm.js b/admin-panel/src/components/user/NewUserForm.js new file mode 100644 index 0000000..605022d --- /dev/null +++ b/admin-panel/src/components/user/NewUserForm.js @@ -0,0 +1,50 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import {reduxForm, Field} from 'redux-form'; +import validator from 'email-validator'; + +import ErrorField from '../common/ErrorField'; + +class NewUserForm extends Component { + static propTypes = { + handleSubmit: PropTypes.func.isRequired + }; + + render() { + return ( +
    +

    Add user

    +
    +
    + First name: +
    +
    + Last name: +
    +
    + Email: +
    + + + +
    + ) + } +} + +const validate = ({ firstName, lastName, email }) => { + const errors = {}; + + if (!email) errors.email = 'Email is a required field'; + if (email && !validator.validate(email)) errors.email = 'Incorrect email format'; + + if (!firstName) errors.firstName = 'First name is a required field'; + if (!lastName) errors.lastName = 'Last name is a required field'; + + return errors; +} + +export default reduxForm({ + form: 'new-user', + validate +})(NewUserForm) \ No newline at end of file diff --git a/admin-panel/src/config.js b/admin-panel/src/config.js index 65bba00..202b263 100644 --- a/admin-panel/src/config.js +++ b/admin-panel/src/config.js @@ -1,14 +1,14 @@ -import firebase from 'firebase' +import firebase from 'firebase'; -export const appName = 'advreact-04-12' +export const appName = 'adv-react-8c4fb'; const config = { - apiKey: "AIzaSyCmDWlgYIhtEr1pWjgKYds3iXKWBl9wbjE", + apiKey: "AIzaSyCPlodrXJgpCXqDWklsFc_V5vHGiD6G2Uc", authDomain: `${appName}.firebaseapp.com`, databaseURL: `https://${appName}.firebaseio.com`, - projectId: appName, - storageBucket: "", - messagingSenderId: "95255462276" -} + projectId: {appName}, + storageBucket: "adv-react-8c4fb.appspot.com", + messagingSenderId: "47356959167" +}; -firebase.initializeApp(config) \ No newline at end of file +firebase.initializeApp(config); \ No newline at end of file diff --git a/admin-panel/src/ducks/events.js b/admin-panel/src/ducks/events.js index f13e342..93a344e 100644 --- a/admin-panel/src/ducks/events.js +++ b/admin-panel/src/ducks/events.js @@ -1,6 +1,6 @@ import {all, takeEvery, put, call} from 'redux-saga/effects' import {appName} from '../config' -import {Record, OrderedSet, OrderedMap} from 'immutable' +import {Record, OrderedSet, OrderedMap, List} from 'immutable' import firebase from 'firebase' import {createSelector} from 'reselect' import {fbToEntities} from './utils' @@ -14,6 +14,9 @@ const prefix = `${appName}/${moduleName}` export const FETCH_ALL_REQUEST = `${prefix}/FETCH_ALL_REQUEST` export const FETCH_ALL_START = `${prefix}/FETCH_ALL_START` export const FETCH_ALL_SUCCESS = `${prefix}/FETCH_ALL_SUCCESS` +export const FETCH_LIMIT_REQUEST = `${prefix}/FETCH_LIMIT_REQUEST` +export const FETCH_LIMIT_START = `${prefix}/FETCH_LIMIT_START` +export const FETCH_LIMIT_SUCCESS = `${prefix}/FETCH_LIMIT_SUCCESS` export const SELECT = `${prefix}/SELECT` @@ -22,9 +25,12 @@ export const SELECT = `${prefix}/SELECT` * */ export const ReducerRecord = Record({ loading: false, + loadingLimit: false, loaded: false, + loadedLimit: false, selected: new OrderedSet(), - entities: new OrderedMap({}) + entities: new OrderedMap({}), + entitiesLimit: new List([]) }) export const EventRecord = Record({ @@ -43,13 +49,18 @@ export default function reducer(state = new ReducerRecord(), action) { switch (type) { case FETCH_ALL_START: return state.set('loading', true) - + case FETCH_LIMIT_START: + return state.set('loadingLimit', true) case FETCH_ALL_SUCCESS: return state .set('loading', false) .set('loaded', true) .set('entities', fbToEntities(payload, EventRecord)) - + case FETCH_LIMIT_SUCCESS: + return state + .set('loadingLimit', false) + .set('loadedLimit', true) + .set('entitiesLimit', fbToEntities(payload, EventRecord)) case SELECT: return state.update('selected', selected => selected.has(payload.uid) ? selected.remove(payload.uid) @@ -67,9 +78,13 @@ export default function reducer(state = new ReducerRecord(), action) { export const stateSelector = state => state[moduleName] export const entitiesSelector = createSelector(stateSelector, state => state.entities) +export const entitiesLimitSelector = createSelector(stateSelector, state => state.entitiesLimit) export const loadingSelector = createSelector(stateSelector, state => state.loading) +export const loadingLimitSelector = createSelector(stateSelector, state => state.loadingLimit) export const loadedSelector = createSelector(stateSelector, state => state.loaded) +export const loadedLimitSelector = createSelector(stateSelector, state => state.loadedLimit) export const eventListSelector = createSelector(entitiesSelector, entities => entities.valueSeq().toArray()) +export const eventLimitListSelector = createSelector(entitiesLimitSelector, entitiesLimitSelector => entitiesLimitSelector.valueSeq().toArray()) /** * Action Creators @@ -81,6 +96,13 @@ export function fetchAllEvents() { } } +export function fetchLimitEvents(startAt, limit) { + return { + type: FETCH_LIMIT_REQUEST, + payload: {startAt, limit} + } +} + export function selectEvent(uid) { return { type: SELECT, @@ -101,16 +123,32 @@ export function* fetchAllSaga() { const snapshot = yield call([ref, ref.once], 'value') - console.log('---', snapshot) - yield put({ type: FETCH_ALL_SUCCESS, payload: snapshot.val() }) } +export function* fetchLimitSaga(action) { + + const {startAt, limit} = action.payload + const ref = firebase.database().ref('events').orderByKey().startAt(startAt.toString()).limitToFirst(limit) + + yield put({ + type: FETCH_LIMIT_START + }) + + const snapshot = yield call([ref, ref.once], 'value') + + yield put({ + type: FETCH_LIMIT_SUCCESS, + payload: snapshot.val() + }) +} + export function* saga() { yield all([ - takeEvery(FETCH_ALL_REQUEST, fetchAllSaga) + takeEvery(FETCH_ALL_REQUEST, fetchAllSaga), + takeEvery(FETCH_LIMIT_REQUEST, fetchLimitSaga) ]) } \ No newline at end of file diff --git a/admin-panel/src/ducks/people.js b/admin-panel/src/ducks/people.js index 9a5f996..cf15eed 100644 --- a/admin-panel/src/ducks/people.js +++ b/admin-panel/src/ducks/people.js @@ -3,20 +3,31 @@ import {Record, OrderedMap} from 'immutable' import {createSelector} from 'reselect' import {put, call, all, takeEvery} from 'redux-saga/effects' import {reset} from 'redux-form' -import {generateId} from './utils' +import firebase from "firebase/index" +import {fbToEntities} from "./utils" /** * Constants * */ export const moduleName = 'people' const prefix = `${appName}/${moduleName}` -export const ADD_PERSON = `${prefix}/ADD_PERSON` +export const ADD_PERSON_REQUEST = `${prefix}/ADD_PERSON_REQUEST` +export const ADD_PERSON_START = `${prefix}/ADD_PERSON_START` export const ADD_PERSON_SUCCESS = `${prefix}/ADD_PERSON_SUCCESS` +export const ADD_PERSON_ERROR = `${prefix}/ADD_PERSON_ERROR` +export const FETCH_ALL_REQUEST = `${prefix}/FETCH_ALL_REQUEST` +export const FETCH_ALL_START = `${prefix}/FETCH_ALL_START` +export const FETCH_ALL_SUCCESS = `${prefix}/FETCH_ALL_SUCCESS` /** * Reducer * */ const ReducerState = Record({ + addPersonLoading: false, + addPersonSuccess: false, + addPersonError: null, + loading: false, + loaded: false, entities: new OrderedMap({}) }) @@ -31,9 +42,24 @@ export default function reducer(state = new ReducerState(), action) { const {type, payload} = action switch (type) { + case ADD_PERSON_START: + return state.set('addPersonLoading', true) case ADD_PERSON_SUCCESS: - return state.setIn(['entities', payload.uid],new PersonRecord(payload)) + return state + .set('addPersonLoading', false) + .set('addPersonSuccess', true) + case ADD_PERSON_ERROR: + return state + .set('addPersonLoading', false) + .set('addPersonError', payload.error.message) + case FETCH_ALL_START: + return state.set('loading', true) + case FETCH_ALL_SUCCESS: + return state + .set('loading', false) + .set('loaded', true) + .set('entities', fbToEntities(payload, PersonRecord)) default: return state } @@ -43,6 +69,11 @@ export default function reducer(state = new ReducerState(), action) { * Selectors * */ export const stateSelector = state => state[moduleName] +export const addPersonLoadingSelector = createSelector(stateSelector, state => state.addPersonLoading) +export const addPersonSuccessSelector = createSelector(stateSelector, state => state.addPersonSuccess) +export const addPersonErrorSelector = createSelector(stateSelector, state => state.addPersonError) +export const loadingSelector = createSelector(stateSelector, state => state.loading) +export const loadedSelector = createSelector(stateSelector, state => state.loaded) export const entitiesSelector = createSelector(stateSelector, state => state.entities) export const peopleListSelector = createSelector(entitiesSelector, entities => entities.valueSeq().toArray()) @@ -52,11 +83,17 @@ export const peopleListSelector = createSelector(entitiesSelector, entities => e export function addPerson(person) { return { - type: ADD_PERSON, + type: ADD_PERSON_REQUEST, payload: { person } } } +export function fetchAllPeople() { + return { + type: FETCH_ALL_REQUEST + } +} + /** * Sagas */ @@ -64,18 +101,52 @@ export function addPerson(person) { export const addPersonSaga = function * (action) { const { person } = action.payload - const uid = yield call(generateId) + const ref = firebase.database().ref('/people') yield put({ - type: ADD_PERSON_SUCCESS, - payload: {uid, ...person} + type: ADD_PERSON_START }) - yield put(reset('person')) + try { + yield call([ref, ref.push], person) + + yield put({ + type: ADD_PERSON_SUCCESS, + }) + + // update peolple + yield put({ + type: FETCH_ALL_REQUEST + }) + + yield put(reset('person')) + } catch (error) { + + yield put({ + type: ADD_PERSON_ERROR, + payload: {error} + }) + } +} + +export function* fetchAllSaga() { + const ref = firebase.database().ref('people') + + yield put({ + type: FETCH_ALL_START + }) + + const snapshot = yield call([ref, ref.once], 'value') + + yield put({ + type: FETCH_ALL_SUCCESS, + payload: snapshot.val() + }) } export const saga = function * () { yield all([ - takeEvery(ADD_PERSON, addPersonSaga) + takeEvery(ADD_PERSON_REQUEST, addPersonSaga), + takeEvery(FETCH_ALL_REQUEST, fetchAllSaga) ]) } \ No newline at end of file diff --git a/admin-panel/src/ducks/utils.js b/admin-panel/src/ducks/utils.js index df63cc2..d459ddb 100644 --- a/admin-panel/src/ducks/utils.js +++ b/admin-panel/src/ducks/utils.js @@ -1,4 +1,4 @@ -import {OrderedMap} from 'immutable' +import {OrderedMap, List} from 'immutable' export function generateId() { return Date.now() @@ -10,4 +10,4 @@ export function fbToEntities(values, DataRecord) { (acc, [uid, value]) => acc.set(uid, new DataRecord({ uid, ...value })), new OrderedMap({}) ) -} \ No newline at end of file +}