├── .bowerrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── PRESS-RELEASE.md ├── README.md ├── STYLE-GUIDE.md ├── bower.json ├── client ├── routes │ ├── D3-Chart.js │ ├── D3-Price-Chart.js │ ├── Dashboard.js │ ├── Home-Amazon-Components.js │ ├── Home-Bestbuy-Components.js │ ├── Home-Compare-Components.js │ ├── Home-Reviews-Components.js │ ├── Home-Walmart-Components.js │ └── Home.js └── scripts │ ├── d3Engine.js │ ├── d3PriceEngine.js │ └── index.js ├── package.json ├── public ├── common.js ├── images │ ├── chimp.png │ ├── favicon.png │ └── spiffygif_46x46.gif ├── index.html └── styles │ └── app.css ├── server.js ├── server ├── auth-routes.js ├── authController.js ├── db │ ├── db.js │ └── schema.sql ├── productsController.js ├── templates │ └── views │ │ └── index.html └── usersController.js └── test ├── client └── Home-test.js └── server └── server.spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## General Workflow 4 | 5 | 1. Fork the repo 6 | 1. Cut a namespaced feature branch from master 7 | - bug/... 8 | - feat/... 9 | - test/... 10 | - doc/... 11 | - refactor/... 12 | 1. Make commits to your feature branch. Prefix each commit like so: 13 | - (feat) Added a new feature 14 | - (fix) Fixed inconsistent tests [Fixes #0] 15 | - (refactor) ... 16 | - (cleanup) ... 17 | - (test) ... 18 | - (doc) ... 19 | 1. When you've finished with your fix or feature, Rebase upstream changes into your branch. submit a [pull request][] 20 | directly to master. Include a description of your changes. 21 | 1. Your pull request will be reviewed by another maintainer. The point of code 22 | reviews is to help keep the codebase clean and of high quality and, equally 23 | as important, to help you grow as a programmer. If your code reviewer 24 | requests you make a change you don't understand, ask them why. 25 | 1. Fix any issues raised by your code reviwer, and push your fixes as a single 26 | new commit. 27 | 1. Once the pull request has been reviewed, it will be merged by another member of the team. Do not merge your own commits. 28 | 29 | ## Detailed Workflow 30 | 31 | ### Fork the repo 32 | 33 | Use github’s interface to make a fork of the repo, then add that repo as an upstream remote: 34 | 35 | ``` 36 | git remote add upstream https://github.com/hackreactor-labs/.git 37 | ``` 38 | 39 | ### Cut a namespaced feature branch from master 40 | 41 | Your branch should follow this naming convention: 42 | - bug/... 43 | - feat/... 44 | - test/... 45 | - doc/... 46 | - refactor/... 47 | 48 | These commands will help you do this: 49 | 50 | ``` bash 51 | 52 | # Creates your branch and brings you there 53 | git checkout -b `your-branch-name` 54 | ``` 55 | 56 | ### Make commits to your feature branch. 57 | 58 | Prefix each commit like so 59 | - (feat) Added a new feature 60 | - (fix) Fixed inconsistent tests [Fixes #0] 61 | - (refactor) ... 62 | - (cleanup) ... 63 | - (test) ... 64 | - (doc) ... 65 | 66 | Make changes and commits on your branch, and make sure that you 67 | only make changes that are relevant to this branch. If you find 68 | yourself making unrelated changes, make a new branch for those 69 | changes. 70 | 71 | #### Commit Message Guidelines 72 | 73 | - Commit messages should be written in the present tense; e.g. "Fix continuous 74 | integration script". 75 | - The first line of your commit message should be a brief summary of what the 76 | commit changes. Aim for about 70 characters max. Remember: This is a summary, 77 | not a detailed description of everything that changed. 78 | - If you want to explain the commit in more depth, following the first line should 79 | be a blank line and then a more detailed description of the commit. This can be 80 | as detailed as you want, so dig into details here and keep the first line short. 81 | 82 | ### Rebase upstream changes into your branch 83 | 84 | Once you are done making changes, you can begin the process of getting 85 | your code merged into the main repo. Step 1 is to rebase upstream 86 | changes to the master branch into yours by running this command 87 | from your branch: 88 | 89 | ```bash 90 | git pull --rebase upstream master 91 | ``` 92 | 93 | This will start the rebase process. You must commit all of your changes 94 | before doing this. If there are no conflicts, this should just roll all 95 | of your changes back on top of the changes from upstream, leading to a 96 | nice, clean, linear commit history. 97 | 98 | If there are conflicting changes, git will start yelling at you part way 99 | through the rebasing process. Git will pause rebasing to allow you to sort 100 | out the conflicts. You do this the same way you solve merge conflicts, 101 | by checking all of the files git says have been changed in both histories 102 | and picking the versions you want. Be aware that these changes will show 103 | up in your pull request, so try and incorporate upstream changes as much 104 | as possible. 105 | 106 | You pick a file by `git add`ing it - you do not make commits during a 107 | rebase. 108 | 109 | Once you are done fixing conflicts for a specific commit, run: 110 | 111 | ```bash 112 | git rebase --continue 113 | ``` 114 | 115 | This will continue the rebasing process. Once you are done fixing all 116 | conflicts you should run the existing tests to make sure you didn’t break 117 | anything, then run your new tests (there are new tests, right?) and 118 | make sure they work also. 119 | 120 | If rebasing broke anything, fix it, then repeat the above process until 121 | you get here again and nothing is broken and all the tests pass. 122 | 123 | ### Make a pull request 124 | 125 | Make a clear pull request from your fork and branch to the upstream master 126 | branch, detailing exactly what changes you made and what feature this 127 | should add. The clearer your pull request is the faster you can get 128 | your changes incorporated into this repo. 129 | 130 | At least one other person MUST give your changes a code review, and once 131 | they are satisfied they will merge your changes into upstream. Alternatively, 132 | they may have some requested changes. You should make more commits to your 133 | branch to fix these, then follow this process again from rebasing onwards. 134 | 135 | Once you get back here, make a comment requesting further review and 136 | someone will look at your code again. If they like it, it will get merged, 137 | else, just repeat again. 138 | 139 | Thanks for contributing! 140 | 141 | ### Guidelines 142 | 143 | 1. Uphold the current code standard: 144 | - Keep your code [DRY][]. 145 | - Apply the [boy scout rule][]. 146 | - Follow [STYLE-GUIDE.md](STYLE-GUIDE.md) 147 | 1. Run the [tests][] before submitting a pull request. 148 | 1. Tests are very, very important. Submit tests if your pull request contains 149 | new, testable behavior. 150 | 1. Your pull request is comprised of a single ([squashed][]) commit. 151 | 152 | ## Checklist: 153 | 154 | This is just to help you organize your process 155 | 156 | - [ ] Did I cut my work branch off of master (don't cut new branches from existing feature brances)? 157 | - [ ] Did I follow the correct naming convention for my branch? 158 | - [ ] Is my branch focused on a single main change? 159 | - [ ] Do all of my changes directly relate to this change? 160 | - [ ] Did I rebase the upstream master branch after I finished all my 161 | work? 162 | - [ ] Did I write a clear pull request message detailing what changes I made? 163 | - [ ] Did I get a code review? 164 | - [ ] Did I make any requested changes from that code review? 165 | 166 | If you follow all of these guidelines and make good changes, you should have 167 | no problem getting your changes merged in. 168 | 169 | 170 | 171 | [style guide]: https://github.com/hackreactor-labs/style-guide 172 | [n-queens]: https://github.com/hackreactor-labs/n-queens 173 | [Underbar]: https://github.com/hackreactor-labs/underbar 174 | [curriculum workflow diagram]: http://i.imgur.com/p0e4tQK.png 175 | [cons of merge]: https://f.cloud.github.com/assets/1577682/1458274/1391ac28-435e-11e3-88b6-69c85029c978.png 176 | [Bookstrap]: https://github.com/hackreactor/bookstrap 177 | [Taser]: https://github.com/hackreactor/bookstrap 178 | [tools workflow diagram]: http://i.imgur.com/kzlrDj7.png 179 | [Git Flow]: http://nvie.com/posts/a-successful-git-branching-model/ 180 | [GitHub Flow]: http://scottchacon.com/2011/08/31/github-flow.html 181 | [Squash]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html 182 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /PRESS-RELEASE.md: -------------------------------------------------------------------------------- 1 | # Shopagator # 2 | 3 | 18 | 19 | ## Data Visualization for Consumers ## 20 | > Name the product in a way the reader (i.e. your target customers) will understand. 21 | 22 | > Shopagator helps shoppers make informed decisions about products they are interested in 23 | 24 | ## Summary ## 25 | > Tired of wading through lousy reviews? Having a hard time deciding on the best product to buy? Shopagator helps shoppers make informed purchasing decisions by providing data visualization for the best reviews, as well as a streamlined search interface for browsing through products from several of the biggest retailers, such as Amazon, Walmart, and Best Buy. 26 | 27 | ## Problem ## 28 | > There are a lot of bad reviews out there that make choosing a product to buy a difficult decision. 29 | 30 | ## Solution ## 31 | > Shopagator uses D3 to provide organized data visualization to help shoppers find the best reviews and make the best purchasing decision possible 32 | 33 | ## Quote from You ## 34 | > "I use Shopagator." - Vinaya 35 | 36 | ## How to Get Started ## 37 | > Just visit our website and immediately start searching for products you are interested in. Our searches are powered by the APIs of some of the largest retailers in the world. 38 | 39 | ## Customer Quote ## 40 | > "Shopagator saved my life." - Billy 41 | 42 | ## Closing and Call to Action ## 43 | > Just go to http://shopagator.herokuapp.com today to experience shopping like never before. 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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-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; -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /client/routes/Home-Compare-Components.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | // "Choose Another Product" column at far right in reviews mode 4 | // Allows user to add a 2nd or 3rd product to compare to current 5 | // one(s). 6 | var ChooseAnotherProductSection = React.createClass({ 7 | handleCompareRequest: function(itemId, site, name, image) { 8 | this.props.onCompareRequest(itemId, site, name, image); 9 | }, 10 | render: function() { 11 | return ( 12 |
13 |
Choose another product to compare
14 | 18 |
19 |
20 | 25 |
26 |
27 | 32 |
33 |
34 |
35 | ); 36 | } 37 | }); 38 | 39 | // "Choose Another Product" section has 2 tabs, 1 for each store 40 | // This component creates one of those tabs. It knows which 41 | // site it's for by "site" being passed in as a prop 42 | var ChooseAnotherProductSectionTab = React.createClass({ 43 | handleCompareRequest: function(itemId, site, name, image) { 44 | this.props.onCompareRequest(itemId, site, name, image); 45 | }, 46 | render: function() { 47 | 48 | var currentId = this.props.currentProductId; 49 | var site = this.props.site; 50 | var resultNodes = this.props.data.results.map(function(result, index) { 51 | // put an if condition here to check if the result is current product already displayed 52 | var resultId; 53 | var image; 54 | if (site === 'Walmart') { 55 | resultId = result.itemId; 56 | image = result.thumbnailImage; 57 | } else if (site === 'Best Buy') { 58 | resultId = result.sku; 59 | image = result.image; 60 | } 61 | if (currentId !== resultId) { 62 | return ( 63 | 70 | ); 71 | } 72 | }.bind(this)); 73 | return ( 74 |
75 |
{this.props.site}
76 | {resultNodes} 77 |
78 | ); 79 | } 80 | }); 81 | 82 | // Component for an individual item in a "choose another product" tab 83 | var IndividualProductCompareChoice = React.createClass({ 84 | handleCompareRequest: function(id, site, name, image) { 85 | this.props.onCompareRequest(this.props.id, this.props.site, this.props.name, this.props.image); 86 | }, 87 | render: function() { 88 | return ( 89 |
92 | 93 | Product: {this.props.name} 94 |
95 | ); 96 | } 97 | }); 98 | 99 | 100 | module.exports.ChooseAnotherProductSection = ChooseAnotherProductSection; -------------------------------------------------------------------------------- /client/routes/Home-Reviews-Components.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | 4 | var ReviewsDisplaySection = React.createClass({ 5 | dismissColumn: function(name, site) { 6 | this.props.onDismissColumn(name, site); 7 | }, 8 | 9 | render: function() { 10 | var reviewColumns = this.props.allReviews.reviewSets.map(function (set, index) { 11 | return ( 12 | 21 | ); 22 | }.bind(this)); 23 | return ( 24 |
25 | {reviewColumns} 26 |
27 | ); 28 | } 29 | }); 30 | 31 | var ReviewsDisplay = React.createClass ({ 32 | dismissColumn: function(name, site) { 33 | this.props.onDismissColumn(this.props.name, this.props.source); 34 | }, 35 | 36 | render: function() { 37 | var resultNodes; 38 | 39 | if (this.props.source === 'Walmart') { 40 | resultNodes = this.props.data.map(function(result, index) { 41 | return ( 42 | 50 | ); 51 | }); 52 | } else if (this.props.source === 'Best Buy') { 53 | resultNodes = this.props.data.map(function(result, index) { 54 | return ( 55 | 62 | ); 63 | }); 64 | } 65 | 66 | return ( 67 |
68 |
69 |

{this.props.source} Reviews 70 |

71 |
for {this.props.name}
72 |
73 |
74 |
75 |
76 |
Average Rating: {this.props.AverageRating}
77 |
Total Reviews: {this.props.ReviewCount}
78 |
79 |
80 |
81 | {resultNodes} 82 |
83 | ); 84 | } 85 | }); 86 | 87 | var WalmartIndividualReviewDisplay = React.createClass({ 88 | render: function() { 89 | return ( 90 |
91 |
92 | {this.props.title} 93 |
94 |
95 | Reviewer: {this.props.reviewer} 96 |
97 |
98 | Review: {this.props.reviewText} 99 |
100 |
101 | +{this.props.upVotes} | -{this.props.downVotes} 102 |
103 |
104 | ); 105 | } 106 | }); 107 | 108 | var BestbuyIndividualReviewDisplay = React.createClass({ 109 | render: function() { 110 | return ( 111 |
112 |
113 | {this.props.title} 114 |
115 |
116 | Reviewer: {this.props.reviewer} 117 |
118 |
119 | Review: {this.props.comment} 120 |
121 |
122 | Rating: {this.props.rating} 123 |
124 |
125 | ); 126 | } 127 | }); 128 | 129 | module.exports.ReviewsDisplaySection = ReviewsDisplaySection; -------------------------------------------------------------------------------- /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.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; -------------------------------------------------------------------------------- /client/scripts/d3Engine.js: -------------------------------------------------------------------------------- 1 | 2 | // ------ CONFIG DATA --------- 3 | 4 | var d3Engine = {}; 5 | 6 | d3Engine.initValues = function (width, height) { 7 | // colors key 8 | d3Engine.colors = ["steelblue", "darkorange", "darkseagreen"]; 9 | d3Engine.prodKey = []; 10 | 11 | // Data for stars legend at bottom 12 | d3Engine.legendData = ['1 star', '2 stars', '3 stars', '4 stars', '5 stars']; 13 | 14 | // overall chart vars 15 | d3Engine.width = width || 600; 16 | d3Engine.height = height || 300; 17 | 18 | // tooltip vars 19 | d3Engine.ttOffset = 10; 20 | d3Engine.ttWidth = 220; 21 | d3Engine.ttHeight = 115; 22 | 23 | // x scale based on star rating 24 | d3Engine.x = d3.scale.linear() 25 | .domain([0, 6]) 26 | .range([0, d3Engine.width]); 27 | 28 | // another scale, narrower range for foci (don't want foci at edge) 29 | d3Engine.fociX = d3.scale.linear() 30 | .domain([0, 5]) 31 | .range([100, d3Engine.width-70]); 32 | 33 | // Create foci (1 per 0.5 star spaced out horizontally across chart) 34 | var fociGen = function (numFoci, x) { 35 | var results = []; 36 | for (var i = 0; i < numFoci; i++) { 37 | results.push({x: d3Engine.fociX(i+1)/2, y: d3Engine.height/2}); 38 | } 39 | return results; 40 | }; 41 | 42 | d3Engine.foci = fociGen(11, d3Engine.x); 43 | }; 44 | 45 | // --------------------------------- 46 | 47 | 48 | // ------ PREP DATA RECEIVED FROM OUTSIDE --------- 49 | d3Engine.populateWMData = function (rawData, prodNum) { 50 | var results = []; 51 | for (var i = 0; i < rawData.length; i++) { 52 | var obj = {}; 53 | obj.reviewLength = rawData[i].reviewText.length; 54 | obj.dotSize = obj.reviewLength/50 + 20; 55 | obj.stars = +rawData[i].overallRating.rating; 56 | obj.prodKey = d3Engine.prodKey[prodNum]; 57 | obj.username = rawData[i].reviewer; 58 | obj.reviewTitle = rawData[i].title.slice(0,24) + "..." 59 | obj.review = rawData[i].reviewText; 60 | obj.reviewStart = obj.review.slice(0, 110) + "..."; 61 | results.push(obj); 62 | } 63 | return results; 64 | }; 65 | 66 | d3Engine.populateBBData = function (rawData, prodNum) { 67 | var results = []; 68 | for (var i = 0; i < rawData.length; i++) { 69 | var obj = {}; 70 | obj.reviewLength = rawData[i].comment.length; 71 | obj.dotSize = obj.reviewLength/50 + 20; 72 | obj.stars = +rawData[i].rating; 73 | obj.prodKey = d3Engine.prodKey[prodNum]; 74 | obj.username = rawData[i].reviewer[0].name; 75 | obj.reviewTitle = rawData[i].title.slice(0,24) + "..." 76 | obj.review = rawData[i].comment; 77 | obj.reviewStart = obj.review.slice(0, 110) + "..."; 78 | results.push(obj); 79 | } 80 | return results; 81 | }; 82 | // --------------------------------- 83 | 84 | 85 | // ------ MAIN CHART CREATION FUNCTION --------- 86 | 87 | d3Engine.create = function (el, width, height, products) { 88 | 89 | d3Engine.initValues(width, height); 90 | 91 | // populate chart with review data 92 | d3Engine.data = []; 93 | for (var i = 0; i < products.length; i++) { 94 | d3Engine.prodKey[i] = {name: products[i].name, color: d3Engine.colors[i], source: products[i].source}; 95 | if (products[i].source === 'Walmart') { 96 | d3Engine.data = d3Engine.data.concat(d3Engine.populateWMData(products[i].Reviews,i)); 97 | } else if (products[i].source === 'Best Buy') { 98 | d3Engine.data = d3Engine.data.concat(d3Engine.populateBBData(products[i].Reviews,i)); 99 | } 100 | } 101 | 102 | // chart overall dimensions 103 | this.chart = d3.select(".chart") 104 | .attr("width", d3Engine.width) 105 | .attr("height", d3Engine.height); 106 | 107 | // clear D3 chart 108 | this.chart.selectAll("g").remove(); 109 | 110 | // create a "g" element for every review (will contain a circle and a text obj) 111 | var circle = this.chart.selectAll("g.node") 112 | .data(d3Engine.data) 113 | .enter().append("g") 114 | .classed("node", true) 115 | .attr("transform", function(d, i) { 116 | return "translate(" + (d3Engine.x(d.stars)+ d.dotSize) + ", 50)"; 117 | }); 118 | 119 | // create a circle element for every g element 120 | circle.append("circle") 121 | .attr("cx", 0) 122 | .attr("cy", 0) 123 | .attr("r", function(d) { return d.dotSize/2; }) 124 | .style("fill", function(d) { return d.prodKey.color; }) 125 | .style("stroke", "white") 126 | .style("stroke-width", 2) 127 | .style("stroke-opacity", 0.5); 128 | 129 | // create a text element for every g 130 | circle.append("text") 131 | .attr("x", 0) 132 | .attr("y", 0) 133 | .attr("dy", ".35em") 134 | .text(function(d) {return d.stars;}); 135 | 136 | // Bottom legend (# of stars) 137 | var legend = this.chart.selectAll("g.legend") 138 | .data(d3Engine.legendData) 139 | .enter().append("g") 140 | .classed("legend", true) 141 | .attr("transform", "translate(0, " + (d3Engine.height-25) + ")"); 142 | 143 | legend.append("text") 144 | .attr("x", function(d, i) { return d3Engine.x((i*1.2)+0.5); }) 145 | .attr("y", 0) 146 | .text(function(d) {return d}); 147 | 148 | // Product legend 149 | var productLegend = this.chart.selectAll("g.productLegend") 150 | .data(d3Engine.prodKey) 151 | .enter().append("g") 152 | .classed("productLegend", true) 153 | .attr("transform", "translate(20,10)"); 154 | 155 | productLegend.append("rect") 156 | .attr("x", 0) 157 | .attr("y", function(d,i) { return i*25; }) 158 | .attr("width", 25) 159 | .attr("height", 25) 160 | .style("fill", function (d) { return d.color; }); 161 | 162 | productLegend.append("text") 163 | .attr("x", 35) 164 | .attr("y", function (d,i) { return i*25 + 13; }) 165 | .attr("dy", "0.35em") 166 | .text(function(d) { 167 | if (d.name.length > 40) { 168 | return d.name.slice(0,40) + "..." + " at " + d.source; 169 | } else { 170 | return d.name + " at " + d.source; 171 | } 172 | }); 173 | 174 | tooltipSetup(); 175 | forceInit(); 176 | }; 177 | // --------------------------------- 178 | 179 | 180 | // ------ TOOLTIP DEF --------- 181 | 182 | function tooltipSetup() { 183 | tooltip = d3.select(".d3-container") 184 | .append("div") 185 | .style("width", d3Engine.ttWidth + "px") 186 | .style("height", d3Engine.ttHeight + "px") 187 | .classed("hoverbox", true); 188 | 189 | tooltip.append('div') 190 | .classed("username", true); 191 | 192 | tooltip.append('div') 193 | .classed("reviewTitle", true); 194 | 195 | tooltip.append('div') 196 | .classed("reviewText", true); 197 | 198 | var nodes = d3Engine.chart.selectAll("g.node"); 199 | 200 | nodes.on('mouseover', function(d) { 201 | var mouseLoc = d3.mouse(this.parentNode); 202 | if (mouseLoc[0] + d3Engine.ttOffset + d3Engine.ttWidth > d3Engine.width) { 203 | mouseLoc[0] = mouseLoc[0] - d3Engine.ttOffset*2 - d3Engine.ttWidth; 204 | } 205 | if (mouseLoc[1] + d3Engine.ttOffset + d3Engine.ttHeight > d3Engine.height) { 206 | mouseLoc[1] = mouseLoc[1] - d3Engine.ttOffset*2 - d3Engine.ttHeight; 207 | } 208 | tooltip 209 | .style("display", "block") 210 | .style("left", (mouseLoc[0]+d3Engine.ttOffset)+"px") 211 | .style("top", (mouseLoc[1]+d3Engine.ttOffset)+"px") 212 | .transition() 213 | .duration(200) 214 | .style('opacity', 1); 215 | var ttHeader = d.username + " on " + d.prodKey.name; 216 | if (ttHeader.length > 20) { 217 | ttHeader = ttHeader.slice(0,20) + "..."; 218 | } 219 | tooltip.select(".username") 220 | .text(ttHeader); 221 | tooltip.select(".reviewTitle") 222 | .text(d.reviewTitle); 223 | tooltip.select(".reviewText") 224 | .text(d.reviewStart); 225 | }); 226 | 227 | nodes.on('mouseout', function(d) { 228 | tooltip.transition() 229 | .duration(200) 230 | .style('opacity', 0) 231 | .each('end', function () { 232 | tooltip.style("display", "none"); 233 | }); 234 | }); 235 | } 236 | 237 | // // --------------------------------- 238 | 239 | 240 | // ------ FORCE DEFINITION AND START--------- 241 | 242 | function forceInit() { 243 | var force = d3.layout.force() 244 | .gravity(0) 245 | .links([]) 246 | .nodes(d3Engine.data) 247 | .charge(function(d) { return d.dotSize * -1.5; }) 248 | .size([d3Engine.width, d3Engine.height]); 249 | 250 | force.start(); 251 | 252 | force.on("tick", function(e) { 253 | var k = .1 * e.alpha; 254 | 255 | d3Engine.data.forEach(function(o,i) { 256 | o.y += (d3Engine.foci[o.stars*2].y - o.y) * k; 257 | o.x += (d3Engine.foci[o.stars*2].x - o.x) * k; 258 | d3Engine.chart.selectAll("g.node") 259 | .attr("transform", function(d) { 260 | return "translate(" + d.x + "," + d.y + ")"; 261 | }); 262 | }); 263 | }); 264 | } 265 | 266 | // --------------------------------- 267 | 268 | 269 | module.exports = d3Engine; 270 | 271 | 272 | -------------------------------------------------------------------------------- /client/scripts/d3PriceEngine.js: -------------------------------------------------------------------------------- 1 | module.exports = function(pricesArray, query) { 2 | 3 | // Set the size of the D3 price chart 4 | var width = 500; 5 | var height = 275; 6 | 7 | // Used to color bubbles 8 | // i & 1 --> two colors, i & 2 --> three colors, etc. 9 | var fill = d3.scale.category10(); 10 | 11 | // Used to find the minimum and maximum price for d3.scale.linear 12 | var pricesOnlyArray = pricesArray.map(function(item) { 13 | return item.salePrice; 14 | }); 15 | 16 | // Find the minimum and maximum price for all products listed in the query results 17 | var min = d3.min(pricesOnlyArray); 18 | var max = d3.max(pricesOnlyArray); 19 | 20 | // Scale for circle radii. Size ranges from 15px to 45px 21 | var radiusScale = d3.scale.linear() 22 | .domain([min, max]) 23 | .range([15, 45]); 24 | 25 | // Scale for font-size in circles. Size ranges from 7px to 20px 26 | var textScale = d3.scale.linear() 27 | .domain([min, max]) 28 | .range([7, 20]); 29 | 30 | // Initializes D3 "force", which provides animation for the bubbles 31 | var force = d3.layout.force() 32 | .nodes(pricesArray) 33 | .size([width, height]) 34 | // "charge" is how strong the attraction between bubbles are 35 | // We use radiusScale for greater repulsion for larger bubbles 36 | .charge(function(d) { return radiusScale(d.salePrice) * -3.5; }) 37 | .on("tick", tick) 38 | .start(); 39 | 40 | // Select the SVG element in the ".d3-price-container" 41 | var svg = d3.select(".price-chart") 42 | .attr("width", width) 43 | .attr("height", height); 44 | 45 | // Appends "Walmart" and "Best Buy" key to the chart 46 | // Text color represents the bubbles the store is associated with 47 | var storesLegend = svg.selectAll("g.stores") 48 | .data(['Walmart', 'Best Buy']) 49 | .enter().append("text") 50 | .attr("x", "400") 51 | .attr("y", function(d,i) { return (i+1) * 30; }) 52 | .attr("font-size", "17px") 53 | .attr("font-weight", "bold") 54 | .attr("fill", function(d,i) { return fill(i & 1); }) 55 | .text(function(d) { return d; }); 56 | 57 | // Create a node for each product in pricesArray 58 | // Use "g" to group things appended to each node 59 | var node = svg.selectAll("g.node") 60 | .data(pricesArray) 61 | .enter().append("g") 62 | .classed("node", true) 63 | // Allows bubbles to be dragged 64 | .call(force.drag) 65 | // Prevents bubbles from scattering on drag event 66 | .on("mousedown", function() { d3.event.stopPropagation(); }); 67 | 68 | // Create a circle/bubble for each product/node 69 | // Size and color are used for data visualization 70 | node.append("circle") 71 | .attr("class", "node") 72 | .attr("cx", 0) 73 | .attr("cy", 0) 74 | // The higher a product's price, the larger the bubble 75 | .attr("r", function(d) { return radiusScale(d.salePrice); }) 76 | // Creates two colors for the two halves of pricesArray 77 | .style("fill", function(d, i) { return fill(i & 1); }) 78 | .style("stroke", function(d, i) { return d3.rgb(fill(i & 1)).darker(2); }); 79 | 80 | // Appends the price to each bubble 81 | node.append("text") 82 | // Adjust the x and y position of the price to be close to the middle 83 | .attr("x", function(d) { return -radiusScale(d.salePrice) * 0.8; }) 84 | .attr("y", function(d) { return radiusScale(d.salePrice) * 0.17; }) 85 | // Adjust the font size based on the size of the bubble 86 | .attr("font-size", function(d) { return textScale(d.salePrice) + "px"; }) 87 | .attr("fill", "white") 88 | .text(function(d) { return "$" + d.salePrice; }); 89 | 90 | svg.style("opacity", 1e-6) 91 | .transition() 92 | .duration(1000) 93 | .style("opacity", 1); 94 | 95 | // Scatters bubbles on click 96 | d3.select(".d3-price-container") 97 | .on("mousedown", mousedown); 98 | 99 | // Initialize the tooltip popup 100 | tooltipSetup(); 101 | 102 | // D3 "force" uses this to animate the bubbles 103 | function tick(e) { 104 | 105 | // Push different nodes in different directions for clustering. 106 | var k = 1.5 * e.alpha; 107 | pricesArray.forEach(function(o, i) { 108 | o.y += i & 1 ? k : -k; 109 | o.x += i & 1 ? k : -k; 110 | }); 111 | 112 | // Sets the x and y attributes of the "g" node for D3's "force" 113 | svg.selectAll("g.node") 114 | .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); 115 | } 116 | 117 | // Function that scatters bubbles on click event 118 | function mousedown() { 119 | pricesArray.forEach(function(o, i) { 120 | o.x += (Math.random() - 0.5) * 40; 121 | o.y += (Math.random() - 0.5) * 40; 122 | 123 | }); 124 | force.resume(); 125 | } 126 | 127 | function tooltipSetup() { 128 | // tooltip vars 129 | tooltipOffset = 10; 130 | tooltipWidth = 220; 131 | tooltipHeight = 105; 132 | 133 | // Append tooltip popup div to container 134 | var tooltip = d3.select(".d3-price-container") 135 | .append("div") 136 | .style("width", tooltipWidth) 137 | .style("height", tooltipHeight) 138 | .classed("hoverbox", true); 139 | 140 | // Append "product-name" div to tooltip div 141 | tooltip.append('div') 142 | .classed("product-name", true); 143 | 144 | // Select all products in the array 145 | var nodes = svg.selectAll("g.node"); 146 | 147 | // On mouseover event over a bubble, display the tooltip popup that displays the product name 148 | nodes.on('mouseover', function(d) { 149 | var mouseLoc = d3.mouse(this.parentNode); 150 | 151 | // Set position and styling of tooltip div 152 | tooltip 153 | .style("display", "block") 154 | .style("left", (mouseLoc[0]-170)+"px") 155 | .style("top", (mouseLoc[1]-80)+"px") 156 | .style("font-size", "13px") 157 | .transition() 158 | .duration(200) 159 | .style('opacity', 1); 160 | 161 | // Set the text in the tooltip popup to be the product name associated with the price in the bubble 162 | tooltip.select(".product-name") 163 | .text(d.name); 164 | }); 165 | 166 | // On mouseout event, tooltip div disappears 167 | nodes.on('mouseout', function(d) { 168 | tooltip.transition() 169 | .duration(200) 170 | .style('opacity', 0) 171 | .each('end', function () { 172 | tooltip.style("display", "none"); 173 | }); 174 | }); 175 | } 176 | 177 | }; 178 | -------------------------------------------------------------------------------- /client/scripts/index.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Router = require('react-router'); 3 | 4 | // Require Famo.us/react-famous components 5 | 6 | var Transform = require('famous/core/Transform'); 7 | var Easing = require('famous/transitions/Easing'); 8 | var Transitionable = require('famous/transitions/Transitionable'); 9 | var Timer = require('famous/utilities/Timer'); 10 | 11 | var Context = require('react-famous/core/Context'); 12 | var Modifier = require('react-famous/core/Modifier'); 13 | var Surface = require('react-famous/core/Surface'); 14 | var FamousScheduler = require('react-famous/lib/FamousScheduler'); 15 | var StateModifier = require('react-famous/modifiers/StateModifier'); 16 | 17 | // Component for the bootstrap navbar 18 | // React Router routes are included in here 19 | var Navbar = React.createClass({ 20 | render: function() { 21 | return ( 22 | 46 | ); 47 | } 48 | }); 49 | 50 | // Component for the header area underneath the navbar 51 | var LogoArea = React.createClass({ 52 | componentDidMount: function() { 53 | 54 | // react-famous code for logo image to bounce up and down 55 | var stateModifier = this.refs.stateModifier.getFamous(); 56 | 57 | FamousScheduler.schedule(function() { 58 | var animate = function() { 59 | stateModifier.halt(); 60 | // Code for chimp to bounce up 61 | stateModifier.setTransform(Transform.translate(0, -40), { 62 | curve: 'easeOut', 63 | duration: 250 64 | }, function() { 65 | // Code for chimp to fall back down 66 | stateModifier.setTransform(Transform.translate(0, 0), { 67 | curve: 'easeIn', 68 | duration: 125 69 | }, function() { 70 | // Repeat the animation infinitely 71 | Timer.setTimeout(animate, 625); 72 | }); 73 | }); 74 | }; 75 | 76 | // Initialize the bouncing animation 77 | animate(); 78 | }); 79 | 80 | // react-famous code for chimp to spin 81 | var modifier = this.refs.modifier.getFamous(); 82 | 83 | // Set the spinning speed 84 | var spinner = { 85 | speed: 15 86 | }; 87 | 88 | // Create a transitionable that will rotate 89 | var rotateY = new Transitionable(0); 90 | 91 | // Timer causes rotation to last infinitely 92 | Timer.every(function() { 93 | var adjustedSpeed = parseFloat(spinner.speed) / 1000; 94 | rotateY.set(rotateY.get() + adjustedSpeed); 95 | // Start the rotating animation/transition 96 | modifier.setTransform(Transform.rotateY(rotateY.get())); 97 | }, 1); 98 | 99 | }, 100 | render: function() { 101 | 102 | return ( 103 |
104 | 105 | {/* StateModifier is the bouncing modifier */} 106 | 107 | {/* Modifier is the spinning modifier */} 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |

ItemChimp

116 |

A Data Visualization Tool for Shoppers

117 |
118 | ); 119 | } 120 | }); 121 | 122 | // Basic structure of the app 123 | // This is implemented by the React Router, which recognizes the "App" variable 124 | var App = React.createClass({ 125 | render: function() { 126 | return ( 127 |
128 |
129 | 130 |
131 | 132 | 133 | 134 |
135 | 136 |
137 |
138 | ); 139 | } 140 | }); 141 | 142 | // Routes for the React Router 143 | // Identifies the files that each route refers to 144 | var routes = { 145 | Home: require('../routes/Home'), 146 | Dashboard: require('../routes/Dashboard') 147 | }; 148 | 149 | // Identifies "App" variable as the handler 150 | // Sets up the app for routing 151 | var routes = ( 152 | 153 | 154 | 155 | 156 | 157 | ); 158 | 159 | // Runs the router with proper parameters 160 | Router.run(routes, Router.HistoryLocation, function (Handler) { 161 | // Route exists in the DOM element with ID "content" 162 | React.render(, document.getElementById('content')); 163 | }); 164 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/common.js: -------------------------------------------------------------------------------- 1 | console.error("Error: Parsing file /Users/michael/code/hr/pebbleframe/client/scripts/index.js: Unexpected token (7:6)"); -------------------------------------------------------------------------------- /public/images/chimp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/item-chimp/63bbe7caab2a15335d2e917920c3268f84e1601b/public/images/chimp.png -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/item-chimp/63bbe7caab2a15335d2e917920c3268f84e1601b/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/spiffygif_46x46.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/item-chimp/63bbe7caab2a15335d2e917920c3268f84e1601b/public/images/spiffygif_46x46.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 300 16px/22px "Lato", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | } 4 | 5 | /* ================ CLEARFIX ============= */ 6 | .clearfix:before, 7 | .clearfix:after { 8 | content: ""; 9 | display: table; 10 | } 11 | 12 | .clearfix:after { 13 | clear: both; 14 | } 15 | 16 | .clearfix { 17 | zoom: 1; /* ie 6/7 */ 18 | } 19 | 20 | /* ================ LOGO AREA ============= */ 21 | 22 | .logo-container { 23 | color: #F6E8B1; 24 | text-align: center; 25 | background: #89725B; 26 | padding-top: 50px; 27 | padding-bottom: 25px; 28 | } 29 | 30 | .logo-title { 31 | font-size: 65px; 32 | font-weight: 100; 33 | } 34 | 35 | .logo-tagline { 36 | font-size: 25px; 37 | font-weight: 100; 38 | } 39 | 40 | /* ========== HOME PAGE =============== */ 41 | 42 | .home-page { 43 | text-align: center; 44 | margin-top: 30px; 45 | } 46 | 47 | .query-form-container img { 48 | display: none; 49 | margin-top: 20px; 50 | } 51 | 52 | .query-form-title { 53 | text-align: center; 54 | font-weight: 200; 55 | } 56 | 57 | .logo-image { 58 | height: 70px; 59 | margin-right: 15px; 60 | margin-top: -8px; 61 | } 62 | 63 | .home-page input { 64 | width: 280px; 65 | margin: 0 auto; 66 | } 67 | 68 | .query-form button { 69 | margin-top: 7px; 70 | } 71 | 72 | /* ============== HOME PAGE RESULTS DISPLAY ============= */ 73 | 74 | /* MAJOR CONTAINERS */ 75 | 76 | .related-results-display-container { 77 | display: none; 78 | } 79 | 80 | .reviews-display-container { 81 | display: none; 82 | margin-top: 10px; 83 | } 84 | 85 | /* RELATED RESULTS CONTAINER */ 86 | 87 | .related-results-display { 88 | float: left; 89 | width: 50%; 90 | } 91 | 92 | .individual-display { 93 | padding: 8px; 94 | margin: 12px 9px; 95 | border: 1px solid #8E8E8E; 96 | border-radius: 7px; 97 | cursor: pointer; 98 | } 99 | 100 | .individual-display:hover { 101 | background: #F8F8F8; 102 | transition: .3s transform; 103 | -webkit-transform: rotate(1deg); 104 | transform: rotate(1deg); 105 | } 106 | 107 | /* REVIEWS CONTAINER */ 108 | 109 | /* REVIEWS CONTAINER - REVIEWS SECTION */ 110 | .reviews-display-section { 111 | display: inline-block; 112 | font-size: 14px; 113 | width: 80%; 114 | margin-top: 10px; 115 | } 116 | .reviews-display-section-3-across { 117 | display: inline-block; 118 | font-size: 14px; 119 | width: 100%; 120 | margin-top: 10px; 121 | } 122 | 123 | .reviews-display-container button { 124 | font-size: 10px; 125 | padding: 2px 4px; 126 | } 127 | 128 | .reviews-display { 129 | padding-left: 8px; 130 | padding-right: 8px; 131 | margin-left: 6px; 132 | margin-right: 6px; 133 | border: 1px solid gray; 134 | border-radius: 5px; 135 | float: left; 136 | width: 45%; 137 | } 138 | 139 | .reviews-display-3-across { 140 | padding-left: 8px; 141 | padding-right: 8px; 142 | margin-left: 6px; 143 | margin-right: 6px; 144 | border: 1px solid gray; 145 | border-radius: 5px; 146 | float: left; 147 | width: 30%; 148 | } 149 | 150 | .reviews-display-header { 151 | line-height: 1.2em; 152 | padding: 3px 0; 153 | } 154 | 155 | .button-dismiss { 156 | margin-right: 10px; 157 | float: right; 158 | background-color: #f66; 159 | } 160 | 161 | .reviews-display-3-across .product-name-review { 162 | margin-top: 0px; 163 | } 164 | .walmart-reviews-display { 165 | padding-left: 8px; 166 | padding-right: 8px; 167 | margin-left: 6px; 168 | margin-right: 6px; 169 | } 170 | 171 | .walmart-reviews-display img { 172 | float: left; 173 | margin-right: 20px; 174 | } 175 | 176 | .bestbuy-reviews-display { 177 | padding: 8px; 178 | margin: 9px 6px; 179 | } 180 | 181 | .bestbuy-reviews-display img { 182 | float: left; 183 | margin-right: 20px; 184 | } 185 | 186 | .individual-review-display { 187 | text-align: left; 188 | font-size: 12px; 189 | line-height: 1.2em; 190 | padding: 5px; 191 | margin: 10px 5px; 192 | max-height: 140px; 193 | border-radius: 4px; 194 | border: 1px solid #C1BCC2; 195 | overflow: scroll; 196 | } 197 | 198 | .individual-review-display h5 { 199 | font-size: 16px; 200 | } 201 | 202 | .upvotes { 203 | color: green; 204 | } 205 | 206 | .downvotes { 207 | color: red; 208 | } 209 | 210 | .sale-price-display { 211 | color: #921B16; 212 | font-size: 18px; 213 | font-weight: bold; 214 | margin: 10px 0; 215 | } 216 | 217 | .description-display { 218 | text-align: left; 219 | font-size: 12px; 220 | line-height: 1.2em; 221 | padding: 6px; 222 | max-height: 100px; 223 | margin-bottom: 10px; 224 | border: 1px solid #C1BCC2; 225 | border-radius: 4px; 226 | overflow: scroll; 227 | } 228 | 229 | .review-rating-display { 230 | text-align: center; 231 | } 232 | 233 | /* REVIEWS CONTAINER - CHOOSE ANOTHER PRODUCT SECTIOn */ 234 | 235 | .choose-another-product-section { 236 | float: right; 237 | text-align: left; 238 | margin-left: 6px; 239 | width: 19%; 240 | } 241 | 242 | .choose-another-product-section h5,h6 { 243 | text-align: center; 244 | } 245 | 246 | .choose-another-product-individual-display { 247 | font-size: 11px; 248 | line-height: 12px; 249 | min-height: 60px; 250 | margin-top: 4px; 251 | padding: 6px; 252 | border: 1px solid #8E8E8E; 253 | border-radius: 3px; 254 | cursor: pointer; 255 | } 256 | 257 | .choose-another-product-individual-display:hover { 258 | background: #F8F8F8; 259 | transition: .3s transform; 260 | -webkit-transform: rotate(1deg); 261 | transform: rotate(1deg); 262 | } 263 | 264 | .choose-another-product-individual-display img { 265 | float: left; 266 | height: 50px; 267 | margin-top: 2px; 268 | } 269 | 270 | /* ============== DASHBOARD DISPLAY ============= */ 271 | 272 | .dashboard-option { 273 | width: 250px; 274 | padding: 8px; 275 | margin: 12px 9px; 276 | border: 1px solid #8E8E8E; 277 | border-radius: 7px; 278 | float: left; 279 | } 280 | 281 | .password-change-field { 282 | margin-bottom: 5px; 283 | } 284 | .contact-change { 285 | margin-bottom: 5px; 286 | } 287 | 288 | a.unfollow:link, a.unfollow:visited { 289 | font-size: 12px; 290 | background-color: #952C2A; 291 | color: white; 292 | margin-left: 5px; 293 | border: 0px; 294 | border-radius: 5px; 295 | padding: 4px; 296 | } 297 | 298 | a.unfollow:hover, a.unfollow:active { 299 | color: white; 300 | background-color: #D93F3C; 301 | text-decoration: none; 302 | } 303 | 304 | a.follow:link, a.follow:visited { 305 | font-size: 12px; 306 | background-color: #406995; 307 | color: white; 308 | margin-left: 5px; 309 | border: 0px; 310 | border-radius: 5px; 311 | padding: 4px; 312 | } 313 | a.follow:hover, a.follow:active { 314 | color: white; 315 | background-color: #5186BE; 316 | text-decoration: none; 317 | } 318 | 319 | .welcome{ 320 | margin-top: 125px; 321 | border-bottom: 1px solid #ccc; 322 | margin-right: 0px; 323 | padding-right: 0px; 324 | border-right: 0px; 325 | } 326 | 327 | .welcomeGroup{ 328 | margin-top:50px; 329 | border-left: 1px solid #ccc; 330 | padding-left: 40px; 331 | margin-left: 0px; 332 | } 333 | .logButton{ 334 | margin-top: 10px; 335 | } 336 | .signupForm{ 337 | border-left: 1px solid #ccc; 338 | padding-left: 50px; 339 | } 340 | .loginForm{ 341 | padding-right: 50px; 342 | 343 | } 344 | .dashboardForm{ 345 | margin-top: 50px; 346 | } 347 | 348 | 349 | /* ========== D3 =============== */ 350 | 351 | .chart { 352 | } 353 | 354 | .chart text { 355 | fill: white; 356 | font: 10px sans-serif; 357 | text-anchor: middle; 358 | } 359 | 360 | .legend text { 361 | fill: gray; 362 | font: 16px sans-serif; 363 | text-anchor: middle; 364 | } 365 | 366 | .productLegend text { 367 | fill: gray; 368 | font: 14px sans-serif; 369 | text-anchor: start; 370 | } 371 | 372 | .d3-container { 373 | display: none; 374 | height: 225px; 375 | width: 500px; 376 | position: relative; 377 | margin-left: auto; 378 | margin-right: auto; 379 | margin-top: 10px; 380 | border: 1px solid #8E8E8E; 381 | border-radius: 10px; 382 | } 383 | 384 | .d3-price-container { 385 | display: none; 386 | position: relative; 387 | width: 500px; 388 | margin: 0 auto; 389 | margin-top: 15px; 390 | border: 1px solid #8E8E8E; 391 | border-radius: 10px; 392 | } 393 | 394 | .d3-price-chart-query { 395 | background: #F8F8F8; 396 | margin: 0; 397 | border-radius: 10px 10px 0 0 ; 398 | } 399 | 400 | .hoverbox { 401 | border: 2px solid rgb(190,190,190); 402 | border-radius: 10px; 403 | background: rgb(255,255,255); 404 | color: #333; 405 | display: block; 406 | font: 14px sans-serif; 407 | left: 0px; 408 | padding: 10px; 409 | position: absolute; 410 | text-align: left; 411 | top: 0px; 412 | z-index: 10; 413 | opacity: 0; 414 | box-sizing: border-box; 415 | } 416 | 417 | .hoverbox .username { 418 | color: #406995; 419 | font: 14px sans-serif; 420 | text-anchor: start; 421 | font-weight: bold; 422 | } 423 | 424 | .hoverbox .reviewTitle { 425 | color: #888; 426 | font: 14px sans-serif; 427 | text-anchor: start; 428 | margin-top: 5px; 429 | } 430 | 431 | .hoverbox .reviewText { 432 | color: black; 433 | font: 12px sans-serif; 434 | text-anchor: start; 435 | margin-top: 5px; 436 | font-style: italic; 437 | } 438 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | //var reactify = require('reactify'); 4 | var nunjucks = require('nunjucks'); 5 | var authRouter = require('./server/auth-routes'); 6 | //var OperationHelper = require('apac').OperationHelper; 7 | var request = require('request'); 8 | 9 | var wmAPIKey = "va35uc9pw8cje38csxx7csk8"; 10 | var bbAPIKey = "n34qnnunjqcb9387gthg8625"; 11 | 12 | var app = express(); 13 | app.use(express.static('public')); 14 | app.use(bodyParser()); 15 | 16 | nunjucks.configure('server/templates/views', { 17 | express: app 18 | }); 19 | 20 | var authRouter = express.Router(); 21 | app.use('/auth', authRouter); 22 | require('./server/auth-routes')(authRouter); 23 | 24 | app.get('/', function(req, res) { 25 | res.render('index.html'); 26 | }); 27 | 28 | // Calls Walmart API for a product keyword search 29 | var walmartGeneralQuery = function(req, res, next){ 30 | var query = req.body.query; 31 | request({ 32 | url: 'http://api.walmartlabs.com/v1/search', 33 | qs: { 34 | query: query, 35 | format: 'json', 36 | apiKey: wmAPIKey 37 | }, 38 | json: true 39 | },function (error, response, walmartBody) { 40 | if (!error && response.statusCode === 200) { 41 | req.walmartResults = walmartBody.items; 42 | next(); 43 | } 44 | }); 45 | }; 46 | 47 | // Calls Best Buy API for a product keyword search 48 | var bestbuyGeneralQuery = function(req, res, next) { 49 | var query = req.body.query; 50 | request({ 51 | url: 'http://api.remix.bestbuy.com/v1/products(name=' + query + '*)', 52 | qs: { 53 | show: [ 54 | 'name', 55 | 'sku', 56 | 'salePrice', 57 | 'customerReviewAverage', 58 | 'customerReviewCount', 59 | 'shortDescription', 60 | 'upc', 61 | 'image' 62 | ].join(","), 63 | sort: 'bestSellingRank', 64 | format: 'json', 65 | apiKey: bbAPIKey 66 | }, 67 | json: true 68 | }, function (error, response, bestbuyBody) { 69 | if (!error && response.statusCode === 200) { 70 | req.bestbuyResults = bestbuyBody.products; 71 | next(); 72 | } 73 | }); 74 | }; 75 | 76 | // Handle request from client for keyword search 77 | // Hits both Walmart and Best Buy APIs and returns search results 78 | app.post('/general-query', [walmartGeneralQuery,bestbuyGeneralQuery], function (req, res) { 79 | res.send([ 80 | {walmart: req.walmartResults}, 81 | {bestbuy: req.bestbuyResults} 82 | ]); 83 | }); 84 | 85 | // Calls Walmart API for reviews of product whose itemID is equal to 86 | // req.body.itemId (if called directly by clicking on a Walmart product) 87 | // or req.itemId (if called after user has clicked on a Best Buy product 88 | // and server is looking for identical item at Walmart) 89 | var walmartReviews = function(req, res, next){ 90 | // If call is coming from get-walmart-reviews, itemId will be in req.body 91 | // If call is coming from get-bestbuy-reviews, a previous middleware function 92 | // will have put the itemId in req.itemId. 93 | if (req.body.itemId) { 94 | req.itemId = req.body.itemId; 95 | } 96 | if (!req.itemId) { 97 | return next(); 98 | } 99 | request({ 100 | url: 'http://api.walmartlabs.com/v1/reviews/' + req.itemId, 101 | qs: { 102 | format: 'json', 103 | apiKey: wmAPIKey 104 | } 105 | }, function (error, response, walmartReviewBody) { 106 | if (!error && response.statusCode === 200) { 107 | req.walmartReviews = walmartReviewBody; 108 | // pass upc to next function by storing it on req 109 | req.upc = JSON.parse(req.walmartReviews).upc; 110 | next(); 111 | } else { 112 | console.log(error); 113 | console.log("Response status code for walmartReviews: " + response.statusCode); 114 | } 115 | } 116 | ); 117 | }; 118 | 119 | // Calls Best Buy PRODUCTS API to convert UPC (universal) to SKU (Best Buy internal tracking #) 120 | // We need to use this in the Best Buy REVIEWS API call later because the Reviews API doesn't 121 | // accept UPC as a search param, but does accept SKU. 122 | var bestbuyUPCToSku = function(req, res, next){ 123 | request({ 124 | url: 'https://api.remix.bestbuy.com/v1/products(upc=' + req.upc + ')', 125 | qs: { 126 | format: 'json', 127 | apiKey: bbAPIKey, 128 | show: [ 129 | 'sku', 130 | 'customerReviewAverage' 131 | ].join(",") 132 | } 133 | }, function (error, response, bestBuySkuBody) { 134 | if (!error && response.statusCode === 200) { 135 | var json = JSON.parse(bestBuySkuBody); 136 | if(json.products.length > 0) { 137 | // pass sku and customerReviewAverage to next function by storing it in req 138 | req.sku = json.products[0].sku; 139 | req.customerReviewAverage = json.products[0].customerReviewAverage; 140 | } 141 | next(); 142 | } 143 | } 144 | ); 145 | }; 146 | 147 | // Calls Best Buy REVIEWS API for reviews of product whose itemID is equal to 148 | // req.body.sku (if called directly by clicking on a Best Buy product) 149 | // or req.sku (if called after user has clicked on a Walmart product 150 | // and server is looking for identical item at Best Buy) 151 | var bestbuyReviews = function(req, res, next){ 152 | // If call is coming from get-walmart-reviews, sku will be in req.body 153 | // If call is coming from get-bestbuy-reviews, a previous middleware function 154 | // will have put the sku in req.sku. 155 | if (req.body.sku) { 156 | req.sku = req.body.sku; 157 | } 158 | if (!req.sku) { 159 | return next(); 160 | } 161 | request({ 162 | url: 'http://api.remix.bestbuy.com/v1/reviews(sku='+req.sku+')', 163 | qs: { 164 | format: 'json', 165 | apiKey: bbAPIKey, 166 | pageSize: 25, 167 | show: [ 168 | 'id', 169 | 'sku', 170 | 'rating', 171 | 'title', 172 | 'comment', 173 | 'reviewer.name' 174 | ].join(",") 175 | } 176 | }, function (error, response, bestbuyReviewBody) { 177 | if (!error && response.statusCode === 200) { 178 | req.bestbuyReviews = bestbuyReviewBody; 179 | } 180 | next(); 181 | } 182 | ); 183 | }; 184 | 185 | // Handles call from client to get reviews for an item the user clicks on 186 | // in the Walmart search results column. 187 | // Gets the Walmart reviews first, then checks to see if there is an identical 188 | // item at Best Buy, and gets those reviews if so. 189 | app.post('/get-walmart-reviews', [walmartReviews,bestbuyUPCToSku,bestbuyReviews], function (req, res) { 190 | if(req.bestbuyReviews) { 191 | // convert req.bestbuyReviews to obj so we can add customerReviewAverage property to it 192 | // then re stringify it 193 | var json = JSON.parse(req.bestbuyReviews); 194 | json.customerReviewAverage = req.customerReviewAverage; 195 | req.bestbuyReviews = JSON.stringify(json); 196 | } 197 | res.send([ 198 | { 199 | walmartReviews: req.walmartReviews, 200 | bestbuyReviews: req.bestbuyReviews 201 | } 202 | ]); 203 | }); 204 | 205 | // Calls Best Buy PRODUCTS API to convert SKU (Best Buy internal tracking #) to UPC (universal) 206 | // We need UPC to check if Walmart has identical item, since Walmart obviously does not recognize 207 | // Best Buy SKUs. 208 | var bestbuySkuToUPC = function(req, res, next){ 209 | request({ 210 | url: 'https://api.remix.bestbuy.com/v1/products(sku='+req.sku+')', 211 | qs: { 212 | format: 'json', 213 | apiKey: bbAPIKey, 214 | show: [ 215 | 'upc', 216 | 'customerReviewAverage' 217 | ].join(",") 218 | } 219 | }, function (error, response, bestbuyReviewBody) { 220 | if (!error && response.statusCode === 200) { 221 | var json = JSON.parse(bestbuyReviewBody); 222 | if(json.products.length>0) { 223 | // Pass upc and customerReviewAverage onto following middleware functions 224 | req.upc = json.products[0].upc; 225 | req.customerReviewAverage = json.products[0].customerReviewAverage; 226 | } 227 | next(); 228 | } else { 229 | console.log(error); 230 | console.log("Response status code for bestbuySkuToUPC: " + response.statusCode); 231 | next(); 232 | } 233 | } 234 | ); 235 | }; 236 | 237 | // Calls Walmart API to convert UPC to itemID (Walmart's internal tracking #) 238 | // We need itemID to look up Walmart reviews for that item, because Walmart's 239 | // reviews API doesn't recognize UPC as a search term. 240 | var bestbuyUPCToItemId = function(req, res, next){ 241 | if (req.upc) { 242 | request({ 243 | url: 'http://api.walmartlabs.com/v1/items', 244 | qs: { 245 | format: 'json', 246 | apiKey: wmAPIKey, 247 | upc: req.upc 248 | } 249 | }, function (error, response, cb3Body) { 250 | if (!error && response.statusCode === 200) { 251 | var json = JSON.parse(cb3Body); 252 | if(json.items.length>0) { 253 | // pass itemId to following middleware functions 254 | req.itemId = json.items[0].itemId; 255 | } 256 | next(); 257 | } else { 258 | console.log(error); 259 | console.log("Response status code for bestbuyUPCToItemId: " + response.statusCode); 260 | next(); 261 | } 262 | } 263 | ); 264 | } 265 | else{ 266 | next(); 267 | } 268 | }; 269 | 270 | // Handles call from client to get reviews for an item the user clicks on 271 | // in the Best Buy search results column. 272 | // Gets the Best Buy reviews first, then checks to see if there is an identical 273 | // item at Walmart, and gets those reviews if so. 274 | app.post('/get-bestbuy-reviews', [bestbuyReviews,bestbuySkuToUPC,bestbuyUPCToItemId,walmartReviews], function (req, res) { 275 | if(req.bestbuyReviews.length>0) { 276 | // convert req.bestbuyReviews to obj so we can add customerReviewAverage property to it 277 | // then re stringify it 278 | var json = JSON.parse(req.bestbuyReviews); 279 | json.customerReviewAverage = req.customerReviewAverage; 280 | req.bestbuyReviews = JSON.stringify(json); 281 | } 282 | res.send([ 283 | {walmartReviews: req.walmartReviews, 284 | bestbuyReviews: req.bestbuyReviews} 285 | ]); 286 | }); 287 | 288 | app.get('*', function(req, res) { 289 | res.status(404) 290 | .send('Page not found!'); 291 | }); 292 | 293 | var port = process.env.PORT || 3000; 294 | 295 | app.listen(port, function() { 296 | console.log('Server listening on port ' + port); 297 | }); 298 | 299 | exports = module.exports = app; -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /server/db/db.js: -------------------------------------------------------------------------------- 1 | //DECLARE GLOBAL VARIABLES ---START 2 | var 3 | Bookshelf = require('bookshelf'), 4 | events = require('events'), 5 | EventEmitter = require("events").EventEmitter, 6 | util = require('util'), 7 | bcrypt = require('bcrypt-nodejs'), 8 | jwt = require('jwt-simple'), 9 | token; 10 | //DECLARE GLOBAL VARIABLES ---END 11 | //Create DataBase wrapper, includes event emitter --- START 12 | function DB(){ 13 | EventEmitter.call(this); 14 | } 15 | 16 | util.inherits(DB, EventEmitter); 17 | 18 | var db = new DB(); 19 | 20 | var knex = require('knex')({ 21 | client: 'mysql', 22 | connection: { 23 | host: 'localhost', 24 | user: 'root', 25 | password: '', 26 | database: 'pebble', 27 | charset: 'utf8', 28 | } 29 | }); 30 | db.orm = require('bookshelf')(knex); 31 | //Create DataBase wrapper, includes event emitter -- END 32 | 33 | 34 | //-------------TABLES VERIFICATION START-----------/ 35 | db.orm.knex.schema.hasTable('users').then(function(exists) { 36 | if (!exists) 37 | console.log('Table users does not exist'); 38 | }); 39 | 40 | db.orm.knex.schema.hasTable('reviews').then(function(exists) { 41 | if (!exists) 42 | console.log('Table reviews does not exist'); 43 | }); 44 | 45 | db.orm.knex.schema.hasTable('products').then(function(exists) { 46 | if (!exists) 47 | console.log('Table products does not exist'); 48 | }); 49 | 50 | db.orm.knex.schema.hasTable('followers').then(function(exists) { 51 | if (!exists) 52 | console.log('Table followers does not exist'); 53 | }); 54 | 55 | db.orm.knex.schema.hasTable('watchers').then(function(exists) { 56 | if (!exists) 57 | console.log('Table watchers does not exist'); 58 | }); 59 | //-------------TABLES VERIFICATION END-------------/ 60 | 61 | //-------------ORM FOR USERS START-----------------/ 62 | //Create user Model 63 | db.User = db.orm.Model.extend({ 64 | tableName:"users" 65 | }); 66 | 67 | //Create user Collection 68 | db.Users = new db.orm.Collection(); 69 | db.Users.model = db.User; 70 | 71 | 72 | //-------------ORM FOR USERS END-------------------/ 73 | 74 | //-------------ORM FOR REVIEWS START---------------/ 75 | //Create Review Model 76 | db.Review = db.orm.Model.extend({ 77 | tableName:"reviews" 78 | }); 79 | 80 | //Create Review Collection 81 | db.Reviews = new db.orm.Collection(); 82 | db.Reviews.model = db.Review; 83 | 84 | // //Create New Review (template) --For Development Only 85 | // var review = new db.Review({ 86 | // user_id: 1, 87 | // upc: 12345678910, 88 | // rating: 2, 89 | // review_text: 'AWESOME' 90 | // }); 91 | // 92 | // //Review Save (template) to the database --For Development Only 93 | // review.save().then(function(newReview) { 94 | // db.Reviews.add(newReview); 95 | // console.log("Review Saved") 96 | // }); 97 | ////-------------ORM FOR REVIEWS END-----------------/ 98 | 99 | //-------------ORM FOR PRODUCTS START--------------/ 100 | //Create Products Model 101 | db.Product = db.orm.Model.extend({ 102 | tableName:"products" 103 | }); 104 | 105 | //Create product Collection 106 | db.Products = new db.orm.Collection(); 107 | db.Products.model = db.Product; 108 | 109 | //Create New Product--For Development Only 110 | var product = new db.Product({ 111 | upc: 123456789101, 112 | price: 400, 113 | review_count: 0 114 | }); 115 | 116 | //Save Product to the database--For Development Only 117 | product.save().then(function(newProduct) { 118 | db.Products.add(newProduct); 119 | console.log("Product Saved"); 120 | }); 121 | //-------------ORM FOR PRODUCTS END----------------/ 122 | 123 | //-------------ORM FOR FOLLOWERS START-------------/ 124 | //Create Follower Model 125 | db.Follower = db.orm.Model.extend({ 126 | tableName:"followers" 127 | }); 128 | 129 | //Create user Collection 130 | db.Followers = new db.orm.Collection(); 131 | db.Followers.model = db.Follower; 132 | 133 | // //Create New Follower--For Development Only 134 | // var follower = new db.Follower({ 135 | // user_id: 1, 136 | // follower_id: 2 137 | // }); 138 | 139 | // //Save follower to the database--For Development Only 140 | // follower.save().then(function(newFollower) { 141 | // db.Followers.add(newFollower); 142 | // console.log("Follower Saved") 143 | // }); 144 | //-------------ORM FOR FOLLOWERS END---------------/ 145 | 146 | //-------------ORM FOR WATCHERS START--------------/ 147 | //Create Watcher Model 148 | db.Watcher = db.orm.Model.extend({ 149 | tableName:"watchers" 150 | }); 151 | 152 | //Create user Collection 153 | db.Watchers = new db.orm.Collection(); 154 | db.Watchers.model = db.Watcher; 155 | 156 | //Create New Watcher--For Development Only 157 | // var watcher = new db.Watcher({ 158 | // user_id: 1, 159 | // product_id: 1 160 | // }); 161 | 162 | // //Save watcher to the database--For Development Only 163 | // watcher.save().then(function(newWatcher) { 164 | // db.Watchers.add(newWatcher); 165 | // console.log("Watcher Saved") 166 | // }); 167 | //-------------ORM FOR WATCHERS END----------------/ 168 | 169 | //-------------USER API CONFIGURATION START--------/ 170 | 171 | db.tokenUser = null; 172 | //This function determines whether a specific user 173 | //already exists in the database 174 | db.findUser = function(userName){ 175 | console.log("Hello" + userName); 176 | db.User.where({username: userName}).fetch() 177 | .then(function (user) { 178 | if (!user) { 179 | user = undefined; 180 | console.log("User"+userName+"Does Not Exist"); 181 | db.emit("foundUser", user); 182 | } 183 | else{ 184 | console.log(user + "Found"); 185 | db.emit("foundUser", user); 186 | } 187 | }); 188 | }; 189 | 190 | db.addUser = function(user){ 191 | //Is listening for finduser() event 192 | db.once('foundUser',function(found){ 193 | //If the user is not in the database, a user will be added after 194 | //there password is salted and hashed 195 | if(!found){ 196 | bcrypt.genSalt(10, function(err, salt) { 197 | if (err) { 198 | return console.log("Error with Salt"); 199 | } 200 | bcrypt.hash(user.password, salt, null, function(err, hash) { 201 | if (err) { 202 | return console.log("Error with hash"); 203 | } 204 | user.password = hash; 205 | console.log(user); 206 | var newUser = new db.User(user); 207 | newUser.save().then(function(newUser) { 208 | db.Users.add(newUser); 209 | console.log("User Saved"); 210 | token = jwt.encode(user.username, 'secret'); 211 | db.emit("userAdded", token); 212 | }); 213 | }); 214 | }); 215 | } 216 | else{ 217 | token = undefined; 218 | console.log('User already exists'); 219 | db.emit("userAdded", token); 220 | } 221 | }); 222 | console.log("User: " + user.username); 223 | //Before adding user, checks to see if the user is already in database 224 | db.findUser(user.username); 225 | }; 226 | 227 | db.login = function(candidate){ 228 | console.log("Logging In"); 229 | //if the user is found in the database 230 | //the password provided will be compared 231 | //to the hashed password in the database 232 | //if it is a match, a token will be generated 233 | db.once("foundUser", function(user){ 234 | if(user){ 235 | console.log(user.get('password')); 236 | var candidatePassword = candidate.password; 237 | var savedPassword = user.get('password'); 238 | bcrypt.compare(candidatePassword, savedPassword, function (err, isMatch) { 239 | if (isMatch) { 240 | token = jwt.encode(user.get('username'), 'secret'); 241 | db.emit('userLogin', { 242 | token: token, 243 | username: user.get('username'), 244 | email: user.get('email') 245 | }); 246 | } 247 | else { 248 | token = undefined; 249 | console.log("Password Incorrect"); 250 | db.emit('userLogin', token); 251 | } 252 | }); 253 | }else{ 254 | token = undefined; 255 | console.log("User Not Found"); 256 | db.emit('userLogin', token); 257 | } 258 | }); 259 | console.log(candidate); 260 | db.findUser(candidate.username); 261 | }; 262 | 263 | 264 | 265 | //-------------API CONFIGURATION END---------------/ 266 | 267 | //Create New Users --For Development Only 268 | var user = { 269 | username: "Gilgamesh", 270 | password: 1, 271 | email: "g@gmail.com" 272 | }; 273 | 274 | //Save user to the database 275 | db.addUser(user); 276 | 277 | user = { 278 | username: "Enkidu", 279 | password: 1, 280 | email: "e@gmail.com" 281 | }; 282 | db.addUser(user); 283 | //Create New Users --For Development Only 284 | 285 | 286 | module.exports = db; 287 | 288 | -------------------------------------------------------------------------------- /server/db/schema.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE if exists pebble; 2 | 3 | CREATE DATABASE pebble; 4 | 5 | USE pebble; 6 | -- --- 7 | -- Globals 8 | -- --- 9 | 10 | -- SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; 11 | -- SET FOREIGN_KEY_CHECKS=0; 12 | 13 | -- --- 14 | -- Table 'users' 15 | -- 16 | -- --- 17 | 18 | DROP TABLE IF EXISTS `users`; 19 | 20 | CREATE TABLE `users` ( 21 | `user_id` INTEGER(10) NULL AUTO_INCREMENT DEFAULT NULL, 22 | `username` VARCHAR(10) NULL DEFAULT NULL, 23 | `password` VARCHAR(100) NULL DEFAULT NULL, 24 | `email` VARCHAR(30) NULL DEFAULT NULL, 25 | PRIMARY KEY (`user_id`) 26 | ); 27 | 28 | -- --- 29 | -- Table 'reviews' 30 | -- 31 | -- --- 32 | 33 | DROP TABLE IF EXISTS `reviews`; 34 | 35 | CREATE TABLE `reviews` ( 36 | `review_id` INTEGER(12) NULL AUTO_INCREMENT DEFAULT NULL, 37 | `user_id` INTEGER(10) NULL DEFAULT NULL, 38 | `upc` BIGINT(12) NULL DEFAULT NULL, 39 | `rating` INTEGER(3) NULL DEFAULT NULL, 40 | `review_text` VARCHAR(500) NULL DEFAULT NULL, 41 | PRIMARY KEY (`review_id`) 42 | ); 43 | 44 | -- --- 45 | -- Table 'following' 46 | -- 47 | -- --- 48 | 49 | DROP TABLE IF EXISTS `followers`; 50 | 51 | CREATE TABLE `followers` ( 52 | `user_id` INTEGER(10) NULL AUTO_INCREMENT DEFAULT NULL, 53 | `follower_id` INTEGER(10) NULL DEFAULT NULL, 54 | PRIMARY KEY (`user_id`, `follower_id`) 55 | ); 56 | 57 | -- --- 58 | -- Table 'watchers' 59 | -- 60 | -- --- 61 | 62 | DROP TABLE IF EXISTS `watchers`; 63 | 64 | CREATE TABLE `watchers` ( 65 | `user_id` INTEGER(10) NULL AUTO_INCREMENT DEFAULT NULL, 66 | `product_id` BIGINT(12) NULL DEFAULT NULL, 67 | PRIMARY KEY (`user_id`, `product_id`) 68 | ); 69 | 70 | -- --- 71 | -- Table 'product' 72 | -- 73 | -- --- 74 | 75 | DROP TABLE IF EXISTS `products`; 76 | 77 | CREATE TABLE `products` ( 78 | `product_id` BIGINT(12) NULL AUTO_INCREMENT DEFAULT NULL, 79 | `upc` BIGINT(12) NULL DEFAULT NULL, 80 | `price` INTEGER(5) NULL DEFAULT NULL, 81 | `review_count` INTEGER(4) NULL DEFAULT 0, 82 | PRIMARY KEY (`product_id`, `upc`) 83 | ); 84 | 85 | -- --- 86 | -- Foreign Keys 87 | -- --- 88 | 89 | ALTER TABLE `reviews` ADD FOREIGN KEY (user_id) REFERENCES `users` (`user_id`); 90 | ALTER TABLE `followers` ADD FOREIGN KEY (user_id) REFERENCES `users` (`user_id`); 91 | ALTER TABLE `followers` ADD FOREIGN KEY (follower_id) REFERENCES `users` (`user_id`); 92 | ALTER TABLE `watchers` ADD FOREIGN KEY (user_id) REFERENCES `users` (`user_id`); 93 | ALTER TABLE `watchers` ADD FOREIGN KEY (product_id) REFERENCES `products` (`product_id`); 94 | 95 | -- --- 96 | -- Table Properties 97 | -- --- 98 | 99 | -- ALTER TABLE `users` ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; 100 | -- ALTER TABLE `reviews` ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; 101 | -- ALTER TABLE `following` ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; 102 | -- ALTER TABLE `watching` ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; 103 | -- ALTER TABLE `product` ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; 104 | 105 | -- --- 106 | -- Test Data 107 | -- --- 108 | 109 | -- INSERT INTO `users` (`user_id`,`username`,`password`,`email`) VALUES 110 | -- ('','','',''); 111 | -- INSERT INTO `reviews` (`review_id`,`user_id`,`upc`,`rating`,`review_text`) VALUES 112 | -- ('','','','',''); 113 | -- INSERT INTO `following` (`user_id`,`follower_id`) VALUES 114 | -- ('',''); 115 | -- INSERT INTO `watching` (`user_id`,`upc`) VALUES 116 | -- ('',''); 117 | -- INSERT INTO `product` (`upc`,`price`,`review_ count`) VALUES 118 | -- ('','',''); -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /test/client/Home-test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/item-chimp/63bbe7caab2a15335d2e917920c3268f84e1601b/test/client/Home-test.js -------------------------------------------------------------------------------- /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 | })(); --------------------------------------------------------------------------------