├── test ├── client │ └── Home-test.js └── server │ └── server.spec.js ├── .bowerrc ├── public ├── images │ ├── chimp.png │ ├── favicon.png │ └── spiffygif_46x46.gif ├── common.js ├── index.html └── styles │ └── app.css ├── .travis.yml ├── server ├── productsController.js ├── usersController.js ├── auth-routes.js ├── templates │ └── views │ │ └── index.html ├── authController.js └── db │ ├── schema.sql │ └── db.js ├── bower.json ├── .gitignore ├── client ├── routes │ ├── D3-Chart.js │ ├── Home-Amazon-Components.js │ ├── D3-Price-Chart.js │ ├── Home-Walmart-Components.js │ ├── Home-Bestbuy-Components.js │ ├── Home-Compare-Components.js │ ├── Home-Reviews-Components.js │ ├── Dashboard.js │ └── Home.js └── scripts │ ├── index.js │ ├── d3PriceEngine.js │ └── d3Engine.js ├── LICENSE ├── package.json ├── Gruntfile.js ├── README.md ├── PRESS-RELEASE.md ├── CONTRIBUTING.md ├── server.js └── STYLE-GUIDE.md /test/client/Home-test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/lib" 3 | } 4 | -------------------------------------------------------------------------------- /public/images/chimp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/item-chimp/HEAD/public/images/chimp.png -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/item-chimp/HEAD/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/spiffygif_46x46.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/item-chimp/HEAD/public/images/spiffygif_46x46.gif -------------------------------------------------------------------------------- /public/common.js: -------------------------------------------------------------------------------- 1 | console.error("Error: Parsing file /Users/michael/code/hr/pebbleframe/client/scripts/index.js: Unexpected token (7:6)"); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.11' 5 | - '0.12' 6 | before_script: 7 | - npm install -g bower grunt-cli 8 | - bower install 9 | - mysql -u root < server/db/schema.sql -------------------------------------------------------------------------------- /server/productsController.js: -------------------------------------------------------------------------------- 1 | var db = require('./db/db.js'); 2 | 3 | module.exports = { 4 | getProduct: function(){ 5 | console.log("users"); 6 | }, 7 | addProduct:function(){ 8 | console.log("reviews"); 9 | }, 10 | createReview: function(){ 11 | console.log("watching"); 12 | } 13 | }; -------------------------------------------------------------------------------- /server/usersController.js: -------------------------------------------------------------------------------- 1 | var db = require('./db/db.js'); 2 | 3 | module.exports = { 4 | users : function(req,res){ 5 | db.once("foundUser", function(user){ 6 | res.json({username: user.get("username"), 7 | email: user.get("email")}); 8 | }); 9 | db.findUser(db.tokenUser); 10 | }, 11 | reviews:function(){ 12 | console.log("reviews"); 13 | }, 14 | watching: function(){ 15 | console.log("watching"); 16 | }, 17 | following: function(){ 18 | console.log("following"); 19 | } 20 | }; -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pebbleframe", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/pebbleframe/pebbleframe", 5 | "authors": [ 6 | "Michael Cheng ", 7 | "Christina Holland", 8 | "Jeff Peoples", 9 | "Vinaya Gopisetti" 10 | ], 11 | "license": "MIT", 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "public/lib", 17 | "test", 18 | "tests" 19 | ], 20 | "dependencies": { 21 | "bootstrap": "~3.3.4", 22 | "jquery": "~2.1.4", 23 | "d3": "~3.5.5" 24 | }, 25 | "devDependencies": { 26 | "mocha": "~2.2.4", 27 | "chai": "~2.3.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/server/server.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var expect = require('expect'); 5 | var app = require('../../server'); 6 | var request = require('supertest'); 7 | 8 | describe('Server: routes', function() { 9 | 10 | it('should serve the home page', function(done) { 11 | request(app) 12 | .get('/') 13 | .end(function(err, res) { 14 | expect(res.statusCode).toEqual(200); 15 | done(); 16 | }); 17 | }); 18 | 19 | it('should 404 for non-root get requests', function(done) { 20 | request(app) 21 | .get('/nonexistentroute') 22 | .end(function(err, res) { 23 | expect(res.statusCode).toEqual(404); 24 | done(); 25 | }); 26 | }); 27 | 28 | }); 29 | 30 | })(); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Bower Components 30 | public/lib 31 | 32 | .idea/ 33 | 34 | # Server changes for Auth and Database 35 | testServer.js 36 | 37 | # Compiled index.js file public/scripts/index.js 38 | public/scripts/index.js 39 | -------------------------------------------------------------------------------- /server/auth-routes.js: -------------------------------------------------------------------------------- 1 | var authController = require('./authController.js'); 2 | var usersController = require('./usersController.js'); 3 | var productsController = require('./productsController.js'); 4 | var authorize = authController.authorize; 5 | 6 | 7 | module.exports = function(app) { 8 | app.post('/signup', authController.signup); 9 | app.post('/login', authController.login); 10 | app.get('/users/', authorize, usersController.users); 11 | app.get('/users/reviews', authorize, usersController.reviews); 12 | app.get('/users/watching', authorize, usersController.watching); 13 | app.get('/users/following', authorize, usersController.following); 14 | app.get('/products/', authorize, productsController.getProduct); 15 | app.post('/products/', authorize, productsController.addProduct); 16 | app.post('/products/review', authorize, productsController.createReview); 17 | }; -------------------------------------------------------------------------------- /client/routes/D3-Chart.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var d3Engine = require('../scripts/d3Engine.js'); 3 | 4 | var D3Chart = React.createClass({ 5 | startEngine: function(width, height, products) { 6 | // expected structure of products: 7 | // [ 8 | // { 9 | // name: 'Apple iPhone...etc.', 10 | // source: 'Best Buy', 11 | // reviews: this.props.bestbuyData.bestbuyReviews 12 | // }, 13 | // { 14 | // name: 'Apple iPhone...etc.', 15 | // source: 'Walmart', 16 | // reviews: this.props.walmartData.walmartReviews 17 | // }, 18 | // ] 19 | var el = this.getDOMNode(); 20 | 21 | d3Engine.create(el, width, height, products); 22 | }, 23 | render: function() { 24 | return ( 25 |
26 | 27 |
28 | ); 29 | } 30 | }); 31 | 32 | module.exports.D3Chart = D3Chart; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | ItemChimp 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /server/templates/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | ShopChimp 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 PebbleFrame 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /client/routes/Home-Amazon-Components.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | // Component that displays related results from Amazon API 4 | var AmazonRelatedResultsDisplay = React.createClass({ 5 | render: function() { 6 | var resultNodes = this.props.data.amazon.map(function(result, index) { 7 | var attributes = result.ItemAttributes[0]; 8 | // console.log(result.SmallImage.URL) 9 | 10 | return ( 11 | 12 | ); 13 | }); 14 | return ( 15 |
16 |

Amazon Related Results

17 | {resultNodes} 18 |
19 | ); 20 | } 21 | }); 22 | 23 | // Component that displays individual results for Amazon 24 | var AmazonIndividualResultDisplay = React.createClass({ 25 | render: function() { 26 | return ( 27 |
28 |
29 | {this.props.name} 30 |
31 |
32 | ); 33 | } 34 | }); 35 | 36 | module.exports.AmazonRelatedResultsDisplay = AmazonRelatedResultsDisplay; 37 | 38 | module.exports.AmazonIndividualResultDisplay = AmazonIndividualResultDisplay; -------------------------------------------------------------------------------- /server/authController.js: -------------------------------------------------------------------------------- 1 | var db = require('./db/db.js'), 2 | jwt = require('jwt-simple'); 3 | 4 | module.exports = { 5 | signup: function (req, res) { 6 | db.once("userAdded", function(token){ 7 | //Sign Up Successful 8 | if(token){ 9 | res.send(true); 10 | } 11 | //Sign Up Failure 12 | else{ 13 | res.send(false); 14 | 15 | } 16 | }); 17 | db.addUser(req.body); 18 | }, 19 | login: function(req,res){ 20 | db.once("userLogin", function(user){ 21 | //Login Successful; user obj contains token property 22 | if(user){ 23 | res.json(user); 24 | //Login Failure 25 | }else{ 26 | console.log("Login Failed"); 27 | res.send(false); 28 | } 29 | return; 30 | }); 31 | db.login(req.body); 32 | }, 33 | 34 | //if a user has signed in, they will have a token 35 | //this function tests if the user has a token 36 | //if the user has a token, the user is given 37 | //access to the desired route 38 | authorize: function(req, res, next) { 39 | var token = req.headers['x-access-token']; 40 | if (!token) { 41 | console.log("No Token Provided"); 42 | res.send(false); 43 | } 44 | else { 45 | var userName = jwt.decode(token, 'secret'); 46 | db.once('foundUser', function(){ 47 | db.tokenUser = userName; 48 | console.log("Authorized " + userName); 49 | next(); 50 | }); 51 | db.findUser(userName); 52 | } 53 | } 54 | }; 55 | 56 | -------------------------------------------------------------------------------- /client/routes/D3-Price-Chart.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var d3PriceEngine = require('../scripts/d3PriceEngine'); 3 | 4 | module.exports = React.createClass({ 5 | startEngine: function(width, height) { 6 | 7 | // Query will be displayed at the top of the D3 price chart 8 | var query = this.props.query; 9 | // This array will populate the D3 data 10 | var pricesArray = []; 11 | 12 | // Push the 10 walmart results (name and price) to pricesArray 13 | this.props.walmartRelatedResults.results.forEach(function(item) { 14 | var itemObject = { 15 | name: item.name, 16 | salePrice: item.salePrice, 17 | source: 'Walmart' 18 | }; 19 | pricesArray.push(itemObject); 20 | }); 21 | 22 | // Push the 10 best buy results (name and price) to pricesArray 23 | // Number of results for each store must be the same for the way the D3 price chart is currently set up 24 | this.props.bestbuyRelatedResults.results.forEach(function(item) { 25 | var itemObject = { 26 | name: item.name, 27 | salePrice: item.salePrice, 28 | source: 'Best Buy' 29 | }; 30 | pricesArray.push(itemObject); 31 | }); 32 | 33 | // Create the D3 price chart 34 | d3PriceEngine(pricesArray, query); 35 | 36 | }, 37 | 38 | render: function() { 39 | return ( 40 |
41 | 42 |
43 | Results for {this.props.query} 44 |
45 | 46 | 47 | 48 |
49 | ); 50 | } 51 | 52 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ItemChimp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/PebbleFrame/shopagator.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/PebbleFrame/shopagator/issues" 17 | }, 18 | "homepage": "https://github.com/PebbleFrame/shopagator", 19 | "dependencies": { 20 | "apac": "^1.0.0", 21 | "bcrypt-nodejs": "0.0.3", 22 | "body-parser": "^1.12.3", 23 | "bookshelf": "^0.7.9", 24 | "browserify-middleware": "^5.0.2", 25 | "express": "^4.12.3", 26 | "famous": "^0.3.5", 27 | "jquery": "^2.1.4", 28 | "jwt-simple": "^0.3.0", 29 | "knex": "^0.7.6", 30 | "lodash": "^3.8.0", 31 | "mysql": "^2.6.2", 32 | "node-event-emitter": "0.0.1", 33 | "nunjucks": "^1.3.4", 34 | "react": "^0.13.2", 35 | "react-famous": "^0.1.7", 36 | "react-router": "^0.13.3", 37 | "reactify": "^1.1.0", 38 | "request": "^2.55.0", 39 | "util": "^0.10.3" 40 | }, 41 | "devDependencies": { 42 | "expect": "^1.6.0", 43 | "grunt": "^0.4.5", 44 | "grunt-browserify": "^3.8.0", 45 | "grunt-concurrent": "^1.0.0", 46 | "grunt-contrib-jshint": "^0.11.2", 47 | "grunt-contrib-watch": "^0.6.1", 48 | "grunt-jest": "^0.1.3", 49 | "grunt-mocha-test": "^0.12.7", 50 | "grunt-react": "^0.12.2", 51 | "grunt-shell": "^1.1.2", 52 | "jest-cli": "^0.4.1", 53 | "mocha": "^2.2.4", 54 | "supertest": "^0.15.0" 55 | }, 56 | "scripts": { 57 | "test": "grunt test" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | 5 | concurrent: { 6 | dev: { 7 | tasks: ['shell:nodemon', 'watch:react'], 8 | options: { 9 | logConcurrentOutput: true 10 | } 11 | } 12 | }, 13 | 14 | jshint: { 15 | ignore_warning: { 16 | options: { 17 | '-W117': true, 18 | }, 19 | src: ['Gruntfile.js', 'server.js', 'server/**/*.js', 'test/**/*.js'], 20 | }, 21 | }, 22 | 23 | watch: { 24 | jshint: { 25 | files: ['<%= jshint.files %>'], 26 | tasks: ['jshint'] 27 | }, 28 | react: { 29 | files: ['client/routes/*.js', 'client/scripts/*.js'], 30 | tasks:['browserify'] 31 | } 32 | }, 33 | 34 | browserify: { 35 | options: { 36 | transform: [ require('grunt-react').browserify ] 37 | }, 38 | app: { 39 | src: 'client/scripts/index.js', 40 | dest: 'public/scripts/index.js' 41 | } 42 | }, 43 | 44 | shell: { 45 | nodemon: { 46 | command: 'nodemon server.js' 47 | } 48 | }, 49 | 50 | mochaTest: { 51 | test: { 52 | options: { 53 | reporter: 'spec' 54 | }, 55 | src: ['test/**/*.js'] 56 | } 57 | }, 58 | 59 | jest: { 60 | options: { 61 | coverage: true, 62 | testPathPattern: /.*-test.js/ 63 | } 64 | } 65 | 66 | }); 67 | 68 | grunt.loadNpmTasks('grunt-contrib-jshint'); 69 | grunt.loadNpmTasks('grunt-contrib-watch'); 70 | grunt.loadNpmTasks('grunt-browserify'); 71 | grunt.loadNpmTasks('grunt-shell'); 72 | grunt.loadNpmTasks('grunt-concurrent'); 73 | grunt.loadNpmTasks('grunt-mocha-test'); 74 | grunt.loadNpmTasks('grunt-jest'); 75 | 76 | grunt.registerTask('serve', [ 77 | 'browserify', 78 | 'concurrent:dev' 79 | ]); 80 | 81 | grunt.registerTask('test', [ 82 | 'jshint', 83 | 'mochaTest' 84 | ]); 85 | 86 | grunt.registerTask('default', [ 87 | 'browserify', 88 | 'concurrent:dev' 89 | ]); 90 | 91 | }; -------------------------------------------------------------------------------- /client/routes/Home-Walmart-Components.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | // Component that displays related results from Walmart API 4 | var WalmartRelatedResultsDisplay = React.createClass({ 5 | handleReviewRequest: function(itemId, site, name, image) { 6 | this.props.onReviewRequest(itemId, site, name, image); 7 | }, 8 | render: function() { 9 | var resultNodes = this.props.data.results.map(function(result, index) { 10 | 11 | result.shortDescription = result.shortDescription || 'n/a'; 12 | 13 | return ( 14 | 26 | ); 27 | }.bind(this)); 28 | return ( 29 |
30 |

Walmart Related Results

31 | {resultNodes} 32 |
33 | ); 34 | } 35 | }); 36 | 37 | // Component that displays an individual result for Walmart 38 | var WalmartIndividualResultDisplay = React.createClass({ 39 | handleReviewRequest: function() { 40 | this.props.onReviewRequest({itemId: this.props.itemId}, 'Walmart', this.props.name, this.props.thumbnailImage); 41 | }, 42 | render: function() { 43 | return ( 44 |
45 |
46 | {this.props.name} 47 |
48 | 49 |
50 | ${this.props.salePrice} 51 |
52 |
53 | Description: {this.props.shortDescription} 54 |
55 |
56 | Rating: {this.props.customerRating} ({this.props.numReviews} reviews) 57 |
58 | 59 | 60 |
61 | ); 62 | } 63 | }); 64 | 65 | 66 | module.exports.WalmartRelatedResultsDisplay = WalmartRelatedResultsDisplay; 67 | 68 | 69 | -------------------------------------------------------------------------------- /client/routes/Home-Bestbuy-Components.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | // Component that displays related results from Best Buy API 4 | var BestbuyRelatedResultsDisplay = React.createClass({ 5 | handleReviewRequest: function(sku, name, image, reviewAverage, reviewCount) { 6 | this.props.onReviewRequest(sku, name, image, reviewAverage, reviewCount); 7 | }, 8 | render: function() { 9 | var resultNodes = this.props.data.results.map(function(result, index) { 10 | 11 | result.shortDescription = result.shortDescription || 'n/a'; 12 | result.customerReviewAverage = result.customerReviewAverage || 'n/a'; 13 | result.customerReviewCount = result.customerReviewCount || 'No'; 14 | 15 | return ( 16 | 27 | ); 28 | }.bind(this)); 29 | return ( 30 |
31 |

Best Buy Related Results

32 | {resultNodes} 33 |
34 | ); 35 | } 36 | }); 37 | 38 | // Component that displays an individual result for Best Buy 39 | var BestbuyIndividualResultDisplay = React.createClass({ 40 | handleReviewRequest: function() { 41 | $('.bestbuy-reviews-display').removeClass('hidden'); 42 | this.props.onReviewRequest({sku: this.props.sku}, 'Best Buy', this.props.name, this.props.image, 43 | this.props.customerReviewAverage, this.props.customerReviewCount); 44 | }, 45 | render: function() { 46 | return ( 47 |
48 |
49 | {this.props.name} 50 |
51 | 52 |
53 | ${this.props.salePrice} 54 |
55 |
56 | Description: {this.props.shortDescription} 57 |
58 |
59 | Rating: {this.props.customerReviewAverage} ({this.props.customerReviewCount} reviews) 60 |
61 |
62 | ); 63 | } 64 | }); 65 | 66 | 67 | module.exports.BestbuyRelatedResultsDisplay = BestbuyRelatedResultsDisplay; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ItemChimp 2 | 3 | > A Data Visualization Tool for Shoppers 4 | 5 | ## Team 6 | 7 | - __Product Owner__: Christina Holland 8 | - __Scrum Master__: Michael Cheng 9 | - __Development Team Members__: Jeff Peoples, Vinaya Gopisetti 10 | 11 | ## Table of Contents 12 | 13 | 1. [Usage](#Usage) 14 | 1. [Requirements](#requirements) 15 | 1. [Development](#development) 16 | 1. [Installing Dependencies](#installing-dependencies) 17 | 1. [Tasks](#tasks) 18 | 1. [Team](#team) 19 | 1. [Contributing](#contributing) 20 | 21 | ## Usage 22 | 23 | > This app is built with React.js on the front-end and Node.js/Express on the back-end. There are several major parts of this app: 24 | 25 | 1. Front-end: React.js - React allows each part of the UI to be broken into modular components. These components can be inserted into other components easily to maintain an organized separation of concerns. The various components for the React front-end are found in the `client` folder. 26 | 27 | 1. Back-end: Node.js/Express Framework - The Express framework provides middleware to make working with Node much simpler. 28 | 29 | 1. Database: MySql (Bookshelf ORM) - MySql, a relational database, is used to store user and review data. The schema and models for our MySql database can be found in `server/db`. 30 | 31 | 1. APIs: The two primary APIs used in this app are the Walmart and Best Buy APIs. The Amazon API is also incorporated into this app, but unfortunately, Amazon does not return reviews directly, so we cannot utilize its review data for our D3 visualization. 32 | 33 | 1. D3 - D3 is used to visualize the data retrieved from API requests. 34 | 35 | 1. Browserify - Browserify is used to allow the `require` statement to be used on browser code. It recursively analyzes all the `require` calls in the app and builds a bundle that is served up to the browser in a single ` 377 | 378 | 379 | 380 | ``` 381 | -------------------------------------------------------------------------------- /client/routes/Dashboard.js: -------------------------------------------------------------------------------- 1 | React = require('react'); 2 | 3 | var Dashboard = React.createClass({ 4 | //only an empty div is rendered to the page until this function is callled 5 | //and either a username is set or login is set to true 6 | //this function checks if the client has a token and if it does 7 | //it retrieves the user data from server and sets the state 8 | //for username and email 9 | loadUserFromServer: function(){ 10 | if(this.state.token){ 11 | $.ajax({ 12 | type: 'GET', 13 | url: '/auth/users', 14 | headers: {'x-access-token': this.state.token}, 15 | success: function (data) { 16 | this.setState({ 17 | username : data.username, 18 | email : data.email}); 19 | }.bind(this), 20 | error: function(xhr,status,err){ 21 | console.error('/auth/users', status, err.toString()); 22 | }.bind(this) 23 | }); 24 | } 25 | else{ 26 | this.setState({login:true}) 27 | } 28 | }, 29 | //Here the components initial state is set 30 | //If the client has a token, then it is set 31 | getInitialState: function() { 32 | if(!localStorage.getItem('tokenChimp')){ 33 | return { 34 | token: false, 35 | username : false, 36 | email : false, 37 | }; 38 | } 39 | else { 40 | console.log("Getting Token") 41 | var token = localStorage.getItem('tokenChimp'); 42 | } 43 | return { 44 | token: token, 45 | username: false, 46 | email: false, 47 | login: false 48 | }; 49 | }, 50 | //this is called after the component is initially rendered 51 | //which on any new visit to the page, will be right after the empty 52 | //div is rendered 53 | componentDidMount: function(){ 54 | this.loadUserFromServer(); 55 | }, 56 | //If a new user is successfully added to database 57 | //they are passed onto the login submit, where they are logged in 58 | handleSignupSubmit: function(user){ 59 | $.ajax({ 60 | url: '/auth/signup', 61 | dataType: 'json', 62 | type: 'POST', 63 | data: user, 64 | success: function(data) { 65 | if(data) { 66 | console.log("Added new User; Logging in") 67 | this.handleLoginSubmit({username: user.username, password: user.password}); 68 | } 69 | }.bind(this), 70 | error: function(xhr, status, err) { 71 | console.log('/auth/login', status, err.toString()); 72 | }.bind(this) 73 | }); 74 | }, 75 | //if user is in database and password is valid 76 | //user is given token and state is set 77 | handleLoginSubmit: function(user) { 78 | $.ajax({ 79 | url: '/auth/login', 80 | dataType: 'json', 81 | type: 'POST', 82 | data: user, 83 | success: function(data) { 84 | if(data) { 85 | console.log("setting login state") 86 | this.setState({ 87 | username: data.username, 88 | email: data.email 89 | }); 90 | localStorage.setItem('tokenChimp', data.token); 91 | } 92 | }.bind(this), 93 | error: function(xhr, status, err) { 94 | console.log('/auth/login', status, err.toString()); 95 | }.bind(this) 96 | }); 97 | }, 98 | //users token is destroyed, state changed to reflect 99 | handleLogout: function(){ 100 | this.setState({ 101 | username: false, 102 | email: false, 103 | }); 104 | localStorage.removeItem('tokenChimp'); 105 | this.setState({login: true}); 106 | }, 107 | 108 | //Component is rendered depenending on state; if a user is logged in 109 | //then dashboard is rendered; if user is not logged in login portal is rendered 110 | //it renders an empty div initially so that user information can be checked before 111 | //significant rendering and also prevents visual glitch when entering dashboard 112 | //with a token--prevents the signup page from showing before switch to dashboard 113 | render: function() { 114 | if(this.state.username) { 115 | return ( 116 |
117 |
118 |
119 |
120 |

Welcome {this.state.username}!

121 |
122 |
123 | 124 |
125 |
126 |
127 |
128 | 129 | 130 | 131 | 132 | 133 |
134 |
135 |
136 |
137 | ); 138 | } 139 | else 140 | if(this.state.login) 141 | { 142 | return( 143 |
144 |
145 | 146 | 147 |
148 |
149 | ); 150 | } 151 | else{ 152 | return(
); 153 | } 154 | } 155 | }); 156 | 157 | 158 | 159 | //Is rendered when user needs to sign in or signup 160 | var UserLoginPanel = React.createClass({ 161 | handleSubmit:function(e){ 162 | e.preventDefault(); 163 | var username = React.findDOMNode(this.refs.username).value.trim(); 164 | var password = React.findDOMNode(this.refs.password).value.trim(); 165 | if (!username || !password) { 166 | return; 167 | } 168 | this.props.onLoginSubmit({username: username, password: password}); 169 | React.findDOMNode(this.refs.username).value = ''; 170 | React.findDOMNode(this.refs.password).value = ''; 171 | return; 172 | }, 173 | render: function(){ 174 | return( 175 |
176 |

Member Login

177 |
178 | 179 | 180 | 181 |
182 |
183 | ) 184 | } 185 | }); 186 | 187 | //Is rendered when user needs to sign in or signup 188 | var SignUpPanel = React.createClass({ 189 | handleSubmit:function(e){ 190 | e.preventDefault(); 191 | var username = React.findDOMNode(this.refs.username).value.trim(); 192 | var password = React.findDOMNode(this.refs.password).value.trim(); 193 | var email = React.findDOMNode(this.refs.email).value.trim(); 194 | if (!username || !password || !email) { 195 | return; 196 | } 197 | this.props.onSignupSubmit({username: username, password: password, email: email}); 198 | React.findDOMNode(this.refs.username).value = ''; 199 | React.findDOMNode(this.refs.password).value = ''; 200 | React.findDOMNode(this.refs.email).value = ''; 201 | return; 202 | }, 203 | render: function(){ 204 | return( 205 |
206 |

Sign Up

207 |
208 | 209 | 210 | 211 | 212 |
213 |
214 | ) 215 | } 216 | }); 217 | 218 | //Rendered after signing in 219 | var WatchingPanel = React.createClass({ 220 | render: function(){ 221 | return( 222 |
223 | 230 |
231 |
232 | 233 |
234 |
235 |
236 | ); 237 | } 238 | }); 239 | 240 | //Rendered after signing in 241 | var PasswordPanel = React.createClass({ 242 | render: function(){ 243 | return( 244 |
245 | 252 |
253 |
254 | 255 |
256 |
257 |
258 | ); 259 | } 260 | }); 261 | 262 | //Rendered after signing in 263 | var ContactPanel = React.createClass({ 264 | render: function(){ 265 | return( 266 |
267 | 274 |
275 |
276 | 277 |
278 |
279 |
280 | ); 281 | } 282 | }) 283 | 284 | //Rendered after signing in 285 | var FollowingPanel = React.createClass({ 286 | render: function(){ 287 | return( 288 |
289 | 296 |
297 |
298 | 299 |
300 |
301 |
302 | ); 303 | } 304 | }) 305 | 306 | //Rendered after signing in 307 | var FollowersPanel = React.createClass({ 308 | render: function(){ 309 | return( 310 |
311 | 318 |
319 |
320 | 321 |
322 |
323 |
324 | ); 325 | } 326 | }) 327 | 328 | //Rendered after signing in 329 | var WatchingBox = React.createClass({ 330 | render: function() { 331 | return ( 332 |
333 | Hello 334 |
335 | ); 336 | } 337 | }); 338 | 339 | //Rendered after signing in 340 | var ChangePasswordBox = React.createClass({ 341 | render: function() { 342 | return ( 343 |
344 | 345 | 346 | 347 |
348 | ); 349 | } 350 | }); 351 | 352 | //Rendered after signing in 353 | var EditContactInfoBox = React.createClass({ 354 | render: function() { 355 | return ( 356 |
357 |
Email: {this.props.email}
358 | 359 |
360 | ); 361 | } 362 | }); 363 | 364 | //Rendered after signing in 365 | var YouAreFollowingBox = React.createClass({ 366 | render: function() { 367 | return ( 368 |
369 | 370 | 371 |
372 | ); 373 | } 374 | }); 375 | 376 | //Rendered after signing in 377 | var FollowingYouBox = React.createClass({ 378 | render: function() { 379 | return ( 380 |
381 | 382 | 383 |
384 | ); 385 | } 386 | }); 387 | 388 | //Rendered after signing in 389 | var FavoriteUsersDisplay = React.createClass({ 390 | handleUnfollow: function(e) { 391 | e.preventDefault(); 392 | console.log('requested to unfollow ' + this.props.user); 393 | }, 394 | render: function() { 395 | return ( 396 |
397 | {this.props.user} 398 | unfollow 399 |
400 | ); 401 | } 402 | }); 403 | 404 | //Rendered after signing in 405 | var FollowersDisplay = React.createClass({ 406 | handleUnfollow: function(e) { 407 | e.preventDefault(); 408 | console.log('requested to unfollow ' + this.props.user); 409 | }, 410 | handleFollow: function(e) { 411 | e.preventDefault(); 412 | console.log('requested to follow ' + this.props.user); 413 | }, 414 | render: function() { 415 | return ( 416 |
417 | {this.props.user} 418 | follow 419 | unfollow 420 |
421 | ); 422 | } 423 | }); 424 | 425 | 426 | module.exports = Dashboard; 427 | -------------------------------------------------------------------------------- /client/routes/Home.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var WalmartRelatedResultsDisplay = require('./Home-Walmart-Components').WalmartRelatedResultsDisplay; 4 | var BestbuyRelatedResultsDisplay = require('./Home-Bestbuy-Components').BestbuyRelatedResultsDisplay; 5 | var ReviewsDisplaySection = require('./Home-Reviews-Components').ReviewsDisplaySection; 6 | var ChooseAnotherProductSection = require('./Home-Compare-Components').ChooseAnotherProductSection; 7 | var D3Chart = require('./D3-Chart').D3Chart; 8 | 9 | var D3PriceChart = require('./D3-Price-Chart'); 10 | 11 | // Centralized display for all components on the Home page 12 | var DisplayBox = React.createClass({ 13 | // Sets initial state properties to empty arrays to avoid undefined errors 14 | getInitialState: function() { 15 | return { 16 | // We set the initial state to the format {'API name': [Array of results]} 17 | // to help organize the results we get back from the server, since the 18 | // general-query request returns results from three different APIs 19 | amazon: {results: []}, 20 | walmart: {results: []}, 21 | bestbuy: {results: []}, 22 | allReviews: {reviewSets: []} 23 | }; 24 | }, 25 | 26 | // Called when user submits a query 27 | handleQuerySubmit: function(query) { 28 | $.ajax({ 29 | url: 'general-query', 30 | dataType: 'json', 31 | type: 'POST', 32 | data: query, 33 | success: function(data) { 34 | 35 | // reset review column data to empty 36 | this.setState({ 37 | allReviews: { reviewSets: [] }, 38 | }); 39 | this.adjustColumnDisplay(); 40 | 41 | // Show Related Results after user submits query 42 | $('.related-results-display-container').fadeIn(); 43 | $('.logo-container').slideUp(); 44 | $('.query-form').find('input').attr('placeholder', 'Search again'); 45 | 46 | // Show D3 price chart 47 | $('.d3-price-container').show(); 48 | 49 | // Set the state to contain data for each separate API 50 | // data[0] --> {walmart: [Array of Walmart results]} 51 | // data[1] --> {amazon: [Array of Amazon results]} 52 | // data[2] --> {bestbuy: [Array of Best Buy results]} 53 | var wmResults = {results: data[0].walmart}; 54 | var bbResults = {results: data[1].bestbuy}; 55 | this.setState({ 56 | walmart: wmResults, 57 | bestbuy: bbResults, 58 | // We removed Amazon because they do not allow keys to be in our public repo 59 | // amazon: data[2], 60 | query: query.query 61 | }); 62 | 63 | // initialize d3 price chart 64 | // params are (width, height) 65 | this.refs.d3PriceChart.startEngine(500, 275); 66 | 67 | // Hide the spinner after all API requests have been completed 68 | $('.query-form-container img').hide(); 69 | 70 | }.bind(this), 71 | error: function(xhr, status, err) { 72 | console.error('general-query', status, err.toString()); 73 | }.bind(this) 74 | }); 75 | }, 76 | // Final handler for reviews request 77 | // This call is the result of calls bubbling up from the individual review results 78 | // var "id" may be itemId or SKU 79 | handleReviewRequest: function(id, site, name, image) { 80 | 81 | var queryUrl; 82 | 83 | if (site === 'Walmart') { 84 | queryUrl = 'get-walmart-reviews'; 85 | } else if (site === 'Best Buy') { 86 | queryUrl = 'get-bestbuy-reviews'; 87 | } 88 | 89 | // Makes a specific API call to get reviews for the product clicked on 90 | $.ajax({ 91 | url: queryUrl, 92 | dataType: 'json', 93 | type: 'POST', 94 | // "id" is itemId for Walmart 95 | // and it's SKU for Best Buy 96 | data: id, 97 | success: function(data) { 98 | 99 | // Remove the general results display to display reviews 100 | $('.related-results-display-container').fadeOut(); 101 | $('.d3-price-container').fadeOut(); 102 | 103 | // Display the reviews-display only after an item is clicked on 104 | $('.reviews-display-container').delay(500).fadeIn(); 105 | $('.d3-container').delay(500).fadeIn(); 106 | $('.choose-another-product-section').delay(500).fadeIn(); 107 | 108 | // Create array of review sets to show 109 | var reviewSetsArray = []; 110 | 111 | if (data[0].walmartReviews) { 112 | // Get the reviews array from the response data 113 | reviewSetsArray.push( 114 | this.makeReviewSetFromRawData( 115 | JSON.parse(data[0].walmartReviews), 'Walmart', name, image 116 | ) 117 | ); 118 | } 119 | if (data[0].bestbuyReviews) { 120 | // Get the reviews array from the response data 121 | reviewSetsArray.push( 122 | this.makeReviewSetFromRawData( 123 | JSON.parse(data[0].bestbuyReviews), 'Best Buy', name, image 124 | ) 125 | ); 126 | 127 | } 128 | // Put all reviews into an array stored in allReviews state 129 | this.setState({ 130 | allReviews: { reviewSets: reviewSetsArray }, 131 | }); 132 | 133 | this.adjustColumnDisplay(); 134 | 135 | // initialize d3 chart 136 | // params are (width, height) 137 | this.refs.d3chart.startEngine(500, 225, reviewSetsArray); 138 | 139 | }.bind(this), 140 | error: function(xhr, status, err) { 141 | console.error(queryUrl, status, err.toString()); 142 | }.bind(this) 143 | }); 144 | }, 145 | 146 | // take raw review data from different sites and turn it into an 147 | // object that is more or less the same across different stores... 148 | // or at least has some same property names. 149 | // This enables the review columns display functions to be more 150 | // or less agnostic about where the data came from 151 | makeReviewSetFromRawData: function(rawObj, site, name, image) { 152 | var ReviewsFromData; 153 | var AverageRating; 154 | var ReviewCount; 155 | 156 | if (site === 'Walmart') { 157 | // array of reviews 158 | ReviewsFromData = rawObj.reviews; 159 | AverageRating = rawObj.reviewStatistics.averageOverallRating; 160 | ReviewCount = rawObj.reviewStatistics.totalReviewCount; 161 | // saves id of current item so it won't show up in 162 | // "choose another product" column 163 | // doesn't hold up if you have 2 columns with 2 different 164 | // items 165 | this.setState({currentProductItemID: rawObj.itemId}); 166 | return ({ 167 | source: 'Walmart', 168 | name: name, 169 | image: image, 170 | Reviews: ReviewsFromData, 171 | AverageRating: AverageRating, 172 | ReviewCount: ReviewCount 173 | }); 174 | } else if (site === 'Best Buy') { 175 | // array of reviews 176 | ReviewsFromData = rawObj.reviews; 177 | AverageRating = rawObj.customerReviewAverage; 178 | ReviewCount = rawObj.total; 179 | // saves id of current item so it won't show up in 180 | // "choose another product" column 181 | // doesn't hold up if you have 2 columns with 2 different 182 | // items 183 | this.setState({currentProductSKU: rawObj.reviews[0].sku}); 184 | return({ 185 | source: 'Best Buy', 186 | name: name, 187 | image: image, 188 | Reviews: ReviewsFromData, 189 | AverageRating: AverageRating, 190 | ReviewCount: ReviewCount 191 | }); 192 | 193 | } 194 | }, 195 | 196 | // checks how many columns (items in reviewSets array) and 197 | // switches to 3-across styling if there's 3, or 2-across 198 | // styling if there's less than 3 199 | adjustColumnDisplay: function() { 200 | 201 | if (this.state.allReviews.reviewSets.length > 2) { 202 | // switch classes on columns to allow 3-across column display 203 | $('.reviews-display') 204 | .addClass('reviews-display-3-across') 205 | .removeClass('reviews-display') 206 | $('.reviews-display-section') 207 | .addClass('reviews-display-section-3-across') 208 | .removeClass('reviews-display-section') 209 | // hide compare selection column 210 | $('.choose-another-product-section').fadeOut(); 211 | } else { 212 | // switch classes on columns to go back to 2-column display 213 | $('.reviews-display-3-across') 214 | .addClass('reviews-display') 215 | .removeClass('reviews-display-3-across') 216 | $('.reviews-display-section-3-across') 217 | .addClass('reviews-display-section') 218 | .removeClass('reviews-display-section-3-across') 219 | // show compare selection column 220 | $('.choose-another-product-section').fadeIn(); 221 | } 222 | 223 | }, 224 | 225 | // Handles event where user clicks on an item in "choose another 226 | // product" column, adds another column with reviews for that 227 | // product 228 | handleCompareRequest: function(id, site, name, image) { 229 | 230 | var queryUrl; 231 | var data; 232 | 233 | // id for lookup will be itemId for walmart and sku for Best Buy 234 | if (site === 'Walmart') { 235 | queryUrl = 'get-walmart-reviews'; 236 | data = {itemId: id}; 237 | } else if (site === 'Best Buy') { 238 | queryUrl = 'get-bestbuy-reviews'; 239 | data = {sku: id}; 240 | } 241 | 242 | // Makes a specific API call to get reviews for the product clicked on 243 | $.ajax({ 244 | url: queryUrl, 245 | dataType: 'json', 246 | type: 'POST', 247 | // "id" is itemId for Walmart 248 | // and it's SKU for Best Buy 249 | data: data, 250 | success: function(data) {; 251 | 252 | // will need to get this.state.allReviews.reviewSets array 253 | var reviewSetsTmp = this.state.allReviews.reviewSets; 254 | // add an element to it 255 | 256 | if (site === 'Walmart') { 257 | // Get the reviews array from the response data 258 | reviewSetsTmp.push( 259 | this.makeReviewSetFromRawData( 260 | JSON.parse(data[0].walmartReviews), 'Walmart', name, image 261 | ) 262 | ); 263 | } 264 | if (site === 'Best Buy') { 265 | // Get the reviews array from the response data 266 | reviewSetsTmp.push( 267 | this.makeReviewSetFromRawData( 268 | JSON.parse(data[0].bestbuyReviews), 'Best Buy', name, image 269 | ) 270 | ); 271 | } 272 | // put it back with setState 273 | this.setState({ 274 | allReviews: { reviewSets: reviewSetsTmp }, 275 | }); 276 | 277 | this.refs.d3chart.startEngine(500, 225, reviewSetsTmp); 278 | 279 | this.adjustColumnDisplay(reviewSetsTmp.length); 280 | 281 | }.bind(this), 282 | error: function(xhr, status, err) { 283 | console.error(queryUrl, status, err.toString()); 284 | }.bind(this) 285 | }); 286 | 287 | 288 | }, 289 | 290 | // Handler for dismissing a column (by clicking the red X) 291 | handleDismissColumn: function(name, site) { 292 | 293 | // will need to get this.state.allReviews.reviewSets array 294 | var reviewSetsTmp = this.state.allReviews.reviewSets; 295 | 296 | // look for column to dismiss 297 | for (var i = 0; i < reviewSetsTmp.length; i++) { 298 | if (reviewSetsTmp[i].name === name && reviewSetsTmp[i].source === site) { 299 | // splice it out of array 300 | reviewSetsTmp.splice(i, 1); 301 | break; 302 | } 303 | } 304 | // put it back with setState 305 | this.setState({ 306 | allReviews: { reviewSets: reviewSetsTmp }, 307 | }); 308 | // make sure column display style is appropriate for new number of columns 309 | this.adjustColumnDisplay(); 310 | // refresh d3 review chart 311 | this.refs.d3chart.startEngine(500, 275, reviewSetsTmp); 312 | }, 313 | 314 | // Shows search results columns and hides reviews columns 315 | showResultsHideReviews: function() { 316 | $('.reviews-display-container').fadeOut(); 317 | $('.d3-container').fadeOut(); 318 | this.refs.d3PriceChart.startEngine(500, 275); 319 | $('.d3-price-container').delay(500).fadeIn(); 320 | $('.related-results-display-container').delay(500).fadeIn(); 321 | }, 322 | 323 | render: function() { 324 | // Attributes are "props" which can be accessed by the component 325 | // Many "props" are set as the "state", which is set based on data received from API calls 326 | return ( 327 |
328 | 329 | 330 | 331 | 333 | 334 |
335 | 336 |
337 | 338 | 341 | 342 | 348 | 349 |
350 | 351 | 356 | 357 |
358 | 359 | 362 | 365 | {/* Taken out because API key could not be in public repo 366 | */} 367 |
368 | 369 |
370 | ); 371 | } 372 | }); 373 | 374 | // Component for the query-submit form (general, not reviews) 375 | var SearchForm = React.createClass({ 376 | handleSubmit: function(e) { 377 | // Prevent page from reloading on submit 378 | e.preventDefault(); 379 | 380 | // Show the spinner when a query is submitted 381 | $('.query-form-container img').show(); 382 | 383 | // Hide containers 384 | $('.d3-container').fadeOut(); 385 | $('.related-results-display-container').fadeOut(); 386 | $('.reviews-display-container').fadeOut(); 387 | 388 | // Grab query content from "ref" in input box 389 | var query = React.findDOMNode(this.refs.query).value.trim(); 390 | 391 | // Passes the query to the central DisplayBox component 392 | // DisplayBox will make AJAX call and display results 393 | this.props.onQuerySubmit({query: query}); 394 | 395 | // Clear the input box after submit 396 | React.findDOMNode(this.refs.query).value = ''; 397 | }, 398 | render: function() { 399 | return ( 400 |
401 |

ItemChimp, at your service.

402 | 403 |
404 | 405 | 406 |
407 |
408 | 409 |
410 | ); 411 | } 412 | }); 413 | 414 | 415 | // Home page container for the DisplayBox component 416 | var Home = React.createClass({ 417 | render: function() { 418 | return ( 419 |
420 | 421 |
422 | ); 423 | } 424 | }); 425 | 426 | module.exports = Home; --------------------------------------------------------------------------------