From de0b7f7d959967d6823af1408d3faa7556a815f6 Mon Sep 17 00:00:00 2001 From: Oleksii Kutas Date: Thu, 9 Feb 2017 13:22:50 +0200 Subject: [PATCH] done --- src/db.js => db.js | 2 +- index.js | 16 ++++++++-- package.json | 10 ++---- src/actions.js | 13 ++++++-- src/app.js | 12 ++++--- src/components.jsx | 21 ++++++++++--- src/containers.js | 14 +++++++-- src/reducers/index.js | 2 ++ src/reducers/userData.js | 31 ++++++++++++++++++ src/root.jsx | 30 ++++++++++++++++-- src/sagas.js | 32 +++++++++++++++++-- src/services.js | 27 ++++++++++++++++ src/style.scss | 29 +++++++++++++++-- src/wsSaga.js | 68 ++++++++++++++++++++++++++++++++++++++++ webpack.config.js | 14 ++++++--- 15 files changed, 284 insertions(+), 37 deletions(-) rename src/db.js => db.js (94%) create mode 100644 src/reducers/userData.js create mode 100644 src/wsSaga.js diff --git a/src/db.js b/db.js similarity index 94% rename from src/db.js rename to db.js index 768b762..f49dbdc 100644 --- a/src/db.js +++ b/db.js @@ -1,4 +1,4 @@ -module.export = { +module.exports = { users: [ { id: 1, diff --git a/index.js b/index.js index 721b81c..f0af115 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ const app = require('express')(); const WebSocket = require('ws'); -const db = require('./src/db'); +const db = require('./db'); const wss = new WebSocket.Server({ port: 2322 }); @@ -15,8 +15,18 @@ wss.on('connection', function connection(ws) { }); app.get('/users', function(req, res) { - // res.send('[{"id": 1, "name": "BOB"}]') - res.json([{id: 1, name: "BOB"}]) + setTimeout( + () => res.json(db.users), + 500 + ) +}) +app.get('/user/:userId', function(req, res) { + const id = req.params.userId; + const userData = db.extendedData[id]; + setTimeout( + () => res.json(userData), + id * 750 + ) }) app.listen(2323); diff --git a/package.json b/package.json index 6099fbf..c2f4d42 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,7 @@ "axios": "^0.15.3", "babel-core": "^6.22.1", "babel-loader": "^6.2.10", - "babel-preset-es2016": "^6.22.0", - "babel-preset-react": "^6.22.0", "express": "^4.14.1", - "jsx-loader": "^0.13.2", "react": "^15.3.2", "react-dom": "^15.4.2", "react-hot-loader": "^3.0.0-beta.6", @@ -23,11 +20,9 @@ "babel-core": "^6.17.0", "babel-loader": "^6.2.5", "babel-polyfill": "^6.22.0", - "babel-preset-es2015": "^6.16.0", - "babel-preset-es2016": "^6.16.0", + "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-0": "^6.22.0", - "babel-preset-stage-2": "^6.22.0", "css-loader": "^0.25.0", "jsx-loader": "^0.13.2", "node-sass": "^3.10.1", @@ -39,7 +34,8 @@ "webpack-dev-server": "^1.16.1" }, "scripts": { - "start": "./node_modules/webpack-dev-server/bin/webpack-dev-server.js --hot --progress --colors --port 8888 --content-base build/" + "server": "node index", + "start": "webpack-dev-server --hot --progress --colors --port 4242 --content-base build/" }, "author": "", "license": "ISC" diff --git a/src/actions.js b/src/actions.js index a41d119..91f02e6 100644 --- a/src/actions.js +++ b/src/actions.js @@ -1,13 +1,22 @@ export default { + INIT: 'INIT', + USER_SELECTED: 'USER_SELECTED', USERS_FETCH_ING: 'USERS_FETCH_ING', USERS_FETCH_SUCCESS: 'USERS_FETCH_SUCCESS', USERS_FETCH_FAIL: 'USERS_FETCH_FAIL', + CANCEL_INFO_FETCH: 'CANCEL_INFO_FETCH', + USER_INFO_ING: 'USER_INFO_ING', USER_INFO_SUCCESS: 'USER_INFO_SUCCESS', USER_INFO_FAIL: 'USER_INFO_FAIL', - RCV_MSG: 'RCV_MSG', - SND_MSG: 'SND_MSG', + WS_OPEN_PENDING: 'WS_OPEN_PENDING', + WS_OPEN_SUCCESS: 'WS_OPEN_SUCCESS', + WS_OPEN_FAIL: 'WS_OPEN_FAIL', + WS_CLOSING: 'WS_CLOSING', + WS_CLOSED: 'WS_CLOSED', + WS_MSG_RECEIVED: 'WS_MSG_RECEIVED', + WS_MSG_SEND: 'WS_MSG_SEND', } diff --git a/src/app.js b/src/app.js index 3adda3a..e7b0bbd 100644 --- a/src/app.js +++ b/src/app.js @@ -1,11 +1,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware, compose } from 'redux'; import { Provider } from 'react-redux'; import createSagaMiddleware from 'redux-saga'; import reducer from './reducers/index' import Root from './root.jsx'; import rootSaga from './sagas'; +import wsSaga from './wsSaga'; import './style.scss'; const sagaMiddleware = createSagaMiddleware(); @@ -23,9 +24,12 @@ const store = createStore( ), ), ); -sagaMiddleware.run(rootSaga); - - +sagaMiddleware.run(function* (){ + yield [ + rootSaga(), + // wsSaga(), + ] +}); ReactDOM.render( ( diff --git a/src/components.jsx b/src/components.jsx index df837c5..f383481 100644 --- a/src/components.jsx +++ b/src/components.jsx @@ -2,13 +2,26 @@ import React from 'react'; const User = ({ user, onClick, isSelected }) => (
onClick(user)} > {user.name}
); - -export const Table = ({ users, onClickUser }) => (
- {users.map((user) =>
)} +export const UserData = ({ userName, age, loading }) => ( +
+
Full Name: {userName}
+
Age: {age}
+
+); +export const Table = ({ users, onClickUser, selected }) => ( +
+ {users.map((user) => + + )}
); diff --git a/src/containers.js b/src/containers.js index 7aaf978..eef1ec3 100644 --- a/src/containers.js +++ b/src/containers.js @@ -2,15 +2,23 @@ import { connect } from 'react-redux'; import actions from './actions'; import { Table, -} from './components'; + UserData, +} from './components.jsx'; export const TableConnected = connect( (state) => ({ users: state.users.data, + selected: state.users.selectedUser, }), (dispatch) => ({ - onClick: (user) => dispatch({ type: actions.USER_SELECTED, payload: user }), + onClickUser: (user) => dispatch({ type: actions.USER_SELECTED, payload: user }), }) )(Table); - +export const UserDataConnected = connect( + (state) => ({ + loading: state.userData.loading, + userName: state.userData.data.fullName, + age: state.userData.data.age, + }) +)(UserData) diff --git a/src/reducers/index.js b/src/reducers/index.js index c1f95b8..d266c07 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,8 +1,10 @@ import { combineReducers } from 'redux'; import users from './users'; +import userData from './userData'; export default combineReducers({ + userData, users, }) diff --git a/src/reducers/userData.js b/src/reducers/userData.js new file mode 100644 index 0000000..d0cd80a --- /dev/null +++ b/src/reducers/userData.js @@ -0,0 +1,31 @@ +import actions from '../actions'; + +const initState = { + data: {}, + loading: false, + error: null, +} + +export default function(state = initState, action) { + switch (action.type) { + case actions.USER_INFO_ING: + return { + ...initState, + loading: true, + } + case actions.USER_INFO_SUCCESS: + return { + ...state, + loading: false, + data: action.payload, + } + case actions.USER_INFO_FAIL: + return { + ...state, + loading: false, + error: action.error, + } + default: + return state; + } +} diff --git a/src/root.jsx b/src/root.jsx index 373e688..ebfc6ca 100644 --- a/src/root.jsx +++ b/src/root.jsx @@ -1,6 +1,30 @@ import React from 'react'; +import { connect } from 'react-redux'; +import actions from './actions'; import { - Table, -} from './components.jsx'; + TableConnected, + UserDataConnected, +} from './containers'; -export default function() { return (
app
); }; \ No newline at end of file +class Root extends React.Component { + componentDidMount() { + this.props.getUsers(); + // this.props.init(); + } + render() { + return ( +
+ + + {/**/} +
); + } +}; + +export default connect(null, + (dispatch) => ({ + getUsers: () => dispatch({ type: actions.USERS_FETCH_ING }), + // init: () => dispatch({ type: actions.INIT }), + // cancelRequest: () => dispatch({ type: actions.CANCEL_INFO_FETCH }), + }) +)(Root); diff --git a/src/sagas.js b/src/sagas.js index 43737a3..b49910a 100644 --- a/src/sagas.js +++ b/src/sagas.js @@ -1,7 +1,13 @@ import { takeLatest, takeEvery } from 'redux-saga'; -import { put, select, call } from 'redux-saga/effects'; +import { put, select, call, take, race } from 'redux-saga/effects'; import api from './services'; import actions from './actions'; +const WS_HOST = 'ws://localhost:2322' + +// function* init() { +// yield put({ type: actions.USERS_FETCH_ING }); +// yield put({ type: actions.WS_OPEN_PENDING, payload: WS_HOST}) +// } function* getUsers(action) { try { @@ -12,10 +18,32 @@ function* getUsers(action) { } } +function* selectUser(action) { + yield put({ type: actions.USER_INFO_ING, payload: action.payload.id }) +} + +function* getUserInfo(action) { + try { + const userInfo = yield call(api.getUserById, action.payload); + yield put({ type: actions.USER_INFO_SUCCESS, payload: userInfo.data }) + } catch (error) { + yield put({ type: actions.USER_INFO_FAIL, error}) + } +} +// function* getInfoCancellable(action) { +// const { cancel, fetch } = yield race({ +// fetch: getUserInfo(action), +// cancel: take(actions.CANCEL_INFO_FETCH), +// }) +// } export default function* () { yield [ - takeEvery(actions.USERS_FETCH_ING, getUsers) + takeEvery(actions.USERS_FETCH_ING, getUsers), + takeEvery(actions.USER_INFO_ING, getUserInfo), + takeEvery(actions.USER_SELECTED, selectUser), + // takeEvery(actions.USER_INFO_ING, getInfoCancellable), + // takeLatest(actions.INIT, init), ] } \ No newline at end of file diff --git a/src/services.js b/src/services.js index 8fc0f2e..f368136 100644 --- a/src/services.js +++ b/src/services.js @@ -7,4 +7,31 @@ export default { getUsers() { return axios.get('/users'); }, + getUserById(id) { + return axios.get(`/user/${id}`); + }, + ws: { + open(hostname) { + const ws = new WebSocket(hostname); + + return new Promise((resolve, reject) => { + ws.onopen = () => resolve(ws); + ws.onerror = (err) => { + reject(new Error(err)); + }; + }); + }, + onReceive(ws, cb) { + ws.onmessage = cb; + }, + onClose(ws, cb) { + ws.onclose = cb; + }, + send(ws, msg) { + ws.send(msg); + }, + close(ws) { + ws.close(); + }, + }, } \ No newline at end of file diff --git a/src/style.scss b/src/style.scss index 2028902..510bf04 100644 --- a/src/style.scss +++ b/src/style.scss @@ -1,11 +1,34 @@ .users { + margin: auto; display: flex; + font-family: OpenSans, sans-serif; + font-size: 40px; + font-weight: 400; } .user { - padding: 10px; + padding: 10px 20px; cursor: pointer; - .selected { - background-color: #AAA; + &.selected { + background-color: #AAC366; + } +} +.userdata { + font-size: 40px; + font-family: OpenSans, sans-serif; + &.loading { + background-color: #ccc; + } + border: 1px solid #AAC366; + .name { + padding: 10px 5px; } + .age { + padding: 5px; + } +} +button { + font-size: 40px; + padding: 10px; + margin: 30px auto; } \ No newline at end of file diff --git a/src/wsSaga.js b/src/wsSaga.js new file mode 100644 index 0000000..4cb29c7 --- /dev/null +++ b/src/wsSaga.js @@ -0,0 +1,68 @@ +import { takeLatest, takeEvery, eventChannel } from 'redux-saga'; +import { call, put, spawn, take, cancel, fork } from 'redux-saga/effects'; +import actions from './actions'; +import api from './services'; + +const { ws: wsApi } = api; +const { + WS_OPEN_PENDING, + WS_OPEN_SUCCESS, + WS_OPEN_FAIL, + WS_CLOSING, + WS_CLOSED, + WS_MSG_RECEIVED, + WS_MSG_SEND, +} = actions; + +function createSender(ws) { + return function* sendMsg(action) { + yield call(wsApi.send, ws, action.payload); + }; +} + +function createListener(chan) { + return function* listen() { + while (true) { + const msg = yield take(chan); + yield put(msg); + } + }; +} + +function* waitForClose(ws, senderTask, listener) { + yield take([WS_CLOSING, WS_CLOSED]); + yield cancel(senderTask); + yield cancel(listener); +} + +const createSocketEmitter = ws => (emit) => { + wsApi.onReceive(ws, msg => + emit({ type: WS_MSG_RECEIVED, payload: msg.data })); + wsApi.onClose(ws, reason => + emit({ type: WS_CLOSED, payload: reason })); + return () => wsApi.close(ws); +}; + +function* openConn(action) { + try { + // init ws + const ws = yield call(wsApi.open, action.payload); + yield put({ type: WS_OPEN_SUCCESS }); + // create a channel to get msgs from ws conn + const chan = eventChannel(createSocketEmitter(ws)); + // sender has a task interface + const sender = yield takeEvery(WS_MSG_SEND, createSender(ws)); + // listener is a task too + const listener = yield fork(createListener(chan)); + // we do not want to know what will happen + yield spawn(waitForClose, ws, sender, listener); + } catch (error) { + yield put({ type: WS_OPEN_FAIL, error }); + } +} + +export default function () { + return [ + takeLatest(WS_OPEN_PENDING, openConn), + ]; +} diff --git a/webpack.config.js b/webpack.config.js index bc24f25..36d80b3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,15 +4,19 @@ module.exports = { entry: { main: [ 'babel-polyfill', - 'webpack-dev-server/client?http://localhost:8888', // WebpackDevServer host and port + 'webpack-dev-server/client?http://localhost:4242', // WebpackDevServer host and port 'webpack/hot/only-dev-server', './src/app.js' ] }, - proxy: { - "/api/*": { - target: "http://localhost:2323", - pathRewrite: {"^/api" : ""} + devServer: { + proxy: { + "/api/*": { + target: "http://localhost:2323", + pathRewrite: { + '/api': '', + } + } } }, output: {