diff --git a/.babelrc b/.babelrc index 41d0cf6..9557a50 100644 --- a/.babelrc +++ b/.babelrc @@ -12,13 +12,15 @@ ], "extra": { "react-transform": { - "transforms": [{ - "transform": "react-transform-catch-errors", - "imports": [ - "react", - "redbox-react" - ] - }] + "transforms": [ + { + "transform": "react-transform-catch-errors", + "imports": [ + "react", + "redbox-react" + ] + } + ] } } } diff --git a/.gitignore b/.gitignore index 99c998e..e7a6731 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,8 @@ node_modules/ dist/ *.iml webpack-stats.json +webpack-stats.debug.json npm-debug.log -/rethinkdb_data +rethinkdb_data/ +uploads/ +doc/ diff --git a/api/api.js b/api/api.js index 26bc33a..c48d39c 100644 --- a/api/api.js +++ b/api/api.js @@ -6,14 +6,28 @@ import cookieParser from 'cookie-parser'; import config from '../src/config'; import { users, products } from './functions'; import PrettyError from 'pretty-error'; -import Thinky from 'thinky'; +import multer from 'multer'; +import mimetypes from 'mimetypes'; const pretty = new PrettyError(); +var storage = multer.diskStorage({ + destination: 'uploads', + filename: function (req, file, cb) { + cb(null, file.fieldname + '-' + Date.now() + '.' + mimetypes.detectExtension(file.mimetype)); + } +}); +const upload = multer({ storage: storage }); const app = express(); app.use(cookieParser()); app.use(bodyParser.json()); +const staticOptions = {}; +if (config.isProduction) { + staticOptions.maxAge = '60 days'; +} +app.use('/uploads', require('serve-static')('uploads/', staticOptions)); + app.get('/load', users.load); app.post('/login', users.login); app.get('/logout', users.logout); @@ -28,16 +42,22 @@ app.route('/users/:id') .delete(users.auth, users.isOwner, users.deleteUser); app.route('/products') - .get(products.getProducts) - .post(products.addProduct); + .get(users.auth, users.isAdmin, products.getProducts) + .post(users.auth, users.isAdmin, products.addProduct); app.route('/products/:id') - .get(products.getProduct) - .put(products.updateProduct) - .delete(products.deleteProduct); + .get(users.auth, products.getProduct) + .put(users.auth, users.isAdmin, products.updateProduct) + .delete(users.auth, users.isAdmin, products.deleteProduct); + +app.get('/market', users.auth, products.getMarket); app.get('/search/:text', products.search); +app.post('/picture', users.auth, upload.single('picture'), (req, res) => { + res.json({url: req.file.path}); +}); + if (config.apiPort) { app.listen(config.apiPort, (err) => { if (err) { diff --git a/api/functions/products.js b/api/functions/products.js index 9f21f48..3a9a33c 100644 --- a/api/functions/products.js +++ b/api/functions/products.js @@ -1,89 +1,59 @@ import { Product } from '../models'; +import { shuffle } from '../utils/functions'; function getProducts(req, res) { - res.json([{ - title: 'Title', - description: 'Nike shoes', - imageUrl: 'product.jpg', - price: '125$' - }]); + Product.orderBy('-createdAt').run().then((result) => { + res.json(result); + }); } function getProduct(req, res) { - /* pourquoi product pop en orange ? */ - res.json(req.product.getPublic()); + Product.get(req.params.id).run().then((product) => { + res.json(product); + }, (error) => { + res.status(404).json({msg: 'Product not found'}); + }); } function addProduct(req, res) { - const product = new Product({ title: req.body.title, description: req.body.description, imageUrl: req.body.imageUrl, - price: req.body.price + price: 0 }); product.save().then(() => { res.json(product); - }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); - }); + }, (error) => { + console.error(error); + res.status(500).json({msg: 'Contact an administrator', err: error}); + }); } function updateProduct(req, res) { - const product = {}; - const promises = []; - - if (req.body.title && req.body.title != req.product.title) { - promises.push(new Promise((resolve, reject) => { - product.title = req.body.title; - resolve(); - }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); - reject(); - })); - } - - if (req.body.price && req.body.price !== req.product.price) { - promises.push(new Promise((resolve) => { - product.price = req.body.price; - resolve(); - }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); - reject(); - })); - } - - if (req.body.description && req.body.description !== req.product.description) { - promises.push(new Promise((resolve) => { - product.description = req.body.description; - resolve(); - }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); - reject(); - })); - } - - Promise.all(promises).then(() => { - req.product.merge(product).save().then((result) => { - res.json(req.product.getPublic()); + Product.get(req.params.id).run().then((product) => { + product.merge(req.body).save().then((result) => { + res.json(product); }, (error) => { console.error(error); res.status(400).json({msg: 'Something went wrong', err: error}); }); + }, (error) => { + res.status(404).json({msg: 'Product not found'}); }); } function deleteProduct(req, res) { - req.product.delete().then(() => { - res.json({msg: 'Account deleted'}); + Product.get(req.params.id).run().then((product) => { + product.delete().then(() => { + res.json({msg: 'Product deleted'}); + }, (error) => { + console.error(error); + res.status(500).json({msg: 'Contact an administrator', err: error}); + }); }, (error) => { - console.error(error); - res.status(500).json({msg: 'Contact an administrator', err: error}); + res.status(404).json({msg: 'Product not found'}); }); } @@ -91,13 +61,20 @@ function search(req, res) { } +function getMarket(req, res) { + Product.run().then((result) => { + res.json(shuffle(result)); + }); +} + const products = { getProducts: getProducts, getProduct: getProduct, addProduct: addProduct, updateProduct: updateProduct, deleteProduct: deleteProduct, - search: search + search: search, + getMarket: getMarket }; export default products; diff --git a/api/functions/users.js b/api/functions/users.js index c1b8d29..bdec1a6 100644 --- a/api/functions/users.js +++ b/api/functions/users.js @@ -79,10 +79,41 @@ function logout(req, res) { res.json(null); } +/** + * @api {get} /users Request All Users + * @apiName GetUsers + * @apiGroup User + */ function getUsers(req, res) { - res.json([{username: 'kant'}]); + User.orderBy('-createdAt').run().then((result) => { + res.json(result); + }); } +/** + * @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()); } diff --git a/api/models/Product.js b/api/models/Product.js index 94481d7..ba8e0f5 100644 --- a/api/models/Product.js +++ b/api/models/Product.js @@ -6,7 +6,8 @@ const Product = thinky.createModel('Product', { title: type.string().required(), description: type.string().optional(), imageUrl: type.string().optional(), - price: type.number().required() + price: type.number().required(), + createdAt: type.date().default(thinky.r.now()) }); export default Product; diff --git a/api/models/User.js b/api/models/User.js index 64e611f..29dfefa 100644 --- a/api/models/User.js +++ b/api/models/User.js @@ -6,9 +6,10 @@ const User = thinky.createModel('User', { username: type.string().required(), email: type.string().email().required(), password: type.string().required(), - admin: type.boolean().default(false), + admin: type.boolean().default(true), createdAt: type.date().default(thinky.r.now()), - token: type.string() + token: type.string(), + pictureUrl: type.string() }); User.define('getPublic', function() { diff --git a/api/models/index.js b/api/models/index.js index c542341..6ab6ea6 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,3 +1,3 @@ export User from './User'; -//export Product from './Product'; +export Product from './Product'; export Email from './Email'; diff --git a/api/utils/functions.js b/api/utils/functions.js new file mode 100644 index 0000000..70f94b6 --- /dev/null +++ b/api/utils/functions.js @@ -0,0 +1,17 @@ +export function shuffle(array) { + var m = array.length, t, i; + + // While there remain elements to shuffle… + while (m) { + + // Pick a remaining element… + i = Math.floor(Math.random() * m--); + + // And swap it with the current element. + t = array[m]; + array[m] = array[i]; + array[i] = t; + } + + return array; +} diff --git a/apidoc.json b/apidoc.json new file mode 100644 index 0000000..4d60da2 --- /dev/null +++ b/apidoc.json @@ -0,0 +1,5 @@ +{ + "name": "WhatAShop API", + "version": "0.1.1", + "description": "API description for WhatAShop" +} diff --git a/package.json b/package.json index 586bf9f..ef526d3 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "start-dev-api": "node ./node_modules/better-npm-run start-dev-api", "watch-client": "node ./node_modules/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" + "test": "karma start", + "doc": "apidoc -i api/" }, "betterScripts": { "start-prod": { @@ -88,55 +89,56 @@ } }, "dependencies": { - "babel": "5.8.23", - "babel-plugin-typecheck": "1.3.0", + "babel": "6.0.14", + "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.12.5", + "history": "1.13.0", "http-proxy": "1.12.0", - "jsonwebtoken": "^5.4.0", + "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", "multireducer": "1.0.2", "piping": "0.3.0", "pretty-error": "1.2.0", "query-string": "3.0.0", - "react": "^0.14.0", + "react": "^0.14.1", "react-document-meta": "2.0.0", - "react-dom": "0.14.0", + "react-dom": "0.14.1", "react-redux": "4.0.0", "react-router": "1.0.0-rc3", - "redux": "3.0.3", - "redux-form": "2.4.5", + "redux": "3.0.4", "redux-router": "^1.0.0-beta3", "serialize-javascript": "1.1.2", "serve-favicon": "2.3.0", "serve-static": "1.10.0", "superagent": "1.4.0", - "thinky": "^2.1.9", + "thinky": "^2.1.11", "url-loader": "0.5.6", - "webpack-isomorphic-tools": "1.0.2" + "webpack-isomorphic-tools": "2.1.2" }, "devDependencies": { - "autoprefixer-stylus": "0.8.0", + "autoprefixer-stylus": "0.8.1", "babel-core": "5.8.25", "babel-eslint": "4.1.3", "babel-loader": "5.3.2", "babel-plugin-react-transform": "1.1.1", - "babel-runtime": "5.8.25", - "better-npm-run": "0.0.2", + "babel-runtime": "6.0.12", + "better-npm-run": "0.0.3", "chai": "3.4.0", - "clean-webpack-plugin": "0.1.3", + "clean-webpack-plugin": "0.1.4", "concurrently": "0.1.1", "css-loader": "0.21.0", - "eslint": "1.7.3", + "eslint": "1.8.0", "eslint-config-airbnb": "0.1.0", - "eslint-loader": "1.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", @@ -150,8 +152,8 @@ "karma-sourcemap-loader": "0.3.6", "karma-webpack": "1.7.0", "mocha": "2.3.3", - "react-a11y": "0.2.6", - "react-addons-test-utils": "0.14.0", + "react-a11y": "0.2.8", + "react-addons-test-utils": "0.14.1", "react-transform-catch-errors": "^1.0.0", "react-transform-hmr": "1.0.1", "redbox-react": "^1.1.1", diff --git a/server.babel.js b/server.babel.js index b79caf5..3b69db1 100644 --- a/server.babel.js +++ b/server.babel.js @@ -12,4 +12,4 @@ try { console.error(err); } -require('babel/register')(config); +require('babel-core/register')(config); diff --git a/src/client.js b/src/client.js index 2d7ba48..a7b7951 100644 --- a/src/client.js +++ b/src/client.js @@ -1,7 +1,7 @@ /** * THIS IS THE ENTRY POINT FOR THE CLIENT, JUST LIKE server.js IS THE ENTRY POINT FOR THE SERVER. */ -import 'babel/polyfill'; +import 'babel-core/polyfill'; import React from 'react'; import ReactDOM from 'react-dom'; import createHistory from 'history/lib/createBrowserHistory'; diff --git a/src/components/ProductVignette/ProductVignette.js b/src/components/ProductVignette/ProductVignette.js index 8b9ca57..41eaeb9 100644 --- a/src/components/ProductVignette/ProductVignette.js +++ b/src/components/ProductVignette/ProductVignette.js @@ -1,16 +1,22 @@ -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; import { Link } from 'react-router'; export default class ProductVignette extends Component { + static propTypes = { + product: PropTypes.object.isRequired + }; + render() { + const { product } = this.props; const styles = require('./ProductVignette.styl'); + return (
- - + +
-

Title

+

{product.title}

); diff --git a/src/components/TabNav/TabNav.styl b/src/components/TabNav/TabNav.styl index ade356a..69fbd09 100644 --- a/src/components/TabNav/TabNav.styl +++ b/src/components/TabNav/TabNav.styl @@ -15,7 +15,7 @@ border-bottom: 1px solid transparent; text-decoration: none; - &:not(.active):hover { + &:not(.active):hover, &:not(.active):focus { color: #7D7D8E; border-bottom: 1px solid #7D7D8E; } diff --git a/src/components/Title/Title.js b/src/components/Title/Title.js new file mode 100644 index 0000000..a4af674 --- /dev/null +++ b/src/components/Title/Title.js @@ -0,0 +1,20 @@ +import React, { Component, PropTypes } from 'react'; + +export default class Title extends Component { + static propTypes = { + title: PropTypes.string + }; + + render() { + const {title} = this.props; + const styles = require('./Title.styl'); + + return ( +
+
+ {title} +
+
+ ); + } +} diff --git a/src/components/Title/Title.styl b/src/components/Title/Title.styl new file mode 100644 index 0000000..0d4ef65 --- /dev/null +++ b/src/components/Title/Title.styl @@ -0,0 +1,18 @@ +.container { + flex: 1 0 55px; + display: flex; + align-items: stretch; + justify-content: center; + box-shadow: inset 0 -1px 0 #E7E7EC; +} + +.tab { + display: flex; + align-items: center; + margin-right: 30px; + color: #999; + + span { + font-weight: 300; + } +} diff --git a/src/components/index.js b/src/components/index.js index 663f588..e2ccb08 100755 --- a/src/components/index.js +++ b/src/components/index.js @@ -9,3 +9,4 @@ export Tabs from './Tabs/Tabs'; export Tab from './Tab/Tab'; export TabNav from './TabNav/TabNav'; export FakeInput from './FakeInput/FakeInput'; +export Title from './Title/Title'; diff --git a/src/containers/Market/Market.js b/src/containers/Market/Market.js index 67bc047..2690d12 100755 --- a/src/containers/Market/Market.js +++ b/src/containers/Market/Market.js @@ -1,24 +1,29 @@ 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'; -@connect(state => ({user: state.auth.user})) +@connect(state => ({market: state.product.market})) export default class Market extends Component { static propTypes = { - children: PropTypes.object, - user: PropTypes.object + market: PropTypes.array }; + static fetchDataDeferred(getState, dispatch) { + if (!isMarketLoaded(getState())) { + return dispatch(loadMarket()); + } + } + render() { - // const {user, children} = this.props; + const {market} = this.props; const styles = require('./Market.styl'); - const products = []; - for (let index = 0; index < 50; index++) { - products.push(); - } + return (
- {products} + {market && market.map((product) => { + return (); + })}
); } diff --git a/src/containers/Product/Product.js b/src/containers/Product/Product.js index 15c175a..6834b7d 100644 --- a/src/containers/Product/Product.js +++ b/src/containers/Product/Product.js @@ -1,45 +1,58 @@ -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; +import { Title } from 'components'; +import { connect } from 'react-redux'; +import { isProductLoaded, getById } from 'redux/modules/product'; +@connect(state => ({product: state.product.product})) export default class Product extends Component { + static propTypes = { + product: PropTypes.object, + params: PropTypes.object.isRequired + }; + + static fetchDataDeferred(getState, dispatch, location, params) { + if (!isProductLoaded(getState())) { + return dispatch(getById(params.id)); + } + } + render() { + const {product} = this.props; const styles = require('./Product.styl'); - return ( -
- + const finalRender = product ? ( +
+ - <div className={styles.mainInformations}> + <div className={styles.productContainer}> - <div className={styles.mainInformationsLeft}> - <h3>Brand</h3> - <h3>Price</h3> - </div> + <img src={'/api/' + product.imageUrl}/> - <div className={styles.mainInformationsRight}> - <p>Nike</p> - <p>120$</p> - </div> + <div className={styles.mainInformations}> - </div> + <div className={styles.mainInformationsLeft}> + <h3>Brand</h3> + <h3>Price</h3> + </div> - <div className={styles.additionalInformations}> - <p>Tempore quo primis auspiciis in mundanum fulgorem surgeret victura dum erunt homines Roma, ut augeretur sublimibus incrementis, foedere pacis aeternae Virtus convenit atque Fortuna plerumque dissidentes, quarum si altera defuisset, ad perfectam non venerat summitatem.</p> - </div> + <div className={styles.mainInformationsRight}> + <p>{product.title}</p> + <p>{product.price + ' $'}</p> + </div> + </div> + + <div className={styles.additionalInformations}> + <p>{product.description}</p> + </div> + </div> + </div> + ) : ( + <div className={styles.container}> + <Title title="Loading"/> </div> ); + + return finalRender; } } - -/* OLD TEST -<div className={styles.productContainer}> - <img src="product.jpg"/> - <div className={styles.productInfos}> - <h1>Title</h1> - <h2>Price</h2> - <div className={styles.productDescription}> - <p>Description : Tempore quo primis auspiciis in mundanum fulgorem surgeret victura dum erunt homines Roma, ut augeretur sublimibus incrementis, foedere pacis aeternae Virtus convenit atque Fortuna plerumque dissidentes, quarum si altera defuisset, ad perfectam non venerat summitatem.</p> - </div> - </div> -</div> -*/ diff --git a/src/containers/Product/Product.styl b/src/containers/Product/Product.styl index bf6c847..9e8b1f1 100644 --- a/src/containers/Product/Product.styl +++ b/src/containers/Product/Product.styl @@ -1,3 +1,10 @@ +.container { + overflow: auto; + display: flex; + flex-direction: column; + border-bottom: 1px solid #E7E7EC; +} + .productContainer { padding: 20px; display: flex; @@ -7,7 +14,7 @@ .productContainer img { padding: 20px; - max-width: 200px; + max-width: 500px; cursor: pointer; } diff --git a/src/containers/Products/Products.js b/src/containers/Products/Products.js index 5345fa5..338cd89 100644 --- a/src/containers/Products/Products.js +++ b/src/containers/Products/Products.js @@ -1,20 +1,124 @@ -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { create, isAllLoaded, getAll } from 'redux/modules/product'; +import ApiClient from 'helpers/ApiClient'; +@connect(state => ({products: state.product.products}), {create}) export default class Products extends Component { + static propTypes = { + create: PropTypes.func.isRequired, + products: PropTypes.array + }; + + constructor() { + super(); + this.state = { + active: false, + newFile: null + }; + this.toggleActive = this.toggleActive.bind(this); + this.addProduct = this.addProduct.bind(this); + this.open = this.open.bind(this); + this.checkFile = this.checkFile.bind(this); + } + + static fetchDataDeferred(getState, dispatch) { + if (!isAllLoaded(getState())) { + return dispatch(getAll()); + } + } + + addProduct() { + const self = this; + const {title, desc} = 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 + }; + self.props.create(product).then(() => { + self.toggleActive(); + }); + }); + } + + toggleActive() { + if (this.state.active) { + this.setState({newFile: null, picture: null}); + } + this.setState({active: !this.state.active}); + } + + checkFile(event) { + const file = event.target.files[0]; + + let picture = null; + const reader = new FileReader(); + reader.onload = (evnt) => { + picture = evnt.target.result; + this.setState({ picture: picture }); + }; + reader.readAsDataURL(file); + + const formData = new FormData(); + formData.append('picture', file); + this.setState({ newFile: formData }); + } + + open() { + const { file } = this.refs; + file.value = null; + file.click(); + } + render() { const styles = require('./Products.styl'); - const productsList = []; + const {active, picture} = this.state; + const {products} = this.props; + + const createProduct = ( + <div className={styles.createProduct + (active ? ' ' + styles.active : '')} onClick={!active && this.toggleActive}> + {!active && <p>Click to create a product</p>} + {active && + <div className={styles.formContainer}> + <div className={styles.form}> + <div className={styles.imgContainer} onClick={this.open}> + <input type="file" ref="file" onChange={this.checkFile}/> + {!picture && <span>Add an image</span>} + {picture && <img src={picture}/>} + </div> + <div className={styles.infosContainer}> + <input type="text" ref="title" placeholder="Title"/> + <textarea ref="desc" placeholder="Description..."/> + </div> + </div> + <div className={styles.buttons}> + <button className={styles.button + ' ' + styles.red} onClick={this.toggleActive}>Cancel</button> + <button className={styles.button} onClick={this.addProduct}>Add</button> + </div> + </div> + } + </div> + ); - for (let index = 0; index < 50; index++) { - productsList.push( - <div className={styles.element} key={index}> - <img src="/product.jpg"/> - <span>Product Test</span> - </div> - ); + const productsList = []; + if (products) { + for (const product of products) { + productsList.push( + <div className={styles.element} key={product.id}> + <img src={'/api/' + product.imageUrl}/> + <h4>{product.title}</h4> + </div> + ); + } } + return ( <div className={styles.container}> + {createProduct} {productsList} </div> ); diff --git a/src/containers/Products/Products.styl b/src/containers/Products/Products.styl index a4f7e58..81e3c0c 100644 --- a/src/containers/Products/Products.styl +++ b/src/containers/Products/Products.styl @@ -6,6 +6,127 @@ overflow: auto; } +.createProduct { + display: flex; + border-radius: 4px; + margin-bottom: 10px; + + &:not(.active) { + align-items: center; + justify-content: center; + border: 2px dashed #999; + cursor: pointer; + color: #999; + } + + &.active { + background: white; + box-shadow: 0 1px 2px rgba(0,0,0,.05),0 0 0 1px rgba(63,63,68,.1); + } +} + +.formContainer { + padding: 10px; + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.form { + display: flex; + flex: 0 0 150px; +} + +.imgContainer { + display: flex; + border: 1px solid #cbcbd2; + flex: 0 0 150px; + align-items: center; + justify-content: center; + font-size: 14px; + color: #999; + margin-right: 20px; + cursor: pointer; + overflow: hidden; + border-radius: 4px; + box-shadow: inset 0 1px 2px rgba(203,203,210,0.4); + + img { + max-width: 100%; + height: auto !important; + } + + input { + display: none; + } +} + +.infosContainer { + display: flex; + flex-direction: column; + flex-grow: 1; + + input { + width: 50%; + flex: 0 0 34px; + padding: 8px 12px; + margin: 0; + margin-bottom: 20px; + display: block; + border: 1px solid #cbcbd2; + background: white; + font-size: 14px; + border-radius: 4px; + box-shadow: inset 0 1px 2px rgba(203,203,210,0.4); + + &:focus { + border-color: #3585b5; + outline: 0; + } + } + + textarea { + width: 50%; + max-width: 50%; + flex: 0 0 68px; + padding: 8px 12px; + margin: 0; + margin-bottom: 20px; + display: block; + border: 1px solid #cbcbd2; + background: white; + font-size: 14px; + border-radius: 4px; + box-shadow: inset 0 1px 2px rgba(203,203,210,0.4); + resize: none; + + &:focus { + border-color: #3585b5; + outline: 0; + } + } +} + +.buttons { + display: flex; + justify-content: flex-end; +} + +.button { + margin-left: 20px; + border: 0; + border-radius: 4px; + background: #3585b5; + color: white; + font-size: 14px; + padding: 10px 20px; + cursor: pointer; + + &.red { + background: #D64242; + } +} + .element { display: flex; align-items: center; @@ -19,10 +140,9 @@ img { height: 40px; width: auto; - border-radius: 50%; } - span { + h4 { margin-left: 10px; font-size: 16px; font-weight: 400; diff --git a/src/containers/Users/Users.js b/src/containers/Users/Users.js index 4a1f5a4..1476010 100644 --- a/src/containers/Users/Users.js +++ b/src/containers/Users/Users.js @@ -8,7 +8,7 @@ export default class Users extends Component { users: PropTypes.array }; - static fetchData(getState, dispatch) { + static fetchDataDeferred(getState, dispatch) { if (!isUsersLoaded(getState())) { return dispatch(getUsers()); } @@ -20,7 +20,7 @@ export default class Users extends Component { return ( <div className={styles.container}> - {users.map((user, index) => { + {users && users.map((user, index) => { return ( <div className={styles.element} key={user.username + index}> <img src="/default-user.png"/> diff --git a/src/helpers/ApiClient.js b/src/helpers/ApiClient.js index 0bf8e5c..8fdd051 100644 --- a/src/helpers/ApiClient.js +++ b/src/helpers/ApiClient.js @@ -13,7 +13,7 @@ function formatUrl(path) { return '/api' + adjustedPath; } -export default class ApiClient { +class _ApiClient { constructor(req, res) { methods.forEach((method) => { this[method] = (path, { params, data } = {}) => { @@ -47,3 +47,7 @@ export default class ApiClient { }); } } + +const ApiClient = _ApiClient; + +export default ApiClient; diff --git a/src/helpers/Html.js b/src/helpers/Html.js index aaea203..297ea78 100644 --- a/src/helpers/Html.js +++ b/src/helpers/Html.js @@ -34,13 +34,13 @@ export default class Html extends Component { {/* 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" - rel="stylesheet" type="text/css"/> + rel="stylesheet" type="text/css" charSet="UTF-8"/> )} </head> <body> <div id="content" className="fullFlex" dangerouslySetInnerHTML={{__html: content}}/> - <script dangerouslySetInnerHTML={{__html: `window.__data=${serialize(store.getState())};`}} /> - <script src={assets.javascript.main}/> + <script dangerouslySetInnerHTML={{__html: `window.__data=${serialize(store.getState())};`}} charSet="UTF-8"/> + <script src={assets.javascript.main} charSet="UTF-8"/> </body> </html> ); diff --git a/src/redux/modules/product.js b/src/redux/modules/product.js new file mode 100644 index 0000000..da6a3ac --- /dev/null +++ b/src/redux/modules/product.js @@ -0,0 +1,142 @@ +const LOAD = 'product/LOAD'; +const LOAD_SUCCESS = 'product/LOAD_SUCCESS'; +const LOAD_FAIL = 'product/LOAD_FAIL'; +const CREATE = 'product/CREATE'; +const CREATE_SUCCESS = 'product/CREATE_SUCCESS'; +const CREATE_FAIL = 'product/CREATE_FAIL'; +const GET = 'product/GET'; +const GET_SUCCESS = 'product/GET_SUCCESS'; +const GET_FAIL = 'product/GET_FAIL'; +const GETBYID = 'product/GETBYID'; +const GETBYID_SUCCESS = 'product/GETBYID_SUCCESS'; +const GETBYID_FAIL = 'product/GETBYID_FAIL'; + +const initialState = { + loaded: false +}; + +export default function reducer(state = initialState, action = {}) { + switch (action.type) { + case LOAD: + return { + ...state, + loading: true + }; + case LOAD_SUCCESS: + return { + ...state, + loading: false, + loaded: true, + market: action.result + }; + case LOAD_FAIL: + return { + ...state, + loading: false, + loaded: false, + error: action.error + }; + case CREATE: + return { + ...state, + creating: true + }; + case CREATE_SUCCESS: + return { + ...state, + creating: false, + created: true, + createdProduct: action.result + }; + case CREATE_FAIL: + return { + ...state, + creating: false, + created: false, + error: action.error + }; + case GET: + return { + ...state, + getting: true + }; + case GET_SUCCESS: + return { + ...state, + getting: false, + got: true, + products: action.result + }; + case GET_FAIL: + return { + ...state, + getting: false, + got: false, + error: action.error + }; + case GETBYID: + return { + ...state, + gettingById: true, + product: null + }; + case GETBYID_SUCCESS: + return { + ...state, + gettingById: false, + gotById: true, + product: action.result + }; + case GETBYID_FAIL: + return { + ...state, + gettingById: false, + gotById: false, + product: null, + error: action.error + }; + default: + return state; + } +} + +export function isLoaded(globalState) { + return globalState.product && globalState.product.loaded; +} + +export function load() { + return { + types: [LOAD, LOAD_SUCCESS, LOAD_FAIL], + promise: (client) => client.get('/market') + }; +} + +export function create(product) { + return { + types: [CREATE, CREATE_SUCCESS, CREATE_FAIL], + promise: (client) => client.post('/products', { data: product }) + }; +} + +export function isAllLoaded(globalState) { + return globalState.product && globalState.product.got; +} + +export function getAll() { + return { + types: [GET, GET_SUCCESS, GET_FAIL], + promise: (client) => client.get('/products') + }; +} + +export function isProductLoaded(globalState, id) { + return globalState.product && globalState.product.gotById && + globalState.product.product.id === id; +} + +export function getById(id) { + return { + types: [GETBYID, GETBYID_SUCCESS, GETBYID_FAIL], + promise: (client) => client.get('/products/' + id) + }; +} diff --git a/src/redux/modules/reducer.js b/src/redux/modules/reducer.js index f3bb674..c6da9fe 100644 --- a/src/redux/modules/reducer.js +++ b/src/redux/modules/reducer.js @@ -4,9 +4,11 @@ import { routerStateReducer } from 'redux-router'; import auth from './auth'; import search from './search'; +import product from './product'; export default combineReducers({ router: routerStateReducer, auth, - search + search, + product }); diff --git a/src/routes.js b/src/routes.js index ff408a8..e6c7790 100644 --- a/src/routes.js +++ b/src/routes.js @@ -73,7 +73,7 @@ export default function(store) { <IndexRoute component={Profile}/> <Route path="orders" component={Orders}/> </Route> - <Route path="product" component={Product} onEnter={requireAuth}/> + <Route path="product/:id" component={Product} onEnter={requireAuth}/> <Route path="admin" component={Admin} onEnter={requireAdmin}> <IndexRoute component={Panel}/> <Route path="users" component={Users}/> diff --git a/webpack/webpack-isomorphic-tools.js b/webpack/webpack-isomorphic-tools.js index c182049..1ed53cd 100644 --- a/webpack/webpack-isomorphic-tools.js +++ b/webpack/webpack-isomorphic-tools.js @@ -27,7 +27,7 @@ module.exports = { } return (regex.test(m.name) && m.name.slice(-4) === 'styl' && m.reasons[0].moduleName.slice(-4) === 'styl'); }, - naming: function (m, options) { + 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);