├── images └── .keep ├── bin ├── setup └── mkapplink.js ├── public ├── d41d8cd98f00b204e9800998ecf8427e.woff2 ├── favicon.ico ├── 3293616ec0c605c7c2db25829a0a509e.woff ├── 448c34a56d699c29117adc64c43affeb.woff2 ├── 674f50d287a8c48dc19ba404d20fe713.eot ├── 8b27bc96115c2d24350f0d09e6a9433f.eot ├── af7ae505a9eed503f8b8e6982036873e.woff2 ├── b06871f281fee6b241d60582ae9369b9.ttf ├── dcb26c7239d850266941e80370e207c1.ttf ├── e18bbf611f2a2e43afc071aa2f4e1512.ttf ├── f4769f9bdb7466be65088239c12046d1.eot ├── fa2772327f55d8198301fdb8bcfc8158.woff ├── fee66e712a8a08eef5805a46892932ad.woff └── index.html ├── .DS_Store ├── .profile ├── fonts ├── .DS_Store └── bootstrap │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── stylesheets ├── .DS_Store ├── fonts │ ├── .DS_Store │ ├── FontAwesome.otf │ ├── flexslider-icon.eot │ ├── flexslider-icon.ttf │ ├── flexslider-icon.woff │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── bootstrap │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── flexslider-icon.svg ├── font-awesome │ ├── .DS_Store │ └── scss │ │ ├── _fixed-width.scss │ │ ├── _screen-reader.scss │ │ ├── _larger.scss │ │ ├── _list.scss │ │ ├── _core.scss │ │ ├── font-awesome.scss │ │ ├── _stacked.scss │ │ ├── _bordered-pulled.scss │ │ ├── _rotated-flipped.scss │ │ ├── _path.scss │ │ ├── _animated.scss │ │ └── _mixins.scss ├── footer.scss ├── bootstrap │ ├── mixins │ │ ├── _center-block.scss │ │ ├── _opacity.scss │ │ ├── _size.scss │ │ ├── _text-overflow.scss │ │ ├── _resize.scss │ │ ├── _labels.scss │ │ ├── _progress-bar.scss │ │ ├── _text-emphasis.scss │ │ ├── _reset-filter.scss │ │ ├── _nav-divider.scss │ │ ├── _background-variant.scss │ │ ├── _alerts.scss │ │ ├── _tab-focus.scss │ │ ├── _nav-vertical-align.scss │ │ ├── _reset-text.scss │ │ ├── _border-radius.scss │ │ ├── _pagination.scss │ │ ├── _responsive-visibility.scss │ │ ├── _panels.scss │ │ ├── _hide-text.scss │ │ ├── _clearfix.scss │ │ ├── _list-group.scss │ │ ├── _table-row.scss │ │ ├── _image.scss │ │ ├── _buttons.scss │ │ ├── _grid-framework.scss │ │ └── _forms.scss │ ├── _wells.scss │ ├── _responsive-embed.scss │ ├── _breadcrumbs.scss │ ├── _close.scss │ ├── _component-animations.scss │ ├── _utilities.scss │ ├── _thumbnails.scss │ ├── _pager.scss │ ├── _mixins.scss │ ├── _media.scss │ ├── _jumbotron.scss │ ├── _labels.scss │ ├── _badges.scss │ ├── _code.scss │ ├── _grid.scss │ ├── _alerts.scss │ ├── _progress-bars.scss │ ├── _pagination.scss │ ├── _print.scss │ ├── _tooltip.scss │ └── _list-group.scss ├── _bootstrap-sprockets.scss ├── _bootstrap-compass.scss ├── particles.scss ├── style.scss ├── main-display.scss ├── _bootstrap-mincer.scss ├── _bootstrap.scss └── general.scss ├── .babelrc ├── .gitignore ├── db ├── models │ ├── orders.js │ ├── selectedBooks.js │ ├── review.js │ ├── index.js │ ├── book.js │ ├── oauth.js │ └── user.js └── index.js ├── app ├── review │ ├── reviewListContainer.js │ ├── singleReviewContainer.js │ ├── reviewListComponent.js │ ├── reviewReducer.js │ ├── singleReviewComponent.js │ ├── reviewActionCreator.js │ ├── newReviewForm.js │ └── newReviewFormContainer.js ├── order │ ├── orderListContainer.js │ ├── ShoppingCartContainer.js │ ├── singleOrderContainer.js │ ├── order-reducer.js │ ├── singleOrder.js │ ├── orderList.js │ └── ShoppingCartComponent.js ├── book │ ├── genresContainer.js │ ├── singleBookContainer.js │ ├── authorsContainer.js │ ├── bookListContainer.js │ ├── SelectedAuthorsContainer.js │ ├── singleBookReview.js │ ├── book-reducer.js │ ├── SelectedAuthorsComponent.js │ ├── bookListComponent.js │ ├── authorsComponent.js │ ├── newBookFormContainer.js │ ├── book-actions.js │ ├── genresComponent.js │ ├── newBookForm.js │ └── singleBookComponent.js ├── store.jsx ├── footer │ └── footerComponent.js ├── user │ ├── allUserContainer.js │ ├── singleUserComponent.js │ ├── singleUserContainer.js │ ├── user-reducer.js │ ├── allUserComponent.js │ └── user-actions.js ├── rootReducer.js ├── auth │ ├── components │ │ ├── WhoAmI.jsx │ │ ├── Login.jsx │ │ ├── newUserContainer.js │ │ └── newUserComponent.js │ └── reducers │ │ └── auth.jsx ├── routes.js ├── app.jsx ├── navbar │ ├── loginModal.js │ └── index.js └── dummy-data │ └── particles.data.js ├── server ├── api.js ├── auth.filters.js ├── reviews.js ├── users.js ├── book.js ├── start.js └── orders.js ├── tests ├── singleOrder.test.js ├── user-actions.test.js ├── usermodel.test.js ├── singleUserComponent.test.js ├── allUserComponent.test.js ├── bookListComponent.test.js ├── ordersmodel.test.js ├── singleBookComponent.test.js ├── books-actions.test.js ├── orderList.test.js ├── order-actions.test.js ├── reviewListComponent.test.js ├── reviewActionCreator.test.js ├── reviewReducer.test.js ├── singleReviewComponent.test.js ├── selectedBooks.test.js ├── users.test.js ├── user-reducer.test.js ├── WhoAmI.test.jsx ├── order-reducer.test.js ├── Login.test.jsx ├── reviews.test.js ├── orders.test.js ├── bookmodel.test.js ├── books-reducer.test.js ├── auth.test.js └── book.test.js ├── webpack.config.js ├── index.js ├── README.md └── package.json /images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | mkapplink.js -------------------------------------------------------------------------------- /public/d41d8cd98f00b204e9800998ecf8427e.woff2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/.DS_Store -------------------------------------------------------------------------------- /.profile: -------------------------------------------------------------------------------- 1 | echo 'in the .profile!' 2 | 3 | npm install -dev 4 | npm run build 5 | -------------------------------------------------------------------------------- /fonts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/fonts/.DS_Store -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /stylesheets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-2" 6 | ] 7 | } -------------------------------------------------------------------------------- /stylesheets/fonts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/.DS_Store -------------------------------------------------------------------------------- /stylesheets/font-awesome/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/font-awesome/.DS_Store -------------------------------------------------------------------------------- /stylesheets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /stylesheets/fonts/flexslider-icon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/flexslider-icon.eot -------------------------------------------------------------------------------- /stylesheets/fonts/flexslider-icon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/flexslider-icon.ttf -------------------------------------------------------------------------------- /stylesheets/fonts/flexslider-icon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/flexslider-icon.woff -------------------------------------------------------------------------------- /stylesheets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /stylesheets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /stylesheets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/3293616ec0c605c7c2db25829a0a509e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/3293616ec0c605c7c2db25829a0a509e.woff -------------------------------------------------------------------------------- /public/448c34a56d699c29117adc64c43affeb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/448c34a56d699c29117adc64c43affeb.woff2 -------------------------------------------------------------------------------- /public/674f50d287a8c48dc19ba404d20fe713.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/674f50d287a8c48dc19ba404d20fe713.eot -------------------------------------------------------------------------------- /public/8b27bc96115c2d24350f0d09e6a9433f.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/8b27bc96115c2d24350f0d09e6a9433f.eot -------------------------------------------------------------------------------- /public/af7ae505a9eed503f8b8e6982036873e.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/af7ae505a9eed503f8b8e6982036873e.woff2 -------------------------------------------------------------------------------- /public/b06871f281fee6b241d60582ae9369b9.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/b06871f281fee6b241d60582ae9369b9.ttf -------------------------------------------------------------------------------- /public/dcb26c7239d850266941e80370e207c1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/dcb26c7239d850266941e80370e207c1.ttf -------------------------------------------------------------------------------- /public/e18bbf611f2a2e43afc071aa2f4e1512.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/e18bbf611f2a2e43afc071aa2f4e1512.ttf -------------------------------------------------------------------------------- /public/f4769f9bdb7466be65088239c12046d1.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/f4769f9bdb7466be65088239c12046d1.eot -------------------------------------------------------------------------------- /public/fa2772327f55d8198301fdb8bcfc8158.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/fa2772327f55d8198301fdb8bcfc8158.woff -------------------------------------------------------------------------------- /public/fee66e712a8a08eef5805a46892932ad.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/public/fee66e712a8a08eef5805a46892932ad.woff -------------------------------------------------------------------------------- /stylesheets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /fonts/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/fonts/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /fonts/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/fonts/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /fonts/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/fonts/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /fonts/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/fonts/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /stylesheets/footer.scss: -------------------------------------------------------------------------------- 1 | #footer { 2 | height: $footer-height; 3 | padding: 1rem; 4 | background-color: $primary-color; 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /stylesheets/fonts/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /stylesheets/fonts/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /stylesheets/fonts/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /stylesheets/fonts/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbwheeler/Grace-Reader/HEAD/stylesheets/fonts/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_center-block.scss: -------------------------------------------------------------------------------- 1 | // Center-align a block level element 2 | 3 | @mixin center-block() { 4 | display: block; 5 | margin-left: auto; 6 | margin-right: auto; 7 | } 8 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_opacity.scss: -------------------------------------------------------------------------------- 1 | // Opacity 2 | 3 | @mixin opacity($opacity) { 4 | opacity: $opacity; 5 | // IE8 filter 6 | $opacity-ie: ($opacity * 100); 7 | filter: alpha(opacity=$opacity-ie); 8 | } 9 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_size.scss: -------------------------------------------------------------------------------- 1 | // Sizing shortcuts 2 | 3 | @mixin size($width, $height) { 4 | width: $width; 5 | height: $height; 6 | } 7 | 8 | @mixin square($size) { 9 | @include size($size, $size); 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all node_modules 2 | node_modules/* 3 | 4 | # ...except the symlink to ourselves. 5 | !node_modules/APP 6 | 7 | # Compiled JS 8 | public/bundle.js 9 | public/bundle.js.map 10 | 11 | # NPM errors 12 | npm-debug.log -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_text-overflow.scss: -------------------------------------------------------------------------------- 1 | // Text overflow 2 | // Requires inline-block or block for proper styling 3 | 4 | @mixin text-overflow() { 5 | overflow: hidden; 6 | text-overflow: ellipsis; 7 | white-space: nowrap; 8 | } 9 | -------------------------------------------------------------------------------- /stylesheets/_bootstrap-sprockets.scss: -------------------------------------------------------------------------------- 1 | @function twbs-font-path($path) { 2 | @return font-path($path); 3 | } 4 | 5 | @function twbs-image-path($path) { 6 | @return image-path($path); 7 | } 8 | 9 | $bootstrap-sass-asset-helper: true; 10 | -------------------------------------------------------------------------------- /stylesheets/_bootstrap-compass.scss: -------------------------------------------------------------------------------- 1 | @function twbs-font-path($path) { 2 | @return font-url($path, true); 3 | } 4 | 5 | @function twbs-image-path($path) { 6 | @return image-url($path, true); 7 | } 8 | 9 | $bootstrap-sass-asset-helper: true; 10 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_resize.scss: -------------------------------------------------------------------------------- 1 | // Resize anything 2 | 3 | @mixin resizable($direction) { 4 | resize: $direction; // Options: horizontal, vertical, both 5 | overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible` 6 | } 7 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_labels.scss: -------------------------------------------------------------------------------- 1 | // Labels 2 | 3 | @mixin label-variant($color) { 4 | background-color: $color; 5 | 6 | &[href] { 7 | &:hover, 8 | &:focus { 9 | background-color: darken($color, 10%); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_progress-bar.scss: -------------------------------------------------------------------------------- 1 | // Progress bars 2 | 3 | @mixin progress-bar-variant($color) { 4 | background-color: $color; 5 | 6 | // Deprecated parent class requirement as of v3.2.0 7 | .progress-striped & { 8 | @include gradient-striped; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_text-emphasis.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | 3 | // [converter] $parent hack 4 | @mixin text-emphasis-variant($parent, $color) { 5 | #{$parent} { 6 | color: $color; 7 | } 8 | a#{$parent}:hover, 9 | a#{$parent}:focus { 10 | color: darken($color, 10%); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_reset-filter.scss: -------------------------------------------------------------------------------- 1 | // Reset filters for IE 2 | // 3 | // When you need to remove a gradient background, do not forget to use this to reset 4 | // the IE filter for IE9 and below. 5 | 6 | @mixin reset-filter() { 7 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 8 | } 9 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_nav-divider.scss: -------------------------------------------------------------------------------- 1 | // Horizontal dividers 2 | // 3 | // Dividers (basically an hr) within dropdowns and nav lists 4 | 5 | @mixin nav-divider($color: #e5e5e5) { 6 | height: 1px; 7 | margin: (($line-height-computed / 2) - 1) 0; 8 | overflow: hidden; 9 | background-color: $color; 10 | } 11 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_background-variant.scss: -------------------------------------------------------------------------------- 1 | // Contextual backgrounds 2 | 3 | // [converter] $parent hack 4 | @mixin bg-variant($parent, $color) { 5 | #{$parent} { 6 | background-color: $color; 7 | } 8 | a#{$parent}:hover, 9 | a#{$parent}:focus { 10 | background-color: darken($color, 10%); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /db/models/orders.js: -------------------------------------------------------------------------------- 1 | const db = require('APP/db'); 2 | const Sequelize = require('sequelize'); 3 | 4 | const Orders = db.define('orders', { 5 | total: { 6 | type: Sequelize.FLOAT, 7 | defaultValue: 0 8 | }, 9 | isCart: { 10 | type: Sequelize.BOOLEAN, 11 | defaultValue: true 12 | } 13 | }) 14 | 15 | module.exports = Orders; 16 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /stylesheets/particles.scss: -------------------------------------------------------------------------------- 1 | .particles-js-canvas-el { 2 | position: fixed; 3 | width: 100%; 4 | height: 100%; 5 | z-index: -1; 6 | top: 0; 7 | margin-bottom: -$footer-height - '60px'; 8 | left: 0; 9 | right: 0; 10 | bottom: 0; 11 | } 12 | 13 | #particles { 14 | position: fixed; 15 | z-index: -1; 16 | } 17 | -------------------------------------------------------------------------------- /app/review/reviewListContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReviewListComponent from './reviewListComponent'; 4 | 5 | const mapStateToProps = (state, getState) => { 6 | return { 7 | reviews: state.reviews.allReviews 8 | } 9 | } 10 | 11 | export default connect(mapStateToProps)(ReviewListComponent) 12 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_alerts.scss: -------------------------------------------------------------------------------- 1 | // Alerts 2 | 3 | @mixin alert-variant($background, $border, $text-color) { 4 | background-color: $background; 5 | border-color: $border; 6 | color: $text-color; 7 | 8 | hr { 9 | border-top-color: darken($border, 5%); 10 | } 11 | .alert-link { 12 | color: darken($text-color, 10%); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/order/orderListContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import OrderList from './orderList'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | allOrders: state.orders.allOrders 8 | }; 9 | } 10 | 11 | const OrderListContainer = connect(mapStateToProps)(OrderList); 12 | 13 | export default OrderListContainer; 14 | -------------------------------------------------------------------------------- /app/review/singleReviewContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import SingleReviewComponent from './singleReviewComponent'; 4 | 5 | const mapStateToProps = (state, getState) => { 6 | return { 7 | selectedReview: state.reviews.selectedReview 8 | } 9 | } 10 | 11 | export default connect(mapStateToProps)(SingleReviewComponent) 12 | -------------------------------------------------------------------------------- /app/book/genresContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import GenresComponent from './genresComponent'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | allBooks: state.books.allBooks 8 | }; 9 | } 10 | 11 | const GenresContainer = connect(mapStateToProps)(GenresComponent); 12 | 13 | export default GenresContainer; 14 | -------------------------------------------------------------------------------- /app/store.jsx: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import rootReducer from './rootReducer' 3 | import createLogger from 'redux-logger' 4 | import thunkMiddleware from 'redux-thunk' 5 | 6 | const store = createStore(rootReducer, global.__REDUX_DEVTOOLS_EXTENSION__ && global.__REDUX_DEVTOOLS_EXTENSION__(), applyMiddleware(createLogger(), thunkMiddleware)) 7 | 8 | export default store 9 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_tab-focus.scss: -------------------------------------------------------------------------------- 1 | // WebKit-style focus 2 | 3 | @mixin tab-focus() { 4 | // WebKit-specific. Other browsers will keep their default outline style. 5 | // (Initially tried to also force default via `outline: initial`, 6 | // but that seems to erroneously remove the outline in Firefox altogether.) 7 | outline: 5px auto -webkit-focus-ring-color; 8 | outline-offset: -2px; 9 | } 10 | -------------------------------------------------------------------------------- /app/book/singleBookContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SingleBookComponent from './singleBookComponent'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | currentBook: state.books.currentBook 8 | }; 9 | } 10 | 11 | const SingleBookContainer = connect(mapStateToProps)(SingleBookComponent); 12 | 13 | export default SingleBookContainer; 14 | -------------------------------------------------------------------------------- /app/footer/footerComponent.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | export default class Footer extends Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | return ( 10 | 15 | ); 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /db/models/selectedBooks.js: -------------------------------------------------------------------------------- 1 | const db = require('APP/db'); 2 | const Sequelize = require('sequelize'); 3 | 4 | const SelectedBooks = db.define('selectedBooks', { 5 | quantity: { 6 | type: Sequelize.INTEGER, 7 | defaultValue: 1 8 | } 9 | }, { 10 | instanceMethods: { 11 | incrementQuantity: function() { 12 | return this.quantity + 1; 13 | } 14 | } 15 | }); 16 | 17 | module.exports = SelectedBooks; 18 | -------------------------------------------------------------------------------- /app/book/authorsContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import AuthorsComponent from './authorsComponent'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | allBooks: state.books.allBooks, 8 | selectedBooks: state.books.selectedBooks 9 | }; 10 | } 11 | 12 | const AuthorsContainer = connect(mapStateToProps)(AuthorsComponent); 13 | 14 | export default AuthorsContainer; 15 | -------------------------------------------------------------------------------- /app/book/bookListContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import BookListComponent from './bookListComponent'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | allBooks: state.books.allBooks, 8 | genre: state.books.currentGenre 9 | }; 10 | } 11 | 12 | const BookListContainer = connect(mapStateToProps)(BookListComponent); 13 | 14 | export default BookListContainer; 15 | -------------------------------------------------------------------------------- /app/order/ShoppingCartContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ShoppingCartComponent from './ShoppingCartComponent'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | shoppingCart: state.orders.shoppingCart, 8 | user: state.auth 9 | }; 10 | } 11 | 12 | const ShoppingCartContainer = connect(mapStateToProps)(ShoppingCartComponent); 13 | 14 | export default ShoppingCartContainer; 15 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_nav-vertical-align.scss: -------------------------------------------------------------------------------- 1 | // Navbar vertical align 2 | // 3 | // Vertically center elements in the navbar. 4 | // Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. 5 | 6 | @mixin navbar-vertical-align($element-height) { 7 | margin-top: (($navbar-height - $element-height) / 2); 8 | margin-bottom: (($navbar-height - $element-height) / 2); 9 | } 10 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /stylesheets/style.scss: -------------------------------------------------------------------------------- 1 | @import 'normalize.css'; 2 | @import "./font-awesome/scss/font-awesome"; 3 | @import 'general'; 4 | @import './_bootstrap'; 5 | @import 'navigation'; 6 | @import 'main-display'; 7 | @import 'footer'; 8 | @import 'particles.scss'; 9 | 10 | .thumbnail:hover{ 11 | background-color: #FDB608; 12 | } 13 | 14 | #booklisttitle { 15 | margin-left: 10%; 16 | font-weight: 130%; 17 | } 18 | 19 | #rightsidelogin { 20 | margin-left: 85%; 21 | } 22 | -------------------------------------------------------------------------------- /app/user/allUserContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import AllUsersComponent from './AllUsersComponent'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | users: state.users.allUsers 8 | }; 9 | } 10 | 11 | function mapDispatchToProps(state) { 12 | return {}; 13 | } 14 | const AllUsersContainer = connect(mapStateToProps, mapDispatchToProps)(AllUsersComponent); 15 | 16 | export default AllUsersContainer; 17 | -------------------------------------------------------------------------------- /app/book/SelectedAuthorsContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SelectedAuthorsComponent from './SelectedAuthorsComponent'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | allBooks: state.books.allBooks, 8 | selectedBooks: state.books.selectedBooks 9 | }; 10 | } 11 | 12 | const SelectedAuthorsContainer = connect(mapStateToProps)(SelectedAuthorsComponent); 13 | 14 | export default SelectedAuthorsContainer; 15 | -------------------------------------------------------------------------------- /app/order/singleOrderContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SingleOrder from './singleOrder'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | currentOrder: state.orders.currentOrder 8 | }; 9 | } 10 | 11 | function mapDispatchToProps(state) { 12 | return {}; 13 | } 14 | 15 | const SingleOrderContainer = connect(mapStateToProps, mapDispatchToProps)(SingleOrder); 16 | 17 | export default SingleOrderContainer; 18 | -------------------------------------------------------------------------------- /app/user/singleUserComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | 5 | class SingleUserComponent extends Component { 6 | constructor(props){ 7 | super(props) 8 | } 9 | 10 | render() { 11 | const user = this.props.currentUser; 12 | 13 | return ( 14 |
15 |

{user.name}

16 |
17 |
18 |
19 | ) 20 | } 21 | } 22 | 23 | export default SingleUserComponent; 24 | -------------------------------------------------------------------------------- /app/user/singleUserContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SingleUserComponent from './singleUserComponent'; 3 | import store from '../store'; 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | currentUser: state.users.currentUser 8 | }; 9 | } 10 | 11 | function mapDispatchToProps(state) { 12 | return {}; 13 | } 14 | const SingleUserContainer = connect(mapStateToProps, mapDispatchToProps)(SingleUserComponent); 15 | 16 | export default SingleUserContainer; 17 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /db/models/review.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const db = require('APP/db'); 4 | const Sequelize = require('sequelize'); 5 | 6 | const Review = db.define('reviews', { 7 | rating: { 8 | type: Sequelize.FLOAT, 9 | allowNull: false 10 | }, 11 | content: { 12 | type: Sequelize.TEXT, 13 | validate: { 14 | len: { 15 | args: 10, 16 | msg: 'Review must be at least 10 characters in length' 17 | }, 18 | notEmpty: true 19 | } 20 | } 21 | }); 22 | 23 | module.exports = Review; 24 | -------------------------------------------------------------------------------- /stylesheets/main-display.scss: -------------------------------------------------------------------------------- 1 | #mainDisplay { 2 | display:table; 3 | width:100%; 4 | padding-top: 60px; 5 | color: $primary-color; 6 | margin-bottom: -$footer-height; 7 | padding-bottom: $footer-height; 8 | } 9 | 10 | .list-item { 11 | max-width: 200px; 12 | a { 13 | text-decoration: none; 14 | img { 15 | height: 200px 16 | } 17 | div { 18 | height: 60px 19 | } 20 | } 21 | a:hover { 22 | background-color: $primary-color; 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import bookReducer from './book/book-reducer' 3 | import authReducer from './auth/reducers/auth' 4 | import orderReducer from './order/order-reducer' 5 | import reviewReducer from './review/reviewReducer' 6 | import userReducer from './user/user-reducer' 7 | 8 | const rootReducer = combineReducers({ 9 | auth: authReducer, 10 | books: bookReducer, 11 | orders: orderReducer, 12 | reviews: reviewReducer, 13 | users: userReducer 14 | }) 15 | 16 | export default rootReducer 17 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /app/auth/components/WhoAmI.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const WhoAmI = ({ user, logout }) => { 4 | return ( 5 |
6 | {user && `${user.firstName} ${user.lastName}` } 7 | 8 |
9 | ) 10 | } 11 | 12 | import {logout} from 'APP/app/auth/reducers/auth' 13 | import {connect} from 'react-redux' 14 | 15 | export default connect ( 16 | ({ auth }) => ({ user: auth }), 17 | {logout}) (WhoAmI) 18 | -------------------------------------------------------------------------------- /app/auth/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {login} from '../reducers/auth' 3 | import {connect} from 'react-redux' 4 | 5 | export const Login = ({ login }) => ( 6 |
{ 7 | evt.preventDefault() 8 | login(evt.target.username.value, evt.target.password.value) 9 | } }> 10 | 11 | 12 | 13 |
14 | ) 15 | 16 | export default connect ( 17 | state => ({}), 18 | {login} 19 | ) (Login) 20 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_reset-text.scss: -------------------------------------------------------------------------------- 1 | @mixin reset-text() { 2 | font-family: $font-family-base; 3 | // We deliberately do NOT reset font-size. 4 | font-style: normal; 5 | font-weight: normal; 6 | letter-spacing: normal; 7 | line-break: auto; 8 | line-height: $line-height-base; 9 | text-align: left; // Fallback for where `start` is not supported 10 | text-align: start; 11 | text-decoration: none; 12 | text-shadow: none; 13 | text-transform: none; 14 | white-space: normal; 15 | word-break: normal; 16 | word-spacing: normal; 17 | word-wrap: normal; 18 | } 19 | -------------------------------------------------------------------------------- /app/book/singleBookReview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router' 3 | import StarRatingComponent from 'react-star-rating-component' 4 | 5 | const SingleBookReview = ({ review }) => { 6 | return ( 7 |
8 | } 14 | /> 15 | {review.content} 16 |
17 | ) 18 | } 19 | 20 | export default SingleBookReview; 21 | -------------------------------------------------------------------------------- /app/review/reviewListComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SingleReviewComponent from './singleReviewComponent'; 3 | import { Link } from 'react-router'; 4 | 5 | const ReviewListComponent = ({ reviews }) => { 6 | return ( 7 |
8 | { reviews && reviews.map(review => { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | })} 15 |
16 | ) 17 | } 18 | 19 | export default ReviewListComponent; 20 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_border-radius.scss: -------------------------------------------------------------------------------- 1 | // Single side border-radius 2 | 3 | @mixin border-top-radius($radius) { 4 | border-top-right-radius: $radius; 5 | border-top-left-radius: $radius; 6 | } 7 | @mixin border-right-radius($radius) { 8 | border-bottom-right-radius: $radius; 9 | border-top-right-radius: $radius; 10 | } 11 | @mixin border-bottom-radius($radius) { 12 | border-bottom-right-radius: $radius; 13 | border-bottom-left-radius: $radius; 14 | } 15 | @mixin border-left-radius($radius) { 16 | border-bottom-left-radius: $radius; 17 | border-top-left-radius: $radius; 18 | } 19 | -------------------------------------------------------------------------------- /app/review/reviewReducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_REVIEWS, RECEIVE_REVIEW } from './reviewActionCreator'; 2 | 3 | const reviewsInitialState = { 4 | selectedReview: '', 5 | allReviews: [] 6 | } 7 | 8 | export default (state = reviewsInitialState, action) => { 9 | const newState = Object.assign({}, state); 10 | 11 | switch (action.type) { 12 | case RECEIVE_REVIEW: 13 | newState.selectedReview = action.review; 14 | break; 15 | case RECEIVE_REVIEWS: 16 | newState.allReviews = action.reviews; 17 | break; 18 | default: 19 | return state; 20 | } 21 | 22 | return newState; 23 | } 24 | -------------------------------------------------------------------------------- /app/user/user-reducer.js: -------------------------------------------------------------------------------- 1 | import { FETCH_SINGLE_USER, FETCH_ALL_USERS } from './user-actions'; 2 | 3 | const initialState = { 4 | allUsers: [], 5 | currentUser: {} 6 | } 7 | 8 | const userReducer = function(state = initialState, action) { 9 | const newState = Object.assign({}, state); 10 | 11 | switch (action.type) { 12 | case FETCH_ALL_USERS: 13 | newState.allUsers = action.users 14 | break; 15 | case FETCH_SINGLE_USER: 16 | newState.currentUser = action.user; 17 | break; 18 | default: return state; 19 | } 20 | return newState; 21 | } 22 | 23 | export default userReducer; 24 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_pagination.scss: -------------------------------------------------------------------------------- 1 | // Pagination 2 | 3 | @mixin pagination-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) { 4 | > li { 5 | > a, 6 | > span { 7 | padding: $padding-vertical $padding-horizontal; 8 | font-size: $font-size; 9 | line-height: $line-height; 10 | } 11 | &:first-child { 12 | > a, 13 | > span { 14 | @include border-left-radius($border-radius); 15 | } 16 | } 17 | &:last-child { 18 | > a, 19 | > span { 20 | @include border-right-radius($border-radius); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_responsive-visibility.scss: -------------------------------------------------------------------------------- 1 | // Responsive utilities 2 | 3 | // 4 | // More easily include all the states for responsive-utilities.less. 5 | // [converter] $parent hack 6 | @mixin responsive-visibility($parent) { 7 | #{$parent} { 8 | display: block !important; 9 | } 10 | table#{$parent} { display: table !important; } 11 | tr#{$parent} { display: table-row !important; } 12 | th#{$parent}, 13 | td#{$parent} { display: table-cell !important; } 14 | } 15 | 16 | // [converter] $parent hack 17 | @mixin responsive-invisibility($parent) { 18 | #{$parent} { 19 | display: none !important; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const api = module.exports = require('express').Router() 5 | 6 | api 7 | .get('/heartbeat', (req, res) => res.send({ok: true})) 8 | .use('/auth', require('./auth')) 9 | .use('/users', require('./users')) 10 | .use('/orders', require('./orders')) 11 | .use('/cart', require('./cart')) 12 | .use('/reviews', require('./reviews')) 13 | .use('/books', require('./book')) 14 | 15 | // Send along any errors 16 | api.use((err, req, res, next) => { 17 | console.error(err) 18 | res.status(500).send(err) 19 | }) 20 | 21 | // No routes matched? 404. 22 | api.use((req, res) => res.status(404).end()) 23 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_panels.scss: -------------------------------------------------------------------------------- 1 | // Panels 2 | 3 | @mixin panel-variant($border, $heading-text-color, $heading-bg-color, $heading-border) { 4 | border-color: $border; 5 | 6 | & > .panel-heading { 7 | color: $heading-text-color; 8 | background-color: $heading-bg-color; 9 | border-color: $heading-border; 10 | 11 | + .panel-collapse > .panel-body { 12 | border-top-color: $border; 13 | } 14 | .badge { 15 | color: $heading-bg-color; 16 | background-color: $heading-text-color; 17 | } 18 | } 19 | & > .panel-footer { 20 | + .panel-collapse > .panel-body { 21 | border-bottom-color: $border; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_wells.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Wells 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | .well { 8 | min-height: 20px; 9 | padding: 19px; 10 | margin-bottom: 20px; 11 | background-color: $well-bg; 12 | border: 1px solid $well-border; 13 | border-radius: $border-radius-base; 14 | @include box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); 15 | blockquote { 16 | border-color: #ddd; 17 | border-color: rgba(0,0,0,.15); 18 | } 19 | } 20 | 21 | // Sizes 22 | .well-lg { 23 | padding: 24px; 24 | border-radius: $border-radius-large; 25 | } 26 | .well-sm { 27 | padding: 9px; 28 | border-radius: $border-radius-small; 29 | } 30 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_hide-text.scss: -------------------------------------------------------------------------------- 1 | // CSS image replacement 2 | // 3 | // Heads up! v3 launched with only `.hide-text()`, but per our pattern for 4 | // mixins being reused as classes with the same name, this doesn't hold up. As 5 | // of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. 6 | // 7 | // Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 8 | 9 | // Deprecated as of v3.0.1 (has been removed in v4) 10 | @mixin hide-text() { 11 | font: 0/0 a; 12 | color: transparent; 13 | text-shadow: none; 14 | background-color: transparent; 15 | border: 0; 16 | } 17 | 18 | // New mixin to use as of v3.0.1 19 | @mixin text-hide() { 20 | @include hide-text; 21 | } 22 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_responsive-embed.scss: -------------------------------------------------------------------------------- 1 | // Embeds responsive 2 | // 3 | // Credit: Nicolas Gallagher and SUIT CSS. 4 | 5 | .embed-responsive { 6 | position: relative; 7 | display: block; 8 | height: 0; 9 | padding: 0; 10 | overflow: hidden; 11 | 12 | .embed-responsive-item, 13 | iframe, 14 | embed, 15 | object, 16 | video { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | bottom: 0; 21 | height: 100%; 22 | width: 100%; 23 | border: 0; 24 | } 25 | } 26 | 27 | // Modifier class for 16:9 aspect ratio 28 | .embed-responsive-16by9 { 29 | padding-bottom: 56.25%; 30 | } 31 | 32 | // Modifier class for 4:3 aspect ratio 33 | .embed-responsive-4by3 { 34 | padding-bottom: 75%; 35 | } 36 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_clearfix.scss: -------------------------------------------------------------------------------- 1 | // Clearfix 2 | // 3 | // For modern browsers 4 | // 1. The space content is one way to avoid an Opera bug when the 5 | // contenteditable attribute is included anywhere else in the document. 6 | // Otherwise it causes space to appear at the top and bottom of elements 7 | // that are clearfixed. 8 | // 2. The use of `table` rather than `block` is only necessary if using 9 | // `:before` to contain the top-margins of child elements. 10 | // 11 | // Source: http://nicolasgallagher.com/micro-clearfix-hack/ 12 | 13 | @mixin clearfix() { 14 | &:before, 15 | &:after { 16 | content: " "; // 1 17 | display: table; // 2 18 | } 19 | &:after { 20 | clear: both; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/review/singleReviewComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StarRatingComponent from 'react-star-rating-component'; 3 | 4 | const SingleReviewComponent = ({ selectedReview }) => { 5 | return ( 6 |
7 | { 13 | return ; 14 | }} 15 | renderStarIconHalf={() => } 16 | /> 17 |

{ selectedReview.content }

18 |
19 | ) 20 | } 21 | 22 | export default SingleReviewComponent; 23 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /db/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Require our models. Running each module registers the model into sequelize 4 | // so any other part of the application could call sequelize.model('User') 5 | // to get access to the User model. 6 | 7 | const User = require('./user'); 8 | const Book = require('./book'); 9 | const Review = require('./review'); 10 | const Order = require('./orders'); 11 | const SelectedBooks = require('./selectedBooks') 12 | 13 | Review.belongsTo(Book); 14 | Book.hasMany(Review); 15 | 16 | Order.belongsTo(User); 17 | User.hasMany(Order); 18 | 19 | Order.belongsToMany(Book, { through: SelectedBooks}); 20 | Book.belongsToMany(Order, { through: SelectedBooks}); 21 | 22 | 23 | module.exports = { User, Book, Review, Order } 24 | 25 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /tests/singleOrder.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {expect} from 'chai'; 3 | import {shallow} from 'enzyme'; 4 | 5 | import SingleOrder from 'APP/app/order/singleOrder'; 6 | 7 | describe(' component', () => { 8 | 9 | let order, total; 10 | beforeEach('Create component', () => { 11 | total = [{total: 300, created_at: 'T12480', id: 1, title: 'test', imageUrl: 'aklsd', selectedBooks: ['a']}] 12 | order = shallow(); 13 | }); 14 | 15 | it('should be a
', () => { 16 | expect(order.is('div')).to.be.equal(true); 17 | }); 18 | 19 | it('should have a currentOrder prop with an expected value', () => { 20 | expect(order.props().children[0].props.children[0].type).to.equal('h4') 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /tests/user-actions.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | import { getAllUsers, getSingleUser, FETCH_ALL_USERS, FETCH_SINGLE_USER } from 'APP/app/user/user-actions' 4 | 5 | describe('User actions', () => { 6 | describe('Fetch all users', () => { 7 | it('returns the properly formatted action', () => { 8 | const testUsers = [{}, {}, 'this', 'is', 'an', 'array', 'of', 'test', 'users'] 9 | expect(getAllUsers(testUsers)).to.be.deep.equal({ type: FETCH_ALL_USERS, users: testUsers }) 10 | }) 11 | 12 | }) 13 | 14 | describe('Fetch single user', () => { 15 | it('returns the properly formatted action', () => { 16 | const testUser = { name: 'this is a test user' }; 17 | expect(getSingleUser(testUser)).to.be.deep.equal({ type: FETCH_SINGLE_USER, user: testUser}) 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /tests/usermodel.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const User = require('APP/db/models/user') 5 | const {expect} = require('chai') 6 | 7 | describe('User', () => { 8 | before('wait for the db', () => db.didSync) 9 | 10 | describe('authenticate(plaintext: String) ~> Boolean', () => { 11 | it('resolves true if the password matches', () => 12 | User.create({ firstName: 'sam', lastName: 'wheeler', password: 'ok' }) 13 | .then(user => user.authenticate('ok')) 14 | .then(result => expect(result).to.be.true)) 15 | 16 | it("resolves false if the password doesn't match", () => 17 | User.create({ firstName: 'sam', lastName: 'wheeler', password: 'ok' }) 18 | .then(user => user.authenticate('not ok')) 19 | .then(result => expect(result).to.be.false)) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /server/auth.filters.js: -------------------------------------------------------------------------------- 1 | const mustBeLoggedIn = (req, res, next) => { 2 | if (!req.user) { 3 | return res.status(401).send('You must be logged in') 4 | } 5 | next() 6 | } 7 | 8 | const selfOnly = action => (req, res, next) => { 9 | if (+req.params.userId !== req.user.id) { 10 | return res.status(403).send(`You can only ${action}`) 11 | } 12 | next() 13 | } 14 | 15 | const forbidden = message => (req, res, next) => { 16 | res.status(403).send(message) 17 | } 18 | 19 | const adminOnly = (req, res, next) => { 20 | if (!req.user.adminStatus) { 21 | return res.status(401).send('You must be logged in') 22 | } else if (req.user.adminStatus) { 23 | next() 24 | } else { 25 | return res.status(403).send('Admin access only') 26 | } 27 | } 28 | 29 | module.exports = {mustBeLoggedIn, selfOnly, forbidden, adminOnly} 30 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_list-group.scss: -------------------------------------------------------------------------------- 1 | // List Groups 2 | 3 | @mixin list-group-item-variant($state, $background, $color) { 4 | .list-group-item-#{$state} { 5 | color: $color; 6 | background-color: $background; 7 | 8 | // [converter] extracted a&, button& to a.list-group-item-#{$state}, button.list-group-item-#{$state} 9 | } 10 | 11 | a.list-group-item-#{$state}, 12 | button.list-group-item-#{$state} { 13 | color: $color; 14 | 15 | .list-group-item-heading { 16 | color: inherit; 17 | } 18 | 19 | &:hover, 20 | &:focus { 21 | color: $color; 22 | background-color: darken($background, 5%); 23 | } 24 | &.active, 25 | &.active:hover, 26 | &.active:focus { 27 | color: #fff; 28 | background-color: $color; 29 | border-color: $color; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Breadcrumbs 3 | // -------------------------------------------------- 4 | 5 | 6 | .breadcrumb { 7 | padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal; 8 | margin-bottom: $line-height-computed; 9 | list-style: none; 10 | background-color: $breadcrumb-bg; 11 | border-radius: $border-radius-base; 12 | 13 | > li { 14 | display: inline-block; 15 | 16 | + li:before { 17 | // [converter] Workaround for https://github.com/sass/libsass/issues/1115 18 | $nbsp: "\00a0"; 19 | content: "#{$breadcrumb-separator}#{$nbsp}"; // Unicode space added since inline-block means non-collapsing white-space 20 | padding: 0 5px; 21 | color: $breadcrumb-color; 22 | } 23 | } 24 | 25 | > .active { 26 | color: $breadcrumb-active-color; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /stylesheets/_bootstrap-mincer.scss: -------------------------------------------------------------------------------- 1 | // Mincer asset helper functions 2 | // 3 | // This must be imported into a .css.ejs.scss file. 4 | // Then, <% %>-interpolations will be parsed as strings by Sass, and evaluated by EJS after Sass compilation. 5 | 6 | 7 | @function twbs-font-path($path) { 8 | // do something like following 9 | // from "path/to/font.ext#suffix" to "<%- asset_path(path/to/font.ext)) + #suffix %>" 10 | // from "path/to/font.ext?#suffix" to "<%- asset_path(path/to/font.ext)) + ?#suffix %>" 11 | // or from "path/to/font.ext" just "<%- asset_path(path/to/font.ext)) %>" 12 | @return "<%- asset_path("#{$path}".replace(/[#?].*$/, '')) + "#{$path}".replace(/(^[^#?]*)([#?]?.*$)/, '$2') %>"; 13 | } 14 | 15 | @function twbs-image-path($file) { 16 | @return "<%- asset_path("#{$file}") %>"; 17 | } 18 | 19 | $bootstrap-sass-asset-helper: true; 20 | -------------------------------------------------------------------------------- /tests/singleUserComponent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | import { spy } from 'sinon'; 5 | 6 | import SingleUserComponent from 'APP/app/user/singleUserComponent'; 7 | 8 | describe('Single user component', () => { 9 | let singleUser; 10 | let currentUser = {name: 'this is a test'}; 11 | 12 | beforeEach('Create component', () => { 13 | singleUser = shallow(); 14 | }) 15 | 16 | it('Should have the correct current user prop', () => { 17 | expect(singleUser.instance().props).to.be.deep.equal({ currentUser: currentUser } ) 18 | }) 19 | 20 | it('Should display the correct user prop name within the H1 element', () => { 21 | expect(singleUser.find('h1').text()).to.be.deep.equal(currentUser.name); 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_table-row.scss: -------------------------------------------------------------------------------- 1 | // Tables 2 | 3 | @mixin table-row-variant($state, $background) { 4 | // Exact selectors below required to override `.table-striped` and prevent 5 | // inheritance to nested tables. 6 | .table > thead > tr, 7 | .table > tbody > tr, 8 | .table > tfoot > tr { 9 | > td.#{$state}, 10 | > th.#{$state}, 11 | &.#{$state} > td, 12 | &.#{$state} > th { 13 | background-color: $background; 14 | } 15 | } 16 | 17 | // Hover states for `.table-hover` 18 | // Note: this is not available for cells or rows within `thead` or `tfoot`. 19 | .table-hover > tbody > tr { 20 | > td.#{$state}:hover, 21 | > th.#{$state}:hover, 22 | &.#{$state}:hover > td, 23 | &:hover > .#{$state}, 24 | &.#{$state}:hover > th { 25 | background-color: darken($background, 5%); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /app/user/allUserComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | 5 | class AllUsersComponent extends Component { 6 | constructor(props){ 7 | super(props) 8 | } 9 | 10 | render() { 11 | const users = this.props.users; 12 | 13 | return ( 14 |
15 |

ALL USERS

16 |
17 | { 18 | users && users.map(user => ( 19 |
20 | 21 |
22 |
23 | { user.name } 24 |
25 |
26 | 27 |
28 | )) 29 | } 30 |
31 |
32 | ) 33 | } 34 | } 35 | 36 | export default AllUsersComponent; 37 | -------------------------------------------------------------------------------- /tests/allUserComponent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | import { spy } from 'sinon'; 5 | 6 | import AllUsersComponent from 'APP/app/user/allUserComponent'; 7 | 8 | describe('All users component', () => { 9 | let allUsers; 10 | let userList = [{name: 'this is a test', id: 1}, {name: 'i am a user', id: 2}, {name: 'harry potter', id: 3 }, {name: 'Sam Wheeler', id: 4 }] 11 | 12 | beforeEach('Create component', () => { 13 | allUsers = shallow(); 14 | }) 15 | 16 | it('Should have the correct amount of users listed', () => { 17 | expect(allUsers.find('h5').length).to.be.equal(userList.length); 18 | }) 19 | 20 | it('Should list the correct user names from props', () => { 21 | expect(allUsers.instance().props).to.be.deep.equal({ users: userList } ) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/bookListComponent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {expect} from 'chai'; 3 | import {shallow} from 'enzyme'; 4 | import {spy} from 'sinon'; 5 | import BookListComponent from 'APP/app/book/bookListComponent'; 6 | 7 | 8 | describe('Book List Component', () => { 9 | 10 | const newBooks = [ 11 | { id: 0, title: 'harry potter', price: 15}, 12 | { id: 1, title: 'enders game', price: 17.20 }, 13 | { id: 2, title: 'LOTR', price: 9.09} 14 | ] 15 | 16 | let newBookList; 17 | beforeEach('Create component', () => { 18 | newBookList = shallow() 19 | }) 20 | 21 | it('should be a
with an expected background', () => { 22 | expect(newBookList.is('div')).to.be.true 23 | }) 24 | 25 | it('should have reviews on its prop', () => { 26 | expect(newBookList.instance().props.books).to.equal(newBooks) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/ordersmodel.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const db = require('APP/db') 4 | const Orders = require('APP/db/models/orders') 5 | const {expect} = require('chai') 6 | 7 | describe('book', () => { 8 | before('wait for the db', () => db.didSync); 9 | 10 | beforeEach(function() { 11 | return Orders.create({ 12 | selected: [ 13 | {id: 1, price: 1.00, quantity: 1}, 14 | {id: 2, price: 2.00, quantity: 2}, 15 | {id: 3, price: 3.21, quantity: 3} 16 | ] 17 | }) 18 | }); 19 | 20 | afterEach(function(){ 21 | return db.sync({force: true}); 22 | }); 23 | 24 | describe('hooks', () => { 25 | 26 | xdescribe('beforeCreate', () => { 27 | it('sets total price to correct value', () => { 28 | return Orders.findById(1) 29 | .then(foundOrder => { 30 | expect(foundOrder.total).to.equal(14.63) 31 | }) 32 | }) 33 | }) 34 | 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /tests/singleBookComponent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import SingleBookComponent from 'APP/app/book/singleBookComponent'; 6 | 7 | describe('Single Book Component', () => { 8 | let book = {title: 'Harry Potter', price: 15} 9 | let newSingleBook; 10 | beforeEach('Create component', () => { 11 | newSingleBook = shallow() 12 | }) 13 | 14 | it('should be a
', () => { 15 | expect(newSingleBook.is('div')).to.be.true 16 | }) 17 | 18 | it('should have title and price on its prop', () => { 19 | expect(newSingleBook.instance().props.currentBook.title).to.equal('Harry Potter') 20 | expect(newSingleBook.instance().props.currentBook.price).to.equal(15) 21 | }) 22 | 23 | it('should have initial state set to false', () => { 24 | expect(newSingleBook.state()).to.deep.equal({ addedToCart: false }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /app/user/user-actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const FETCH_ALL_USERS = 'FETCH_ALL_USERS'; 4 | export const FETCH_SINGLE_USER = 'FETCH_SINGLE_USER'; 5 | 6 | 7 | export function getAllUsers(users) { 8 | return { 9 | type: FETCH_ALL_USERS, 10 | users 11 | } 12 | } 13 | 14 | export function getSingleUser(user) { 15 | return { 16 | type: FETCH_SINGLE_USER, 17 | user 18 | } 19 | } 20 | 21 | 22 | export function fetchAllUsers() { 23 | return function (dispatch) { 24 | axios.get('/api/users') 25 | .then(res => res.data) 26 | .then(foundUsers => { 27 | dispatch(getAllUsers(foundUsers)) 28 | }) 29 | .catch(console.error) 30 | } 31 | } 32 | 33 | export function fetchSingleUser(id) { 34 | return function (dispatch) { 35 | axios.get(`/api/users/${id}`) 36 | .then(res => res.data) 37 | .then(foundUser => { 38 | dispatch(getSingleUser(foundUser)) 39 | }) 40 | .catch(console.error) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/book/book-reducer.js: -------------------------------------------------------------------------------- 1 | import { FETCH_SINGLE_BOOK, FETCH_ALL_BOOKS, SET_GENRE, GET_AUTHOR, SELECTED_BOOKS } from './book-actions'; 2 | 3 | const initialState = { 4 | allBooks: [], 5 | currentBook: {}, 6 | currentGenre: '', 7 | currentAuthor: {}, 8 | selectedBooks: [] 9 | } 10 | 11 | const bookReducer = function(state = initialState, action) { 12 | switch (action.type) { 13 | case FETCH_ALL_BOOKS: 14 | return Object.assign({}, state, { allBooks: action.books }); 15 | case FETCH_SINGLE_BOOK: 16 | return Object.assign({}, state, { currentBook: action.book }); 17 | case SET_GENRE: 18 | return Object.assign({}, state, { currentGenre: action.genre }); 19 | case GET_AUTHOR: 20 | return Object.assign({}, state, { currentAuthor: action.author }); 21 | case SELECTED_BOOKS: 22 | return Object.assign({}, state, { selectedBooks: action.selectedBooks }); 23 | default: return state; 24 | } 25 | } 26 | 27 | export default bookReducer; 28 | -------------------------------------------------------------------------------- /tests/books-actions.test.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | 3 | import { getAllBooks, getSingleBook } from 'APP/app/book/book-actions.js'; 4 | 5 | describe('Book Actions', () => { 6 | 7 | describe('getAllBooks', () => { 8 | 9 | it('returns properly formatted action creator', () => { 10 | 11 | const testBook = ['harry potter', 'enders game']; 12 | 13 | expect(getAllBooks(testBook)).to.be.deep.equal({ 14 | type: 'FETCH_ALL_BOOKS', 15 | books: testBook 16 | }); 17 | 18 | }); 19 | 20 | }); 21 | 22 | describe('getAllBooks', () => { 23 | 24 | it('returns properly formatted action creator', () => { 25 | 26 | const testBook = ['harry potter']; 27 | 28 | expect(getSingleBook(testBook)).to.be.deep.equal({ 29 | type: 'FETCH_SINGLE_BOOK', 30 | book: testBook 31 | }); 32 | 33 | }); 34 | 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /tests/orderList.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {expect} from 'chai'; 3 | import {shallow} from 'enzyme'; 4 | 5 | import { Link } from 'react-router'; 6 | 7 | import OrderList from 'APP/app/order/orderList'; 8 | 9 | xdescribe('OrderList component', () => { 10 | 11 | let list, orders; 12 | beforeEach('Create component', () => { 13 | orders = [ 14 | {id:1, price:2, quantity: 3}, 15 | {id:2, price:3, quantity: 2}, 16 | {id:4, price:20, quantity: 1} 17 | ]; 18 | list = shallow(); 19 | }); 20 | 21 | it('uses ', () => { 22 | expect(list.find(Link).length).to.be.equal(3); 23 | }); 24 | 25 | it('passes its own price prop to ', () => { 26 | const usedOrders = list.find('#price').nodes[1]; 27 | expect(usedOrders.props.children.join(' ')).to.be.equal('Pay me ' + orders[1].price); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_close.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: ($font-size-base * 1.5); 9 | font-weight: $close-font-weight; 10 | line-height: 1; 11 | color: $close-color; 12 | text-shadow: $close-text-shadow; 13 | @include opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: $close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | @include opacity(.5); 21 | } 22 | 23 | // [converter] extracted button& to button.close 24 | } 25 | 26 | // Additional properties for button version 27 | // iOS requires the button element instead of an anchor tag. 28 | // If you want the anchor version, it requires `href="#"`. 29 | // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile 30 | button.close { 31 | padding: 0; 32 | cursor: pointer; 33 | background: transparent; 34 | border: 0; 35 | -webkit-appearance: none; 36 | } 37 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_component-animations.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | @include transition(opacity .15s linear); 13 | &.in { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .collapse { 19 | display: none; 20 | 21 | &.in { display: block; } 22 | // [converter] extracted tr&.in to tr.collapse.in 23 | // [converter] extracted tbody&.in to tbody.collapse.in 24 | } 25 | 26 | tr.collapse.in { display: table-row; } 27 | 28 | tbody.collapse.in { display: table-row-group; } 29 | 30 | .collapsing { 31 | position: relative; 32 | height: 0; 33 | overflow: hidden; 34 | @include transition-property(height, visibility); 35 | @include transition-duration(.35s); 36 | @include transition-timing-function(ease); 37 | } 38 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_utilities.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Utility classes 3 | // -------------------------------------------------- 4 | 5 | 6 | // Floats 7 | // ------------------------- 8 | 9 | .clearfix { 10 | @include clearfix; 11 | } 12 | .center-block { 13 | @include center-block; 14 | } 15 | .pull-right { 16 | float: right !important; 17 | } 18 | .pull-left { 19 | float: left !important; 20 | } 21 | 22 | 23 | // Toggling content 24 | // ------------------------- 25 | 26 | // Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 27 | .hide { 28 | display: none !important; 29 | } 30 | .show { 31 | display: block !important; 32 | } 33 | .invisible { 34 | visibility: hidden; 35 | } 36 | .text-hide { 37 | @include text-hide; 38 | } 39 | 40 | 41 | // Hide from screenreaders and browsers 42 | // 43 | // Credit: HTML5 Boilerplate 44 | 45 | .hidden { 46 | display: none !important; 47 | } 48 | 49 | 50 | // For Affix plugin 51 | // ------------------------- 52 | 53 | .affix { 54 | position: fixed; 55 | } 56 | -------------------------------------------------------------------------------- /app/order/order-reducer.js: -------------------------------------------------------------------------------- 1 | import { FETCH_SINGLE_ORDER, FETCH_ALL_ORDERS, FETCH_SINGLE_ORDER_ADMIN, FETCH_ALL_ORDERS_ADMIN, FETCH_SHOPPING_CART } from './order-actions'; 2 | 3 | const initialState = { 4 | allOrders: [], 5 | currentOrder: [], 6 | shoppingCart: [] 7 | } 8 | 9 | const orderReducer = function(state = initialState, action) { 10 | switch (action.type) { 11 | case FETCH_ALL_ORDERS: // USER 12 | return Object.assign({}, state, { allOrders: action.orders }); 13 | case FETCH_SINGLE_ORDER: // USER 14 | return Object.assign({}, state, { currentOrder: action.currentOrder }); 15 | case FETCH_ALL_ORDERS_ADMIN: // ADMIN 16 | return Object.assign({}, state, { allOrders: action.orders }); 17 | case FETCH_SINGLE_ORDER_ADMIN: // ADMIN 18 | return Object.assign({}, state, { currentOrder: action.currentOrder }); 19 | case FETCH_SHOPPING_CART: // USER 20 | return Object.assign({}, state, { shoppingCart: action.cart }); 21 | default: return state; 22 | } 23 | } 24 | 25 | export default orderReducer; 26 | -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { connect } from 'react-redux'; 3 | // import { Router, Route, IndexRoute, browserHistory } from 'react-router'; 4 | // import App from './'; 5 | // import bookListContainer from './book/bookListContainer'; 6 | 7 | // import { fetchAllBooks } from './book/book-actions'; 8 | 9 | // /* ----------------- COMPONENT ------------------ */ 10 | 11 | // const Routes = ({ fetchAllData }) => ( 12 | // 13 | // 14 | // 15 | // 16 | // 17 | // 18 | // ); 19 | 20 | // /* ----------------- CONTAINER ------------------ */ 21 | 22 | // const mapState = null; 23 | 24 | // const mapDispatch = dispatch => ({ 25 | // fetchAllData: () => { 26 | // dispatch(fetchAllBooks()) 27 | // } 28 | // }) 29 | 30 | // export default connect(mapState, mapDispatch)(Routes); 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | var webpack = require('webpack'); 5 | 6 | module.exports = { 7 | entry: './app/main.jsx', 8 | output: { 9 | path: path.join(__dirname, 'public'), 10 | publicPath: '/', 11 | filename: './bundle.js' 12 | }, 13 | context: __dirname, 14 | devtool: 'source-map', 15 | resolve: { 16 | extensions: ['', '.js', '.jsx', '.scss'] 17 | }, 18 | plugins: [ 19 | new webpack.ProvidePlugin({ 20 | $: "jquery", 21 | jQuery: "jquery" 22 | }) 23 | ], 24 | module: { 25 | loaders: [ 26 | { 27 | test: /jsx?$/, 28 | exclude: /(node_modules|bower_components)/, 29 | loader: 'babel', 30 | query: { 31 | presets: ['react', 'es2015', 'stage-2'] 32 | } 33 | }, 34 | {test: /\.scss?$/, loaders: ['style', 'css', 'sass']}, 35 | { 36 | test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?.*$|$)/, 37 | loader: 'file' 38 | } 39 | ] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_thumbnails.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Thumbnails 3 | // -------------------------------------------------- 4 | 5 | 6 | // Mixin and adjust the regular image class 7 | .thumbnail { 8 | display: block; 9 | padding: $thumbnail-padding; 10 | margin-bottom: $line-height-computed; 11 | line-height: $line-height-base; 12 | background-color: $thumbnail-bg; 13 | border: 1px solid $thumbnail-border; 14 | border-radius: $thumbnail-border-radius; 15 | @include transition(border .2s ease-in-out); 16 | 17 | > img, 18 | a > img { 19 | @include img-responsive; 20 | margin-left: auto; 21 | margin-right: auto; 22 | } 23 | 24 | // [converter] extracted a&:hover, a&:focus, a&.active to a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active 25 | 26 | // Image captions 27 | .caption { 28 | padding: $thumbnail-caption-padding; 29 | color: $thumbnail-caption-color; 30 | } 31 | } 32 | 33 | // Add a hover state for linked versions only 34 | a.thumbnail:hover, 35 | a.thumbnail:focus, 36 | a.thumbnail.active { 37 | border-color: $link-color; 38 | } 39 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_pager.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Pager pagination 3 | // -------------------------------------------------- 4 | 5 | 6 | .pager { 7 | padding-left: 0; 8 | margin: $line-height-computed 0; 9 | list-style: none; 10 | text-align: center; 11 | @include clearfix; 12 | li { 13 | display: inline; 14 | > a, 15 | > span { 16 | display: inline-block; 17 | padding: 5px 14px; 18 | background-color: $pager-bg; 19 | border: 1px solid $pager-border; 20 | border-radius: $pager-border-radius; 21 | } 22 | 23 | > a:hover, 24 | > a:focus { 25 | text-decoration: none; 26 | background-color: $pager-hover-bg; 27 | } 28 | } 29 | 30 | .next { 31 | > a, 32 | > span { 33 | float: right; 34 | } 35 | } 36 | 37 | .previous { 38 | > a, 39 | > span { 40 | float: left; 41 | } 42 | } 43 | 44 | .disabled { 45 | > a, 46 | > a:hover, 47 | > a:focus, 48 | > span { 49 | color: $pager-disabled-color; 50 | background-color: $pager-bg; 51 | cursor: $cursor-disabled; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/order-actions.test.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | 3 | import {getAllOrders, getSingleOrder} from 'APP/app/order/order-actions'; 4 | 5 | 6 | describe('Order actions', () => { 7 | 8 | describe('getAllOrders', () => { 9 | 10 | it('returns properly formatted action', () => { 11 | 12 | const testOrders = [ 13 | {id: 1, price: 2, quantity: 3}, 14 | {id: 2, price: 4, quantity: 2}, 15 | {id: 3, price: 6, quantity: 1} 16 | ]; 17 | 18 | expect(getAllOrders(testOrders)).to.be.deep.equal({ 19 | type: 'FETCH_ALL_ORDERS', 20 | orders: testOrders 21 | }); 22 | 23 | }); 24 | 25 | }); 26 | 27 | describe('getSingleOrder', () => { 28 | 29 | it('returns properly formatted action', () => { 30 | 31 | const testOrder = {id: 1, price: 2, quantity: 3}; 32 | 33 | expect(getSingleOrder(testOrder)).to.be.deep.equal({ 34 | type: 'FETCH_SINGLE_ORDER', 35 | currentOrder: testOrder 36 | }); 37 | 38 | }); 39 | 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------------------------------- 3 | 4 | // Utilities 5 | @import "mixins/hide-text"; 6 | @import "mixins/opacity"; 7 | @import "mixins/image"; 8 | @import "mixins/labels"; 9 | @import "mixins/reset-filter"; 10 | @import "mixins/resize"; 11 | @import "mixins/responsive-visibility"; 12 | @import "mixins/size"; 13 | @import "mixins/tab-focus"; 14 | @import "mixins/reset-text"; 15 | @import "mixins/text-emphasis"; 16 | @import "mixins/text-overflow"; 17 | @import "mixins/vendor-prefixes"; 18 | 19 | // Components 20 | @import "mixins/alerts"; 21 | @import "mixins/buttons"; 22 | @import "mixins/panels"; 23 | @import "mixins/pagination"; 24 | @import "mixins/list-group"; 25 | @import "mixins/nav-divider"; 26 | @import "mixins/forms"; 27 | @import "mixins/progress-bar"; 28 | @import "mixins/table-row"; 29 | 30 | // Skins 31 | @import "mixins/background-variant"; 32 | @import "mixins/border-radius"; 33 | @import "mixins/gradients"; 34 | 35 | // Layout 36 | @import "mixins/clearfix"; 37 | @import "mixins/center-block"; 38 | @import "mixins/nav-vertical-align"; 39 | @import "mixins/grid-framework"; 40 | @import "mixins/grid"; 41 | -------------------------------------------------------------------------------- /app/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from './navbar/'; 3 | import Footer from './footer/footerComponent'; 4 | import { Link } from 'react-router'; 5 | import { connect } from 'react-redux'; 6 | import 'bootstrap-sass'; 7 | 8 | import {LoginModal} from './navbar/loginModal'; 9 | 10 | import { particlesConfig } from './dummy-data/particles.data'; 11 | import 'particles.js'; 12 | 13 | export class App extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | } 17 | 18 | componentDidMount() { 19 | particlesJS('particles', particlesConfig); 20 | } 21 | 22 | render() { 23 | let user = this.props.user; 24 | let children = this.props.children 25 | return ( 26 |
27 | 28 | 29 |
30 | { children } 31 |
32 |
33 | 34 |
35 |
36 |
37 | ) 38 | } 39 | } 40 | 41 | const mapStateToProps = (state) => { 42 | return { 43 | user: state.auth 44 | } 45 | } 46 | 47 | export default connect(mapStateToProps)(App) 48 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_media.scss: -------------------------------------------------------------------------------- 1 | .media { 2 | // Proper spacing between instances of .media 3 | margin-top: 15px; 4 | 5 | &:first-child { 6 | margin-top: 0; 7 | } 8 | } 9 | 10 | .media, 11 | .media-body { 12 | zoom: 1; 13 | overflow: hidden; 14 | } 15 | 16 | .media-body { 17 | width: 10000px; 18 | } 19 | 20 | .media-object { 21 | display: block; 22 | 23 | // Fix collapse in webkit from max-width: 100% and display: table-cell. 24 | &.img-thumbnail { 25 | max-width: none; 26 | } 27 | } 28 | 29 | .media-right, 30 | .media > .pull-right { 31 | padding-left: 10px; 32 | } 33 | 34 | .media-left, 35 | .media > .pull-left { 36 | padding-right: 10px; 37 | } 38 | 39 | .media-left, 40 | .media-right, 41 | .media-body { 42 | display: table-cell; 43 | vertical-align: top; 44 | } 45 | 46 | .media-middle { 47 | vertical-align: middle; 48 | } 49 | 50 | .media-bottom { 51 | vertical-align: bottom; 52 | } 53 | 54 | // Reset margins on headings for tighter default spacing 55 | .media-heading { 56 | margin-top: 0; 57 | margin-bottom: 5px; 58 | } 59 | 60 | // Media list variation 61 | // 62 | // Undo default ul/ol styles 63 | .media-list { 64 | padding-left: 0; 65 | list-style: none; 66 | } 67 | -------------------------------------------------------------------------------- /app/review/reviewActionCreator.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { browserHistory } from 'react-router' 3 | import { fetchSingleBook } from '../book/book-actions' 4 | 5 | export const RECEIVE_REVIEWS = 'RECEIVE_REVIEWS'; 6 | export const RECEIVE_REVIEW = 'RECEIVE_REVIEW'; 7 | 8 | export const receiveReviews = reviews => { 9 | return { 10 | type: RECEIVE_REVIEWS, 11 | reviews 12 | } 13 | } 14 | 15 | export const receiveReview = review => { 16 | return { 17 | type: RECEIVE_REVIEW, 18 | review 19 | } 20 | } 21 | 22 | // -------------- axios request using thunk 23 | 24 | // doesn't exist in backend routes yet 25 | export const getReviewById = reviewId => dispatch => { 26 | axios.get(`/api/reviews/${reviewId}`) 27 | .then(res => res.data) 28 | .then(data => dispatch(receiveReview(data))) 29 | } 30 | 31 | export const getAllReviews = () => dispatch => { 32 | axios.get('/api/reviews/') 33 | .then(res => res.data) 34 | .then(data => dispatch(receiveReviews(data))) 35 | } 36 | 37 | export const addNewReview = (review) => (dispatch, getState) => { 38 | return axios.post('/api/reviews', review) 39 | .then(res => res.data) 40 | .then(newReview => { 41 | dispatch(fetchSingleBook(newReview.book_id)) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /server/reviews.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const router = require('express').Router() 5 | const Review = db.model('reviews') 6 | 7 | router.get('/', (req, res, next) => { 8 | Review.findAll() 9 | .then(allReviews => { 10 | res.send(allReviews) 11 | }) 12 | .catch(next) 13 | }) 14 | 15 | router.get('/:id', (req, res, next) => { 16 | Review.findById(req.params.id) 17 | .then(foundReview => { 18 | res.send(foundReview) 19 | }) 20 | .catch(next) 21 | }) 22 | 23 | router.post('/', (req, res, next) => { 24 | Review.create(req.body) 25 | .then(createdReview => { 26 | res.status(201).send(createdReview) 27 | }) 28 | .catch(next) 29 | }) 30 | 31 | router.put('/:id', (req, res, next) => { 32 | Review.update(req.body, { 33 | where: { 34 | id: req.params.id 35 | }, 36 | returning: true 37 | }) 38 | .then(updatedReview => { 39 | res.send(updatedReview[1][0]) 40 | }) 41 | .catch(next) 42 | }) 43 | 44 | router.delete('/:id', (req, res, next) => { 45 | Review.destroy({ 46 | where: { 47 | id: req.params.id 48 | }, 49 | returning: true 50 | }) 51 | .then(() => { 52 | res.sendStatus(204) 53 | }) 54 | .catch(next) 55 | }) 56 | 57 | module.exports = router 58 | -------------------------------------------------------------------------------- /app/book/SelectedAuthorsComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | 5 | const SelectedAuthors = ({ allBooks, genre, selectedBooks }) => { 6 | //filtering books by the genre currently on the state, if there is no genre, display all books 7 | let books; 8 | if (selectedBooks === []) books = allBooks 9 | else { 10 | books = selectedBooks 11 | } 12 | 13 | 14 | return ( 15 |
16 |

Books

17 |
18 | { 19 | books && books.map(book => ( 20 |
21 | 22 | 23 |
24 |
25 | { book.title } 26 |
27 | By { book.author } 28 |
29 |
30 | 31 |
32 | )) 33 | } 34 |
35 |
36 | ) 37 | 38 | } 39 | 40 | export default SelectedAuthors; 41 | -------------------------------------------------------------------------------- /tests/reviewListComponent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import ReviewListComponent from 'APP/app/review/reviewListComponent'; 6 | import SingleReviewComponent from 'APP/app/review/singleReviewComponent'; 7 | import { Link } from 'react-router'; 8 | 9 | describe('Review List Component', () => { 10 | 11 | const reviewsPassedDown = [ 12 | { id: 0, rating: 1.0, content:'something something' }, 13 | { id: 1, rating: 3.2, content: 'another review' }, 14 | { id: 2, rating: 4.9, content: 'a different review'} 15 | ] 16 | 17 | let reviewList; 18 | beforeEach('Create component', () => { 19 | reviewList = shallow() 20 | }) 21 | 22 | it('should be a
with an expected background', () => { 23 | expect(reviewList.is('div')).to.be.true 24 | }) 25 | 26 | it('should have reviews on its prop', () => { 27 | expect(reviewList.instance().props.reviews).to.equal(reviewsPassedDown) 28 | }) 29 | 30 | it('should have 3 links and 3 singleReviewComponents', () => { 31 | expect(reviewList.find(Link)).to.have.length(3) 32 | expect(reviewList.find(SingleReviewComponent)).to.have.length(3) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /tests/reviewActionCreator.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { RECEIVE_REVIEWS, RECEIVE_REVIEW, receiveReviews, receiveReview} from 'APP/app/review/reviewActionCreator' 3 | 4 | describe('Review Action Creators', () => { 5 | describe('receiveReviews', () => { 6 | it('should return the correct object', () => { 7 | const reviews = [ 8 | {rating: 3.5, content: 'This is some content. More content'}, 9 | {rating: 4.5, content: 'This is some content. More content. More content.'}, 10 | {rating: 2.5, content: 'This is some content. More content. More content. More content'} 11 | ] 12 | 13 | const newAction = receiveReviews(reviews) 14 | expect(newAction.type).to.deep.equal(RECEIVE_REVIEWS) 15 | expect(newAction.reviews).to.deep.equal(reviews) 16 | }) 17 | }) 18 | 19 | describe('receiveReview', () => { 20 | it('should return the correct object', () => { 21 | const review = 'Some praise for a movie. Then some faults with the movie because I am an expert in movies. More criticism. Then more praise.' 22 | 23 | const newAction = receiveReview(review); 24 | expect(newAction.type).to.deep.equal(RECEIVE_REVIEW) 25 | expect(newAction.review).to.deep.equal(review) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/reviewReducer.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { createStore } from 'redux'; 3 | 4 | import reducer from 'APP/app/review/reviewReducer'; 5 | import { RECEIVE_REVIEWS, RECEIVE_REVIEW } from 'APP/app/review/reviewActionCreator'; 6 | 7 | describe('Review Reducers', () => { 8 | let testStore; 9 | beforeEach('Create testing store', () => { 10 | testStore = createStore(reducer); 11 | }) 12 | 13 | it('has the expected initial state', () => { 14 | expect(testStore.getState()).to.deep.equal({ 15 | selectedReview: '', 16 | allReviews: [] 17 | }) 18 | }) 19 | 20 | it('RECEIVE_REVIEWS', () => { 21 | const manyReviews = ['', 'sdadasd', '1231231'] 22 | testStore.dispatch({type: RECEIVE_REVIEWS, reviews: manyReviews }) 23 | 24 | const newState = testStore.getState(); 25 | 26 | expect(newState.allReviews).to.deep.equal(manyReviews) 27 | expect(newState.selectedReview).to.equal('') 28 | }) 29 | 30 | it('RECEIVE_REVIEW', () => { 31 | const review = 'something something something' 32 | testStore.dispatch({type: RECEIVE_REVIEW, review: review }) 33 | 34 | const newState = testStore.getState(); 35 | 36 | expect(newState.allReviews).to.deep.equal([]) 37 | expect(newState.selectedReview).to.equal(review) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/singleReviewComponent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import SingleReviewComponent from 'APP/app/review/singleReviewComponent'; 6 | import StarRatingComponent from 'react-star-rating-component'; 7 | 8 | describe('Single Review Component', () => { 9 | const reviewEx = { rating: 3.5, content: 'content content content' } 10 | const rating = 3.5 11 | const content = 'content content content' 12 | let review; 13 | beforeEach('Create component', () => { 14 | review = shallow() 15 | }) 16 | 17 | it('should be a
with an expected background', () => { 18 | expect(review.is('div')).to.be.true 19 | }) 20 | 21 | it('should have rating and content on its prop', () => { 22 | expect(review.instance().props.selectedReview.rating).to.equal(rating) 23 | expect(review.instance().props.selectedReview.content).to.equal(content) 24 | }) 25 | 26 | it('should have a StarRatingComponent', () => { 27 | expect(review.find(StarRatingComponent)).to.have.length(1); 28 | }) 29 | 30 | it('StarRatingComponent should have a value set to props.rating', () => { 31 | expect(review.find(StarRatingComponent).props().value).to.equal(3.5) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /app/review/newReviewForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ({ handleChange, handleSubmit, rating, content }){ 4 | return ( 5 |
6 |
7 | Add a New Review 8 |
9 | 10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_image.scss: -------------------------------------------------------------------------------- 1 | // Image Mixins 2 | // - Responsive image 3 | // - Retina image 4 | 5 | 6 | // Responsive image 7 | // 8 | // Keep images from scaling beyond the width of their parents. 9 | @mixin img-responsive($display: block) { 10 | display: $display; 11 | max-width: 100%; // Part 1: Set a maximum relative to the parent 12 | height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching 13 | } 14 | 15 | 16 | // Retina image 17 | // 18 | // Short retina mixin for setting background-image and -size. Note that the 19 | // spelling of `min--moz-device-pixel-ratio` is intentional. 20 | @mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) { 21 | background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-1x}"), "#{$file-1x}")); 22 | 23 | @media 24 | only screen and (-webkit-min-device-pixel-ratio: 2), 25 | only screen and ( min--moz-device-pixel-ratio: 2), 26 | only screen and ( -o-min-device-pixel-ratio: 2/1), 27 | only screen and ( min-device-pixel-ratio: 2), 28 | only screen and ( min-resolution: 192dpi), 29 | only screen and ( min-resolution: 2dppx) { 30 | background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-2x}"), "#{$file-2x}")); 31 | background-size: $width-1x $height-1x; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/book/bookListComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | 5 | const BookListComponent = ({ allBooks, genre }) => { 6 | //filtering books by the genre currently on the state, if there is no genre, display all books 7 | let books; 8 | if (genre === '') books = allBooks; 9 | else if (allBooks) { 10 | books = allBooks.filter(book => { 11 | return book.genre.includes(genre) 12 | }) 13 | } 14 | 15 | return ( 16 |
17 | { 18 | genre ?

Our {genre} Books

:

Our Books

19 | } 20 | 21 |
22 | { 23 | books && books.map(book => ( 24 |
25 | 26 | 27 | 28 |
29 | 30 |
31 | { book.title } 32 |
33 | By { book.author } 34 |
35 |
36 | 37 |
38 | )) 39 | } 40 |
41 |
42 | ) 43 | 44 | } 45 | 46 | export default BookListComponent; 47 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_jumbotron.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Jumbotron 3 | // -------------------------------------------------- 4 | 5 | 6 | .jumbotron { 7 | padding-top: $jumbotron-padding; 8 | padding-bottom: $jumbotron-padding; 9 | margin-bottom: $jumbotron-padding; 10 | color: $jumbotron-color; 11 | background-color: $jumbotron-bg; 12 | 13 | h1, 14 | .h1 { 15 | color: $jumbotron-heading-color; 16 | } 17 | 18 | p { 19 | margin-bottom: ($jumbotron-padding / 2); 20 | font-size: $jumbotron-font-size; 21 | font-weight: 200; 22 | } 23 | 24 | > hr { 25 | border-top-color: darken($jumbotron-bg, 10%); 26 | } 27 | 28 | .container &, 29 | .container-fluid & { 30 | border-radius: $border-radius-large; // Only round corners at higher resolutions if contained in a container 31 | padding-left: ($grid-gutter-width / 2); 32 | padding-right: ($grid-gutter-width / 2); 33 | } 34 | 35 | .container { 36 | max-width: 100%; 37 | } 38 | 39 | @media screen and (min-width: $screen-sm-min) { 40 | padding-top: ($jumbotron-padding * 1.6); 41 | padding-bottom: ($jumbotron-padding * 1.6); 42 | 43 | .container &, 44 | .container-fluid & { 45 | padding-left: ($jumbotron-padding * 2); 46 | padding-right: ($jumbotron-padding * 2); 47 | } 48 | 49 | h1, 50 | .h1 { 51 | font-size: $jumbotron-heading-font-size; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/selectedBooks.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const db = require('APP/db') 4 | const SelectedBooks = require('APP/db/models/selectedBooks') 5 | const {expect} = require('chai') 6 | const Orders = require('APP/db/models/orders') 7 | const Book = require('APP/db/models/book'); 8 | 9 | xdescribe('selectedBooks', () => { 10 | before('wait for the db', () => db.didSync); 11 | 12 | beforeEach(function() { 13 | return db.Promise.map([ 14 | {'order_id': 1, 'book_id': 1, quantity: 5}, 15 | {'order_id': 2, 'book_id': 1, quantity: 2}, 16 | {'order_id': 3, 'book_id': 4, quantity: 3}, 17 | {'order_id': 4, 'book_id': 5, quantity: 1}, 18 | {'order_id': 4, 'book_id': 6, quantity: 2}, 19 | {'order_id': 4, 'book_id': 7, quantity: 1}, 20 | {'order_id': 1, 'book_id': 4, quantity: 7}], selectedBook => db.model('selectedBooks').create(selectedBook)) 21 | }); 22 | 23 | afterEach(function(){ 24 | return db.sync({force: true}); 25 | }); 26 | 27 | describe('hooks', () => { 28 | 29 | describe('beforeCreate', () => { 30 | it('gets an items from database', () => { 31 | return SelectedBooks.findOne({ 32 | where: { 33 | 'order_id': 1, 34 | 'book_id': 1 35 | } 36 | }) 37 | .then(foundSelectedBooks => { 38 | expect(foundSelectedBooks.quantity).to.equal(5) 39 | }) 40 | }) 41 | }) 42 | 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /bin/mkapplink.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const chalk = require('chalk') 4 | const fs = require('fs') 5 | const {resolve} = require('path') 6 | 7 | const appLink = resolve(__dirname, '..', 'node_modules', 'APP') 8 | 9 | const symlinkError = error => 10 | `******************************************************************* 11 | ${appLink} must point to '..' 12 | 13 | This symlink lets you require('APP/some/path') rather than 14 | ../../../some/path 15 | 16 | I tried to create it, but got this error: 17 | ${error.message} 18 | 19 | You might try this: 20 | 21 | rm ${appLink} 22 | 23 | Then run me again. 24 | 25 | ~ xoxo, bones 26 | ********************************************************************` 27 | 28 | function makeAppSymlink() { 29 | console.log(`Linking '${appLink}' to '..'`) 30 | try { 31 | try { fs.unlinkSync(appLink) } catch(swallowed) { } 32 | fs.symlinkSync('..', appLink) 33 | } catch (error) { 34 | console.error(chalk.red(symlinkError(error))) 35 | process.exit(1) 36 | } 37 | console.log(`Ok, created ${appLink}`) 38 | } 39 | 40 | function ensureAppSymlink() { 41 | try { 42 | const currently = fs.readlinkSync(appLink) 43 | if (currently !== '..') { 44 | throw new Error(`${appLink} is pointing to '${currently}' rather than '..'`) 45 | } 46 | } catch (error) { 47 | makeAppSymlink() 48 | } 49 | } 50 | 51 | if (module === require.main) { 52 | ensureAppSymlink() 53 | } -------------------------------------------------------------------------------- /server/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const User = db.model('users') 5 | const { mustBeLoggedIn, forbidden } = require('./auth.filters') 6 | const express = require('express') 7 | const router = express.Router(); 8 | 9 | module.exports = router; 10 | 11 | router.get('/', forbidden('only admins can list users'), (req, res, next) => { 12 | User.findAll() 13 | .then(users => res.json(users)) 14 | .catch(next) 15 | }) 16 | 17 | router.post('/', (req, res, next) => { 18 | User.create(req.body) 19 | .then(user => { 20 | res.status(201).json(user) 21 | }) 22 | .catch(next) 23 | }) 24 | 25 | router.get('/:id', mustBeLoggedIn, (req, res, next) => { 26 | User.findById(req.params.id) 27 | .then(user => res.json(user)) 28 | .catch(next) 29 | }) 30 | 31 | //does not yet require login 32 | router.put('/:id', mustBeLoggedIn, (req, res, next) => { 33 | User.update(req.body, { 34 | where: { id: req.params.id }, 35 | returning: true 36 | }) 37 | .then(user => { 38 | const updated = user[1][0]; 39 | res.send(updated); 40 | }) 41 | .catch(next) 42 | }) 43 | 44 | //implement so only admins can delete or users can delete themselves? 45 | router.delete('/:id', mustBeLoggedIn, (req, res, next) => { 46 | User.destroy({ 47 | where: { 48 | id: req.params.id 49 | } 50 | }) 51 | .then(() => { 52 | res.sendStatus(204); 53 | }) 54 | .catch(next) 55 | }) 56 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_labels.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Labels 3 | // -------------------------------------------------- 4 | 5 | .label { 6 | display: inline; 7 | padding: .2em .6em .3em; 8 | font-size: 75%; 9 | font-weight: bold; 10 | line-height: 1; 11 | color: $label-color; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25em; 16 | 17 | // [converter] extracted a& to a.label 18 | 19 | // Empty labels collapse automatically (not available in IE8) 20 | &:empty { 21 | display: none; 22 | } 23 | 24 | // Quick fix for labels in buttons 25 | .btn & { 26 | position: relative; 27 | top: -1px; 28 | } 29 | } 30 | 31 | // Add hover effects, but only for links 32 | a.label { 33 | &:hover, 34 | &:focus { 35 | color: $label-link-hover-color; 36 | text-decoration: none; 37 | cursor: pointer; 38 | } 39 | } 40 | 41 | // Colors 42 | // Contextual variations (linked labels get darker on :hover) 43 | 44 | .label-default { 45 | @include label-variant($label-default-bg); 46 | } 47 | 48 | .label-primary { 49 | @include label-variant($label-primary-bg); 50 | } 51 | 52 | .label-success { 53 | @include label-variant($label-success-bg); 54 | } 55 | 56 | .label-info { 57 | @include label-variant($label-info-bg); 58 | } 59 | 60 | .label-warning { 61 | @include label-variant($label-warning-bg); 62 | } 63 | 64 | .label-danger { 65 | @include label-variant($label-danger-bg); 66 | } 67 | -------------------------------------------------------------------------------- /tests/users.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest-as-promised') 2 | const { expect } = require('chai') 3 | const db = require('APP/db') 4 | const User = require('APP/db/models/user') 5 | const app = require('APP/server/start') 6 | 7 | describe('/api/users', () => { 8 | before('wait for the db', () => db.didSync); 9 | 10 | afterEach(() => { 11 | return db.sync({ force: true }); 12 | }) 13 | 14 | describe('when not logged in', () => { 15 | it('GET /:id fails 401 (Unauthorized)', () => 16 | request(app) 17 | .get(`/api/users/1`) 18 | .expect(401) 19 | ) 20 | 21 | it('POST creates a user', () => 22 | request(app) 23 | .post('/api/users') 24 | .send({ 25 | firstName: 'beth', 26 | lastName: 'wheeler', 27 | email: 'beth@secrets.org', 28 | password: '12345' 29 | }) 30 | .expect(201) 31 | ) 32 | 33 | it('POST redirects to the user it just made', () => 34 | request(app) 35 | .post('/api/users') 36 | .send({ 37 | firstName: 'eve', 38 | lastName: 'ye', 39 | email: 'eve@interloper.com', 40 | password: '23456', 41 | }) 42 | .redirects(1) 43 | .then(res => expect(res.body).to.contain({ 44 | email: 'eve@interloper.com' 45 | })) 46 | ) 47 | 48 | 49 | it('DELETE /:id fails 401 (Unauthorized)', () => 50 | request(app) 51 | .delete('/api/users/1') 52 | .expect(401) 53 | ) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /tests/user-reducer.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { createStore } from 'redux'; 3 | import userReducer from 'APP/app/user/user-reducer'; 4 | import { FETCH_SINGLE_USER, FETCH_ALL_USERS } from 'APP/app/user/user-actions'; 5 | 6 | describe('User reducer', () => { 7 | let testStore; 8 | 9 | beforeEach('Create testing store', () => { 10 | testStore = createStore(userReducer); 11 | }) 12 | 13 | it('Has expected initial state', () => { 14 | expect(testStore.getState()).to.be.deep.equal({ 15 | allUsers: [], 16 | currentUser: {} 17 | }) 18 | }) 19 | 20 | 21 | describe('FETCH_ALL_USERS', () => { 22 | it('Sets all users to the action\'s users property', () => { 23 | const action = {type: FETCH_ALL_USERS, users: [{}, {}, 'this', 'is', 'an', 'array', 'of', 'test', 'users']} 24 | testStore.dispatch(action) 25 | const newState = testStore.getState() 26 | expect(newState.allUsers).to.be.deep.equal(action.users) 27 | expect(newState.currentUser).to.be.deep.equal({}) 28 | }) 29 | }) 30 | 31 | describe('FETCH_SINGLE_USER', () => { 32 | it('Sets the single user to the action\'s user property', () => { 33 | const action = {type: FETCH_SINGLE_USER, user: { name: 'this is a test user' }}; 34 | testStore.dispatch(action) 35 | const newState = testStore.getState() 36 | expect(newState.currentUser).to.be.deep.equal(action.user) 37 | expect(newState.allUsers).to.be.deep.equal([]) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /app/auth/reducers/auth.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { browserHistory } from 'react-router' 3 | 4 | const reducer = (state=null, action) => { 5 | switch(action.type) { 6 | case AUTHENTICATED: 7 | return action.user 8 | } 9 | return state 10 | } 11 | 12 | const AUTHENTICATED = 'AUTHENTICATED' 13 | export const authenticated = user => ({ 14 | type: AUTHENTICATED, user 15 | }) 16 | 17 | export const login = (username, password) => { 18 | return (dispatch) => 19 | axios.post('/api/auth/local/login', 20 | {username, password}) 21 | .then(() => dispatch(whoami())) 22 | .then(() => browserHistory.push('/')) 23 | .catch(() => dispatch(whoami())) 24 | } 25 | 26 | export const signUp = (credentials) => { 27 | return (dispatch) => 28 | axios.post('/api/users', 29 | credentials) 30 | .then(res => res.data) 31 | .then(({email, password}) => dispatch(login(email, password))) 32 | .catch(() => dispatch(whoami())) 33 | } 34 | 35 | 36 | export const logout = () => 37 | dispatch => 38 | axios.post('/api/auth/logout') 39 | .then(() => dispatch(whoami())) 40 | .then(() => browserHistory.push('/')) 41 | .catch(() => dispatch(whoami())) 42 | 43 | export const whoami = () => 44 | dispatch => 45 | axios.get('/api/auth/whoami') 46 | .then(response => { 47 | const user = response.data 48 | dispatch(authenticated(user)) 49 | }) 50 | .catch(failed => dispatch(authenticated(null))) 51 | 52 | export default reducer 53 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_badges.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Badges 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | .badge { 8 | display: inline-block; 9 | min-width: 10px; 10 | padding: 3px 7px; 11 | font-size: $font-size-small; 12 | font-weight: $badge-font-weight; 13 | color: $badge-color; 14 | line-height: $badge-line-height; 15 | vertical-align: middle; 16 | white-space: nowrap; 17 | text-align: center; 18 | background-color: $badge-bg; 19 | border-radius: $badge-border-radius; 20 | 21 | // Empty badges collapse automatically (not available in IE8) 22 | &:empty { 23 | display: none; 24 | } 25 | 26 | // Quick fix for badges in buttons 27 | .btn & { 28 | position: relative; 29 | top: -1px; 30 | } 31 | 32 | .btn-xs &, 33 | .btn-group-xs > .btn & { 34 | top: 0; 35 | padding: 1px 5px; 36 | } 37 | 38 | // [converter] extracted a& to a.badge 39 | 40 | // Account for badges in navs 41 | .list-group-item.active > &, 42 | .nav-pills > .active > a > & { 43 | color: $badge-active-color; 44 | background-color: $badge-active-bg; 45 | } 46 | 47 | .list-group-item > & { 48 | float: right; 49 | } 50 | 51 | .list-group-item > & + & { 52 | margin-right: 5px; 53 | } 54 | 55 | .nav-pills > li > a > & { 56 | margin-left: 3px; 57 | } 58 | } 59 | 60 | // Hover state, but only for links 61 | a.badge { 62 | &:hover, 63 | &:focus { 64 | color: $badge-link-hover-color; 65 | text-decoration: none; 66 | cursor: pointer; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /db/models/book.js: -------------------------------------------------------------------------------- 1 | const db = require('APP/db'); 2 | const Sequelize = require('sequelize'); 3 | const Review = require('./review'); 4 | 5 | const Book = db.define('books', { 6 | title: { 7 | type: Sequelize.STRING, 8 | validate: { 9 | notEmpty: true 10 | } 11 | }, 12 | author: { 13 | type: Sequelize.STRING, 14 | validate: { 15 | notEmpty: true 16 | } 17 | }, 18 | genre: Sequelize.ARRAY(Sequelize.STRING), 19 | price: { 20 | type: Sequelize.FLOAT, 21 | allowNull: false 22 | }, 23 | description: { 24 | type: Sequelize.TEXT, 25 | validate: { 26 | notEmpty: true 27 | } 28 | }, 29 | stockCount: { 30 | type: Sequelize.INTEGER, 31 | defaultValue: 0 32 | }, 33 | imageUrl: { 34 | type: Sequelize.STRING, 35 | defaultValue: 'https://freeiconshop.com/files/edd/book-open-flat.png', 36 | validate: { 37 | isUrl: true 38 | } 39 | } 40 | // average: Sequelize.INTEGER 41 | }, { 42 | getterMethods: { 43 | //pulls the average rating from reviews of the book 44 | ratingAverage: function() { 45 | return Review.findAll({ 46 | attributes: ['rating'], 47 | where: { 48 | book_id: this.id 49 | } 50 | }) 51 | .then(ratings => { 52 | const length = ratings.length; 53 | let ratingsArr = ratings.map(instance => instance.rating); 54 | return ratingsArr.reduce((a, b) => { 55 | return a + b; },0) / length; 56 | }); 57 | } 58 | } 59 | }); 60 | 61 | module.exports = Book; 62 | -------------------------------------------------------------------------------- /tests/WhoAmI.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | import {spy} from 'sinon' 6 | chai.use(require('sinon-chai')) 7 | import {createStore} from 'redux' 8 | 9 | import WhoAmIContainer, {WhoAmI} from 'APP/app/auth/components/WhoAmI' 10 | 11 | describe('', () => { 12 | const user = { 13 | firstName: 'bonesy', 14 | lastName: 'wheeler' 15 | } 16 | const logout = spy() 17 | let root 18 | beforeEach('render the root', () => 19 | root = shallow() 20 | ) 21 | 22 | it('greets the user', () => { 23 | expect(root.text()).to.contain(user.firstName) 24 | }) 25 | 26 | it('has a logout button', () => { 27 | expect(root.find('button.logout')).to.have.length(1) 28 | }) 29 | 30 | it('calls props.logout when logout is tapped', () => { 31 | root.find('button.logout').simulate('click') 32 | expect(logout).to.have.been.called 33 | }) 34 | }) 35 | 36 | describe("'s connection", () => { 37 | const state = { 38 | auth: { firstName: 'bonesy', 39 | lastName: 'wheeler' } 40 | } 41 | 42 | let root, store, dispatch 43 | beforeEach('create store and render the root', () => { 44 | store = createStore(state => state, state) 45 | dispatch = spy(store, 'dispatch') 46 | root = shallow() 47 | }) 48 | 49 | it('gets prop.user from state.auth', () => { 50 | expect(root.find(WhoAmI)).to.have.prop('user').eql(state.auth) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /app/navbar/loginModal.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import WhoAmI from '../auth/components/WhoAmI' 3 | import Login from '../auth/components/Login' 4 | import { Link } from 'react-router' 5 | import store from '../store' 6 | 7 | export class LoginModal extends Component { 8 | constructor(props) { 9 | super(props) 10 | console.log(this.props) 11 | this.state = { auth: this.props.auth} 12 | } 13 | 14 | onLogin() { 15 | this.setState({auth: this.props.auth}) 16 | } 17 | 18 | render() { 19 | return ( 20 | 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/book.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db'); 4 | const Book = db.model('books'); 5 | const Review = db.model('reviews'); 6 | const router = require('express').Router() 7 | 8 | router.get('/', (req, res, next) => { 9 | Book.findAll() 10 | .then(foundBooks => res.send(foundBooks)) 11 | .catch(next) 12 | }) 13 | 14 | router.get('/:bookId', (req, res, next) => { 15 | Book.findOne({ 16 | where: { 17 | id: req.params.bookId 18 | }, 19 | include: [{ 20 | model: Review, 21 | where: { 22 | book_id: req.params.bookId 23 | }, 24 | required: false 25 | }] 26 | }) 27 | .then(foundBook => { 28 | res.send(foundBook) 29 | }) 30 | .catch(next) 31 | }) 32 | 33 | router.post('/', (req, res, next) => { 34 | if (!req.body.imageUrl) delete req.body.imageUrl 35 | if (typeof req.body.genre === 'string') req.body.genre = req.body.genre.split(', ') 36 | Book.create(req.body) 37 | .then(createdBook => res.status(201).send(createdBook)) 38 | .catch(next) 39 | }) 40 | 41 | router.delete('/:bookId', (req, res, next) => { 42 | Book.destroy({ 43 | where: { 44 | id: req.params.bookId 45 | }, 46 | returning: true 47 | }) 48 | .then(destroyedBook => { 49 | res.sendStatus(204) 50 | }) 51 | .catch(next) 52 | }) 53 | 54 | router.put('/:bookId', (req, res, next) => { 55 | Book.update(req.body, { 56 | where: { 57 | id: req.params.bookId 58 | }, 59 | returning: true 60 | }) 61 | .then(updatedBook => { 62 | res.send(updatedBook[1][0]) 63 | }) 64 | .catch(next) 65 | }) 66 | 67 | module.exports = router; 68 | -------------------------------------------------------------------------------- /app/order/singleOrder.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | class SingleOrder extends Component { 6 | constructor(props){ 7 | super(props) 8 | } 9 | 10 | render() { 11 | let date; 12 | return ( 13 |
14 | { this.props.currentOrder.length ? 15 |
16 |

Order Placed On: {this.props.currentOrder[0].created_at.slice(0, this.props.currentOrder[0].created_at.indexOf('T')) }

17 |

${this.props.currentOrder.map(book => book.price * book.selectedBooks.quantity).reduce((a, b) => a + b).toFixed(2)}

18 |
19 |
20 | : 21 |
22 | } 23 | 24 | { 25 | this.props.currentOrder && this.props.currentOrder.map((book,idx) => { 26 | date = book.created_at 27 | return ( 28 |
29 |

{book.title}

30 | 31 |

${book.price}

32 |

{book.selectedBooks.quantity === 1 ?

{book.selectedBooks.quantity} Copy

:

{book.selectedBooks.quantity} Copies

}

33 |

Total Cost: {(book.price * book.selectedBooks.quantity).toFixed(2)}

34 |
35 |
36 | )}) 37 | } 38 |
39 | ) 40 | } 41 | } 42 | 43 | export default SingleOrder; 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {resolve} = require('path') 4 | const chalk = require('chalk') 5 | const pkg = require('./package.json') 6 | const debug = require('debug')(`${pkg.name}:boot`) 7 | 8 | const nameError = 9 | `******************************************************************* 10 | You need to give your app a proper name. 11 | 12 | The package name 13 | 14 | ${pkg.name} 15 | 16 | isn't valid. If you don't change it, things won't work right. 17 | 18 | Please change it in ${__dirname}/package.json 19 | ~ xoxo, bones 20 | ********************************************************************` 21 | 22 | const reasonableName = /^[a-z0-9\-_]+$/ 23 | if (!reasonableName.test(pkg.name)) { 24 | console.error(chalk.red(nameError)) 25 | } 26 | 27 | // This will load a secrets file from 28 | // 29 | // ~/.your_app_name.env.js 30 | // or ~/.your_app_name.env.json 31 | // 32 | // and add it to the environment. 33 | const env = Object.create(process.env) 34 | , secretsFile = resolve(env.HOME, `.${pkg.name}.env`) 35 | try { 36 | Object.assign(env, require(secretsFile)) 37 | } catch (error) { 38 | debug('%s: %s', secretsFile, error.message) 39 | debug('%s: env file not found or invalid, moving on', secretsFile) 40 | } 41 | 42 | module.exports = { 43 | get name() { return pkg.name }, 44 | get isTesting() { return !!global.it }, 45 | get isProduction() { 46 | return process.env.NODE_ENV === 'production' 47 | }, 48 | get baseUrl() { 49 | return env.BASE_URL || `http://localhost:${PORT}` 50 | }, 51 | get port() { 52 | return env.PORT || 1337 53 | }, 54 | package: pkg, 55 | env, 56 | } 57 | -------------------------------------------------------------------------------- /app/book/authorsComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | import store from '../store'; 4 | import { selectBooks } from './book-actions'; 5 | 6 | 7 | export default class AuthorsComponent extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.setSelectedBooks = this.setSelectedBooks.bind(this); 11 | } 12 | 13 | setSelectedBooks (author) { 14 | event.preventDefault(); 15 | const filteredBooks = this.props.allBooks.filter(book => { 16 | if (book.author === author){ 17 | return book 18 | } 19 | }) 20 | store.dispatch(selectBooks(filteredBooks)) 21 | } 22 | 23 | render() { 24 | 25 | const arrayOfAuthors = this.props.allBooks.map(book => book.author) 26 | 27 | let filteredAuthors = arrayOfAuthors.filter((author, i) => arrayOfAuthors.indexOf(author) === i 28 | ) 29 | 30 | return ( 31 |
32 |

Authors

33 |
34 | { 35 | this.props.allBooks && filteredAuthors.map(author => ( 36 |
this.setSelectedBooks(author)} key={author}> 37 | 38 |
39 |
40 | { author } 41 |
42 |
43 | 44 |
45 | )) 46 | } 47 |
48 |
49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/order-reducer.test.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | 3 | import {createStore} from 'redux'; 4 | import orderReducer from 'APP/app/order/order-reducer'; 5 | 6 | describe('Order reducer', () => { 7 | 8 | let testStore; 9 | beforeEach('Create testing store', () => { 10 | testStore = createStore(orderReducer); 11 | }); 12 | 13 | it('has expected initial state', () => { 14 | expect(testStore.getState()).to.be.deep.equal({ 15 | allOrders: [], 16 | currentOrder: [], 17 | shoppingCart: [] 18 | }); 19 | }); 20 | 21 | describe('FETCH_SINGLE_ORDER', () => { 22 | 23 | it('sets order to action order', () => { 24 | testStore.dispatch({ type: 'FETCH_SINGLE_ORDER', currentOrder: {id:1, price:2, quantity: 3} }); 25 | const newState = testStore.getState(); 26 | expect(newState.currentOrder).to.be.deep.equal({id:1, price:2, quantity: 3}); 27 | }); 28 | 29 | }); 30 | 31 | describe('FETCH_ALL_ORDERS', () => { 32 | 33 | it('sets orders to action orders', () => { 34 | const newOrders = [ 35 | {id:1, price:2, quantity: 3}, 36 | {id:2, price:3, quantity: 2}, 37 | {id:4, price:20, quantity: 1} 38 | ]; 39 | testStore.dispatch({ type: 'FETCH_ALL_ORDERS', orders: newOrders }); 40 | const newState = testStore.getState(); 41 | expect(newState.allOrders).to.be.deep.equal([ 42 | {id:1, price:2, quantity: 3}, 43 | {id:2, price:3, quantity: 2}, 44 | {id:4, price:20, quantity: 1} 45 | ]); 46 | }); 47 | 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_code.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Code (inline and block) 3 | // -------------------------------------------------- 4 | 5 | 6 | // Inline and block code styles 7 | code, 8 | kbd, 9 | pre, 10 | samp { 11 | font-family: $font-family-monospace; 12 | } 13 | 14 | // Inline code 15 | code { 16 | padding: 2px 4px; 17 | font-size: 90%; 18 | color: $code-color; 19 | background-color: $code-bg; 20 | border-radius: $border-radius-base; 21 | } 22 | 23 | // User input typically entered via keyboard 24 | kbd { 25 | padding: 2px 4px; 26 | font-size: 90%; 27 | color: $kbd-color; 28 | background-color: $kbd-bg; 29 | border-radius: $border-radius-small; 30 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); 31 | 32 | kbd { 33 | padding: 0; 34 | font-size: 100%; 35 | font-weight: bold; 36 | box-shadow: none; 37 | } 38 | } 39 | 40 | // Blocks of code 41 | pre { 42 | display: block; 43 | padding: (($line-height-computed - 1) / 2); 44 | margin: 0 0 ($line-height-computed / 2); 45 | font-size: ($font-size-base - 1); // 14px to 13px 46 | line-height: $line-height-base; 47 | word-break: break-all; 48 | word-wrap: break-word; 49 | color: $pre-color; 50 | background-color: $pre-bg; 51 | border: 1px solid $pre-border-color; 52 | border-radius: $border-radius-base; 53 | 54 | // Account for some code outputs that place code tags in pre tags 55 | code { 56 | padding: 0; 57 | font-size: inherit; 58 | color: inherit; 59 | white-space: pre-wrap; 60 | background-color: transparent; 61 | border-radius: 0; 62 | } 63 | } 64 | 65 | // Enable scrollable blocks of code 66 | .pre-scrollable { 67 | max-height: $pre-scrollable-max-height; 68 | overflow-y: scroll; 69 | } 70 | -------------------------------------------------------------------------------- /app/order/orderList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | class OrderListComponent extends Component { 6 | constructor(props){ 7 | super(props) 8 | } 9 | 10 | render() { 11 | const orders = this.props.allOrders; 12 | return ( 13 |
14 |

Your Orders

15 |
{orders.length} Orders Placed
16 |
17 | { orders && orders.reverse().map(order => ( 18 |
19 |
20 |
21 |

Order Placed On {order[0].selectedBooks.created_at.slice(0, order[0].selectedBooks.created_at.indexOf('T'))}

22 |
{(order.map(book => book.selectedBooks.quantity).reduce((a, b) => a + b))} Copies Total:
23 | { order.map(book => ( 24 |
25 |

{book.title}

26 | 27 |
28 | )) } 29 |
30 |
${order.map(book => book.price * book.selectedBooks.quantity).reduce((a, b) => a + b).toFixed(2)} Total
31 | Order Details 32 |
33 |
34 | )) 35 | } 36 |
37 | ) 38 | } 39 | } 40 | 41 | 42 | 43 | export default OrderListComponent; 44 | -------------------------------------------------------------------------------- /stylesheets/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | // Core variables and mixins 8 | @import "bootstrap/variables"; 9 | @import "bootstrap/mixins"; 10 | 11 | // Reset and dependencies 12 | @import "bootstrap/normalize"; 13 | @import "bootstrap/print"; 14 | @import "bootstrap/glyphicons"; 15 | 16 | // Core CSS 17 | @import "bootstrap/scaffolding"; 18 | @import "bootstrap/type"; 19 | @import "bootstrap/code"; 20 | @import "bootstrap/grid"; 21 | @import "bootstrap/tables"; 22 | @import "bootstrap/forms"; 23 | @import "bootstrap/buttons"; 24 | 25 | // Components 26 | @import "bootstrap/component-animations"; 27 | @import "bootstrap/dropdowns"; 28 | @import "bootstrap/button-groups"; 29 | @import "bootstrap/input-groups"; 30 | @import "bootstrap/navs"; 31 | @import "bootstrap/navbar"; 32 | @import "bootstrap/breadcrumbs"; 33 | @import "bootstrap/pagination"; 34 | @import "bootstrap/pager"; 35 | @import "bootstrap/labels"; 36 | @import "bootstrap/badges"; 37 | @import "bootstrap/jumbotron"; 38 | @import "bootstrap/thumbnails"; 39 | @import "bootstrap/alerts"; 40 | @import "bootstrap/progress-bars"; 41 | @import "bootstrap/media"; 42 | @import "bootstrap/list-group"; 43 | @import "bootstrap/panels"; 44 | @import "bootstrap/responsive-embed"; 45 | @import "bootstrap/wells"; 46 | @import "bootstrap/close"; 47 | 48 | // Components w/ JavaScript 49 | @import "bootstrap/modals"; 50 | @import "bootstrap/tooltip"; 51 | @import "bootstrap/popovers"; 52 | @import "bootstrap/carousel"; 53 | 54 | // Utility classes 55 | @import "bootstrap/utilities"; 56 | @import "bootstrap/responsive-utilities"; 57 | -------------------------------------------------------------------------------- /app/review/newReviewFormContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import NewReviewForm from './newReviewForm'; 4 | import { addNewReview } from './reviewActionCreator' 5 | 6 | 7 | const mapDispatchToProps = (dispatch) => { 8 | return { 9 | addNewReview (review) { 10 | dispatch(addNewReview(review)) 11 | } 12 | } 13 | } 14 | 15 | class NewReviewWrapper extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | rating: 0, 20 | content: '' 21 | }; 22 | this.handleSubmit = this.handleSubmit.bind(this); 23 | this.handleChange = this.handleChange.bind(this); 24 | } 25 | 26 | 27 | handleChange(event) { 28 | const name = event.target.name; 29 | 30 | //this syntax allows us to pass a variable name into the object literal, this name will be the state property and is coming off the 'name' html properties in NewReviewForm 31 | this.setState({ 32 | [name]: event.target.value 33 | }) 34 | } 35 | 36 | 37 | handleSubmit(event) { 38 | event.preventDefault(); 39 | this.props.addNewReview(Object.assign({}, this.state, { book_id: this.props.book.id })); 40 | 41 | this.setState({ 42 | rating: 0, 43 | content: '' 44 | }) 45 | } 46 | 47 | render() { 48 | return ( 49 | 55 | ) 56 | } 57 | } 58 | 59 | export default connect(null, mapDispatchToProps)(NewReviewWrapper) 60 | 61 | 62 | //pass this into the book view so you can pass current book down to it 63 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_buttons.scss: -------------------------------------------------------------------------------- 1 | // Button variants 2 | // 3 | // Easily pump out default styles, as well as :hover, :focus, :active, 4 | // and disabled options for all buttons 5 | 6 | @mixin button-variant($color, $background, $border) { 7 | color: $color; 8 | background-color: $background; 9 | border-color: $border; 10 | 11 | &:focus, 12 | &.focus { 13 | color: $color; 14 | background-color: darken($background, 10%); 15 | border-color: darken($border, 25%); 16 | } 17 | &:hover { 18 | color: $color; 19 | background-color: darken($background, 10%); 20 | border-color: darken($border, 12%); 21 | } 22 | &:active, 23 | &.active, 24 | .open > &.dropdown-toggle { 25 | color: $color; 26 | background-color: darken($background, 10%); 27 | border-color: darken($border, 12%); 28 | 29 | &:hover, 30 | &:focus, 31 | &.focus { 32 | color: $color; 33 | background-color: darken($background, 17%); 34 | border-color: darken($border, 25%); 35 | } 36 | } 37 | &:active, 38 | &.active, 39 | .open > &.dropdown-toggle { 40 | background-image: none; 41 | } 42 | &.disabled, 43 | &[disabled], 44 | fieldset[disabled] & { 45 | &:hover, 46 | &:focus, 47 | &.focus { 48 | background-color: $background; 49 | border-color: $border; 50 | } 51 | } 52 | 53 | .badge { 54 | color: $background; 55 | background-color: $color; 56 | } 57 | } 58 | 59 | // Button sizes 60 | @mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) { 61 | padding: $padding-vertical $padding-horizontal; 62 | font-size: $font-size; 63 | line-height: $line-height; 64 | border-radius: $border-radius; 65 | } 66 | -------------------------------------------------------------------------------- /stylesheets/general.scss: -------------------------------------------------------------------------------- 1 | //++++++++++++++++++++++COLORS++++++++++++++++++++++ 2 | $primary-color: #fdb608; 3 | $default-color: #4d4d4e; 4 | $dark-theme: #181818; 5 | $logo-background: #070707; 6 | $background: #252627; 7 | //++++++++++++++++++++++COLORS++++++++++++++++++++++ 8 | 9 | 10 | //++++++++++++++++++++++HEIGHTS+++++++++++++++++++++ 11 | $footer-height: 30px; 12 | //++++++++++++++++++++++HEIGHTS+++++++++++++++++++++ 13 | 14 | html { 15 | height: 100%; 16 | color: #222; 17 | font-size: 1em; 18 | line-height: 1.4; 19 | } 20 | 21 | li { 22 | text-decoration: none 23 | } 24 | 25 | body { 26 | height: 100%; 27 | margin: 0; 28 | background: $background !important; 29 | //overflow: hidden; 30 | display: block; 31 | } 32 | 33 | #main { 34 | height: 100%; 35 | div[data-reactroot] { 36 | height: 100%; 37 | #mainDisplay { 38 | height: 100% 39 | } 40 | } 41 | } 42 | 43 | .col-centered{ 44 | margin-left: 20%; 45 | } 46 | 47 | a { 48 | background-color: transparent; 49 | text-decoration: none; 50 | color: $default-color; 51 | } 52 | 53 | a:hover { 54 | text-decoration: none; 55 | } 56 | 57 | audio, canvas, iframe, img, svg, video { 58 | vertical-align: middle; 59 | } 60 | 61 | *, *:after, *:before { 62 | -moz-box-sizing: border-box; 63 | box-sizing: border-box; 64 | -webkit-font-smoothing: antialiased; 65 | font-smoothing: antialiased; 66 | text-rendering: optimizeLegibility; 67 | } 68 | 69 | ::selection { 70 | background: #b3d4fc; 71 | text-shadow: none; 72 | } 73 | 74 | ::selection { 75 | background: #04A4CC; 76 | color: #FFF; 77 | text-shadow: none; 78 | } 79 | 80 | #authornamelist { 81 | text-align: center; 82 | } 83 | 84 | 85 | -------------------------------------------------------------------------------- /app/order/ShoppingCartComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | import { orderShoppingCart } from './order-actions' 4 | import store from '../store' 5 | 6 | 7 | export default class ShoppingCartComponent extends Component { 8 | constructor(props) { 9 | super(props) 10 | this.orderCart = this.orderCart.bind(this) 11 | } 12 | 13 | orderCart() { 14 | event.preventDefault(); 15 | store.dispatch(orderShoppingCart(this.props.user.id)) 16 | } 17 | 18 | render () { 19 | return ( 20 |
21 |

Your Cart

22 | { 23 | this.props.shoppingCart && this.props.shoppingCart.map(book => ( 24 |
25 | 26 | 27 |
28 |
29 | { book.title } 30 | { book.author } 31 | { book.price } 32 | { book.selectedBooks.quantity } 33 | Total price: { (book.price * book.selectedBooks.quantity).toFixed(2) } 34 |
35 |
36 | 37 |
38 | )) 39 | } 40 | { this.props.shoppingCart.length ? :

Add something to your cart!

41 | } 42 |
43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_grid.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Grid system 3 | // -------------------------------------------------- 4 | 5 | 6 | // Container widths 7 | // 8 | // Set the container width, and override it for fixed navbars in media queries. 9 | 10 | .container { 11 | @include container-fixed; 12 | 13 | @media (min-width: $screen-sm-min) { 14 | width: $container-sm; 15 | } 16 | @media (min-width: $screen-md-min) { 17 | width: $container-md; 18 | } 19 | @media (min-width: $screen-lg-min) { 20 | width: $container-lg; 21 | } 22 | } 23 | 24 | 25 | // Fluid container 26 | // 27 | // Utilizes the mixin meant for fixed width containers, but without any defined 28 | // width for fluid, full width layouts. 29 | 30 | .container-fluid { 31 | @include container-fixed; 32 | } 33 | 34 | 35 | // Row 36 | // 37 | // Rows contain and clear the floats of your columns. 38 | 39 | .row { 40 | @include make-row; 41 | } 42 | 43 | 44 | // Columns 45 | // 46 | // Common styles for small and large grid columns 47 | 48 | @include make-grid-columns; 49 | 50 | 51 | // Extra small grid 52 | // 53 | // Columns, offsets, pushes, and pulls for extra small devices like 54 | // smartphones. 55 | 56 | @include make-grid(xs); 57 | 58 | 59 | // Small grid 60 | // 61 | // Columns, offsets, pushes, and pulls for the small device range, from phones 62 | // to tablets. 63 | 64 | @media (min-width: $screen-sm-min) { 65 | @include make-grid(sm); 66 | } 67 | 68 | 69 | // Medium grid 70 | // 71 | // Columns, offsets, pushes, and pulls for the desktop device range. 72 | 73 | @media (min-width: $screen-md-min) { 74 | @include make-grid(md); 75 | } 76 | 77 | 78 | // Large grid 79 | // 80 | // Columns, offsets, pushes, and pulls for the large desktop device range. 81 | 82 | @media (min-width: $screen-lg-min) { 83 | @include make-grid(lg); 84 | } 85 | -------------------------------------------------------------------------------- /tests/Login.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | import {spy} from 'sinon' 6 | chai.use(require('sinon-chai')) 7 | 8 | import {Login} from 'APP/app/auth/components/Login' 9 | 10 | describe('', () => { 11 | let root 12 | beforeEach('render the root', () => 13 | root = shallow() 14 | ) 15 | 16 | it('shows a login form', () => { 17 | expect(root.find('input[name="username"]')).to.have.length(1) 18 | expect(root.find('input[name="password"]')).to.have.length(1) 19 | }) 20 | 21 | it('shows a password field', () => { 22 | const pw = root.find('input[name="password"]') 23 | expect(pw).to.have.length(1) 24 | expect(pw.at(0)).to.have.attr('type').equals('password') 25 | }) 26 | 27 | it('has a login button', () => { 28 | const submit = root.find('input[type="submit"]') 29 | expect(submit).to.have.length(1) 30 | }) 31 | 32 | describe('when submitted', () => { 33 | const login = spy() 34 | const root = shallow() 35 | const submitEvent = { 36 | preventDefault: spy(), 37 | target: { 38 | username: {value: 'bones@example.com'}, 39 | password: {value: '12345'}, 40 | } 41 | } 42 | 43 | beforeEach('submit', () => { 44 | login.reset() 45 | submitEvent.preventDefault.reset() 46 | root.simulate('submit', submitEvent) 47 | }) 48 | 49 | it('calls props.login with credentials', () => { 50 | expect(login).to.have.been.calledWith( 51 | submitEvent.target.username.value, 52 | submitEvent.target.password.value, 53 | ) 54 | }) 55 | 56 | it('calls preventDefault', () => { 57 | expect(submitEvent.preventDefault).to.have.been.called 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /server/start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | const bodyParser = require('body-parser') 5 | const {resolve} = require('path') 6 | const passport = require('passport') 7 | 8 | // Bones has a symlink from node_modules/APP to the root of the app. 9 | // That means that we can require paths relative to the app root by 10 | // saying require('APP/whatever'). 11 | // 12 | // This next line requires our root index.js: 13 | const pkg = require('APP') 14 | 15 | const app = express() 16 | 17 | if (!pkg.isProduction && !pkg.isTesting) { 18 | // Logging middleware (dev only) 19 | app.use(require('volleyball')) 20 | } 21 | 22 | module.exports = app 23 | // We'll store the whole session in a cookie 24 | .use(require('cookie-session') ({ 25 | name: 'session', 26 | keys: [process.env.SESSION_SECRET || 'an insecure secret key'], 27 | })) 28 | 29 | // Body parsing middleware 30 | .use(bodyParser.urlencoded({ extended: true })) 31 | .use(bodyParser.json()) 32 | 33 | // Authentication middleware 34 | .use(passport.initialize()) 35 | .use(passport.session()) 36 | 37 | // Serve static files from ../public 38 | .use(express.static(resolve(__dirname, '..', 'public'))) 39 | 40 | // Serve our api 41 | .use('/api', require('./api')) 42 | 43 | // Send index.html for anything else. 44 | .get('/*', (_, res) => res.sendFile(resolve(__dirname, '..', 'public', 'index.html'))) 45 | 46 | if (module === require.main) { 47 | // Start listening only if we're the main module. 48 | // 49 | // https://nodejs.org/api/modules.html#modules_accessing_the_main_module 50 | const server = app.listen( 51 | process.env.PORT || 1337, 52 | () => { 53 | console.log(`--- Started HTTP Server for ${pkg.name} ---`) 54 | console.log(`Listening on ${JSON.stringify(server.address())}`) 55 | } 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_alerts.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Alerts 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // ------------------------- 8 | 9 | .alert { 10 | padding: $alert-padding; 11 | margin-bottom: $line-height-computed; 12 | border: 1px solid transparent; 13 | border-radius: $alert-border-radius; 14 | 15 | // Headings for larger alerts 16 | h4 { 17 | margin-top: 0; 18 | // Specified for the h4 to prevent conflicts of changing $headings-color 19 | color: inherit; 20 | } 21 | 22 | // Provide class for links that match alerts 23 | .alert-link { 24 | font-weight: $alert-link-font-weight; 25 | } 26 | 27 | // Improve alignment and spacing of inner content 28 | > p, 29 | > ul { 30 | margin-bottom: 0; 31 | } 32 | 33 | > p + p { 34 | margin-top: 5px; 35 | } 36 | } 37 | 38 | // Dismissible alerts 39 | // 40 | // Expand the right padding and account for the close button's positioning. 41 | 42 | .alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. 43 | .alert-dismissible { 44 | padding-right: ($alert-padding + 20); 45 | 46 | // Adjust close link position 47 | .close { 48 | position: relative; 49 | top: -2px; 50 | right: -21px; 51 | color: inherit; 52 | } 53 | } 54 | 55 | // Alternate styles 56 | // 57 | // Generate contextual modifier classes for colorizing the alert. 58 | 59 | .alert-success { 60 | @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); 61 | } 62 | 63 | .alert-info { 64 | @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); 65 | } 66 | 67 | .alert-warning { 68 | @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); 69 | } 70 | 71 | .alert-danger { 72 | @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); 73 | } 74 | -------------------------------------------------------------------------------- /app/auth/components/newUserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import NewUserComponent from './newUserComponent'; 4 | import { signUp } from '../reducers/auth' 5 | import store from '../../store' 6 | 7 | 8 | const mapDispatchToProps = (dispatch) => { 9 | return { 10 | signUp (user) { 11 | dispatch(signUp(user)) 12 | } 13 | } 14 | } 15 | 16 | class NewUserWrapper extends Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | firstName: '', 21 | lastName: '', 22 | email: '', 23 | password: '' 24 | }; 25 | this.handleSubmit = this.handleSubmit.bind(this); 26 | this.handleChange = this.handleChange.bind(this); 27 | } 28 | 29 | 30 | handleChange(event) { 31 | const name = event.target.name; 32 | 33 | //this syntax allows us to pass a variable name into the object literal, this name will be the state property and is coming off the 'name' html properties in NewReviewForm 34 | this.setState({ 35 | [name]: event.target.value 36 | }) 37 | } 38 | 39 | 40 | handleSubmit(event) { 41 | event.preventDefault(); 42 | this.props.signUp(this.state); 43 | 44 | this.setState({ 45 | firstName: '', 46 | lastName: '', 47 | email: '', 48 | password: '' 49 | }) 50 | } 51 | 52 | render() { 53 | return ( 54 | 62 | ) 63 | } 64 | } 65 | 66 | export default connect(null, mapDispatchToProps)(NewUserWrapper) 67 | 68 | 69 | //pass this into the book view so you can pass current book down to it 70 | -------------------------------------------------------------------------------- /tests/reviews.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest-as-promised') 2 | const {expect} = require('chai') 3 | const db = require('APP/db') 4 | const Review = require('APP/db/models/review') 5 | const app = require('APP/server/start') 6 | 7 | describe('Review routes', () => { 8 | before('wait for the db', () => db.didSync); 9 | 10 | beforeEach(() => { 11 | return Review.create({ 12 | rating: 3.5, 13 | content: 'LOLOLOLOLOLOLOLOLOLOL. SO FUNNY.' 14 | }) 15 | }) 16 | 17 | afterEach(() => { 18 | return db.sync({force: true}); 19 | }) 20 | 21 | it('GET gets all review', () => { 22 | return request(app) 23 | .get('/api/reviews') 24 | .expect(200) 25 | .then(res => { 26 | expect(res.body.length).to.equal(1) 27 | }); 28 | }) 29 | 30 | it('POST creates a review', () => { 31 | return request(app) 32 | .post('/api/reviews') 33 | .send({ 34 | rating: 5.0, 35 | content: 'This movie was good, really good. I mean really really REALLY good.' 36 | }) 37 | .expect(201) 38 | .then(res => { 39 | expect(res.body.rating).to.equal(5.0) 40 | expect(res.body.content).to.equal('This movie was good, really good. I mean really really REALLY good.') 41 | }) 42 | }) 43 | 44 | it('PUT updates a review', () => { 45 | return request(app) 46 | .put('/api/reviews/1') 47 | .send({ 48 | rating: 4.7, 49 | content: 'I love this movie. It was awesome. Best. Movie. Ever.' 50 | }) 51 | .expect(200) 52 | .then(res => { 53 | expect(res.body.rating).to.equal(4.7) 54 | expect(res.body.content).to.equal('I love this movie. It was awesome. Best. Movie. Ever.') 55 | }) 56 | }) 57 | 58 | it('DELETE deletes a review', () => { 59 | return request(app) 60 | .delete('/api/reviews/1') 61 | .expect(204) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /stylesheets/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/book/newBookFormContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import NewBookForm from './newBookForm' 4 | import { addNewBook } from './book-actions' 5 | 6 | 7 | const mapDispatchToProps = (dispatch) => { 8 | return { 9 | addNewBook (book) { 10 | dispatch(addNewBook(book)) 11 | } 12 | } 13 | } 14 | 15 | class NewBookWrapper extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | title: '', 20 | author: '', 21 | price: 0, 22 | description: '', 23 | stockCount: 0, 24 | imageUrl: '', 25 | genre: '' 26 | }; 27 | this.handleSubmit = this.handleSubmit.bind(this); 28 | this.handleChange = this.handleChange.bind(this); 29 | } 30 | 31 | 32 | handleChange(event) { 33 | const name = event.target.name; 34 | 35 | this.setState({ 36 | [name]: event.target.value 37 | }) 38 | } 39 | 40 | handleSubmit(event) { 41 | console.log('IN HANDLE SUBMIT') 42 | event.preventDefault(); 43 | this.props.addNewBook(this.state); 44 | 45 | this.setState({ 46 | title: '', 47 | author: '', 48 | price: 0, 49 | description: '', 50 | stockCount: 0, 51 | imageUrl: '', 52 | genre: [''] 53 | }) 54 | } 55 | 56 | render() { 57 | return ( 58 | 69 | ) 70 | } 71 | } 72 | 73 | export default connect(null, mapDispatchToProps)(NewBookWrapper) 74 | 75 | 76 | //pass this into the book view so you can pass current book down to it 77 | -------------------------------------------------------------------------------- /tests/orders.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest-as-promised') 2 | const {expect} = require('chai') 3 | const db = require('APP/db') 4 | const Order = require('APP/db/models/orders') 5 | const app = require('APP/server/start') 6 | 7 | 8 | xdescribe('/api/orders', () => { 9 | 10 | describe('when ', () => { 11 | before('wait for the db', () => db.didSync); 12 | beforeEach(function() { 13 | return Order.create({ 14 | selected: [ 15 | {id: 1, price: 1.00, quantity: 1}, 16 | {id: 2, price: 2.00, quantity: 2}, 17 | {id: 3, price: 3.21, quantity: 3} 18 | ] 19 | }) 20 | }); 21 | 22 | afterEach(function(){ 23 | return db.sync({force: true}); 24 | }); 25 | 26 | it('GET / returns all orders', () => 27 | request(app) 28 | .get(`/api/orders`) 29 | .expect(200) 30 | ) 31 | 32 | it('GET /:id returns one order', () => 33 | request(app) 34 | .get(`/api/orders/1`) 35 | .expect(200) 36 | .then( res => { 37 | return expect(res.body.selected.length).to.equal(3); 38 | }) 39 | ) 40 | 41 | it('POST creates an order', () => 42 | request(app) 43 | .post('/api/orders') 44 | .send({ 45 | selected: [ 46 | {id: 1, price: 1, quantity: 2}, 47 | {id: 2, price: 5, quantity: 4}] 48 | }) 49 | .expect(201) 50 | ) 51 | 52 | it('PUT updates the element ', () => 53 | request(app) 54 | .put('/api/orders/1') 55 | .send({selected: [ 56 | {id: 1, price: 1, quantity: 2}, 57 | {id: 2, price: 5, quantity: 4}] 58 | }) 59 | .then(updatedOrder => { 60 | return Order.findById(1) 61 | .then( order => { 62 | return expect(order.selected[1].price).to.equal(2); 63 | }) 64 | }) 65 | ) 66 | 67 | it('DELETE removes an order', () => 68 | request(app) 69 | .delete('/api/orders/1') 70 | .expect(202) 71 | ) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /app/auth/components/newUserComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export default function ( { handleChange, handleSubmit, firstName, lastName, email, password }) { 5 | return ( 6 |
7 |
8 | New User 9 |
10 | 11 |
12 | 13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 | ); 46 | } 47 | 48 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const debug = require('debug')('sql') 3 | const chalk = require('chalk') 4 | const Sequelize = require('sequelize') 5 | const app = require('APP') 6 | 7 | const name = (process.env.DATABASE_NAME || app.name) + 8 | (app.isTesting ? '_test' : '') 9 | 10 | const url = process.env.DATABASE_URL || `postgres://localhost:5432/${name}` 11 | 12 | console.log(chalk.yellow(`Opening database connection to ${url}`)); 13 | 14 | // create the database instance 15 | const db = module.exports = new Sequelize(url, { 16 | logging: debug, // export DEBUG=sql in the environment to get SQL queries 17 | native: true, // lets Sequelize know we can use pg-native for ~30% more speed 18 | define: { 19 | underscored: true, // use snake_case rather than camelCase column names 20 | freezeTableName: true, // don't change table names from the one specified 21 | timestamps: true, // automatically include timestamp columns 22 | } 23 | }) 24 | 25 | // pull in our models 26 | require('./models') 27 | 28 | // sync the db, creating it if necessary 29 | function sync(force=app.isTesting, retries=0, maxRetries=5) { 30 | return db.sync({force}) 31 | .then(ok => console.log(`Synced models to db ${url}`)) 32 | .catch(fail => { 33 | // Don't do this auto-create nonsense in prod, or 34 | // if we've retried too many times. 35 | if (app.isProduction || retries > maxRetries) { 36 | console.error(chalk.red(`********** database error ***********`)) 37 | console.error(chalk.red(` Couldn't connect to ${url}`)) 38 | console.error() 39 | console.error(chalk.red(fail)) 40 | console.error(chalk.red(`*************************************`)) 41 | return 42 | } 43 | // Otherwise, do this autocreate nonsense 44 | console.log(`${retries ? `[retry ${retries}]` : ''} Creating database ${name}...`) 45 | return new Promise((resolve, reject) => 46 | require('child_process').exec(`createdb "${name}"`, resolve) 47 | ).then(() => sync(true, retries + 1)) 48 | }) 49 | } 50 | 51 | db.didSync = sync() 52 | -------------------------------------------------------------------------------- /db/models/oauth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('oauth') 4 | const Sequelize = require('sequelize') 5 | const db = require('APP/db') 6 | 7 | const OAuth = db.define('oauths', { 8 | uid: Sequelize.STRING, 9 | provider: Sequelize.STRING, 10 | 11 | // OAuth v2 fields 12 | accessToken: Sequelize.STRING, 13 | refreshToken: Sequelize.STRING, 14 | 15 | // OAuth v1 fields 16 | token: Sequelize.STRING, 17 | tokenSecret: Sequelize.STRING, 18 | 19 | // The whole profile as JSON 20 | profileJson: Sequelize.JSON, 21 | }, { 22 | indexes: [{fields: ['uid'], unique: true,}], 23 | }) 24 | 25 | OAuth.V2 = (accessToken, refreshToken, profile, done) => 26 | this.findOrCreate({ 27 | where: { 28 | provider: profile.provider, 29 | uid: profile.id, 30 | }}) 31 | .then(oauth => { 32 | debug('provider:%s will log in user:{name=%s uid=%s}', 33 | profile.provider, 34 | profile.displayName, 35 | token.uid) 36 | oauth.profileJson = profile 37 | return db.Promise.props({ 38 | oauth, 39 | user: token.getUser(), 40 | _saveProfile: oauth.save(), 41 | }) 42 | }) 43 | .then(({ oauth, user }) => user || 44 | User.create({ 45 | name: profile.displayName, 46 | }).then(user => db.Promise.props({ 47 | user, 48 | _setOauthUser: oauth.setUser(user) 49 | })) 50 | ) 51 | .then(({ user }) => done(null, user)) 52 | .catch(done) 53 | 54 | 55 | OAuth.setupStrategy = 56 | ({ 57 | provider, 58 | strategy, 59 | config, 60 | oauth=OAuth.V2, 61 | passport 62 | }) => { 63 | const undefinedKeys = Object.keys(config) 64 | .map(k => config[k]) 65 | .filter(value => typeof value === 'undefined') 66 | if (undefinedKeys.length) { 67 | undefinedKeys.forEach(key => 68 | debug('provider:%s: needs environment var %s', provider, key)) 69 | debug('provider:%s will not initialize', provider) 70 | return 71 | } 72 | 73 | debug('initializing provider:%s', provider) 74 | passport.use(new strategy(config, oauth)) 75 | } 76 | 77 | module.exports = OAuth -------------------------------------------------------------------------------- /app/book/book-actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { browserHistory } from 'react-router' 3 | 4 | /*********************************CONSTS******************************/ 5 | 6 | export const FETCH_ALL_BOOKS = 'FETCH_ALL_BOOKS'; 7 | export const FETCH_SINGLE_BOOK = 'FETCH_SINGLE_BOOK'; 8 | export const SET_GENRE = 'SET_GENRE'; 9 | export const GET_AUTHOR = 'GET_AUTHOR'; 10 | export const SELECTED_BOOKS = 'SELECTED_BOOKS'; 11 | 12 | /****************************ACTION CREATORS****************************/ 13 | export function getAllBooks(books) { 14 | return { 15 | type: FETCH_ALL_BOOKS, 16 | books 17 | } 18 | } 19 | 20 | export function getSingleBook(book) { 21 | return { 22 | type: FETCH_SINGLE_BOOK, 23 | book 24 | } 25 | } 26 | 27 | export function setGenre(genre) { 28 | return { 29 | type: SET_GENRE, 30 | genre 31 | } 32 | } 33 | 34 | export function getAuthor(author) { 35 | return { 36 | type: GET_AUTHOR, 37 | author 38 | } 39 | } 40 | 41 | export function selectBooks(selectedBooks) { 42 | return { 43 | type: SELECTED_BOOKS, 44 | selectedBooks 45 | } 46 | } 47 | 48 | 49 | /*************************THUNKS*********************************/ 50 | 51 | export function fetchAllBooks() { 52 | return function(dispatch) { 53 | axios.get('/api/books') 54 | .then(res => res.data) 55 | .then(foundBooks => { 56 | dispatch(getAllBooks(foundBooks)) 57 | }) 58 | .catch(console.error) 59 | } 60 | } 61 | 62 | export function fetchSingleBook(id) { 63 | return function(dispatch) { 64 | axios.get(`/api/books/${id}`) 65 | .then(res => { 66 | return res.data 67 | }) 68 | .then(foundBook => { 69 | dispatch(getSingleBook(foundBook)) 70 | }) 71 | .catch(console.error) 72 | } 73 | } 74 | 75 | export function addNewBook (book) { 76 | return function(dispatch, getState) { 77 | axios.post('/api/books', book) 78 | .then(res => res.data) 79 | .then(newBook => { 80 | dispatch(fetchSingleBook(newBook.id)) 81 | browserHistory.push(`/books/${newBook.id}`) 82 | }) 83 | .catch(console.error) 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /db/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const bcrypt = require('bcrypt') 4 | const Sequelize = require('sequelize') 5 | const db = require('APP/db') 6 | 7 | const User = db.define('users', { 8 | firstName: { 9 | type: Sequelize.STRING, 10 | allowNull: false, 11 | validate: { 12 | notEmpty: true 13 | } 14 | }, 15 | lastName: { 16 | type: Sequelize.STRING, 17 | allowNull: false, 18 | validate: { 19 | notEmpty: true 20 | } 21 | }, 22 | email: { 23 | type: Sequelize.STRING, 24 | validate: { 25 | isEmail: true, 26 | notEmpty: true, 27 | } 28 | }, 29 | adminStatus: { 30 | type: Sequelize.BOOLEAN, 31 | defaultValue: false 32 | }, 33 | shoppingCart: { 34 | type: Sequelize.ARRAY(Sequelize.INTEGER), 35 | defaultValue: [] 36 | }, 37 | // We support oauth, so users may or may not have passwords. 38 | password_digest: Sequelize.STRING, 39 | password: Sequelize.VIRTUAL 40 | }, { 41 | indexes: [{fields: ['email'], unique: true}], 42 | hooks: { 43 | beforeCreate: setEmailAndPassword, 44 | beforeUpdate: setEmailAndPassword, 45 | }, 46 | instanceMethods: { 47 | authenticate(plaintext) { 48 | return new Promise((resolve, reject) => 49 | bcrypt.compare(plaintext, this.password_digest, 50 | (err, result) => 51 | err ? reject(err) : resolve(result)) 52 | ) 53 | } 54 | }, 55 | getterMethods: { 56 | fullName: function() { 57 | return this.firstName + ' ' + this.lastName; 58 | } 59 | }, 60 | setterMethods: { 61 | fullName: function(value) { 62 | let names = value.split(' '); 63 | 64 | this.setDataValue('firstName', names.slice(0, -1).join(' ')) 65 | this.setDataValue('lastName', names.slice(-1).join(' ')) 66 | } 67 | } 68 | }) 69 | 70 | function setEmailAndPassword(user) { 71 | user.email = user.email && user.email.toLowerCase() 72 | if (!user.password) return Promise.resolve(user) 73 | 74 | return new Promise((resolve, reject) => 75 | bcrypt.hash(user.get('password'), 10, (err, hash) => { 76 | if (err) reject(err) 77 | user.set('password_digest', hash) 78 | resolve(user) 79 | }) 80 | ) 81 | } 82 | 83 | module.exports = User 84 | -------------------------------------------------------------------------------- /app/navbar/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import WhoAmI from '../auth/components/WhoAmI' 3 | import Login from '../auth/components/Login' 4 | import newUser from '../auth/components/newUserComponent' 5 | import { Link } from 'react-router' 6 | import store from '../store' 7 | import { setGenre } from '../book/book-actions' 8 | 9 | 10 | export default class Navbar extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | menuClicked: false, 15 | active: true 16 | } 17 | this.showMenu = this.showMenu.bind(this); 18 | this.bookLinkClick = this.bookLinkClick.bind(this); 19 | } 20 | 21 | showMenu () { 22 | if (this.state.menuClicked) { 23 | this.setState({menuClicked: false}) 24 | } else { 25 | this.setState({menuClicked: true}) 26 | } 27 | } 28 | 29 | bookLinkClick() { 30 | event.preventDefault(); 31 | store.dispatch(setGenre('')) 32 | } 33 | 34 | render() { 35 | return ( 36 | 59 | ); 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hi, I'm bones 2 | 3 | I'm a happy little skeleton. You can clone me to use as a starter on your projects! 4 | I have React, Redux, Sequelize, and Express all just rattling around in here ready 5 | to go. 6 | 7 | ## I need node >= 6.7.0 8 | 9 | If you don't have it, I'll complain and tell you how to install it. 10 | 11 | ## 1. Make me into something! 12 | 13 | Create a git repo however you want to. You can fork me on Github, but you can only do 14 | that once (so weird!). You can also create a Github repo and clone it, or just do 15 | `git init` in an empty directory on your machine. 16 | 17 | After you have a repo on your machine: 18 | 19 | ``` 20 | git remote add bones https://github.com/queerviolet/bones.git 21 | git fetch bones 22 | git merge bones/master 23 | ``` 24 | 25 | And then you'll have me! If I change—which I probably will—you can get the most recent 26 | version by doing this again: 27 | 28 | ``` 29 | git fetch bones 30 | git merge bones/master 31 | ``` 32 | 33 | ## 2. I need a name. 34 | 35 | I don't have a name. I think I used to have one, but it turned to dust right along with my 36 | heart and liver and pituitary gland and all that stuff. 37 | 38 | Anyway, I'll need one. Give me a name in `package.json`. 39 | 40 | ## 3. Start my dusty heart 41 | 42 | Short and sweet: 43 | 44 | ``` 45 | npm install 46 | npm run build-watch 47 | npm start 48 | ``` 49 | 50 | `npm start` doesn't build, so watch out for that. The reason it doesn't build is because you 51 | probably want to watch the build and run me in separate terminals. Otherwise, build errors get 52 | all mixed in with HTTP request logging. 53 | 54 | ## My anatomy 55 | 56 | `/app` has the React/Redux setup. `main.jsx` is the entry point. 57 | 58 | `/db` has the Sequelize models and database setup. It'll create the database for you if it doesn't exist, 59 | assuming you're using postgres. 60 | 61 | `/server` has the Express server and routes. `start.js` is the entry point. 62 | 63 | `/bin` has scripts. (Right now it has *one* script that creates a useful symlink.) 64 | 65 | ## Conventions 66 | 67 | I use `require` and `module.exports` in `.js` files. 68 | 69 | I use `import` and `export` in `.jsx` files, unless `require` makes for cleaner code. 70 | 71 | I use two spaces, no semi-colons, and trailing commas where possible. I'll 72 | have a linter someday soon. 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /stylesheets/fonts/flexslider-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG font generated by IcoMoon. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_progress-bars.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Progress bars 3 | // -------------------------------------------------- 4 | 5 | 6 | // Bar animations 7 | // ------------------------- 8 | 9 | // WebKit 10 | @-webkit-keyframes progress-bar-stripes { 11 | from { background-position: 40px 0; } 12 | to { background-position: 0 0; } 13 | } 14 | 15 | // Spec and IE10+ 16 | @keyframes progress-bar-stripes { 17 | from { background-position: 40px 0; } 18 | to { background-position: 0 0; } 19 | } 20 | 21 | 22 | // Bar itself 23 | // ------------------------- 24 | 25 | // Outer container 26 | .progress { 27 | overflow: hidden; 28 | height: $line-height-computed; 29 | margin-bottom: $line-height-computed; 30 | background-color: $progress-bg; 31 | border-radius: $progress-border-radius; 32 | @include box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); 33 | } 34 | 35 | // Bar of progress 36 | .progress-bar { 37 | float: left; 38 | width: 0%; 39 | height: 100%; 40 | font-size: $font-size-small; 41 | line-height: $line-height-computed; 42 | color: $progress-bar-color; 43 | text-align: center; 44 | background-color: $progress-bar-bg; 45 | @include box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); 46 | @include transition(width .6s ease); 47 | } 48 | 49 | // Striped bars 50 | // 51 | // `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the 52 | // `.progress-bar-striped` class, which you just add to an existing 53 | // `.progress-bar`. 54 | .progress-striped .progress-bar, 55 | .progress-bar-striped { 56 | @include gradient-striped; 57 | background-size: 40px 40px; 58 | } 59 | 60 | // Call animation for the active one 61 | // 62 | // `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the 63 | // `.progress-bar.active` approach. 64 | .progress.active .progress-bar, 65 | .progress-bar.active { 66 | @include animation(progress-bar-stripes 2s linear infinite); 67 | } 68 | 69 | 70 | // Variations 71 | // ------------------------- 72 | 73 | .progress-bar-success { 74 | @include progress-bar-variant($progress-bar-success-bg); 75 | } 76 | 77 | .progress-bar-info { 78 | @include progress-bar-variant($progress-bar-info-bg); 79 | } 80 | 81 | .progress-bar-warning { 82 | @include progress-bar-variant($progress-bar-warning-bg); 83 | } 84 | 85 | .progress-bar-danger { 86 | @include progress-bar-variant($progress-bar-danger-bg); 87 | } 88 | -------------------------------------------------------------------------------- /tests/bookmodel.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const db = require('APP/db'); 3 | const { Review, Book } = require('APP/db/models/index'); 4 | 5 | //BASIC TESTING FOR BOOKS AND REVIEWS 6 | 7 | xdescribe('Book model', () => { 8 | //have not implemented validation tests for the books / review models however manual testing shows the validations are working 9 | before('wait for the db', () => db.didSync); 10 | 11 | afterEach(function() { 12 | return db.sync({ force: true }); 13 | }); 14 | 15 | describe('Rating average getter function', () => { 16 | beforeEach(function() { 17 | return Promise.all([ 18 | Book.create({ title: 'Harry Potter', author: 'JK Rowling', genre: ['fantasy'], price: 19.95, description: 'a book about wizards' }), 19 | Review.create({ rating: 4.5, content: 'this book was pretty good', 'book_id': 1 }), 20 | Review.create({ rating: 2, content: 'this book was alright but i thought hagrid was overrated', 'book_id': 1 }) 21 | ]); 22 | }); 23 | 24 | it('returns the average of the ratings for that book', () => { 25 | return Book.findById(1) 26 | .then(foundBook => { 27 | //getter methods do NOT get invoked, and this getter method returns a promise 28 | return foundBook.ratingAverage; 29 | }) 30 | .then(avg => { 31 | expect(avg).to.equal(3.25); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('Book and review association', () => { 37 | it('review belongs to a book using the setBook method', () => { 38 | const createdBook = Book.create({ title: 'Harry Potter', author: 'JK Rowling', genre: ['fantasy'], price: 19.95, description: 'a book about wizards' }); 39 | const createdReview = Review.create({ rating: 4.5, content: 'this book was pretty good' }); 40 | 41 | return Promise.all([createdBook, createdReview]) 42 | .then((result) => { 43 | const book = result[0]; 44 | const review = result[1]; 45 | return review.setBook(book); 46 | }) 47 | .then(() => { 48 | return Review.findOne({ 49 | where: { rating: 4.5 }, 50 | include: { model: Book, as: 'book' } 51 | }); 52 | }) 53 | .then(foundReview => { 54 | expect(foundReview.book).to.exist; 55 | expect(foundReview.book.title).to.equal('Harry Potter'); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/books-reducer.test.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | 3 | import {createStore} from 'redux'; 4 | import bookReducer from 'APP/app/book/book-reducer.js'; 5 | 6 | describe('Book Reducer', () => { 7 | 8 | let testStore; 9 | beforeEach('Create testing store', () => { 10 | testStore = createStore(bookReducer); 11 | }); 12 | 13 | it('has expected initial state', () => { 14 | expect(testStore.getState()).to.be.deep.equal({ 15 | allBooks: [], 16 | currentBook: {}, 17 | currentGenre: '', 18 | currentAuthor: {}, 19 | selectedBooks: [] 20 | }); 21 | }); 22 | 23 | describe('FETCH_ALL_BOOKS', () => { 24 | it('returns all of the books', () => { 25 | testStore.dispatch({ type: 'FETCH_ALL_BOOKS', books: [{title: 'harry potter'}, { title: 'enders game'}, { title: 'LOTR'} ]}); 26 | const newState = testStore.getState(); 27 | expect(newState.allBooks).to.be.deep.equal([{title: 'harry potter'}, { title: 'enders game'}, { title: 'LOTR'} ]) 28 | expect(newState.allBooks[0]).to.be.deep.equal({title: 'harry potter'}); 29 | expect(newState.allBooks[1]).to.be.deep.equal({title: 'enders game'}) 30 | expect(newState.allBooks[2]).to.be.deep.equal({title: 'LOTR'}) 31 | }); 32 | }); 33 | 34 | describe('FETCH_SINGLE_BOOKS', () => { 35 | it('returns single book by id', () => { 36 | testStore.dispatch({ type: 'FETCH_SINGLE_BOOK', book: { 37 | id: 1, 38 | title: 'harry potter', 39 | genre: 'fantasy' 40 | }}); 41 | const newState = testStore.getState(); 42 | expect(newState.currentBook).to.be.deep.equal({ 43 | id: 1, 44 | title: 'harry potter', 45 | genre: 'fantasy' 46 | }); 47 | }) 48 | 49 | it('returns single book by id', () => { 50 | testStore.dispatch({ type: 'FETCH_SINGLE_BOOK', book: { 51 | id: 2, 52 | title: 'enders game', 53 | genre: 'fantasy' 54 | }}); 55 | const newState = testStore.getState(); 56 | expect(newState.currentBook).to.be.deep.equal({ 57 | id: 2, 58 | title: 'enders game', 59 | genre: 'fantasy' 60 | }); 61 | }) 62 | 63 | }) 64 | }); 65 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_pagination.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Pagination (multiple pages) 3 | // -------------------------------------------------- 4 | .pagination { 5 | display: inline-block; 6 | padding-left: 0; 7 | margin: $line-height-computed 0; 8 | border-radius: $border-radius-base; 9 | 10 | > li { 11 | display: inline; // Remove list-style and block-level defaults 12 | > a, 13 | > span { 14 | position: relative; 15 | float: left; // Collapse white-space 16 | padding: $padding-base-vertical $padding-base-horizontal; 17 | line-height: $line-height-base; 18 | text-decoration: none; 19 | color: $pagination-color; 20 | background-color: $pagination-bg; 21 | border: 1px solid $pagination-border; 22 | margin-left: -1px; 23 | } 24 | &:first-child { 25 | > a, 26 | > span { 27 | margin-left: 0; 28 | @include border-left-radius($border-radius-base); 29 | } 30 | } 31 | &:last-child { 32 | > a, 33 | > span { 34 | @include border-right-radius($border-radius-base); 35 | } 36 | } 37 | } 38 | 39 | > li > a, 40 | > li > span { 41 | &:hover, 42 | &:focus { 43 | z-index: 2; 44 | color: $pagination-hover-color; 45 | background-color: $pagination-hover-bg; 46 | border-color: $pagination-hover-border; 47 | } 48 | } 49 | 50 | > .active > a, 51 | > .active > span { 52 | &, 53 | &:hover, 54 | &:focus { 55 | z-index: 3; 56 | color: $pagination-active-color; 57 | background-color: $pagination-active-bg; 58 | border-color: $pagination-active-border; 59 | cursor: default; 60 | } 61 | } 62 | 63 | > .disabled { 64 | > span, 65 | > span:hover, 66 | > span:focus, 67 | > a, 68 | > a:hover, 69 | > a:focus { 70 | color: $pagination-disabled-color; 71 | background-color: $pagination-disabled-bg; 72 | border-color: $pagination-disabled-border; 73 | cursor: $cursor-disabled; 74 | } 75 | } 76 | } 77 | 78 | // Sizing 79 | // -------------------------------------------------- 80 | 81 | // Large 82 | .pagination-lg { 83 | @include pagination-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); 84 | } 85 | 86 | // Small 87 | .pagination-sm { 88 | @include pagination-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); 89 | } 90 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/_print.scss: -------------------------------------------------------------------------------- 1 | /*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ 2 | 3 | // ========================================================================== 4 | // Print styles. 5 | // Inlined to avoid the additional HTTP request: h5bp.com/r 6 | // ========================================================================== 7 | 8 | @media print { 9 | *, 10 | *:before, 11 | *:after { 12 | background: transparent !important; 13 | color: #000 !important; // Black prints faster: h5bp.com/s 14 | box-shadow: none !important; 15 | text-shadow: none !important; 16 | } 17 | 18 | a, 19 | a:visited { 20 | text-decoration: underline; 21 | } 22 | 23 | a[href]:after { 24 | content: " (" attr(href) ")"; 25 | } 26 | 27 | abbr[title]:after { 28 | content: " (" attr(title) ")"; 29 | } 30 | 31 | // Don't show links that are fragment identifiers, 32 | // or use the `javascript:` pseudo protocol 33 | a[href^="#"]:after, 34 | a[href^="javascript:"]:after { 35 | content: ""; 36 | } 37 | 38 | pre, 39 | blockquote { 40 | border: 1px solid #999; 41 | page-break-inside: avoid; 42 | } 43 | 44 | thead { 45 | display: table-header-group; // h5bp.com/t 46 | } 47 | 48 | tr, 49 | img { 50 | page-break-inside: avoid; 51 | } 52 | 53 | img { 54 | max-width: 100% !important; 55 | } 56 | 57 | p, 58 | h2, 59 | h3 { 60 | orphans: 3; 61 | widows: 3; 62 | } 63 | 64 | h2, 65 | h3 { 66 | page-break-after: avoid; 67 | } 68 | 69 | // Bootstrap specific changes start 70 | 71 | // Bootstrap components 72 | .navbar { 73 | display: none; 74 | } 75 | .btn, 76 | .dropup > .btn { 77 | > .caret { 78 | border-top-color: #000 !important; 79 | } 80 | } 81 | .label { 82 | border: 1px solid #000; 83 | } 84 | 85 | .table { 86 | border-collapse: collapse !important; 87 | 88 | td, 89 | th { 90 | background-color: #fff !important; 91 | } 92 | } 93 | .table-bordered { 94 | th, 95 | td { 96 | border: 1px solid #ddd !important; 97 | } 98 | } 99 | 100 | // Bootstrap specific changes end 101 | } 102 | -------------------------------------------------------------------------------- /app/book/genresComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | import store from '../store' 4 | import { setGenre } from './book-actions' 5 | 6 | 7 | export default class GenresComponent extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.imageLinks = { 11 | Fantasy: 'http://hopeofglory.typepad.com/.a/6a00d83451d62469e2016302e0ee2f970d-pi', 12 | Epic: 'https://i.ytimg.com/vi/3TAUnYZpMbA/0.jpg', 13 | 'Science Fiction' : 'https://s-media-cache-ak0.pinimg.com/originals/d2/c1/79/d2c17900b9df2140d3d3c4d9e1a2c5d2.jpg', 14 | 'Computer Science' : 'http://casanovainfo.com/wp-content/uploads/2016/07/hardware.jpg', 15 | 'Postmodern' : 'http://kingofwallpapers.com/postmodern/postmodern-002.jpg', 16 | Adventure: 'http://www.clker.com/cliparts/S/d/G/r/8/W/category-genre-adventure-md.png', 17 | Western: 'http://2.bp.blogspot.com/-efdQ7LMDUKs/UgarLFHogUI/AAAAAAAABGE/MmV_mdYk_MI/s1600/western%C2%A0pictures9.jpg', 18 | default: 'http://www.thenatterbox.com/wp-content/uploads/2015/10/Book-stock-photo.jpg' 19 | } 20 | this.setGenre = this.setGenre.bind(this); 21 | } 22 | 23 | setGenre(genre) { 24 | event.preventDefault(); 25 | store.dispatch(setGenre(genre)) 26 | } 27 | 28 | render() { 29 | //potentially use for dropdown genre menu in nav bar? 30 | //this is pulling the unique genres from the list of all of our books 31 | const genres = []; 32 | const arrayOfBookGenres = this.props.allBooks.map(book => book.genre) 33 | arrayOfBookGenres.forEach(genreArray => { 34 | genreArray.forEach(genre => { 35 | if (genres.indexOf(genre) === -1) genres.push(genre) 36 | }) 37 | }) 38 | 39 | return ( 40 |
41 |

Genres

42 |
43 | { 44 | this.props.allBooks && genres.map(genre => ( 45 |
this.setGenre(genre)} key={genre}> 46 | 47 | 48 |
49 |
50 | { genre } 51 |
52 |
53 | 54 |
55 | )) 56 | } 57 |
58 |
59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/dummy-data/particles.data.js: -------------------------------------------------------------------------------- 1 | export const particlesConfig = { 2 | "particles": { 3 | "number": { 4 | "value": 80, 5 | "density": { 6 | "enable": true, 7 | "value_area": 800 8 | } 9 | }, 10 | "color": { 11 | "value": "#fdb608" 12 | }, 13 | "shape": { 14 | "type": "circle", 15 | "stroke": { 16 | "width": 0, 17 | "color": "#000000" 18 | }, 19 | "polygon": { 20 | "nb_sides": 5 21 | }, 22 | "image": { 23 | "src": "img/github.svg", 24 | "width": 100, 25 | "height": 100 26 | } 27 | }, 28 | "opacity": { 29 | "value": 0.5, 30 | "random": false, 31 | "anim": { 32 | "enable": false, 33 | "speed": 1, 34 | "opacity_min": 0.1, 35 | "sync": false 36 | } 37 | }, 38 | "size": { 39 | "value": 3, 40 | "random": true, 41 | "anim": { 42 | "enable": false, 43 | "speed": 40, 44 | "size_min": 0.1, 45 | "sync": false 46 | } 47 | }, 48 | "line_linked": { 49 | "enable": true, 50 | "distance": 176.3753266952075, 51 | "color": "#ffefca", 52 | "opacity": 0.2966312312601217, 53 | "width": 0.5 54 | }, 55 | "move": { 56 | "enable": true, 57 | "speed": 1, 58 | "direction": "none", 59 | "random": false, 60 | "straight": false, 61 | "out_mode": "out", 62 | "bounce": false, 63 | "attract": { 64 | "enable": false, 65 | "rotateX": 600, 66 | "rotateY": 1200 67 | } 68 | } 69 | }, 70 | "interactivity": { 71 | "detect_on": "canvas", 72 | "events": { 73 | "onhover": { 74 | "enable": true, 75 | "mode": "grab" 76 | }, 77 | "onclick": { 78 | "enable": true, 79 | "mode": "push" 80 | }, 81 | "resize": true 82 | }, 83 | "modes": { 84 | "grab": { 85 | "distance": 227.77222777222775, 86 | "line_linked": { 87 | "opacity": 1 88 | } 89 | }, 90 | "bubble": { 91 | "distance": 400, 92 | "size": 40, 93 | "duration": 2, 94 | "opacity": 8, 95 | "speed": 3 96 | }, 97 | "repulse": { 98 | "distance": 200, 99 | "duration": 0.4 100 | }, 101 | "push": { 102 | "particles_nb": 4 103 | }, 104 | "remove": { 105 | "particles_nb": 2 106 | } 107 | } 108 | }, 109 | "retina_detect": true 110 | } 111 | -------------------------------------------------------------------------------- /tests/auth.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest-as-promised') 2 | const {expect} = require('chai') 3 | const db = require('APP/db') 4 | const User = require('APP/db/models/user') 5 | const app = require('APP/server/start') 6 | 7 | const alice = { 8 | firstName: 'sam', 9 | lastName: 'wheeler', 10 | username: 'alice@secrets.org', 11 | password: '12345' 12 | } 13 | 14 | describe('/api/auth', () => { 15 | before('create a user', () => 16 | db.didSync 17 | .then(() => 18 | User.create( 19 | {firstName: alice.firstName, 20 | lastName: alice.lastName, 21 | email: alice.username, 22 | password: alice.password 23 | }) 24 | ) 25 | ) 26 | 27 | describe('POST /local/login (username, password)', () => { 28 | it('succeeds with a valid username and password', () => 29 | request(app) 30 | .post('/api/auth/local/login') 31 | .send(alice) 32 | .expect(302) 33 | .expect('Set-Cookie', /session=.*/) 34 | .expect('Location', '/') 35 | ) 36 | 37 | it('fails with an invalid username and password', () => 38 | request(app) 39 | .post('/api/auth/local/login') 40 | .send({username: alice.username, password: 'wrong'}) 41 | .expect(401) 42 | ) 43 | }) 44 | 45 | describe('GET /whoami', () => { 46 | describe('when logged in,', () => { 47 | const agent = request.agent(app) 48 | before('log in', () => agent 49 | .post('/api/auth/local/login') 50 | .send(alice)) 51 | 52 | it('responds with the currently logged in user', () => 53 | agent.get('/api/auth/whoami') 54 | .set('Accept', 'application/json') 55 | .expect(200) 56 | .then(res => expect(res.body).to.contain({ 57 | email: alice.username 58 | })) 59 | ) 60 | }) 61 | 62 | it('when not logged in, responds with an empty object', () => 63 | request(app).get('/api/auth/whoami') 64 | .expect(200) 65 | .then(res => expect(res.body).to.eql({})) 66 | ) 67 | }) 68 | 69 | describe('POST /logout when logged in', () => { 70 | const agent = request.agent(app) 71 | 72 | before('log in', () => agent 73 | .post('/api/auth/local/login') 74 | .send(alice)) 75 | 76 | it('logs you out and redirects to whoami', () => agent 77 | .post('/api/auth/logout') 78 | .expect(302) 79 | .expect('Location', '/api/auth/whoami') 80 | .then(() => 81 | agent.get('/api/auth/whoami') 82 | .expect(200) 83 | .then(rsp => expect(rsp.body).eql({})) 84 | ) 85 | ) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_grid-framework.scss: -------------------------------------------------------------------------------- 1 | // Framework grid generation 2 | // 3 | // Used only by Bootstrap to generate the correct number of grid classes given 4 | // any value of `$grid-columns`. 5 | 6 | // [converter] This is defined recursively in LESS, but Sass supports real loops 7 | @mixin make-grid-columns($i: 1, $list: ".col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}") { 8 | @for $i from (1 + 1) through $grid-columns { 9 | $list: "#{$list}, .col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}"; 10 | } 11 | #{$list} { 12 | position: relative; 13 | // Prevent columns from collapsing when empty 14 | min-height: 1px; 15 | // Inner gutter via padding 16 | padding-left: ceil(($grid-gutter-width / 2)); 17 | padding-right: floor(($grid-gutter-width / 2)); 18 | } 19 | } 20 | 21 | 22 | // [converter] This is defined recursively in LESS, but Sass supports real loops 23 | @mixin float-grid-columns($class, $i: 1, $list: ".col-#{$class}-#{$i}") { 24 | @for $i from (1 + 1) through $grid-columns { 25 | $list: "#{$list}, .col-#{$class}-#{$i}"; 26 | } 27 | #{$list} { 28 | float: left; 29 | } 30 | } 31 | 32 | 33 | @mixin calc-grid-column($index, $class, $type) { 34 | @if ($type == width) and ($index > 0) { 35 | .col-#{$class}-#{$index} { 36 | width: percentage(($index / $grid-columns)); 37 | } 38 | } 39 | @if ($type == push) and ($index > 0) { 40 | .col-#{$class}-push-#{$index} { 41 | left: percentage(($index / $grid-columns)); 42 | } 43 | } 44 | @if ($type == push) and ($index == 0) { 45 | .col-#{$class}-push-0 { 46 | left: auto; 47 | } 48 | } 49 | @if ($type == pull) and ($index > 0) { 50 | .col-#{$class}-pull-#{$index} { 51 | right: percentage(($index / $grid-columns)); 52 | } 53 | } 54 | @if ($type == pull) and ($index == 0) { 55 | .col-#{$class}-pull-0 { 56 | right: auto; 57 | } 58 | } 59 | @if ($type == offset) { 60 | .col-#{$class}-offset-#{$index} { 61 | margin-left: percentage(($index / $grid-columns)); 62 | } 63 | } 64 | } 65 | 66 | // [converter] This is defined recursively in LESS, but Sass supports real loops 67 | @mixin loop-grid-columns($columns, $class, $type) { 68 | @for $i from 0 through $columns { 69 | @include calc-grid-column($i, $class, $type); 70 | } 71 | } 72 | 73 | 74 | // Create grid for specific class 75 | @mixin make-grid($class) { 76 | @include float-grid-columns($class); 77 | @include loop-grid-columns($grid-columns, $class, width); 78 | @include loop-grid-columns($grid-columns, $class, pull); 79 | @include loop-grid-columns($grid-columns, $class, push); 80 | @include loop-grid-columns($grid-columns, $class, offset); 81 | } 82 | -------------------------------------------------------------------------------- /tests/book.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest-as-promised') 2 | const db = require('APP/db') 3 | const Book = require('APP/db/models/book') 4 | const Review = require('APP/db/models/review') 5 | const app = require('APP/server/start') 6 | const {expect} = require('chai') 7 | 8 | 9 | describe('Book Routes', () => { 10 | before('wait for the db', () => db.didSync); 11 | 12 | before('Make a Book', () => { 13 | return Book.create({ 14 | title: 'Harry Potter', 15 | author: 'J.K Rowling', 16 | genre: ['sci-fi'], 17 | price: 15.00, 18 | description: 'a book about some dude that becomes a wizard', 19 | stockCount: 2, 20 | imageUrl: 'http://www.chestersu.com/wp-content/uploads/2013/01/241153480-30235112.jpg' 21 | }) 22 | }) 23 | 24 | after('Synchronize and clear database', () => db.sync({ 25 | force: true 26 | })); 27 | 28 | describe('routing checks', () => { 29 | before('make reviews', () => { 30 | return Promise.all([ 31 | Review.create({ rating: 4.5, content: 'this book was pretty good', book_id: 1 32 | }), 33 | Review.create({ rating: 2, content: 'this book was alright but i thought hagrid was overrated', book_id: 1 34 | }) 35 | ]) 36 | }) 37 | 38 | it('GET /api/books', () => { 39 | return request(app) 40 | .get(`/api/books`) 41 | .expect(200) 42 | .then(res => { 43 | expect(res.body.length).to.equal(1) 44 | expect(res.body).to.be.instanceof(Array) 45 | }) 46 | }) 47 | 48 | it('GET /api/books/1', () => { 49 | return request(app) 50 | .get(`/api/books/1`) 51 | .expect(200) 52 | .then(res => { 53 | expect(res.body.reviews.length).to.equal(2) 54 | expect(res.body.reviews[0].rating).to.equal(4.5) 55 | }) 56 | }) 57 | 58 | it('POST /api/books', () => { 59 | return request(app) 60 | .post('/api/books') 61 | .send({ 62 | title: 'Enders Game', 63 | author: 'Orsen Scott Card', 64 | genre: ['sci-fi'], 65 | price: 12.00, 66 | description: 'Book about futuristic military and children', 67 | stockCount: 3, 68 | imageUrl: 'http://www.chestersu.com/wp-content/uploads/2013/01/241153480-30235112.jpg' 69 | }) 70 | .expect(201) 71 | .then(res => { 72 | expect(res.body.title).to.equal('Enders Game') 73 | expect(res.body.price).equal(12.00) 74 | }) 75 | }) 76 | it('PUT /api/books', () => { 77 | return request(app) 78 | .put('/api/books/1') 79 | .send({ 80 | stockCount: 6 81 | }) 82 | .expect(200) 83 | .then(res => { 84 | expect(res.body.stockCount).to.equal(6) 85 | }) 86 | }) 87 | it('DELETE /api/delete', () => { 88 | return request(app) 89 | .delete('/api/books/1') 90 | .expect(204) 91 | }) 92 | }) 93 | }) 94 | 95 | -------------------------------------------------------------------------------- /app/book/newBookForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function({ handleChange, handleSubmit, title, author, price, description, stockCount, imageUrl, genre}) { 4 | 5 | return ( 6 |
7 |
8 | Add a New Book 9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 | 43 |
44 | 45 |
46 | 47 |
48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grace_reader", 3 | "version": "0.0.1", 4 | "description": "A happy little skeleton.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "check-node-version --node '>= 6.7.0' && bin/setup && mocha --compilers js:babel-register tests/*.test.js tests/*.jsx", 8 | "test-watch": "check-node-version --node '>= 6.7.0' && bin/setup && mocha --compilers js:babel-register --watch app/**/*.test.js app/**/*.test.jsx db/**/*.test.js server/**/*.test.js", 9 | "build": "check-node-version --node '>= 6.7.0' && bin/setup && webpack", 10 | "build-watch": "check-node-version --node '>= 6.7.0' && bin/setup && webpack -w", 11 | "start": "check-node-version --node '>= 6.7.0' && bin/setup && nodemon server/start.js", 12 | "seed": "node db/seed.js", 13 | "postinstall": "node db/seed.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/queerviolet/bones.git" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "redux", 22 | "skeleton" 23 | ], 24 | "author": "Ashi Krishnan ", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/queerviolet/bones/issues" 28 | }, 29 | "engines": { 30 | "node": "6.7.0" 31 | }, 32 | "homepage": "https://github.com/queerviolet/bones#readme", 33 | "dependencies": { 34 | "axios": "^0.15.2", 35 | "babel-preset-stage-2": "^6.18.0", 36 | "bcrypt": "^0.8.7", 37 | "bluebird": "^3.4.7", 38 | "body-parser": "^1.15.2", 39 | "bootstrap-sass": "^3.3.7", 40 | "chai-enzyme": "^0.5.2", 41 | "chalk": "^1.1.3", 42 | "check-node-version": "^1.1.2", 43 | "cookie-session": "^2.0.0-alpha.1", 44 | "enzyme": "^2.5.1", 45 | "express": "^4.14.0", 46 | "file-loader": "^0.9.0", 47 | "jquery": "^3.1.1", 48 | "node-sass": "^4.0.0", 49 | "nodemon": "^1.11.0", 50 | "particles.js": "^2.0.0", 51 | "passport": "^0.3.2", 52 | "passport-facebook": "^2.1.1", 53 | "passport-github2": "^0.1.10", 54 | "passport-google-oauth": "^1.0.0", 55 | "passport-local": "^1.0.0", 56 | "pg": "^6.1.0", 57 | "pg-native": "^1.10.0", 58 | "react": "^15.3.2", 59 | "react-dom": "^15.3.2", 60 | "react-redux": "^4.4.5", 61 | "react-router": "^3.0.0", 62 | "react-star-rating-component": "^1.2.2", 63 | "redux": "^3.6.0", 64 | "redux-logger": "^2.7.0", 65 | "redux-thunk": "^2.1.0", 66 | "sequelize": "^3.24.6", 67 | "sinon": "^1.17.6", 68 | "sinon-chai": "^2.8.0" 69 | }, 70 | "devDependencies": { 71 | "babel": "^6.5.2", 72 | "babel-core": "^6.18.0", 73 | "babel-loader": "^6.2.7", 74 | "babel-preset-es2015": "^6.18.0", 75 | "babel-preset-react": "^6.16.0", 76 | "chai": "^3.5.0", 77 | "css-loader": "^0.26.1", 78 | "enzyme": "^2.7.0", 79 | "extract-text-webpack-plugin": "^1.0.1", 80 | "imports-loader": "^0.7.0", 81 | "mocha": "^3.1.2", 82 | "sass-loader": "^4.1.0", 83 | "style-loader": "^0.13.1", 84 | "supertest": "^2.0.1", 85 | "supertest-as-promised": "^4.0.1", 86 | "volleyball": "^1.4.1", 87 | "webpack": "^1.13.3" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /stylesheets/bootstrap/mixins/_forms.scss: -------------------------------------------------------------------------------- 1 | // Form validation states 2 | // 3 | // Used in forms.less to generate the form validation CSS for warnings, errors, 4 | // and successes. 5 | 6 | @mixin form-control-validation($text-color: #555, $border-color: #ccc, $background-color: #f5f5f5) { 7 | // Color the label and help text 8 | .help-block, 9 | .control-label, 10 | .radio, 11 | .checkbox, 12 | .radio-inline, 13 | .checkbox-inline, 14 | &.radio label, 15 | &.checkbox label, 16 | &.radio-inline label, 17 | &.checkbox-inline label { 18 | color: $text-color; 19 | } 20 | // Set the border and box shadow on specific inputs to match 21 | .form-control { 22 | border-color: $border-color; 23 | @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work 24 | &:focus { 25 | border-color: darken($border-color, 10%); 26 | $shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten($border-color, 20%); 27 | @include box-shadow($shadow); 28 | } 29 | } 30 | // Set validation states also for addons 31 | .input-group-addon { 32 | color: $text-color; 33 | border-color: $border-color; 34 | background-color: $background-color; 35 | } 36 | // Optional feedback icon 37 | .form-control-feedback { 38 | color: $text-color; 39 | } 40 | } 41 | 42 | 43 | // Form control focus state 44 | // 45 | // Generate a customized focus state and for any input with the specified color, 46 | // which defaults to the `$input-border-focus` variable. 47 | // 48 | // We highly encourage you to not customize the default value, but instead use 49 | // this to tweak colors on an as-needed basis. This aesthetic change is based on 50 | // WebKit's default styles, but applicable to a wider range of browsers. Its 51 | // usability and accessibility should be taken into account with any change. 52 | // 53 | // Example usage: change the default blue border and shadow to white for better 54 | // contrast against a dark gray background. 55 | @mixin form-control-focus($color: $input-border-focus) { 56 | $color-rgba: rgba(red($color), green($color), blue($color), .6); 57 | &:focus { 58 | border-color: $color; 59 | outline: 0; 60 | @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px $color-rgba); 61 | } 62 | } 63 | 64 | // Form control sizing 65 | // 66 | // Relative text size, padding, and border-radii changes for form controls. For 67 | // horizontal sizing, wrap controls in the predefined grid classes. `