From f717222fde389eb72f529aa5df5a778d95a2b0c6 Mon Sep 17 00:00:00 2001 From: Justkant Date: Mon, 2 Nov 2015 15:32:13 +0800 Subject: [PATCH 1/9] Cart first design --- api/api.js | 9 +++++++- api/functions/users.js | 17 ++++++++++++++- src/components/Navbar/Navbar.js | 11 +++++++++- src/components/Navbar/Navbar.styl | 36 +++++++++++++++++++++++++++++++ src/containers/Cart/Cart.js | 30 ++++++++++++++++++++++++++ src/containers/Cart/Cart.styl | 28 ++++++++++++++++++++++++ src/containers/Orders/Orders.js | 2 +- src/containers/Orders/Orders.styl | 8 +------ src/containers/Panel/Panel.js | 2 +- src/containers/Panel/Panel.styl | 3 +++ src/containers/index.js | 1 + src/routes.js | 4 +++- 12 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 src/containers/Cart/Cart.js create mode 100644 src/containers/Cart/Cart.styl diff --git a/api/api.js b/api/api.js index c48d39c..08e4b96 100644 --- a/api/api.js +++ b/api/api.js @@ -41,6 +41,13 @@ app.route('/users/:id') .put(users.auth, users.isOwner, users.updateUser) .delete(users.auth, users.isOwner, users.deleteUser); +app.route('/users/:id/cart') + .get(users.auth, users.getUserCart) + .post(users.auth, users.addUserProduct); + +app.route('/users/:id/orders') + .get(users.auth, users.getUserOrders); + app.route('/products') .get(users.auth, users.isAdmin, products.getProducts) .post(users.auth, users.isAdmin, products.addProduct); @@ -52,7 +59,7 @@ app.route('/products/:id') app.get('/market', users.auth, products.getMarket); -app.get('/search/:text', products.search); +app.get('/search/:text', users.auth, products.search); app.post('/picture', users.auth, upload.single('picture'), (req, res) => { res.json({url: req.file.path}); diff --git a/api/functions/users.js b/api/functions/users.js index bdec1a6..f577fbf 100644 --- a/api/functions/users.js +++ b/api/functions/users.js @@ -249,6 +249,18 @@ function isAdmin(req, res, next) { } } +function getUserCart(req, res) { + res.json(req.user.cart); +} + +function addUserProduct(req, res) { + res.json(req.body); +} + +function getUserOrders(req, res) { + res.json(req.user.Orders); +} + const users = { auth: auth, load: load, @@ -260,7 +272,10 @@ const users = { updateUser: updateUser, deleteUser: deleteUser, isOwner: isOwner, - isAdmin: isAdmin + isAdmin: isAdmin, + getUserCart: getUserCart, + addUserProduct: addUserProduct, + getUserOrders: getUserOrders }; export default users; diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js index 8069599..49435d9 100644 --- a/src/components/Navbar/Navbar.js +++ b/src/components/Navbar/Navbar.js @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { IndexLink } from 'react-router'; +import { IndexLink, Link } from 'react-router'; import { connect } from 'react-redux'; import { DropDownButton } from 'components'; import { logout } from 'redux/modules/auth'; @@ -37,6 +37,15 @@ export default class Navbar extends React.Component {
+
+ Cart +
+

Nike shoes

+

Nike shoes long long text

+

Nike shoes

+

Nike shoes

+
+
); } diff --git a/src/components/Navbar/Navbar.styl b/src/components/Navbar/Navbar.styl index ea9af42..3ec2a1d 100644 --- a/src/components/Navbar/Navbar.styl +++ b/src/components/Navbar/Navbar.styl @@ -71,3 +71,39 @@ height: 1px; background-image: linear-gradient(to right,transparent 0,rgba(0,0,0,.07) 15%,rgba(0,0,0,.1) 50%,rgba(0,0,0,.1) 85%,rgba(0,0,0,.1) 100%); } + +.cartContainer { + display: flex; + flex-direction: column; + padding: 0 25px; +} + +.cartTitle { + font-size: 14px; + margin: 10px; + text-decoration: none; + color: white; + + &:hover { + color: #dadada; + } +} + +.productList { + div { + border-radius: 4px; + padding: 10px 20px; + cursor: pointer; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } + + p { + margin: 0; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } +} diff --git a/src/containers/Cart/Cart.js b/src/containers/Cart/Cart.js new file mode 100644 index 0000000..d70e159 --- /dev/null +++ b/src/containers/Cart/Cart.js @@ -0,0 +1,30 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { Title } from 'components'; + +@connect(state => ({user: state.auth.user})) +export default class Cart extends Component { + static propTypes = { + user: PropTypes.object + }; + + render() { + const styles = require('./Cart.styl'); + const {user} = this.props; + const array = []; + for (let index = 0; index < 50; index++) { + array.push('Product of ' + user.username); + } + + return ( +
+ + <div className={styles.productContainer}> + {array.map((value, index) => { + return (<div className={styles.element} key={value + index}><h4>{value}</h4></div>); + })} + </div> + </div> + ); + } +} diff --git a/src/containers/Cart/Cart.styl b/src/containers/Cart/Cart.styl new file mode 100644 index 0000000..44f442f --- /dev/null +++ b/src/containers/Cart/Cart.styl @@ -0,0 +1,28 @@ +.container { + overflow: auto; + display: flex; + flex-direction: column; + border-bottom: 1px solid #E7E7EC; +} + +.productContainer { + background-color: #f5f5f5; + padding: 20px; + display: flex; + flex-direction: column; + overflow: auto; +} + +.element { + background-color: white; + border-radius: 4px; + margin-bottom: 20px; + box-shadow: 0 1px 2px rgba(0,0,0,.05),0 0 0 1px rgba(63,63,68,.1); + flex-shrink: 0; + + h4 { + margin-left: 10px; + font-size: 16px; + font-weight: 400; + } +} diff --git a/src/containers/Orders/Orders.js b/src/containers/Orders/Orders.js index c71231a..30b1d55 100644 --- a/src/containers/Orders/Orders.js +++ b/src/containers/Orders/Orders.js @@ -11,7 +11,7 @@ export default class Orders extends Component { return ( <div className={styles.container}> {array.map((value, index) => { - return (<div className={styles.element} key={value + index}><h1>{value}</h1></div>); + return (<div className={styles.element} key={value + index}><h4>{value}</h4></div>); })} </div> ); diff --git a/src/containers/Orders/Orders.styl b/src/containers/Orders/Orders.styl index 39bfc09..2dde5da 100644 --- a/src/containers/Orders/Orders.styl +++ b/src/containers/Orders/Orders.styl @@ -13,13 +13,7 @@ box-shadow: 0 1px 2px rgba(0,0,0,.05),0 0 0 1px rgba(63,63,68,.1); flex-shrink: 0; - img { - height: 40px; - width: auto; - border-radius: 50%; - } - - span { + h4 { margin-left: 10px; font-size: 16px; font-weight: 400; diff --git a/src/containers/Panel/Panel.js b/src/containers/Panel/Panel.js index a89f6d9..3bcde9d 100644 --- a/src/containers/Panel/Panel.js +++ b/src/containers/Panel/Panel.js @@ -6,7 +6,7 @@ export default class Panel extends Component { return ( <div className={styles.container}> - <h4>Panel</h4> + <h4>Panel in construction</h4> </div> ); } diff --git a/src/containers/Panel/Panel.styl b/src/containers/Panel/Panel.styl index e69de29..bc4cb86 100644 --- a/src/containers/Panel/Panel.styl +++ b/src/containers/Panel/Panel.styl @@ -0,0 +1,3 @@ +.container { + padding: 20px; +} diff --git a/src/containers/index.js b/src/containers/index.js index ecd30a8..a7abc17 100755 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -11,3 +11,4 @@ export Panel from './Panel/Panel'; export Products from './Products/Products'; export ProfileContainer from './ProfileContainer/ProfileContainer'; export Users from './Users/Users'; +export Cart from './Cart/Cart'; diff --git a/src/routes.js b/src/routes.js index e6c7790..3e69251 100644 --- a/src/routes.js +++ b/src/routes.js @@ -14,7 +14,8 @@ import { Users, Product, Products, - NotFound + NotFound, + Cart } from './containers'; export default function(store) { @@ -73,6 +74,7 @@ export default function(store) { <IndexRoute component={Profile}/> <Route path="orders" component={Orders}/> </Route> + <Route path="cart" component={Cart} onEnter={requireAuth}/> <Route path="product/:id" component={Product} onEnter={requireAuth}/> <Route path="admin" component={Admin} onEnter={requireAdmin}> <IndexRoute component={Panel}/> From 3a5b1b2f5af2527c4de8a9ad381e4def5e748681 Mon Sep 17 00:00:00 2001 From: Justkant <quent92100@gmail.com> Date: Wed, 4 Nov 2015 15:32:52 +0800 Subject: [PATCH 2/9] Finish models for cart & orders --- api/models/Cart.js | 12 ++++++++++++ api/models/Order.js | 14 ++++++++++++++ api/models/Product.js | 4 ++++ api/models/User.js | 7 +++++++ package.json | 1 + 5 files changed, 38 insertions(+) create mode 100644 api/models/Cart.js create mode 100644 api/models/Order.js diff --git a/api/models/Cart.js b/api/models/Cart.js new file mode 100644 index 0000000..80efc2e --- /dev/null +++ b/api/models/Cart.js @@ -0,0 +1,12 @@ +import thinky from '../utils/thinky'; +const type = thinky.type; + +const Cart = thinky.createModel('Cart', { + id: type.string(), + nbItem: type.number(), + userId: type.string(), + orderId: type.string(), + productId: type.string() +}); + +export default Cart; diff --git a/api/models/Order.js b/api/models/Order.js new file mode 100644 index 0000000..cd6e455 --- /dev/null +++ b/api/models/Order.js @@ -0,0 +1,14 @@ +import thinky from '../utils/thinky'; +import Cart from './Cart'; +const type = thinky.type; + +const Order = thinky.createModel('Order', { + id: type.string(), + createdAt: type.date().default(thinky.r.now()), + userId: type.string() +}); + +Order.hasMany(Cart, 'cart', 'id', 'orderId'); +Cart.belongsTo(Order, 'order', 'orderId', 'id'); + +export default Order; diff --git a/api/models/Product.js b/api/models/Product.js index ba8e0f5..e8609de 100644 --- a/api/models/Product.js +++ b/api/models/Product.js @@ -1,4 +1,5 @@ import thinky from '../utils/thinky'; +import Cart from './Cart'; const type = thinky.type; const Product = thinky.createModel('Product', { @@ -10,4 +11,7 @@ const Product = thinky.createModel('Product', { createdAt: type.date().default(thinky.r.now()) }); +Product.hasMany(Cart, 'usersCart', 'id', 'productId'); +Cart.belongsTo(Product, 'product', 'productId', 'id'); + export default Product; diff --git a/api/models/User.js b/api/models/User.js index 29dfefa..8919559 100644 --- a/api/models/User.js +++ b/api/models/User.js @@ -1,4 +1,6 @@ import thinky from '../utils/thinky'; +import Cart from './Cart'; +import Order from './Order'; const type = thinky.type; const User = thinky.createModel('User', { @@ -17,4 +19,9 @@ User.define('getPublic', function() { return this; }); +User.hasMany(Cart, 'cart', 'id', 'userId'); +Cart.belongsTo(User, 'user', 'userId', 'id'); +User.hasMany(Order, 'orders', 'id', 'userId'); +Order.belongsTo(User, 'user', 'userId', 'id'); + export default User; diff --git a/package.json b/package.json index ef526d3..0cd7fd3 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "webpack-isomorphic-tools": "2.1.2" }, "devDependencies": { + "apidoc": "^0.13.1", "autoprefixer-stylus": "0.8.1", "babel-core": "5.8.25", "babel-eslint": "4.1.3", From d1eea106e0e5d20356d4656f28641c789ffdaffc Mon Sep 17 00:00:00 2001 From: Justkant <quent92100@gmail.com> Date: Wed, 4 Nov 2015 15:54:55 +0800 Subject: [PATCH 3/9] add user function for cart & orders --- api/api.js | 17 ++++++++++++++--- api/functions/users.js | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/api/api.js b/api/api.js index 08e4b96..616beea 100644 --- a/api/api.js +++ b/api/api.js @@ -42,11 +42,22 @@ app.route('/users/:id') .delete(users.auth, users.isOwner, users.deleteUser); app.route('/users/:id/cart') - .get(users.auth, users.getUserCart) - .post(users.auth, users.addUserProduct); + .get(users.auth, users.isOwner, users.getUserCart) + .post(users.auth, users.isOwner, users.addUserProduct); + +app.route('/users/:id/cart/:cartId') + .get(users.auth, users.isOwner, users.getUserCartItem) + .put(users.auth, users.isOwner, users.updateCartItem) + .delete(users.auth, users.isOwner, users.deleteCartItem); app.route('/users/:id/orders') - .get(users.auth, users.getUserOrders); + .get(users.auth, users.isOwner, users.getUserOrders) + .post(users.auth, users.isOwner, users.validateCart); + +app.route('/users/:id/orders/:orderId') + .get(users.auth, users.isOwner, users.getUserOrder) + .put(users.auth, users.isAdmin, users.updateOrder) + .delete(users.auth, users.isAdmin, users.deleteOrder); app.route('/products') .get(users.auth, users.isAdmin, products.getProducts) diff --git a/api/functions/users.js b/api/functions/users.js index f577fbf..b6e0ccd 100644 --- a/api/functions/users.js +++ b/api/functions/users.js @@ -257,10 +257,38 @@ function addUserProduct(req, res) { res.json(req.body); } +function getUserCartItem(req, res) { + res.json(null); +} + +function updateCartItem(req, res) { + res.json(null); +} + +function deleteCartItem(req, res) { + res.json(null); +} + function getUserOrders(req, res) { res.json(req.user.Orders); } +function validateCart(req, res) { + res.json(null); +} + +function getUserOrder(req, res) { + res.json(null); +} + +function updateOrder(req, res) { + res.json(null); +} + +function deleteOrder(req, res) { + res.json(null); +} + const users = { auth: auth, load: load, @@ -275,7 +303,14 @@ const users = { isAdmin: isAdmin, getUserCart: getUserCart, addUserProduct: addUserProduct, - getUserOrders: getUserOrders + getUserCartItem: getUserCartItem, + updateCartItem: updateCartItem, + deleteCartItem: deleteCartItem, + getUserOrders: getUserOrders, + validateCart: validateCart, + getUserOrder: getUserOrder, + updateOrder: updateOrder, + deleteOrder: deleteOrder }; export default users; From e974b47876f5722b870bfdf4c0a4591cd4def562 Mon Sep 17 00:00:00 2001 From: Justkant <quent92100@gmail.com> Date: Mon, 9 Nov 2015 04:07:23 +0800 Subject: [PATCH 4/9] API cart & orders ready UPDATE: - Styles of Navbar & Cart - Dependencies - JWT token is now using the user id to get it easily - Improve API console errors --- .eslintrc | 1 + api/api.js | 3 +- api/functions/users.js | 281 ++++++++++++++++++++-------- api/models/Cart.js | 2 +- api/models/Order.js | 1 + api/models/index.js | 2 + api/utils/token.js | 4 +- package.json | 30 +-- src/components/Navbar/Navbar.js | 6 +- src/components/Navbar/Navbar.styl | 15 +- src/containers/Cart/Cart.js | 22 ++- src/containers/Cart/Cart.styl | 71 +++++++ webpack/webpack-isomorphic-tools.js | 2 +- 13 files changed, 334 insertions(+), 106 deletions(-) diff --git a/.eslintrc b/.eslintrc index a0616f5..a051370 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,6 +13,7 @@ "import/no-duplicates": 0, "import/named": 0, "import/namespace": 0, + "react/no-multi-comp": 0, "import/no-unresolved": 0, "import/no-named-as-default": 2, "jsx-quotes": 2, diff --git a/api/api.js b/api/api.js index 616beea..8a750ce 100644 --- a/api/api.js +++ b/api/api.js @@ -43,7 +43,8 @@ app.route('/users/:id') app.route('/users/:id/cart') .get(users.auth, users.isOwner, users.getUserCart) - .post(users.auth, users.isOwner, users.addUserProduct); + .post(users.auth, users.isOwner, users.addUserProduct) + .delete(users.auth, users.isOwner, users.deleteUserCart); app.route('/users/:id/cart/:cartId') .get(users.auth, users.isOwner, users.getUserCartItem) diff --git a/api/functions/users.js b/api/functions/users.js index b6e0ccd..fa85809 100644 --- a/api/functions/users.js +++ b/api/functions/users.js @@ -1,4 +1,4 @@ -import { User, Email } from '../models'; +import { User, Email, Product, Cart, Order } from '../models'; import { hash_password, authenticate } from '../utils/auth'; import { generate, verify } from '../utils/token'; import ms from 'ms'; @@ -7,26 +7,25 @@ function checkToken(req, res, cb, error) { const token = req.get('x-api-token') || req.cookies.auth; if (token) { verify(token).then((decoded) => { - User.filter({email: decoded.email}).then((results) => { - if (results.length > 0) { - let user = results[0]; + if (decoded.id) { + User.get(decoded.id).getJoin({orders: {cart: {product: true}}, cart: {product: true}}).then((user) => { if (user.token === token) { cb(user); } else { res.clearCookie('auth'); res.status(error.status).json(error.body); } - } else { + }, (err) => { + console.error(err.message); res.clearCookie('auth'); res.status(error.status).json(error.body); - } - }, (error) => { - console.error(error); + }); + } else { res.clearCookie('auth'); res.status(error.status).json(error.body); - }); - }, (error) => { - console.error(error); + } + }, (err) => { + console.error(err.message); res.clearCookie('auth'); res.status(error.status).json(error.body); }); @@ -53,24 +52,24 @@ function login(req, res) { if (results.length > 0) { let user = results[0]; authenticate(req.body.password, user.password).then(() => { - user.token = generate(user.email); + user.token = generate(user.id); user.save().then(() => { res.cookie('auth', user.token, {maxAge: ms('7 days')}); res.json(user.getPublic()); }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); + console.error(error.message); + res.status(500).json({msg: 'Contact an administrator', err: error.message}); }); }, (error) => { - console.error(error); - res.status(400).json({msg: 'Bad password', err: error}); + console.error(error.message); + res.status(400).json({msg: 'Bad password', err: error.message}); }); } else { res.status(404).json({msg: 'No user with this email'}); } }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); + console.error(error.message); + res.status(500).json({msg: 'Contact an administrator', err: error.message}); }); } @@ -80,10 +79,10 @@ function logout(req, res) { } /** - * @api {get} /users Request All Users - * @apiName GetUsers - * @apiGroup User - */ +* @api {get} /users Request All Users +* @apiName GetUsers +* @apiGroup User +*/ function getUsers(req, res) { User.orderBy('-createdAt').run().then((result) => { res.json(result); @@ -91,29 +90,29 @@ function getUsers(req, res) { } /** - * @api {get} /users/:id Request User Information - * @apiName GetUser - * @apiGroup User - * - * @apiParam {Number} id Users unique ID. - * - * @apiSuccess {String} username The users name. - * @apiSuccess {String} email The users email. - * @apiSuccess {String} token The users token. - * @apiSuccess {String} pictureUrl The users picture url. - * @apiSuccess {Boolean} admin The users right. - * @apiSuccess {Date} createdAt The users creation date. - * - * @apiSuccessExample Example data on success: - * { - * username: 'Kant', - * email: 'Kant@gmail.com', - * token: 'IOEJVofz@fohinsmov24azd5niermogunqeprofinzqoe8297', - * pictureUrl: 'uploads/picture-94305067460.png', - * admin: true, - * createdAt: Wed Oct 21 2015 14:33:53 GMT+00:00 - * } - */ +* @api {get} /users/:id Request User Information +* @apiName GetUser +* @apiGroup User +* +* @apiParam {Number} id Users unique ID. +* +* @apiSuccess {String} username The users name. +* @apiSuccess {String} email The users email. +* @apiSuccess {String} token The users token. +* @apiSuccess {String} pictureUrl The users picture url. +* @apiSuccess {Boolean} admin The users right. +* @apiSuccess {Date} createdAt The users creation date. +* +* @apiSuccessExample Example data on success: +* { +* username: 'Kant', +* email: 'Kant@gmail.com', +* token: 'IOEJVofz@fohinsmov24azd5niermogunqeprofinzqoe8297', +* pictureUrl: 'uploads/picture-94305067460.png', +* admin: true, +* createdAt: Wed Oct 21 2015 14:33:53 GMT+00:00 +* } +*/ function getUser(req, res) { res.json(req.user.getPublic()); } @@ -128,31 +127,36 @@ function addUser(req, res) { const user = new User({ username: req.body.username, email: req.body.email, - password: password, - token: generate(req.body.email) + password: password }); user.save().then(() => { email.done = true; email.save().then(() => { - res.cookie('auth', user.token, {maxAge: ms('7 days')}); - res.json(user); + user.token = generate(user.id); + user.save().then(() => { + res.cookie('auth', user.token, {maxAge: ms('7 days')}); + res.json(user); + }, (error) => { + console.error(error.message); + res.status(500).json({msg: 'Contact an administrator', err: error.message}); + }) }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); + console.error(error.message); + res.status(500).json({msg: 'Contact an administrator', err: error.message}); }); }, (error) => { email.delete(); - console.error(error); - res.status(400).json({msg: 'Something went wrong', err: error}); + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); }); }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); + console.error(error.message); + res.status(500).json({msg: 'Contact an administrator', err: error.message}); }); }, (error) => { - console.error(error); - res.status(409).json({msg: 'Email already exist', err: error}); + console.error(error.message); + res.status(409).json({msg: 'Email already exist', err: error.message}); }); } @@ -170,17 +174,15 @@ function updateUser(req, res) { email.save().then(() => { Email.get(req.user.email).delete().then(() => { user.email = req.body.email; - user.token = generate(user.email); - res.cookie('auth', user.token, {maxAge: ms('7 days')}); resolve(); }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); + console.error(error.message); + res.status(500).json({msg: 'Contact an administrator', err: error.message}); reject(); }); }, (error) => { - console.error(error); - res.status(409).json({msg: 'Email already exist', err: error}); + console.error(error.message); + res.status(409).json({msg: 'Email already exist', err: error.message}); reject(); }); })); @@ -192,13 +194,13 @@ function updateUser(req, res) { user.password = password; resolve(); }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); + console.error(error.message); + res.status(500).json({msg: 'Contact an administrator', err: error.message}); reject(); }); }, (error) => { - console.error(error); - res.status(400).json({msg: 'Bad password', err: error}); + console.error(error.message); + res.status(400).json({msg: 'Bad password', err: error.message}); reject(); }); })); @@ -213,23 +215,23 @@ function updateUser(req, res) { req.user.merge(user).save().then((result) => { res.json(req.user.getPublic()); }, (error) => { - console.error(error); - res.status(400).json({msg: 'Something went wrong', err: error}); + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); }); }); } function deleteUser(req, res) { - req.user.delete().then(() => { + req.user.deleteAll({cart: true, orders: {cart: true}}).then(() => { Email.get(req.user.email).delete().then(() => { res.json({msg: 'Account deleted'}); }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); + console.error(error.message); + res.status(500).json({msg: 'Contact an administrator', err: error.message}); }); }, (error) => { - console.error(error); - res.status(400).json({msg: 'Something went wrong', err: error}); + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); }); } @@ -254,39 +256,151 @@ function getUserCart(req, res) { } function addUserProduct(req, res) { - res.json(req.body); + Product.get(req.body.productId).run().then((product) => { + const cart = new Cart({ + id: product.id + '-' + req.user.id, + nbItem: req.body.nbItem, + product: product, + userId: req.user.id + }); + + cart.saveAll().then((result) => { + res.json(result); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); + }, (error) => { + console.error(error.message); + res.status(404).json({msg: 'Product not found', err: error.message}); + }); +} + +function deleteCart(cart) { + const deletes = [], promises = []; + + cart.forEach((cartItem) => { + deletes.push(cartItem.delete.bind(cartItem)); + }); + + deletes.forEach((del) => { + promises.push(del()); + }); + + return Promise.all(promises); +} + +function deleteUserCart(req, res) { + deleteCart(req.user.cart).then(() => { + res.json({msg: 'Cart deleted'}); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }) } function getUserCartItem(req, res) { - res.json(null); + Cart.get(req.params.cartId).getJoin().run().then((cartItem) => { + res.json(cartItem); + }, (error) => { + console.error(error.message); + res.status(404).json({msg: 'Cart Item not found', err: error.message}); + }); } function updateCartItem(req, res) { - res.json(null); + Cart.get(req.params.cartId).getJoin().run().then((cartItem) => { + cartItem.nbItem = req.body.nbItem; + cartItem.save().then(() => { + res.json(cartItem); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); + }, (error) => { + console.error(error.message); + res.status(404).json({msg: 'Cart Item not found', err: error.message}); + }); } function deleteCartItem(req, res) { - res.json(null); + Cart.get(req.params.cartId).run().then((cartItem) => { + cartItem.delete().then(() => { + res.json({msg: 'Cart item deleted'}); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); + }, (error) => { + console.error(error.message); + res.status(404).json({msg: 'Cart Item not found', err: error.message}); + }); } function getUserOrders(req, res) { - res.json(req.user.Orders); + res.json(req.user.orders); } function validateCart(req, res) { - res.json(null); + (new Order({ + userId: req.user.id + })).save().then((result) => { + req.user.cart.forEach((cartItem) => { + (new Cart({ + nbItem: cartItem.nbItem, + productId: cartItem.productId, + orderId: result.id + })).save(); + }); + + deleteCart(req.user.cart).then(() => { + res.json(result); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); } function getUserOrder(req, res) { - res.json(null); + Order.get(req.params.orderId).getJoin().run().then((order) => { + res.json(order); + }, (error) => { + console.error(error.message); + res.status(404).json({msg: 'Order not found', err: error.message}); + }); } function updateOrder(req, res) { - res.json(null); + Order.get(req.params.orderId).getJoin().run().then((order) => { + order.status = req.body.status; + order.save().then(() => { + res.json(order); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); + }, (error) => { + console.error(error.message); + res.status(404).json({msg: 'Order not found', err: error.message}); + }); } function deleteOrder(req, res) { - res.json(null); + Order.get(req.params.orderId).getJoin().run().then((order) => { + order.deleteAll({cart: true}).then(() => { + res.json({msg: 'Order deleted'}); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); + }, (error) => { + console.error(error.message); + res.status(404).json({msg: 'Order not found', err: error.message}); + }); } const users = { @@ -303,6 +417,7 @@ const users = { isAdmin: isAdmin, getUserCart: getUserCart, addUserProduct: addUserProduct, + deleteUserCart: deleteUserCart, getUserCartItem: getUserCartItem, updateCartItem: updateCartItem, deleteCartItem: deleteCartItem, diff --git a/api/models/Cart.js b/api/models/Cart.js index 80efc2e..8016925 100644 --- a/api/models/Cart.js +++ b/api/models/Cart.js @@ -3,7 +3,7 @@ const type = thinky.type; const Cart = thinky.createModel('Cart', { id: type.string(), - nbItem: type.number(), + nbItem: type.number().default(1), userId: type.string(), orderId: type.string(), productId: type.string() diff --git a/api/models/Order.js b/api/models/Order.js index cd6e455..a2f2407 100644 --- a/api/models/Order.js +++ b/api/models/Order.js @@ -5,6 +5,7 @@ const type = thinky.type; const Order = thinky.createModel('Order', { id: type.string(), createdAt: type.date().default(thinky.r.now()), + status: type.string().default('validating'), userId: type.string() }); diff --git a/api/models/index.js b/api/models/index.js index 6ab6ea6..53c2b4f 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,3 +1,5 @@ export User from './User'; export Product from './Product'; export Email from './Email'; +export Cart from './Cart'; +export Order from './Order'; diff --git a/api/utils/token.js b/api/utils/token.js index a86428d..73d389a 100644 --- a/api/utils/token.js +++ b/api/utils/token.js @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import config from '../../src/config'; -export function generate(email) { - return jwt.sign({email: email}, config.secret, { expiresIn: '7 days' }); +export function generate(id) { + return jwt.sign({id: id}, config.secret, { expiresIn: '7 days' }); } export function verify(token) { diff --git a/package.json b/package.json index 0cd7fd3..370e1d4 100644 --- a/package.json +++ b/package.json @@ -109,26 +109,26 @@ "piping": "0.3.0", "pretty-error": "1.2.0", "query-string": "3.0.0", - "react": "^0.14.1", + "react": "^0.14.2", "react-document-meta": "2.0.0", - "react-dom": "0.14.1", + "react-dom": "0.14.2", "react-redux": "4.0.0", - "react-router": "1.0.0-rc3", + "react-router": "1.0.0-rc4", "redux": "3.0.4", - "redux-router": "^1.0.0-beta3", + "redux-router": "^1.0.0-beta4", "serialize-javascript": "1.1.2", "serve-favicon": "2.3.0", "serve-static": "1.10.0", "superagent": "1.4.0", "thinky": "^2.1.11", "url-loader": "0.5.6", - "webpack-isomorphic-tools": "2.1.2" + "webpack-isomorphic-tools": "2.2.11" }, "devDependencies": { "apidoc": "^0.13.1", "autoprefixer-stylus": "0.8.1", "babel-core": "5.8.25", - "babel-eslint": "4.1.3", + "babel-eslint": "4.1.4", "babel-loader": "5.3.2", "babel-plugin-react-transform": "1.1.1", "babel-runtime": "6.0.12", @@ -136,15 +136,15 @@ "chai": "3.4.0", "clean-webpack-plugin": "0.1.4", "concurrently": "0.1.1", - "css-loader": "0.21.0", - "eslint": "1.8.0", + "css-loader": "0.22.0", + "eslint": "1.9.0", "eslint-config-airbnb": "0.1.0", "eslint-loader": "1.1.1", - "eslint-plugin-import": "^0.8.1", - "eslint-plugin-react": "3.6.3", - "extract-text-webpack-plugin": "^0.8.2", + "eslint-plugin-import": "^0.9.1", + "eslint-plugin-react": "3.7.1", + "extract-text-webpack-plugin": "^0.9.1", "json-loader": "0.5.3", - "karma": "0.13.14", + "karma": "0.13.15", "karma-chrome-launcher": "0.2.1", "karma-cli": "0.1.1", "karma-firefox-launcher": "0.1.6", @@ -154,15 +154,15 @@ "karma-webpack": "1.7.0", "mocha": "2.3.3", "react-a11y": "0.2.8", - "react-addons-test-utils": "0.14.1", + "react-addons-test-utils": "0.14.2", "react-transform-catch-errors": "^1.0.0", "react-transform-hmr": "1.0.1", "redbox-react": "^1.1.1", "redux-devtools": "2.1.5", "strip-loader": "^0.1.0", "style-loader": "0.13.0", - "stylus-loader": "1.4.0", - "webpack": "1.12.2", + "stylus-loader": "1.4.2", + "webpack": "1.12.3", "webpack-dev-middleware": "1.2.0", "webpack-hot-middleware": "2.4.1" }, diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js index 49435d9..25aeddd 100644 --- a/src/components/Navbar/Navbar.js +++ b/src/components/Navbar/Navbar.js @@ -38,7 +38,11 @@ export default class Navbar extends React.Component { <DropDownButton infos={infos} links={menuLinks}/> </div> <div className={styles.cartContainer}> - <Link to="/cart" className={styles.cartTitle}>Cart</Link> + <Link to="/cart" className={styles.cartTitle}> + <i className="material-icons md-18">shopping_cart</i> + <span className={styles.cartTitleText}>Cart</span> + <span>38 $</span> + </Link> <div className={styles.productList}> <div><p>Nike shoes</p></div> <div><p>Nike shoes long long text</p></div> diff --git a/src/components/Navbar/Navbar.styl b/src/components/Navbar/Navbar.styl index 3ec2a1d..01ef1cb 100644 --- a/src/components/Navbar/Navbar.styl +++ b/src/components/Navbar/Navbar.styl @@ -78,15 +78,28 @@ padding: 0 25px; } +.cartTitleContainer { + display: flex; + align-items: center; +} + .cartTitle { font-size: 14px; margin: 10px; text-decoration: none; + display: flex; + flex: 1; + align-items: center; color: white; &:hover { color: #dadada; } + + .cartTitleText { + padding-left: 5px; + flex: 1; + } } .productList { @@ -96,7 +109,7 @@ cursor: pointer; &:hover { - background: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.1); } p { diff --git a/src/containers/Cart/Cart.js b/src/containers/Cart/Cart.js index d70e159..707c779 100644 --- a/src/containers/Cart/Cart.js +++ b/src/containers/Cart/Cart.js @@ -21,7 +21,27 @@ export default class Cart extends Component { <Title title="Cart"/> <div className={styles.productContainer}> {array.map((value, index) => { - return (<div className={styles.element} key={value + index}><h4>{value}</h4></div>); + return ( + <div className={styles.element} key={value + index}> + <div className={styles.imageContainer}> + <img src="/product.jpg" className={styles.image}/> + </div> + <div className={styles.infoContainer}> + <p>{value}</p> + <p>Price</p> + <div className={styles.buttonContainer}> + <div className={styles.inputNumber}> + <button><i className="material-icons">remove</i></button> + <input className={styles.number} defaultValue="1"/> + <button><i className="material-icons">add</i></button> + </div> + <button className={styles.deleteButton}> + <i className="material-icons">delete</i> + </button> + </div> + </div> + </div> + ); })} </div> </div> diff --git a/src/containers/Cart/Cart.styl b/src/containers/Cart/Cart.styl index 44f442f..501706c 100644 --- a/src/containers/Cart/Cart.styl +++ b/src/containers/Cart/Cart.styl @@ -19,6 +19,16 @@ margin-bottom: 20px; box-shadow: 0 1px 2px rgba(0,0,0,.05),0 0 0 1px rgba(63,63,68,.1); flex-shrink: 0; + padding: 10px 20px; + display: flex; + + .imageContainer { + display: flex; + flex-direction: column; + } + .image { + max-width: 200px; + } h4 { margin-left: 10px; @@ -26,3 +36,64 @@ font-weight: 400; } } + +.infoContainer { + margin-left: 20px; + display: flex; + align-items: center; + justify-content: space-between; + flex: 1; +} + +.buttonContainer { + display: flex; + align-items: center; +} + +.inputNumber { + display: flex; + + button { + background: #3585b5; + display: flex; + padding: 0; + border: none; + width: 24px; + height: 24px; + color: white; + justify-content: center; + align-items: center; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + cursor: pointer; + + &:first-child { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + } + } + + .number { + display: flex; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; + padding: 0; + border: 1px solid #ececec; + text-align: center; + } +} + +.deleteButton { + cursor: pointer; + border: 0; + background: transparent; + margin-left: 20px; + + &:hover { + color: #D64242; + } +} diff --git a/webpack/webpack-isomorphic-tools.js b/webpack/webpack-isomorphic-tools.js index 1ed53cd..5a95738 100644 --- a/webpack/webpack-isomorphic-tools.js +++ b/webpack/webpack-isomorphic-tools.js @@ -42,7 +42,7 @@ module.exports = { }, parser: function (m, options) { if (m.source) { - var regex = options.development ? /exports\.locals = ((.|\n)+);/ : /module\.exports = ((.|\n)+);/; + var regex = options.development ? /exports\.locals = ((.|\n)+)/ : /module\.exports = ((.|\n)+);/; var match = m.source.match(regex); return match ? JSON.parse(match[1]) : {}; } From bddb9138eb8099f9e53488faa6b0d75ed616f791 Mon Sep 17 00:00:00 2001 From: Justkant <quent92100@gmail.com> Date: Mon, 9 Nov 2015 14:51:42 +0800 Subject: [PATCH 5/9] Add/Del product on website --- api/functions/users.js | 52 +++++++++++--- api/models/Order.js | 3 +- api/models/User.js | 3 +- package.json | 4 +- src/components/Navbar/Navbar.js | 29 ++++---- src/components/Navbar/Navbar.styl | 7 +- .../ProductVignette/ProductVignette.js | 3 +- .../ProductVignette/ProductVignette.styl | 27 ++++++- src/containers/Cart/Cart.js | 28 ++++---- src/containers/Product/Product.js | 40 ++++++----- src/containers/Product/Product.styl | 72 +++++++++---------- src/redux/modules/auth.js | 54 ++++++++++++++ 12 files changed, 227 insertions(+), 95 deletions(-) diff --git a/api/functions/users.js b/api/functions/users.js index fa85809..ee21c03 100644 --- a/api/functions/users.js +++ b/api/functions/users.js @@ -265,7 +265,14 @@ function addUserProduct(req, res) { }); cart.saveAll().then((result) => { - res.json(result); + req.user.cartTotal += product.price * result.nbItem; + req.user.save().then(() => { + req.user.cart.push(result); + res.json(req.user.getPublic()); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); }, (error) => { console.error(error.message); res.status(400).json({msg: 'Something went wrong', err: error.message}); @@ -292,7 +299,13 @@ function deleteCart(cart) { function deleteUserCart(req, res) { deleteCart(req.user.cart).then(() => { - res.json({msg: 'Cart deleted'}); + req.user.cartTotal = 0; + req.user.save().then(() => { + res.json(req.user.getPublic()); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); }, (error) => { console.error(error.message); res.status(400).json({msg: 'Something went wrong', err: error.message}); @@ -310,12 +323,17 @@ function getUserCartItem(req, res) { function updateCartItem(req, res) { Cart.get(req.params.cartId).getJoin().run().then((cartItem) => { + req.user.cartTotal -= cartItem.product.price * cartItem.nbItem; cartItem.nbItem = req.body.nbItem; cartItem.save().then(() => { - res.json(cartItem); + req.user.cartTotal += cartItem.product.price * cartItem.nbItem; + req.user.save().then(() => { + res.json(req.user.getPublic()); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); }, (error) => { - console.error(error.message); - res.status(400).json({msg: 'Something went wrong', err: error.message}); }); }, (error) => { console.error(error.message); @@ -324,9 +342,20 @@ function updateCartItem(req, res) { } function deleteCartItem(req, res) { - Cart.get(req.params.cartId).run().then((cartItem) => { + Cart.get(req.params.cartId).getJoin().run().then((cartItem) => { cartItem.delete().then(() => { - res.json({msg: 'Cart item deleted'}); + req.user.cartTotal -= cartItem.product.price * cartItem.nbItem; + req.user.save().then(() => { + let pos = -1; + req.user.cart.forEach((cartItem2, index) => { + if (cartItem2.id === cartItem.id) pos = index; + }); + if (pos !== -1) req.user.cart.splice(pos, 1); + res.json(req.user.getPublic()); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }); }, (error) => { console.error(error.message); res.status(400).json({msg: 'Something went wrong', err: error.message}); @@ -343,6 +372,7 @@ function getUserOrders(req, res) { function validateCart(req, res) { (new Order({ + cartTotal: req.user.cartTotal, userId: req.user.id })).save().then((result) => { req.user.cart.forEach((cartItem) => { @@ -354,7 +384,13 @@ function validateCart(req, res) { }); deleteCart(req.user.cart).then(() => { - res.json(result); + req.user.cartTotal = 0; + req.user.save().then(() => { + res.json(result); + }, (error) => { + console.error(error.message); + res.status(400).json({msg: 'Something went wrong', err: error.message}); + }) }, (error) => { console.error(error.message); res.status(400).json({msg: 'Something went wrong', err: error.message}); diff --git a/api/models/Order.js b/api/models/Order.js index a2f2407..87c77e7 100644 --- a/api/models/Order.js +++ b/api/models/Order.js @@ -6,7 +6,8 @@ const Order = thinky.createModel('Order', { id: type.string(), createdAt: type.date().default(thinky.r.now()), status: type.string().default('validating'), - userId: type.string() + userId: type.string(), + cartTotal: type.number().required() }); Order.hasMany(Cart, 'cart', 'id', 'orderId'); diff --git a/api/models/User.js b/api/models/User.js index 8919559..345b8a9 100644 --- a/api/models/User.js +++ b/api/models/User.js @@ -11,7 +11,8 @@ const User = thinky.createModel('User', { admin: type.boolean().default(true), createdAt: type.date().default(thinky.r.now()), token: type.string(), - pictureUrl: type.string() + pictureUrl: type.string(), + cartTotal: type.number().default(0) }); User.define('getPublic', function() { diff --git a/package.json b/package.json index 370e1d4..0374ce5 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "babel-plugin-react-transform": "1.1.1", "babel-runtime": "6.0.12", "better-npm-run": "0.0.3", - "chai": "3.4.0", + "chai": "3.4.1", "clean-webpack-plugin": "0.1.4", "concurrently": "0.1.1", "css-loader": "0.22.0", @@ -141,7 +141,7 @@ "eslint-config-airbnb": "0.1.0", "eslint-loader": "1.1.1", "eslint-plugin-import": "^0.9.1", - "eslint-plugin-react": "3.7.1", + "eslint-plugin-react": "3.8.0", "extract-text-webpack-plugin": "^0.9.1", "json-loader": "0.5.3", "karma": "0.13.15", diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js index 25aeddd..df3707b 100644 --- a/src/components/Navbar/Navbar.js +++ b/src/components/Navbar/Navbar.js @@ -37,19 +37,24 @@ export default class Navbar extends React.Component { <div className={styles.menuContainer}> <DropDownButton infos={infos} links={menuLinks}/> </div> - <div className={styles.cartContainer}> - <Link to="/cart" className={styles.cartTitle}> - <i className="material-icons md-18">shopping_cart</i> - <span className={styles.cartTitleText}>Cart</span> - <span>38 $</span> - </Link> - <div className={styles.productList}> - <div><p>Nike shoes</p></div> - <div><p>Nike shoes long long text</p></div> - <div><p>Nike shoes</p></div> - <div><p>Nike shoes</p></div> + {user && user.cart.length > 0 && + <div className={styles.cartContainer}> + <Link to="/cart" className={styles.cartTitle}> + <i className="material-icons md-18">shopping_cart</i> + <span className={styles.cartTitleText}>Cart</span> + <span>{user.cartTotal} $</span> + </Link> + <div className={styles.productList}> + {user.cart.map(({product}) => { + return ( + <Link to={'/product/' + product.id} activeClassName="active"> + <p>{product.title}</p> + </Link> + ); + })} + </div> </div> - </div> + } </div> ); } diff --git a/src/components/Navbar/Navbar.styl b/src/components/Navbar/Navbar.styl index 01ef1cb..dbc1851 100644 --- a/src/components/Navbar/Navbar.styl +++ b/src/components/Navbar/Navbar.styl @@ -103,12 +103,15 @@ } .productList { - div { + a { + display: block; + color: white; border-radius: 4px; padding: 10px 20px; cursor: pointer; + text-decoration: none; - &:hover { + &:hover, &:global(.active) { background: rgba(255, 255, 255, 0.1); } diff --git a/src/components/ProductVignette/ProductVignette.js b/src/components/ProductVignette/ProductVignette.js index 41eaeb9..745e443 100644 --- a/src/components/ProductVignette/ProductVignette.js +++ b/src/components/ProductVignette/ProductVignette.js @@ -16,7 +16,8 @@ export default class ProductVignette extends Component { <img src={'/api/' + product.imageUrl}/> </Link> <div className={styles.productInfos}> - <p>{product.title}</p> + <Link to={'/product/' + product.id} className={styles.title}>{product.title}</Link> + <p className={styles.price}>{product.price} $</p> </div> </div> ); diff --git a/src/components/ProductVignette/ProductVignette.styl b/src/components/ProductVignette/ProductVignette.styl index 93995fe..3a2f706 100644 --- a/src/components/ProductVignette/ProductVignette.styl +++ b/src/components/ProductVignette/ProductVignette.styl @@ -13,8 +13,29 @@ .productInfos { padding: 20px 10px; -} -.productInfos p { - margin: 0; + p { + margin: 0; + } + + .title { + display: block; + margin-bottom: 10px; + color: #3585b5; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + + &:hover { + text-decoration: underline; + } + } + + .price { + color: #D64242; + margin-left: 10px; + font-size: 14px; + font-weight: bold; + } } diff --git a/src/containers/Cart/Cart.js b/src/containers/Cart/Cart.js index 707c779..3d4bbbe 100644 --- a/src/containers/Cart/Cart.js +++ b/src/containers/Cart/Cart.js @@ -1,41 +1,43 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { Title } from 'components'; +import { deleteCartItem } from 'redux/modules/auth'; -@connect(state => ({user: state.auth.user})) +@connect(state => ({user: state.auth.user}), { deleteCartItem }) export default class Cart extends Component { static propTypes = { - user: PropTypes.object + user: PropTypes.object, + deleteCartItem: PropTypes.func }; + deleteItem(productId) { + this.props.deleteCartItem(this.props.user.id, productId); + } + render() { const styles = require('./Cart.styl'); const {user} = this.props; - const array = []; - for (let index = 0; index < 50; index++) { - array.push('Product of ' + user.username); - } return ( <div className={styles.container}> <Title title="Cart"/> <div className={styles.productContainer}> - {array.map((value, index) => { + {user && user.cart.map(({id, product, nbItem}) => { return ( - <div className={styles.element} key={value + index}> + <div className={styles.element} key={product.id}> <div className={styles.imageContainer}> - <img src="/product.jpg" className={styles.image}/> + <img src={'/api/' + product.imageUrl} className={styles.image}/> </div> <div className={styles.infoContainer}> - <p>{value}</p> - <p>Price</p> + <p>{product.title}</p> + <p>{product.price} $</p> <div className={styles.buttonContainer}> <div className={styles.inputNumber}> <button><i className="material-icons">remove</i></button> - <input className={styles.number} defaultValue="1"/> + <input className={styles.number} defaultValue={nbItem}/> <button><i className="material-icons">add</i></button> </div> - <button className={styles.deleteButton}> + <button className={styles.deleteButton} onClick={this.deleteItem.bind(this, id)}> <i className="material-icons">delete</i> </button> </div> diff --git a/src/containers/Product/Product.js b/src/containers/Product/Product.js index 6834b7d..911563a 100644 --- a/src/containers/Product/Product.js +++ b/src/containers/Product/Product.js @@ -2,20 +2,32 @@ import React, { Component, PropTypes } from 'react'; import { Title } from 'components'; import { connect } from 'react-redux'; import { isProductLoaded, getById } from 'redux/modules/product'; +import { addToCart } from 'redux/modules/auth'; -@connect(state => ({product: state.product.product})) +@connect(state => ({user: state.auth.user, product: state.product.product}), {addToCart}) export default class Product extends Component { static propTypes = { + user: PropTypes.object, product: PropTypes.object, - params: PropTypes.object.isRequired + params: PropTypes.object.isRequired, + addToCart: PropTypes.func.isRequired }; + constructor() { + super(); + this.addProduct = this.addProduct.bind(this); + } + static fetchDataDeferred(getState, dispatch, location, params) { if (!isProductLoaded(getState())) { return dispatch(getById(params.id)); } } + addProduct() { + this.props.addToCart(this.props.user.id, this.props.product.id); + } + render() { const {product} = this.props; const styles = require('./Product.styl'); @@ -26,24 +38,20 @@ export default class Product extends Component { <div className={styles.productContainer}> - <img src={'/api/' + product.imageUrl}/> + <div className={styles.imgContainer}> + <img src={'/api/' + product.imageUrl}/> + </div> <div className={styles.mainInformations}> - - <div className={styles.mainInformationsLeft}> - <h3>Brand</h3> - <h3>Price</h3> - </div> - - <div className={styles.mainInformationsRight}> - <p>{product.title}</p> - <p>{product.price + ' $'}</p> - </div> - + <p className={styles.price}><b>Price:</b> {product.price + ' $'}</p> + <p className={styles.desc}>{product.description}</p> </div> - <div className={styles.additionalInformations}> - <p>{product.description}</p> + <div className={styles.addContainer}> + <button className={styles.addButton} onClick={this.addProduct}> + <i className="material-icons md-18">add_shopping_cart</i> + <span>Add</span> + </button> </div> </div> </div> diff --git a/src/containers/Product/Product.styl b/src/containers/Product/Product.styl index 9e8b1f1..510acfd 100644 --- a/src/containers/Product/Product.styl +++ b/src/containers/Product/Product.styl @@ -8,55 +8,55 @@ .productContainer { padding: 20px; display: flex; - flex-direction: column; overflow: auto; } -.productContainer img { - padding: 20px; - max-width: 500px; - cursor: pointer; -} - -.mainInformations { +.imgContainer { display: flex; - padding: 20px 0; + flex-basis: 36.5%; - &:not(:last-child) { - border-bottom: 1px solid #E7E7EC; + img { + padding: 20px; + height: 100%; + width: 100%; } } -.mainInformationsLeft { +.mainInformations { display: flex; flex-direction: column; - flex-basis: 100%; - padding: 0 20px; -} + flex: 1; + padding: 20px 0; -.mainInformationsLeft h3 { - color: #3585b5; - font-weight: 300; - margin: 0; - margin-bottom: 10px; -} + .price { + font-size: 16px; + margin-bottom: 10px; + color: #999; + } -.mainInformationsRight { - display: flex; - flex-direction: column; - flex-basis: 70%; - padding-left: 10%; + .desc { + font-size: 16px; + margin-bottom: 10px; + color: #999; + } } -.mainInformationsRight p { - font-size: 12px; - margin-bottom: 10px; - color: #999; -} +.addContainer { + padding: 20px 0; -.additionalInformations { - padding: 20px; - font-weight: 300; - font-size: 12px; - line-height: 24px; + .addButton { + display: flex; + border: 0; + border-radius: 4px; + background: #3585b5; + color: white; + font-size: 14px; + padding: 10px 20px; + cursor: pointer; + align-items: center; + + span { + margin-left: 5px; + } + } } diff --git a/src/redux/modules/auth.js b/src/redux/modules/auth.js index 588d665..2f708ac 100644 --- a/src/redux/modules/auth.js +++ b/src/redux/modules/auth.js @@ -19,6 +19,12 @@ const DELETE_FAIL = 'auth/DELETE_FAIL'; const USERS = 'auth/USERS'; const USERS_SUCCESS = 'auth/USERS_SUCCESS'; const USERS_FAIL = 'auth/USERS_FAIL'; +const ADDCART = 'auth/ADDCART'; +const ADDCART_SUCCESS = 'auth/ADDCART_SUCCESS'; +const ADDCART_FAIL = 'auth/ADDCART_FAIL'; +const DELCART = 'auth/DELCART'; +const DELCART_SUCCESS = 'auth/DELCART_SUCCESS'; +const DELCART_FAIL = 'auth/DELCART_FAIL'; const initialState = { loaded: false @@ -151,6 +157,40 @@ export default function reducer(state = initialState, action = {}) { usersLoaded: false, usersError: action.error }; + case ADDCART: + return { + ...state, + addingCart: true + }; + case ADDCART_SUCCESS: + return { + ...state, + addingCart: false, + user: action.result + }; + case ADDCART_FAIL: + return { + ...state, + addingCart: false, + cartAddError: action.error + }; + case DELCART: + return { + ...state, + deletingCart: true + }; + case DELCART_SUCCESS: + return { + ...state, + deletingCart: false, + user: action.result + }; + case DELCART_FAIL: + return { + ...state, + deletingCart: false, + cartDelError: action.error + }; default: return state; } @@ -212,3 +252,17 @@ export function logout() { promise: (client) => client.get('/logout') }; } + +export function addToCart(userId, id) { + return { + types: [ADDCART, ADDCART_SUCCESS, ADDCART_FAIL], + promise: (client) => client.post('/users/' + userId + '/cart', {data: {productId: id}}) + }; +} + +export function deleteCartItem(userId, cartId) { + return { + types: [DELCART, DELCART_SUCCESS, DELCART_FAIL], + promise: (client) => client.del('/users/' + userId + '/cart/' + cartId) + }; +} From 79dd70a39ee5e380a446e6bda677badf84c5626c Mon Sep 17 00:00:00 2001 From: Justkant <quent92100@gmail.com> Date: Mon, 9 Nov 2015 15:20:03 +0800 Subject: [PATCH 6/9] Add price for product Fix problem with new user and no cart array --- api/functions/products.js | 2 +- api/functions/users.js | 2 +- src/components/Navbar/Navbar.js | 2 +- src/components/ProductVignette/ProductVignette.js | 2 +- src/components/ProductVignette/ProductVignette.styl | 13 +++++++++++-- src/containers/Products/Products.js | 6 ++++-- 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/api/functions/products.js b/api/functions/products.js index 3a9a33c..6df5c22 100644 --- a/api/functions/products.js +++ b/api/functions/products.js @@ -20,7 +20,7 @@ function addProduct(req, res) { title: req.body.title, description: req.body.description, imageUrl: req.body.imageUrl, - price: 0 + price: req.body.price }); product.save().then(() => { diff --git a/api/functions/users.js b/api/functions/users.js index ee21c03..4eec21b 100644 --- a/api/functions/users.js +++ b/api/functions/users.js @@ -48,7 +48,7 @@ function load(req, res) { } function login(req, res) { - User.filter({email: req.body.email}).then((results) => { + User.filter({email: req.body.email}).getJoin().then((results) => { if (results.length > 0) { let user = results[0]; authenticate(req.body.password, user.password).then(() => { diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js index df3707b..6d3d0a0 100644 --- a/src/components/Navbar/Navbar.js +++ b/src/components/Navbar/Navbar.js @@ -37,7 +37,7 @@ export default class Navbar extends React.Component { <div className={styles.menuContainer}> <DropDownButton infos={infos} links={menuLinks}/> </div> - {user && user.cart.length > 0 && + {user && user.cart && user.cart.length > 0 && <div className={styles.cartContainer}> <Link to="/cart" className={styles.cartTitle}> <i className="material-icons md-18">shopping_cart</i> diff --git a/src/components/ProductVignette/ProductVignette.js b/src/components/ProductVignette/ProductVignette.js index 745e443..c184c76 100644 --- a/src/components/ProductVignette/ProductVignette.js +++ b/src/components/ProductVignette/ProductVignette.js @@ -12,7 +12,7 @@ export default class ProductVignette extends Component { return ( <div className={styles.productContainer}> - <Link to={'/product/' + product.id}> + <Link to={'/product/' + product.id} className={styles.imgContainer}> <img src={'/api/' + product.imageUrl}/> </Link> <div className={styles.productInfos}> diff --git a/src/components/ProductVignette/ProductVignette.styl b/src/components/ProductVignette/ProductVignette.styl index 3a2f706..b9c283b 100644 --- a/src/components/ProductVignette/ProductVignette.styl +++ b/src/components/ProductVignette/ProductVignette.styl @@ -6,9 +6,18 @@ margin: 10px; } -.productContainer img { +.imgContainer { + height: 150px; max-width: 200px; - cursor: pointer; + display: flex; + align-items: center; + overflow: hidden; + + img { + height: auto; + width: 100%; + cursor: pointer; + } } .productInfos { diff --git a/src/containers/Products/Products.js b/src/containers/Products/Products.js index 338cd89..e0d73a0 100644 --- a/src/containers/Products/Products.js +++ b/src/containers/Products/Products.js @@ -30,14 +30,15 @@ export default class Products extends Component { addProduct() { const self = this; - const {title, desc} = this.refs; + const {title, desc, price} = this.refs; const client = new ApiClient(); client.post('/picture', { data: this.state.newFile }).then((result) => { const product = { title: title.value, description: desc.value, - imageUrl: result.url + imageUrl: result.url, + price: price.value }; self.props.create(product).then(() => { self.toggleActive(); @@ -93,6 +94,7 @@ export default class Products extends Component { <div className={styles.infosContainer}> <input type="text" ref="title" placeholder="Title"/> <textarea ref="desc" placeholder="Description..."/> + <input type="number" ref="price" placeholder="Price"/> </div> </div> <div className={styles.buttons}> From da500193a7278313cb12cb2c7ec225f8b556f837 Mon Sep 17 00:00:00 2001 From: Justkant <quent92100@gmail.com> Date: Mon, 9 Nov 2015 15:46:20 +0800 Subject: [PATCH 7/9] Market fetch data every time you go on the page Adding a product is directly visible on the admin list of products --- src/containers/Market/Market.js | 6 ++---- src/redux/modules/product.js | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/containers/Market/Market.js b/src/containers/Market/Market.js index 2690d12..24d3b2a 100755 --- a/src/containers/Market/Market.js +++ b/src/containers/Market/Market.js @@ -1,7 +1,7 @@ import React, { Component, PropTypes } from 'react'; import { ProductVignette } from 'components'; import { connect } from 'react-redux'; -import { isLoaded as isMarketLoaded, load as loadMarket } from 'redux/modules/product'; +import { load as loadMarket } from 'redux/modules/product'; @connect(state => ({market: state.product.market})) export default class Market extends Component { @@ -10,9 +10,7 @@ export default class Market extends Component { }; static fetchDataDeferred(getState, dispatch) { - if (!isMarketLoaded(getState())) { - return dispatch(loadMarket()); - } + return dispatch(loadMarket()); } render() { diff --git a/src/redux/modules/product.js b/src/redux/modules/product.js index da6a3ac..c6e7821 100644 --- a/src/redux/modules/product.js +++ b/src/redux/modules/product.js @@ -46,6 +46,7 @@ export default function reducer(state = initialState, action = {}) { ...state, creating: false, created: true, + products: state.products.concat(action.result), createdProduct: action.result }; case CREATE_FAIL: From a2267dfa056b32db959518c5d8dfe7ce320f0213 Mon Sep 17 00:00:00 2001 From: Justkant <quent92100@gmail.com> Date: Fri, 27 Nov 2015 15:30:09 +0800 Subject: [PATCH 8/9] Update the app dependancies and add connectData decorator --- .travis.yml | 1 + Dockerfile | 13 ++++ api/api.js | 2 +- docker-compose.yml | 13 ++++ package.json | 63 ++++++++++--------- src/client.js | 9 ++- src/config.js | 2 +- src/containers/Market/Market.js | 10 +-- src/containers/NotFound/NotFound.js | 18 +++--- src/containers/Product/Product.js | 14 +++-- src/containers/Products/Products.js | 14 +++-- src/containers/Users/Users.js | 14 +++-- src/helpers/Html.js | 1 + src/helpers/__tests__/connectData-test.js | 29 +++++++++ .../__tests__/getDataDependencies-test.js | 24 ++----- src/helpers/connectData.js | 24 +++++++ src/helpers/getDataDependencies.js | 21 ++++--- src/redux/create.js | 11 +++- src/redux/middleware/transitionMiddleware.js | 14 ++++- src/server.js | 2 +- webpack/dev.config.js | 2 +- webpack/prod.config.js | 2 +- webpack/webpack-isomorphic-tools.js | 44 +++++++------ 23 files changed, 225 insertions(+), 122 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/helpers/__tests__/connectData-test.js create mode 100644 src/helpers/connectData.js diff --git a/.travis.yml b/.travis.yml index 8a28b37..e8fcbf3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: node_js node_js: - "4" + - "5" sudo: false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1507a65 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:latest +MAINTAINER Quentin Jaccarino <jaccarino.quentin@gmail.com> + +ADD package.json /tmp/package.json +RUN cd /tmp && npm install +RUN mkdir -p /src && cp -a /tmp/node_modules /src/ + +WORKDIR /src +ADD . /src + +EXPOSE 3000 + +CMD ["npm", "run", "dev"] diff --git a/api/api.js b/api/api.js index 8a750ce..5f6b23a 100644 --- a/api/api.js +++ b/api/api.js @@ -26,7 +26,7 @@ const staticOptions = {}; if (config.isProduction) { staticOptions.maxAge = '60 days'; } -app.use('/uploads', require('serve-static')('uploads/', staticOptions)); +app.use('/uploads', express.static('uploads/', staticOptions)); app.get('/load', users.load); app.post('/login', users.login); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bcdfa1f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +web: + build: . + ports: + - "3030:3030" + links: + - "db" + +db: + image: rethinkdb + environment: + - POSTGRES_PASSWORD=justkant + - POSTGRES_USER=kant + - POSTGRES_DB=ilovemycity diff --git a/package.json b/package.json index 0374ce5..be96c4b 100644 --- a/package.json +++ b/package.json @@ -25,14 +25,14 @@ "main": "bin/server.js", "scripts": { "start": "concurrent --kill-others \"npm run start-prod\" \"npm run start-prod-api\"", - "start-prod": "node ./node_modules/better-npm-run start-prod", - "start-prod-api": "node ./node_modules/better-npm-run start-prod-api", + "start-prod": "better-npm-run start-prod", + "start-prod-api": "better-npm-run start-prod-api", "build": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js", "postinstall": "webpack --display-error-details --config webpack/prod.config.js", "lint": "eslint -c .eslintrc src", - "start-dev": "node ./node_modules/better-npm-run start-dev", - "start-dev-api": "node ./node_modules/better-npm-run start-dev-api", - "watch-client": "node ./node_modules/better-npm-run watch-client", + "start-dev": "better-npm-run start-dev", + "start-dev-api": "better-npm-run start-dev-api", + "watch-client": "better-npm-run watch-client", "dev": "concurrent --kill-others \"npm run watch-client\" \"npm run start-dev\" \"npm run start-dev-api\"", "test": "karma start", "doc": "apidoc -i api/" @@ -89,15 +89,16 @@ } }, "dependencies": { - "babel": "6.0.14", + "babel": "5.8.29", "babel-plugin-typecheck": "2.0.0", "bcryptjs": "^2.3.0", "body-parser": "1.14.1", "compression": "^1.6.0", "cookie-parser": "^1.4.0", "express": "4.13.3", - "file-loader": "0.8.4", - "history": "1.13.0", + "file-loader": "0.8.5", + "history": "1.13.1", + "hoist-non-react-statics": "^1.0.3", "http-proxy": "1.12.0", "jsonwebtoken": "^5.4.1", "lru-memoize": "1.0.0", @@ -111,50 +112,50 @@ "query-string": "3.0.0", "react": "^0.14.2", "react-document-meta": "2.0.0", - "react-dom": "0.14.2", + "react-dom": "0.14.3", "react-redux": "4.0.0", - "react-router": "1.0.0-rc4", + "react-router": "1.0.0", "redux": "3.0.4", "redux-router": "^1.0.0-beta4", + "scroll-behavior": "^0.3.0", "serialize-javascript": "1.1.2", "serve-favicon": "2.3.0", - "serve-static": "1.10.0", "superagent": "1.4.0", "thinky": "^2.1.11", - "url-loader": "0.5.6", - "webpack-isomorphic-tools": "2.2.11" + "url-loader": "0.5.7", + "webpack-isomorphic-tools": "2.2.18" }, "devDependencies": { "apidoc": "^0.13.1", "autoprefixer-stylus": "0.8.1", - "babel-core": "5.8.25", + "babel-core": "5.8.33", "babel-eslint": "4.1.4", - "babel-loader": "5.3.2", + "babel-loader": "5.3.3", "babel-plugin-react-transform": "1.1.1", - "babel-runtime": "6.0.12", - "better-npm-run": "0.0.3", + "babel-runtime": "5.8.29", + "better-npm-run": "0.0.4", "chai": "3.4.1", "clean-webpack-plugin": "0.1.4", - "concurrently": "0.1.1", - "css-loader": "0.22.0", - "eslint": "1.9.0", + "concurrently": "1.0.0", + "css-loader": "0.23.0", + "eslint": "1.10.1", "eslint-config-airbnb": "0.1.0", "eslint-loader": "1.1.1", - "eslint-plugin-import": "^0.9.1", - "eslint-plugin-react": "3.8.0", + "eslint-plugin-import": "0.10.1", + "eslint-plugin-react": "3.10.0", "extract-text-webpack-plugin": "^0.9.1", - "json-loader": "0.5.3", + "json-loader": "0.5.4", "karma": "0.13.15", "karma-chrome-launcher": "0.2.1", "karma-cli": "0.1.1", - "karma-firefox-launcher": "0.1.6", - "karma-mocha": "0.2.0", - "karma-mocha-reporter": "1.1.1", + "karma-firefox-launcher": "0.1.7", + "karma-mocha": "0.2.1", + "karma-mocha-reporter": "1.1.2", "karma-sourcemap-loader": "0.3.6", "karma-webpack": "1.7.0", - "mocha": "2.3.3", + "mocha": "2.3.4", "react-a11y": "0.2.8", - "react-addons-test-utils": "0.14.2", + "react-addons-test-utils": "0.14.3", "react-transform-catch-errors": "^1.0.0", "react-transform-hmr": "1.0.1", "redbox-react": "^1.1.1", @@ -162,9 +163,9 @@ "strip-loader": "^0.1.0", "style-loader": "0.13.0", "stylus-loader": "1.4.2", - "webpack": "1.12.3", - "webpack-dev-middleware": "1.2.0", - "webpack-hot-middleware": "2.4.1" + "webpack": "1.12.9", + "webpack-dev-middleware": "1.4.0", + "webpack-hot-middleware": "2.5.0" }, "engines": { "node": "4.x" diff --git a/src/client.js b/src/client.js index a7b7951..7d8441d 100644 --- a/src/client.js +++ b/src/client.js @@ -1,10 +1,11 @@ /** * THIS IS THE ENTRY POINT FOR THE CLIENT, JUST LIKE server.js IS THE ENTRY POINT FOR THE SERVER. */ -import 'babel-core/polyfill'; +import 'babel/polyfill'; import React from 'react'; import ReactDOM from 'react-dom'; import createHistory from 'history/lib/createBrowserHistory'; +import useScroll from 'scroll-behavior/lib/useStandardScroll'; import createStore from './redux/create'; import ApiClient from './helpers/ApiClient'; import {Provider} from 'react-redux'; @@ -15,8 +16,12 @@ import makeRouteHooksSafe from './helpers/makeRouteHooksSafe'; const client = new ApiClient(); +// Three different types of scroll behavior available. +// Documented here: https://github.com/rackt/scroll-behavior +const scrollablehistory = useScroll(createHistory); + const dest = document.getElementById('content'); -const store = createStore(reduxReactRouter, makeRouteHooksSafe(getRoutes), createHistory, client, window.__data); +const store = createStore(reduxReactRouter, makeRouteHooksSafe(getRoutes), scrollablehistory, client, window.__data); const component = ( <Provider store={store} key="provider"> diff --git a/src/config.js b/src/config.js index 0cedeba..7787bc0 100644 --- a/src/config.js +++ b/src/config.js @@ -1,4 +1,4 @@ -require('babel-core/polyfill'); +require('babel/polyfill'); const environment = { development: { diff --git a/src/containers/Market/Market.js b/src/containers/Market/Market.js index 24d3b2a..9c77915 100755 --- a/src/containers/Market/Market.js +++ b/src/containers/Market/Market.js @@ -2,17 +2,19 @@ import React, { Component, PropTypes } from 'react'; import { ProductVignette } from 'components'; import { connect } from 'react-redux'; import { load as loadMarket } from 'redux/modules/product'; +import connectData from 'helpers/connectData'; +function fetchDataDeferred(getState, dispatch) { + return dispatch(loadMarket()); +} + +@connectData(null, fetchDataDeferred) @connect(state => ({market: state.product.market})) export default class Market extends Component { static propTypes = { market: PropTypes.array }; - static fetchDataDeferred(getState, dispatch) { - return dispatch(loadMarket()); - } - render() { const {market} = this.props; const styles = require('./Market.styl'); diff --git a/src/containers/NotFound/NotFound.js b/src/containers/NotFound/NotFound.js index df4252f..d15347b 100755 --- a/src/containers/NotFound/NotFound.js +++ b/src/containers/NotFound/NotFound.js @@ -1,12 +1,10 @@ -import React, {Component} from 'react'; +import React from 'react'; -export default class NotFound extends Component { - render() { - return ( - <div className="main"> - <h1>Doh! 404!</h1> - <p>These are <em>not</em> the droids you are looking for!</p> - </div> - ); - } +export default function NotFound() { + return ( + <div className="main"> + <h1>Doh! 404!</h1> + <p>These are <em>not</em> the droids you are looking for!</p> + </div> + ); } diff --git a/src/containers/Product/Product.js b/src/containers/Product/Product.js index 911563a..97fdb84 100644 --- a/src/containers/Product/Product.js +++ b/src/containers/Product/Product.js @@ -3,7 +3,15 @@ import { Title } from 'components'; import { connect } from 'react-redux'; import { isProductLoaded, getById } from 'redux/modules/product'; import { addToCart } from 'redux/modules/auth'; +import connectData from 'helpers/connectData'; +function fetchDataDeferred(getState, dispatch, location, params) { + if (!isProductLoaded(getState())) { + return dispatch(getById(params.id)); + } +} + +@connectData(null, fetchDataDeferred) @connect(state => ({user: state.auth.user, product: state.product.product}), {addToCart}) export default class Product extends Component { static propTypes = { @@ -18,12 +26,6 @@ export default class Product extends Component { this.addProduct = this.addProduct.bind(this); } - static fetchDataDeferred(getState, dispatch, location, params) { - if (!isProductLoaded(getState())) { - return dispatch(getById(params.id)); - } - } - addProduct() { this.props.addToCart(this.props.user.id, this.props.product.id); } diff --git a/src/containers/Products/Products.js b/src/containers/Products/Products.js index e0d73a0..fdb7c62 100644 --- a/src/containers/Products/Products.js +++ b/src/containers/Products/Products.js @@ -2,7 +2,15 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { create, isAllLoaded, getAll } from 'redux/modules/product'; import ApiClient from 'helpers/ApiClient'; +import connectData from 'helpers/connectData'; +function fetchDataDeferred(getState, dispatch) { + if (!isAllLoaded(getState())) { + return dispatch(getAll()); + } +} + +@connectData(null, fetchDataDeferred) @connect(state => ({products: state.product.products}), {create}) export default class Products extends Component { static propTypes = { @@ -22,12 +30,6 @@ export default class Products extends Component { this.checkFile = this.checkFile.bind(this); } - static fetchDataDeferred(getState, dispatch) { - if (!isAllLoaded(getState())) { - return dispatch(getAll()); - } - } - addProduct() { const self = this; const {title, desc, price} = this.refs; diff --git a/src/containers/Users/Users.js b/src/containers/Users/Users.js index 1476010..bf4d056 100644 --- a/src/containers/Users/Users.js +++ b/src/containers/Users/Users.js @@ -1,19 +1,21 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { isUsersLoaded, users as getUsers } from 'redux/modules/auth'; +import connectData from 'helpers/connectData'; +function fetchDataDeferred(getState, dispatch) { + if (!isUsersLoaded(getState())) { + return dispatch(getUsers()); + } +} + +@connectData(null, fetchDataDeferred) @connect(state => ({users: state.auth.users})) export default class Users extends Component { static propTypes = { users: PropTypes.array }; - static fetchDataDeferred(getState, dispatch) { - if (!isUsersLoaded(getState())) { - return dispatch(getUsers()); - } - } - render() { const {users = []} = this.props; const styles = require('./Users.styl'); diff --git a/src/helpers/Html.js b/src/helpers/Html.js index 297ea78..1d5bc43 100644 --- a/src/helpers/Html.js +++ b/src/helpers/Html.js @@ -31,6 +31,7 @@ export default class Html extends Component { <link rel="shortcut icon" href="/favicon.ico" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> {/* styles (will be present only in production with webpack extract text plugin) */} {Object.keys(assets.styles).map((style, index) => <link href={assets.styles[style]} key={index} media="screen, projection" diff --git a/src/helpers/__tests__/connectData-test.js b/src/helpers/__tests__/connectData-test.js new file mode 100644 index 0000000..d61d73b --- /dev/null +++ b/src/helpers/__tests__/connectData-test.js @@ -0,0 +1,29 @@ +import { expect } from 'chai'; +import React from 'react'; +import { div } from 'react-dom'; +import connectData from '../connectData'; + +describe('connectData', () => { + let fetchData; + let fetchDataDeferred; + let WrappedComponent; + let DataComponent; + + beforeEach(() => { + fetchData = 'fetchDataFunction'; + fetchDataDeferred = 'fetchDataDeferredFunction'; + + WrappedComponent = () => + <div />; + + DataComponent = connectData(fetchData, fetchDataDeferred)(WrappedComponent); + }); + + it('should set fetchData as a static property of the final component', () => { + expect(DataComponent.fetchData).to.equal(fetchData); + }); + + it('should set fetchDataDeferred as a static property of the final component', () => { + expect(DataComponent.fetchDataDeferred).to.equal(fetchDataDeferred); + }); +}); diff --git a/src/helpers/__tests__/getDataDependencies-test.js b/src/helpers/__tests__/getDataDependencies-test.js index 08ea664..fb2a066 100644 --- a/src/helpers/__tests__/getDataDependencies-test.js +++ b/src/helpers/__tests__/getDataDependencies-test.js @@ -11,8 +11,7 @@ describe('getDataDependencies', () => { let CompWithFetchData; let CompWithNoData; let CompWithFetchDataDeferred; - let ConnectedCompWithFetchData; - let ConnectedCompWithFetchDataDeferred; + const NullComponent = null; beforeEach(() => { getState = 'getState'; @@ -35,43 +34,30 @@ describe('getDataDependencies', () => { CompWithFetchDataDeferred.fetchDataDeferred = (_getState, _dispatch, _location, _params) => { return `fetchDataDeferred ${_getState} ${_dispatch} ${_location} ${_params}`; }; - - ConnectedCompWithFetchData = () => - <div/>; - - ConnectedCompWithFetchData.WrappedComponent = CompWithFetchData; - - ConnectedCompWithFetchDataDeferred = () => - <div/>; - - ConnectedCompWithFetchDataDeferred.WrappedComponent = CompWithFetchDataDeferred; }); it('should get fetchDatas', () => { const deps = getDataDependencies([ + NullComponent, CompWithFetchData, CompWithNoData, - CompWithFetchDataDeferred, - ConnectedCompWithFetchData, - ConnectedCompWithFetchDataDeferred + CompWithFetchDataDeferred ], getState, dispatch, location, params); expect(deps).to.deep.equal([ - 'fetchData getState dispatch location params', 'fetchData getState dispatch location params' ]); }); it('should get fetchDataDeferreds', () => { const deps = getDataDependencies([ + NullComponent, CompWithFetchData, CompWithNoData, - CompWithFetchDataDeferred, - ConnectedCompWithFetchDataDeferred + CompWithFetchDataDeferred ], getState, dispatch, location, params, true); expect(deps).to.deep.equal([ - 'fetchDataDeferred getState dispatch location params', 'fetchDataDeferred getState dispatch location params' ]); }); diff --git a/src/helpers/connectData.js b/src/helpers/connectData.js new file mode 100644 index 0000000..4ad6727 --- /dev/null +++ b/src/helpers/connectData.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import hoistStatics from 'hoist-non-react-statics'; + +/* + Note: + When this decorator is used, it MUST be the first (outermost) decorator. + Otherwise, we cannot find and call the fetchData and fetchDataDeffered methods. +*/ + +export default function connectData(fetchData, fetchDataDeferred) { + + return function wrapWithFetchData(WrappedComponent) { + class ConnectData extends Component { + render() { + return <WrappedComponent {...this.props} />; + } + } + + ConnectData.fetchData = fetchData; + ConnectData.fetchDataDeferred = fetchDataDeferred; + + return hoistStatics(ConnectData, WrappedComponent); + }; +} diff --git a/src/helpers/getDataDependencies.js b/src/helpers/getDataDependencies.js index 44af9aa..9559c51 100644 --- a/src/helpers/getDataDependencies.js +++ b/src/helpers/getDataDependencies.js @@ -1,15 +1,18 @@ -const getDataDependency = (component = {}, methodName) => { - return component.WrappedComponent ? - getDataDependency(component.WrappedComponent, methodName) : - component[methodName]; -}; - +/** + * 1. Skip holes in route component chain and + * only consider components that implement + * fetchData or fetchDataDeferred + * + * 2. Pull out fetch data methods + * + * 3. Call fetch data methods and gather promises + */ export default (components, getState, dispatch, location, params, deferred) => { const methodName = deferred ? 'fetchDataDeferred' : 'fetchData'; return components - .filter((component) => getDataDependency(component, methodName)) // only look at ones with a static fetchData() - .map((component) => getDataDependency(component, methodName)) // pull out fetch data methods + .filter((component) => component && component[methodName]) // 1 + .map((component) => component[methodName]) // 2 .map(fetchData => - fetchData(getState, dispatch, location, params)); // call fetch data methods and save promises + fetchData(getState, dispatch, location, params)); // 3 }; diff --git a/src/redux/create.js b/src/redux/create.js index b5b8726..85b61d4 100644 --- a/src/redux/create.js +++ b/src/redux/create.js @@ -1,4 +1,4 @@ -import { createStore as _createStore, applyMiddleware } from 'redux'; +import { createStore as _createStore, applyMiddleware, compose } from 'redux'; import createMiddleware from './middleware/clientMiddleware'; import transitionMiddleware from './middleware/transitionMiddleware'; @@ -6,7 +6,14 @@ export default function createStore(reduxReactRouter, getRoutes, createHistory, const middleware = [createMiddleware(client), transitionMiddleware]; let finalCreateStore; - finalCreateStore = applyMiddleware(...middleware)(_createStore); + if (__DEVELOPMENT__ && __CLIENT__) { + finalCreateStore = compose( + applyMiddleware(...middleware), + window.devToolsExtension ? window.devToolsExtension() : funk => funk + )(_createStore); + } else { + finalCreateStore = applyMiddleware(...middleware)(_createStore); + } finalCreateStore = reduxReactRouter({ getRoutes, createHistory })(finalCreateStore); const reducer = require('./modules/reducer'); diff --git a/src/redux/middleware/transitionMiddleware.js b/src/redux/middleware/transitionMiddleware.js index c6dc287..f2fd94f 100644 --- a/src/redux/middleware/transitionMiddleware.js +++ b/src/redux/middleware/transitionMiddleware.js @@ -15,11 +15,21 @@ export default ({getState, dispatch}) => next => action => { const doTransition = () => { next(action); Promise.all(getDataDependencies(components, getState, dispatch, location, params, true)) - .then(resolve, resolve); + .then(resolve) + .catch(error => { + // TODO: You may want to handle errors for fetchDataDeferred here + console.warn('Warning: Error in fetchDataDeferred', error); + return resolve(); + }); }; Promise.all(getDataDependencies(components, getState, dispatch, location, params)) - .then(doTransition, doTransition); + .then(doTransition) + .catch(error => { + // TODO: You may want to handle errors for fetchData here + console.warn('Warning: Error in fetchData', error); + return doTransition(); + }); }); if (__SERVER__) { diff --git a/src/server.js b/src/server.js index 48d67f3..c8a4f4c 100644 --- a/src/server.js +++ b/src/server.js @@ -32,7 +32,7 @@ const staticOptions = {}; if (!__DEVELOPMENT__) { staticOptions.maxAge = '60 days'; } -app.use(require('serve-static')(path.join(__dirname, '..', 'static'), staticOptions)); +app.use(Express.static(path.join(__dirname, '..', 'static'), staticOptions)); // Proxy to API server app.use('/api', (req, res) => { diff --git a/webpack/dev.config.js b/webpack/dev.config.js index a1ffc7f..1e770a8 100644 --- a/webpack/dev.config.js +++ b/webpack/dev.config.js @@ -1,4 +1,4 @@ -require('babel-core/polyfill'); +require('babel/polyfill'); var fs = require('fs'); var path = require('path'); var webpack = require('webpack'); diff --git a/webpack/prod.config.js b/webpack/prod.config.js index 1f7b871..84ce01f 100644 --- a/webpack/prod.config.js +++ b/webpack/prod.config.js @@ -1,6 +1,6 @@ // Webpack config for creating the production bundle. -require('babel-core/polyfill'); +require('babel/polyfill'); var path = require('path'); var webpack = require('webpack'); var CleanPlugin = require('clean-webpack-plugin'); diff --git a/webpack/webpack-isomorphic-tools.js b/webpack/webpack-isomorphic-tools.js index 5a95738..464988e 100644 --- a/webpack/webpack-isomorphic-tools.js +++ b/webpack/webpack-isomorphic-tools.js @@ -21,30 +21,34 @@ module.exports = { }, style_modules: { extension: 'styl', - filter: function (m, regex, options) { - if (!options.development) { - return regex.test(m.name); + filter: function(module, regex, options, log) { + if (options.development) { + // in development mode there's webpack "style-loader", + // so the module.name is not equal to module.name + return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log); + } else { + // in production mode there's no webpack "style-loader", + // so the module.name will be equal to the asset path + return regex.test(module.name); } - return (regex.test(m.name) && m.name.slice(-4) === 'styl' && m.reasons[0].moduleName.slice(-4) === 'styl'); }, - path: function (m, options) { - //find index of '/src' inside the module name, slice it and resolve path - var srcIndex = m.name.indexOf('/src'); - var name = '.' + m.name.slice(srcIndex); - if (name) { - // Resolve the e.g.: "C:\" issue on windows - const i = name.indexOf(':'); - if (i >= 0) { - name = name.slice(i + 1); - } + path: function(module, options, log) { + if (options.development) { + // in development mode there's webpack "style-loader", + // so the module.name is not equal to module.name + return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log); + } else { + // in production mode there's no webpack "style-loader", + // so the module.name will be equal to the asset path + return module.name; } - return name; }, - parser: function (m, options) { - if (m.source) { - var regex = options.development ? /exports\.locals = ((.|\n)+)/ : /module\.exports = ((.|\n)+);/; - var match = m.source.match(regex); - return match ? JSON.parse(match[1]) : {}; + parser: function(module, options, log) { + if (options.development) { + return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log); + } else { + // in production mode there's Extract Text Loader which extracts CSS text away + return module.source; } } } From 4c25f97b84bddb8f6c5eac2329fca89d98790af7 Mon Sep 17 00:00:00 2001 From: Justkant <quent92100@gmail.com> Date: Sat, 28 Nov 2015 04:17:27 +0800 Subject: [PATCH 9/9] Fix development with docker To start dev use: - npm run create-data - npm run start-db & - npm run dev To stop dev: - Ctrl-c - npm run stop-db To start prod use: - npm run compose --- .dockerignore | 11 +++++++++++ Dockerfile | 4 ++-- api/api.js | 2 +- docker-compose.yml | 19 ++++++++++++------- package.json | 41 +++++++++++++++++++++++------------------ src/config.js | 20 +++++++++++++++----- src/server.js | 2 +- 7 files changed, 65 insertions(+), 34 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..148b9b6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.idea/ +.git/ +node_modules/ +dist/ +*.iml +webpack-stats.json +webpack-stats.debug.json +npm-debug.log +rethinkdb_data/ +uploads/ +doc/ diff --git a/Dockerfile b/Dockerfile index 1507a65..b369927 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,6 @@ RUN mkdir -p /src && cp -a /tmp/node_modules /src/ WORKDIR /src ADD . /src -EXPOSE 3000 +EXPOSE 8000 -CMD ["npm", "run", "dev"] +CMD npm run build && npm run start diff --git a/api/api.js b/api/api.js index 5f6b23a..e636d98 100644 --- a/api/api.js +++ b/api/api.js @@ -83,7 +83,7 @@ if (config.apiPort) { console.error(err); } console.info('----\n==> 🌎 API is running on port %s', config.apiPort); - console.info('==> 💻 Send requests to http://localhost:%s ', config.apiPort); + console.info('==> 💻 Send requests to http://%s:%s ', config.host, config.apiPort); }); } else { console.error('==> ERROR: No PORT environment variable has been specified'); diff --git a/docker-compose.yml b/docker-compose.yml index bcdfa1f..519c93e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,18 @@ web: build: . ports: - - "3030:3030" + - "8000:8000" links: - - "db" + - "rdb:webdb" + environment: + - DOCKER_HOST=${DOCKER_HOST} -db: +rdb: image: rethinkdb - environment: - - POSTGRES_PASSWORD=justkant - - POSTGRES_USER=kant - - POSTGRES_DB=ilovemycity + volumes_from: + - rdbdata + +rdbdata: + image: tianon/true + volumes: + - /data diff --git a/package.json b/package.json index be96c4b..5f9494d 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,11 @@ "main": "bin/server.js", "scripts": { "start": "concurrent --kill-others \"npm run start-prod\" \"npm run start-prod-api\"", + "compose": "docker-compose build && docker-compose up", + "create-data": "docker run -v /data --name rdbdata tianon/true", + "start-db": "docker run -p 8080:8080 -p 28015:28015 -p 29015:29015 --volumes-from rdbdata --rm --name rethinkdb rethinkdb", + "stop-db": "docker stop rethinkdb", + "rm-db": "docker rm rethinkdb", "start-prod": "better-npm-run start-prod", "start-prod-api": "better-npm-run start-prod-api", "build": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js", @@ -91,42 +96,42 @@ "dependencies": { "babel": "5.8.29", "babel-plugin-typecheck": "2.0.0", - "bcryptjs": "^2.3.0", + "bcryptjs": "2.3.0", "body-parser": "1.14.1", - "compression": "^1.6.0", - "cookie-parser": "^1.4.0", + "compression": "1.6.0", + "cookie-parser": "1.4.0", "express": "4.13.3", "file-loader": "0.8.5", "history": "1.13.1", - "hoist-non-react-statics": "^1.0.3", + "hoist-non-react-statics": "1.0.3", "http-proxy": "1.12.0", - "jsonwebtoken": "^5.4.1", + "jsonwebtoken": "5.4.1", "lru-memoize": "1.0.0", "map-props": "1.0.0", - "mimetypes": "^0.1.1", - "ms": "^0.7.1", - "multer": "^1.1.0", + "mimetypes": "0.1.1", + "ms": "0.7.1", + "multer": "1.1.0", "multireducer": "1.0.2", "piping": "0.3.0", "pretty-error": "1.2.0", "query-string": "3.0.0", - "react": "^0.14.2", + "react": "0.14.3", "react-document-meta": "2.0.0", "react-dom": "0.14.3", "react-redux": "4.0.0", "react-router": "1.0.0", "redux": "3.0.4", - "redux-router": "^1.0.0-beta4", - "scroll-behavior": "^0.3.0", + "redux-router": "1.0.0-beta4", + "scroll-behavior": "0.3.0", "serialize-javascript": "1.1.2", "serve-favicon": "2.3.0", "superagent": "1.4.0", - "thinky": "^2.1.11", + "thinky": "2.2.0", "url-loader": "0.5.7", "webpack-isomorphic-tools": "2.2.18" }, "devDependencies": { - "apidoc": "^0.13.1", + "apidoc": "0.13.1", "autoprefixer-stylus": "0.8.1", "babel-core": "5.8.33", "babel-eslint": "4.1.4", @@ -143,24 +148,24 @@ "eslint-loader": "1.1.1", "eslint-plugin-import": "0.10.1", "eslint-plugin-react": "3.10.0", - "extract-text-webpack-plugin": "^0.9.1", + "extract-text-webpack-plugin": "0.9.1", "json-loader": "0.5.4", "karma": "0.13.15", "karma-chrome-launcher": "0.2.1", "karma-cli": "0.1.1", "karma-firefox-launcher": "0.1.7", "karma-mocha": "0.2.1", - "karma-mocha-reporter": "1.1.2", + "karma-mocha-reporter": "1.1.3", "karma-sourcemap-loader": "0.3.6", "karma-webpack": "1.7.0", "mocha": "2.3.4", "react-a11y": "0.2.8", "react-addons-test-utils": "0.14.3", - "react-transform-catch-errors": "^1.0.0", + "react-transform-catch-errors": "1.0.0", "react-transform-hmr": "1.0.1", - "redbox-react": "^1.1.1", + "redbox-react": "1.2.0", "redux-devtools": "2.1.5", - "strip-loader": "^0.1.0", + "strip-loader": "0.1.0", "style-loader": "0.13.0", "stylus-loader": "1.4.2", "webpack": "1.12.9", diff --git a/src/config.js b/src/config.js index 7787bc0..bc2ed27 100644 --- a/src/config.js +++ b/src/config.js @@ -1,11 +1,22 @@ require('babel/polyfill'); +const host = (process.env.DOCKER_HOST ? process.env.DOCKER_HOST.match(/([0-9]+\.)+([0-9]+)/g)[0] : 'localhost'); const environment = { development: { - isProduction: false + isProduction: false, + host: 'localhost', + rethinkdb: { + host: host, + port: process.env.DB_PORT + } }, production: { - isProduction: true + isProduction: true, + host: host, + rethinkdb: { + host: process.env.WEBDB_PORT_28015_TCP_ADDR, + port: process.env.WEBDB_PORT_28015_TCP_PORT + } } }[process.env.NODE_ENV || 'development']; @@ -14,9 +25,8 @@ module.exports = Object.assign({ apiPort: process.env.APIPORT, secret: process.env.SECRET, rethinkdb: { - host: process.env.DB_HOST, - port: process.env.DB_PORT, - db: process.env.DB_NAME + db: process.env.DB_NAME, + discovery: false }, app: { title: 'WhatAShop', diff --git a/src/server.js b/src/server.js index c8a4f4c..53b100c 100644 --- a/src/server.js +++ b/src/server.js @@ -108,7 +108,7 @@ if (config.port) { console.error(err); } console.info('----\n==> ✅ %s is running, talking to API server on %s.', config.app.title, config.apiPort); - console.info('==> 💻 Open http://localhost:%s in a browser to view the app.', config.port); + console.info('==> 💻 Open http://%s:%s in a browser to view the app.', config.host, config.port); }); } else { console.error('==> ERROR: No PORT environment variable has been specified');