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}
Nike
-120$
-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.
-{product.title}
+{product.price + ' $'}
+{product.description}
+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.
-Click to create a product
} + {active && +