├── .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 | [](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 |
27 |
28 |
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 |
57 | );
58 |
59 | ingredients.push(button);
60 |
61 | return (
62 |
63 | {ingredients}
64 |
65 | );
66 | }
67 | });
68 |
69 | module.exports = IngredientForm;
70 |
--------------------------------------------------------------------------------
/client/components/ingredient.jsx:
--------------------------------------------------------------------------------
1 | // React
2 | var React = require("react");
3 |
4 | // Create fractions from decimals
5 | // https://www.npmjs.com/package/lb-ratio
6 | var ratio = require("lb-ratio");
7 | var characterFor = require("vulgarities/charFor");
8 |
9 | // Router
10 | var Router = require("react-router");
11 | var RouteHandler = Router.RouteHandler;
12 |
13 | function toUnicode(fractionString) {
14 | var f = fractionString.split("/");
15 | var uniChar = characterFor(f[0], f[1]);
16 | if (uniChar) {
17 | return uniChar;
18 | }
19 | return fractionString;
20 | }
21 |
22 | // Component
23 | var Ingredient = React.createClass({
24 | displayName: "Ingredient",
25 | propTypes: {},
26 | mixins: [],
27 |
28 | getInitialState: function () { return {}; },
29 |
30 | componentWillMount: function () {},
31 |
32 | componentWillUnmount: function () {},
33 |
34 | render: function () {
35 | // TODO: Move to separate function
36 | // Decimal to fraction
37 | var q = this.props.ingredient.quantity;
38 | var fraction;
39 |
40 | if (!isNaN(this.props.multiplier)) {
41 | q = q * this.props.multiplier;
42 | }
43 |
44 | if (!isNaN(q)) {
45 | if (q < 1) {
46 | var uni = toUnicode(ratio.parse(q).simplify().toString());
47 | fraction = uni;
48 | } else if (q % 1 !== 0) {
49 | var frac = Math.floor(q) + " " + toUnicode(ratio.parse(q % 1).simplify().toString());
50 | fraction = frac;
51 | } else {
52 | // Whole number
53 | fraction = q;
54 | }
55 | }
56 | return (
57 |
58 |
59 |
60 | {this.props.ingredient.ingredient}
61 |
62 |
63 |
64 |
65 | {fraction} {this.props.ingredient.measurement} {this.props.ingredient.modifier}
66 |
67 |
68 |
69 |
70 | );
71 | }
72 | });
73 |
74 | module.exports = Ingredient;
75 |
--------------------------------------------------------------------------------
/client/components/input.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 | handleInputChange: function () {
15 | // Proxy to parent moving to generalize input
16 | this.props.inputCallback({
17 | _id: this.props._id,
18 | accessor: this.props.accessor,
19 | index: this.props.index,
20 | value: this.refs.inputValue.getDOMNode().value
21 | });
22 | },
23 |
24 | componentWillUnmount: function () {},
25 |
26 | render: function () {
27 | return (
28 |
35 | );
36 | }
37 | });
38 |
39 | module.exports = Input;
40 |
--------------------------------------------------------------------------------
/client/components/nav.jsx:
--------------------------------------------------------------------------------
1 | // React
2 | var React = require("react");
3 |
4 | // Router
5 | var Router = require("react-router");
6 | var Link = Router.Link;
7 |
8 | // Component
9 | var Nav = React.createClass({
10 | displayName: "Nav",
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 | Home
24 | Recipes
25 | New Recipe
26 |
27 | );
28 | }
29 | });
30 |
31 | module.exports = Nav;
32 |
33 | // Note {...this.props}, see:
34 | // http://facebook.github.io/react/docs/jsx-spread.html
35 | // https://github.com/rackt/react-router/blob/master/docs/guides/overview.md#dynamic-segments
36 |
--------------------------------------------------------------------------------
/client/components/notfound.jsx:
--------------------------------------------------------------------------------
1 | // React
2 | var React = require("react");
3 |
4 | // Component
5 | var NotFound = React.createClass({
6 | displayName: "404",
7 | propTypes: {},
8 | mixins: [],
9 |
10 | getInitialState: function () { return null; },
11 |
12 | componentWillMount: function () {},
13 |
14 | componentWillUnmount: function () {},
15 |
16 | render: function () {
17 | return (
18 |
19 | Page not found
20 |
21 | );
22 | }
23 | });
24 |
25 | module.exports = NotFound;
26 |
--------------------------------------------------------------------------------
/client/components/recipe-details.jsx:
--------------------------------------------------------------------------------
1 | // React
2 | var React = require("react");
3 | var RecipeStore = require("../stores/recipe-store");
4 | var RecipeActions = require("../actions/recipe-actions");
5 |
6 | // Router
7 | var Router = require("react-router");
8 | var RouteHandler = Router.RouteHandler;
9 |
10 | // Child Components
11 | var Ingredient = require("./ingredient");
12 |
13 | // Component
14 | function getRecipeById(id) {
15 | return RecipeStore.getRecipe(id);
16 | }
17 |
18 | var RecipeDetails = React.createClass({
19 | displayName: "RecipeDetails",
20 | propTypes: {},
21 | mixins: [RecipeStore.mixin],
22 |
23 | getInitialState: function () {
24 | return getRecipeById(this.props.params._id);
25 | },
26 |
27 | componentWillMount: function () {
28 | this.parseInstructions();
29 | },
30 |
31 | componentWillUnmount: function () {},
32 |
33 | storeDidChange: function () {
34 | getRecipeById(this.props.params._id);
35 | },
36 |
37 | portionsChanged: function (ev) {
38 | var portions = ev.target.value.trim();
39 |
40 | this.setState({
41 | portions: portions,
42 | multiplier: portions !== "" && !isNaN(portions) ?
43 | portions / getRecipeById(this.props.params._id).portions :
44 | 1
45 | });
46 | },
47 |
48 | savePortions: function () {
49 | // TODO: Check for not valid values and unchanged values
50 | RecipeActions.portionsChanged({
51 | portions: this.state.portions,
52 | _id: this.props.params._id
53 | });
54 | },
55 |
56 | parseInstructions: function () {
57 | // Process instructions to split string on newline
58 | var str = this.state.instructions;
59 | var html = "" + str.replace(/\n([ \t]*\n)+/g, "
")
60 | .replace(/\n/g, "
") + "
";
61 | this.state.parsedInstructions = html;
62 | // Make servings a controlled input
63 | // http://facebook.github.io/react/docs/forms.html#controlled-components
64 | },
65 |
66 | render: function () {
67 | var self = this;
68 |
69 | function createNodes(ingredient, index) {
70 | return (
71 |
72 | );
73 | }
74 |
75 | var ingredientNodes = this.state.ingredients.map(createNodes);
76 |
77 | return (
78 |
79 |
{this.state.title}
80 |
Serves:
81 |
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 |
132 |
133 |
134 |
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 |
46 |
47 |
48 |
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 |
65 |
66 |
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 |
--------------------------------------------------------------------------------