From 90a396668da73fe365f3cd37eff2ad2821cb955a Mon Sep 17 00:00:00 2001 From: VasilZhdankin Date: Mon, 11 Dec 2017 02:13:47 +0200 Subject: [PATCH 01/12] HW 1 --- admin-panel/src/App.js | 2 + admin-panel/src/components/Users/UserForm.js | 50 +++++++++++ admin-panel/src/components/Users/index.js | 44 +++++++++ admin-panel/src/components/routes/Users.js | 24 +++++ admin-panel/src/config.js | 8 +- admin-panel/src/ducks/users.js | 93 ++++++++++++++++++++ admin-panel/src/helpers/index.js | 10 +++ admin-panel/src/redux/index.js | 3 +- admin-panel/src/redux/middlewares.js | 14 +++ admin-panel/src/redux/reducer.js | 4 +- 10 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 admin-panel/src/components/Users/UserForm.js create mode 100644 admin-panel/src/components/Users/index.js create mode 100644 admin-panel/src/components/routes/Users.js create mode 100644 admin-panel/src/ducks/users.js create mode 100644 admin-panel/src/helpers/index.js create mode 100644 admin-panel/src/redux/middlewares.js diff --git a/admin-panel/src/App.js b/admin-panel/src/App.js index fd110f7..d7f8324 100644 --- a/admin-panel/src/App.js +++ b/admin-panel/src/App.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import {Route} from 'react-router-dom' import Auth from './components/routes/auth' import Admin from './components/routes/Admin' +import Users from './components/routes/Users' import ProtectedRoute from './components/common/ProtectedRoute' class App extends Component { @@ -15,6 +16,7 @@ class App extends Component {

Hello world

+ ) } diff --git a/admin-panel/src/components/Users/UserForm.js b/admin-panel/src/components/Users/UserForm.js new file mode 100644 index 0000000..b93584b --- /dev/null +++ b/admin-panel/src/components/Users/UserForm.js @@ -0,0 +1,50 @@ +import React, {Component} from 'react' +import {reduxForm, Field} from 'redux-form' +import validator from 'email-validator' +import ErrorField from '../common/ErrorField' + + +class UserForm extends Component { + render() { + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ) + } +} + +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 : "users", + validate +})(UserForm) \ No newline at end of file diff --git a/admin-panel/src/components/Users/index.js b/admin-panel/src/components/Users/index.js new file mode 100644 index 0000000..e1e184c --- /dev/null +++ b/admin-panel/src/components/Users/index.js @@ -0,0 +1,44 @@ +import React, {Component} from 'react' +import UserForm from './UserForm' +import {connect} from 'react-redux' +import {usersSelector, usersLoadingSelector} from '../../ducks/users' + +class Users extends Component { + render() { + return ( +
+

Users page

+ { + this.renderUserList() + } + +
+ ) + } + renderUserList = () => { + let {users, isLoading} = this.props; + if (isLoading) return
Uploading new user...
+ return ( + users.length ? +
    + { + users.map(u=>( +
  • +

    First Name: {u.firstName}

    +

    Last Name: {u.lastName}

    +

    email: {u.email}

    +
  • + )) + } +
: +

no user

+ ) + } +} + +export default connect( + state => ({ + users : usersSelector(state), + isLoading : usersLoadingSelector(state), + }) +)(Users) \ No newline at end of file diff --git a/admin-panel/src/components/routes/Users.js b/admin-panel/src/components/routes/Users.js new file mode 100644 index 0000000..82dd334 --- /dev/null +++ b/admin-panel/src/components/routes/Users.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react' +import {Route, NavLink} from 'react-router-dom' +import {connect} from 'react-redux' +import {addUser} from '../../ducks/users' +import UsersList from '../Users' + +class Users extends Component { + render() { + return ( +
+

Users page

+
    +
  • Users list
  • +
+ } /> +
+ ) + } + + onAddUser = ({firstName, lastName, email}) => this.props.addUser({firstName, lastName, email}) + +} + +export default connect(null, { addUser })(Users) \ No newline at end of file diff --git a/admin-panel/src/config.js b/admin-panel/src/config.js index 65bba00..a24ff65 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 = 'react-adv' const config = { - apiKey: "AIzaSyCmDWlgYIhtEr1pWjgKYds3iXKWBl9wbjE", + apiKey: "AIzaSyAs8-oh5yTEUxC5KQSOSZpaE8xReLZk0qQ", authDomain: `${appName}.firebaseapp.com`, databaseURL: `https://${appName}.firebaseio.com`, projectId: appName, - storageBucket: "", - messagingSenderId: "95255462276" + storageBucket: `${appName}.appspot.com`, + messagingSenderId: "857981968367" } firebase.initializeApp(config) \ No newline at end of file diff --git a/admin-panel/src/ducks/users.js b/admin-panel/src/ducks/users.js new file mode 100644 index 0000000..5ad3ac8 --- /dev/null +++ b/admin-panel/src/ducks/users.js @@ -0,0 +1,93 @@ +import {appName} from '../config' +import {Record} from 'immutable' +import {arrayToMap} from '../helpers' +import {createSelector} from 'reselect' + +/** + * Constants + * */ +export const moduleName = 'users' +const prefix = `${appName}/${moduleName}` + +export const ADD_USER_START = `${prefix}/_START` +export const ADD_USER_SUCCESS = `${prefix}/_SUCCESS` + +/** + * Reducer + * */ + +const UsersListRecord = Record({ + id : null, + firstName : null, + lastName : null, + email : null, +}) + +const ReducerRecord = Record({ + entities : arrayToMap([], UsersListRecord), + loading : false, + loaded : false, +}) + +const defaultState = new ReducerRecord(); + +export default function reducer(state = defaultState, action) { + const {type, payload} = action + + switch (type) { + case ADD_USER_START : { + return state.set('loading', true); + } + case ADD_USER_SUCCESS : { + let {randomId : id, firstName, lastName, email} = payload; + state = state.set('loading', false); + return state.setIn(['entities', id], new UsersListRecord({ + id, + firstName, + lastName, + email, + })) + } + default: + return state + } +} + +/** + * Selectors + * */ + +export const usersMapSelector = state => state[moduleName].entities + +export const usersSelector = createSelector(usersMapSelector, usersMap => usersMap.valueSeq().toArray()) + +export const usersLoadingSelector = state => state[moduleName].loading + +/** + * Action Creators + * */ + +export const addUser = (params) => { + let {firstName, lastName, email} = params; + return (dispatch) => { + dispatch({ + type : ADD_USER_START, + }); + new Promise((resolve) => { + setTimeout(() => { + resolve() + }, 3000) + }).then(() => { + dispatch({ + type : ADD_USER_SUCCESS, + payload : { + generateId : true, + firstName, + lastName, + email + } + }) + }) + + } +} \ No newline at end of file diff --git a/admin-panel/src/helpers/index.js b/admin-panel/src/helpers/index.js new file mode 100644 index 0000000..12f9b51 --- /dev/null +++ b/admin-panel/src/helpers/index.js @@ -0,0 +1,10 @@ +import {Map} from 'immutable'; + +export let arrayToMap = (arr, ItemRecord, idKey = 'id') => { + let res = arr.reduce(function (acc, item) { + return acc.set(item[idKey], ItemRecord ? new ItemRecord(item) : item); + }, new Map({})); + return res; +} + +export let getRandomId = () => (Date.now() + Math.random()).toString() \ No newline at end of file diff --git a/admin-panel/src/redux/index.js b/admin-panel/src/redux/index.js index 1a37261..6c5314d 100644 --- a/admin-panel/src/redux/index.js +++ b/admin-panel/src/redux/index.js @@ -4,8 +4,9 @@ import {routerMiddleware} from 'react-router-redux' import thunk from 'redux-thunk' import reducer from './reducer' import history from '../history' +import {randomId} from './middlewares' -const store = createStore(reducer, applyMiddleware(thunk, routerMiddleware(history), logger)) +const store = createStore(reducer, applyMiddleware(thunk, routerMiddleware(history), randomId, logger)) //dev only window.store = store diff --git a/admin-panel/src/redux/middlewares.js b/admin-panel/src/redux/middlewares.js new file mode 100644 index 0000000..05ccb06 --- /dev/null +++ b/admin-panel/src/redux/middlewares.js @@ -0,0 +1,14 @@ +import {getRandomId} from '../helpers'; + +export let randomId = store => next => action => { + let {payload = {}} = action; + let {generateId, ...rest} = payload; + if (!generateId) return next(action); + next({ + ...action, + payload : { + ...rest, + randomId : getRandomId() + } + }) +} \ No newline at end of file diff --git a/admin-panel/src/redux/reducer.js b/admin-panel/src/redux/reducer.js index 34143fe..ddc9f75 100644 --- a/admin-panel/src/redux/reducer.js +++ b/admin-panel/src/redux/reducer.js @@ -2,8 +2,10 @@ import {combineReducers} from 'redux' import {routerReducer as router} from 'react-router-redux' import {reducer as form} from 'redux-form' import authReducer, {moduleName as authModule} from '../ducks/auth' +import usersReducer, {moduleName as userModule} from '../ducks/users' export default combineReducers({ router, form, - [authModule]: authReducer + [authModule] : authReducer, + [userModule] : usersReducer, }) \ No newline at end of file From 5805a64ad5a06e2d1253e8b77dde9b7456cbefb3 Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Mon, 11 Dec 2017 17:06:05 +0200 Subject: [PATCH 02/12] add HW1 --- .gitignore | 23 + admin-panel/src/App.js | 23 - admin-panel/src/Root.js | 2 +- admin-panel/src/components/App.js | 29 + admin-panel/src/components/auth/SignInForm.js | 26 +- admin-panel/src/components/auth/SignUpForm.js | 21 +- .../src/components/common/ErrorField.js | 29 +- admin-panel/src/components/common/Loader.js | 12 + .../src/components/people/NewPersonForm.js | 40 + .../src/components/routes/PersonPage.js | 21 + admin-panel/src/ducks/auth.js | 27 +- admin-panel/src/ducks/people.js | 53 + admin-panel/src/mocks/conferences.js | 2384 +++++++++++++++++ admin-panel/src/mocks/index.js | 7 + admin-panel/src/redux/reducer.js | 4 +- 15 files changed, 2640 insertions(+), 61 deletions(-) create mode 100644 .gitignore delete mode 100644 admin-panel/src/App.js create mode 100644 admin-panel/src/components/App.js create mode 100644 admin-panel/src/components/common/Loader.js create mode 100644 admin-panel/src/components/people/NewPersonForm.js create mode 100644 admin-panel/src/components/routes/PersonPage.js create mode 100644 admin-panel/src/ducks/people.js create mode 100644 admin-panel/src/mocks/conferences.js create mode 100644 admin-panel/src/mocks/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b4863a --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.idea \ No newline at end of file diff --git a/admin-panel/src/App.js b/admin-panel/src/App.js deleted file mode 100644 index fd110f7..0000000 --- a/admin-panel/src/App.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, { Component } from 'react' -import {Route} from 'react-router-dom' -import Auth from './components/routes/auth' -import Admin from './components/routes/Admin' -import ProtectedRoute from './components/common/ProtectedRoute' - -class App extends Component { - static propTypes = { - - }; - - render() { - return ( -
-

Hello world

- - -
- ) - } -} - -export default App \ No newline at end of file diff --git a/admin-panel/src/Root.js b/admin-panel/src/Root.js index f69da88..7d7223d 100644 --- a/admin-panel/src/Root.js +++ b/admin-panel/src/Root.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import {ConnectedRouter as Router} from 'react-router-redux' import {Provider} from 'react-redux' -import App from './App' +import App from './components/App' import store from './redux' import history from './history' diff --git a/admin-panel/src/components/App.js b/admin-panel/src/components/App.js new file mode 100644 index 0000000..6abd045 --- /dev/null +++ b/admin-panel/src/components/App.js @@ -0,0 +1,29 @@ +import React, { Component } from 'react' +import {Route, NavLink} from 'react-router-dom' +import AuthPage from './routes/auth' +import AdminPage from './routes/Admin' +import ProtectedRoute from './common/ProtectedRoute' +import PersonPage from './routes/PersonPage' + +class App extends Component { + static propTypes = { + + }; + + render() { + return ( +
+

Hello world

+
    +
  • admin
  • +
  • people
  • +
+ + + +
+ ) + } +} + +export default App \ No newline at end of file diff --git a/admin-panel/src/components/auth/SignInForm.js b/admin-panel/src/components/auth/SignInForm.js index 3033ef6..da2b013 100644 --- a/admin-panel/src/components/auth/SignInForm.js +++ b/admin-panel/src/components/auth/SignInForm.js @@ -1,5 +1,9 @@ import React, { Component } from 'react' +import {connect} from 'react-redux' import {reduxForm, Field} from 'redux-form' +import ErrorField from '../common/ErrorField' +import Loader from '../common/Loader' +import {loadingSelector, errorSelector} from '../../ducks/auth' class SignInForm extends Component { static propTypes = { @@ -7,24 +11,28 @@ class SignInForm extends Component { }; render() { + const {loading, authError} = this.props return (

Sign In

-
- email: -
-
- password: -
+ + + {authError &&

{authError}

} + {loading && } -
) } } -export default reduxForm({ +export default connect(state => { + console.log('---', 3333, errorSelector(state)) + return { + loading: loadingSelector(state), + authError: errorSelector(state) + } +})(reduxForm({ form: 'auth' -})(SignInForm) \ No newline at end of file +})(SignInForm)) \ No newline at end of file diff --git a/admin-panel/src/components/auth/SignUpForm.js b/admin-panel/src/components/auth/SignUpForm.js index d8e9a85..20b0acf 100644 --- a/admin-panel/src/components/auth/SignUpForm.js +++ b/admin-panel/src/components/auth/SignUpForm.js @@ -1,7 +1,10 @@ import React, { Component } from 'react' +import {connect} from 'react-redux' import {reduxForm, Field} from 'redux-form' import validator from 'email-validator' import ErrorField from '../common/ErrorField' +import Loader from '../common/Loader' +import {loadingSelector, errorSelector} from '../../ducks/auth' class SignUpForm extends Component { static propTypes = { @@ -9,16 +12,15 @@ class SignUpForm extends Component { }; render() { + const {loading, authError} = this.props return (

Sign In

-
- email: -
-
- password: -
+ + + {authError &&

{authError}

} + {loading && } @@ -39,7 +41,10 @@ const validate = ({ email, password }) => { return errors } -export default reduxForm({ +export default connect(state => ({ + loading: loadingSelector(state), + authError: errorSelector(state) +}))(reduxForm({ form: 'auth', validate -})(SignUpForm) \ No newline at end of file +})(SignUpForm)) \ No newline at end of file diff --git a/admin-panel/src/components/common/ErrorField.js b/admin-panel/src/components/common/ErrorField.js index e9b16d5..50fe711 100644 --- a/admin-panel/src/components/common/ErrorField.js +++ b/admin-panel/src/components/common/ErrorField.js @@ -1,20 +1,15 @@ -import React, { Component } from 'react' - -class ErrorField extends Component { - static propTypes = { - - }; - - render() { - const {input, meta: { error, touched }, type} = this.props - const errorMessage = error && touched &&

{error}

- return ( -
- - {errorMessage} -
- ) - } +import React from 'react' + +function ErrorField(props) { + const {label, input, type, meta: {error, touched}} = props + const errorText = touched && error &&
{error}
+ return ( +
+ + + {errorText} +
+ ) } export default ErrorField \ No newline at end of file diff --git a/admin-panel/src/components/common/Loader.js b/admin-panel/src/components/common/Loader.js new file mode 100644 index 0000000..9418787 --- /dev/null +++ b/admin-panel/src/components/common/Loader.js @@ -0,0 +1,12 @@ +import React from 'react' + +function Loader() { + return ( +

Loading...

+ ) +} + +Loader.propTypes = { +} + +export default Loader \ No newline at end of file diff --git a/admin-panel/src/components/people/NewPersonForm.js b/admin-panel/src/components/people/NewPersonForm.js new file mode 100644 index 0000000..6707eb7 --- /dev/null +++ b/admin-panel/src/components/people/NewPersonForm.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react' +import {reduxForm, Field} from 'redux-form' +import validateEmail from 'email-validator' +import ErrorField from '../common/ErrorField' + +class NewPersonForm extends Component { + static propTypes = { + + }; + + render() { + return ( +
+
+ + + +
+ +
+ +
+ ) + } +} + +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' + + return errors +} + +export default reduxForm({ + form: 'person', + validate +})(NewPersonForm) \ No newline at end of file diff --git a/admin-panel/src/components/routes/PersonPage.js b/admin-panel/src/components/routes/PersonPage.js new file mode 100644 index 0000000..fd69263 --- /dev/null +++ b/admin-panel/src/components/routes/PersonPage.js @@ -0,0 +1,21 @@ +import React, { Component } from 'react' +import {connect} from 'react-redux' +import {addPerson} from '../../ducks/people' +import NewPersonForm from '../people/NewPersonForm' + +class PersonPage extends Component { + static propTypes = { + + }; + + render() { + return ( +
+

Add new person

+ +
+ ) + } +} + +export default connect(null, {addPerson})(PersonPage) \ No newline at end of file diff --git a/admin-panel/src/ducks/auth.js b/admin-panel/src/ducks/auth.js index f031871..af36e2a 100644 --- a/admin-panel/src/ducks/auth.js +++ b/admin-panel/src/ducks/auth.js @@ -1,4 +1,5 @@ import {appName} from '../config' +import {createSelector} from 'reselect' import {Record} from 'immutable' import firebase from 'firebase' @@ -10,8 +11,10 @@ const prefix = `${appName}/${moduleName}` export const SIGN_IN_START = `${prefix}/SIGN_IN_START` export const SIGN_IN_SUCCESS = `${prefix}/SIGN_IN_SUCCESS` +export const SIGN_IN_ERROR = `${prefix}/SIGN_IN_ERROR` export const SIGN_UP_START = `${prefix}/SIGN_UP_START` export const SIGN_UP_SUCCESS = `${prefix}/SIGN_UP_SUCCESS` +export const SIGN_UP_ERROR = `${prefix}/SIGN_UP_ERROR` /** * Reducer @@ -28,13 +31,22 @@ export default function reducer(state = new ReducerRecord(), action) { switch (type) { case SIGN_IN_START: case SIGN_UP_START: - return state.set('loading', true) + return state + .set('error', null) + .set('loading', true) case SIGN_IN_SUCCESS: case SIGN_UP_SUCCESS: return state .set('loading', false) .set('user', payload.user) + + case SIGN_IN_ERROR: + case SIGN_UP_ERROR: + return state + .set('loading', false) + .set('error', payload.error.message) + default: return state } @@ -44,7 +56,10 @@ export default function reducer(state = new ReducerRecord(), action) { * Selectors * */ -export const userSelector = state => state[moduleName].user +export const stateSelector = state => state[moduleName] +export const userSelector = createSelector(stateSelector, state => state.user) +export const errorSelector = createSelector(stateSelector, state => state.error) +export const loadingSelector = createSelector(stateSelector, state => state.loading) /** * Action Creators @@ -61,6 +76,10 @@ export function signIn(email, password) { type: SIGN_IN_SUCCESS, payload: { user } })) + .catch(error => dispatch({ + type: SIGN_IN_ERROR, + payload: { error } + })) } } @@ -75,6 +94,10 @@ export function signUp(email, password) { type: SIGN_UP_SUCCESS, payload: { user } })) + .catch(error => dispatch({ + type: SIGN_UP_ERROR, + payload: { error } + })) } } diff --git a/admin-panel/src/ducks/people.js b/admin-panel/src/ducks/people.js new file mode 100644 index 0000000..e952557 --- /dev/null +++ b/admin-panel/src/ducks/people.js @@ -0,0 +1,53 @@ +import {appName} from '../config' +import {Record, List} from 'immutable' + +/** + * Constants + * */ +export const moduleName = 'people' +const prefix = `${appName}/${moduleName}` +export const ADD_PERSON = `${prefix}/ADD_PERSON` + +/** + * Reducer + * */ +const ReducerState = Record({ + entities: new List([]) +}) + +const PersonRecord = Record({ + id: null, + firstName: null, + lastName: null, + email: null +}) + +export default function reducer(state = new ReducerState(), action) { + const {type, payload} = action + + switch (type) { + case ADD_PERSON: + return state.update('entities', entities => entities.push(new PersonRecord(payload.person))) + + default: + return state + } +} +/** + * Selectors + * */ + +/** + * Action Creators + * */ + +export function addPerson(person) { + return (dispatch) => { + dispatch({ + type: ADD_PERSON, + payload: { + person: {id: Date.now(), ...person} + } + }) + } +} \ No newline at end of file diff --git a/admin-panel/src/mocks/conferences.js b/admin-panel/src/mocks/conferences.js new file mode 100644 index 0000000..a5bea14 --- /dev/null +++ b/admin-panel/src/mocks/conferences.js @@ -0,0 +1,2384 @@ +export default [ + { + "title": "Agent Conf", + "url": "http://www.agent.sh/", + "where": "Dornbirn, Austria", + "when": "January 20-21, 2017", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "O'Reilly Velocity Conference", + "url": "http://conferences.oreilly.com/velocity/vl-ca", + "where": "San Jose, CA", + "when": "January 19-22, 2017", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "Script17", + "url": "https://scriptconf.org/", + "where": "Linz, Austria", + "when": "January 27, 2017", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "Agile Content Conf", + "url": "https://2017.agilecontentconf.com/", + "where": "London, UK", + "when": "January 30-31, 2017", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "Jfokus", + "url": "http://www.jfokus.se/jfokus/", + "where": "Stockholm, Sweden", + "when": "February 6-8, 2017", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Webstock", + "url": "http://www.webstock.org.nz/17/", + "where": "Wellington, New Zealand", + "when": "February 13-17, 2017", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Sustainable UX", + "url": "http://sustainableux.com/", + "where": "Online", + "when": "February 16, 2017", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "The Rolling Scopes Conference", + "url": "https://2017.conf.rollingscopes.com/", + "where": "Minsk, Belarus", + "when": "February 18-19, 2017", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "The Lead Developer New York", + "url": "http://2017.theleaddeveloper-ny.com/", + "where": "New York City, US", + "when": "February 21, 2017", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Voxxed Days - Zurich", + "url": "https://voxxeddays.com/zurich/", + "where": "Zurich, Switzerland", + "when": "February 23, 2017", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "UX Riga", + "url": "http://www.uxriga.lv/", + "where": "Riga, Latvia", + "when": "February 23, 2017", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Typography Day 2017", + "url": "http://www.typoday.in/", + "where": "Moratuwa, Sri Lanka", + "when": "February 23, 2017", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Voxxed Days - Cern", + "url": "https://voxxeddays.com/cern/", + "where": "Geneva, Switzerland", + "when": "February 25, 2017", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Forward JS", + "url": "https://forwardjs.com/", + "where": "San Francisco, CA", + "when": "March 1st, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Voxxed Days - Bristol", + "url": "https://voxxeddays.com/bristol/", + "where": "Watershed, Bristol", + "when": "March 2nd, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "jDays", + "url": "http://www.jdays.se/", + "where": "Göteborg, Sweden", + "when": "March 7-8, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "A Day of REST", + "url": "https://adayofrest.hm/boston-2017/", + "where": "Boston, MA", + "when": "March 9th, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Voxxed Days - Bucharest", + "url": "https://voxxeddays.com/bucharest/", + "where": "Bucharest, Romania", + "when": "March 10th, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "SXSW", + "url": "https://www.sxsw.com", + "where": "Austin, Texas", + "when": "March 10-19, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "React Conf", + "url": "http://conf.reactjs.org/", + "where": "Santa Clara, CA", + "when": "March 13th-14th, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "SmashingConf Oxford 2017", + "url": "https://shop.smashingmagazine.com/products/smashingconf-oxford-2017", + "where": "Oxford, England", + "when": "March 14th–15th, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "JS Remote Conf", + "url": "https://devchat.tv/conferences/js-remote-conf-2017", + "where": "Online", + "when": "March 15th–16th, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "NG-NL", + "url": "http://ng-nl.org/", + "where": "Amsterdam, Netherlands", + "when": "March 16, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Voxxed Days - Vienna", + "url": "https://voxxeddays.com/vienna/", + "where": "Vienna, Austria", + "when": "March 16-17, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Wroc_love.rb - Wrocław", + "url": "http://www.wrocloverb.com/", + "where": "Wrocław, Poland", + "when": "March 17th-19th, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "JazzCon.Tech", + "url": "http://www.jazzcon.tech/", + "where": "New Orleans, USA", + "when": "March 20th-22nd, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "JSUnconf", + "url": "http://2017.jsunconf.eu/", + "where": "Hamburg, Germany", + "when": "March 25th-26th, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "React London", + "url": "https://react.london/", + "where": "London, UK", + "when": "March 28, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Ember Conf", + "url": "http://emberconf.com/", + "where": "Portland, OR", + "when": "March 28-29, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Render Conf 2017", + "url": "http://2017.render-conf.com/", + "where": "Oxford, England", + "when": "March 30-31, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Design it; Build it", + "url": "http://www.dibiconference.com", + "where": "Edinburgh, Scotland", + "when": "March 30-31, 2017", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "AlterConf", + "url": "https://www.alterconf.com/conferences/london-england", + "where": "London, UK", + "when": "April 1, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "An Event Apart", + "url": "https://aneventapart.com/event/seattle-2017", + "where": "Seattle, WA", + "when": "April 3-5, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "26th International World Wide Web Conference, 2017", + "url": "http://www.www2017.com.au/", + "where": "Perth, Australia", + "when": "April 3-7, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "SmashingConf San Francisco 2017", + "url": "https://shop.smashingmagazine.com/products/smashingconf-san-francisco-2017", + "where": "San Francisco, United States", + "when": "April 4th–5th, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "ng-conf 2017", + "url": "https://www.ng-conf.org/", + "where": "Salt Lake City, Utah", + "when": "April 5-7, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Bulgaria Web Summit 2017", + "url": "https://bulgariawebsummit.com", + "where": "Sofia, Bulgaria", + "when": "April 7th–8th, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "ForwardJS: Ottawa", + "url": "https://forwardjs.com/ottawa", + "where": "Ottawa, Ontario Canada", + "when": "April 6th–8th, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Front End Design Conference", + "url": "https://frontenddesignconference.com/", + "where": "St. Petersburg, FL", + "when": "April 19-21, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "React Amsterdam", + "url": "http://react-amsterdam.com/", + "where": "Amsterdam, NL", + "when": "April 21, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "FITC Toronto", + "url": "http://fitc.ca/event/to17/", + "where": "Toronto, Canada", + "when": "April 23-25, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Open Vis Conf", + "url": "https://openvisconf.com/", + "where": "Boston, MA", + "when": "April 24-25, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Serverlessconf", + "url": "https://austin.serverlessconf.io/", + "where": "Austin, TX", + "when": "April 26-28, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Generate", + "url": "https://www.generateconf.com/new-york-2017/", + "where": "New York City, NY", + "when": "April 27-28, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "CSSConf EU", + "url": "http://2017.cssconf.eu", + "where": "Arena Berlin, Germany", + "when": "May 5, 2017", + "month": "May", + "submissionDeadline": "Jan 1st, 2017" + }, + { + "title": "Voxxed Days - Ticino", + "url": "https://voxxeddays.com/ticino/", + "where": "Palazzo dei Congressi, Lugano", + "when": "May 6, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "JSConf EU", + "url": "http://2017.jsconf.eu", + "where": "Arena Berlin, Germany", + "when": "May 6-7, 2017", + "month": "May", + "submissionDeadline": "Jan 1st, 2017" + }, + { + "title": "OSCON", + "url": "http://conferences.oreilly.com/oscon/oscon-tx", + "where": "Austin, TX", + "when": "May 8-11, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Front", + "url": "https://www.frontutah.com/", + "where": "Salt Lake City, Utah", + "when": "May 9-10, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Angular Summit", + "url": "https://angularsummit.com/conference/chicago/2017/05/home", + "where": "Chicago, IL", + "when": "May 9-11, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Riga Dev Days", + "url": "http://rigadevdays.lv/", + "where": "Riga, Latvia", + "when": "May 15th–17th, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "An Event Apart", + "url": "https://aneventapart.com/event/boston-2017", + "where": "Boston, MA", + "when": "May 15th–17th, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Beyond Tellerrand", + "url": "http://beyondtellerrand.com/", + "where": "Dusseldorf, Germany", + "when": "May 15th–17th, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Voxxed Days - Athens", + "url": "https://voxxeddays.com/athens/", + "where": "Athens, Greece", + "when": "May 18-20, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Syntax", + "url": "https://2017.syntaxcon.com/", + "where": "North Charleston", + "when": "May 18-19, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "React Europe", + "url": "https://www.react-europe.org/", + "where": "Paris, France", + "when": "May 18-19, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "DEVit Conference", + "url": "http://devitconf.org/", + "where": "Thessaloniki, Greece", + "when": "May 20-21, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "GraphQL-Europe", + "url": "https://graphql-europe.org/", + "where": "Berlin, Germany", + "when": "May 21st, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "User Experience Lisbon", + "url": "https://www.ux-lx.com/", + "where": "Lisbon, Portugal", + "when": "May 23-26, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Front-Trends", + "url": "https://2017.front-trends.com/", + "where": "Warsaw, Poland", + "when": "May 24-26, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "UX London", + "url": "http://2017.uxlondon.com/", + "where": "London, UK", + "when": "May 24-26, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Frontend United", + "url": "http://frontendunited.org/", + "where": "Athens, Greece", + "when": "May 26-27, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "NG-Cruise", + "url": "https://ngcruise.com/#/", + "where": "Miami, Florida", + "when": "May 29 - June 2, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "RevolutionConf", + "url": "https://www.revolutionconf.com", + "where": "Virginia Beach, VA", + "when": "June 1-2, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Voxxed Days - Singapore", + "url": "https://voxxeddays.com/singapore/", + "where": "Marina Bay Sands, Singapore", + "when": "June 2, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Kerning", + "url": "http://2017.kerning.it", + "where": "Faenza, Italy", + "when": "June 7-9, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "UX Scotland", + "url": "http://uxscotland.net/2017/", + "where": "Edinburgh, UK", + "when": "June 7-9, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Webconf.asia", + "url": "https://webconf.asia/", + "where": "Hong Kong", + "when": "June 3, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Elm Europe", + "url": "https://elmeurope.org/", + "where": "Paris, France", + "when": "June 8-9, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Generate", + "url": "https://www.generateconf.com/", + "where": "San Francisco, CA", + "when": "June 9, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "ReactJS Day", + "url": "https://www.reactjsday.it", + "where": "Verona, Italy", + "when": "June 10, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "SmashingConf", + "url": "http://lanyrd.com/2017/smashingconf-new-york/", + "where": "New York City, NY", + "when": "June 13-14, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "ConvergeSE", + "url": "http://convergese.com/", + "where": "Columbia, SC", + "when": "June 14-16, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "DinosaurJS", + "url": "http://dinosaurjs.org", + "where": "Denver, CO", + "when": "June 15, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "O'Reilly Fluent", + "url": "http://conferences.oreilly.com/fluent/fl-ca", + "where": "San Jose, CA", + "when": "June 19-22, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "An Event Apart", + "url": "https://aneventapart.com/event/washington-dc-2017", + "where": "Washington, DC", + "when": "July 10-12, 2017", + "month": "July", + "submissionDeadline": "" + }, + { + "title": "Design & Content Conference", + "url": "http://www.designcontentconf.com/", + "where": "Vancouver, BC Canada", + "when": "July 17-19, 2017", + "month": "July", + "submissionDeadline": "" + }, + { + "title": "React Rally", + "url": "http://www.reactrally.com/", + "where": "Salt Lake City, UT", + "when": "August 24-25, 2017", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "BrazilJS", + "url": "https://braziljs.org/conf/", + "where": "Porto Alegre, Brazil", + "when": "August 25-26, 2017", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "An Event Apart", + "url": "https://aneventapart.com/event/chicago-2017", + "where": "Chicago, IL", + "when": "August 28-30, 2017", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "Frontend Conference Zurich", + "url": "https://frontendconf.ch/", + "where": "Zurich, Switzerland", + "when": "August 31 - September 1, 2017", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "Nginx Conf", + "url": "https://www.nginx.com/nginxconf/", + "where": "Portland, Oregon", + "when": "September 6-8, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "React Native EU", + "url": "https://react-native.eu/", + "where": "Wroclaw, Poland", + "when": "September 6-7, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "NordicJS", + "url": "http://nordicjs.com", + "where": "Stockholm, Sweden", + "when": "September 7-8, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "Refresh", + "url": "http://refresh.rocks/", + "where": "Tallinn, Estonia", + "when": "September 8, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "FrontTalks", + "url": "http://fronttalks.ru/", + "where": "Ekaterinburg, Russia", + "when": "September 16-17, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "SmashingConf Freiburg 2017", + "url": "https://shop.smashingmagazine.com/products/smashingconf-freiburg-2017", + "where": "Freiburg, Germany", + "when": "September 11th–12th, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "Generate", + "url": "https://www.generateconf.com/", + "where": "London, UK", + "when": "September 20–22, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "React Boston", + "url": "http://www.reactboston.com/", + "where": "Boston, MA", + "when": "September 23–24, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "Web Unleashed 2017", + "url": "http://fitc.ca/event/webu17/", + "where": "Toronto, Ontario Canada", + "when": "September 25th–26th, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "Node.js Interactive North America 2017", + "url": "http://events.linuxfoundation.org/events/node-interactive", + "where": "Vancouver, BC Canada", + "when": "October 4th–6th, 2017", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "GitHub Universe", + "url": "https://githubuniverse.com/", + "where": "San Francisco, California", + "when": "October 10-12, 2017", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "SmashingConf Barcelona 2017", + "url": "https://shop.smashingmagazine.com/products/smashingconf-barcelona-2017", + "where": "Barcelona, Spain", + "when": "October 17th–18th, 2017", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "Empire JS", + "url": "http://2017.empirejs.org/", + "where": "New York City, NY", + "when": "October 22, 2017", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "Full Stack TO", + "url": "http://fsto.co/", + "where": "Toronto, Ontario Canada", + "when": "October 23-24, 2017", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "Revolve Conference", + "url": "https://2017.revolveconference.com/", + "where": "Charleston, SC", + "when": "October 25-27, 2017", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "An Event Apart", + "url": "https://aneventapart.com/event/san-francisco-2017", + "where": "San Francisco, CA", + "when": "October 30- November 1, 2017", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "Beyond Tellerrand", + "url": "http://beyondtellerrand.com/", + "where": "Berlin, Germany", + "when": "November 6–8, 2017", + "month": "November", + "submissionDeadline": "" + }, + { + "title": "Angular Connect", + "url": "http://angularconnect.com/", + "where": "Excel, London", + "when": "November 7-8, 2017", + "month": "November", + "submissionDeadline": "" + }, + { + "title": "An Event Apart", + "url": "https://aneventapart.com/event/denver-2017", + "where": "Denver, CO", + "when": "December 11-13, 2017", + "month": "December", + "submissionDeadline": "" + }, + { + "title": "WordCamp Europe 2017", + "url": "https://2017.europe.wordcamp.org/", + "where": "Paris, France", + "when": "June 15-17, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Web Rebels", + "url": "https://webrebels.org/", + "where": "Oslo, Norway", + "when": "June 01-02, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "WeAreDevelopers", + "url": "http://www.wearedevelopers.org/", + "where": "Vienna, Austria", + "when": "May 11-12, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "CodeFest", + "url": "https://2017.codefest.ru", + "where": "Novosibirsk, Russia", + "when": "April 1-2, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "DUMP", + "url": "http://dump-conf.ru/", + "where": "Ekaterinburg, Russia", + "when": "April 14, 2017", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "JS Day Italy", + "url": "https://2017.jsday.it/", + "where": "Verona, Italy", + "when": "May 10-11, 2017", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Holy JS", + "url": "https://holyjs-piter.ru/", + "where": "Saint-Petersburg, Russia", + "when": "June 2-3, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Frontend Conf", + "url": "http://frontendconf.ru/", + "where": "Moscow, Russia", + "when": "June 5-6, 2017", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "NodeConf Argentina", + "url": "https://2017.nodeconf.com.ar/", + "where": "Buenos Aires, Argentina", + "when": "October 26-28, 2017", + "month": "October", + "submissionDeadline": "July 15, 2017" + }, + { + "title": "Full Stack Fest", + "url": "https://fullstackfest.barcelona", + "where": "Barcelona, Spain", + "when": "September 4-8, 2017", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "JS Remote Conf", + "url": "http://jsremoteconf.com/", + "where": "Online", + "when": "January 14–16, 2016", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "O’Reilly Design Conference", + "url": "http://oreil.ly/1NRw8kd", + "where": "San Francisco, CA, USA", + "when": "January 19–22, 2016", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "SVG Summit 2016", + "url": "http://environmentsforhumans.com/2016/svg-summit/", + "where": "Online", + "when": "January 21, 2016", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "Awwwards Conference – Amsterdam 2016", + "url": "http://conference.awwwards.com/amsterdam-2016/", + "where": "Amsterdam, Netherlands", + "when": "January 27–29, 2016", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "A Day of REST", + "url": "http://feelingrestful.com/", + "where": "London, UK", + "when": "January 28, 2016", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "VOXXED Days Berlin 2016", + "url": "https://voxxeddays.com/berlin16/", + "where": "Berlin, Germany", + "when": "January 28–29, 2016", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "PhoneGap Day US 2016", + "url": "http://pgday.phonegap.com/", + "where": "Lehi, UT, USA", + "when": "January 28–29, 2016", + "month": "January", + "submissionDeadline": "" + }, + { + "title": "Agile Content Conf 2016", + "url": "https://2016.agilecontentconf.com/", + "where": "London, UK", + "when": "February 1–2, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "JFokus 2016", + "url": "http://www.jfokus.se/jfokus/", + "where": "Stockholm, Sweden", + "when": "February 8–10, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "SustainableUX", + "url": "http://sustainableux.com/", + "where": "Online", + "when": "February 9, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "ConveyUX 2016", + "url": "http://conveyux.com/", + "where": "Seattle, WA, USA", + "when": "February 9–11, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Webstock 2016", + "url": "http://www.webstock.org.nz/16/", + "where": "Wellington, New Zealand", + "when": "February 9–12, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "DevNexus 2016", + "url": "http://www.devnexus.com/", + "where": "Atlanta, GA, USA", + "when": "February 15–17, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Open Set Soul Edition 2016", + "url": "http://www.openset.nl/oskorea/opensetdutchdesignseoulsessions_en.html", + "where": "Seoul, South Korea", + "when": "February 15–27, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Mobile Growth Summit 2016", + "url": "http://bit.ly/MobileGrowthSummit16", + "where": "San Francisco, CA, USA", + "when": "February 17–18, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "World IA Day 2016", + "url": "http://www.2016.worldiaday.org/", + "where": "At locations on every continent", + "when": "February 20, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Bulgaria Web Summit 2016", + "url": "http://bulgariawebsummit.com/", + "where": "Sofia, Bulgaria", + "when": "February 20, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "The Rolling Scopes Conference", + "url": "http://2016.conf.rollingscopes.com/", + "where": "Minsk, Belarus", + "when": "February 20–21, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "React.js Conf", + "url": "http://conf.reactjs.com/", + "where": "San Francisco, CA, USA", + "when": "February 22–23, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "UX Riga 2016", + "url": "http://www.uxriga.lv/", + "where": "Riga, Latvia", + "when": "February 25, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Typoday 2016", + "url": "http://www.typoday.in/", + "where": "Bangalore, India", + "when": "February 25–27, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Planning for a Higher Ed Website Redesign", + "url": "http://www.academicimpressions.com/conference/planning-higher-ed-website-redesign", + "where": "Cincinnati, OH, USA", + "when": "February 29–March 2, 2016", + "month": "February", + "submissionDeadline": "" + }, + { + "title": "Interaction16", + "url": "http://interaction16.ixda.org/", + "where": "Helsinki, Finland", + "when": "March 1–4, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Riga Dev Day 2016", + "url": "http://www.rigadevday.lv/", + "where": "Riga, Latvia", + "when": "March 2–4, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Voxxed Days Zurich", + "url": "https://voxxeddays.com/zurich16/", + "where": "Zurich, Switzerland", + "when": "March 3, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "MIDWEST PHP 2016", + "url": "http://2016.midwestphp.org/", + "where": "Minneapolis, MN, USA", + "when": "March 4–5, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "QCon London 2016", + "url": "http://qconlondon.com/", + "where": "London, UK", + "when": "March 7–11, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "O’Reilly Fluent Conference 2016", + "url": "http://conferences.oreilly.com/fluent/javascript-html-us", + "where": "San Francisco, CA", + "when": "March 8–10, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "UXHK 2016", + "url": "http://www.uxhongkong.com/", + "where": "Hong Kong, China", + "when": "March 11–12, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "SXSW 2016", + "url": "http://www.sxsw.com/", + "where": "Austin, TX, USA", + "when": "March 11–20, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Dev Bootcamp San Francisco March Info Session", + "url": "https://www.eventbrite.com/…san-francisco-march-info-session-tickets-22132155874?ref=ebapi", + "where": "United States,San Francisco", + "when": "March 14th, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "MobileTech Conference Spring 2016", + "url": "https://mobiletechcon.de/", + "where": "Munich, Germany", + "when": "March 14–17, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Ruby for Beginners", + "url": "https://www.eventbrite.com/e/ruby-for-beginners-tickets-21643963678?ref=ebapi", + "where": "United States,San Francisco", + "when": "March 15th, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "SmashingConf Oxford 2016", + "url": "http://www.smashingconf.com/", + "where": "Oxford, UK", + "when": "March 15–16, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "DIBI 2016", + "url": "http://dibiconference.com/", + "where": "Edinburgh, UK", + "when": "March 17–18, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Codemotion Roma 2016", + "url": "http://rome2016.codemotionworld.com/", + "where": "ItalyItaly,Rome", + "when": "March 18th–19th, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Phase 0 Virtual Information Session", + "url": "https://www.eventbrite.com/e/phase-0-virtual-information-session-tickets-22308315773", + "where": "Online conference", + "when": "March 19th, 2016", + "month": "March" + }, + { + "title": "Ruby Remote Conf", + "url": "https://allremoteconfs.com/ruby-2016", + "where": "Online", + "when": "March 23-25, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "EmberConf 2016", + "url": "http://emberconf.com/", + "where": "Portland, OR, USA", + "when": "March 28–30, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "QCon SP 2016", + "url": "http://qconsp.com/", + "where": "São Paolo, Brazil", + "when": "March 28–April 1, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "UX in the City: Oxford 2016", + "url": "http://uxinthecity.net/2016/oxford/", + "where": "Oxford, UK", + "when": "March 31–April 1, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Workshop: Building JavaScript Applications with ReactJS", + "url": "http://www.whiteoctoberevents.co.uk/event/reactjs-workshop-march-16/", + "where": "England,London", + "when": "March 21st, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Dev Bootcamp March Virtual Info Session", + "url": "https://www.eventbrite.com/e/dev-bootcamp-march-virtual-info-session-tickets-22477541933", + "where": "Online conference", + "when": "March 24th, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Meet the TAG - London", + "url": "", + "where": "England,London", + "when": "March 29th, 2016, 6pm", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "RWD Summit 2016", + "url": "http://environmentsforhumans.com/2016/responsive-web-design-summit/", + "where": "Online conference", + "when": "March 29th–31st, 2016", + "month": "March", + "submissionDeadline": "" + }, + { + "title": "Fronteers Spring Thing 2016", + "url": "https://fronteers.nl/spring", + "where": "Amsterdam, Netherlands", + "when": "April 1, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Space City JS 2016", + "url": "http://spacecity.codes/", + "where": "Houston, TX, USA", + "when": "April 2, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "MobCon Europe 2016", + "url": "http://mobcon.com/mobcon-europe/", + "where": "Sofia, Bulgaria", + "when": "April 4, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "An Event Apart 2016: Seattle", + "url": "http://aneventapart.com/event/seattle-2016", + "where": "Seattle, WA, USA", + "when": "April 4–6, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "SmashingConf San Francisco 2016", + "url": "http://smashingconf.com/sf-2016/", + "where": "San Francisco, CA, USA", + "when": "April 5–6, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Le Web a Quebec 2016 — 6e edition", + "url": "http://www.webaquebec.org/", + "where": "Quebec, Canada", + "when": "April 6–8, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "WebVisions New York City 2016", + "url": "http://www.webvisionsevent.com/", + "where": "New York City, NY, USA", + "when": "April 7, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Respond 2016", + "url": "http://www.webdirections.org/respond16/", + "where": "Melbourne, Australia", + "when": "April 11-12, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Lone Star PHP 2016", + "url": "http://lonestarphp.com/", + "where": "Dallas, TX, USA", + "when": "April 7–9, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Jazoon Techdays Spring 2016", + "url": "http://jazoon.com/", + "where": "Berne, Switzerland", + "when": "April 8, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Yggdrasil 2016", + "url": "http://yggdrasilkonferansen.no/", + "where": "Sandefjord, Norway", + "when": "April 11–12, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Now What? Conference 2016", + "url": "http://www.nowwhatconference.com/", + "where": "Sioux Falls, SD, USA", + "when": "April 13–14, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Peers Conference 2016", + "url": "http://peersconf.com/", + "where": "St. Petersburg, FL, USA", + "when": "April 13–15, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "ConvergeSE 2016", + "url": "http://convergese.com/", + "where": "Columbia, SC, USA", + "when": "April 13–15, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "ACE! 2016", + "url": "http://aceconf.com/", + "where": "Krakow, Poland", + "when": "April 14–15, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "From Business to Buttons", + "url": "http://frombusinesstobuttons.com/", + "where": "Stockholm, Sweden", + "when": "April 14–16, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "JSConf 2016", + "url": "https://jsconf.uy/", + "where": "Montevideo, Uruguay", + "when": "April 15–16, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "JavaScript Frameworks Day 2016", + "url": "http://frameworksdays.com/event/js-frameworks-day-2016", + "where": "Kiev, Ukraine", + "when": "April 16, 2016", + "month": "April" + }, + { + "title": "Ancient City Ruby 2016", + "url": "", + "where": "United States,St. Augustine", + "when": "April 6th–8th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Lone Star PHP 2016", + "url": "", + "where": "United States,Addison", + "when": "April 7th–9th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Building a Real-time App with React and Firebase", + "url": "", + "where": "Online conference", + "when": "April 7th, 2016, 9am", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "CoderDojo Nürnberg #1", + "url": "", + "where": "Germany,Nuremberg", + "when": "April 10th 2016, 4pm", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Nürnberg Web Week 2016", + "url": "", + "where": "Germany,Nuremberg", + "when": "April 11th–18th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Accessibility Club #3", + "url": "http://accessibility-club.org/", + "where": "Germany,Nürnberg", + "when": "April 12th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Digital Croydon #8 Laura Elizabeth, Jason Bootle, Amy Whitney", + "url": "", + "where": "England,London Borough of Croydon", + "when": "April 14th, 2016, 6:30pm", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Using External Plugins with React", + "url": "http://www.oreilly.com/online-training/react-for-web-developers.html", + "where": "Online conference", + "when": "April 14th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Hexagon Geospatial IGNITE Session - Atlanta", + "url": "https://www.eventbrite.com/e/hexagon-geospatial-ignite-session-atlanta-tickets-22668441920?ref=ebapi", + "where": "United States,Norcross", + "when": "April 15th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "2016 DC-Baltimore Perl Workshop", + "url": "http://dcbpw.org/dcbpw2016/", + "where": "United States,Balitmore", + "when": "April 16th–17th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Workshop: Rapid API development with Node.js and LoopBack", + "url": "", + "where": "England,Oxford", + "when": "April 20th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Industry Conf", + "url": "https://industryconf.com/", + "where": "Newcastle upon Tyne, England", + "when": "April 20, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "MCE3", + "url": "http://mceconf.com/", + "where": "Warsaw, Poland", + "when": "April 21–22, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Render Conf", + "url": "http://www.render-conf.com/", + "where": "Oxford, England", + "when": "April 21-22, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Generate NYC 2016", + "url": "http://www.generateconf.com/new-york-2016", + "where": "New York City, NY, USA", + "when": "April 22, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "RSJS", + "url": "http://rsjs.org/2016/", + "where": "Porto Alegre, Brazil", + "when": "April 23, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "JSUnconf", + "url": "http://2016.jsunconf.eu/", + "where": "Hamburg, Germany", + "when": "April 23-24, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "JET Conference", + "url": "", + "where": "Belarus,Minsk", + "when": "April 25th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Future of Web Design London", + "url": "https://futureofwebdesign.com/london-2016/", + "where": "London, England", + "when": "April 25-27, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Open Vis Conf", + "url": "https://openvisconf.com/", + "where": "Boston, MA, USA", + "when": "April 25–26, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "M2M WORLD CONGRESS 2016", + "url": "http://www.m2mconference.com/", + "where": "London, UK", + "when": "April 26–27, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Squares Conference", + "url": "http://squaresconference.com/", + "where": "Dallas (Grapevine), Texas USA", + "when": "April 27-29, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Workshop: Angular 2", + "url": "", + "where": "England,London", + "when": "April 29th, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "JSDayES 2016", + "url": "http://jsday.es/", + "where": "Madrid, Spain", + "when": "April 29-30, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Front in Rio – HTML Edition", + "url": "http://frontinrio.com.br/", + "where": "Rio de Janeiro, Brazil", + "when": "April 30, 2016", + "month": "April", + "submissionDeadline": "" + }, + { + "title": "Future Insights Live", + "url": "https://futureinsightslive.com/", + "where": "Chicago, Illinois USA", + "when": "May 2-5, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "ng-conf", + "url": "http://www.ng-conf.org/", + "where": "Salt Lake City, Utah USA", + "when": "May 4-6, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "IA Summit 2016", + "url": "http://2016.iasummit.org/", + "where": "Atlanta, GA, USA", + "when": "May 4–8, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Syntax Convention", + "url": "http://syntaxcon.com/", + "where": "Charleston, South Carolina USA", + "when": "May 6-7, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Beyond Tellerrand", + "url": "http://beyondtellerrand.com/", + "where": "Düsseldorf, Germany", + "when": "May 9-11, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "DRUPALCON", + "url": "https://events.drupal.org/neworleans2016", + "where": "New Orleans, LA, USA", + "when": "May 9–13, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "CSSConf Budapest", + "url": "http://cssconfbp.rocks/", + "where": "Budapest, Hungary", + "when": "May 11, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "jsday", + "url": "http://2016.jsday.it/", + "where": "Verona, Italy", + "when": "May 11-12, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "UX Alive", + "url": "http://www.uxalive.com/", + "where": "Istanbul, Turkey", + "when": "May 11-13, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "JsDay", + "url": "http://2016.jsday.it/", + "where": "Verona, Italy", + "when": "May 11–12, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "UX Alive", + "url": "http://www.uxalive.com/", + "where": "Istanbul, Turkey", + "when": "May 11–13, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "JSConf Budapest", + "url": "http://jsconfbp.com/", + "where": "Budapest, Hungary", + "when": "May 12-13, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Create Upstate", + "url": "http://createupstate.com/", + "where": "Syracuse, NY, USA", + "when": "May 12–13, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Front", + "url": "http://www.frontutah.com/", + "where": "Salt Lake City, UT, USA", + "when": "May 12–13, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "JSConf Budapest", + "url": "http://jsconfbp.com/", + "where": "Budapest, Hungary", + "when": "May 12–13, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "phpDay", + "url": "http://2016.phpday.it/", + "where": "Verona, Italy", + "when": "May 13–14, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "An Event Apart Boston", + "url": "http://aneventapart.com/event/boston-2016", + "where": "Boston, Massachusetts USA", + "when": "May 16-18, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "SDD Conf", + "url": "http://www.sddconf.com/", + "where": "London, UK", + "when": "May 16–20, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "OSCON", + "url": "http://conferences.oreilly.com/oscon/open-source-us", + "where": "Austin, TX, USA", + "when": "May 18–19, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "UX London", + "url": "http://2016.uxlondon.com/", + "where": "London, UK", + "when": "May 18–20, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Front-Trends", + "url": "http://front-trends.com/", + "where": "Warsaw, Poland", + "when": "May 18–20, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "PhoneGap Day EU 2016", + "url": "http://pgday.phonegap.com/eu2016/", + "where": "Amsterdam, Netherlands", + "when": "May 19-20, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Port 80", + "url": "http://port80events.co.uk/", + "where": "Newport, UK", + "when": "May 20, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "JandBeyong - An international Joomla! Conference", + "url": "http://JandBeyong.org/", + "where": "Barcelona, Spain", + "when": "May 20-22, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "GOTO Chicago", + "url": "http://gotocon.com/chicago-2016", + "where": "Chicago, IL, USA", + "when": "May 23–26, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "User Experience Lisbon", + "url": "https://www.ux-lx.com/", + "where": "Lisbon, Portugal", + "when": "May 24-27, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "DevSum16", + "url": "http://www.devsum.se/", + "where": "Stockholm, Sweden", + "when": "May 25-27, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Empire JS", + "url": "http://empirejs.org/", + "where": "New York City, New York USA", + "when": "May 26-27, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Frontend United", + "url": "http://frontendunited.org/", + "where": "Ghent, Belgium", + "when": "May 27-28, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "UpFront 2016", + "url": "http://upfrontconf.com/", + "where": "Manchester, UK", + "when": "May 27, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Sud Web 2016", + "url": "http://sudweb.fr/", + "where": "Bordeaux, France", + "when": "May 27–28, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "Webinale 2016", + "url": "https://webinale.de/", + "where": "Berlin, Germany", + "when": "May 29–June 02, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "UXPA 2016", + "url": "http://www.uxpa2016.org/", + "where": "Seattle, WA, USA", + "when": "May 31–June 03, 2016", + "month": "May", + "submissionDeadline": "" + }, + { + "title": "CSSconf Nordic 2016", + "url": "http://cssconf.no/", + "where": "Oslo, Norway", + "when": "June 1, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "ScotlandCSS 2016", + "url": "http://scotlandcss.launchrock.com/", + "where": "Edinburgh, UK", + "when": "June 1, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Web Rebels 2016", + "url": "https://www.webrebels.org/", + "where": "Oslo, Norway", + "when": "June 2–3, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Soap! 2016", + "url": "http://soapconf.com/", + "where": "Krakow, Poland", + "when": "June 2–3, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "UXScotland 2016", + "url": "http://uxscotland.net/2016/", + "where": "Edinburgh, UK", + "when": "June 8–10, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "M-Enabling Summit 2016", + "url": "http://www.m-enabling.com/", + "where": "Washington, D.C., USA", + "when": "June 13–14, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "QCon New York 2016", + "url": "https://qconnewyork.com/", + "where": "New York City, NY, USA", + "when": "June 13–17, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Front End Design Conference", + "url": "http://frontenddesignconference.com/", + "where": "St. Petersburg, Florida USA", + "when": "June 15-17, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Droidcon Berlin 2016", + "url": "http://droidcon.de/", + "where": "Berlin, Germany", + "when": "June 15–17, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "DWX Developer Week 2016", + "url": "http://www.developer-week.de/", + "where": "Nuremberg, Germany", + "when": "June 20–23, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "KCDC", + "url": "http://www.kcdc.info/#!/", + "where": "Kansas City, Utah", + "when": "June 22-24, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Dinosaur.js", + "url": "http://dinosaurjs.org/", + "where": "Denver, Colorado USA", + "when": "June 24, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Web Design Day", + "url": "http://webdesignday.com", + "where": "Pittsburgh, Pennsylvania USA", + "when": "June 24, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Devoxx Poland 2016", + "url": "http://devoxx.pl/", + "where": "Krakow, Poland", + "when": "June 22–25, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "The Lead Developer 2016", + "url": "http://2016.theleaddeveloper.com/", + "where": "London, UK", + "when": "June 23–24, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Dutch PHP Conference 2016", + "url": "http://www.phpconference.nl/", + "where": "Amsterdam, Netherlands", + "when": "June 23–25, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Wicked Good Ember Conf", + "url": "https://wickedgoodember.com", + "where": "Boston, MA, USA", + "when": "June 27-28, 2016", + "month": "June", + "submissionDeadline": "May 13, 2016" + }, + { + "title": "CSSconf Nordic", + "url": "http://cssconf.no/", + "where": "Oslo, Norway", + "when": "June 1, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "Web Rebels", + "url": "https://www.webrebels.org/", + "where": "Oslo, Norway", + "when": "June 2-3, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "ReactEurope 2016", + "url": "https://www.react-europe.org/", + "where": "Paris, France", + "when": "June 2-3, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "ScotlandJS 2016", + "url": "http://scotlandjs.com/", + "where": "Edinburgh, Scotland", + "when": "June 2-3, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "JSCamp Romania", + "url": "http://www.jscamp.ro/", + "where": "Bucharest, Romania", + "when": "June 7, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "UX Scotland", + "url": "http://uxscotland.net/2016/", + "where": "Edinburgh, Scotland", + "when": "June 8, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "enterJS 2016", + "url": "https://www.enterjs.de/", + "where": "Darmstadt, Germany", + "when": "June 14-16, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "CSS Day 2016", + "url": "http://cssday.nl/", + "where": "Amsterdam, The Netherlands", + "when": "June 16-17, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "YGLF", + "url": "http://yougottalovefrontend.com/#page-home", + "where": "Tel Aviv, Israel", + "when": "June 27-28, 2016", + "month": "June", + "submissionDeadline": "" + }, + { + "title": "FrontinSampa", + "url": "http://frontinsampa.com.br/", + "where": "São Paulo, Brazil", + "when": "July 02, 2016", + "month": "July", + "submissionDeadline": "" + }, + { + "title": "FullStack 2016", + "url": "https://skillsmatter.com/conferences/7278-fullstack", + "where": "England, London", + "when": "July 13–15, 2016", + "month": "July", + "submissionDeadline": "" + }, + { + "title": "Generate San Francisco 2016", + "url": "http://generateconf.com/", + "where": "San Francisco, California USA", + "when": "July 15, 2016", + "month": "July", + "submissionDeadline": "" + }, + { + "title": "An Event Apart Washington D.C.", + "url": "http://aneventapart.com/event/washington-dc-2016", + "where": "Washington, D.C. USA", + "when": "July 25-27, 2016", + "month": "July", + "submissionDeadline": "" + }, + { + "title": "ForwardJS 5", + "url": "http://forwardjs.com/", + "where": "San Francisco, California USA", + "when": "July 25-31, 2016", + "month": "July", + "submissionDeadline": "" + }, + { + "title": "CSS Summit 2016", + "url": "http://environmentsforhumans.com/2016/css-summit/", + "where": "online", + "when": "July 26-28, 2016", + "month": "July", + "submissionDeadline": "" + }, + { + "title": "Mobile & Web CodeCamp", + "url": "http://www.mobilewebcodecamp.com/", + "where": "Salt Lake City, Utah USA", + "when": "July 28-29, 2016", + "month": "July", + "submissionDeadline": "" + }, + { + "title": "NDC Sydney", + "url": "http://ndcsydney.com/", + "where": "Sydney, Australia", + "when": "August 1-5, 2016", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "CSSConf Argentina", + "url": "http://cssconfar.com", + "where": "Buenos Aires, Argentina", + "when": "August 7, 2016", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "JSConf Iceland", + "url": "http://2016.jsconf.is/", + "where": "Reykjavik, Iceland", + "when": "August 25-26, 2016", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "React Rally", + "url": "http://www.reactrally.com/", + "where": "Salt Lake City, Utah", + "when": "August 25-26, 2016", + "month": "August", + "submissionDeadline": "April 28, 2016" + }, + { + "title": "BrazilJS Conf", + "url": "https://braziljs.org/conf", + "where": "Porto Alegre, Brazil", + "when": "August 26-27, 2016", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "AltConf", + "url": "http://www.alterconf.com/sessions/cape-town-south-africa", + "where": "Cape Town, South Africa", + "when": "August 27, 2016", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "An Event Apart Chicago", + "url": "http://aneventapart.com/event/chicago-2016", + "where": "Chicago, Illinois USA", + "when": "August 29-31, 2016", + "month": "August", + "submissionDeadline": "" + }, + { + "title": "ColdFront 2016", + "url": "https://2016.coldfrontconf.com/", + "where": "Copenhagen, Denmark", + "when": "September 1, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "Frontend Conference Zurich", + "url": "https://frontendconf.ch/", + "where": "Zurich, Switzerland", + "when": "September 1-2, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "Generate Sydney 2016", + "url": "http://generateconf.com/", + "where": "Sydney Australia", + "when": "September 5, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "Full Stack Fest", + "url": "http://2016.fullstackfest.com/", + "where": "Barcelona, Spain", + "when": "September 5-9, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "SmashingConf Freiburg 2016", + "url": "http://smashingconf.com/", + "where": "Freiburg, Germany", + "when": "September 12-13, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "From the Front", + "url": "http://2016.fromthefront.it/", + "where": "Bologna, Italy", + "when": "September 15-16, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "Strange Loop", + "url": "http://www.thestrangeloop.com/", + "where": "St. Louis, USA", + "when": "September 15-17, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "SmartWeb Conference 2016", + "url": "http://www.smartwebconf.com/", + "where": "Bucharest, Romania", + "when": "September 20, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "Generate London 2016", + "url": "http://generateconf.com/", + "where": "London England", + "when": "September 21-23, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "AngularConnect 2016", + "url": "http://angularconnect.com/", + "where": "London, England", + "when": "September 27-28, 2016", + "month": "September", + "submissionDeadline": "" + }, + { + "title": "An Event Apart Orlando", + "url": "http://aneventapart.com/event/orlando-2016", + "where": "Orlando, Florida USA", + "when": "October 3-5, 2016", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "LoopConf", + "url": "https://loopconf.com/", + "where": "Fort Lauderdale, Florida USA", + "when": "October 5-7, 2016", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "Full Stack Toronto", + "url": "https://fsto.co/", + "where": "Toronto, Ontario Canada", + "when": "October 17-18, 2016", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "CSS Dev Conf", + "url": "http://2016.cssdevconf.com/", + "where": "San Antonio, Texas USA", + "when": "October 17-19, 2016", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "Connect.Tech", + "url": "http://connect-js.com/", + "where": "Atlanta, Georgia USA", + "when": "October 20-22, 2016", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "ng-europe 2016", + "url": "https://ngeurope.org/", + "where": "Paris, France", + "when": "October 25-26, 2016", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "An Event Apart San Francisco", + "url": "http://aneventapart.com/event/san-francisco-2016", + "where": "San Francisco, California USA", + "when": "October 31-November 2, 2016", + "month": "October", + "submissionDeadline": "" + }, + { + "title": "Thunder Plains", + "url": "http://thunderplainsconf.com/", + "where": "Oklahoma City, USA", + "when": "November 3-4, 2016", + "month": "November", + "submissionDeadline": "" + }, + { + "title": "SenchaCon 2016", + "url": "http://www.senchacon.com/", + "where": "Las Vegas, Nevada USA", + "when": "November 7-9, 2016", + "month": "November", + "submissionDeadline": "" + }, + { + "title": "Beyond Tellerrand", + "url": "http://beyondtellerrand.com/", + "where": "Berlin Germany", + "when": "November 7-9, 2016", + "month": "November", + "submissionDeadline": "" + }, + { + "title": "FrontierConf London 2016", + "url": "https://www.frontierconf.com/", + "where": "London, England", + "when": "November 16, 2016", + "month": "November", + "submissionDeadline": "" + }, + { + "title": "Generate Bangalore 2016", + "url": "http://www.generateconf.com/", + "where": "Bangalore, India", + "when": "November 25, 2016", + "month": "November", + "submissionDeadline": "" + }, + { + "title": "JS Kongress Munich", + "url": "http://js-kongress.de/", + "where": "Munich, Germany", + "when": "November 28-29, 2016", + "month": "November", + "submissionDeadline": "" + }, + { + "title": "CSSConf Australia", + "url": "http://2016.cssconf.com.au/", + "where": "Melbourne Australia", + "when": "November 30, 2016", + "month": "November", + "submissionDeadline": "" + }, + { + "title": "Decompress", + "url": "http://decompress.com.au/", + "where": "Melbourne, Australia", + "when": "December 2, 2016", + "month": "December", + "submissionDeadline": "" + }, + { + "title": "dotCSS", + "url": "http://www.dotcss.io/", + "where": "Paris, France", + "when": "December 2, 2016", + "month": "December", + "submissionDeadline": "" + }, + { + "title": "dotJS", + "url": "http://www.dotjs.io/", + "where": "Paris, France", + "when": "December 5, 2016", + "month": "December", + "submissionDeadline": "" + } +] \ No newline at end of file diff --git a/admin-panel/src/mocks/index.js b/admin-panel/src/mocks/index.js new file mode 100644 index 0000000..25fe7c5 --- /dev/null +++ b/admin-panel/src/mocks/index.js @@ -0,0 +1,7 @@ +import conferences from './conferences' +import firebase from 'firebase' + +export function saveEventsToFB() { + const eventsRef = firebase.database().ref('/events') + conferences.forEach(conference => eventsRef.push(conference)) +} \ No newline at end of file diff --git a/admin-panel/src/redux/reducer.js b/admin-panel/src/redux/reducer.js index 34143fe..cc6e0aa 100644 --- a/admin-panel/src/redux/reducer.js +++ b/admin-panel/src/redux/reducer.js @@ -2,8 +2,10 @@ import {combineReducers} from 'redux' 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' export default combineReducers({ router, form, - [authModule]: authReducer + [authModule]: authReducer, + [peopleModule]: peopleReducer }) \ No newline at end of file From c28ac67bb1f68d22433446a5896682f0dc4bdccc Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Mon, 11 Dec 2017 19:06:42 +0200 Subject: [PATCH 03/12] add simple saga --- admin-panel/package.json | 1 + admin-panel/src/components/auth/SignInForm.js | 7 ++-- admin-panel/src/ducks/people.js | 33 ++++++++++++++----- admin-panel/src/redux/index.js | 8 ++++- admin-panel/yarn.lock | 4 +++ 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/admin-panel/package.json b/admin-panel/package.json index 5fbeba9..1ada3cf 100644 --- a/admin-panel/package.json +++ b/admin-panel/package.json @@ -17,6 +17,7 @@ "redux": "^3.7.2", "redux-form": "^7.2.0", "redux-logger": "^3.0.6", + "redux-saga": "^0.16.0", "redux-thunk": "^2.2.0", "reselect": "^3.0.1" }, diff --git a/admin-panel/src/components/auth/SignInForm.js b/admin-panel/src/components/auth/SignInForm.js index da2b013..df7f35b 100644 --- a/admin-panel/src/components/auth/SignInForm.js +++ b/admin-panel/src/components/auth/SignInForm.js @@ -27,12 +27,9 @@ class SignInForm extends Component { } } -export default connect(state => { - console.log('---', 3333, errorSelector(state)) - return { +export default connect(state => ({ loading: loadingSelector(state), authError: errorSelector(state) - } -})(reduxForm({ +}))(reduxForm({ form: 'auth' })(SignInForm)) \ No newline at end of file diff --git a/admin-panel/src/ducks/people.js b/admin-panel/src/ducks/people.js index e952557..ddb584f 100644 --- a/admin-panel/src/ducks/people.js +++ b/admin-panel/src/ducks/people.js @@ -1,5 +1,6 @@ import {appName} from '../config' import {Record, List} from 'immutable' +import {put, takeEvery} from 'redux-saga/effects' /** * Constants @@ -7,6 +8,7 @@ import {Record, List} from 'immutable' export const moduleName = 'people' const prefix = `${appName}/${moduleName}` export const ADD_PERSON = `${prefix}/ADD_PERSON` +export const ADD_PERSON_SUCCESS = `${prefix}/ADD_PERSON_SUCCESS` /** * Reducer @@ -26,13 +28,14 @@ export default function reducer(state = new ReducerState(), action) { const {type, payload} = action switch (type) { - case ADD_PERSON: + case ADD_PERSON_SUCCESS: return state.update('entities', entities => entities.push(new PersonRecord(payload.person))) default: return state } } + /** * Selectors * */ @@ -42,12 +45,26 @@ export default function reducer(state = new ReducerState(), action) { * */ export function addPerson(person) { - return (dispatch) => { - dispatch({ - type: ADD_PERSON, - payload: { - person: {id: Date.now(), ...person} - } - }) + return { + type: ADD_PERSON, + payload: { person } } +} + +/** + * Sagas + */ + +export const addPersonSaga = function * (action) { + const { person } = action.payload + const id = Date.now() + + yield put({ + type: ADD_PERSON_SUCCESS, + payload: {id, ...person} + }) +} + +export const saga = function * () { + yield takeEvery(ADD_PERSON, addPersonSaga) } \ No newline at end of file diff --git a/admin-panel/src/redux/index.js b/admin-panel/src/redux/index.js index 1a37261..ff0e4c1 100644 --- a/admin-panel/src/redux/index.js +++ b/admin-panel/src/redux/index.js @@ -2,10 +2,16 @@ import {createStore, applyMiddleware} from 'redux' import logger from 'redux-logger' import {routerMiddleware} from 'react-router-redux' import thunk from 'redux-thunk' +import createSagaMiddleware from 'redux-saga' import reducer from './reducer' import history from '../history' +import {saga} from '../ducks/people' -const store = createStore(reducer, applyMiddleware(thunk, routerMiddleware(history), logger)) +const sagaMiddleware = createSagaMiddleware() + +const store = createStore(reducer, applyMiddleware(sagaMiddleware, thunk, routerMiddleware(history), logger)) + +sagaMiddleware.run(saga) //dev only window.store = store diff --git a/admin-panel/yarn.lock b/admin-panel/yarn.lock index c9c597c..5e59e02 100644 --- a/admin-panel/yarn.lock +++ b/admin-panel/yarn.lock @@ -5497,6 +5497,10 @@ redux-logger@^3.0.6: dependencies: deep-diff "^0.3.5" +redux-saga@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-0.16.0.tgz#0a231db0a1489301dd980f6f2f88d8ced418f724" + redux-thunk@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5" From 3aaaf8ffb8e493d1ef28429aaeffb5d154564e3d Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Mon, 11 Dec 2017 19:27:46 +0200 Subject: [PATCH 04/12] add simple test --- admin-panel/src/ducks/people.js | 6 ++++-- admin-panel/src/ducks/people.test.js | 32 ++++++++++++++++++++++++++++ admin-panel/src/ducks/utils.js | 3 +++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 admin-panel/src/ducks/people.test.js create mode 100644 admin-panel/src/ducks/utils.js diff --git a/admin-panel/src/ducks/people.js b/admin-panel/src/ducks/people.js index ddb584f..a5f38b9 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, takeEvery} from 'redux-saga/effects' +import {put, call, takeEvery} from 'redux-saga/effects' +import {generateId} from './utils' /** * Constants @@ -57,7 +58,8 @@ export function addPerson(person) { export const addPersonSaga = function * (action) { const { person } = action.payload - const id = Date.now() + + const id = yield call(generateId) yield put({ type: ADD_PERSON_SUCCESS, diff --git a/admin-panel/src/ducks/people.test.js b/admin-panel/src/ducks/people.test.js new file mode 100644 index 0000000..e1bf76f --- /dev/null +++ b/admin-panel/src/ducks/people.test.js @@ -0,0 +1,32 @@ +import {call, put} from 'redux-saga/effects' +import {addPersonSaga, ADD_PERSON, ADD_PERSON_SUCCESS} from './people' +import {generateId} from './utils' + +describe('people saga', () => { + it('should add person', () => { + const person = { + firstName: 'Roman', + lastName: 'Iakobchuk', + email: 'r.iakobchuk@javascript.ru' + } + + const action = { + type: ADD_PERSON, + payload: { person } + } + + const generator = addPersonSaga(action) + + expect(generator.next().value).toEqual(call(generateId)) + + const id = generateId() + + expect(generator.next(id).value).toEqual(put({ + type: ADD_PERSON_SUCCESS, + payload: {id, ...person} + })) + + expect(generator.next().done).toBe(true) + + }); +}); \ No newline at end of file diff --git a/admin-panel/src/ducks/utils.js b/admin-panel/src/ducks/utils.js new file mode 100644 index 0000000..f1e9445 --- /dev/null +++ b/admin-panel/src/ducks/utils.js @@ -0,0 +1,3 @@ +export function generateId() { + return Date.now() +} \ No newline at end of file From 16393622f550dca97f294d32f5e5c9fe53e70f3d Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Mon, 11 Dec 2017 19:58:37 +0200 Subject: [PATCH 05/12] rewrite app into saga --- admin-panel/src/ducks/auth.js | 104 +++++++++++++++++++++++--------- admin-panel/src/ducks/people.js | 6 +- admin-panel/src/redux/index.js | 5 +- admin-panel/src/redux/saga.js | 10 +++ 4 files changed, 91 insertions(+), 34 deletions(-) create mode 100644 admin-panel/src/redux/saga.js diff --git a/admin-panel/src/ducks/auth.js b/admin-panel/src/ducks/auth.js index af36e2a..7ad6671 100644 --- a/admin-panel/src/ducks/auth.js +++ b/admin-panel/src/ducks/auth.js @@ -1,3 +1,4 @@ +import {all, takeEvery, take, put, apply, call} from 'redux-saga/effects' import {appName} from '../config' import {createSelector} from 'reselect' import {Record} from 'immutable' @@ -9,9 +10,12 @@ import firebase from 'firebase' export const moduleName = 'auth' const prefix = `${appName}/${moduleName}` +export const SIGN_IN_REQUEST = `${prefix}/SIGN_IN_REQUEST` export const SIGN_IN_START = `${prefix}/SIGN_IN_START` export const SIGN_IN_SUCCESS = `${prefix}/SIGN_IN_SUCCESS` export const SIGN_IN_ERROR = `${prefix}/SIGN_IN_ERROR` + +export const SIGN_UP_REQUEST = `${prefix}/SIGN_UP_REQUEST` export const SIGN_UP_START = `${prefix}/SIGN_UP_START` export const SIGN_UP_SUCCESS = `${prefix}/SIGN_UP_SUCCESS` export const SIGN_UP_ERROR = `${prefix}/SIGN_UP_ERROR` @@ -65,45 +69,87 @@ export const loadingSelector = createSelector(stateSelector, state => state.load * Action Creators * */ -export function signIn(email, password) { - return (dispatch) => { - dispatch({ - type: SIGN_IN_START - }) +export function signUp(email, password) { + return { + type: SIGN_UP_REQUEST, + payload: { email, password } + } +} + - firebase.auth().signInWithEmailAndPassword(email, password) - .then(user => dispatch({ - type: SIGN_IN_SUCCESS, - payload: { user } - })) - .catch(error => dispatch({ - type: SIGN_IN_ERROR, - payload: { error } - })) +export function signIn(email, password) { + return { + type: SIGN_IN_REQUEST, + payload: { email, password } } } -export function signUp(email, password) { - return (dispatch) => { - dispatch({ + +firebase.auth().onAuthStateChanged(user => { + if (user) window.store.dispatch({ + type: SIGN_IN_SUCCESS, + payload: { user } + }) +}) + +/** + * Sagas + */ + +export const signUpSaga = function * () { + while (true) { + const action = yield take(SIGN_UP_REQUEST) + const {email, password} = action.payload + + yield put({ type: SIGN_UP_START }) - firebase.auth().createUserWithEmailAndPassword(email, password) - .then(user => dispatch({ + try { + const auth = firebase.auth() + const user = yield call([auth, auth.createUserWithEmailAndPassword], email, password) + + yield put({ type: SIGN_UP_SUCCESS, - payload: { user } - })) - .catch(error => dispatch({ + payload: {user} + }) + + } catch (error) { + yield put({ type: SIGN_UP_ERROR, - payload: { error } - })) + payload: {error} + }) + } } } -firebase.auth().onAuthStateChanged(user => { - if (user) window.store.dispatch({ - type: SIGN_IN_SUCCESS, - payload: { user } +export const signInSaga = function * (action) { + const { email, password } = action.payload + + yield put({ + type: SIGN_IN_START }) -}) \ No newline at end of file + + try { + const auth = firebase.auth() + const user = yield apply(auth, auth.signInWithEmailAndPassword, [email, password]) + + yield put({ + type: SIGN_IN_SUCCESS, + payload: { user } + }) + + } catch (error) { + yield put({ + type: SIGN_IN_ERROR, + payload: { error } + }) + } +} + +export const saga = function * () { + yield all([ + takeEvery(SIGN_IN_REQUEST, signInSaga), + signUpSaga() + ]) +} \ No newline at end of file diff --git a/admin-panel/src/ducks/people.js b/admin-panel/src/ducks/people.js index a5f38b9..52bce70 100644 --- a/admin-panel/src/ducks/people.js +++ b/admin-panel/src/ducks/people.js @@ -1,6 +1,6 @@ import {appName} from '../config' import {Record, List} from 'immutable' -import {put, call, takeEvery} from 'redux-saga/effects' +import {put, call, all, takeEvery} from 'redux-saga/effects' import {generateId} from './utils' /** @@ -68,5 +68,7 @@ export const addPersonSaga = function * (action) { } export const saga = function * () { - yield takeEvery(ADD_PERSON, addPersonSaga) + yield all([ + takeEvery(ADD_PERSON, addPersonSaga) + ]) } \ No newline at end of file diff --git a/admin-panel/src/redux/index.js b/admin-panel/src/redux/index.js index ff0e4c1..b69ed29 100644 --- a/admin-panel/src/redux/index.js +++ b/admin-panel/src/redux/index.js @@ -1,15 +1,14 @@ import {createStore, applyMiddleware} from 'redux' import logger from 'redux-logger' import {routerMiddleware} from 'react-router-redux' -import thunk from 'redux-thunk' import createSagaMiddleware from 'redux-saga' import reducer from './reducer' import history from '../history' -import {saga} from '../ducks/people' +import saga from './saga' const sagaMiddleware = createSagaMiddleware() -const store = createStore(reducer, applyMiddleware(sagaMiddleware, thunk, routerMiddleware(history), logger)) +const store = createStore(reducer, applyMiddleware(sagaMiddleware, routerMiddleware(history), logger)) sagaMiddleware.run(saga) diff --git a/admin-panel/src/redux/saga.js b/admin-panel/src/redux/saga.js new file mode 100644 index 0000000..4a6c841 --- /dev/null +++ b/admin-panel/src/redux/saga.js @@ -0,0 +1,10 @@ +import {all} from 'redux-saga/effects' +import {saga as peopleSaga} from '../ducks/people' +import {saga as authSaga} from '../ducks/auth' + +export default function * rootSaga() { + yield all([ + peopleSaga(), + authSaga() + ]) +} \ No newline at end of file From 0bac3e87d34f7c336034b8bcdab6aea3144a2bec Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Mon, 11 Dec 2017 20:06:30 +0200 Subject: [PATCH 06/12] add HT2 --- admin-panel/README.md | 7 ++++++- admin-panel/src/index.js | 1 + admin-panel/src/mocks/index.js | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/admin-panel/README.md b/admin-panel/README.md index f445bfc..6c0fee0 100644 --- a/admin-panel/README.md +++ b/admin-panel/README.md @@ -1,3 +1,8 @@ ##HT1.1 Завести собственный проект на firebase, работать с ним ##HT1.2 Сделать форму для добавлегния людей(firstName, lastName, email) -##HT1.3 Показывать лоадер и ошибки в форме аутентификации \ No newline at end of file +##HT1.3 Показывать лоадер и ошибки в форме аутентификации + +##HT2.1 Написать тесты на auth +##HT2.2 Чистить форму people после успешного добавления в стор +##HT2.3 Отображать список людей +##HT2.4 Загружать и отображать список ивентов \ No newline at end of file diff --git a/admin-panel/src/index.js b/admin-panel/src/index.js index ad60938..adb21d9 100644 --- a/admin-panel/src/index.js +++ b/admin-panel/src/index.js @@ -2,5 +2,6 @@ import React from 'react' import ReactDOM from 'react-dom' import './config' import Root from './Root' +import './mocks' ReactDOM.render(, document.getElementById('root')) diff --git a/admin-panel/src/mocks/index.js b/admin-panel/src/mocks/index.js index 25fe7c5..d1a851f 100644 --- a/admin-panel/src/mocks/index.js +++ b/admin-panel/src/mocks/index.js @@ -4,4 +4,6 @@ import firebase from 'firebase' export function saveEventsToFB() { const eventsRef = firebase.database().ref('/events') conferences.forEach(conference => eventsRef.push(conference)) -} \ No newline at end of file +} + +window.saveEventsToFB = saveEventsToFB \ No newline at end of file From 7475835e38db16b262a6ff1764d2959ec30d623e Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Thu, 14 Dec 2017 15:32:50 +0200 Subject: [PATCH 07/12] add HW2 --- admin-panel/.gitignore | 3 +- admin-panel/src/components/App.js | 8 +- .../src/components/events/EventsTable.js | 41 +++++++ .../src/components/people/PeopleTable.js | 33 ++++++ .../src/components/routes/EventsPage.js | 18 +++ .../src/components/routes/PersonPage.js | 2 + admin-panel/src/ducks/auth.js | 29 +++-- admin-panel/src/ducks/auth.test.js | 110 ++++++++++++++++++ admin-panel/src/ducks/events.js | 100 ++++++++++++++++ admin-panel/src/ducks/people.js | 9 +- admin-panel/src/ducks/people.test.js | 3 + admin-panel/src/ducks/utils.js | 10 ++ admin-panel/src/redux/reducer.js | 4 +- admin-panel/src/redux/saga.js | 4 +- 14 files changed, 352 insertions(+), 22 deletions(-) create mode 100644 admin-panel/src/components/events/EventsTable.js create mode 100644 admin-panel/src/components/people/PeopleTable.js create mode 100644 admin-panel/src/components/routes/EventsPage.js create mode 100644 admin-panel/src/ducks/auth.test.js create mode 100644 admin-panel/src/ducks/events.js diff --git a/admin-panel/.gitignore b/admin-panel/.gitignore index 5b4863a..7b63ae9 100644 --- a/admin-panel/.gitignore +++ b/admin-panel/.gitignore @@ -20,4 +20,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -.idea \ No newline at end of file +.idea +./src/config.js \ No newline at end of file diff --git a/admin-panel/src/components/App.js b/admin-panel/src/components/App.js index 6abd045..207fb54 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/EventsPage' class App extends Component { static propTypes = { @@ -15,11 +16,12 @@ class App extends Component {

Hello world

    -
  • admin
  • -
  • people
  • +
  • admin
  • +
  • people
- + +
) diff --git a/admin-panel/src/components/events/EventsTable.js b/admin-panel/src/components/events/EventsTable.js new file mode 100644 index 0000000..dae9db9 --- /dev/null +++ b/admin-panel/src/components/events/EventsTable.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react' +import {connect} from 'react-redux' +import {fetchAllEvents, eventListSelector, loadedSelector, loadingSelector} from '../../ducks/events' +import Loader from '../common/Loader' + +class EventsTable extends Component { + static propTypes = { + + }; + + componentDidMount() { + this.props.fetchAllEvents() + } + + render() { + if (this.props.loading) return + return ( + + + {this.getRows()} + +
+ ) + } + + getRows = () => this.props.events.map(this.getRow) + + getRow = (event) => ( + + {event.title} + {event.when} + {event.where} + + ) +} + +export default connect((state) => ({ + events: eventListSelector(state), + loading: loadingSelector(state), + loaded: loadedSelector(state) +}), { fetchAllEvents })(EventsTable) \ No newline at end of file diff --git a/admin-panel/src/components/people/PeopleTable.js b/admin-panel/src/components/people/PeopleTable.js new file mode 100644 index 0000000..c658acf --- /dev/null +++ b/admin-panel/src/components/people/PeopleTable.js @@ -0,0 +1,33 @@ +import React, { Component } from 'react' +import {connect} from 'react-redux' +import {peopleListSelector} from '../../ducks/people' + +class PeopleTable extends Component { + static propTypes = { + + }; + + render() { + return ( + + + {this.getRows()} + +
+ ) + } + + getRows = () => this.props.people.map(this.getRow) + + getRow = (person) => ( + + {person.firstName} + {person.lastName} + {person.email} + + ) +} + +export default connect((state) => ({ + people: peopleListSelector(state) +}))(PeopleTable) \ No newline at end of file diff --git a/admin-panel/src/components/routes/EventsPage.js b/admin-panel/src/components/routes/EventsPage.js new file mode 100644 index 0000000..be61d16 --- /dev/null +++ b/admin-panel/src/components/routes/EventsPage.js @@ -0,0 +1,18 @@ +import React, { Component } from 'react' +import EventsTable from '../events/EventsTable' + +class EventsPage extends Component { + static propTypes = { + + }; + + render() { + return ( +
+ +
+ ) + } +} + +export default EventsPage \ No newline at end of file diff --git a/admin-panel/src/components/routes/PersonPage.js b/admin-panel/src/components/routes/PersonPage.js index fd69263..a8bab2f 100644 --- a/admin-panel/src/components/routes/PersonPage.js +++ b/admin-panel/src/components/routes/PersonPage.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import {connect} from 'react-redux' import {addPerson} from '../../ducks/people' import NewPersonForm from '../people/NewPersonForm' +import PeopleTable from '../people/PeopleTable' class PersonPage extends Component { static propTypes = { @@ -13,6 +14,7 @@ class PersonPage extends Component {

Add new person

+
) } diff --git a/admin-panel/src/ducks/auth.js b/admin-panel/src/ducks/auth.js index 7ad6671..92ed143 100644 --- a/admin-panel/src/ducks/auth.js +++ b/admin-panel/src/ducks/auth.js @@ -3,6 +3,7 @@ import {appName} from '../config' import {createSelector} from 'reselect' import {Record} from 'immutable' import firebase from 'firebase' +import {replace} from 'react-router-redux' /** * Constants @@ -107,13 +108,7 @@ export const signUpSaga = function * () { try { const auth = firebase.auth() - const user = yield call([auth, auth.createUserWithEmailAndPassword], email, password) - - yield put({ - type: SIGN_UP_SUCCESS, - payload: {user} - }) - + yield call([auth, auth.createUserWithEmailAndPassword], email, password) } catch (error) { yield put({ type: SIGN_UP_ERROR, @@ -132,13 +127,7 @@ export const signInSaga = function * (action) { try { const auth = firebase.auth() - const user = yield apply(auth, auth.signInWithEmailAndPassword, [email, password]) - - yield put({ - type: SIGN_IN_SUCCESS, - payload: { user } - }) - + yield apply(auth, auth.signInWithEmailAndPassword, [email, password]) } catch (error) { yield put({ type: SIGN_IN_ERROR, @@ -147,9 +136,19 @@ export const signInSaga = function * (action) { } } +export function * watchStatusChangeSaga() { + while (true) { + yield take(SIGN_IN_SUCCESS) + + yield (put(replace('/events'))) + } +} + + export const saga = function * () { yield all([ takeEvery(SIGN_IN_REQUEST, signInSaga), - signUpSaga() + signUpSaga(), + watchStatusChangeSaga() ]) } \ 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..ab502c2 --- /dev/null +++ b/admin-panel/src/ducks/auth.test.js @@ -0,0 +1,110 @@ +import firebase from 'firebase' +import reducer, { + signUpSaga, signInSaga, watchStatusChangeSaga, + SIGN_UP_REQUEST, SIGN_UP_START, SIGN_UP_SUCCESS, SIGN_UP_ERROR, + SIGN_IN_REQUEST, SIGN_IN_START, SIGN_IN_SUCCESS, SIGN_IN_ERROR, + ReducerRecord +} from './auth' +import {take, call, put} from 'redux-saga/effects' +import {replace} from 'react-router-redux' + +/** + * Saga tests + * */ + +it('should sign up', () => { + const saga = signUpSaga() + const auth = firebase.auth() + const authData = { + email: 'lala@example.com', + password: '12341234' + } + + const user = { + email: authData.email, + uid: Math.random().toString() + } + + const requestAction = { + type: SIGN_UP_REQUEST, + payload: authData + } + + expect(saga.next().value).toEqual(take(SIGN_UP_REQUEST)) + + expect(saga.next(requestAction).value).toEqual(put({type: SIGN_UP_START})) + + expect(saga.next(requestAction).value).toEqual(call( + [auth, auth.createUserWithEmailAndPassword], + authData.email, authData.password + )) + + const error = new Error + + expect(saga.throw(error).value).toEqual(put({ + type: SIGN_UP_ERROR, + payload: {error} + })) +}) + +it('should sign in', () => { + const auth = firebase.auth() + const authData = { + email: 'lala@example.com', + password: '12341234' + } + + const user = { + email: authData.email, + uid: Math.random().toString() + } + + const requestAction = { + type: SIGN_IN_REQUEST, + payload: authData + } + + const saga = signInSaga(requestAction) + + expect(saga.next().value).toEqual(put({type: SIGN_IN_START})) + + expect(saga.next().value).toEqual(call( + [auth, auth.signInWithEmailAndPassword], + authData.email, authData.password + )) + + const error = new Error + + expect(saga.throw(error).value).toEqual(put({ + type: SIGN_IN_ERROR, + payload: {error} + })) +}) + +it('should redirect', () => { + const saga = watchStatusChangeSaga() + + expect(saga.next().value).toEqual(take(SIGN_IN_SUCCESS)) + + expect(saga.next().value).toEqual(put(replace('/events'))) +}) + +/** + * Reducer Tests + * */ + +it('should sign in', () => { + const state = new ReducerRecord() + const user = { + email: 'lala@example.com', + uid: Math.random().toString() + } + + const newState = reducer(state, { + type: SIGN_IN_SUCCESS, + payload: {user} + }) + + expect(newState).toEqual(new ReducerRecord({user})) +}) + diff --git a/admin-panel/src/ducks/events.js b/admin-panel/src/ducks/events.js new file mode 100644 index 0000000..115455c --- /dev/null +++ b/admin-panel/src/ducks/events.js @@ -0,0 +1,100 @@ +import {all, takeEvery, put, call} from 'redux-saga/effects' +import {appName} from '../config' +import {Record, List} from 'immutable' +import firebase from 'firebase' +import {createSelector} from 'reselect' +import {fbToEntities} from './utils' + +/** + * Constants + * */ +export const moduleName = 'events' +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` + +/** + * Reducer + * */ +export const ReducerRecord = Record({ + loading: false, + loaded: false, + entities: new List([]) +}) + +export const EventRecord = Record({ + uid: null, + month: null, + submissionDeadline: null, + title: null, + url: null, + when: null, + where: null +}) + +export default function reducer(state = new ReducerRecord(), action) { + const {type, payload} = action + + switch (type) { + 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, EventRecord)) + + default: + return state + } +} + +/** + * Selectors + * */ + +export const stateSelector = state => state[moduleName] +export const entitiesSelector = createSelector(stateSelector, state => state.entities) +export const loadingSelector = createSelector(stateSelector, state => state.loading) +export const loadedSelector = createSelector(stateSelector, state => state.loaded) +export const eventListSelector = createSelector(entitiesSelector, entities => entities.toArray()) + +/** + * Action Creators + * */ + +export function fetchAllEvents() { + return { + type: FETCH_ALL_REQUEST + } +} + +/** + * Sagas + * */ + +export function* fetchAllSaga() { + const ref = firebase.database().ref('events') + + yield put({ + type: FETCH_ALL_START + }) + + const snapshot = yield call([ref, ref.once], 'value') + + console.log('---', snapshot) + + yield put({ + type: FETCH_ALL_SUCCESS, + payload: snapshot.val() + }) +} + +export function* saga() { + yield all([ + takeEvery(FETCH_ALL_REQUEST, fetchAllSaga) + ]) +} \ No newline at end of file diff --git a/admin-panel/src/ducks/people.js b/admin-panel/src/ducks/people.js index 52bce70..99e6e45 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 {createSelector} from 'reselect' import {put, call, all, takeEvery} from 'redux-saga/effects' +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 @@ -40,6 +42,9 @@ export default function reducer(state = new ReducerState(), action) { /** * Selectors * */ +export const stateSelector = state => state[moduleName] +export const entitiesSelector = createSelector(stateSelector, state => state.entities) +export const peopleListSelector = createSelector(entitiesSelector, entities => entities.toArray()) /** * Action Creators @@ -65,6 +70,8 @@ export const addPersonSaga = function * (action) { 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..96c76e7 100644 --- a/admin-panel/src/ducks/people.test.js +++ b/admin-panel/src/ducks/people.test.js @@ -1,5 +1,6 @@ import {call, put} from 'redux-saga/effects' import {addPersonSaga, ADD_PERSON, ADD_PERSON_SUCCESS} from './people' +import {reset} from 'redux-form' import {generateId} from './utils' describe('people saga', () => { @@ -26,6 +27,8 @@ describe('people saga', () => { payload: {id, ...person} })) + expect(generator.next().value).toEqual(put(reset('person'))) + expect(generator.next().done).toBe(true) }); diff --git a/admin-panel/src/ducks/utils.js b/admin-panel/src/ducks/utils.js index f1e9445..604924c 100644 --- a/admin-panel/src/ducks/utils.js +++ b/admin-panel/src/ducks/utils.js @@ -1,3 +1,13 @@ +import {List} from 'immutable' + export function generateId() { return Date.now() +} + +export function fbToEntities(values, DataRecord) { + return Object.entries(values) + .reduce( + (acc, [uid, value]) => acc.push(new DataRecord({ uid, ...value })), + new List([]) + ) } \ No newline at end of file diff --git a/admin-panel/src/redux/reducer.js b/admin-panel/src/redux/reducer.js index cc6e0aa..4afcd66 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..d9bb32f 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 {saga as eventsSaga} from '../ducks/events' export default function * rootSaga() { yield all([ peopleSaga(), - authSaga() + authSaga(), + eventsSaga() ]) } \ No newline at end of file From 5e1fc2f3db2acd5fb5536045279c2dad07e6b432 Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Thu, 14 Dec 2017 18:50:11 +0200 Subject: [PATCH 08/12] rewrite to map --- admin-panel/src/ducks/events.js | 6 +++--- admin-panel/src/ducks/people.js | 12 ++++++------ admin-panel/src/ducks/people.test.js | 6 +++--- admin-panel/src/ducks/utils.js | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/admin-panel/src/ducks/events.js b/admin-panel/src/ducks/events.js index 115455c..f4be6f3 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, List} from 'immutable' +import {Record, List, OrderedMap} from 'immutable' import firebase from 'firebase' import {createSelector} from 'reselect' import {fbToEntities} from './utils' @@ -21,7 +21,7 @@ export const FETCH_ALL_SUCCESS = `${prefix}/FETCH_ALL_SUCCESS` export const ReducerRecord = Record({ loading: false, loaded: false, - entities: new List([]) + entities: new OrderedMap({}) }) export const EventRecord = Record({ @@ -60,7 +60,7 @@ export const stateSelector = state => state[moduleName] export const entitiesSelector = createSelector(stateSelector, state => state.entities) export const loadingSelector = createSelector(stateSelector, state => state.loading) export const loadedSelector = createSelector(stateSelector, state => state.loaded) -export const eventListSelector = createSelector(entitiesSelector, entities => entities.toArray()) +export const eventListSelector = createSelector(entitiesSelector, entities => entities.valueSeq().toArray()) /** * Action Creators diff --git a/admin-panel/src/ducks/people.js b/admin-panel/src/ducks/people.js index 99e6e45..9a5f996 100644 --- a/admin-panel/src/ducks/people.js +++ b/admin-panel/src/ducks/people.js @@ -1,5 +1,5 @@ import {appName} from '../config' -import {Record, List} from 'immutable' +import {Record, OrderedMap} from 'immutable' import {createSelector} from 'reselect' import {put, call, all, takeEvery} from 'redux-saga/effects' import {reset} from 'redux-form' @@ -17,7 +17,7 @@ export const ADD_PERSON_SUCCESS = `${prefix}/ADD_PERSON_SUCCESS` * Reducer * */ const ReducerState = Record({ - entities: new List([]) + entities: new OrderedMap({}) }) const PersonRecord = Record({ @@ -32,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))) + return state.setIn(['entities', payload.uid],new PersonRecord(payload)) default: return state @@ -44,7 +44,7 @@ export default function reducer(state = new ReducerState(), action) { * */ export const stateSelector = state => state[moduleName] export const entitiesSelector = createSelector(stateSelector, state => state.entities) -export const peopleListSelector = createSelector(entitiesSelector, entities => entities.toArray()) +export const peopleListSelector = createSelector(entitiesSelector, entities => entities.valueSeq().toArray()) /** * Action Creators @@ -64,11 +64,11 @@ export function addPerson(person) { export const addPersonSaga = function * (action) { const { person } = action.payload - const id = yield call(generateId) + const uid = yield call(generateId) yield put({ type: ADD_PERSON_SUCCESS, - payload: {id, ...person} + payload: {uid, ...person} }) yield put(reset('person')) diff --git a/admin-panel/src/ducks/people.test.js b/admin-panel/src/ducks/people.test.js index 96c76e7..d24f765 100644 --- a/admin-panel/src/ducks/people.test.js +++ b/admin-panel/src/ducks/people.test.js @@ -20,11 +20,11 @@ describe('people saga', () => { expect(generator.next().value).toEqual(call(generateId)) - const id = generateId() + const uid = generateId() - expect(generator.next(id).value).toEqual(put({ + expect(generator.next(uid).value).toEqual(put({ type: ADD_PERSON_SUCCESS, - payload: {id, ...person} + payload: {uid, ...person} })) expect(generator.next().value).toEqual(put(reset('person'))) diff --git a/admin-panel/src/ducks/utils.js b/admin-panel/src/ducks/utils.js index 604924c..df63cc2 100644 --- a/admin-panel/src/ducks/utils.js +++ b/admin-panel/src/ducks/utils.js @@ -1,4 +1,4 @@ -import {List} from 'immutable' +import {OrderedMap} from 'immutable' export function generateId() { return Date.now() @@ -7,7 +7,7 @@ export function generateId() { export function fbToEntities(values, DataRecord) { return Object.entries(values) .reduce( - (acc, [uid, value]) => acc.push(new DataRecord({ uid, ...value })), - new List([]) + (acc, [uid, value]) => acc.set(uid, new DataRecord({ uid, ...value })), + new OrderedMap({}) ) } \ No newline at end of file From 69e9770197f8100436415b9317a96250882efc9b Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Thu, 14 Dec 2017 19:21:57 +0200 Subject: [PATCH 09/12] basic tests --- admin-panel/package.json | 4 + .../src/components/events/EventsTable.js | 8 +- .../src/components/events/EventsTable.test.js | 48 +++++ admin-panel/src/ducks/events.js | 18 +- admin-panel/src/setupTests.js | 4 + admin-panel/yarn.lock | 199 +++++++++++++++++- 6 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 admin-panel/src/components/events/EventsTable.test.js create mode 100644 admin-panel/src/setupTests.js diff --git a/admin-panel/package.json b/admin-panel/package.json index 1ada3cf..3bd3d70 100644 --- a/admin-panel/package.json +++ b/admin-panel/package.json @@ -26,5 +26,9 @@ "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" + }, + "devDependencies": { + "enzyme": "^3.2.0", + "enzyme-adapter-react-16": "^1.1.0" } } diff --git a/admin-panel/src/components/events/EventsTable.js b/admin-panel/src/components/events/EventsTable.js index dae9db9..432e7a1 100644 --- a/admin-panel/src/components/events/EventsTable.js +++ b/admin-panel/src/components/events/EventsTable.js @@ -1,9 +1,9 @@ import React, { Component } from 'react' import {connect} from 'react-redux' -import {fetchAllEvents, eventListSelector, loadedSelector, loadingSelector} from '../../ducks/events' +import {fetchAllEvents, selectEvent, eventListSelector, loadedSelector, loadingSelector} from '../../ducks/events' import Loader from '../common/Loader' -class EventsTable extends Component { +export class EventsTable extends Component { static propTypes = { }; @@ -26,7 +26,7 @@ class EventsTable extends Component { getRows = () => this.props.events.map(this.getRow) getRow = (event) => ( - + this.props.selectEvent(event.uid)}> {event.title} {event.when} {event.where} @@ -38,4 +38,4 @@ export default connect((state) => ({ events: eventListSelector(state), loading: loadingSelector(state), loaded: loadedSelector(state) -}), { fetchAllEvents })(EventsTable) \ No newline at end of file +}), { fetchAllEvents, selectEvent })(EventsTable) \ No newline at end of file diff --git a/admin-panel/src/components/events/EventsTable.test.js b/admin-panel/src/components/events/EventsTable.test.js new file mode 100644 index 0000000..4cf75c1 --- /dev/null +++ b/admin-panel/src/components/events/EventsTable.test.js @@ -0,0 +1,48 @@ +import React from 'react' +import {shallow} from 'enzyme' +import events from '../../mocks/conferences' + +import {EventsTable} from './EventsTable' +import Loader from '../common/Loader' + +const eventList = events.map(event => ({...event, uid: Math.random()})) + +describe('Events Table', () => { + it('should render a loader', () => { + const table = shallow(, { disableLifecycleMethods: true }) + + expect(table.contains()) + }); + + it('should render N rows', () => { + const table = shallow(, { disableLifecycleMethods: true }) + + expect(table.find('.test__event_table_row').length).toBe(eventList.length) + }); + + it('should request fetch all events', function (done) { + shallow( done()} + />) + }); + + it('should select an event', () => { + let selected = null + + const table = shallow( selected = uid} + />, { disableLifecycleMethods: true }) + + table.find('.test__event_table_row').first().simulate('click') + + expect(selected).toEqual(eventList[0].uid) + + }); + +}); \ No newline at end of file diff --git a/admin-panel/src/ducks/events.js b/admin-panel/src/ducks/events.js index f4be6f3..f13e342 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, List, OrderedMap} from 'immutable' +import {Record, OrderedSet, OrderedMap} from 'immutable' import firebase from 'firebase' import {createSelector} from 'reselect' import {fbToEntities} from './utils' @@ -15,12 +15,15 @@ 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 SELECT = `${prefix}/SELECT` + /** * Reducer * */ export const ReducerRecord = Record({ loading: false, loaded: false, + selected: new OrderedSet(), entities: new OrderedMap({}) }) @@ -47,6 +50,12 @@ export default function reducer(state = new ReducerRecord(), action) { .set('loaded', true) .set('entities', fbToEntities(payload, EventRecord)) + case SELECT: + return state.update('selected', selected => selected.has(payload.uid) + ? selected.remove(payload.uid) + : selected.add(payload.uid) + ) + default: return state } @@ -72,6 +81,13 @@ export function fetchAllEvents() { } } +export function selectEvent(uid) { + return { + type: SELECT, + payload: { uid } + } +} + /** * Sagas * */ diff --git a/admin-panel/src/setupTests.js b/admin-panel/src/setupTests.js new file mode 100644 index 0000000..88c8c3b --- /dev/null +++ b/admin-panel/src/setupTests.js @@ -0,0 +1,4 @@ +import Enzyme from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' + +Enzyme.configure({ adapter: new Adapter() }) diff --git a/admin-panel/yarn.lock b/admin-panel/yarn.lock index 5e59e02..7b1cacb 100644 --- a/admin-panel/yarn.lock +++ b/admin-panel/yarn.lock @@ -50,6 +50,10 @@ version "0.2.4" resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.4.tgz#ec8438b780baff09254076efe345f30fdb41243e" +"@types/node@*": + version "8.5.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.1.tgz#4ec3020bcdfe2abffeef9ba3fbf26fca097514b5" + abab@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -1352,6 +1356,17 @@ chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" +cheerio@^1.0.0-rc.2: + version "1.0.0-rc.2" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + chokidar@^1.6.0, chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" @@ -1474,6 +1489,10 @@ colormin@^1.0.5: css-color-names "0.0.4" has "^1.0.1" +colors@0.5.x: + version "0.5.1" + resolved "https://registry.yarnpkg.com/colors/-/colors-0.5.1.tgz#7d0023eaeb154e8ee9fce75dcb923d0ed1667774" + colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -1702,7 +1721,7 @@ css-loader@0.28.7: postcss-value-parser "^3.3.0" source-list-map "^2.0.0" -css-select@^1.1.0: +css-select@^1.1.0, css-select@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" dependencies: @@ -1935,6 +1954,10 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -1971,7 +1994,7 @@ dom-converter@~0.1: dependencies: utila "~0.3" -dom-serializer@0: +dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" dependencies: @@ -1992,7 +2015,7 @@ domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" -domelementtype@1: +domelementtype@1, domelementtype@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" @@ -2006,6 +2029,12 @@ domhandler@2.1: dependencies: domelementtype "1" +domhandler@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" + dependencies: + domelementtype "1" + domutils@1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" @@ -2019,6 +2048,13 @@ domutils@1.5.1: dom-serializer "0" domelementtype "1" +domutils@^1.5.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" + dependencies: + dom-serializer "0" + domelementtype "1" + dot-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" @@ -2096,10 +2132,45 @@ enhanced-resolve@^3.4.0: object-assign "^4.0.1" tapable "^0.2.7" -entities@~1.1.1: +entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" +enzyme-adapter-react-16@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.0.tgz#86c5db7c10f0be6ec25d54ca41b59f2abb397cf4" + dependencies: + enzyme-adapter-utils "^1.1.0" + lodash "^4.17.4" + object.assign "^4.0.4" + object.values "^1.0.4" + prop-types "^15.5.10" + react-test-renderer "^16.0.0-0" + +enzyme-adapter-utils@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.2.0.tgz#7f4471ee0a70b91169ec8860d2bf0a6b551664b2" + dependencies: + lodash "^4.17.4" + object.assign "^4.0.4" + prop-types "^15.5.10" + +enzyme@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.2.0.tgz#998bdcda0fc71b8764a0017f7cc692c943f54a7a" + dependencies: + cheerio "^1.0.0-rc.2" + function.prototype.name "^1.0.3" + has "^1.0.1" + is-subset "^0.1.1" + lodash "^4.17.4" + object-is "^1.0.1" + object.assign "^4.0.4" + object.entries "^1.0.4" + object.values "^1.0.4" + raf "^3.4.0" + rst-selector-parser "^2.2.3" + errno@^0.1.3, errno@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" @@ -2112,7 +2183,7 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.7.0: +es-abstract@^1.6.1, es-abstract@^1.7.0: version "1.10.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" dependencies: @@ -2779,10 +2850,18 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.0.2, function-bind@^1.1.1: +function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" +function.prototype.name@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.3.tgz#0099ae5572e9dd6f03c97d023fd92bcc5e639eac" + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.0" + is-callable "^1.1.3" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -3125,6 +3204,17 @@ html-webpack-plugin@2.29.0: pretty-error "^2.0.2" toposort "^1.0.0" +htmlparser2@^3.9.1: + version "3.9.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" + dependencies: + domelementtype "^1.3.0" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^2.0.2" + htmlparser2@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" @@ -3476,6 +3566,10 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" +is-subset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + is-svg@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" @@ -4055,6 +4149,10 @@ lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -4321,6 +4419,14 @@ ncname@1.0.x: dependencies: xml-char-classes "^1.0.0" +nearley@^2.7.10: + version "2.11.0" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.11.0.tgz#5e626c79a6cd2f6ab9e7e5d5805e7668967757ae" + dependencies: + nomnom "~1.6.2" + railroad-diagrams "^1.0.0" + randexp "^0.4.2" + negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" @@ -4403,6 +4509,13 @@ node-status-codes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" +nomnom@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.6.2.tgz#84a66a260174408fc5b77a18f888eccc44fb6971" + dependencies: + colors "0.5.x" + underscore "~1.4.4" + nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" @@ -4483,10 +4596,31 @@ object-hash@^1.1.4: version "1.2.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.2.0.tgz#e96af0e96981996a1d47f88ead8f74f1ebc4422b" -object-keys@^1.0.8: +object-is@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" + +object-keys@^1.0.10, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" +object.assign@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.0" + object-keys "^1.0.10" + +object.entries@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.0.4.tgz#1bf9a4dd2288f5b33f3a993d257661f05d161a5f" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.6.1" + function-bind "^1.1.0" + has "^1.0.1" + object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -4494,6 +4628,15 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" +object.values@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.6.1" + function-bind "^1.1.0" + has "^1.0.1" + obuf@^1.0.0, obuf@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" @@ -4657,6 +4800,12 @@ parse5@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + dependencies: + "@types/node" "*" + parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" @@ -5206,12 +5355,23 @@ querystringify@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" -raf@3.4.0: +raf@3.4.0, raf@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" dependencies: performance-now "^2.1.0" +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + +randexp@^0.4.2: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + randomatic@^1.1.3: version "1.1.7" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" @@ -5375,6 +5535,14 @@ react-scripts@1.0.17: optionalDependencies: fsevents "1.1.2" +react-test-renderer@^16.0.0-0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.2.0.tgz#bddf259a6b8fcd8555f012afc8eacc238872a211" + dependencies: + fbjs "^0.8.16" + object-assign "^4.1.1" + prop-types "^15.6.0" + react@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" @@ -5730,6 +5898,10 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -5749,6 +5921,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" +rst-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" + dependencies: + lodash.flattendeep "^4.4.0" + nearley "^2.7.10" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -6397,6 +6576,10 @@ uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" +underscore@~1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604" + uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" From fa9726d905269533d5260cde87b89b571e9606ae Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Thu, 14 Dec 2017 19:42:33 +0200 Subject: [PATCH 10/12] add virtualized table --- admin-panel/package.json | 1 + .../events/EventsTableVirtualized.js | 55 +++++++++++++++++++ .../src/components/routes/EventsPage.js | 4 +- admin-panel/yarn.lock | 22 +++++++- 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 admin-panel/src/components/events/EventsTableVirtualized.js diff --git a/admin-panel/package.json b/admin-panel/package.json index 3bd3d70..d61a23f 100644 --- a/admin-panel/package.json +++ b/admin-panel/package.json @@ -14,6 +14,7 @@ "react-router-dom": "^4.2.2", "react-router-redux": "^5.0.0-alpha.8", "react-scripts": "1.0.17", + "react-virtualized": "^9.13.0", "redux": "^3.7.2", "redux-form": "^7.2.0", "redux-logger": "^3.0.6", diff --git a/admin-panel/src/components/events/EventsTableVirtualized.js b/admin-panel/src/components/events/EventsTableVirtualized.js new file mode 100644 index 0000000..c4f8c3e --- /dev/null +++ b/admin-panel/src/components/events/EventsTableVirtualized.js @@ -0,0 +1,55 @@ +import React, { Component } from 'react' +import {connect} from 'react-redux' +import {fetchAllEvents, selectEvent, eventListSelector, loadedSelector, loadingSelector} from '../../ducks/events' +import Loader from '../common/Loader' +import {Table, Column} from 'react-virtualized' +import 'react-virtualized/styles.css' + +export class EventsTableVirtualized extends Component { + static propTypes = { + + }; + + componentDidMount() { + this.props.fetchAllEvents() + } + + render() { + if (this.props.loading) return + return ( + + + + +
+ ) + } + + rowGetter = ({ index }) => this.props.events[index] +} + +export default connect((state) => ({ + events: eventListSelector(state), + loading: loadingSelector(state), + loaded: loadedSelector(state) +}), { fetchAllEvents, selectEvent })(EventsTableVirtualized) \ 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 be61d16..eedf79d 100644 --- a/admin-panel/src/components/routes/EventsPage.js +++ b/admin-panel/src/components/routes/EventsPage.js @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import EventsTable from '../events/EventsTable' +import EventsTableVirtualized from '../events/EventsTableVirtualized' class EventsPage extends Component { static propTypes = { @@ -9,7 +9,7 @@ class EventsPage extends Component { render() { return (
- +
) } diff --git a/admin-panel/yarn.lock b/admin-panel/yarn.lock index 7b1cacb..2efe11e 100644 --- a/admin-panel/yarn.lock +++ b/admin-panel/yarn.lock @@ -967,7 +967,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@6.26.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@6.26.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -1403,6 +1403,10 @@ clap@^1.0.9: dependencies: chalk "^1.1.3" +classnames@^2.2.3: + version "2.2.5" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" + clean-css@4.1.x: version "4.1.9" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.9.tgz#35cee8ae7687a49b98034f70de00c4edd3826301" @@ -1994,6 +1998,10 @@ dom-converter@~0.1: dependencies: utila "~0.3" +"dom-helpers@^2.4.0 || ^3.0.0": + version "3.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" + dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -4194,7 +4202,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -5543,6 +5551,16 @@ react-test-renderer@^16.0.0-0: object-assign "^4.1.1" prop-types "^15.6.0" +react-virtualized@^9.13.0: + version "9.13.0" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.13.0.tgz#83e4d984271a37631225e5fe6faeaeada6e59f53" + dependencies: + babel-runtime "^6.23.0" + classnames "^2.2.3" + dom-helpers "^2.4.0 || ^3.0.0" + loose-envify "^1.3.0" + prop-types "^15.5.4" + react@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" From 920a98074576a888ecc7fdc0efa152fd839e6f3e Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Thu, 14 Dec 2017 19:54:52 +0200 Subject: [PATCH 11/12] add list --- .../src/components/people/PeopleList.js | 44 +++++++++++++++++++ .../src/components/routes/PersonPage.js | 4 +- 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 admin-panel/src/components/people/PeopleList.js diff --git a/admin-panel/src/components/people/PeopleList.js b/admin-panel/src/components/people/PeopleList.js new file mode 100644 index 0000000..1fe2549 --- /dev/null +++ b/admin-panel/src/components/people/PeopleList.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react' +import {connect} from 'react-redux' +import {peopleListSelector} from '../../ducks/people' +import {List} from 'react-virtualized' +import 'react-virtualized/styles.css' + +class PeopleList extends Component { + static propTypes = { + + }; + + render() { + return + + } + + rowRenderer = ({ style, index, key }) => { + const person = this.props.people[index] + return ( +
+

{person.firstName} {person.lastName}

+

{person.email}

+
+ ) + } + + getRow = (person) => ( + + {person.firstName} + {person.lastName} + {person.email} + + ) +} + +export default connect((state) => ({ + people: peopleListSelector(state) +}))(PeopleList) \ No newline at end of file diff --git a/admin-panel/src/components/routes/PersonPage.js b/admin-panel/src/components/routes/PersonPage.js index a8bab2f..9bf8df9 100644 --- a/admin-panel/src/components/routes/PersonPage.js +++ b/admin-panel/src/components/routes/PersonPage.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import {connect} from 'react-redux' import {addPerson} from '../../ducks/people' import NewPersonForm from '../people/NewPersonForm' -import PeopleTable from '../people/PeopleTable' +import PeopleList from '../people/PeopleList' class PersonPage extends Component { static propTypes = { @@ -14,7 +14,7 @@ class PersonPage extends Component {

Add new person

- +
) } From 291751df6d5470479b3a7a4d7cff31e841ebfb38 Mon Sep 17 00:00:00 2001 From: Roman Iakobchuk Date: Thu, 14 Dec 2017 20:00:26 +0200 Subject: [PATCH 12/12] add HT3 --- admin-panel/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/admin-panel/README.md b/admin-panel/README.md index 6c0fee0..75e4ae5 100644 --- a/admin-panel/README.md +++ b/admin-panel/README.md @@ -5,4 +5,8 @@ ##HT2.1 Написать тесты на auth ##HT2.2 Чистить форму people после успешного добавления в стор ##HT2.3 Отображать список людей -##HT2.4 Загружать и отображать список ивентов \ No newline at end of file +##HT2.4 Загружать и отображать список ивентов + +##HT3.1 Добавлять людей в firebase +##HT3.2 Реализовать lazy-loading ивентов(InfiniteLoader), ref.orderByKey().startAt(...).limitToFirst(10) +##HT3.3 Тесты на Virtualized Events \ No newline at end of file