diff --git a/.gitignore b/.gitignore
index ddd3836..bc9cc4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+**/node_modules/
**/*.sw?
-**/node_modules
-**/db
+**/*bundle.js
+**/db/
+**/build
+**/.DS_Store
diff --git a/README.md b/README.md
index 5e09c5c..f11549f 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,16 @@
-Mongo Backed REST API
-==========================
-To complete this assignment:
- * fork this repository (the sub module for this specific assignment)
- * clone down your fork
- * place all of your work in a folder that is your full name, use `_`s instead of spaces
- * push back up to your fork
- * create a pull request back to the original repo
- * submit a link to the PR in canvas
+Add a CSS process to your Gulpfile including:
+-Minification
+-Concatenation
+-And a watch
-Assignment Description
---------------------------
-Create a single resource rest API with Express that's backed by Mongo.
+Organize your CSS using the SMACSS file structure. I would like to see a proper separation of concerns.
+-base.css
+-layout.css
+-module.css
+-state.css
-I'm leaving this pretty open to interpretation. I want you to write this from scratch, don't just copy and paste code from class or previous projects.
+Your process should produce a single .min.css file into your build directory
-Add a feature of Mongoose that we didn't use class, such as data validation.
-
-Also, implement a non CRUD resource (meaning that it doesn't use the full GET/POST/PUT/PATCH/DELETE interface).
-
-
-
-Rubric
-
- * Use of Express: 3pts
- * Use of Mongo: 3pts
- * Tests: 2pts
- * Project Organization: 2pts
+Your application should be Responsive
+-Use a flexible layout
+-Use media queries where necessary
diff --git a/emily_landi/app/css/base.css b/emily_landi/app/css/base.css
new file mode 100644
index 0000000..eaccabc
--- /dev/null
+++ b/emily_landi/app/css/base.css
@@ -0,0 +1,40 @@
+/*Base rules are the defaults.*/
+body {
+ font-family: 'Georgia', 'Arial';
+ font-size: 1em;
+ color: black;
+ background-color: darkgrey;
+}
+h1 {
+ text-align: center;
+ margin: 0;
+ width: 100%;
+ height: 2.5em;
+ background-color: black;
+ color: white;
+ font-size: 4.5em;
+}
+h2, p {
+ padding-left: 1em;
+ padding-right: 1em;
+}
+#books {
+ background-color: aqua;
+}
+#newBooks {
+ padding: 1em;
+ border: 0.5em solid aqua;
+}
+img {
+ height: 40%;
+ width: 40%;
+ padding-left: 30%;
+}
+footer {
+ margin-top: 1em;
+ padding: 0.75em;
+ background-color: black;
+ color: white;
+ font-size: 1em;
+ text-align: center;
+}
diff --git a/emily_landi/app/css/layout.css b/emily_landi/app/css/layout.css
new file mode 100644
index 0000000..b329f8b
--- /dev/null
+++ b/emily_landi/app/css/layout.css
@@ -0,0 +1,420 @@
+/*Layout rules divide the page into sections. */
+
+/*
+* 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%; }
+
+}
+
+
+/* Base Styles
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+/* NOTE
+html is set to 62.5% so that all the REM measurements throughout Skeleton
+are based on 10px sizing. So basically 1.5rem = 15px :) */
+html {
+ font-size: 62.5%; }
+body {
+ font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
+ line-height: 1.6;
+ font-weight: 400;
+ /*font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;*/
+ color: #222; }
+
+
+/* Typography
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+h1, h2, h3, h4, h5, h6 {
+ margin-top: 0;
+ margin-bottom: 2rem;
+ font-weight: 300; }
+/*h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}*/
+h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
+h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
+h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
+h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
+h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
+
+/* Larger than phablet */
+@media (min-width: 550px) {
+ h1 { font-size: 5.0rem; }
+ h2 { font-size: 4.2rem; }
+ h3 { font-size: 3.6rem; }
+ h4 { font-size: 3.0rem; }
+ h5 { font-size: 2.4rem; }
+ h6 { font-size: 1.5rem; }
+}
+
+p {
+ margin-top: 0; }
+
+
+/* Links
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+a {
+ color: #1EAEDB; }
+a:hover {
+ color: #0FA0CE; }
+
+
+/* Buttons
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.button,
+button,
+input[type="submit"],
+input[type="reset"],
+input[type="button"] {
+ display: inline-block;
+ height: 38px;
+ padding: 0 30px;
+ color: #555;
+ text-align: center;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 38px;
+ letter-spacing: .1rem;
+ text-transform: uppercase;
+ text-decoration: none;
+ white-space: nowrap;
+ background-color: transparent;
+ border-radius: 4px;
+ border: 1px solid #bbb;
+ cursor: pointer;
+ box-sizing: border-box; }
+.button:hover,
+button:hover,
+input[type="submit"]:hover,
+input[type="reset"]:hover,
+input[type="button"]:hover,
+.button:focus,
+button:focus,
+input[type="submit"]:focus,
+input[type="reset"]:focus,
+input[type="button"]:focus {
+ color: #333;
+ border-color: #888;
+ outline: 0; }
+.button.button-primary,
+button.button-primary,
+input[type="submit"].button-primary,
+input[type="reset"].button-primary,
+input[type="button"].button-primary {
+ color: #FFF;
+ background-color: #33C3F0;
+ border-color: #33C3F0; }
+.button.button-primary:hover,
+button.button-primary:hover,
+input[type="submit"].button-primary:hover,
+input[type="reset"].button-primary:hover,
+input[type="button"].button-primary:hover,
+.button.button-primary:focus,
+button.button-primary:focus,
+input[type="submit"].button-primary:focus,
+input[type="reset"].button-primary:focus,
+input[type="button"].button-primary:focus {
+ color: #FFF;
+ background-color: #1EAEDB;
+ border-color: #1EAEDB; }
+
+
+/* Forms
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="text"],
+input[type="tel"],
+input[type="url"],
+input[type="password"],
+textarea,
+select {
+ height: 38px;
+ padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
+ background-color: #fff;
+ border: 1px solid #D1D1D1;
+ border-radius: 4px;
+ box-shadow: none;
+ box-sizing: border-box; }
+/* Removes awkward default styles on some inputs for iOS */
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="text"],
+input[type="tel"],
+input[type="url"],
+input[type="password"],
+textarea {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none; }
+textarea {
+ min-height: 65px;
+ padding-top: 6px;
+ padding-bottom: 6px; }
+input[type="email"]:focus,
+input[type="number"]:focus,
+input[type="search"]:focus,
+input[type="text"]:focus,
+input[type="tel"]:focus,
+input[type="url"]:focus,
+input[type="password"]:focus,
+textarea:focus,
+select:focus {
+ border: 1px solid #33C3F0;
+ outline: 0; }
+label,
+legend {
+ display: block;
+ margin-bottom: .5rem;
+ font-weight: 600; }
+fieldset {
+ padding: 0;
+ border-width: 0; }
+input[type="checkbox"],
+input[type="radio"] {
+ display: inline; }
+label > .label-body {
+ display: inline-block;
+ margin-left: .5rem;
+ font-weight: normal; }
+
+
+/* Lists
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+ul {
+ list-style: circle inside; }
+ol {
+ list-style: decimal inside; }
+ol, ul {
+ padding-left: 0;
+ margin-top: 0; }
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+ margin: 1.5rem 0 1.5rem 3rem;
+ font-size: 90%; }
+li {
+ margin-bottom: 1rem; }
+
+
+/* Code
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+code {
+ padding: .2rem .5rem;
+ margin: 0 .2rem;
+ font-size: 90%;
+ white-space: nowrap;
+ background: #F1F1F1;
+ border: 1px solid #E1E1E1;
+ border-radius: 4px; }
+pre > code {
+ display: block;
+ padding: 1rem 1.5rem;
+ white-space: pre; }
+
+
+/* Tables
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+th,
+td {
+ padding: 12px 15px;
+ text-align: left;
+ border-bottom: 1px solid #E1E1E1; }
+th:first-child,
+td:first-child {
+ padding-left: 0; }
+th:last-child,
+td:last-child {
+ padding-right: 0; }
+
+
+/* Spacing
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+button,
+.button {
+ margin-bottom: 1rem; }
+input,
+textarea,
+select,
+fieldset {
+ margin-bottom: 1.5rem; }
+pre,
+blockquote,
+dl,
+figure,
+table,
+p,
+ul,
+ol,
+form {
+ margin-bottom: 2.5rem; }
+
+
+/* Utilities
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.u-full-width {
+ width: 100%;
+ box-sizing: border-box; }
+.u-max-full-width {
+ max-width: 100%;
+ box-sizing: border-box; }
+.u-pull-right {
+ float: right; }
+.u-pull-left {
+ float: left; }
+
+
+/* Misc
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+hr {
+ margin-top: 3rem;
+ margin-bottom: 3.5rem;
+ border-width: 0;
+ border-top: 1px solid #E1E1E1; }
+
+
+/* Clearing
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+
+/* Self Clearing Goodness */
+.container:after,
+.row:after,
+.u-cf {
+ content: "";
+ display: table;
+ clear: both; }
+
+
+/* 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/emily_landi/app/css/module.css b/emily_landi/app/css/module.css
new file mode 100644
index 0000000..d220e4a
--- /dev/null
+++ b/emily_landi/app/css/module.css
@@ -0,0 +1,9 @@
+/*Modules are the reusable, modular parts of our design. They are the callouts, the sidebar sections, the product lists and so on.*/
+
+input {
+ background-color: white;
+}
+button {
+ color: white;
+ background-color: lightblue;
+}
diff --git a/emily_landi/app/css/reset.css b/emily_landi/app/css/reset.css
new file mode 100644
index 0000000..5e5e3c8
--- /dev/null
+++ b/emily_landi/app/css/reset.css
@@ -0,0 +1,424 @@
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS and IE text size adjust after device 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/10/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 of focused elements when they are also in an
+ * active/hover state.
+ */
+
+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 {
+ 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.
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ box-sizing: content-box; /* 2 */
+}
+
+/**
+ * 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/emily_landi/app/img/book.png b/emily_landi/app/img/book.png
new file mode 100644
index 0000000..a155b06
Binary files /dev/null and b/emily_landi/app/img/book.png differ
diff --git a/emily_landi/app/index.html b/emily_landi/app/index.html
new file mode 100644
index 0000000..4f7120f
--- /dev/null
+++ b/emily_landi/app/index.html
@@ -0,0 +1,76 @@
+
+
+
+
+ The Bookstore
+
+
+
+
+
+
+
+ Get Smart. Read.
+
+ Slider bootstrap javascript page dom page speed jQuery. App dom SQL FTP Django sql javascript angular page html dom python TCP page speed bootstrap tablet markdown ember css puppet css jQuery javascript c# yaml. Slider FTP python page puppet TCP. Wireshark page speed javascript tablet yaml dom. Swift yaml sql dom hosting bootstrap page html markdown tablet FTP ember angular dom Django javascript python SQL puppet javascript jQuery TCP c#.
+
+
+ Book List:
+ -
+ {{book.title}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/emily_landi/app/js/books/books.js b/emily_landi/app/js/books/books.js
new file mode 100644
index 0000000..d04708d
--- /dev/null
+++ b/emily_landi/app/js/books/books.js
@@ -0,0 +1,3 @@
+module.exports = function(app) {
+ require('./controllers/books_controller')(app);
+};
diff --git a/emily_landi/app/js/books/controllers/books_controller.js b/emily_landi/app/js/books/controllers/books_controller.js
new file mode 100644
index 0000000..9cee238
--- /dev/null
+++ b/emily_landi/app/js/books/controllers/books_controller.js
@@ -0,0 +1,64 @@
+var angular = window.angular;
+module.exports = function(app) {
+ app.controller('BooksController', ['$scope', '$http', 'cfResource', function($scope, $http, cfResource) {
+ $scope.books = [];
+ $scope.defaults = {rating: 'Excellent'};
+ $scope.newBook = angular.copy($scope.defaults);
+ $scope.orig = {};
+ var booksResource = cfResource('books');
+
+ $scope.getAll = function() {
+ booksResource.getAll(function(err, data) {
+ if (err) return err;
+ $scope.books = data;
+ });
+ };
+
+ $scope.create = function(book) {
+ booksResource.create(book, function(err, data) {
+ if (err) return err;
+ $scope.books.push(data);
+ $scope.newBook = angular.copy($scope.defaults);
+ });
+ };
+
+ $scope.update = function(book) {
+ book.editing = false;
+ $http.put('/api/books/' + book._id, book)
+ .then(function(res) {
+ }, function(res) {
+ console.log(err.data);
+ });
+ };
+
+ $scope.edit = function(book) {
+ $scope.orig.title = book.title;
+ $scope.orig.author = book.author;
+ $scope.orig.pages = book.pages;
+ $scope.orig.rating = book.rating;
+ book.editing = true;
+ console.log('Edit saved.');
+ };
+
+ $scope.cancelEdit = function(book) {
+ book.title = $scope.orig.title;
+ book.author = $scope.orig.author;
+ book.pages = $scope.orig.pages;
+ book.rating = $scope.orig.rating;
+ book.editing = false;
+ console.log('Edit cancelled.');
+ };
+
+ $scope.delete = function(book) {
+ $scope.books.splice($scope.books.indexOf(book), 1);
+ $http.delete('/api/books/' + book._id)
+ .then(function(res) {
+ console.log('This book has been deleted.');
+ }, function(res) {
+ console.log(err.data);
+ $scope.getAll();
+ });
+ };
+ }]);
+};
+
diff --git a/emily_landi/app/js/controllers/controllers.js b/emily_landi/app/js/controllers/controllers.js
new file mode 100644
index 0000000..30bc759
--- /dev/null
+++ b/emily_landi/app/js/controllers/controllers.js
@@ -0,0 +1,5 @@
+module.exports = function(app) {
+ app.controller('Controller', ['$scope', function($scope) {
+
+ }]);
+};
diff --git a/emily_landi/app/js/entry.js b/emily_landi/app/js/entry.js
new file mode 100644
index 0000000..fd6cf94
--- /dev/null
+++ b/emily_landi/app/js/entry.js
@@ -0,0 +1,7 @@
+require('angular/angular');
+var angular = window.angular;
+
+var bookstoreApp = angular.module('BookstoreApp', []);
+require('./services/services')(bookstoreApp);
+require('./controllers/controllers')(bookstoreApp);
+require('./books/books')(bookstoreApp);
diff --git a/emily_landi/app/js/services/cf_resource.js b/emily_landi/app/js/services/cf_resource.js
new file mode 100644
index 0000000..cd35f06
--- /dev/null
+++ b/emily_landi/app/js/services/cf_resource.js
@@ -0,0 +1,29 @@
+var handleSuccess = function(callback) {
+ return function(res) {
+ callback(null, res.data);
+ };
+};
+
+var handleFail = function(callback) {
+ return function(res) {
+ callback(res.data);
+ }
+};
+
+module.exports = function(app) {
+ app.factory('cfResource', ['$http', function($http) {
+ return function(resourceName) {
+ var resource = {}
+ resource.getAll = function(callback) {
+ $http.get('/api/' + resourceName)
+ .then(handleSuccess(callback), handleFail(callback));
+ };
+
+ resource.create = function(data, callback) {
+ $http.post('/api/' + resourceName, data)
+ .then(handleSuccess(callback), handleFail(callback));
+ };
+ return resource;
+ };
+ }]);
+};
diff --git a/emily_landi/app/js/services/services.js b/emily_landi/app/js/services/services.js
new file mode 100644
index 0000000..d43501e
--- /dev/null
+++ b/emily_landi/app/js/services/services.js
@@ -0,0 +1,3 @@
+module.exports = function(app) {
+ require('./cf_resource')(app);
+};
diff --git a/emily_landi/app/sass/partials/_base.scss b/emily_landi/app/sass/partials/_base.scss
new file mode 100644
index 0000000..8096d35
--- /dev/null
+++ b/emily_landi/app/sass/partials/_base.scss
@@ -0,0 +1,60 @@
+/* Setting variables, including color manipulation with percentages */
+
+$primary-color: black;
+$secondary-color: white;
+$tertiary-color: darkgray;
+$accent-color: aqua;
+$shadow-color: darken($accent-color, 15%);
+
+body {
+ font-family: 'Georgia', 'Arial';
+ font-size: 1em;
+ background-color: $tertiary-color;
+ color: $primary-color;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+header {
+ h1 {
+ text-align: center;
+ margin: 0;
+ width: 100%;
+ height: 2.5em;
+ background-color: $primary-color;
+ color: $secondary-color;
+ text-shadow: 3px 3px $shadow-color;
+ font-size: 4.5em;
+ }
+}
+#listHead {
+ background-color: $accent-color;
+ padding: .5em;
+}
+li {
+ list-style: none;
+}
+span {
+ display: block;
+}
+#addBookForm {
+ padding: 1em;
+ border: 0.5em solid $accent-color;
+ width: 250px;
+ @media (max-width: 800px) {
+ text-align: center;
+ float: right;
+ }
+}
+img {
+ height: 40%;
+ width: 40%;
+ padding-left: 30%;
+}
+footer {
+ margin-top: 1em;
+ padding: 0.75em;
+ background-color: $primary-color;
+ color: $secondary-color;
+ font-size: 1em;
+ text-align: center;
+}
diff --git a/emily_landi/app/sass/partials/_layout.scss b/emily_landi/app/sass/partials/_layout.scss
new file mode 100644
index 0000000..fa0c973
--- /dev/null
+++ b/emily_landi/app/sass/partials/_layout.scss
@@ -0,0 +1,94 @@
+/*
+* 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
+*/
+
+/* 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: 770px) {
+ .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%; }
+
+}
diff --git a/emily_landi/app/sass/partials/_module.scss b/emily_landi/app/sass/partials/_module.scss
new file mode 100644
index 0000000..eb05362
--- /dev/null
+++ b/emily_landi/app/sass/partials/_module.scss
@@ -0,0 +1,37 @@
+input {
+ height: 38px;
+ padding: 6px 10px;
+ background-color: white;
+ border: 1px solid #D1D1D1;
+ border-radius: 4px;
+}
+label {
+ display: block;
+ margin-bottom: .5rem;
+ font-weight: 600;
+}
+button {
+ color: white;
+ background-color: lightblue;
+ display: inline-block;
+ height: 38px;
+ padding: 0 10px;
+ text-align: center;
+ text-transform: uppercase;
+ border-radius: 4px;
+ border: 1px solid #bbb;
+}
+#bookList {
+ background-color: rgba(255, 255, 255, .1);
+ border-radius: 30px;
+ padding: 1%;
+ @media (max-width: 800px) {
+ button {
+ display: block;
+ }
+ }
+}
+.book {
+ line-height: 2em;
+ margin: 8px;
+}
diff --git a/emily_landi/app/sass/partials/_reset.scss b/emily_landi/app/sass/partials/_reset.scss
new file mode 100644
index 0000000..458eea1
--- /dev/null
+++ b/emily_landi/app/sass/partials/_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/emily_landi/app/sass/styles.scss b/emily_landi/app/sass/styles.scss
new file mode 100644
index 0000000..da75b43
--- /dev/null
+++ b/emily_landi/app/sass/styles.scss
@@ -0,0 +1,4 @@
+@import 'partials/reset';
+@import 'partials/base';
+@import 'partials/layout';
+@import 'partials/module';
diff --git a/emily_landi/gulpfile.js b/emily_landi/gulpfile.js
new file mode 100644
index 0000000..8e09730
--- /dev/null
+++ b/emily_landi/gulpfile.js
@@ -0,0 +1,72 @@
+var gulp = require('gulp');
+var webpack = require('webpack-stream');
+var minifyCss = require('gulp-minify-css');
+var watch = require('gulp-watch');
+var sass = require('gulp-sass');
+var maps = require('gulp-sourcemaps');
+
+gulp.task('static:dev', function() {
+ gulp.src('app/**/*.html')
+ .pipe(gulp.dest('build/'));
+});
+
+gulp.task('webpack:dev', function() {
+ return gulp.src('app/js/entry.js')
+ .pipe(webpack({
+ output: {
+ filename: 'bundle.js'
+ }
+ }))
+ .pipe(gulp.dest('build/'));
+});
+
+gulp.task('sass:dev', function() {
+ gulp.src('./app/sass/styles.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('img:dev', function() {
+ gulp.src(['app/img/*.jpeg', 'app/img/*.jpg', 'app/img/*.png'])
+ .pipe(gulp.dest('build/'));
+});
+
+gulp.task('webpack:test', function() {
+ return gulp.src('test/client/test_entry.js')
+ .pipe(webpack({
+ output: {
+ filename: 'test_bundle.js'
+ }
+ }))
+ .pipe(gulp.dest('test/client/'));
+});
+
+gulp.task('jshint', function() {
+ return gulp.src(appFiles)
+ .pipe(jshint({
+ node: true,
+ globals: {
+ describe: true,
+ it: true,
+ before: true,
+ after: true,
+ }
+ }))
+ .pipe(jshint.reporter('default'));
+});
+
+gulp.task('mocha', function() {
+ return gulp.src(testFiles, {read: false})
+ .pipe(mocha({reporter: 'landing'}));
+});
+
+gulp.task('build:dev', ['webpack:dev', 'static:dev', 'sass:dev', 'img:dev']);
+gulp.task('default', ['build:dev']);
+
diff --git a/emily_landi/karma.conf.js b/emily_landi/karma.conf.js
new file mode 100644
index 0000000..c13fdf0
--- /dev/null
+++ b/emily_landi/karma.conf.js
@@ -0,0 +1,69 @@
+// Karma configuration
+// Generated on Wed Dec 02 2015 21:24:47 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: [],
+
+
+ // 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/emily_landi/lib/basic_http_auth.js b/emily_landi/lib/basic_http_auth.js
new file mode 100644
index 0000000..c5b5e5d
--- /dev/null
+++ b/emily_landi/lib/basic_http_auth.js
@@ -0,0 +1,16 @@
+module.exports = function(req, res, next) {
+ var pW = (req.headers.authorization || ':').split(' ')[1];
+ var pWBuffer = new Buffer(pW, 'base64');
+ var pWSplit = pWBuffer.toString('utf8').split(':');
+ req.auth = {
+ username: pWSplit[0],
+ password: pWSplit[1]
+ };
+
+ if (!(req.auth.username && req.auth.password)) {
+ console.log('No basic Auth provided.');
+ return res.status(401).json({msg: 'No basic Auth provided.'});
+ };
+
+ next();
+};
diff --git a/emily_landi/lib/eat_auth.js b/emily_landi/lib/eat_auth.js
new file mode 100644
index 0000000..1a23543
--- /dev/null
+++ b/emily_landi/lib/eat_auth.js
@@ -0,0 +1,33 @@
+var eat = require('eat');
+var User = require(__dirname + '/../models/auth');
+
+module.exports = exports = function(req, res, next) {
+ var token = req.headers.token || ((req.body) ? req.body.token : undefined);
+
+ if (!token) {
+ console.log('No token.');
+ return res.status(401).json({msg: 'No token.'});
+ }
+
+ eat.decode(token, process.env.APP_SECRET, function(err, decoded) {
+ if (err) {
+ console.log(err);
+ return res.status(401).json({msg: 'Access denied.'});
+ }
+
+ User.findOne({_id: decoded.id}, function(err, user) {
+ if (err) {
+ console.log(err);
+ return res.status(401).json({msg: 'Access denied.'});
+ }
+
+ if (!user) {
+ console.log(err);
+ return res.status(401).json({msg: 'Access denied.'});
+ }
+
+ req.user = user;
+ next();
+ });
+ });
+};
diff --git a/emily_landi/lib/handleError.js b/emily_landi/lib/handleError.js
new file mode 100644
index 0000000..8b2031a
--- /dev/null
+++ b/emily_landi/lib/handleError.js
@@ -0,0 +1,4 @@
+module.exports = function(err, res) {
+ console.log(err);
+ res.status(500).json({msg: 'Server Error'});
+};
diff --git a/emily_landi/models/auth.js b/emily_landi/models/auth.js
new file mode 100644
index 0000000..d09631c
--- /dev/null
+++ b/emily_landi/models/auth.js
@@ -0,0 +1,27 @@
+var mongoose = require('mongoose');
+var bcrypt = require('bcrypt');
+var eat = require('eat');
+
+var userSchema = new mongoose.Schema({
+ username: String,
+ basic: {
+ username: String,
+ password: String
+ }
+});
+
+userSchema.methods.hashPW = function(password) {
+ var hash = this.basic.password = bcrypt.hashSync(password, 8);
+ return hash;
+};
+
+userSchema.methods.checkPW = function(password) {
+ return bcrypt.compareSync(password, this.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/emily_landi/models/book.js b/emily_landi/models/book.js
index e7f605e..a845b58 100644
--- a/emily_landi/models/book.js
+++ b/emily_landi/models/book.js
@@ -3,15 +3,8 @@ var mongoose = require('mongoose');
var bookSchema = new mongoose.Schema({
title: {type: String, required: true},
author: String,
- pages: { type: Number, validate: {
- validator: function(val) {
- Number.max = 500;
- return val == 1;
- },
- message: '{VALUE} is above the maximum number of pages.'
- }
- },
- rating: {type: String, default: 'excellent'}
+ pages: { type: Number, max: 1000 },
+ rating: {type: String, default: 'Excellent'}
});
module.exports = mongoose.model('Book', bookSchema);
diff --git a/emily_landi/package.json b/emily_landi/package.json
index 9150889..254a8b8 100644
--- a/emily_landi/package.json
+++ b/emily_landi/package.json
@@ -21,14 +21,32 @@
},
"homepage": "https://github.com/emlandi/mongo_backed_rest_api#readme",
"dependencies": {
+ "bcrypt": "^0.8.5",
"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",
- "mocha": "^2.3.3"
+ "gulp-concat-css": "^2.2.0",
+ "gulp-jshint": "^2.0.0",
+ "gulp-minify-css": "^1.2.2",
+ "gulp-mocha": "^2.2.0",
+ "gulp-sass": "^2.1.0",
+ "gulp-sourcemaps": "^1.6.0",
+ "gulp-watch": "^4.3.5",
+ "jasmine-core": "^2.3.4",
+ "jshint": "^2.8.0",
+ "karma": "^0.13.15",
+ "karma-jasmine": "^0.3.6",
+ "karma-phantomjs-launcher": "^0.2.1",
+ "mocha": "^2.3.3",
+ "phantomjs": "^1.9.19",
+ "webpack-stream": "^2.1.1"
}
}
diff --git a/emily_landi/routes/auth_routes.js b/emily_landi/routes/auth_routes.js
new file mode 100644
index 0000000..8f5be2b
--- /dev/null
+++ b/emily_landi/routes/auth_routes.js
@@ -0,0 +1,47 @@
+var express = require('express');
+var jsonParser = require('body-parser').json();
+var handleError = require(__dirname + '/../lib/handleError');
+var basicHttp = require(__dirname + '/../lib/basic_http_auth');
+var User = require(__dirname + '/../models/auth');
+var usersRouter = module.exports = exports = express.Router();
+
+usersRouter.post('/signup', jsonParser, function(req, res) {
+ var user = new User();
+ user.basic.username = req.body.username;
+ user.username = req.body.username;
+ user.hashPW(req.body.password);
+
+ user.save(function(err, data) {
+ if (err) return handleError(err, res);
+
+ user.generateToken(function(err, token) {
+ if (err) return handleError(err, res);
+ res.json({token: token});
+ });
+ });
+});
+
+usersRouter.get('/signin', basicHttp, function(req, res) {
+
+ User.findOne({'basic.username': req.auth.username}, function(err, user) {
+ if (err) {
+ console.log('No basic Auth provided.');
+ return res.status(401).json({msg: 'Access denied.'});
+ }
+
+ if (!user) {
+ console.log('No basic Auth provided.');
+ return res.status(401).json({msg: 'Access denied.'});
+ }
+
+ if (!user.checkPW(req.auth.password)) {
+ console.log('No basic Auth provided.');
+ return res.status(401).json({msg: 'Access denied.'});
+ }
+
+ user.generateToken(function(err, token) {
+ if (err) return handleError(err, res);
+ res.json({token: token});
+ });
+ });
+});
diff --git a/emily_landi/routes/books_routes.js b/emily_landi/routes/books_routes.js
index 7920e41..103ecc4 100644
--- a/emily_landi/routes/books_routes.js
+++ b/emily_landi/routes/books_routes.js
@@ -1,14 +1,10 @@
var express = require('express');
var bodyParser = require('body-parser');
var Book = require(__dirname + '/../models/book');
+var handleError = require(__dirname + '/../lib/handleError');
var booksRouter = module.exports = exports = express.Router();
-var handleError = function(err, res, next) {
- console.log(err);
- res.status(500).json({msg: 'server error'});
-};
-
booksRouter.get('/books', function(req, res) {
Book.find({}, function(err, data) {
if (err) return handleError(err, res);
@@ -16,6 +12,13 @@ booksRouter.get('/books', function(req, res) {
});
});
+booksRouter.get('/books/:id', function(req, res) {
+ Book.find({_id: req.params.id}).count(function(err, count) {
+ if (err) return handleError(err, res);
+ res.json({msg: 'The number of books is: ' + count});
+ });
+});
+
booksRouter.post('/books', bodyParser.json(), function(req, res) {
var newBook = new Book(req.body);
newBook.save(function(err, data) {
@@ -40,9 +43,4 @@ booksRouter.delete('/books/:id', function(req, res) {
});
});
-booksRouter.get('/books/:id', function(req, res) {
- Book.find({_id: req.params.id}).count(function(err, count) {
- if (err) return handleError(err, res);
- res.json({msg: 'The number of books is: ' + count});
- });
-});
+
diff --git a/emily_landi/server.js b/emily_landi/server.js
index 9d6c1fc..efe6532 100644
--- a/emily_landi/server.js
+++ b/emily_landi/server.js
@@ -2,11 +2,17 @@ var mongoose = require('mongoose');
var express = require('express');
var app = express();
var booksRouter = require(__dirname + '/routes/books_routes');
+var authRouter = require(__dirname + '/routes/auth_routes');
+process.env.APP_SECRET = process.env.APP_SECRET || 'somethingelse';
mongoose.connect(process.env.MONGOLAB_URI || 'mongodb://localhost/book_stream');
+app.use(express.static(__dirname + '/build/'));
+
app.use('/api', booksRouter);
-app.listen(process.env.PORT || 3000, function() {
- console.log('server up');
+app.use('/api', authRouter);
+
+app.listen(3000, function() {
+ console.log('Server up & running!');
});
diff --git a/emily_landi/test/auth_test.js b/emily_landi/test/auth_test.js
new file mode 100644
index 0000000..0c72f26
--- /dev/null
+++ b/emily_landi/test/auth_test.js
@@ -0,0 +1,77 @@
+var chai = require('chai');
+var chaihttp = require('chai-http');
+chai.use(chaihttp);
+var expect = chai.expect;
+
+process.env.MONGOLAB_URI = 'mongodb://localhost/book_test';
+require(__dirname + '/../server');
+
+var mongoose = require('mongoose');
+var eatAuth = require(__dirname + '/../lib/eat_auth');
+var basicHttp = require(__dirname + '/../lib/basic_http_auth');
+var User = require(__dirname + '/../models/auth');
+
+describe('basicHttp', function() {
+ it('should parse basic http auth', function() {
+ var req = {
+ headers: {
+ authorization: 'Basic: ' + new Buffer('testUsername:testPassword').toString('base64')
+ }
+ };
+
+ basicHttp(req, {}, function() {
+ expect(typeof req.auth).to.eql('object');
+ expect(req.auth.username).to.eql('testUsername');
+ expect(req.auth.password).to.eql('testPassword');
+ });
+ });
+});
+
+describe('authorization routes', function() {
+ after(function(done) {
+ mongoose.connection.db.dropDatabase(function() {
+ done();
+ });
+ });
+
+ it('POST route should create a user', function(done) {
+ chai.request('localhost:3000/api')
+ .post('/signup')
+ .send({username: 'testUsername', password: 'testPassword'})
+ .end(function(err, res) {
+ expect(err).to.eql(null);
+ expect(res.body).to.have.property('token');
+ done();
+ });
+ });
+
+ describe('existing user', function() {
+ beforeEach(function(done) {
+ var user = new User();
+ user.username = 'testUsername';
+ user.basic.username = 'testUsername';
+ user.hashPW('testPassword', function(err, res) {
+ if (err) throw err;
+ user.save(function(err, data) {
+ if (err) throw err;
+ user.generateToken(function(err, token) {
+ if (err) throw err;
+ this.token = token;
+ }.bind(this));
+ }.bind(this));
+ }.bind(this));
+ done();
+ });
+
+ it('GET route should sign in existing user', function(done) {
+ chai.request('localhost:3000/api')
+ .get('/signin')
+ .auth('testUsername', 'testPassword')
+ .end(function(err, res) {
+ expect(err).to.eql(null);
+ expect(res.body).to.have.property('token');
+ done();
+ });
+ });
+ });
+});
diff --git a/emily_landi/test/books_test.js b/emily_landi/test/books_test.js
index ccc0d41..4241cb5 100644
--- a/emily_landi/test/books_test.js
+++ b/emily_landi/test/books_test.js
@@ -4,7 +4,7 @@ chai.use(chaihttp);
var expect = chai.expect;
var mongoose = require('mongoose');
-process.env.MONGOLAB_URI = 'mongodb://localhost/book_stream_test';
+process.env.MONGOLAB_URI = 'mongodb://localhost/book_test';
require(__dirname + '/../server');
var Book = require(__dirname + '/../models/book');
@@ -36,20 +36,20 @@ describe('book routes', function() {
.send(addBook)
.end(function(err, res) {
expect(err).to.eql(null);
- expect(res.body.msg).to.eql('server error');
+ expect(res.body.msg).to.eql('Server Error');
expect(res.body).to.not.have.property('_id');
done();
});
});
- it('should not add book over 500 pages', function(done) {
- var addBook = {title: 'test', pages: 555};
+ it('should not add book over 1000 pages', function(done) {
+ var addBook = {title: 'test', pages: 1001};
chai.request('localhost:3000')
.post('/api/books')
.send(addBook)
.end(function(err, res) {
expect(err).to.eql(null);
- expect(res.body.msg).to.eql('server error');
+ expect(res.body.msg).to.eql('Server Error');
expect(res.body).to.not.have.property('_id');
done();
});
diff --git a/emily_landi/test/client/books_controller_test.js b/emily_landi/test/client/books_controller_test.js
new file mode 100644
index 0000000..c4f062b
--- /dev/null
+++ b/emily_landi/test/client/books_controller_test.js
@@ -0,0 +1,87 @@
+require(__dirname + '/../../app/js/entry');
+require('angular-mocks');
+
+describe('books controller', function() {
+ var $httpBackend;
+ var $ControllerConstructor;
+ var $scope;
+
+ beforeEach(angular.mock.module('BookstoreApp'));
+
+ beforeEach(angular.mock.inject(function($rootScope, $controller) {
+ $scope = $rootScope.$new();
+ $ControllerConstructor = $controller;
+ }));
+
+ it('should be able to create a controller', function() {
+ var controller = $ControllerConstructor('BooksController', {$scope: $scope});
+ expect(typeof $scope).toBe('object');
+ expect(typeof controller).toBe('object');
+ expect(Array.isArray($scope.books)).toBe(true);
+ });
+
+ describe('REST request functions', function() {
+ beforeEach(angular.mock.inject(function(_$httpBackend_, $rootScope) {
+ $httpBackend = _$httpBackend_;
+ $scope = $rootScope.$new();
+ $ControllerConstructor('BooksController', {$scope: $scope});
+ }));
+
+ afterEach(function() {
+ $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+//GET
+ it('should add an array to books with getAll()', function() {
+ $httpBackend.expectGET('/api/books').respond(200, [{_id: 1, title: 'test title'}]);
+ $scope.getAll();
+ $httpBackend.flush();
+ expect($scope.books[0].title).toBe('test title');
+ });
+
+//POST
+ it('should be able to create a new book', function() {
+ $httpBackend.expectPOST('/api/books', {title: 'test title', rating: 'Excellent'}).respond(200, {title: 'newBook title'});
+
+ expect($scope.books.length).toBe(0);
+ expect($scope.newBook).toEqual($scope.defaults);
+
+ $scope.newBook.title = 'test title';
+ $scope.create($scope.newBook);
+ $httpBackend.flush();
+
+ expect($scope.books[0].title).toBe('newBook title');
+ expect($scope.newBook).toEqual($scope.defaults);
+ });
+
+//PUT
+ it('should modify a book with update()', function() {
+ $scope.books[0] = {title: 'edited title', rating: 'good', _id: 1};
+
+ $httpBackend.expectPUT('/api/books/1', $scope.books[0]).respond(200);
+ $scope.update($scope.books[0]);
+ $httpBackend.flush();
+
+ expect($scope.books[0].title).toBe('edited title');
+ expect($scope.books[0]._id).toBe(1);
+ expect($scope.books[0].editing).toBe(false);
+ });
+
+//DELETE
+ it('should remove a book with delete()', function() {
+ $scope.books[0] = {title: 'title1', author: 'author1', pages: '100', rating: 'good', _id: 1};
+ $scope.books[1] = {title: 'title2', author: 'author2', pages: '200', rating: 'great', _id: 2};
+
+ $httpBackend.expectDELETE('/api/books/1').respond(200);
+ $scope.delete($scope.books[0]);
+ $httpBackend.flush();
+
+ expect($scope.books[0].title).toBe('title2');
+ expect($scope.books[0].author).toBe('author2');
+ expect($scope.books[0].pages).toBe('200');
+ expect($scope.books[0].rating).toBe('great');
+ expect($scope.books[0]._id).toBe(2);
+ });
+ });
+});
diff --git a/emily_landi/test/client/test_entry.js b/emily_landi/test/client/test_entry.js
new file mode 100644
index 0000000..ca85527
--- /dev/null
+++ b/emily_landi/test/client/test_entry.js
@@ -0,0 +1 @@
+require('./books_controller_test');