├── .eslintrc ├── .gitignore ├── .jscsrc ├── .travis.yml ├── README.md ├── client ├── actions │ └── recipe-actions.js ├── app.js ├── biff.js ├── components │ ├── app.jsx │ ├── button.jsx │ ├── home.jsx │ ├── ingredient-form-input.jsx │ ├── ingredient-form.jsx │ ├── ingredient.jsx │ ├── input.jsx │ ├── nav.jsx │ ├── notfound.jsx │ ├── recipe-details.jsx │ ├── recipe-form.jsx │ ├── recipe.jsx │ └── recipes.jsx ├── router.jsx └── stores │ └── recipe-store.js ├── db.json ├── db.json.bak ├── db └── db.js ├── gulpfile.js ├── hot ├── entry.js ├── index.html └── server.js ├── package.json ├── server ├── index.js └── mock-db.js ├── styles ├── _bootstrap_custom.scss ├── _normalize.scss ├── base │ ├── _base.scss │ ├── _content.scss │ └── _variables.scss ├── docs.md ├── layout │ └── _grid.scss ├── main.scss └── modules │ ├── _nav.scss │ ├── _recipeDetails.scss │ └── _recipes.scss ├── templates └── index.hbs ├── webpack.config.js ├── webpack.dev-config.js └── webpack.hot-config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | ecmaFeatures: 2 | jsx: true 3 | 4 | env: 5 | browser: false 6 | node: false 7 | amd: false 8 | mocha: false 9 | jasmine: false 10 | 11 | globals: 12 | require: true 13 | module: true 14 | 15 | rules: 16 | ########################################################################### 17 | # # 18 | # POSSIBLE ERRORS: these rules point out areas where you might have # 19 | # made mistakes. # 20 | # # 21 | ########################################################################### 22 | 23 | no-comma-dangle: 1 # disallow trailing commas in object literals 24 | no-cond-assign: 2 # disallow assignment in conditional expressions 25 | no-console: 2 # disallow use of console 26 | no-constant-condition: 2 # disallow use of constant expressions in conditions 27 | no-control-regex: 2 # disallow control characters in regular expressions 28 | no-debugger: 2 # disallow use of debugger 29 | no-dupe-keys: 2 # disallow duplicate keys when creating object literals 30 | no-empty: 2 # disallow empty statements 31 | no-empty-class: 2 # disallow the use of empty character classes in regular expressions 32 | no-ex-assign: 2 # disallow assigning to the exception in a catch block 33 | no-extra-boolean-cast: 2 # disallow double-negation boolean casts in a boolean context 34 | no-extra-parens: 0 # disallow unnecessary parentheses 35 | # NOTE: Allow for `return (/* JSX STUFF*/);` situations 36 | no-extra-semi: 2 # disallow unnecessary semicolons 37 | no-func-assign: 2 # disallow overwriting functions written as function declarations 38 | no-inner-declarations: 1 # disallow function or variable declarations in nested blocks 39 | no-invalid-regexp: 2 # disallow invalid regular expression strings in the RegExp 40 | # constructor 41 | no-irregular-whitespace: 2 # disallow irregular whitespace outside of strings and comments 42 | no-negated-in-lhs: 2 # disallow negation of the left operand of an in expression 43 | no-obj-calls: 2 # disallow the use of object properties of the global object (Math 44 | # and JSON) as functions 45 | no-regex-spaces: 1 # disallow multiple spaces in a regular expression literal 46 | no-reserved-keys: 1 # disallow reserved words being used as object literal keys 47 | no-sparse-arrays: 2 # disallow sparse arrays 48 | no-unreachable: 2 # disallow unreachable statements after a return, throw, continue, 49 | # or break statement 50 | use-isnan: 2 # disallow comparisons with the value NaN 51 | valid-typeof: 2 # ensure that the results of typeof are compared against a 52 | # valid string 53 | 54 | valid-jsdoc: # ensure JSDoc comments are valid 55 | [1, { "prefer": { "return": "returns" }, "requireReturn": false }] 56 | 57 | ########################################################################### 58 | # # 59 | # BEST PRACTICES: these rules are designed to prevent you from making # 60 | # mistakes. They either prescribe a better way of doing something or # 61 | # help you avoid pitfalls. # 62 | # # 63 | ########################################################################### 64 | 65 | block-scoped-var: 1 # treat var statements as if they were block scoped 66 | complexity: [1, 250] # specify the maximum cyclomatic complexity allowed in a program 67 | consistent-return: 0 # require return statements to either always or never specify values 68 | curly: 2 # specify curly brace conventions for all control statements 69 | default-case: 2 # require default case in switch statements 70 | dot-notation: 1 # encourages use of dot notation whenever possible 71 | eqeqeq: 2 # require the use of === and !== 72 | guard-for-in: 1 # make sure for-in loops have an if statement 73 | no-alert: 2 # disallow the use of alert, confirm, and prompt 74 | no-caller: 2 # disallow use of arguments.caller or arguments.callee 75 | no-div-regex: 1 # disallow division operators explicitly at beginning of regular 76 | # expression 77 | no-else-return: 1 # disallow else after a return in an if 78 | no-empty-label: 2 # disallow use of labels for anything other then loops and switches 79 | no-eq-null: 2 # disallow comparisons to null without a type-checking operator 80 | no-eval: 2 # disallow use of eval() 81 | no-extend-native: 2 # disallow adding to native types 82 | no-extra-bind: 2 # disallow unnecessary function binding 83 | no-fallthrough: 2 # disallow fallthrough of case statements 84 | no-floating-decimal: 2 # disallow the use of leading or trailing decimal points in numeric 85 | # literals 86 | no-implied-eval: 2 # disallow use of eval()-like methods 87 | no-iterator: 2 # disallow usage of __iterator__ property 88 | no-labels: 2 # disallow use of labeled statements 89 | no-lone-blocks: 2 # disallow unnecessary nested blocks 90 | no-loop-func: 0 # disallow creation of functions within loops 91 | no-multi-spaces: 0 # disallow use of multiple spaces 92 | no-multi-str: 2 # disallow use of multiline strings 93 | no-native-reassign: 2 # disallow reassignments of native objects 94 | no-new: 2 # disallow use of new operator when not part of the assignment or 95 | # comparison 96 | no-new-func: 2 # disallow use of new operator for Function object 97 | no-new-wrappers: 2 # disallows creating new instances of String,Number, and Boolean 98 | no-octal: 2 # disallow use of octal literals 99 | no-octal-escape: 2 # disallow use of octal escape sequences in string literals, such as 100 | # `var foo = "Copyright \251"` 101 | no-process-env: 0 # disallow use of process.env 102 | no-proto: 2 # disallow usage of __proto__ property 103 | no-redeclare: 1 # disallow declaring the same variable more then once 104 | no-return-assign: 0 # disallow use of assignment in return statement 105 | no-script-url: 2 # disallow use of javascript urls. 106 | no-self-compare: 2 # disallow comparisons where both sides are exactly the same 107 | no-sequences: 2 # disallow use of comma operator 108 | no-unused-expressions: 0 # disallow usage of expressions in statement position 109 | no-void: 2 # disallow use of void operator 110 | no-warning-comments: 0 # disallow usage of configurable warning terms in comments - e.g. 111 | # TODO or FIXME 112 | no-with: 2 # disallow use of the with statement 113 | radix: 2 # require use of the second argument for parseInt() 114 | vars-on-top: 0 # requires to declare all vars on top of their containing scope 115 | wrap-iife: [2, "inside"] # require immediate function invocation to be wrapped in parentheses 116 | yoda: "never" # require or disallow Yoda conditions 117 | 118 | ########################################################################### 119 | # # 120 | # STRICT MODE: these rules relate to using strict mode. # 121 | # # 122 | ########################################################################### 123 | 124 | global-strict: [2, "never"] # require or disallow the "use strict" pragma in the global scope 125 | no-extra-strict: 2 # disallow use of "use strict" when already in strict mode 126 | strict: 0 # require that all functions are run in strict mode 127 | 128 | ########################################################################### 129 | # # 130 | # VARIABLES: these rules have to do with variable declarations. # 131 | # # 132 | ########################################################################### 133 | 134 | no-catch-shadow: 2 # disallow the catch clause parameter name being the same as a 135 | # variable in the outer scope 136 | no-delete-var: 2 # disallow deletion of variables 137 | no-label-var: 2 # disallow labels that share a name with a variable 138 | no-shadow: 1 # disallow declaration of variables already declared in the 139 | # outer scope 140 | no-shadow-restricted-names: 2 # disallow shadowing of names such as arguments 141 | no-undef: 2 # disallow use of undeclared variables unless mentioned in a 142 | # /*global */ block 143 | no-undef-init: 2 # disallow use of undefined when initializing variables 144 | no-undefined: 2 # disallow use of undefined variable 145 | no-unused-vars: 2 # disallow declaration of variables that are not used in 146 | # the code 147 | no-use-before-define: 2 # disallow use of variables before they are defined 148 | 149 | ########################################################################### 150 | # # 151 | # NODE: these rules relate to functionality provided in Node.js. # 152 | # # 153 | ########################################################################### 154 | 155 | handle-callback-err: 0 # enforces error handling in callbacks 156 | no-mixed-requires: [1, true] # disallow mixing regular variable and require declarations 157 | no-new-require: 2 # disallow use of new operator with the require function 158 | no-path-concat: 2 # disallow string concatenation with __dirname and __filename 159 | no-process-exit: 0 # disallow process.exit() 160 | no-restricted-modules: 0 # restrict usage of specified node modules 161 | no-sync: 0 # disallow use of synchronous methods 162 | 163 | ########################################################################### 164 | # # 165 | # STYLISTIC ISSUES: these rules are purely matters of style and, # 166 | # while valueable to enforce consistently across a project, are # 167 | # quite subjective. # 168 | # # 169 | ########################################################################### 170 | 171 | brace-style: # enforce one true brace style 172 | [2, "1tbs", { "allowSingleLine": true }] 173 | camelcase: 2 # require camel case names 174 | comma-spacing: 2 # enforce spacing before and after comma 175 | comma-style: 2 # enforce one true comma style 176 | consistent-this: [2, "self"] # enforces consistent naming when capturing the current execution context 177 | eol-last: 2 # enforce newline at the end of file, with no multiple empty lines 178 | func-names: 0 # require function expressions to have a name 179 | func-style: 0 # enforces use of function declarations or expressions 180 | key-spacing: 2 # enforces spacing between keys and values in object literal properties 181 | max-nested-callbacks: [2, 4] # specify the maximum depth callbacks can be nested 182 | new-cap: 2 # require a capital letter for constructors 183 | new-parens: 2 # disallow the omission of parentheses when invoking a constructor with no arguments 184 | no-array-constructor: 2 # disallow use of the Array constructor 185 | no-lonely-if: 0 # disallow if as the only statement in an else block 186 | no-mixed-spaces-and-tabs: 2 # disallow mixed spaces and tabs for indentation 187 | no-nested-ternary: 2 # disallow nested ternary expressions 188 | no-new-object: 1 # disallow use of the Object constructor 189 | no-space-before-semi: 2 # disallow space before semicolon 190 | no-spaced-func: 2 # disallow space between function identifier and application 191 | no-ternary: 0 # disallow the use of ternary operators 192 | 193 | no-trailing-spaces: 2 # disallow trailing whitespace at the end of lines 194 | no-multiple-empty-lines: 2 # disallow multiple empty lines 195 | no-underscore-dangle: 0 # disallow dangling underscores in identifiers 196 | no-wrap-func: 2 # disallow wrapping of non-IIFE statements in parens 197 | one-var: 0 # allow just one var statement per function 198 | padded-blocks: 0 # enforce padding within blocks 199 | quotes: # specify whether double or single quotes should be used 200 | [1, "double", "avoid-escape"] 201 | quote-props: 0 # require quotes around object literal property names 202 | semi: [2, "always"] # require or disallow use of semicolons instead of ASI 203 | sort-vars: 0 # sort variables within the same declaration block 204 | space-after-keywords: "always" # require a space after certain keywords 205 | space-before-blocks: 2 # require or disallow space before blocks 206 | space-in-brackets: 0 # require or disallow spaces inside brackets 207 | space-in-parens: 0 # require or disallow spaces inside parentheses 208 | space-infix-ops: 2 # require spaces around operators 209 | space-return-throw-case: 2 # require a space after return, throw, and case 210 | spaced-line-comment: 2 # require or disallow a space immediately following 211 | # the // in a line comment 212 | wrap-regex: 0 # require regex literals to be wrapped in parentheses 213 | 214 | ########################################################################### 215 | # # 216 | # LEGACY: these rules are included for compatibility with JSHint and # 217 | # JSLint. While the names of the rules may not match up with their # 218 | # JSHint/JSLint counterpart, the functionality is the same. # 219 | # # 220 | ########################################################################### 221 | 222 | max-depth: 0 # specify the maximum depth that blocks can be nested 223 | max-len: [2, 100, 4] # specify the maximum length of a line in your program 224 | max-params: [1, 3] # limits the number of parameters that can be used in the function 225 | # declaration. 226 | max-statements: 0 # specify the maximum number of statement allowed in a function 227 | no-bitwise: 0 # disallow use of bitwise operators 228 | no-plusplus: 0 # disallow use of unary operators, ++ and -- 229 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \.git 2 | \.hg 3 | 4 | \.DS_Store 5 | \.project 6 | bower_components 7 | node_modules 8 | npm-debug\.log 9 | 10 | # Keep vendor and build libraries out of source. 11 | app/js/vendor 12 | app/js-dist 13 | app/js-map 14 | app/css-dist 15 | app/css-map 16 | test/mocha/js-dist 17 | test/jasmine/js-dist 18 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowOperatorBeforeLineBreak": ["."], 3 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 4 | 5 | "requireCurlyBraces": [ 6 | "if", 7 | "else", 8 | "for", 9 | "while", 10 | "do", 11 | "try", 12 | "catch" 13 | ], 14 | 15 | "requireSpaceBeforeKeywords": [ 16 | "if", 17 | "else", 18 | "for", 19 | "while", 20 | "do", 21 | "switch", 22 | "case", 23 | "return", 24 | "try", 25 | "catch" 26 | ], 27 | "requireSpacesInFunctionDeclaration": { 28 | "beforeOpeningCurlyBrace": true 29 | }, 30 | "requireSpacesInFunctionExpression": { 31 | "beforeOpeningCurlyBrace": true, 32 | "beforeOpeningRoundBrace": true 33 | }, 34 | "requireSpacesInAnonymousFunctionExpression": { 35 | "beforeOpeningCurlyBrace": true, 36 | "beforeOpeningRoundBrace": true 37 | }, 38 | "disallowSpacesInNamedFunctionExpression": { 39 | "beforeOpeningRoundBrace": true 40 | }, 41 | "disallowSpacesInFunctionDeclaration": { 42 | "beforeOpeningRoundBrace": true 43 | }, 44 | "requireSpaceAfterLineComment": true, 45 | "requireSpaceBeforeObjectValues": true, 46 | "requireSpaceBetweenArguments": true, 47 | "requireSpaceAfterBinaryOperators": true, 48 | "requireSpaceBeforeBinaryOperators": true, 49 | "requireSpaceAfterKeywords": true, 50 | "requireSpaceBeforeBlockStatements": true, 51 | "requireSpacesInConditionalExpression": true, 52 | "requireSpacesInForStatement": true, 53 | "disallowSpaceAfterObjectKeys": true, 54 | "disallowSpacesInsideArrayBrackets": { 55 | "allExcept": [ "[", "]", "{", "}" ] 56 | }, 57 | "requireSpacesInsideObjectBrackets": "all", 58 | 59 | "disallowMixedSpacesAndTabs": true, 60 | "disallowMultipleLineBreaks": true, 61 | "disallowMultipleLineStrings": true, 62 | "disallowMultipleVarDecl": true, 63 | "disallowNewlineBeforeBlockStatements": true, 64 | "disallowQuotedKeysInObjects": "allButReserved", 65 | "disallowTrailingComma": true, 66 | "disallowTrailingWhitespace": true, 67 | "disallowSpacesInCallExpression": true, 68 | "requireCommaBeforeLineBreak": true, 69 | "requireSpacesInForStatement": true, 70 | "disallowEmptyBlocks": true, 71 | "disallowYodaConditions": true, 72 | "disallowKeywordsOnNewLine": ["else"], 73 | "requireCamelCaseOrUpperCaseIdentifiers": true, 74 | "requireCapitalizedConstructors": true, 75 | "requireOperatorBeforeLineBreak": true, 76 | "requireParenthesesAroundIIFE": true, 77 | "safeContextKeyword": ["self"], 78 | "validateIndentation": 2, 79 | "validateQuoteMarks": "\"" 80 | } 81 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | 6 | before_install: 7 | # GUI for real browsers. 8 | - export DISPLAY=:99.0 9 | - sh -e /etc/init.d/xvfb start 10 | 11 | script: 12 | - ./node_modules/.bin/gulp build 13 | - ./node_modules/.bin/gulp build:dev 14 | - ./node_modules/.bin/gulp check:ci 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Recipes! (w/ Flux) 2 | ================== 3 | 4 | [![Build Status][trav_img]][trav_site] 5 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/seattlejs/seattlejs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 6 | 7 | ## Server 8 | 9 | ### Dev Mode 10 | 11 | Install, setup. 12 | 13 | ``` 14 | $ npm install 15 | ``` 16 | 17 | Run the watchers, dev and source maps servers 18 | 19 | ``` 20 | $ gulp dev 21 | ``` 22 | 23 | URLS to test things out: 24 | 25 | * `http://127.0.0.1:3000/`: Server-side bootstrap, JS takes over. 26 | 27 | ### Production 28 | 29 | Install, setup. 30 | 31 | ``` 32 | $ npm install --production 33 | $ npm run-script build 34 | ``` 35 | 36 | Run the server. 37 | 38 | ``` 39 | $ NODE_ENV=production node server/index.js 40 | ``` 41 | 42 | [trav]: https://travis-ci.org/ 43 | [trav_img]: https://api.travis-ci.org/FormidableLabs/recipes-flux.svg 44 | [trav_site]: https://travis-ci.org/FormidableLabs/recipes-flux 45 | -------------------------------------------------------------------------------- /client/actions/recipe-actions.js: -------------------------------------------------------------------------------- 1 | var Biff = require("../biff"); 2 | 3 | // Request 4 | var request = require("superagent"); 5 | 6 | var RecipeActions = Biff.createActions({ 7 | recipeCreated: function (data) { 8 | var self = this; 9 | 10 | request 11 | .post("/recipes/create") 12 | .send({ recipe: data }) 13 | .set("Accept", "application/json") 14 | .end(function () { 15 | self.dispatch({ 16 | actionType: "RECIPE_CREATE", 17 | data: data 18 | }); 19 | }); 20 | }, 21 | recipeDeleted: function (data) { 22 | var self = this; 23 | 24 | request 25 | .del("/recipes/delete") 26 | .send({ _id: data._id }) 27 | .set("Accept", "application/json") 28 | .end(function () { 29 | self.dispatch({ 30 | actionType: "RECIPE_DELETE", 31 | data: data 32 | }); 33 | }); 34 | }, 35 | syncRecipe: function (data) { 36 | request 37 | .put("/recipes/update") 38 | .send({ recipe: data }) 39 | .set("Accept", "application/json") 40 | .end(function () {}); 41 | }, 42 | loadRecipes: function (data) { 43 | this.dispatch({ 44 | actionType: "RECIPES_LOAD", 45 | data: JSON.parse(data) 46 | }); 47 | }, 48 | portionsChanged: function (data) { 49 | this.dispatch({ 50 | actionType: "PORTIONS_CHANGED", 51 | data: data 52 | }); 53 | }, 54 | inputChanged: function (data) { 55 | this.dispatch({ 56 | actionType: "INPUT_CHANGED", 57 | data: data 58 | }); 59 | }, 60 | ingredientDeleted: function (data) { 61 | this.dispatch({ 62 | actionType: "INGREDIENT_DELETED", 63 | data: data 64 | }); 65 | }, 66 | ingredientCreated: function (data) { 67 | this.dispatch({ 68 | actionType: "INGREDIENT_CREATED", 69 | data: data 70 | }); 71 | } 72 | }); 73 | 74 | module.exports = RecipeActions; 75 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | // ENTRY POINT 2 | 3 | // Router 4 | var Router = require("./router"); 5 | 6 | // Fire up the router and attach to DOM 7 | Router.run(document.getElementById("js-content")); 8 | -------------------------------------------------------------------------------- /client/biff.js: -------------------------------------------------------------------------------- 1 | var Biff = require("biff"); 2 | 3 | module.exports = new Biff(); 4 | -------------------------------------------------------------------------------- /client/components/app.jsx: -------------------------------------------------------------------------------- 1 | // React 2 | var React = require("react"); 3 | 4 | // Router 5 | var Router = require("react-router"); 6 | var RouteHandler = Router.RouteHandler; 7 | 8 | // Child Components 9 | var Nav = require("./nav"); 10 | 11 | // Component 12 | var App = React.createClass({ 13 | displayName: "App", 14 | propTypes: {}, 15 | mixins: [], 16 | 17 | getInitialState: function () { return null; }, 18 | 19 | componentWillMount: function () {}, 20 | 21 | componentWillUnmount: function () {}, 22 | 23 | render: function () { 24 | return ( 25 |
26 |
29 | ); 30 | } 31 | }); 32 | 33 | module.exports = App; 34 | -------------------------------------------------------------------------------- /client/components/button.jsx: -------------------------------------------------------------------------------- 1 | // React 2 | var React = require("react"); 3 | 4 | // Component 5 | var Input = React.createClass({ 6 | displayName: "Input", 7 | propTypes: {}, 8 | mixins: [], 9 | 10 | getInitialState: function () { return null; }, 11 | 12 | componentWillMount: function () {}, 13 | 14 | handleButtonClick: function () { 15 | // Proxy to parent moving to generalize input 16 | this.props.buttonCallback( 17 | this.props._id, 18 | this.props.accessor, 19 | this.props.index 20 | ); 21 | }, 22 | 23 | componentWillUnmount: function () {}, 24 | 25 | render: function () { 26 | return ( 27 | 34 | ); 35 | } 36 | }); 37 | 38 | module.exports = Input; 39 | -------------------------------------------------------------------------------- /client/components/home.jsx: -------------------------------------------------------------------------------- 1 | // React 2 | var React = require("react"); 3 | 4 | // Router 5 | var Router = require("react-router"); 6 | var RouteHandler = Router.RouteHandler; 7 | 8 | // Component 9 | var Home = React.createClass({ 10 | displayName: "Home", 11 | propTypes: {}, 12 | mixins: [], 13 | 14 | getInitialState: function () { return null; }, 15 | 16 | componentWillMount: function () {}, 17 | 18 | componentWillUnmount: function () {}, 19 | 20 | render: function () { 21 | return ( 22 |
23 |

24 | home rendered 25 |

26 | 27 |
28 | ); 29 | } 30 | }); 31 | 32 | module.exports = Home; 33 | -------------------------------------------------------------------------------- /client/components/ingredient-form-input.jsx: -------------------------------------------------------------------------------- 1 | // React 2 | var React = require("react"); 3 | var RecipeActions = require("../actions/recipe-actions"); 4 | 5 | // Child Components 6 | var Input = require("./input"); 7 | 8 | // Component 9 | var IngredientFormInput = React.createClass({ 10 | getInitialState: function () { 11 | return { 12 | value: null 13 | }; 14 | }, 15 | 16 | getDefaultProps: function () { 17 | return { 18 | labelHidden: true 19 | }; 20 | }, 21 | 22 | handleChange: function () { 23 | var newValue = this.refs.input.getDOMNode().value; 24 | 25 | RecipeActions.inputChanged({ 26 | _id: this.props._id, 27 | accessor: this.props.accessor, 28 | index: this.props.index, 29 | value: newValue 30 | }); 31 | 32 | this.setState({ 33 | value: newValue 34 | }); 35 | }, 36 | 37 | render: function () { 38 | var value = this.state.value || this.props.value; 39 | 40 | return ( 41 | 46 | ); 47 | } 48 | }); 49 | 50 | module.exports = IngredientFormInput; 51 | -------------------------------------------------------------------------------- /client/components/ingredient-form.jsx: -------------------------------------------------------------------------------- 1 | // React 2 | var React = require("react"); 3 | var RecipeActions = require("../actions/recipe-actions"); 4 | 5 | // Child Components 6 | var Button = require("./button"); 7 | var IngredientFormInput = require("./ingredient-form-input"); 8 | 9 | // Component 10 | var IngredientForm = React.createClass({ 11 | deleteIngredient: function () { 12 | RecipeActions.ingredientDeleted({ 13 | _id: this.props._id, 14 | index: this.props.index 15 | }); 16 | }, 17 | 18 | buildField: function (field, index) { 19 | return ( 20 | 28 | ); 29 | }, 30 | 31 | render: function () { 32 | var ingredientFields = [ 33 | { 34 | name: "Ingredient", 35 | accessor: "ingredient" 36 | }, 37 | { 38 | name: "Quantity", 39 | accessor: "quantity" 40 | }, 41 | { 42 | name: "Measurement", 43 | accessor: "measurement" 44 | }, 45 | { 46 | name: "Modifier", 47 | accessor: "modifier" 48 | } 49 | ]; 50 | 51 | var ingredients = ingredientFields.map(this.buildField); 52 | var button = ( 53 | 82 |

83 |
84 |
85 | {ingredientNodes} 86 |
87 |
91 |
92 | 93 |
94 | ); 95 | } 96 | }); 97 | 98 | module.exports = RecipeDetails; 99 | -------------------------------------------------------------------------------- /client/components/recipe-form.jsx: -------------------------------------------------------------------------------- 1 | // React 2 | var React = require("react"); 3 | var RecipeStore = require("../stores/recipe-store"); 4 | var RecipeActions = require("../actions/recipe-actions"); 5 | var uuid = require("uuid"); 6 | 7 | // Router 8 | var Router = require("react-router"); 9 | var RouteHandler = Router.RouteHandler; 10 | 11 | // Child Components 12 | var Button = require("./button"); 13 | var IngredientForm = require("./ingredient-form"); 14 | var IngredientFormInput = require("./ingredient-form-input"); 15 | 16 | // Component 17 | function getState(id) { 18 | return RecipeStore.getRecipe(id); 19 | } 20 | 21 | var RecipeForm = React.createClass({ 22 | displayName: "RecipeForm", 23 | propTypes: {}, 24 | mixins: [RecipeStore.mixin], 25 | 26 | getInitialState: function () { 27 | if (this.props.params._id) { 28 | // User came in from the edit button of an existing recipe, 29 | // so let's use the params to figure out which recipe so that we can populate the forms 30 | this._id = this.props.params.id; 31 | return RecipeStore.getRecipe(this.props.params._id); 32 | } 33 | 34 | // Create the blank recipe in the store to edit 35 | // this will create an empty record if they leave, but that's 36 | // not terrible because they can edit or delete it from the inbox 37 | var newRecipe = { 38 | _id: uuid.v4(), 39 | title: "New Recipe (edit me)", 40 | portions: "", 41 | totalTimeInMinutes: "", 42 | instructions: "", 43 | ingredients: [ 44 | { 45 | ingredient: "Brown Rice", 46 | quantity: 2.5, 47 | measurement: "cups", 48 | modifier: "cooked" 49 | }, 50 | { 51 | ingredient: "", 52 | quantity: "", 53 | measurement: "", 54 | modifier: "" 55 | } 56 | ], 57 | saved: false 58 | }; 59 | 60 | RecipeActions.recipeCreated(newRecipe); 61 | this._id = newRecipe._id; 62 | return newRecipe; 63 | }, 64 | 65 | componentWillMount: function () {}, 66 | 67 | componentWillUnmount: function () {}, 68 | 69 | storeDidChange: function () { 70 | RecipeActions.syncRecipe(this.state); 71 | this.setState(getState(this.state._id)); 72 | }, 73 | 74 | ingredientCreated: function () { 75 | RecipeActions.ingredientCreated({ 76 | _id: this.state._id 77 | }); 78 | }, 79 | 80 | createNodes: function (ingredient, index) { 81 | return ( 82 | 87 | ); 88 | }, 89 | 90 | render: function () { 91 | var ingredientFormNodes = this.state.ingredients.map( 92 | this.createNodes 93 | ); 94 | 95 | return ( 96 |
97 | 104 | 111 | 118 | 126 | 127 | {ingredientFormNodes} 128 | 129 |
135 | ); 136 | } 137 | }); 138 | 139 | module.exports = RecipeForm; 140 | -------------------------------------------------------------------------------- /client/components/recipe.jsx: -------------------------------------------------------------------------------- 1 | // React 2 | var React = require("react"); 3 | var RecipeActions = require("../actions/recipe-actions"); 4 | 5 | // Router 6 | var Router = require("react-router"); 7 | var RouteHandler = Router.RouteHandler; 8 | var Link = Router.Link; 9 | 10 | // Child Components 11 | var Button = require("./button"); 12 | 13 | // Component 14 | var Recipe = React.createClass({ 15 | displayName: "Recipe", 16 | propTypes: {}, 17 | mixins: [], 18 | 19 | getInitialState: function () { return null; }, 20 | 21 | componentWillMount: function () {}, 22 | 23 | componentWillUnmount: function () {}, 24 | 25 | deleteRecipe: function () { 26 | RecipeActions.recipeDeleted({ 27 | _id: this.props.recipe._id 28 | }); 29 | }, 30 | 31 | render: function () { 32 | return ( 33 |
34 |

35 | 36 | {this.props.recipe.title} 37 | 38 |   39 | 40 | *Edit* 41 | 42 |

49 | ); 50 | } 51 | }); 52 | 53 | module.exports = Recipe; 54 | -------------------------------------------------------------------------------- /client/components/recipes.jsx: -------------------------------------------------------------------------------- 1 | // React 2 | var React = require("react"); 3 | var Recipe = require("./recipe"); 4 | var RecipeStore = require("../stores/recipe-store"); 5 | 6 | // Router 7 | var Router = require("react-router"); 8 | var RouteHandler = Router.RouteHandler; 9 | 10 | // Component 11 | function getState() { 12 | return { 13 | store: RecipeStore.getRecipes() 14 | }; 15 | } 16 | 17 | var Recipes = React.createClass({ 18 | displayName: "Recipes", 19 | mixins: [RecipeStore.mixin], 20 | 21 | getInitialState: function () { 22 | return getState(); 23 | }, 24 | 25 | componentWillMount: function () {}, 26 | 27 | componentWillUnmount: function () {}, 28 | 29 | createRecipeNodes: function () { 30 | var nodes = this.state.store.map(function (recipe) { 31 | return ( 32 | 33 | ); 34 | }); 35 | return nodes; 36 | }, 37 | 38 | storeDidChange: function () { 39 | this.setState(getState()); 40 | }, 41 | 42 | render: function () { 43 | var recipeNodes = this.createRecipeNodes(); 44 | 45 | return ( 46 |
47 |

Recipe Bank:

48 | {recipeNodes} 49 | 50 |
51 | ); 52 | } 53 | }); 54 | 55 | module.exports = Recipes; 56 | -------------------------------------------------------------------------------- /client/router.jsx: -------------------------------------------------------------------------------- 1 | // React 2 | var React = require("react"); 3 | var Router = require("react-router"); 4 | var App = require("./components/app"); 5 | var Home = require("./components/home"); 6 | var Recipes = require("./components/recipes"); 7 | var RecipeDetails = require("./components/recipe-details"); 8 | var RecipeForm = require("./components/recipe-form"); 9 | var NotFound = require("./components/notfound"); 10 | var RecipeActions = require("./actions/recipe-actions"); 11 | 12 | // Request 13 | var request = require("superagent"); 14 | 15 | // Set up Router object 16 | var Route = Router.Route; 17 | var DefaultRoute = Router.DefaultRoute; 18 | var NotFoundRoute = Router.NotFoundRoute; 19 | 20 | // Declare routes 21 | var routes = ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | module.exports = { 33 | run: function (el) { 34 | request 35 | .get("/recipes") 36 | .set("Accept", "application/json") 37 | .end(function (error, res) { 38 | RecipeActions.loadRecipes(res.text); 39 | Router.run(routes, function (Handler, state) { 40 | // "Alternatively, you can pass the param data down..." 41 | // https://github.com/rackt/react-router/blob/master/docs/guides/ 42 | // overview.md#dynamic-segments 43 | var params = state.params; 44 | React.render(, el); 45 | }); 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /client/stores/recipe-store.js: -------------------------------------------------------------------------------- 1 | var Biff = require("../biff"); 2 | var _ = require("lodash"); 3 | 4 | // Creates a DataStore 5 | var RecipeStore = Biff.createStore({ 6 | // Initial setup 7 | _recipes: [], 8 | 9 | updateRecipeIngredientList: function (_id, index) { 10 | var recipe = this.getRecipe(_id); 11 | if (index || index === 0) { 12 | // Delete operation 13 | recipe.ingredients.splice(index, 1); 14 | } else { 15 | // Create operation 16 | recipe.ingredients.push( 17 | { 18 | ingredient: "", 19 | quantity: "", 20 | measurement: "", 21 | modifier: "" 22 | } 23 | ); 24 | } 25 | }, 26 | 27 | updateRecipe: function (data) { 28 | var recipe = this.getRecipe(data._id); 29 | if (data.index || data.index === 0) { 30 | recipe.ingredients[data.index][data.accessor] = data.value; 31 | } else { 32 | recipe[data.accessor] = data.value; 33 | } 34 | }, 35 | 36 | updatePortions: function (data) { 37 | // TODO: validate data 38 | var recipe = this.getRecipe(data._id); 39 | 40 | if (recipe.portions !== data.portions) { 41 | var multiplier = data.portions / recipe.portions; 42 | recipe.ingredients.map(function (ing) { 43 | ing.quantity = ing.quantity * multiplier; 44 | }); 45 | 46 | recipe.portions = data.portions; 47 | } 48 | }, 49 | 50 | loadRecipes: function (recipes) { 51 | this._recipes = recipes; 52 | }, 53 | 54 | createRecipe: function (recipe) { 55 | this._recipes.push(recipe); 56 | }, 57 | 58 | createIngredient: function () {}, 59 | 60 | deleteRecipe: function (_id) { 61 | _.remove(this._recipes, { _id: _id }); 62 | }, 63 | 64 | getRecipe: function (_id) { 65 | return _.find(this._recipes, { _id: _id }); 66 | }, 67 | 68 | getRecipes: function () { 69 | return this._recipes; 70 | } 71 | }, function (payload) { 72 | if (payload.actionType === "RECIPE_CREATE") { 73 | this.createRecipe(payload.data); 74 | this.emitChange(); 75 | } 76 | if (payload.actionType === "RECIPE_DELETE") { 77 | this.deleteRecipe(payload.data._id); 78 | this.emitChange(); 79 | } 80 | if (payload.actionType === "RECIPES_LOAD") { 81 | this.loadRecipes(payload.data); 82 | this.emitChange(); 83 | } 84 | if (payload.actionType === "INPUT_CHANGED") { 85 | this.updateRecipe({ 86 | _id: payload.data._id, 87 | accessor: payload.data.accessor, 88 | index: payload.data.index, 89 | value: payload.data.value 90 | }); 91 | this.emitChange(); 92 | } 93 | if (payload.actionType === "INGREDIENT_DELETED") { 94 | this.updateRecipeIngredientList( 95 | payload.data._id, payload.data.index 96 | ); 97 | this.emitChange(); 98 | } 99 | if (payload.actionType === "INGREDIENT_CREATED") { 100 | RecipeStore.updateRecipeIngredientList(payload.data._id); 101 | RecipeStore.emitChange(); 102 | } 103 | if (payload.actionType === "PORTIONS_CHANGED") { 104 | RecipeStore.updatePortions(payload.data); 105 | RecipeStore.emitChange(); 106 | } 107 | }); 108 | 109 | module.exports = RecipeStore; 110 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "recipes": [ 3 | { 4 | "_id": "781493c4-0b32-4186-aaa0-b7c6cb4b0c49", 5 | "saved": true, 6 | "title": "Stuffed Chard Leaves", 7 | "portions": 6, 8 | "totalTimeInMinutes": 60, 9 | "ingredients": [ 10 | { 11 | "ingredient": "onion", 12 | "quantity": 1, 13 | "measurement": null, 14 | "modifier": "chopped" 15 | }, 16 | { 17 | "ingredient": "oil", 18 | "quantity": 1, 19 | "measurement": "tablespoon", 20 | "modifier": null 21 | }, 22 | { 23 | "ingredient": "brown rice", 24 | "quantity": 2.5, 25 | "measurement": "cups", 26 | "modifier": "cooked" 27 | }, 28 | { 29 | "ingredient": "cottage cheese", 30 | "quantity": 1.5, 31 | "measurement": "cups", 32 | "modifier": null 33 | }, 34 | { 35 | "ingredient": "egg", 36 | "quantity": 1, 37 | "measurement": null, 38 | "modifier": "beaten" 39 | }, 40 | { 41 | "ingredient": "parsley", 42 | "quantity": 0.5, 43 | "measurement": "cup", 44 | "modifier": "chopped" 45 | }, 46 | { 47 | "ingredient": "raisins", 48 | "quantity": 0.75, 49 | "measurement": "cup", 50 | "modifier": null 51 | }, 52 | { 53 | "ingredient": "dill", 54 | "quantity": 1, 55 | "measurement": "teaspoon", 56 | "modifier": null 57 | }, 58 | { 59 | "ingredient": "salt", 60 | "quantity": 0.75, 61 | "measurement": "teaspoon", 62 | "modifier": null 63 | }, 64 | { 65 | "ingredient": "swiss chard leaves", 66 | "quantity": 16, 67 | "measurement": "leaves", 68 | "modifier": "large" 69 | } 70 | ], 71 | "instructions": "Preheat oven to 350°F. \n Saute onion in oil. Mix all ingredients except chard. \n Wash and dry chard leaves and remove stems, including the fat part of the rib if it extends rigidly up into the leaf (select leaves that are not too 'ribby'). Place 2 tablespoons or more of filling on the underside of the leaf, a third of the way from the bottom. Fold over the sides of the leaf and roll up into a square packet. Place seam-side down in a greased casserole. Cover and bake for about 30 minutes. \n Alternatively, steam the rolls in a steamer basket over boiling water until the leaves are tender, about 20 minutes. Bake any extra filling and serve with stuffed leaves." 72 | }, 73 | { 74 | "_id": "70dd964b-6225-4d7c-8b1e-7e983d901a80", 75 | "saved": true, 76 | "title": "Helen's Polenta with Eggplant", 77 | "portions": 6, 78 | "totalTimeInMinutes": 120, 79 | "ingredients": [ 80 | { 81 | "ingredient": "onion", 82 | "quantity": 1, 83 | "measurement": null, 84 | "modifier": "chopped fine" 85 | }, 86 | { 87 | "ingredient": "green pepper", 88 | "quantity": 1, 89 | "measurement": null, 90 | "modifier": "chopped fine" 91 | }, 92 | { 93 | "ingredient": "garlic clove", 94 | "quantity": 1, 95 | "measurement": null, 96 | "modifier": null 97 | }, 98 | { 99 | "ingredient": "olive oil", 100 | "quantity": 1, 101 | "measurement": "tablespoon", 102 | "modifier": null 103 | }, 104 | { 105 | "ingredient": "tomato", 106 | "quantity": 3, 107 | "measurement": "cups", 108 | "modifier": "chopped" 109 | }, 110 | { 111 | "ingredient": "parsley", 112 | "quantity": 0.25, 113 | "measurement": "cup", 114 | "modifier": "chopped" 115 | }, 116 | { 117 | "ingredient": "basil", 118 | "quantity": 1, 119 | "measurement": "teaspoon", 120 | "modifier": "dried" 121 | }, 122 | { 123 | "ingredient": "eggplant", 124 | "quantity": 1.5, 125 | "measurement": "pounds", 126 | "modifier": null 127 | }, 128 | { 129 | "ingredient": "mozzarella cheese", 130 | "quantity": 0.75, 131 | "measurement": "cup", 132 | "modifier": "grated" 133 | } 134 | ], 135 | "instructions": "Place polenta in top of a double boiler with 4 cups of boiling water and 1/2 teaspoon of the salt. Bring to a boil, reduce heat to low, and cook for 30 to 40 minutes, until mush is quite thick. Pack into round, straight sided containers that are, ideally, the same diameter as the eggplants. Refrigerate. \n Meanwhile, saute onion, pepper and garlic clove in oil until tender. Crush garlic with a fork. Then add Tomatoes, parsley, basil, and remaining 1 teaspoon of salt. Bring to a boil and simmer, stirring often, for 15 minutes, breaking up tomatoes as you stir. \n When polenta is chilled, slice it in 1/2 rounds. Do the same with the eggplants. Oil a 9 x 13 inch baking dish and overlap alternating slices of eggplant and polenta in a pretty, fish scale design. Or, if the eggplant is too big, layer it lasagna style. Pour tomato sauce over the whole works and sprinkle cheese on top. Cover the dish and bake in a 350°F oven for 45 minutes, or until eggplant tests done with a fork." 136 | }, 137 | { 138 | "_id": "48690fc9-466b-4285-81ea-eeeda7411d92", 139 | "saved": true, 140 | "title": "Guacamole", 141 | "portions": 4, 142 | "totalTimeinMinutes": 10, 143 | "ingredients": [ 144 | { 145 | "ingredient": "avocados", 146 | "quantity": 2, 147 | "measurement": null, 148 | "modifier": "halved, peeled, pitted, and chopped" 149 | }, 150 | { 151 | "ingredient": "lime juice", 152 | "quantity": 2, 153 | "measurement": "tablespoon", 154 | "modifier": "fresh" 155 | }, 156 | { 157 | "ingredient": "fresh cilantro", 158 | "quantity": 2, 159 | "measurement": "tablespoon", 160 | "modifier": "chopped" 161 | }, 162 | { 163 | "ingredient": "salt", 164 | "quantity": null, 165 | "measurement": null, 166 | "modifier": "pinch" 167 | } 168 | ], 169 | "instructions": "Mash avocado, lime juice, and a pinch of salt in a mortar and pestle or a medium bowl with a fork until thick and smooth. Mix in 1/4 cup water 1 tablespoonful at a time until mixture is creamy and smooth. Stir in cilantro. Season with salt." 170 | }, 171 | { 172 | "_id": "00f5ac76-1709-4b17-bd99-02a780047855", 173 | "saved": true, 174 | "title": "Roast Chicken", 175 | "portions": 4, 176 | "totalTimeInMinutes": 130, 177 | "ingredients": [ 178 | { 179 | "ingredient": "chicken", 180 | "quantity": 4, 181 | "measurement": "lbs", 182 | "modifier": "whole" 183 | }, 184 | { 185 | "ingredient": "unsalted butter", 186 | "quantity": 0.25, 187 | "measurement": "cup", 188 | "modifier": "melted" 189 | }, 190 | { 191 | "ingredient": "salt", 192 | "quantity": 1, 193 | "measurement": "tablespoon", 194 | "modifier": "kosher" 195 | } 196 | ], 197 | "instructions": "Rub or pat salt onto breast, legs, and thighs of chicken. \n Place chicken in a large resealable plastic bag. \n Set open bag in a large bowl, keeping chicken breast side up. \n Chill for at least 8 hours and up to 2 days.Arrange a rack in upper third of oven; \n preheat to 500°F. \n Set a wire rack in a large heavy roasting pan. \n Remove chicken from bag. \n Pat dry with paper towels (do not rinse) Place chicken, breast side up, on prepared rack. \n Loosely tie legs together with kitchen twine and tuck wing tips under. \n Brush chicken all over with some of the butter. \n Pour 1 cup water into pan. \n Roast chicken, brushing with butter after 15 minutes, until skin is light golden brown and taut, about 30 minutes. \n Reduce oven temperature to 350°F. \n Remove chicken from oven and brush with more butter. \n Let rest for 15-20 minutes. \n Return chicken to oven; \n roast, basting with butter every 10 minutes, until skin is golden brown and a thermometer inserted into the thickest part of the thigh registers 165°F, 40-45 minutes. \n Let rest for 20 minutes. \n Carve and serve with pan juices. \n " 198 | }, 199 | { 200 | "_id": "09d7b4d4-0411-4ab5-b139-c964c153d6d3", 201 | "saved": true, 202 | "title": "Persian Rice", 203 | "portions": 6, 204 | "totalTimeInMinutes": 60, 205 | "ingredients": [ 206 | { 207 | "ingredient": "basmati rice", 208 | "quantity": 2, 209 | "measurement": "cups", 210 | "modifier": null 211 | }, 212 | { 213 | "ingredient": "yogurt", 214 | "quantity": 2, 215 | "measurement": "cups", 216 | "modifier": "whole milk plain" 217 | }, 218 | { 219 | "ingredient": "butter", 220 | "quantity": 3, 221 | "measurement": "tablespoons", 222 | "modifier": "unsalted" 223 | }, 224 | { 225 | "ingredient": "saffron threads", 226 | "quantity": 1, 227 | "measurement": null, 228 | "modifier": "pinch" 229 | }, 230 | { 231 | "ingredient": "salt", 232 | "quantity": "3", 233 | "measurement": "teaspoons", 234 | "modifiers": "kosher divided" 235 | } 236 | ], 237 | "instructions": "Place rice in a medium saucepan; \n add 2 teaspoons salt and cold water to cover by 2. \n Bring to a boil over medium heat; \n reduce heat to low and simmer for 5 minutes. \n Drain rice, reserving 3/4 cup cooking liquid. \n Place saffron and 1/2 cup reserved cooking liquid in a small bowl; \n let saffron soften for 5 minutes. \n Place yogurt in a medium bowl and stir in remaining 1 teaspoon salt and saffron water. \n Add rice and stir to coat. \n Melt butter in a large deep nonstick skillet over medium heat; \n swirl to coat bottom and sides of pan. n Add rice, mounding slightly in center. n Poke 6-7 holes in rice with the end of a wooden spoon. n Cover with foil, then a lid. n Cook, rotating skillet over burner for even cooking, for 10 minutes (do not stir). n Reduce heat to low; cook, adding more reserved cooking liquid by tablespoonfuls if rice has not finished cooking when water evaporates, until a golden brown crust forms on bottom of rice, 20-25 minutes. \n Remove lid and foil; \n invert a plate over skillet. n Using oven mitts, carefully invert rice onto plate; \n use a heatproof spatula to remove any crust remaining in skillet. \n" 238 | }, 239 | { 240 | "_id": "6d0bd7a5-0623-4819-a479-2763d0728a03", 241 | "saved": true, 242 | "title": "Roasted Beets with Cumin and Mint", 243 | "portions": 6, 244 | "totalTimeInMinutes": 105, 245 | "ingredients": [ 246 | { 247 | "ingredient": "beets", 248 | "quantity": 3, 249 | "measurement": null, 250 | "modifier": "medium" 251 | }, 252 | { 253 | "ingredient": "cumin seeds", 254 | "quantity": 1, 255 | "measurement": "teaspoon", 256 | "modifier": "toasted and slightly cracked" 257 | }, 258 | { 259 | "ingredient": "lemon juice", 260 | "quantity": 1, 261 | "measurement": "tablespoon", 262 | "modifier": null 263 | }, 264 | { 265 | "ingredient": "salt", 266 | "quantity": 0.5, 267 | "measurement": "teaspoon", 268 | "modifier": null 269 | }, 270 | { 271 | "ingredient": "pepper", 272 | "quantity": 0.25, 273 | "measurement": "teaspoon", 274 | "modifier": "black" 275 | }, 276 | { 277 | "ingredient": "olive oil", 278 | "quantity": 2, 279 | "measurement": "tablespoon", 280 | "modifier": "Extra Virgin" 281 | }, 282 | { 283 | "ingredient": "mint", 284 | "quantity": 0.66, 285 | "measurement": "cup", 286 | "modifier": "fresh, coarsely chopped" 287 | } 288 | ], 289 | "instructions": "Stir together lemon juice, cumin seeds, salt, and pepper in a medium bowl. \n Stir in oil and let stand while roasting beets. \n Put oven rack in middle position and preheat oven to 425°F. \n Tightly wrap beets in a double layer of foil and roast on a baking sheet until tender, 1 to 1 1/4 hours. \n Cool to warm in foil package, about 20 minutes. \n When beets are cool enough to handle, peel them, discarding stems and root ends, then cut into 1/2-inch-wide wedges. \n Toss warm beets with dressing. \n Stir in mint just before serving. \n cooks' note: \n Beets can be roasted and tossed with dressing 4 hours ahead, then kept, covered, at room temperature. \n" 290 | }, 291 | { 292 | "_id": "17126c4a-11d6-4c7b-a4ac-03e98cf6333f", 293 | "saved": true, 294 | "title": "Chili Relleno Casserole", 295 | "portions": 6, 296 | "totalTimeInMinutes": 95, 297 | "ingredients": [ 298 | { 299 | "ingredient": "eggs", 300 | "quantity": 4, 301 | "measurement": null, 302 | "modifier": null 303 | }, 304 | { 305 | "ingredient": "whole chilis", 306 | "quantity": 3, 307 | "measurement": "seven ounce cans", 308 | "modifier": "split" 309 | }, 310 | { 311 | "ingredient": "cheddar", 312 | "quantity": 4, 313 | "measurement": "cups", 314 | "modifier": "shredded" 315 | }, 316 | { 317 | "ingredient": "monterey jack", 318 | "quantity": 4, 319 | "measurement": "cups", 320 | "modifier": "shredded" 321 | }, 322 | { 323 | "ingredient": "milk", 324 | "quantity": 1.5, 325 | "measurement": "cups", 326 | "modifier": null 327 | }, 328 | { 329 | "ingredient": "pepper", 330 | "quantity": 0.5, 331 | "measurement": "teaspoon", 332 | "modifier": "black" 333 | }, 334 | { 335 | "ingredient": "salt", 336 | "quantity": 0.25, 337 | "measurement": "teaspoon", 338 | "modifier": "kosher" 339 | }, 340 | { 341 | "ingredient": "flour", 342 | "quantity": 2, 343 | "measurement": "tablespoon", 344 | "modifier": "all purpose" 345 | } 346 | ], 347 | "instructions": "Lightly grease 9x13-inch glass baking dish. \n Beat first 5 ingredients in medium bowl to blend. \n Arrange chilies from 1 can in prepared dish, covering bottom completely. \n Sprinkle with 1/3 of each cheese. \n Repeat layering twice. \n Pour egg mixture over cheese. \n Let stand 30 minutes. \n ( \n Can be prepared 1 day ahead. \n Cover and refrigerate. \n ) \n Preheat oven to 350°F. \n Bake until casserole is slightly puffed in center and golden brown on edges, about 45 minutes. \n Cool 20 minutes and serve. \n " 348 | } 349 | ] 350 | } -------------------------------------------------------------------------------- /db.json.bak: -------------------------------------------------------------------------------- 1 | { 2 | "recipes": [ 3 | { 4 | "_id": "781493c4-0b32-4186-aaa0-b7c6cb4b0c49", 5 | "saved": true, 6 | "title": "Stuffed Chard Leaves", 7 | "portions": 6, 8 | "totalTimeInMinutes": 60, 9 | "ingredients": [ 10 | { 11 | "ingredient": "onion", 12 | "quantity": 1, 13 | "measurement": null, 14 | "modifier": "chopped" 15 | }, 16 | { 17 | "ingredient": "oil", 18 | "quantity": 1, 19 | "measurement": "tablespoon", 20 | "modifier": null 21 | }, 22 | { 23 | "ingredient": "brown rice", 24 | "quantity": 2.5, 25 | "measurement": "cups", 26 | "modifier": "cooked" 27 | }, 28 | { 29 | "ingredient": "cottage cheese", 30 | "quantity": 1.5, 31 | "measurement": "cups", 32 | "modifier": null 33 | }, 34 | { 35 | "ingredient": "egg", 36 | "quantity": 1, 37 | "measurement": null, 38 | "modifier": "beaten" 39 | }, 40 | { 41 | "ingredient": "parsley", 42 | "quantity": 0.5, 43 | "measurement": "cup", 44 | "modifier": "chopped" 45 | }, 46 | { 47 | "ingredient": "raisins", 48 | "quantity": 0.75, 49 | "measurement": "cup", 50 | "modifier": null 51 | }, 52 | { 53 | "ingredient": "dill", 54 | "quantity": 1, 55 | "measurement": "teaspoon", 56 | "modifier": null 57 | }, 58 | { 59 | "ingredient": "salt", 60 | "quantity": 0.75, 61 | "measurement": "teaspoon", 62 | "modifier": null 63 | }, 64 | { 65 | "ingredient": "swiss chard leaves", 66 | "quantity": 16, 67 | "measurement": "leaves", 68 | "modifier": "large" 69 | } 70 | ], 71 | "instructions": "Preheat oven to 350°F. \n Saute onion in oil. Mix all ingredients except chard. \n Wash and dry chard leaves and remove stems, including the fat part of the rib if it extends rigidly up into the leaf (select leaves that are not too 'ribby'). Place 2 tablespoons or more of filling on the underside of the leaf, a third of the way from the bottom. Fold over the sides of the leaf and roll up into a square packet. Place seam-side down in a greased casserole. Cover and bake for about 30 minutes. \n Alternatively, steam the rolls in a steamer basket over boiling water until the leaves are tender, about 20 minutes. Bake any extra filling and serve with stuffed leaves." 72 | }, 73 | { 74 | "_id": "70dd964b-6225-4d7c-8b1e-7e983d901a80", 75 | "saved": true, 76 | "title": "Helen's Polenta with Eggplant", 77 | "portions": 6, 78 | "totalTimeInMinutes": 120, 79 | "ingredients": [ 80 | { 81 | "ingredient": "onion", 82 | "quantity": 1, 83 | "measurement": null, 84 | "modifier": "chopped fine" 85 | }, 86 | { 87 | "ingredient": "green pepper", 88 | "quantity": 1, 89 | "measurement": null, 90 | "modifier": "chopped fine" 91 | }, 92 | { 93 | "ingredient": "garlic clove", 94 | "quantity": 1, 95 | "measurement": null, 96 | "modifier": null 97 | }, 98 | { 99 | "ingredient": "olive oil", 100 | "quantity": 1, 101 | "measurement": "tablespoon", 102 | "modifier": null 103 | }, 104 | { 105 | "ingredient": "tomato", 106 | "quantity": 3, 107 | "measurement": "cups", 108 | "modifier": "chopped" 109 | }, 110 | { 111 | "ingredient": "parsley", 112 | "quantity": 0.25, 113 | "measurement": "cup", 114 | "modifier": "chopped" 115 | }, 116 | { 117 | "ingredient": "basil", 118 | "quantity": 1, 119 | "measurement": "teaspoon", 120 | "modifier": "dried" 121 | }, 122 | { 123 | "ingredient": "eggplant", 124 | "quantity": 1.5, 125 | "measurement": "pounds", 126 | "modifier": null 127 | }, 128 | { 129 | "ingredient": "mozzarella cheese", 130 | "quantity": 0.75, 131 | "measurement": "cup", 132 | "modifier": "grated" 133 | } 134 | ], 135 | "instructions": "Place polenta in top of a double boiler with 4 cups of boiling water and 1/2 teaspoon of the salt. Bring to a boil, reduce heat to low, and cook for 30 to 40 minutes, until mush is quite thick. Pack into round, straight sided containers that are, ideally, the same diameter as the eggplants. Refrigerate. \n Meanwhile, saute onion, pepper and garlic clove in oil until tender. Crush garlic with a fork. Then add Tomatoes, parsley, basil, and remaining 1 teaspoon of salt. Bring to a boil and simmer, stirring often, for 15 minutes, breaking up tomatoes as you stir. \n When polenta is chilled, slice it in 1/2 rounds. Do the same with the eggplants. Oil a 9 x 13 inch baking dish and overlap alternating slices of eggplant and polenta in a pretty, fish scale design. Or, if the eggplant is too big, layer it lasagna style. Pour tomato sauce over the whole works and sprinkle cheese on top. Cover the dish and bake in a 350°F oven for 45 minutes, or until eggplant tests done with a fork." 136 | }, 137 | { 138 | "_id": "48690fc9-466b-4285-81ea-eeeda7411d92", 139 | "saved": true, 140 | "title": "Guacamole", 141 | "portions": 4, 142 | "totalTimeinMinutes": 10, 143 | "ingredients": [ 144 | { 145 | "ingredient": "avocados", 146 | "quantity": 2, 147 | "measurement": null, 148 | "modifier": "halved, peeled, pitted, and chopped" 149 | }, 150 | { 151 | "ingredient": "lime juice", 152 | "quantity": 2, 153 | "measurement": "tablespoon", 154 | "modifier": "fresh" 155 | }, 156 | { 157 | "ingredient": "fresh cilantro", 158 | "quantity": 2, 159 | "measurement": "tablespoon", 160 | "modifier": "chopped" 161 | }, 162 | { 163 | "ingredient": "salt", 164 | "quantity": null, 165 | "measurement": null, 166 | "modifier": "pinch" 167 | } 168 | ], 169 | "instructions": "Mash avocado, lime juice, and a pinch of salt in a mortar and pestle or a medium bowl with a fork until thick and smooth. Mix in 1/4 cup water 1 tablespoonful at a time until mixture is creamy and smooth. Stir in cilantro. Season with salt." 170 | }, 171 | { 172 | "_id": "00f5ac76-1709-4b17-bd99-02a780047855", 173 | "saved": true, 174 | "title": "Roast Chicken", 175 | "portions": 4, 176 | "totalTimeInMinutes": 130, 177 | "ingredients": [ 178 | { 179 | "ingredient": "chicken", 180 | "quantity": 4, 181 | "measurement": "lbs", 182 | "modifier": "whole" 183 | }, 184 | { 185 | "ingredient": "unsalted butter", 186 | "quantity": 0.25, 187 | "measurement": "cup", 188 | "modifier": "melted" 189 | }, 190 | { 191 | "ingredient": "salt", 192 | "quantity": 1, 193 | "measurement": "tablespoon", 194 | "modifier": "kosher" 195 | } 196 | ], 197 | "instructions": "Rub or pat salt onto breast, legs, and thighs of chicken. \n Place chicken in a large resealable plastic bag. \n Set open bag in a large bowl, keeping chicken breast side up. \n Chill for at least 8 hours and up to 2 days.Arrange a rack in upper third of oven; \n preheat to 500°F. \n Set a wire rack in a large heavy roasting pan. \n Remove chicken from bag. \n Pat dry with paper towels (do not rinse) Place chicken, breast side up, on prepared rack. \n Loosely tie legs together with kitchen twine and tuck wing tips under. \n Brush chicken all over with some of the butter. \n Pour 1 cup water into pan. \n Roast chicken, brushing with butter after 15 minutes, until skin is light golden brown and taut, about 30 minutes. \n Reduce oven temperature to 350°F. \n Remove chicken from oven and brush with more butter. \n Let rest for 15-20 minutes. \n Return chicken to oven; \n roast, basting with butter every 10 minutes, until skin is golden brown and a thermometer inserted into the thickest part of the thigh registers 165°F, 40-45 minutes. \n Let rest for 20 minutes. \n Carve and serve with pan juices. \n " 198 | }, 199 | { 200 | "_id": "09d7b4d4-0411-4ab5-b139-c964c153d6d3", 201 | "saved": true, 202 | "title": "Persian Rice", 203 | "portions": 6, 204 | "totalTimeInMinutes": 60, 205 | "ingredients": [ 206 | { 207 | "ingredient": "basmati rice", 208 | "quantity": 2, 209 | "measurement": "cups", 210 | "modifier": null 211 | }, 212 | { 213 | "ingredient": "yogurt", 214 | "quantity": 2, 215 | "measurement": "cups", 216 | "modifier": "whole milk plain" 217 | }, 218 | { 219 | "ingredient": "butter", 220 | "quantity": 3, 221 | "measurement": "tablespoons", 222 | "modifier": "unsalted" 223 | }, 224 | { 225 | "ingredient": "saffron threads", 226 | "quantity": 1, 227 | "measurement": null, 228 | "modifier": "pinch" 229 | }, 230 | { 231 | "ingredient": "salt", 232 | "quantity": "3", 233 | "measurement": "teaspoons", 234 | "modifiers": "kosher divided" 235 | } 236 | ], 237 | "instructions": "Place rice in a medium saucepan; \n add 2 teaspoons salt and cold water to cover by 2. \n Bring to a boil over medium heat; \n reduce heat to low and simmer for 5 minutes. \n Drain rice, reserving 3/4 cup cooking liquid. \n Place saffron and 1/2 cup reserved cooking liquid in a small bowl; \n let saffron soften for 5 minutes. \n Place yogurt in a medium bowl and stir in remaining 1 teaspoon salt and saffron water. \n Add rice and stir to coat. \n Melt butter in a large deep nonstick skillet over medium heat; \n swirl to coat bottom and sides of pan. n Add rice, mounding slightly in center. n Poke 6-7 holes in rice with the end of a wooden spoon. n Cover with foil, then a lid. n Cook, rotating skillet over burner for even cooking, for 10 minutes (do not stir). n Reduce heat to low; cook, adding more reserved cooking liquid by tablespoonfuls if rice has not finished cooking when water evaporates, until a golden brown crust forms on bottom of rice, 20-25 minutes. \n Remove lid and foil; \n invert a plate over skillet. n Using oven mitts, carefully invert rice onto plate; \n use a heatproof spatula to remove any crust remaining in skillet. \n" 238 | }, 239 | { 240 | "_id": "6d0bd7a5-0623-4819-a479-2763d0728a03", 241 | "saved": true, 242 | "title": "Roasted Beets with Cumin and Mint", 243 | "portions": 6, 244 | "totalTimeInMinutes": 105, 245 | "ingredients": [ 246 | { 247 | "ingredient": "beets", 248 | "quantity": 3, 249 | "measurement": null, 250 | "modifier": "medium" 251 | }, 252 | { 253 | "ingredient": "cumin seeds", 254 | "quantity": 1, 255 | "measurement": "teaspoon", 256 | "modifier": "toasted and slightly cracked" 257 | }, 258 | { 259 | "ingredient": "lemon juice", 260 | "quantity": 1, 261 | "measurement": "tablespoon", 262 | "modifier": null 263 | }, 264 | { 265 | "ingredient": "salt", 266 | "quantity": 0.5, 267 | "measurement": "teaspoon", 268 | "modifier": null 269 | }, 270 | { 271 | "ingredient": "pepper", 272 | "quantity": 0.25, 273 | "measurement": "teaspoon", 274 | "modifier": "black" 275 | }, 276 | { 277 | "ingredient": "olive oil", 278 | "quantity": 2, 279 | "measurement": "tablespoon", 280 | "modifier": "Extra Virgin" 281 | }, 282 | { 283 | "ingredient": "mint", 284 | "quantity": 0.66, 285 | "measurement": "cup", 286 | "modifier": "fresh, coarsely chopped" 287 | } 288 | ], 289 | "instructions": "Stir together lemon juice, cumin seeds, salt, and pepper in a medium bowl. \n Stir in oil and let stand while roasting beets. \n Put oven rack in middle position and preheat oven to 425°F. \n Tightly wrap beets in a double layer of foil and roast on a baking sheet until tender, 1 to 1 1/4 hours. \n Cool to warm in foil package, about 20 minutes. \n When beets are cool enough to handle, peel them, discarding stems and root ends, then cut into 1/2-inch-wide wedges. \n Toss warm beets with dressing. \n Stir in mint just before serving. \n cooks' note: \n Beets can be roasted and tossed with dressing 4 hours ahead, then kept, covered, at room temperature. \n" 290 | }, 291 | { 292 | "_id": "17126c4a-11d6-4c7b-a4ac-03e98cf6333f", 293 | "saved": true, 294 | "title": "Chili Relleno Casserole", 295 | "portions": 6, 296 | "totalTimeInMinutes": 95, 297 | "ingredients": [ 298 | { 299 | "ingredient": "eggs", 300 | "quantity": 4, 301 | "measurement": null, 302 | "modifier": null 303 | }, 304 | { 305 | "ingredient": "whole chilis", 306 | "quantity": 3, 307 | "measurement": "seven ounce cans", 308 | "modifier": "split" 309 | }, 310 | { 311 | "ingredient": "cheddar", 312 | "quantity": 4, 313 | "measurement": "cups", 314 | "modifier": "shredded" 315 | }, 316 | { 317 | "ingredient": "monterey jack", 318 | "quantity": 4, 319 | "measurement": "cups", 320 | "modifier": "shredded" 321 | }, 322 | { 323 | "ingredient": "milk", 324 | "quantity": 1.5, 325 | "measurement": "cups", 326 | "modifier": null 327 | }, 328 | { 329 | "ingredient": "pepper", 330 | "quantity": 0.5, 331 | "measurement": "teaspoon", 332 | "modifier": "black" 333 | }, 334 | { 335 | "ingredient": "salt", 336 | "quantity": 0.25, 337 | "measurement": "teaspoon", 338 | "modifier": "kosher" 339 | }, 340 | { 341 | "ingredient": "flour", 342 | "quantity": 2, 343 | "measurement": "tablespoon", 344 | "modifier": "all purpose" 345 | } 346 | ], 347 | "instructions": "Lightly grease 9x13-inch glass baking dish. \n Beat first 5 ingredients in medium bowl to blend. \n Arrange chilies from 1 can in prepared dish, covering bottom completely. \n Sprinkle with 1/3 of each cheese. \n Repeat layering twice. \n Pour egg mixture over cheese. \n Let stand 30 minutes. \n ( \n Can be prepared 1 day ahead. \n Cover and refrigerate. \n ) \n Preheat oven to 350°F. \n Bake until casserole is slightly puffed in center and golden brown on edges, about 45 minutes. \n Cool 20 minutes and serve. \n " 348 | } 349 | ] 350 | } -------------------------------------------------------------------------------- /db/db.js: -------------------------------------------------------------------------------- 1 | var low = require('lowdb'); 2 | var db = low('db.json'); 3 | 4 | module.exports = { 5 | createRecipe: function (recipe) { 6 | return db('recipes').push(recipe); 7 | }, 8 | deleteRecipe: function (id) { 9 | return db('recipes').remove({_id: id}); 10 | }, 11 | getRecipe: function (id) { 12 | return db('recipes').find({ _id: id}); 13 | }, 14 | getRecipes: function () { 15 | return db('recipes'); 16 | }, 17 | updateRecipe: function (recipe) { 18 | return db('recipes') 19 | .chain() 20 | .find({ _id: recipe._id }) 21 | .assign({ingredients: recipe.ingredients}); 22 | } 23 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Gulpfile 2 | var fs = require("fs"); 3 | // var _ = require("lodash"); 4 | var gulp = require("gulp"); 5 | var gutil = require("gulp-util"); 6 | var jsxcs = require("gulp-jsxcs"); 7 | var eslint = require("gulp-eslint"); 8 | var nodemon = require("gulp-nodemon"); 9 | var connect = require("gulp-connect"); 10 | var shell = require("gulp-shell"); 11 | var webpack = require("webpack"); 12 | var rimraf = require("gulp-rimraf"); 13 | 14 | var buildCfg = require("./webpack.config"); 15 | var buildDevCfg = require("./webpack.dev-config"); 16 | 17 | // ---------------------------------------------------------------------------- 18 | // Constants 19 | // ---------------------------------------------------------------------------- 20 | var FRONTEND_FILES = [ 21 | "client/**/*.{js,jsx}" 22 | ]; 23 | 24 | var BACKEND_FILES = [ 25 | "scripts/**/*.js", 26 | "server/**/*.js", 27 | "test/**/*.js", 28 | "*.js" 29 | ]; 30 | 31 | // ---------------------------------------------------------------------------- 32 | // Helpers 33 | // ---------------------------------------------------------------------------- 34 | // Strip comments from JsHint JSON files (naive). 35 | var _jsonCfg = function (name) { 36 | var raw = fs.readFileSync(name).toString(); 37 | return JSON.parse(raw.replace(/\/\/.*\n/g, "")); 38 | }; 39 | 40 | // ---------------------------------------------------------------------------- 41 | // EsLint 42 | // ---------------------------------------------------------------------------- 43 | gulp.task("eslint-frontend", function () { 44 | return gulp 45 | .src(FRONTEND_FILES) 46 | .pipe(eslint({ 47 | envs: [ 48 | "browser" 49 | ] 50 | })) 51 | .pipe(eslint.formatEach("stylish", process.stderr)) 52 | .pipe(eslint.failOnError()); 53 | }); 54 | 55 | gulp.task("eslint-backend", function () { 56 | return gulp 57 | .src(BACKEND_FILES) 58 | .pipe(eslint({ 59 | envs: [ 60 | "node" 61 | ] 62 | })) 63 | .pipe(eslint.formatEach("stylish", process.stderr)) 64 | .pipe(eslint.failOnError()); 65 | }); 66 | 67 | gulp.task("eslint", ["eslint-frontend", "eslint-backend"]); 68 | 69 | // ---------------------------------------------------------------------------- 70 | // JsCs 71 | // ---------------------------------------------------------------------------- 72 | gulp.task("jscs", function () { 73 | return gulp 74 | .src([].concat( 75 | FRONTEND_FILES, 76 | BACKEND_FILES 77 | )) 78 | .pipe(jsxcs(_jsonCfg(".jscsrc"))); 79 | }); 80 | 81 | // ---------------------------------------------------------------------------- 82 | // Quality 83 | // ---------------------------------------------------------------------------- 84 | gulp.task("check", ["jscs", "eslint"]); 85 | gulp.task("check:ci", ["jscs", "eslint"]); 86 | gulp.task("check:all", ["jscs", "eslint"]); 87 | 88 | // ---------------------------------------------------------------------------- 89 | // Cleaning 90 | // ---------------------------------------------------------------------------- 91 | gulp.task("clean:all", function () { 92 | return gulp 93 | .src([ 94 | "app/css-dist", 95 | "app/js-dist" 96 | ], { read: false }) 97 | .pipe(rimraf()); 98 | }); 99 | 100 | gulp.task("clean:dist", function () { 101 | return gulp 102 | .src([ 103 | "app/css-dist", 104 | "app/js-dist" 105 | ], { read: false }) 106 | .pipe(rimraf()); 107 | }); 108 | 109 | gulp.task("build:dev", function (done) { 110 | webpack(buildDevCfg).run(function (err, stats) { 111 | if (err) { throw new gutil.PluginError("webpack", err); } 112 | 113 | gutil.log("[webpack]", stats.toString({ 114 | hash: true, 115 | colors: true, 116 | cached: false 117 | })); 118 | 119 | done(); 120 | }); 121 | }); 122 | 123 | gulp.task("watch:dev", function () { 124 | gulp.watch([ 125 | "client/**/*.{js,jsx}" 126 | ], ["build:dev"]); 127 | }); 128 | gulp.task("watch", ["watch:dev"]); 129 | 130 | // ---------------------------------------------------------------------------- 131 | // Production 132 | // ---------------------------------------------------------------------------- 133 | gulp.task("build:prod", function (done) { 134 | webpack(buildCfg).run(function (err, stats) { 135 | if (err) { throw new gutil.PluginError("webpack", err); } 136 | 137 | gutil.log("[webpack]", stats.toString({ 138 | hash: true, 139 | colors: true, 140 | cached: false 141 | })); 142 | 143 | done(); 144 | }); 145 | }); 146 | 147 | gulp.task("build:prod-full", ["clean:dist"], function () { 148 | return gulp.run("build:prod"); 149 | }); 150 | 151 | gulp.task("watch:prod", function () { 152 | gulp.watch([ 153 | "client/**/*.{js,jsx}" 154 | ], ["build:prod"]); 155 | }); 156 | 157 | // ---------------------------------------------------------------------------- 158 | // Servers 159 | // ---------------------------------------------------------------------------- 160 | // Dev. server 161 | gulp.task("server", function () { 162 | nodemon({ 163 | script: "server/index.js", 164 | ext: "js,jsx", 165 | watch: [ 166 | "server", 167 | "client" 168 | ] 169 | }); 170 | }); 171 | 172 | // Hot reload webpack server 173 | gulp.task("webpack-server", shell.task(["node ./hot/server"])); 174 | 175 | // Source maps server 176 | gulp.task("server:sources", function () { 177 | connect.server({ 178 | root: __dirname, 179 | port: 3001 180 | }); 181 | }); 182 | 183 | // ---------------------------------------------------------------------------- 184 | // Aggregations 185 | // ---------------------------------------------------------------------------- 186 | gulp.task("ls", ["build:ls", "watch:ls", "server:sources"]); 187 | gulp.task("dev", ["build:dev", "watch:dev", "server", "server:sources"]); 188 | gulp.task("hot", ["webpack-server"]); 189 | gulp.task("prod", ["build:prod", "watch:prod", "server", "server:sources"]); 190 | gulp.task("build", ["build:prod-full"]); 191 | gulp.task("default", ["build:dev", "check"]); 192 | -------------------------------------------------------------------------------- /hot/entry.js: -------------------------------------------------------------------------------- 1 | require("../styles/main.scss"); 2 | require("../client/app"); -------------------------------------------------------------------------------- /hot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Recipes 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /hot/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('../webpack.hot-config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | contentBase: config.contentBase, 8 | hot: true 9 | }).listen(3000, 'localhost', function (err, result) { 10 | if (err) { 11 | console.log(err); 12 | } 13 | 14 | console.log('Listening at localhost:3000'); 15 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recipes-flux", 3 | "version": "0.0.1", 4 | "description": "Recipes (Flux example)", 5 | "dependencies": { 6 | "biff": "0.1.0", 7 | "body-parser": "1.2.0", 8 | "compression": "1.2.0", 9 | "express": "4.2.0", 10 | "express-handlebars": "1.1.0", 11 | "imports-loader": "0.6.3", 12 | "jsx-loader": "0.12.0", 13 | "lb-ratio": "0.4.1", 14 | "lodash": "2.4.1", 15 | "lowdb": "^0.7.2", 16 | "markdown": "0.5.0", 17 | "node-jsx": "0.12.0", 18 | "node-sass": "2.0.0-beta", 19 | "ps-tree": "0.0.3", 20 | "react-router": "0.11.6", 21 | "superagent": "0.21.0", 22 | "uuid": "2.0.1", 23 | "vulgarities": "0.0.2", 24 | "webpack": "^1.5.1", 25 | "dropbox": "~0.10.3" 26 | }, 27 | "devDependencies": { 28 | "gulp": "3.8.7", 29 | "gulp-connect": "2.0.6", 30 | "gulp-eslint": "0.2.2", 31 | "gulp-jsxcs": "0.1.6", 32 | "gulp-nodemon": "1.0.4", 33 | "gulp-rimraf": "0.1.0", 34 | "gulp-util": "3.0.1", 35 | "react-hot-loader": "^1.1.1", 36 | "webpack-dev-server": "1.6.4", 37 | "gulp-shell": "~0.3.0", 38 | "sass-loader": "~0.4.0-beta.1", 39 | "style-loader": "~0.8.3", 40 | "css-loader": "~0.9.1" 41 | }, 42 | "scripts": { 43 | "test": "gulp build check", 44 | "build-js": "webpack --config webpack.config.js", 45 | "build-css": "rm -rf app/css-dist && mkdir -p app/css-dist && node-sass --output-style compressed styles/main.scss app/css-dist/bundle.css", 46 | "build": "npm run-script build-js && npm run-script build-css" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // Patch require 2 | require("node-jsx").install({ extension: ".jsx" }); 3 | 4 | // Server 5 | var path = require("path"); 6 | var express = require("express"); 7 | var compress = require("compression"); 8 | var exphbs = require("express-handlebars"); 9 | var bodyParser = require("body-parser"); 10 | 11 | // DB 12 | var db = require("../db/db"); 13 | 14 | var app = express(); 15 | var PORT = process.env.PORT || 3000; 16 | 17 | // ---------------------------------------------------------------------------- 18 | // Setup, Static Routes 19 | // ---------------------------------------------------------------------------- 20 | app.use(compress()); 21 | app.use(bodyParser()); 22 | app.engine(".hbs", exphbs({ extname: ".hbs" })); 23 | app.set("view engine", ".hbs"); 24 | app.set("views", path.join(__dirname, "../templates")); 25 | 26 | // ---------------------------------------------------------------------------- 27 | // Static Routes 28 | // ---------------------------------------------------------------------------- 29 | app.use("/app/js-dist/*.map", function (req, res) { 30 | res.send(404, "404"); // Prevent sourcemap serving. 31 | }); 32 | app.use("/app/js-dist", express.static("app/js-dist")); 33 | app.use("/app/css-dist", express.static("app/css-dist")); 34 | 35 | // ---------------------------------------------------------------------------- 36 | // API 37 | // ---------------------------------------------------------------------------- 38 | // TODO: Example wrapper. 39 | // var _errOrData = function (res, dataOverride) { 40 | // return function (err, data) { 41 | // if (err) { 42 | // return res.status(500).json({ error: err.message || err.toString() }); 43 | // } 44 | 45 | // res.json(dataOverride || data); 46 | // }; 47 | // }; 48 | 49 | // TODO: Old REST route using wrapper. 50 | // app["delete"]("/api/notes/:id", function (req, res) { 51 | // db.run("delete from notes where id=?", req.params.id, _errOrData(res, {})); 52 | // }); 53 | 54 | // ---------------------------------------------------------------------------- 55 | // Dynamic Routes 56 | // ---------------------------------------------------------------------------- 57 | app.get("/", function (req, res) { 58 | return res.render("index", {}); 59 | }); 60 | 61 | // ---------------------------------------------------------------------------- 62 | // Recipes crud 63 | // ---------------------------------------------------------------------------- 64 | 65 | app.get("/recipes", function (req, res) { 66 | return res.json(db.getRecipes()); 67 | }); 68 | 69 | app.get("/recipes/get/:id", function (req, res) { 70 | return res.json(db.getRecipe(req.params.id)); 71 | }); 72 | 73 | app.post("/recipes/create", function (req, res) { 74 | return res.json(db.createRecipe(req.body.recipe)); 75 | }); 76 | 77 | app.put("/recipes/update", function (req, res) { 78 | return res.json(db.updateRecipe(req.body.recipe)); 79 | }); 80 | 81 | app.delete("/recipes/delete", function (req, res) { 82 | return res.json(db.deleteRecipe(req.body._id)); 83 | }); 84 | 85 | // ---------------------------------------------------------------------------- 86 | // Start 87 | // ---------------------------------------------------------------------------- 88 | var start = function (opts, callback) { 89 | callback = callback || function () {}; 90 | opts = opts || {}; 91 | opts.port = opts.port || PORT; 92 | app.listen(opts.port, callback); 93 | }; 94 | 95 | module.exports = { 96 | start: start 97 | }; 98 | 99 | // Script. Use defaults (init dev. database). 100 | if (require.main === module) { 101 | start(); 102 | } 103 | -------------------------------------------------------------------------------- /server/mock-db.js: -------------------------------------------------------------------------------- 1 | // Mock Database of Recipes 2 | module.exports = [ 3 | { 4 | id: "781493c4-0b32-4186-aaa0-b7c6cb4b0c49", 5 | title: "Stuffed Chard Leaves", 6 | portions: 6, 7 | totalTimeInMinutes: 60, 8 | ingredients: [ 9 | { 10 | ingredient: "onion", 11 | quantity: 1, 12 | measurement: null, 13 | modifier: "chopped" 14 | }, 15 | { 16 | ingredient: "oil", 17 | quantity: 1, 18 | measurement: "tablespoon", 19 | modifier: null 20 | }, 21 | { 22 | ingredient: "brown rice", 23 | quantity: 2.5, 24 | measurement: "cups", 25 | modifier: "cooked" 26 | }, 27 | { 28 | ingredient: "cottage cheese", 29 | quantity: 1.5, 30 | measurement: "cups", 31 | modifier: null 32 | }, 33 | { 34 | ingredient: "egg", 35 | quantity: 1, 36 | measurement: null, 37 | modifier: "beaten" 38 | }, 39 | { 40 | ingredient: "parsley", 41 | quantity: 0.5, 42 | measurement: "cup", 43 | modifier: "chopped" 44 | }, 45 | { 46 | ingredient: "raisins", 47 | quantity: 0.75, 48 | measurement: "cup", 49 | modifier: null 50 | }, 51 | { 52 | ingredient: "dill", 53 | quantity: 1, 54 | measurement: "teaspoon", 55 | modifier: null 56 | }, 57 | { 58 | ingredient: "salt", 59 | quantity: 0.75, 60 | measurement: "teaspoon", 61 | modifier: null 62 | }, 63 | { 64 | ingredient: "swiss chard leaves", 65 | quantity: 16, 66 | measurement: "leaves", 67 | modifier: "large" 68 | } 69 | ], 70 | instructions: "Preheat oven to 350°F. \n Saute onion in oil. " + 71 | "Mix all ingredients except chard. \n Wash and dry chard leaves " + 72 | "and remove stems, including the fat part of the rib if it extends " + 73 | "rigidly up into the leaf (select leaves that are not too 'ribby'). " + 74 | "Place 2 tablespoons or more of filling on the underside of the " + 75 | "leaf, a third of the way from the bottom. Fold over the sides of " + 76 | "the leaf and roll up into a square packet. Place seam-side down " + 77 | "in a greased casserole. Cover and bake for about 30 minutes. \n " + 78 | "Alternatively, steam the rolls in a steamer basket over boiling " + 79 | "water until the leaves are tender, about 20 minutes. Bake any extra " + 80 | "filling and serve with stuffed leaves." 81 | }, 82 | { 83 | id: "70dd964b-6225-4d7c-8b1e-7e983d901a80", 84 | title: "Helen's Polenta with Eggplant", 85 | portions: 6, 86 | totalTimeInMinutes: 120, 87 | ingredients: [ 88 | { 89 | ingredient: "onion", 90 | quantity: 1, 91 | measurement: null, 92 | modifier: "chopped fine" 93 | }, 94 | { 95 | ingredient: "green pepper", 96 | quantity: 1, 97 | measurement: null, 98 | modifier: "chopped fine" 99 | }, 100 | { 101 | ingredient: "garlic clove", 102 | quantity: 1, 103 | measurement: null, 104 | modifier: null 105 | }, 106 | { 107 | ingredient: "olive oil", 108 | quantity: 1, 109 | measurement: "tablespoon", 110 | modifier: null 111 | }, 112 | { 113 | ingredient: "tomato", 114 | quantity: 3, 115 | measurement: "cups", 116 | modifier: "chopped" 117 | }, 118 | { 119 | ingredient: "parsley", 120 | quantity: 0.25, 121 | measurement: "cup", 122 | modifier: "chopped" 123 | }, 124 | { 125 | ingredient: "basil", 126 | quantity: 1, 127 | measurement: "teaspoon", 128 | modifier: "dried" 129 | }, 130 | { 131 | ingredient: "eggplant", 132 | quantity: 1.5, 133 | measurement: "pounds", 134 | modifier: null 135 | }, 136 | { 137 | ingredient: "mozzarella cheese", 138 | quantity: 0.75, 139 | measurement: "cup", 140 | modifier: "grated" 141 | } 142 | ], 143 | instructions: "Place polenta in top of a double boiler " + 144 | "with 4 cups of boiling water and 1/2 teaspoon of the salt. " + 145 | "Bring to a boil, reduce heat to low, and cook for 30 to 40 " + 146 | "minutes, until mush is quite thick. Pack into round, straight " + 147 | "sided containers that are, ideally, the same diameter as the " + 148 | "eggplants. Refrigerate. \n Meanwhile, saute onion, pepper and " + 149 | "garlic clove in oil until tender. Crush garlic with a fork. " + 150 | "Then add Tomatoes, parsley, basil, and remaining 1 teaspoon of " + 151 | "salt. Bring to a boil and simmer, stirring often, for 15 minutes, " + 152 | "breaking up tomatoes as you stir. \n When polenta is chilled, slice " + 153 | "it in 1/2 rounds. Do the same with the eggplants. Oil a 9 x 13 inch " + 154 | "baking dish and overlap alternating slices of eggplant and polenta " + 155 | "in a pretty, fish scale design. Or, if the eggplant is too big, layer " + 156 | "it lasagna style. Pour tomato sauce over the whole works and sprinkle " + 157 | "cheese on top. Cover the dish and bake in a 350°F oven for 45 minutes, " + 158 | "or until eggplant tests done with a fork." 159 | } 160 | ]; 161 | -------------------------------------------------------------------------------- /styles/_bootstrap_custom.scss: -------------------------------------------------------------------------------- 1 | /* VARIABLES & MIXINS */ 2 | @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/variables"; 3 | @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/mixins"; 4 | 5 | /* RESET & DEPENDENCIES */ 6 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/normalize"; 7 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/print"; 8 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/glyphicons"; 9 | 10 | /* CORE CSS */ 11 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/scaffolding"; 12 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/type"; 13 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/code"; 14 | @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/grid"; 15 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/tables"; 16 | @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/forms"; 17 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/buttons"; 18 | 19 | /* COMPONENTS */ 20 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/component-animations"; 21 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/dropdowns"; 22 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/button-groups"; 23 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/input-groups"; 24 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/navs"; 25 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/navbar"; 26 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/breadcrumbs"; 27 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/pagination"; 28 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/pager"; 29 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/labels"; 30 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/badges"; 31 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/jumbotron"; 32 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/thumbnails"; 33 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/alerts"; 34 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/progress-bars"; 35 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/media"; 36 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/list-group"; 37 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/panels"; 38 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/responsive-embed"; 39 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/wells"; 40 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/close"; 41 | 42 | /* JS COMPONENTS */ 43 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/modals"; 44 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/tooltip"; 45 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/popovers"; 46 | // @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/carousel"; 47 | 48 | /* UTILITIES */ 49 | @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/utilities"; 50 | @import "../../bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/responsive-utilities"; 51 | -------------------------------------------------------------------------------- /styles/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.1 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox. 29 | * Correct `block` display not defined for `main` in IE 11. 30 | */ 31 | 32 | article, 33 | aside, 34 | details, 35 | figcaption, 36 | figure, 37 | footer, 38 | header, 39 | hgroup, 40 | main, 41 | nav, 42 | section, 43 | summary { 44 | display: block; 45 | } 46 | 47 | /** 48 | * 1. Correct `inline-block` display not defined in IE 8/9. 49 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 50 | */ 51 | 52 | audio, 53 | canvas, 54 | progress, 55 | video { 56 | display: inline-block; /* 1 */ 57 | vertical-align: baseline; /* 2 */ 58 | } 59 | 60 | /** 61 | * Prevent modern browsers from displaying `audio` without controls. 62 | * Remove excess height in iOS 5 devices. 63 | */ 64 | 65 | audio:not([controls]) { 66 | display: none; 67 | height: 0; 68 | } 69 | 70 | /** 71 | * Address `[hidden]` styling not present in IE 8/9/10. 72 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 73 | */ 74 | 75 | [hidden], 76 | template { 77 | display: none; 78 | } 79 | 80 | /* Links 81 | ========================================================================== */ 82 | 83 | /** 84 | * Remove the gray background color from active links in IE 10. 85 | */ 86 | 87 | a { 88 | background: transparent; 89 | } 90 | 91 | /** 92 | * Improve readability when focused and also mouse hovered in all browsers. 93 | */ 94 | 95 | a:active, 96 | a:hover { 97 | outline: 0; 98 | } 99 | 100 | /* Text-level semantics 101 | ========================================================================== */ 102 | 103 | /** 104 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 105 | */ 106 | 107 | abbr[title] { 108 | border-bottom: 1px dotted; 109 | } 110 | 111 | /** 112 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 113 | */ 114 | 115 | b, 116 | strong { 117 | font-weight: bold; 118 | } 119 | 120 | /** 121 | * Address styling not present in Safari and Chrome. 122 | */ 123 | 124 | dfn { 125 | font-style: italic; 126 | } 127 | 128 | /** 129 | * Address variable `h1` font-size and margin within `section` and `article` 130 | * contexts in Firefox 4+, Safari, and Chrome. 131 | */ 132 | 133 | h1 { 134 | font-size: 2em; 135 | margin: 0.67em 0; 136 | } 137 | 138 | /** 139 | * Address styling not present in IE 8/9. 140 | */ 141 | 142 | mark { 143 | background: #ff0; 144 | color: #000; 145 | } 146 | 147 | /** 148 | * Address inconsistent and variable font size in all browsers. 149 | */ 150 | 151 | small { 152 | font-size: 80%; 153 | } 154 | 155 | /** 156 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 157 | */ 158 | 159 | sub, 160 | sup { 161 | font-size: 75%; 162 | line-height: 0; 163 | position: relative; 164 | vertical-align: baseline; 165 | } 166 | 167 | sup { 168 | top: -0.5em; 169 | } 170 | 171 | sub { 172 | bottom: -0.25em; 173 | } 174 | 175 | /* Embedded content 176 | ========================================================================== */ 177 | 178 | /** 179 | * Remove border when inside `a` element in IE 8/9/10. 180 | */ 181 | 182 | img { 183 | border: 0; 184 | } 185 | 186 | /** 187 | * Correct overflow not hidden in IE 9/10/11. 188 | */ 189 | 190 | svg:not(:root) { 191 | overflow: hidden; 192 | } 193 | 194 | /* Grouping content 195 | ========================================================================== */ 196 | 197 | /** 198 | * Address margin not present in IE 8/9 and Safari. 199 | */ 200 | 201 | figure { 202 | margin: 1em 40px; 203 | } 204 | 205 | /** 206 | * Address differences between Firefox and other browsers. 207 | */ 208 | 209 | hr { 210 | -moz-box-sizing: content-box; 211 | box-sizing: content-box; 212 | height: 0; 213 | } 214 | 215 | /** 216 | * Contain overflow in all browsers. 217 | */ 218 | 219 | pre { 220 | overflow: auto; 221 | } 222 | 223 | /** 224 | * Address odd `em`-unit font size rendering in all browsers. 225 | */ 226 | 227 | code, 228 | kbd, 229 | pre, 230 | samp { 231 | font-family: monospace, monospace; 232 | font-size: 1em; 233 | } 234 | 235 | /* Forms 236 | ========================================================================== */ 237 | 238 | /** 239 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 240 | * styling of `select`, unless a `border` property is set. 241 | */ 242 | 243 | /** 244 | * 1. Correct color not being inherited. 245 | * Known issue: affects color of disabled elements. 246 | * 2. Correct font properties not being inherited. 247 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 248 | */ 249 | 250 | button, 251 | input, 252 | optgroup, 253 | select, 254 | textarea { 255 | color: inherit; /* 1 */ 256 | font: inherit; /* 2 */ 257 | margin: 0; /* 3 */ 258 | } 259 | 260 | /** 261 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 262 | */ 263 | 264 | button { 265 | overflow: visible; 266 | } 267 | 268 | /** 269 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 270 | * All other form control elements do not inherit `text-transform` values. 271 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 272 | * Correct `select` style inheritance in Firefox. 273 | */ 274 | 275 | button, 276 | select { 277 | text-transform: none; 278 | } 279 | 280 | /** 281 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 282 | * and `video` controls. 283 | * 2. Correct inability to style clickable `input` types in iOS. 284 | * 3. Improve usability and consistency of cursor style between image-type 285 | * `input` and others. 286 | */ 287 | 288 | button, 289 | html input[type="button"], /* 1 */ 290 | input[type="reset"], 291 | input[type="submit"] { 292 | -webkit-appearance: button; /* 2 */ 293 | cursor: pointer; /* 3 */ 294 | } 295 | 296 | /** 297 | * Re-set default cursor for disabled elements. 298 | */ 299 | 300 | button[disabled], 301 | html input[disabled] { 302 | cursor: default; 303 | } 304 | 305 | /** 306 | * Remove inner padding and border in Firefox 4+. 307 | */ 308 | 309 | button::-moz-focus-inner, 310 | input::-moz-focus-inner { 311 | border: 0; 312 | padding: 0; 313 | } 314 | 315 | /** 316 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 317 | * the UA stylesheet. 318 | */ 319 | 320 | input { 321 | line-height: normal; 322 | } 323 | 324 | /** 325 | * It's recommended that you don't attempt to style these elements. 326 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 327 | * 328 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 329 | * 2. Remove excess padding in IE 8/9/10. 330 | */ 331 | 332 | input[type="checkbox"], 333 | input[type="radio"] { 334 | box-sizing: border-box; /* 1 */ 335 | padding: 0; /* 2 */ 336 | } 337 | 338 | /** 339 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 340 | * `font-size` values of the `input`, it causes the cursor style of the 341 | * decrement button to change from `default` to `text`. 342 | */ 343 | 344 | input[type="number"]::-webkit-inner-spin-button, 345 | input[type="number"]::-webkit-outer-spin-button { 346 | height: auto; 347 | } 348 | 349 | /** 350 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 351 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 352 | * (include `-moz` to future-proof). 353 | */ 354 | 355 | input[type="search"] { 356 | -webkit-appearance: textfield; /* 1 */ 357 | -moz-box-sizing: content-box; 358 | -webkit-box-sizing: content-box; /* 2 */ 359 | box-sizing: content-box; 360 | } 361 | 362 | /** 363 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 364 | * Safari (but not Chrome) clips the cancel button when the search input has 365 | * padding (and `textfield` appearance). 366 | */ 367 | 368 | input[type="search"]::-webkit-search-cancel-button, 369 | input[type="search"]::-webkit-search-decoration { 370 | -webkit-appearance: none; 371 | } 372 | 373 | /** 374 | * Define consistent border, margin, and padding. 375 | */ 376 | 377 | fieldset { 378 | border: 1px solid #c0c0c0; 379 | margin: 0 2px; 380 | padding: 0.35em 0.625em 0.75em; 381 | } 382 | 383 | /** 384 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 385 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 386 | */ 387 | 388 | legend { 389 | border: 0; /* 1 */ 390 | padding: 0; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove default vertical scrollbar in IE 8/9/10/11. 395 | */ 396 | 397 | textarea { 398 | overflow: auto; 399 | } 400 | 401 | /** 402 | * Don't inherit the `font-weight` (applied by a rule above). 403 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 404 | */ 405 | 406 | optgroup { 407 | font-weight: bold; 408 | } 409 | 410 | /* Tables 411 | ========================================================================== */ 412 | 413 | /** 414 | * Remove most spacing between table cells. 415 | */ 416 | 417 | table { 418 | border-collapse: collapse; 419 | border-spacing: 0; 420 | } 421 | 422 | td, 423 | th { 424 | padding: 0; 425 | } 426 | -------------------------------------------------------------------------------- /styles/base/_base.scss: -------------------------------------------------------------------------------- 1 | /* BASE */ 2 | html { 3 | box-sizing: border-box; 4 | } 5 | 6 | // Necessary for footer 7 | html, 8 | body { 9 | height: 100%; 10 | // html and body elements cannot have any padding or margin 11 | } 12 | 13 | *, *:before, *:after { 14 | box-sizing: inherit; 15 | } 16 | 17 | body { 18 | font-size: 16px; 19 | font-family: $serif-font; 20 | text-rendering: optimizeLegibility; 21 | } 22 | 23 | h1, h2, h3, h4, h5, h6, p, ul, ol { 24 | margin-top: 0; 25 | 26 | font-family: inherit; 27 | } 28 | 29 | // Make this responsive 30 | .epicureContainer { 31 | margin: 20px; 32 | } 33 | -------------------------------------------------------------------------------- /styles/base/_content.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/recipes-flux/8995383b15258bb504cb90fe10ee9337bf5738f7/styles/base/_content.scss -------------------------------------------------------------------------------- /styles/base/_variables.scss: -------------------------------------------------------------------------------- 1 | /* FONTS */ 2 | $serif-font: "Crimson Text", "Hoefler Text", "Baskerville", "Garamond", "Cambria", Georgia, serif; 3 | -------------------------------------------------------------------------------- /styles/docs.md: -------------------------------------------------------------------------------- 1 | # Epicure Style Guide 2 | 3 | ## Modular CSS 4 | 5 | ### Base 6 | 7 | * `/base` styles are the foundation of the site, which include: 8 | ** `_base` - styles are applied directly to element using an element selector 9 | ** `_utilities` - not reflective of application state 10 | ** `_variables` - site-wide variables such as fonts, colors, widths, etc. 11 | ** `_content` - universal text and content styles reside here 12 | 13 | ### Layout 14 | 15 | * `/layout` determine how sections of the page are structured. 16 | 17 | ### Modules 18 | 19 | * `/modules` contain discrete components of the page, such as navigation, alert dialogs, buttons, etc. Any new feature or component will be added to this section. 20 | 21 | ### States 22 | 23 | * `/states` augment and override all other styles, such as whether an element is expanded or collapsed, or if the element is in an error or active state. 24 | 25 | #### Distinguishing states and modifiers 26 | 27 | A state should be prefixed with `.is-` and reflects a state on the component itself (`.is-active`, `.is-expanded`). 28 | 29 | A modifier should be prefixed with `.has-` and generally reflects a state on the child of a component usually the existence of a modifier is kind of circumstantial. The child element has its own state that for styling purposes requires additional styles on the parent. E.g. `.has-expanded-sidebar` 30 | 31 | ## SUIT-flavored BEM 32 | 33 | BEM, meaning _block, element, modifier_, provides meaningful and easy to parse naming conventions that make your CSS easy to understand. It helps you write more maintainable CSS to think in those terms as well. 34 | 35 | [SUIT-flavored BEM](http://nicolasgallagher.com/about-html-semantics-front-end-architecture/) is just a slightly nicer looking version of BEM, as used by Nicolas Gallagher's (creator of Normalize.css) [SUIT framework](https://github.com/suitcss/suit). It looks like this: 36 | 37 | ```scss 38 | /* Utility */ 39 | .u-utilityName {} 40 | 41 | /* Component */ 42 | .ComponentName {} 43 | 44 | /* Component modifier */ 45 | .ComponentName--modifierName {} 46 | 47 | /* Component descendant */ 48 | .ComponentName-descendant {} 49 | 50 | /* Component descendant modifier */ 51 | .ComponentName-descendant--modifierName {} 52 | 53 | /* Component state (scoped to component) */ 54 | .ComponentName.is-stateOfComponent {} 55 | ``` 56 | 57 | Note the camelCasing! It looks crazy at first, but it's really pretty pleasant. (It also maps really well to Components/Views). 58 | 59 | The resulting HTML would look like this: 60 | 61 | ```html 62 |
63 |

64 |
65 | 66 |
67 |

68 |
69 | ``` 70 | -------------------------------------------------------------------------------- /styles/layout/_grid.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/recipes-flux/8995383b15258bb504cb90fe10ee9337bf5738f7/styles/layout/_grid.scss -------------------------------------------------------------------------------- /styles/main.scss: -------------------------------------------------------------------------------- 1 | /* EPICURE STYLE GUIDE */ 2 | @import 'normalize'; 3 | 4 | /* BASE */ 5 | @import 'base/variables'; 6 | @import 'base/base'; 7 | // @import 'base/content'; 8 | // @import 'base/mixins'; 9 | 10 | /* LAYOUT */ 11 | @import 'layout/grid'; 12 | 13 | /* MODULES */ 14 | // @import 'modules/buttons'; 15 | // @import 'modules/footer'; 16 | // @import 'modules/forms'; 17 | // @import 'modules/lists'; 18 | // @import 'modules/navigation'; 19 | // @import 'modules/notifications'; 20 | @import 'modules/_recipeDetails'; 21 | @import 'modules/_nav'; 22 | @import 'modules/_recipes'; 23 | 24 | /* STATES */ 25 | // @import 'states/states'; 26 | 27 | /* UTILITIES */ 28 | // @import 'base/utilities'; 29 | 30 | /* BOOTSTRAP */ 31 | // TODO: REMOVE? 32 | // @import '_bootstrap_custom.scss'; 33 | 34 | /* FONT AWESOME */ 35 | // TODO: REMOVE? 36 | // @import "../../bower_components/font-awesome/scss/variables"; 37 | // @import "../../bower_components/font-awesome/scss/mixins"; 38 | // @import "../../bower_components/font-awesome/scss/path"; 39 | // @import "../../bower_components/font-awesome/scss/core"; 40 | // @import "../../bower_components/font-awesome/scss/extras"; 41 | // @import "../../bower_components/font-awesome/scss/icons"; 42 | -------------------------------------------------------------------------------- /styles/modules/_nav.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/recipes-flux/8995383b15258bb504cb90fe10ee9337bf5738f7/styles/modules/_nav.scss -------------------------------------------------------------------------------- /styles/modules/_recipeDetails.scss: -------------------------------------------------------------------------------- 1 | /* RECIPE DETAILS */ 2 | .Recipe-title { 3 | margin-top: 10px; 4 | margin-bottom: 0; 5 | 6 | color: black; 7 | font-size: 24px; 8 | } 9 | 10 | /* INGREDIENTS */ 11 | .Recipe-ingredientLeft { 12 | margin-bottom: 0; 13 | 14 | font-weight: 700; 15 | text-align: right; 16 | } 17 | 18 | .Recipe-ingredientRight { 19 | margin-bottom: 0; 20 | } 21 | -------------------------------------------------------------------------------- /styles/modules/_recipes.scss: -------------------------------------------------------------------------------- 1 | /* RECIPES */ 2 | .Recipes-title { 3 | margin: 10px 0; 4 | 5 | color: black; 6 | font-size: 24px; 7 | } 8 | -------------------------------------------------------------------------------- /templates/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Recipes 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Webpack configuration 2 | var path = require("path"); 3 | var webpack = require("webpack"); 4 | 5 | module.exports = { 6 | cache: true, 7 | context: path.join(__dirname, "client"), 8 | entry: "./app.js", 9 | output: { 10 | path: path.join(__dirname, "app/js-dist"), 11 | filename: "bundle.js" 12 | }, 13 | module: { 14 | loaders: [ 15 | { test: /\.jsx$/, loader: "jsx-loader" } 16 | ] 17 | }, 18 | resolve: { 19 | extensions: ["", ".js", ".jsx"] 20 | }, 21 | plugins: [ 22 | // Optimize 23 | new webpack.optimize.DedupePlugin(), 24 | new webpack.optimize.UglifyJsPlugin(), 25 | new webpack.DefinePlugin({ 26 | "process.env": { 27 | // Signal production mode for React JS libs. 28 | NODE_ENV: JSON.stringify("production") 29 | } 30 | }), 31 | // Manually do source maps to use alternate host. 32 | new webpack.SourceMapDevToolPlugin( 33 | "bundle.js.map", 34 | "\n//# sourceMappingURL=http://127.0.0.1:3001/app/js-dist/[url]") 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /webpack.dev-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack configuration 3 | */ 4 | 5 | var _ = require("lodash"); 6 | var webpack = require("webpack"); 7 | var prodConfig = require("./webpack.config"); 8 | 9 | module.exports = _.extend({}, prodConfig, { 10 | plugins: [ 11 | // Manually do source maps to use alternate host. 12 | new webpack.SourceMapDevToolPlugin( 13 | "bundle.js.map", 14 | "\n//# sourceMappingURL=http://127.0.0.1:3001/app/js-dist/[url]") 15 | ] 16 | }); 17 | -------------------------------------------------------------------------------- /webpack.hot-config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var path = require("path"); 3 | 4 | module.exports = { 5 | devtool: "eval", 6 | cache: true, 7 | entry: [ 8 | "webpack-dev-server/client?http://localhost:3000", 9 | "webpack/hot/only-dev-server", 10 | "./hot/entry" 11 | ], 12 | contentBase: path.join(__dirname, "/hot"), 13 | output: { 14 | path: path.join(__dirname, "/app/"), 15 | filename: "bundle.js", 16 | publicPath: "/app/js-dist/" 17 | }, 18 | plugins: [ 19 | new webpack.HotModuleReplacementPlugin(), 20 | new webpack.NoErrorsPlugin() 21 | ], 22 | resolve: { 23 | extensions: ["", ".js", ".jsx"] 24 | }, 25 | module: { 26 | loaders: [ 27 | { test: /\.jsx$/, loaders: ["react-hot-loader", "jsx-loader?harmony"] }, 28 | { test: /\.scss$/, loader: "style!css!sass?outputStyle=expanded" } 29 | ] 30 | } 31 | }; 32 | --------------------------------------------------------------------------------