diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index ddd3836..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-**/*.sw?
-**/node_modules
-**/db
diff --git a/kenji_crosland/.gitignore b/kenji_crosland/.gitignore
new file mode 100644
index 0000000..114214c
--- /dev/null
+++ b/kenji_crosland/.gitignore
@@ -0,0 +1,5 @@
+**/*.sw?
+node_modules
+db
+build
+**/*bundle.js
diff --git a/kenji_crosland/app/index.html b/kenji_crosland/app/index.html
new file mode 100644
index 0000000..7c44701
--- /dev/null
+++ b/kenji_crosland/app/index.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ Our Super Recipe App
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
Ingredients:
+
+
+
+
+
+
+
+
+
+
diff --git a/kenji_crosland/app/js/entry.js b/kenji_crosland/app/js/entry.js
new file mode 100644
index 0000000..6c9d721
--- /dev/null
+++ b/kenji_crosland/app/js/entry.js
@@ -0,0 +1,6 @@
+require('angular/angular');
+var angular = window.angular;
+
+var recipeApp = angular.module('recipeApp', []);
+require('./services/services')(recipeApp);
+require('./recipes/recipes')(recipeApp);
diff --git a/kenji_crosland/app/js/recipes/controllers/recipes_controller.js b/kenji_crosland/app/js/recipes/controllers/recipes_controller.js
new file mode 100644
index 0000000..7709fde
--- /dev/null
+++ b/kenji_crosland/app/js/recipes/controllers/recipes_controller.js
@@ -0,0 +1,64 @@
+module.exports = function(app) {
+ app.controller('RecipesController', ['$scope', '$http', 'cfResource', function($scope, $http, cfResource){
+ $scope.recipes = [];
+ $scope.editing = {};
+ $scope.currentRecipe = null;
+
+ $scope.seeRecipe = function(recipe){
+ $scope.currentRecipe = recipe;
+ }
+
+ $scope.makeCopy = function(recipe) {
+ recipe.editing = true;
+ $scope.editing[recipe._id] = angular.copy(recipe);
+ }
+
+ $scope.cancelEditing = function(recipe) {
+ $scope.editing[recipe._id].editing = false;
+ for(i = 0; i < $scope.recipes.length; i++){
+ if($scope.recipes[i]._id === recipe._id){
+ angular.copy($scope.editing[recipe._id], $scope.recipes[i]);
+ delete $scope.editing[recipe._id];
+ return;
+ }
+ }
+ }
+
+ $scope.addIngredientField = function(recipe) {
+ if (recipe.ingredients[recipe.ingredients.length - 1] !== "") {
+ recipe.ingredients.push("");
+ }
+ }
+
+ $scope.removeIngredientField = function(recipe, ingredient) {
+ recipe.ingredients.splice(recipe.ingredients.indexOf(ingredient), 1);
+ }
+
+ $scope.getAll = function() {
+ cfResource('/allrecipes').getAll(function(err, data){
+ if (err) return err;
+ $scope.recipes = data;
+ });
+ };
+
+ $scope.create = function(recipe) {
+ cfResource('/recipes', recipe).post(function(err, data){
+ $scope.recipes.push(data);
+ });
+ };
+
+ $scope.update = function(recipe) {
+ recipe.editing = false;
+ cfResource('/recipes/' + recipe._id, recipe).put(function(err, data) {
+ console.log('Recipe updated!');
+ });
+ }
+
+ $scope.remove = function(recipe) {
+ $scope.recipes.splice($scope.recipes.indexOf(recipe), 1);
+ cfResource('/recipes/' + recipe._id).delete(function(err, data){
+ console.log('Totes cool. Recipe is gone.')
+ })
+ }
+ }]);
+}
diff --git a/kenji_crosland/app/js/recipes/directives/card_directive.js b/kenji_crosland/app/js/recipes/directives/card_directive.js
new file mode 100644
index 0000000..a2f9b53
--- /dev/null
+++ b/kenji_crosland/app/js/recipes/directives/card_directive.js
@@ -0,0 +1,15 @@
+module.exports = function(app) {
+ app.directive('cardDirective', function() {
+ return {
+ restrict: 'AC',
+ replace: true,
+ transclude: true,
+ templateUrl: '/templates/card_template.html',
+ scope: {
+ header: '@',
+ headerText: '@',
+ width: '@' //two columns, three columns, etc
+ }
+ }
+ })
+}
diff --git a/kenji_crosland/app/js/recipes/directives/recipe_form_directive.js b/kenji_crosland/app/js/recipes/directives/recipe_form_directive.js
new file mode 100644
index 0000000..f8e99c4
--- /dev/null
+++ b/kenji_crosland/app/js/recipes/directives/recipe_form_directive.js
@@ -0,0 +1,19 @@
+module.exports = function(app) {
+ app.directive('recipeFormDirective', function() {
+ return {
+ restrict: 'AC',
+ replace: true,
+ templateUrl: '/templates/recipe_form_template.html',
+ scope: {
+ buttonText: '@',
+ formClass: '@',
+ formName: '@',
+ recipe: '=',
+ addIngredientField: '&',
+ removeIngredientField: '&',
+ cancel: '&',
+ save: '&'
+ }
+ }
+ })
+}
diff --git a/kenji_crosland/app/js/recipes/recipes.js b/kenji_crosland/app/js/recipes/recipes.js
new file mode 100644
index 0000000..b2e2a10
--- /dev/null
+++ b/kenji_crosland/app/js/recipes/recipes.js
@@ -0,0 +1,5 @@
+module.exports = function(app){
+ require('./controllers/recipes_controller')(app);
+ require('./directives/card_directive')(app);
+ require('./directives/recipe_form_directive')(app);
+};
diff --git a/kenji_crosland/app/js/services/cf_resource.js b/kenji_crosland/app/js/services/cf_resource.js
new file mode 100644
index 0000000..12c9ba7
--- /dev/null
+++ b/kenji_crosland/app/js/services/cf_resource.js
@@ -0,0 +1,43 @@
+//Made with help from watching in-class code videos
+
+var handleSuccess = function(callback) {
+ return function(res) {
+ callback(null, res.data)
+ };
+};
+
+var handleFail = function(callback) {
+ return function(res) {
+ console.log(res);
+ callback(res.data);
+ }
+}
+
+module.exports = function(app) {
+ app.factory('cfResource', ['$http', function($http) {
+ return function(resourceName, resourceData) {
+ var resource = {};
+ resource.getAll = function(callback) {
+ $http.get(resourceName)
+ .then(handleSuccess(callback), handleFail(callback));
+ };
+ resource.get = function(callback) {
+ $http.get(resourceName)
+ .then(handleSuccess(callback), handleFail(callback));
+ };
+ resource.post = function(callback) {
+ $http.post(resourceName, resourceData)
+ .then(handleSuccess(callback), handleFail(callback));
+ };
+ resource.put = function(callback) {
+ $http.put(resourceName, resourceData)
+ .then(handleSuccess(callback), handleFail(callback));
+ }
+ resource.delete = function(callback) {
+ $http.delete(resourceName)
+ .then(handleSuccess(callback), handleFail(callback));
+ }
+ return resource;
+ };
+ }]);
+};
diff --git a/kenji_crosland/app/js/services/services.js b/kenji_crosland/app/js/services/services.js
new file mode 100644
index 0000000..d43501e
--- /dev/null
+++ b/kenji_crosland/app/js/services/services.js
@@ -0,0 +1,3 @@
+module.exports = function(app) {
+ require('./cf_resource')(app);
+};
diff --git a/kenji_crosland/app/sass/_base.scss b/kenji_crosland/app/sass/_base.scss
new file mode 100644
index 0000000..007fed4
--- /dev/null
+++ b/kenji_crosland/app/sass/_base.scss
@@ -0,0 +1,41 @@
+@import url(https://fonts.googleapis.com/css?family=Nunito);
+@import url(https://fonts.googleapis.com/css?family=Dancing+Script);
+@import url(https://fonts.googleapis.com/css?family=Ledger);
+
+html {font-size: 1.125em;}
+
+body {
+ font-family: 'Ledger', serif;
+ background-color: $light-background;
+ font-weight: 400;
+ line-height: 1.45;
+ color: #333;
+}
+
+p {margin-bottom: 1.3em;}
+
+h1, h2, h3, h4 {
+ font-family: 'Ledger', serif;
+ margin: 1.414em 0 0.5em;
+ font-weight: inherit;
+ line-height: 1.2;
+}
+
+h1 {
+ margin-top: 0;
+ font-size: 2.074em;
+}
+
+h2 {font-size: 1.728em;}
+
+h3 {font-size: 1.44em;}
+
+h4 {font-size: 1.2em;}
+
+small, .font_small {font-size: 0.833em;}
+
+//Forms
+
+input {
+ display: table-cell;
+}
diff --git a/kenji_crosland/app/sass/_reset.scss b/kenji_crosland/app/sass/_reset.scss
new file mode 100644
index 0000000..458eea1
--- /dev/null
+++ b/kenji_crosland/app/sass/_reset.scss
@@ -0,0 +1,427 @@
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box; /* 2 */
+ box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+}
diff --git a/kenji_crosland/app/sass/application.scss b/kenji_crosland/app/sass/application.scss
new file mode 100644
index 0000000..b2c50cb
--- /dev/null
+++ b/kenji_crosland/app/sass/application.scss
@@ -0,0 +1,7 @@
+@import 'colors/colors';
+@import 'reset';
+@import 'base';
+@import 'buttons/buttons';
+@import 'forms/forms';
+@import 'modules/module';
+@import 'layouts/layouts';
diff --git a/kenji_crosland/app/sass/buttons/_add-ingredient-button.scss b/kenji_crosland/app/sass/buttons/_add-ingredient-button.scss
new file mode 100644
index 0000000..e5867c6
--- /dev/null
+++ b/kenji_crosland/app/sass/buttons/_add-ingredient-button.scss
@@ -0,0 +1,3 @@
+.add-ingredient {
+ margin-top: 1em;
+}
diff --git a/kenji_crosland/app/sass/buttons/_buttons.scss b/kenji_crosland/app/sass/buttons/_buttons.scss
new file mode 100644
index 0000000..e470c4d
--- /dev/null
+++ b/kenji_crosland/app/sass/buttons/_buttons.scss
@@ -0,0 +1,3 @@
+@import 'default-button';
+@import 'add-ingredient-button';
+@import 'state';
diff --git a/kenji_crosland/app/sass/buttons/_default-button.scss b/kenji_crosland/app/sass/buttons/_default-button.scss
new file mode 100644
index 0000000..098aee8
--- /dev/null
+++ b/kenji_crosland/app/sass/buttons/_default-button.scss
@@ -0,0 +1,5 @@
+.btn {
+ border: none;
+ background: $primary2;
+ color: $light-background;
+}
diff --git a/kenji_crosland/app/sass/buttons/_state.scss b/kenji_crosland/app/sass/buttons/_state.scss
new file mode 100644
index 0000000..39853f1
--- /dev/null
+++ b/kenji_crosland/app/sass/buttons/_state.scss
@@ -0,0 +1,5 @@
+button[disabled] {
+ background-color: map-get($button, disabled-background);
+ border-color: map-get($button, disabled-border);
+ color: map-get($button, disabled-text);
+}
diff --git a/kenji_crosland/app/sass/colors/_button.scss b/kenji_crosland/app/sass/colors/_button.scss
new file mode 100644
index 0000000..5970ee2
--- /dev/null
+++ b/kenji_crosland/app/sass/colors/_button.scss
@@ -0,0 +1,5 @@
+$button: (
+ disabled-background: lighten($primary2, 20%),
+ disabled-border: lighten($primary2, 50%),
+ disabled-text: $light-shading
+);
diff --git a/kenji_crosland/app/sass/colors/_colors.scss b/kenji_crosland/app/sass/colors/_colors.scss
new file mode 100644
index 0000000..7486d5e
--- /dev/null
+++ b/kenji_crosland/app/sass/colors/_colors.scss
@@ -0,0 +1,2 @@
+@import 'theme';
+@import 'button';
diff --git a/kenji_crosland/app/sass/colors/_theme.scss b/kenji_crosland/app/sass/colors/_theme.scss
new file mode 100644
index 0000000..20242f4
--- /dev/null
+++ b/kenji_crosland/app/sass/colors/_theme.scss
@@ -0,0 +1,19 @@
+//Color Scheme
+//https://color.adobe.com/pomegranate-explosion-color-theme-7287945/
+//'Pomegranate Explosion' Color Palate
+$dark-turquoise: #63A69F;
+$linen: #FBF5E5;
+$wheat: #F2E1AC;
+$salmon: #F2836B;
+$carnation: #F2594B;
+$cardinal-red: #CD2C24;
+$light-gray: #eef;
+
+//Theme variables
+$light-shading: $light-gray;
+$background: $wheat;
+$light-background: $linen;
+$primary1: $salmon;
+$primary2: $dark-turquoise;
+$accent: $carnation;
+$strong-accent: $cardinal-red;
diff --git a/kenji_crosland/app/sass/forms/_forms.scss b/kenji_crosland/app/sass/forms/_forms.scss
new file mode 100644
index 0000000..fbd6859
--- /dev/null
+++ b/kenji_crosland/app/sass/forms/_forms.scss
@@ -0,0 +1 @@
+@import 'edit-recipe/edit-recipe-form';
diff --git a/kenji_crosland/app/sass/forms/edit-recipe/_edit-recipe-form.scss b/kenji_crosland/app/sass/forms/edit-recipe/_edit-recipe-form.scss
new file mode 100644
index 0000000..e898297
--- /dev/null
+++ b/kenji_crosland/app/sass/forms/edit-recipe/_edit-recipe-form.scss
@@ -0,0 +1,16 @@
+.edit-recipe-form {
+ label {
+ font-weight: 600;
+ }
+ input {
+ width: 100%;
+ }
+ fieldset {
+ padding: 1em;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ input {
+ width:75%;
+ }
+ }
+}
diff --git a/kenji_crosland/app/sass/layouts/_grid.scss b/kenji_crosland/app/sass/layouts/_grid.scss
new file mode 100644
index 0000000..b82885c
--- /dev/null
+++ b/kenji_crosland/app/sass/layouts/_grid.scss
@@ -0,0 +1,142 @@
+body {
+ padding: 1em;
+}
+/*
+* Skeleton V2.0.4
+* Copyright 2014, Dave Gamache
+* www.getskeleton.com
+* Free to use under the MIT license.
+* http://www.opensource.org/licenses/mit-license.php
+* 12/29/2014
+*/
+
+
+/* Table of contents
+––––––––––––––––––––––––––––––––––––––––––––––––––
+- Grid
+- Base Styles
+- Typography
+- Links
+- Buttons
+- Forms
+- Lists
+- Code
+- Tables
+- Spacing
+- Utilities
+- Clearing
+- Media Queries
+*/
+
+
+/* Grid
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.container {
+ position: relative;
+ width: 100%;
+ max-width: 960px;
+ margin: 0 auto;
+ padding: 0 20px;
+ box-sizing: border-box; }
+.column,
+.columns {
+ width: 100%;
+ float: left;
+ box-sizing: border-box; }
+
+/* For devices larger than 400px */
+@media (min-width: 400px) {
+ .container {
+ width: 85%;
+ padding: 0; }
+}
+
+/* For devices larger than 550px */
+@media (min-width: 550px) {
+ .container {
+ width: 80%; }
+ .column,
+ .columns {
+ margin-left: 4%; }
+ .column:first-child,
+ .columns:first-child {
+ margin-left: 0; }
+
+ .one.column,
+ .one.columns { width: 4.66666666667%; }
+ .two.columns { width: 13.3333333333%; }
+ .three.columns { width: 22%; }
+ .four.columns { width: 30.6666666667%; }
+ .five.columns { width: 39.3333333333%; }
+ .six.columns { width: 48%; }
+ .seven.columns { width: 56.6666666667%; }
+ .eight.columns { width: 65.3333333333%; }
+ .nine.columns { width: 74.0%; }
+ .ten.columns { width: 82.6666666667%; }
+ .eleven.columns { width: 91.3333333333%; }
+ .twelve.columns { width: 100%; margin-left: 0; }
+
+ .one-third.column { width: 30.6666666667%; }
+ .two-thirds.column { width: 65.3333333333%; }
+
+ .one-half.column { width: 48%; }
+
+ /* Offsets */
+ .offset-by-one.column,
+ .offset-by-one.columns { margin-left: 8.66666666667%; }
+ .offset-by-two.column,
+ .offset-by-two.columns { margin-left: 17.3333333333%; }
+ .offset-by-three.column,
+ .offset-by-three.columns { margin-left: 26%; }
+ .offset-by-four.column,
+ .offset-by-four.columns { margin-left: 34.6666666667%; }
+ .offset-by-five.column,
+ .offset-by-five.columns { margin-left: 43.3333333333%; }
+ .offset-by-six.column,
+ .offset-by-six.columns { margin-left: 52%; }
+ .offset-by-seven.column,
+ .offset-by-seven.columns { margin-left: 60.6666666667%; }
+ .offset-by-eight.column,
+ .offset-by-eight.columns { margin-left: 69.3333333333%; }
+ .offset-by-nine.column,
+ .offset-by-nine.columns { margin-left: 78.0%; }
+ .offset-by-ten.column,
+ .offset-by-ten.columns { margin-left: 86.6666666667%; }
+ .offset-by-eleven.column,
+ .offset-by-eleven.columns { margin-left: 95.3333333333%; }
+
+ .offset-by-one-third.column,
+ .offset-by-one-third.columns { margin-left: 34.6666666667%; }
+ .offset-by-two-thirds.column,
+ .offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
+
+ .offset-by-one-half.column,
+ .offset-by-one-half.columns { margin-left: 52%; }
+
+}
+
+
+/* Media Queries
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+/*
+Note: The best way to structure the use of media queries is to create the queries
+near the relevant code. For example, if you wanted to change the styles for buttons
+on small devices, paste the mobile query code up in the buttons section and style it
+there.
+*/
+
+
+/* Larger than mobile */
+@media (min-width: 400px) {}
+
+/* Larger than phablet (also point when grid becomes active) */
+@media (min-width: 550px) {}
+
+/* Larger than tablet */
+@media (min-width: 750px) {}
+
+/* Larger than desktop */
+@media (min-width: 1000px) {}
+
+/* Larger than Desktop HD */
+@media (min-width: 1200px) {}
diff --git a/kenji_crosland/app/sass/layouts/_layouts.scss b/kenji_crosland/app/sass/layouts/_layouts.scss
new file mode 100644
index 0000000..4becf10
--- /dev/null
+++ b/kenji_crosland/app/sass/layouts/_layouts.scss
@@ -0,0 +1 @@
+@import 'grid';
diff --git a/kenji_crosland/app/sass/modules/_module.scss b/kenji_crosland/app/sass/modules/_module.scss
new file mode 100644
index 0000000..69d005c
--- /dev/null
+++ b/kenji_crosland/app/sass/modules/_module.scss
@@ -0,0 +1,3 @@
+@import "main-headline/module_main-headline";
+@import "card/module_card";
+@import "recipe-list/module_recipe-list";
diff --git a/kenji_crosland/app/sass/modules/card/_mixin.scss b/kenji_crosland/app/sass/modules/card/_mixin.scss
new file mode 100644
index 0000000..379dde5
--- /dev/null
+++ b/kenji_crosland/app/sass/modules/card/_mixin.scss
@@ -0,0 +1,6 @@
+@mixin card-headline($background-color) {
+ padding: .5em;
+ color: #fff;
+ margin-top: 0;
+ background-color: $background-color;
+}
diff --git a/kenji_crosland/app/sass/modules/card/_module_card.scss b/kenji_crosland/app/sass/modules/card/_module_card.scss
new file mode 100644
index 0000000..a5fa89a
--- /dev/null
+++ b/kenji_crosland/app/sass/modules/card/_module_card.scss
@@ -0,0 +1,19 @@
+@import 'mixin';
+
+.card {
+ h2 {
+ @include card-headline($primary2);
+ }
+ h3 {
+ @include card-headline($primary1);
+ }
+ .card-top {
+ padding: 0;
+ }
+ .card-bottom {
+ padding: 1em;
+ }
+ background-color: $background;
+ margin-top: 1em;
+ box-shadow: 3px 3px 1px #bbb;
+}
diff --git a/kenji_crosland/app/sass/modules/main-headline/_module_main-headline.scss b/kenji_crosland/app/sass/modules/main-headline/_module_main-headline.scss
new file mode 100644
index 0000000..9b02003
--- /dev/null
+++ b/kenji_crosland/app/sass/modules/main-headline/_module_main-headline.scss
@@ -0,0 +1,5 @@
+.main-headline {
+ font-family: 'Dancing Script', cursive;
+ margin-top: 0;
+ color: $accent;
+}
diff --git a/kenji_crosland/app/sass/modules/recipe-list/_module_recipe-list.scss b/kenji_crosland/app/sass/modules/recipe-list/_module_recipe-list.scss
new file mode 100644
index 0000000..75c140a
--- /dev/null
+++ b/kenji_crosland/app/sass/modules/recipe-list/_module_recipe-list.scss
@@ -0,0 +1,9 @@
+ul.recipe-list {
+ li {
+ list-style-type: none;
+ }
+ li button {
+ margin-bottom: .4em;
+ }
+ margin: 0 0 0 -1.75em;
+}
diff --git a/kenji_crosland/app/templates/card_template.html b/kenji_crosland/app/templates/card_template.html
new file mode 100644
index 0000000..9ca1386
--- /dev/null
+++ b/kenji_crosland/app/templates/card_template.html
@@ -0,0 +1,11 @@
+
+
+
{{headerText}}
+ {{headerText}}
+ {{headerText}}
+ {{headerText}}
+
+
+
diff --git a/kenji_crosland/app/templates/recipe_form_template.html b/kenji_crosland/app/templates/recipe_form_template.html
new file mode 100644
index 0000000..8e937d6
--- /dev/null
+++ b/kenji_crosland/app/templates/recipe_form_template.html
@@ -0,0 +1,14 @@
+
diff --git a/kenji_crosland/gulpfile.js b/kenji_crosland/gulpfile.js
index 2d415d4..fb61990 100644
--- a/kenji_crosland/gulpfile.js
+++ b/kenji_crosland/gulpfile.js
@@ -1,8 +1,13 @@
var gulp = require('gulp');
var jshint = require('gulp-jshint');
var mocha = require('gulp-mocha');
+var webpack = require('webpack-stream');
+var minifyCss = require('gulp-minify-css');
+var gulpWatch = require('gulp-watch');
+var sass = require('gulp-sass');
+var maps = require('gulp-sourcemaps');
var appFiles = ['index.js','models/**/*.js', 'routes/**/*.js', 'gulpfile.js', 'lib/**/*.js'];
-var testFiles = ['./test/**/*.js'];
+var testFiles = ['./test/test.js', './test/recipes_controller_test.js'];
gulp.task('jshint:test', function(){
return gulp.src(testFiles)
@@ -31,5 +36,45 @@ gulp.task('mocha', ['jshint'], function(){
.pipe(mocha({reporter: 'spec'}));
});
+gulp.task('static:dev', function(){
+ gulp.src('app/**/*.html')
+ .pipe(gulp.dest('build/'));
+});
+
+gulp.task('sass:dev', function(){
+ gulp.src('./app/sass/**/*.scss')
+ .pipe(maps.init())
+ .pipe(sass().on('error', sass.logError))
+ .pipe(minifyCss())
+ .pipe(maps.write('./'))
+ .pipe(gulp.dest('build/'));
+});
+
+gulp.task('sass:watch', function() {
+ gulp.watch(['./app/sass/**/*.scss', './app/index.html'], ['sass:dev','static:dev']);
+});
+
+gulp.task('webpack:dev', function(){
+ gulp.src('app/js/entry.js')
+ .pipe(webpack({
+ output: {
+ filename: 'bundle.js'
+ }
+ }))
+ .pipe(gulp.dest('build/'));
+});
+
+gulp.task('webpack:test', function(){
+ gulp.src('test/client/test_entry.js')
+ .pipe(webpack({
+ output: {
+ filename: 'test_bundle.js'
+ }
+ }))
+ .pipe(gulp.dest('test/client/'));
+});
+
+gulp.task('static:watch');
+gulp.task('build:dev', ['webpack:dev', 'static:dev', 'sass:dev']);
gulp.task('jshint', ['jshint:test', 'jshint:app']);
-gulp.task('default', ['jshint', 'mocha']);
+gulp.task('default', ['build:dev', 'jshint', 'mocha']);
diff --git a/kenji_crosland/karma.conf.js b/kenji_crosland/karma.conf.js
new file mode 100644
index 0000000..394be09
--- /dev/null
+++ b/kenji_crosland/karma.conf.js
@@ -0,0 +1,69 @@
+// Karma configuration
+// Generated on Wed Dec 02 2015 18:17:05 GMT-0800 (PST)
+
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'test/client/test_bundle.js'
+ ],
+
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ },
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['progress'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['PhantomJS'],
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: true,
+
+ // Concurrency level
+ // how many browser should be started simultanous
+ concurrency: Infinity
+ })
+}
diff --git a/kenji_crosland/lib/basic_http_authentication.js b/kenji_crosland/lib/basic_http_authentication.js
new file mode 100644
index 0000000..1fc4338
--- /dev/null
+++ b/kenji_crosland/lib/basic_http_authentication.js
@@ -0,0 +1,18 @@
+//Got help from in class code
+module.exports = function(req, res, next) {
+ try {
+ //Could be chained into one line
+ var authString = req.headers.authorization;
+ var basicString = authString.split(' ')[1];
+ var basicBuffer = new Buffer(basicString, 'base64');
+ var authArray = basicBuffer.toString().split(':');
+ req.auth = {
+ username: authArray[0],
+ password: authArray[1]
+ };
+ next();
+ } catch (e) {
+ console.log(e);
+ return res.status(401).json({msg: 'could not authentiCat'});
+ }
+};
diff --git a/kenji_crosland/lib/eat_auth.js b/kenji_crosland/lib/eat_auth.js
new file mode 100644
index 0000000..4053e21
--- /dev/null
+++ b/kenji_crosland/lib/eat_auth.js
@@ -0,0 +1,31 @@
+//Got help from in class code
+var eat = require('eat');
+var User = require(__dirname + '/../models/user');
+
+module.exports = exports = function(req, res, next) {
+ var token = req.headers.token || (req.body)? req.body.token : '';
+ if (!token){
+ return res.json({msg: 'authentiCat seyz noe!1111@! and is watching youuuu!'});
+ }
+ eat.decode(token, process.env.APP_SECRET, function(err, decoded){
+ if (err) {
+ console.log(err);
+ return res.json({msg: 'authenitCat seyz noe!1111@!'});
+ }
+
+ User.findOne({_id: decoded.id}, function(err, user){
+ if (err) {
+ console.log(err);
+ return res.status(401).json({msg: 'authentiCat seyz noe121!'});
+ }
+
+ if (!user) {
+ console.log(err);
+ return res.status(401).json({msg: 'authentiCat seyz noe121!'});
+ }
+
+ req.user = user;
+ next();
+ });
+ });
+};
diff --git a/kenji_crosland/models/recipe.js b/kenji_crosland/models/recipe.js
index 9c98069..39042b0 100644
--- a/kenji_crosland/models/recipe.js
+++ b/kenji_crosland/models/recipe.js
@@ -5,6 +5,8 @@ var reviewSchema = require(__dirname + '/review.js').reviewSchema;
var recipeSchema = mongoose.Schema({
title: String,
ingredients: Array,
+ user: String,
+ userId: String,
reviews: [reviewSchema]
});
diff --git a/kenji_crosland/models/user.js b/kenji_crosland/models/user.js
new file mode 100644
index 0000000..fe53c62
--- /dev/null
+++ b/kenji_crosland/models/user.js
@@ -0,0 +1,32 @@
+//Got validation help here: http://nraj.tumblr.com/post/38706353543/handling-uniqueness-validation-in-mongomongoose
+
+var mongoose = require('mongoose');
+var bcrypt = require('bcrypt');
+var eat = require('eat');
+var userSchema = new mongoose.Schema({
+//Adding it at the beginning so it doesn't get deleted.
+ username: {type: String, required: true, unique: true, trim: true},
+ auth: {
+ basic:{
+ username: String,
+ password: String
+ }
+ }
+});
+
+userSchema.methods.hashPassword = function(password) {
+ var hash = this.auth.basic.password = bcrypt.hashSync(password, 8);
+ return hash;
+};
+
+userSchema.methods.checkPassword = function(password) {
+ return bcrypt.compareSync(password, this.auth.basic.password);
+
+};
+
+userSchema.methods.generateToken = function(callback){
+ var id = this._id;
+ eat.encode({id: id}, process.env.APP_SECRET, callback);
+};
+
+module.exports = mongoose.model('User', userSchema);
diff --git a/kenji_crosland/package.json b/kenji_crosland/package.json
index 534ce00..f8a80c7 100644
--- a/kenji_crosland/package.json
+++ b/kenji_crosland/package.json
@@ -9,17 +9,34 @@
"author": "Kenji Crosland",
"license": "MIT",
"dependencies": {
+ "bcrypt": "latest",
"body-parser": "^1.14.1",
+ "eat": "^0.1.1",
"express": "^4.13.3",
"mongoose": "^4.2.5"
},
"devDependencies": {
+ "angular": "^1.4.8",
+ "angular-mocks": "^1.4.8",
"chai": "^3.4.1",
"chai-http": "^1.0.0",
"gulp": "^3.9.0",
- "gulp-jshint": "^1.12.0",
+ "gulp-concat-css": "^2.2.0",
+ "gulp-jshint": "^2.0.0",
+ "gulp-minify-css": "^1.2.2",
"gulp-mocha": "^2.1.3",
+ "gulp-sass": "^2.1.0",
+ "gulp-sourcemaps": "^1.6.0",
+ "gulp-watch": "^4.3.5",
+ "jasmine-core": "^2.3.4",
+ "jshint": "^2.8.0",
"jshint-stylish": "^2.0.1",
- "mocha": "^2.3.3"
+ "karma": "^0.13.15",
+ "karma-jasmine": "^0.3.6",
+ "karma-phantomjs-launcher": "^0.2.1",
+ "mocha": "^2.3.3",
+ "phantomjs": "^1.9.19",
+ "sass-list-maps": "^1.0.0-b",
+ "webpack-stream": "^2.1.1"
}
}
diff --git a/kenji_crosland/routes/auth_routes.js b/kenji_crosland/routes/auth_routes.js
new file mode 100644
index 0000000..fbc82df
--- /dev/null
+++ b/kenji_crosland/routes/auth_routes.js
@@ -0,0 +1,53 @@
+var express = require('express');
+var jsonParser = require('body-parser').json();
+var basicHttp = require(__dirname + '/../lib/basic_http_authentication');
+var User = require(__dirname + '/../models/user');
+
+var authRouter = module.exports = exports = express.Router();
+
+authRouter.post('/signup', jsonParser, function(req, res){
+ var user = new User();
+ user.username = req.body.username;
+ user.auth.basic.username = req.body.username;
+ user.hashPassword(req.body.password);
+
+ user.save(function(err,data){
+ if (err && (11000 === err.code || 11001 === err.code)){
+ return res.status(401).json({msg: 'authentiCat seayz this user already exists'});
+ }
+ if (err) throw err;
+
+ user.generateToken(function(err, token){
+ if (err) throw err;
+ res.json({token: token});
+ });
+
+ });
+});
+
+authRouter.get('/signin', basicHttp, function(req, res){
+ if (!(req.auth.username && req.auth.password)) {
+ console.log('no basic auth provided');
+ return res.status(401).json({msg: 'authentiCat seayz noe!'});
+ }
+
+ User.findOne({'auth.basic.username': req.auth.username}, function(err, user){
+ if (err) {
+ console.log('no basic auth provided');
+ return res.status(401).json({msg: 'authentiCat seayz noe!'});
+ }
+ if (!user){
+ console.log('you are not a user');
+ return res.status(401).json({msg: 'authentiCat seayz u r not a urzr!'});
+ }
+ if (!user.checkPassword(req.auth.password)) {
+ console.log('the password donut match');
+ return res.status(401).json({msg: 'authentiCat seeeez noe!'});
+ }
+ user.generateToken(function(err, token){
+ if (err) throw err;
+
+ res.json({token: token});
+ });
+ });
+});
diff --git a/kenji_crosland/routes/recipes_routes.js b/kenji_crosland/routes/recipes_routes.js
index bd75e1f..e80ee87 100644
--- a/kenji_crosland/routes/recipes_routes.js
+++ b/kenji_crosland/routes/recipes_routes.js
@@ -3,9 +3,18 @@ var express = require('express');
var bodyParser = require('body-parser');
var Recipe = require(__dirname + '/../models/recipe').Recipe;
var Review = require(__dirname + '/../models/review').Review;
+var eatAuth = require(__dirname + '/../lib/eat_auth');
+
var recipeRouter = module.exports = express.Router();
-recipeRouter.get('/recipes', function(req, res){
+recipeRouter.get('/recipes', bodyParser.json(), function(req, res){
+ Recipe.find({userId: req.user._id}, function(err, data){
+ if (err) throw err;
+ res.json(data);
+ });
+});
+
+recipeRouter.get('/allrecipes', function(req, res){
Recipe.find({}, function(err, data){
if (err) throw err;
res.json(data);
@@ -14,6 +23,8 @@ recipeRouter.get('/recipes', function(req, res){
recipeRouter.post('/recipes', bodyParser.json(), function(req, res){
var newRecipe = new Recipe(req.body);
+ //newRecipe.userId = req.user._id;
+ //newRecipe.user = req.user.username;
newRecipe.save(function(err, data){
if (err) throw err;
res.json(data);
@@ -37,7 +48,7 @@ recipeRouter.put('/recipes/:id', bodyParser.json(), function(req, res){
});
});
-recipeRouter.delete('/recipes/:id', function(req, res){
+recipeRouter.delete('/recipes/:id', bodyParser.json(), function(req, res){
Recipe.remove({_id: req.params.id}, function(err){
if (err) return err;
res.json({msg: 'Recipe deleted!'});
diff --git a/kenji_crosland/server.js b/kenji_crosland/server.js
index d5d7ff4..d306bfb 100644
--- a/kenji_crosland/server.js
+++ b/kenji_crosland/server.js
@@ -1,17 +1,23 @@
//Made with help from in class code
-var mongoose = require('mongoose')
+var mongoose = require('mongoose');
var express = require('express');
+var fs = require('fs');
var app = express();
var recipesRouter = require(__dirname + '/routes/recipes_routes');
var ingredientsRouter = require(__dirname + '/routes/ingredients_routes');
-
-
+var authRouter = require(__dirname + '/routes/auth_routes');
+process.env.APP_SECRET = process.env.APP_SECRET = "changeme" //Use secret key
mongoose.connect(process.env.MONGOLAB_URI || 'mongodb://localhost/recipe_database');
app.use(recipesRouter);
app.use(ingredientsRouter);
-app.use(express.static(__dirname + '/public'));
+app.use(authRouter);
+app.use(express.static(__dirname + '/build'));
+
+app.use(function(req, res){
+ res.status(404).send('could not find file');
+});
app.listen(process.env.PORT || 3000, function(){
console.log('server up');
diff --git a/kenji_crosland/test/client/recipes_controller_test.js b/kenji_crosland/test/client/recipes_controller_test.js
new file mode 100644
index 0000000..f39b802
--- /dev/null
+++ b/kenji_crosland/test/client/recipes_controller_test.js
@@ -0,0 +1,88 @@
+require(__dirname + '/../../app/js/entry');
+require('angular-mocks');
+
+describe('recipes controller', function() {
+ var $httpBackend;
+ var $ControllerConstructor;
+ var $scope;
+
+ beforeEach(angular.mock.module('recipeApp'));
+
+ beforeEach(angular.mock.inject(function($rootScope, $controller){
+ $scope = $rootScope.$new();
+ $ControllerConstructor = $controller;
+ }));
+
+ it('should be able to create a controller', function() {
+ var controller = $ControllerConstructor('RecipesController', {$scope: $scope});
+ expect(typeof $scope).toBe('object');
+ expect(typeof controller).toBe('object');
+ expect(Array.isArray($scope.recipes)).toBe(true);
+ });
+
+ it('should be able to cancel editing', function(){
+ var controller = $ControllerConstructor('RecipesController', {$scope: $scope});
+ $scope.recipes.push({_id:1, title: 'test recipe'});
+ var recipe = $scope.recipes[0];
+ expect(recipe.title).toBe('test recipe');
+ $scope.makeCopy(recipe);
+ expect(recipe.editing).toBe(true);
+ expect($scope.editing[recipe._id]).toEqual(recipe);
+ $scope.cancelEditing(recipe);
+ expect(recipe.editing).toBe(false);
+ expect($scope.editing[recipe._id]).toBe(undefined);
+ });
+
+ describe('REST requests', function() {
+ beforeEach(angular.mock.inject(function(_$httpBackend_, $rootScope){
+ $httpBackend = _$httpBackend_;
+ $scope = $rootScope.$new();
+ $ControllerConstructor('RecipesController', {$scope: $scope})
+ }));
+
+ afterEach(function() {
+ $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should make a GET request when getAll() is called', function(){
+ $httpBackend.expectGET('/allrecipes').respond(200, [{title: 'test recipe'}]);
+ $scope.getAll();
+ $httpBackend.flush();
+ expect($scope.recipes[0].title).toBe('test recipe');
+ });
+
+ it('should be able to create a recipe', function() {
+ $httpBackend.expectPOST('/recipes').respond(200, {title: 'Hipster Sriracha Tacos'});
+ expect($scope.recipes.length).toBe(0);
+ $scope.create({title: 'Hipster Sriracha Tacos'});
+ $httpBackend.flush();
+ expect($scope.recipes[0].title).toBe('Hipster Sriracha Tacos');
+ });
+
+ it('should be able to update a recipe', function(){
+ $scope.recipes.push({title: 'hello'});
+ expect($scope.recipes[0].title).toBe('hello');
+ $scope.newRecipe = angular.copy($scope.recipes[0]);
+ $scope.newRecipe.title = 'Spicy Hipster Sriracha Tacos';
+ $scope.newRecipe._id = '123';
+ $httpBackend.expectPUT('/recipes/' + $scope.newRecipe._id, {"_id":$scope.newRecipe._id,"title":"Spicy Hipster Sriracha Tacos","editing":false}).respond(200, {});
+ $scope.update($scope.newRecipe);
+ $httpBackend.flush();
+ expect($scope.newRecipe.editing).toBe(false);
+ });
+
+ it('should be able to delete a recipe', function() {
+ var recipe = $scope.recipes.push({_id:1, title: 'test recipe'});
+ expect($scope.recipes[0].title).toBe('test recipe');
+ expect($scope.recipes.length).toBe(1);
+ $httpBackend.expectDELETE('/recipes/' + $scope.recipes._id).respond(200,{});
+ $scope.remove(recipe);
+ $httpBackend.flush();
+ expect($scope.recipes.length).toBe(0);
+ });
+
+ });
+});
+
+
diff --git a/kenji_crosland/test/client/test_entry.js b/kenji_crosland/test/client/test_entry.js
new file mode 100644
index 0000000..57bb626
--- /dev/null
+++ b/kenji_crosland/test/client/test_entry.js
@@ -0,0 +1 @@
+require('./recipes_controller_test');
diff --git a/kenji_crosland/test/test.js b/kenji_crosland/test/test.js
index 7eb7d59..6f5adba 100644
--- a/kenji_crosland/test/test.js
+++ b/kenji_crosland/test/test.js
@@ -4,6 +4,7 @@ var chai = require('chai');
var chaiHttp = require('chai-http');
chai.use(chaiHttp);
var expect = chai.expect;
+var User = require(__dirname + '/../models/user');
process.env.MONGOLAB_URI = 'mongodb://localhost/recipe_test';
@@ -11,71 +12,88 @@ require(__dirname + '/../server');
var mongoose = require('mongoose');
var Recipe = require(__dirname + '/../models/recipe').Recipe;
-describe('recipe routes', function(){
+describe('All routes on on the recipe app', function(){
+ var token = "";
+
after(function(done){
mongoose.connection.db.dropDatabase(function(){
done();
});
});
- it('should create a recipe with a POST request',function(done){
- var recipeData = {title:'Tacos', ingredients:['pork', 'cumin', 'chili powder', 'garlic', 'onions', 'tomatoes']};
- chai.request('http://localhost:3000')
- .post('/recipes')
- .send(recipeData)
- .end(function(err,res){
+ describe('authorization routes', function(done){
+ before(function(done){
+ var userData = {username: 'Iron Chef', password:'foobar123'};
+ chai.request('http://localhost:3000')
+ .post('/signup')
+ .send(userData)
+ .end(function(err, res){
+ token = res.body.token;
expect(err).to.eql(null);
- expect(res.body.title).to.eql('Tacos');
- expect(res.body.ingredients).to.include('tomatoes');
- expect(res.body).to.have.property('_id');
+ expect(token).to.not.eql("");
done();
});
});
- it('should get all the recipes with a GET request', function(done){
- chai.request('http://localhost:3000')
- .get('/recipes')
- .end(function(err, res){
- expect(err).to.eql(null);
- expect(Array.isArray(res.body)).to.eql(true);
- done();
- });
- });
-
- describe('a test recipe', function(){
- beforeEach(function(done){
- (new Recipe({title: "Teriyaki", ingredients:['rice', 'chicken', 'soy sauce', 'sugar']}))
- .save(function(err, data){
- this.recipe = data;
+ it('should have created a user with a the post request above', function(done){
+ User.findOne({'auth.basic.username': 'Iron Chef'}, function(err, user){
+ expect(user).to.not.eql(null);
+ expect(user.username).to.eql('Iron Chef');
done();
- }.bind(this));
+ });
+ });
+ it('should be able to log in with an existing username', function(done){
+ chai.request('http://localhost:3000')
+ .get('/signin')
+ .auth('Iron Chef', 'foobar123')
+ .end(function(err, res){
+ expect(err).to.eql(null);
+ expect(token).to.not.eql("");
+ done();
});
+ });
- it('should be modified by a PUT request', function(done){
+ it('should NOT be able to log in with an existing username', function(done){
chai.request('http://localhost:3000')
- .put('/recipes/' + this.recipe._id)
- .send({title:"Spicy Teriyaki", ingredients:['rice', 'chicken', 'soy sauce', 'siracha', 'sugar'], reviews: [{text:"It was okay", rating:3}]})
+ .get('/signin')
+ .auth('Iron Chef', 'wrongpassword')
.end(function(err, res){
- expect(err).to.eql(null);
- expect(res.body.msg).to.eql('Recipe updated!');
+ expect(res.body.msg).to.eql('authentiCat seeeez noe!');
done();
});
});
- it('should be modified by a PUT request with a review', function(done){
+ it('should NOT create a user with an existing username', function(done){
+ var userData = {username: 'Iron Chef', password:'foobar123'};
chai.request('http://localhost:3000')
- .put('/recipes/review/' + this.recipe._id)
- .send({text:"It was tasty!", rating:5})
+ .post('/signup')
+ .send(userData)
.end(function(err, res){
+ expect(res.body.msg).to.eql('authentiCat seayz this user already exists');
+ done();
+ });
+ });
+ });
+
+ describe('recipe routes with a valid token', function(){
+
+ it('should create a recipe with a POST request',function(done){
+ var recipeData = {title:'Tacos', ingredients:['pork', 'cumin', 'chili powder', 'garlic', 'onions', 'tomatoes']/*, token: token*/};
+ chai.request('http://localhost:3000')
+ .post('/recipes')
+ .send(recipeData)
+ .end(function(err,res){
expect(err).to.eql(null);
- expect(res.body.msg).to.eql('Review Added!');
+ expect(res.body.title).to.eql('Tacos');
+ expect(res.body.ingredients).to.include('tomatoes');
+ expect(res.body).to.have.property('_id');
done();
});
});
- it('should return all recipes with a specified ingredient', function(done){
+ it('should get all the recipes with a GET request', function(done){
chai.request('http://localhost:3000')
- .get('/recipes-made-with/' + this.recipe.ingredients[0])
+ .get('/allrecipes')
.end(function(err, res){
expect(err).to.eql(null);
expect(Array.isArray(res.body)).to.eql(true);
@@ -83,14 +101,160 @@ describe('recipe routes', function(){
});
});
- it('should be deleted by a DELETE request', function(done){
+ describe('a test recipe', function(){
+ beforeEach(function(done){
+ (new Recipe({title: "Teriyaki", ingredients:['rice', 'chicken', 'soy sauce', 'sugar']}))
+ .save(function(err, data){
+ this.recipe = data;
+ done();
+ }.bind(this));
+ });
+
+ it('should be modified by a PUT request', function(done){
+ chai.request('http://localhost:3000')
+ .put('/recipes/' + this.recipe._id)
+ .send({title:"Spicy Teriyaki", ingredients:['rice', 'chicken', 'soy sauce', 'siracha', 'sugar'], reviews: [{text:"It was okay", rating:3}], token: token})
+ .end(function(err, res){
+ expect(err).to.eql(null);
+ expect(res.body.msg).to.eql('Recipe updated!');
+ done();
+ });
+ });
+
+ it('should be modified by a PUT request with a review', function(done){
+ chai.request('http://localhost:3000')
+ .put('/recipes/review/' + this.recipe._id)
+ .send({text:"It was tasty!", rating:5, token: token})
+ .end(function(err, res){
+ expect(err).to.eql(null);
+ expect(res.body.msg).to.eql('Review Added!');
+ done();
+ });
+ });
+
+ it('should return all recipes with a specified ingredient', function(done){
+ chai.request('http://localhost:3000')
+ .get('/recipes-made-with/' + this.recipe.ingredients[0])
+ .end(function(err, res){
+ expect(err).to.eql(null);
+ expect(Array.isArray(res.body)).to.eql(true);
+ done();
+ });
+ });
+
+ it('should be deleted by a DELETE request', function(done){
+ chai.request('http://localhost:3000')
+ .delete('/recipes/' + this.recipe._id)
+ .send({token: token})
+ .end(function(err, res){
+ expect(err).to.eql(null);
+ expect(res.body.msg).to.eql('Recipe deleted!');
+ done();
+ });
+ });
+ });
+ });
+ describe('recipe routes without sending a token', function(){
+
+ // it('should NOT create a recipe with a POST request',function(done){
+ // var recipeData = {title:'Tacos', ingredients:['pork', 'cumin', 'chili powder', 'garlic', 'onions', 'tomatoes']};
+ // chai.request('http://localhost:3000')
+ // .post('/recipes')
+ // .send(recipeData)
+ // .end(function(err,res){
+ // expect(res.body.msg).to.eql('authentiCat seyz noe!1111@! and is watching youuuu!');
+ // done();
+ // });
+ // });
+
+ // it('should NOT create a recipe with a POST request and an invalid token',function(done){
+ // var recipeData = {title:'Tacos', ingredients:['pork', 'cumin', 'chili powder', 'garlic', 'onions', 'tomatoes'], token: 'Nadda real token'};
+ // chai.request('http://localhost:3000')
+ // .post('/recipes')
+ // .send(recipeData)
+ // .end(function(err,res){
+ // expect(res.body.msg).to.eql('authenitCat seyz noe!1111@!');
+ // done();
+ // });
+ // });
+
+ it('should still get all the recipes with a GET request without the token', function(done){
chai.request('http://localhost:3000')
- .delete('/recipes/' + this.recipe._id)
+ .get('/allrecipes')
.end(function(err, res){
expect(err).to.eql(null);
- expect(res.body.msg).to.eql('Recipe deleted!');
+ expect(Array.isArray(res.body)).to.eql(true);
done();
});
});
- });
+
+ describe('a test recipe', function(){
+ beforeEach(function(done){
+ (new Recipe({title: "Teriyaki", ingredients:['rice', 'chicken', 'soy sauce', 'sugar']}))
+ .save(function(err, data){
+ this.recipe = data;
+ done();
+ }.bind(this));
+ });
+
+ // it('should NOT be modified by a PUT request', function(done){
+ // chai.request('http://localhost:3000')
+ // .put('/recipes/' + this.recipe._id)
+ // .send({title:"Spicy Teriyaki", ingredients:['rice', 'chicken', 'soy sauce', 'siracha', 'sugar'], reviews: [{text:"It was okay", rating:3}]})
+ // .end(function(err, res){
+ // expect(res.body.msg).to.eql('authentiCat seyz noe!1111@! and is watching youuuu!');
+ // done();
+ // });
+ // });
+
+ // it('should NOT be modified by a PUT request with an invalid token', function(done){
+ // chai.request('http://localhost:3000')
+ // .put('/recipes/' + this.recipe._id)
+ // .send({title:"Spicy Teriyaki", ingredients:['rice', 'chicken', 'soy sauce', 'siracha', 'sugar'], reviews: [{text:"It was okay", rating:3}], token: "Nadda real token"})
+ // .end(function(err, res){
+ // expect(res.body.msg).to.eql('authenitCat seyz noe!1111@!');
+ // done();
+ // });
+ // });
+
+ // it('should NOT be modified by a PUT request with a review', function(done){
+ // chai.request('http://localhost:3000')
+ // .put('/recipes/review/' + this.recipe._id)
+ // .send({text:"It was tasty!", rating:5})
+ // .end(function(err, res){
+ // expect(res.body.msg).to.eql('authentiCat seyz noe!1111@! and is watching youuuu!');
+ // done();
+ // });
+ // });
+
+ it('should still return all recipes with a specified ingredient without a token', function(done){
+ chai.request('http://localhost:3000')
+ .get('/recipes-made-with/' + this.recipe.ingredients[0])
+ .end(function(err, res){
+ expect(err).to.eql(null);
+ expect(Array.isArray(res.body)).to.eql(true);
+ done();
+ });
+ });
+
+ // it('should NOT be deleted by a DELETE request with an invalid token', function(done){
+ // chai.request('http://localhost:3000')
+ // .delete('/recipes/' + this.recipe._id)
+ // .send({token: 'Nadda real token'})
+ // .end(function(err, res){
+ // expect(res.body.msg).to.eql('authenitCat seyz noe!1111@!');
+ // done();
+ // });
+ // });
+
+ // it('should NOT be deleted by a DELETE request without a token', function(done){
+ // chai.request('http://localhost:3000')
+ // .delete('/recipes/' + this.recipe._id)
+ // .end(function(err, res){
+ // expect(res.body.msg).to.eql('authentiCat seyz noe!1111@! and is watching youuuu!');
+ // done();
+ // });
+ // });
+ });
+});
});