Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions admin/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ class App extends Component {
people
</NavLink>
</div>
<div>
<NavLink to="/admin/events" activeStyle={{ color: 'red' }}>
events
</NavLink>
</div>
<div>
<NavLink to="/auth/sign-in" activeStyle={{ color: 'red' }}>
auth
Expand Down
43 changes: 43 additions & 0 deletions admin/src/components/events/events-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import {
eventsSelector,
loadingSelector,
loadedSelector,
fetchEvents
} from '../../ducks/events'

class EventsList extends Component {
componentDidMount() {
this.fetchEvents()
}

fetchEvents = () => {
if (this.props.loading || this.props.loaded) return

this.props.fetchEvents()
}

render() {
if (this.props.loading) return 'Loading...'

return (
<ul>
{this.props.events.map(({ id, title, url, where, when }) => (
<li key={id}>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я б для Event сразу отдельный компонент заводил

<a href={url}>{title}</a> in {where} at {when}
</li>
))}
</ul>
)
}
}

export default connect(
(state) => ({
events: eventsSelector(state),
loading: loadingSelector(state),
loaded: loadedSelector(state)
}),
{ fetchEvents }
)(EventsList)
110 changes: 110 additions & 0 deletions admin/src/ducks/auth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { apply, call, put, take } from 'redux-saga/effects'
import firebase from 'firebase/app'
import reducer, {
ReducerRecord,
signUpSaga,
SIGN_UP_REQUEST,
SIGN_UP_SUCCESS,
SIGN_UP_ERROR,
signInSaga,
SIGN_IN_REQUEST,
SIGN_IN_SUCCESS,
SIGN_IN_ERROR,
SIGN_IN_REQUESTS_LIMIT
} from './auth'

describe('auth duck', () => {
/**
* Reducer
* */
it('should sign in', () => {
const state = ReducerRecord()
const user = {
email: 'test@test.com'
}

const newState = reducer(state, {
type: SIGN_IN_SUCCESS,
payload: { user }
})

expect(newState).toEqual(ReducerRecord({ user }))
})

/**
* Sagas
*/
it('should sign up', () => {
const email = 'test@test.com'
const password = 'password'
const user = {
email
}

const sagaProcess = signUpSaga({
type: SIGN_UP_REQUEST,
payload: { email, password }
})

const auth = firebase.auth()

expect(sagaProcess.next().value).toEqual(
call([auth, auth.createUserWithEmailAndPassword], email, password)
)

expect(sagaProcess.next(user).value).toEqual(
put({ type: SIGN_UP_SUCCESS, payload: { user } })
)

const error = new Error()

expect(sagaProcess.throw(error).value).toEqual(
put({ type: SIGN_UP_ERROR, error })
)
})

it('should sign in', () => {
const email = 'test@test.com'
const password = 'password'
const user = {
email
}

const sagaProcess = signInSaga()

expect(sagaProcess.next().value).toEqual(take(SIGN_IN_REQUEST))

const auth = firebase.auth()

expect(
sagaProcess.next({ type: SIGN_IN_REQUEST, payload: { email, password } })
.value
).toEqual(apply(auth, auth.signInWithEmailAndPassword, [email, password]))

expect(sagaProcess.next(user).value).toEqual(
put({ type: SIGN_IN_SUCCESS, payload: { user } })
)
})

it('should have limit for sign in', () => {
const email = 'test@test.com'
const password = 'password'

const sagaProcess = signInSaga()

for (let i = 0; i < 3; i++) {
sagaProcess.next()
sagaProcess.next({ type: SIGN_IN_REQUEST, payload: { email, password } })

const error = new Error()

expect(sagaProcess.throw(error).value).toEqual(
put({ type: SIGN_IN_ERROR, error })
)
}

expect(sagaProcess.next().value).toEqual(
put({ type: SIGN_IN_REQUESTS_LIMIT })
)
})
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ок, но можно бы и reducer потестить

})
98 changes: 98 additions & 0 deletions admin/src/ducks/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { appName } from '../config'
import { Record, OrderedMap } from 'immutable'
import firebase from 'firebase/app'
import { createSelector } from 'reselect'
import { takeEvery, all, put, call } from 'redux-saga/effects'
import { objectToOrderedMap } from './utils'

/**
* Constants
* */
export const moduleName = 'events'
const prefix = `${appName}/${moduleName}`
export const FETCH_EVENTS_REQUEST = `${prefix}/FETCH_EVENTS_REQUEST`
export const FETCH_EVENTS_SUCCESS = `${prefix}/FETCH_EVENTS_SUCCESS`
export const FETCH_EVENTS_ERROR = `${prefix}/FETCH_EVENTS_ERROR`

/**
* Reducer
* */
const ReducerState = Record({
entities: OrderedMap(),
loading: false,
loaded: false
})

const EventRecord = Record({
id: null,
title: null,
url: null,
where: null,
when: null,
month: null,
submissionDeadline: null
})

export default function reducer(state = ReducerState(), action) {
const { type, payload } = action

switch (type) {
case FETCH_EVENTS_REQUEST:
return state.set('loading', true)

case FETCH_EVENTS_SUCCESS:
return state
.set('loading', false)
.set('loaded', true)
.set('entities', objectToOrderedMap(payload, EventRecord))

case FETCH_EVENTS_ERROR:
return state.set('loading', false)

default:
return state
}
}

/**
* Selectors
* */
export const stateSelector = (state) => state[moduleName]
export const loadingSelector = createSelector(
stateSelector,
(state) => state.loading
)
export const loadedSelector = createSelector(
stateSelector,
(state) => state.loaded
)
export const eventsSelector = createSelector(stateSelector, (state) =>
state.entities.valueSeq().toArray()
)

/**
* Action Creators
* */
export function fetchEvents() {
return {
type: FETCH_EVENTS_REQUEST
}
}

/**
* Sagas
* */
export function* fetchEventsSaga() {
const ref = firebase.database().ref('/events')

try {
const snapshot = yield call([ref, ref.once], 'value')
yield put({ type: FETCH_EVENTS_SUCCESS, payload: snapshot.val() })
} catch (error) {
yield put({ type: FETCH_EVENTS_ERROR, payload: error })
}
}

export function* saga() {
yield all([takeEvery(FETCH_EVENTS_REQUEST, fetchEventsSaga)])
}
8 changes: 8 additions & 0 deletions admin/src/ducks/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { OrderedMap } from 'immutable'

export function generateId() {
return Date.now() + Math.random()
}

export const objectToOrderedMap = (obj, ItemRecord) =>
Object.entries(obj).reduce(
(acc, [id, value]) => acc.set(id, ItemRecord({ id, ...value })),
OrderedMap()
)
4 changes: 3 additions & 1 deletion admin/src/redux/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { routerReducer as router } from 'react-router-redux'
import { reducer as form } from 'redux-form'
import authReducer, { moduleName as authModule } from '../ducks/auth'
import peopleReducer, { moduleName as peopleModule } from '../ducks/people'
import eventsReducer, { moduleName as eventsModule } from '../ducks/events'

export default combineReducers({
router,
form,
[authModule]: authReducer,
[peopleModule]: peopleReducer
[peopleModule]: peopleReducer,
[eventsModule]: eventsReducer
})
3 changes: 2 additions & 1 deletion admin/src/redux/saga.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { all } from 'redux-saga/effects'
import { saga as authSaga } from '../ducks/auth'
import { saga as peopleSaga } from '../ducks/people'
import { saga as eventsSaga } from '../ducks/events'

export default function*() {
yield all([authSaga(), peopleSaga()])
yield all([authSaga(), peopleSaga(), eventsSaga()])
}
2 changes: 2 additions & 0 deletions admin/src/routes/admin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { Component } from 'react'
import { Route } from 'react-router-dom'
import PersonPage from './person-page'
import EventsPage from './events-page'

class AdminPage extends Component {
static propTypes = {}
Expand All @@ -10,6 +11,7 @@ class AdminPage extends Component {
<div>
<h1>Admin Page</h1>
<Route path="/admin/people" component={PersonPage} />
<Route path="/admin/events" component={EventsPage} />
</div>
)
}
Expand Down
15 changes: 15 additions & 0 deletions admin/src/routes/events-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { Component } from 'react'
import EventsList from '../components/events/events-list'

class EventsPage extends Component {
render() {
return (
<div>
<h2>Events</h2>
<EventsList />
</div>
)
}
}

export default EventsPage