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(
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 |
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 |
21 |
22 |
23 |
24 |
25 |
Account Information
26 |
27 |
28 |
{this.props.auth ? : }
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
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 |
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 |
37 |
38 |
45 |
46 | - {!this.props.user && }
47 |
48 | -
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
58 |
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 |
--------------------------------------------------------------------------------
/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 |
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. `