├── static └── .gitkeep ├── .eslintignore ├── src ├── main.js ├── startup │ └── remove-webflow.js ├── modules │ ├── convertToNumber.js │ ├── fetchGroceryList.js │ └── formatGroceryList.js └── vue │ ├── data.js │ ├── index.js │ ├── computed.js │ └── methods.js ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── .gitignore ├── .editorconfig ├── .postcssrc.js ├── .babelrc ├── README.md ├── .eslintrc.js ├── package.json ├── docs └── index.html └── index.html /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import './startup/remove-webflow'; 2 | import './vue'; 3 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/startup/remove-webflow.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | $('.w--current').removeClass('w--current'); 4 | $('.w--tab-active').removeClass('w--tab-active'); 5 | $('[data-w-tab]').removeAttr('data-w-tab'); 6 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/convertToNumber.js: -------------------------------------------------------------------------------- 1 | const convertToNumber = (value) => { 2 | const stringValue = value || ''; 3 | const numericString = stringValue.replace(/[^\d.-]/g, ''); 4 | 5 | return isNaN(numericString) ? 0 : Number(numericString); 6 | }; 7 | 8 | export default convertToNumber; 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "presets": ["env", "stage-2"], 11 | "plugins": [ "istanbul" ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/fetchGroceryList.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import formatGroceryList from './formatGroceryList'; 4 | 5 | const GROCERY_LIST_URL = 'https://spreadsheets.google.com/feeds/list/1A4PjmL_R3Zn9T85feCy3YnGyQ5rLGxzmsDeKUc-IsPk/od6/public/values?alt=json'; 6 | 7 | const fetchGroceryList = () => ( 8 | $.getJSON(GROCERY_LIST_URL) 9 | .then(formatGroceryList) 10 | ); 11 | 12 | export default fetchGroceryList; 13 | -------------------------------------------------------------------------------- /src/vue/data.js: -------------------------------------------------------------------------------- 1 | import queryString from 'query-string'; 2 | 3 | import convertToNumber from '../modules/convertToNumber'; 4 | 5 | const { budget, totalCalories } = queryString.parse(window.location.search); 6 | 7 | export default { 8 | budget: convertToNumber(budget), 9 | totalCalories: convertToNumber(totalCalories), 10 | groupedGroceryList: {}, 11 | groupNames: [], 12 | currentTab: 0, 13 | activeContent: 0, 14 | styles: { 15 | emptyCartModal: {}, 16 | overBudgetModal: {}, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # houston-food-bank 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | ``` 20 | 21 | For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 22 | -------------------------------------------------------------------------------- /src/modules/formatGroceryList.js: -------------------------------------------------------------------------------- 1 | const formatGroceryList = (data) => { 2 | const rows = data.feed.entry; 3 | const groupedGroceryList = {}; 4 | 5 | let currentGroup = ''; 6 | 7 | rows.forEach((row) => { 8 | if (row.content.$t) { 9 | groupedGroceryList[currentGroup].push({ 10 | name: row.gsx$item.$t, 11 | price: Number(row.gsx$price.$t.replace('$', '')), 12 | calories: Number(row.gsx$calories.$t), 13 | servings: `${row.gsx$servings.$t} ${row.gsx$servingsunits.$t || 'servings'}`, 14 | nonNutritious: !!row['gsx$non-nutritious'].$t, 15 | quantity: 0, 16 | }); 17 | } else { 18 | currentGroup = row.title.$t; 19 | groupedGroceryList[currentGroup] = []; 20 | } 21 | }); 22 | 23 | return groupedGroceryList; 24 | }; 25 | 26 | export default formatGroceryList; 27 | -------------------------------------------------------------------------------- /src/vue/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import Vue from 'vue'; 3 | 4 | import data from './data'; 5 | import computed from './computed'; 6 | import methods from './methods'; 7 | import fetchGroceryList from '../modules/fetchGroceryList'; 8 | 9 | const IDEAL_LOADING_TIME = 2000; 10 | 11 | /* eslint-disable no-new */ 12 | new Vue({ 13 | data, 14 | computed, 15 | methods, 16 | el: '#v-app', 17 | mounted() { 18 | const startFetch = new Date(); 19 | 20 | fetchGroceryList() 21 | .then((groupedGroceryList = {}) => { 22 | const endFetch = new Date(); 23 | const timeElapsed = (endFetch.getTime() - startFetch.getTime()); 24 | 25 | const loadingTimeLeft = IDEAL_LOADING_TIME - timeElapsed; 26 | 27 | setTimeout(() => { 28 | this.groupedGroceryList = groupedGroceryList; 29 | this.groupNames = Object.keys(groupedGroceryList); 30 | 31 | $('.loading-screen').hide(); 32 | }, loadingTimeLeft); 33 | }); 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | extends: 'airbnb-base', 13 | // required to lint *.vue files 14 | plugins: [ 15 | 'html' 16 | ], 17 | // check if imports actually resolve 18 | 'settings': { 19 | 'import/resolver': { 20 | 'webpack': { 21 | 'config': 'build/webpack.base.conf.js' 22 | } 23 | } 24 | }, 25 | // add your custom rules here 26 | 'rules': { 27 | // don't require .vue extension when importing 28 | 'import/extensions': ['error', 'always', { 29 | 'js': 'never', 30 | 'vue': 'never' 31 | }], 32 | // allow optionalDependencies 33 | 'import/no-extraneous-dependencies': ['error', { 34 | 'optionalDependencies': ['test/unit/index.js'] 35 | }], 36 | // allow debugger during development 37 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/vue/computed.js: -------------------------------------------------------------------------------- 1 | import { flatten } from 'lodash'; 2 | 3 | export default { 4 | selectedItems() { 5 | const groupedSelectedItems = this.groupNames.map(groupName => ( 6 | this.groupedGroceryList[groupName].filter(item => item.quantity > 0) 7 | )); 8 | 9 | return flatten(groupedSelectedItems); 10 | }, 11 | 12 | cartTotalPrice() { 13 | return this.selectedItems.reduce((total, item) => ( 14 | total + (item.price * item.quantity) 15 | ), 0); 16 | }, 17 | 18 | cartIsEmpty() { 19 | return this.selectedItems.length === 0; 20 | }, 21 | 22 | nutritiousCalories() { 23 | return this.selectedItems 24 | .filter(item => !item.nonNutritious) 25 | .reduce((total, item) => ( 26 | total + (item.calories * item.quantity) 27 | ), 0); 28 | }, 29 | 30 | nonNutritiousCalories() { 31 | return this.selectedItems 32 | .filter(item => item.nonNutritious) 33 | .reduce((total, item) => ( 34 | total + (item.calories * item.quantity) 35 | ), 0); 36 | }, 37 | 38 | adjustedTotalCalories() { 39 | return this.nutritiousCalories + this.nonNutritiousCalories; 40 | }, 41 | 42 | adjustedBudget() { 43 | return this.budget - this.cartTotalPrice; 44 | }, 45 | 46 | calorieGoalMet() { 47 | return this.adjustedTotalCalories > 0 && this.adjustedTotalCalories >= this.totalCalories; 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/vue/methods.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import numeral from 'numeral'; 4 | 5 | export default { 6 | decimal(number) { 7 | return numeral(number).format('0,0[.]00'); 8 | }, 9 | 10 | currency(number) { 11 | return numeral(number).format('$0,0[.]00'); 12 | }, 13 | 14 | toggle(elementName) { 15 | const elementStyle = this.styles[elementName] || {}; 16 | const display = !elementStyle.display || elementStyle.display === 'none' 17 | ? 'block' 18 | : 'none'; 19 | 20 | this.styles[elementName] = { display }; 21 | }, 22 | 23 | isCurrent(groupIndex) { 24 | return groupIndex === this.currentTab ? 'w--current' : ''; 25 | }, 26 | 27 | selectTab(groupIndex) { 28 | this.currentTab = groupIndex; 29 | this.activeContent = groupIndex; 30 | }, 31 | 32 | isActive(groupIndex) { 33 | return groupIndex === this.activeContent ? 'w--tab-active' : ''; 34 | }, 35 | 36 | removeItem(item) { 37 | if (item.quantity !== 0) { 38 | item.quantity -= 1; 39 | } 40 | }, 41 | 42 | addItem(item) { 43 | if (this.adjustedBudget >= item.price) { 44 | item.quantity += 1; 45 | } else { 46 | this.toggle('overBudgetModal'); 47 | } 48 | }, 49 | 50 | removeAllOfItem(item) { 51 | item.quantity = 0; 52 | }, 53 | 54 | emptyCart() { 55 | this.selectedItems.forEach((item) => { 56 | item.quantity = 0; 57 | }); 58 | 59 | this.toggle('emptyCartModal'); 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "houston-food-bank", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "Evan Corl ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "build": "node build/build.js", 10 | "lint": "eslint --ext .js,.vue src" 11 | }, 12 | "dependencies": { 13 | "jquery": "^3.1.1", 14 | "lodash": "^4.17.4", 15 | "numeral": "^2.0.4", 16 | "query-string": "^4.3.2", 17 | "vue": "^2.2.2" 18 | }, 19 | "devDependencies": { 20 | "autoprefixer": "^6.7.2", 21 | "babel-core": "^6.22.1", 22 | "babel-eslint": "^7.1.1", 23 | "babel-loader": "^6.2.10", 24 | "babel-plugin-transform-runtime": "^6.22.0", 25 | "babel-preset-env": "^1.2.1", 26 | "babel-preset-stage-2": "^6.22.0", 27 | "babel-register": "^6.22.0", 28 | "chalk": "^1.1.3", 29 | "connect-history-api-fallback": "^1.3.0", 30 | "copy-webpack-plugin": "^4.0.1", 31 | "css-loader": "^0.26.1", 32 | "eslint": "^3.14.1", 33 | "eslint-friendly-formatter": "^2.0.7", 34 | "eslint-loader": "^1.6.1", 35 | "eslint-plugin-html": "^2.0.0", 36 | "eslint-config-airbnb-base": "^11.0.1", 37 | "eslint-import-resolver-webpack": "^0.8.1", 38 | "eslint-plugin-import": "^2.2.0", 39 | "eventsource-polyfill": "^0.9.6", 40 | "express": "^4.14.1", 41 | "extract-text-webpack-plugin": "^2.0.0", 42 | "file-loader": "^0.10.0", 43 | "friendly-errors-webpack-plugin": "^1.1.3", 44 | "function-bind": "^1.1.0", 45 | "html-webpack-plugin": "^2.28.0", 46 | "http-proxy-middleware": "^0.17.3", 47 | "webpack-bundle-analyzer": "^2.2.1", 48 | "semver": "^5.3.0", 49 | "opn": "^4.0.2", 50 | "optimize-css-assets-webpack-plugin": "^1.3.0", 51 | "ora": "^1.1.0", 52 | "rimraf": "^2.6.0", 53 | "url-loader": "^0.5.7", 54 | "vue-loader": "^11.1.4", 55 | "vue-style-loader": "^2.0.0", 56 | "vue-template-compiler": "^2.2.4", 57 | "webpack": "^2.2.1", 58 | "webpack-dev-middleware": "^1.10.0", 59 | "webpack-hot-middleware": "^2.16.1", 60 | "webpack-merge": "^2.6.1" 61 | }, 62 | "engines": { 63 | "node": ">= 4.0.0", 64 | "npm": ">= 3.0.0" 65 | }, 66 | "browserslist": [ 67 | "> 1%", 68 | "last 2 versions", 69 | "not ie <= 8" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Shopping Cart
570 calories
/
$98.26
Apples (3)
285 calories
3 servings
$1.37
* These calories are non-nutritious.
2
My shopping cart
Your family has $101 to spend on 60,550 calories
Nutritious calories: 570
Non-nutritious calories: 0
QTY
ITEM
PRICE
2
Apples (3)
570 calories
$2.74
Total
$2.74
Empty cart
You haven't selected any groceries yet!
-------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Shopping Cart 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 30 | 35 |
36 |
37 |
38 | 39 |
40 |
570 calories
41 |
/
42 |
$98.26
43 |
44 | 45 |
46 |
47 |
48 | 53 |
54 |
55 |
56 |
Apples (3)
57 |
58 |
285 calories
59 |
60 |
61 |
3 servings
62 |
63 |
64 |
$1.37
65 |
66 |
* These calories are non-nutritious.
67 |
68 |
69 | 70 |
71 |
2
72 |
73 | 74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
My shopping cart
85 |
Your family has $101 to spend on 60,550 calories
86 |
Nutritious calories: 570
87 |
Non-nutritious calories: 0
88 |
89 |
90 |
91 |
92 |
QTY
93 |
94 |
95 |
ITEM
96 |
97 |
98 |
99 |
PRICE
100 |
101 |
102 |
103 |
104 |
105 |
2
106 |
107 |
108 |
109 |
Apples (3)
110 |
111 |
112 |
570 calories
113 |
$2.74
114 |
115 | 116 |
117 |
118 |
119 |
120 |
Total
121 |
122 |
$2.74
123 |
124 |
Empty cart
125 |
126 |
127 |
You haven't selected any groceries yet!
128 |
129 |
130 |
131 |
132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | --------------------------------------------------------------------------------