├── Procfile ├── .gitattributes ├── circle.yml ├── .eslintignore ├── .env ├── config ├── generators │ ├── component │ │ ├── styles.scss.hbs │ │ ├── export.js.hbs │ │ ├── test.js.hbs │ │ ├── README.md.hbs │ │ ├── stateless.js.hbs │ │ ├── es6class.js.hbs │ │ └── index.js │ ├── container │ │ ├── styles.scss.hbs │ │ ├── export.js.hbs │ │ ├── constants.js.hbs │ │ ├── actions.js.hbs │ │ ├── test.js.hbs │ │ ├── reducer.test.js.hbs │ │ ├── reducer.js.hbs │ │ ├── README.md.hbs │ │ ├── actions.test.js.hbs │ │ ├── index.js.hbs │ │ └── index.js │ ├── page │ │ ├── export.js.hbs │ │ ├── index.module.scss.hbs │ │ ├── route.js.hbs │ │ ├── index.js.hbs │ │ ├── README.md.hbs │ │ └── index.js │ ├── utils │ │ ├── trimTemplateFile.js │ │ └── componentNameCheck.js │ └── index.js ├── testing │ ├── test-bundler.js │ └── karma.conf.js └── webpack │ └── webpack.test.babel.js ├── server.js ├── app ├── src │ ├── containers │ │ ├── RestaurantsGridContainer │ │ │ ├── index.module.scss │ │ │ ├── tests │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ └── actions.test.js │ │ │ ├── constants.js │ │ │ ├── README.md │ │ │ ├── reducer.js │ │ │ └── actions.js │ │ ├── LandingContainer │ │ │ ├── constants.js │ │ │ ├── index.module.scss │ │ │ ├── tests │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ └── actions.test.js │ │ │ ├── actions.js │ │ │ ├── README.md │ │ │ ├── reducer.js │ │ │ └── index.js │ │ ├── AddReviewContainer │ │ │ ├── constants.js │ │ │ ├── index.module.scss │ │ │ ├── tests │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ └── actions.test.js │ │ │ ├── actions.js │ │ │ ├── validation │ │ │ │ └── index.js │ │ │ ├── README.md │ │ │ ├── reducer.js │ │ │ └── index.js │ │ ├── FullReviewModalContainer │ │ │ ├── index.js │ │ │ └── README.md │ │ ├── SingleRestaurantContainer │ │ │ ├── index.module.scss │ │ │ ├── constants.js │ │ │ ├── README.md │ │ │ ├── reducer.js │ │ │ ├── actions.js │ │ │ └── index.js │ │ └── index.js │ ├── pages │ │ ├── AboutPage │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── SingleRestaurantPage │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── LandingPage │ │ │ ├── index.module.scss │ │ │ └── index.js │ │ ├── index.js │ │ └── NotFoundPage │ │ │ ├── index.module.scss │ │ │ └── index.js │ ├── components │ │ ├── AddReviewForm │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── SingleRestaurant │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── StarRating │ │ │ ├── index.module.scss │ │ │ ├── index.js │ │ │ └── README.md │ │ ├── AboutInfo │ │ │ ├── README.md │ │ │ ├── index.module.scss │ │ │ └── index.js │ │ ├── CodeBlock │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── FilterHeading │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── LoadingIndicator │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── Navbar │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── ReviewSrOnly │ │ │ ├── index.module.scss │ │ │ ├── index.js │ │ │ └── README.md │ │ ├── SronlyContent │ │ │ ├── index.module.scss │ │ │ ├── index.js │ │ │ └── README.md │ │ ├── RestaurantGrid │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── NoRestaurantsFound │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── ErrorAlert │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── AppFooter │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── RestaurantHours │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── App │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── FilterMenu │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── BannerHeader │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── AddButton │ │ │ ├── README.md │ │ │ ├── index.module.scss │ │ │ └── index.js │ │ ├── RestaurantHoursListItem │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── RestaurantPanel │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── FilterRestaurants │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── HeroCarousel │ │ │ ├── README.md │ │ │ ├── index.module.scss │ │ │ └── index.js │ │ ├── ReviewGrid │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── RestaurantInfo │ │ │ ├── index.module.scss │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── FullReviewModal │ │ │ ├── README.md │ │ │ ├── index.module.scss │ │ │ └── index.js │ │ ├── RestaurantGridItem │ │ │ ├── README.md │ │ │ ├── index.module.scss │ │ │ └── index.js │ │ ├── RestaurantReview │ │ │ ├── README.md │ │ │ ├── index.module.scss │ │ │ └── index.js │ │ └── index.js │ ├── index.js │ ├── reducers.js │ ├── routes.js │ └── store.js └── utils │ ├── sanityCheck.js │ ├── fixDate.js │ ├── filter.js │ ├── a11y.js │ ├── fixLongText.js │ └── validator.js ├── .eslintrc ├── .gitignore ├── index.html ├── server ├── public │ └── index.html └── app.js ├── LICENSE ├── webpack.config.babel.js ├── README.md └── package.json /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.scss linguist-language=JavaScript 2 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.2.4 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | app/build/ 2 | app/dist/ 3 | webpack.config.*.js 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BASE_URL=https://restaurant-reviewer-api.herokuapp.com/api/v1/ 2 | -------------------------------------------------------------------------------- /config/generators/component/styles.scss.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /config/generators/container/styles.scss.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require("babel-core/register"); 2 | var app = require('./server/app'); 3 | -------------------------------------------------------------------------------- /app/src/containers/RestaurantsGridContainer/index.module.scss: -------------------------------------------------------------------------------- 1 | .restaurantsGrid { 2 | padding: 0; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/pages/AboutPage/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /config/generators/component/export.js.hbs: -------------------------------------------------------------------------------- 1 | $1 2 | export {{ properCase name }} from './{{ properCase name }}'; 3 | -------------------------------------------------------------------------------- /app/src/pages/SingleRestaurantPage/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /config/generators/page/export.js.hbs: -------------------------------------------------------------------------------- 1 | $1 2 | export {{ properCase name }}Page from './{{ properCase name }}Page/index'; 3 | -------------------------------------------------------------------------------- /config/generators/container/export.js.hbs: -------------------------------------------------------------------------------- 1 | $1 2 | export {{ properCase name }}Container from './{{ properCase name }}Container'; 3 | -------------------------------------------------------------------------------- /app/utils/sanityCheck.js: -------------------------------------------------------------------------------- 1 | const sanityCheck = (x) => 2 | typeof x !== undefined && x !== null; 3 | 4 | export default sanityCheck; 5 | -------------------------------------------------------------------------------- /config/generators/container/constants.js.hbs: -------------------------------------------------------------------------------- 1 | export const {{ uppercase name }}_DEFAULT_ACTION = '{{ uppercase name }}_DEFAULT_ACTION'; 2 | -------------------------------------------------------------------------------- /app/src/components/AddReviewForm/index.module.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | margin-top: 40px; 3 | } 4 | 5 | .button { 6 | margin-right: 10px; 7 | } 8 | -------------------------------------------------------------------------------- /config/generators/page/index.module.scss.hbs: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | width: 100%; 4 | } 5 | .{{ camelCase name }} { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /config/generators/page/route.js.hbs: -------------------------------------------------------------------------------- 1 | 2 | $1 3 | -------------------------------------------------------------------------------- /app/src/components/SingleRestaurant/index.module.scss: -------------------------------------------------------------------------------- 1 | .starRating { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | margin: 20px; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/components/StarRating/index.module.scss: -------------------------------------------------------------------------------- 1 | .starRating { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | margin-top: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/containers/LandingContainer/constants.js: -------------------------------------------------------------------------------- 1 | export const LOAD_IMAGES_SUCCESS = 'LOAD_IMAGES_SUCCESS'; 2 | export const LOAD_IMAGES_INITIATION = 'LOAD_IMAGES_INITIATION'; 3 | -------------------------------------------------------------------------------- /app/src/containers/LandingContainer/index.module.scss: -------------------------------------------------------------------------------- 1 | .landing { 2 | padding-top: 0 !important; 3 | } 4 | 5 | .headline { 6 | text-align: center; 7 | padding: 100px; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/components/AboutInfo/README.md: -------------------------------------------------------------------------------- 1 | ## AboutInfo Component 2 | A presentational component that shows info about the project. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | -------------------------------------------------------------------------------- /app/src/pages/LandingPage/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | width: 100%; 4 | }; 5 | 6 | .header { 7 | font-size: 32px; 8 | font: 'Open Sans'; 9 | } 10 | -------------------------------------------------------------------------------- /app/utils/fixDate.js: -------------------------------------------------------------------------------- 1 | const fixDate = (date) => { 2 | const dateParts = date.split('/'); 3 | const [a, b, c] = dateParts; 4 | return [b, a, c].join('/'); 5 | }; 6 | 7 | export default fixDate; 8 | -------------------------------------------------------------------------------- /app/src/components/CodeBlock/index.module.scss: -------------------------------------------------------------------------------- 1 | .codeBlock { 2 | display: block; 3 | overflow-x: auto; 4 | padding: 0.5em; 5 | color: #333; 6 | background: #f8f8f8; 7 | -webkit-text-size-adjust: none; 8 | } 9 | -------------------------------------------------------------------------------- /app/utils/filter.js: -------------------------------------------------------------------------------- 1 | const shouldBeEnabled = (filter) => 2 | Object 3 | .keys(filter) 4 | .map(i => filter[i]) 5 | .filter(i => i !== 'All') 6 | .length > 0; 7 | 8 | export default shouldBeEnabled; 9 | -------------------------------------------------------------------------------- /app/src/containers/AddReviewContainer/constants.js: -------------------------------------------------------------------------------- 1 | export const TOGGLE_ADD_REVIEW = 'TOGGLE_ADD_REVIEW'; 2 | export const ADD_REVIEW_INVALID = 'ADD_REVIEW_INVALID,'; 3 | export const CLEAR_ADD_REVIEW_ERRORS = 'CLEAR_ADD_REVIEW_ERRORS'; 4 | -------------------------------------------------------------------------------- /app/src/components/FilterHeading/index.module.scss: -------------------------------------------------------------------------------- 1 | .filterHeading { 2 | width: 100%; 3 | padding: 40px; 4 | } 5 | 6 | .noStyle { 7 | display: none; 8 | } 9 | 10 | .filterSubHeading { 11 | width: 100%; 12 | text-align: center; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/components/LoadingIndicator/index.module.scss: -------------------------------------------------------------------------------- 1 | .loadingIndicator { 2 | height: 300px; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | 8 | .ripple { 9 | height: 70px; 10 | width: 70px; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/components/Navbar/index.module.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | border-bottom: 1px solid gray; 3 | } 4 | 5 | .logo { 6 | max-height: 50px; 7 | max-width: 100px; 8 | margin-left: 20px; 9 | } 10 | 11 | .menu { 12 | margin-right: 20px; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/components/ReviewSrOnly/index.module.scss: -------------------------------------------------------------------------------- 1 | .srOnly { 2 | position: absolute; 3 | clip: rect(1px, 1px, 1px, 1px); 4 | width: 1px; 5 | height: 1px; 6 | padding: 0; 7 | margin: -1px; 8 | overflow: hidden; 9 | border: 0; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/components/SronlyContent/index.module.scss: -------------------------------------------------------------------------------- 1 | .sronlyContent { 2 | position: absolute; 3 | clip: rect(1px, 1px, 1px, 1px); 4 | width: 1px; 5 | height: 1px; 6 | padding: 0; 7 | margin: -1px; 8 | overflow: hidden; 9 | border: 0; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/components/Navbar/README.md: -------------------------------------------------------------------------------- 1 | ## Navbar Component 2 | A component that is a visual navbar with a logo and a link for home page. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | None 12 | 13 | ### Other Information 14 | None 15 | -------------------------------------------------------------------------------- /app/src/components/RestaurantGrid/index.module.scss: -------------------------------------------------------------------------------- 1 | .restaurantGrid { 2 | display: flex; 3 | flex-wrap: wrap; 4 | margin-left: auto; 5 | margin-right: auto; 6 | @media screen and (min-width: 1400px) { 7 | width: 1400px; 8 | } 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/pages/index.js: -------------------------------------------------------------------------------- 1 | /* Assemble all pages for export */ 2 | export AboutPage from './AboutPage/index'; 3 | export SingleRestaurantPage from './SingleRestaurantPage/index'; 4 | export NotFoundPage from './NotFoundPage/index'; 5 | export LandingPage from './LandingPage/index'; 6 | -------------------------------------------------------------------------------- /app/src/pages/AboutPage/README.md: -------------------------------------------------------------------------------- 1 | ## AboutPage 2 | A top level page container that corresponds to a route by the same name. 3 | 4 | ### Route Parameters 5 | Any parameters that might be part of the route. 6 | 7 | ### Example Usage 8 | 9 | ```js 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /app/src/components/NoRestaurantsFound/index.module.scss: -------------------------------------------------------------------------------- 1 | .centerAndPad { 2 | width: 100%; 3 | padding: 100px; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .paragraphCenter { 10 | min-width: 100%; 11 | text-align: center; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ // React must be in scope here 2 | import React from 'react'; 3 | /* eslint-enable */ 4 | import { render } from 'react-dom'; 5 | import routes from './routes'; 6 | import '../styles/styles.scss'; 7 | 8 | render(routes, document.getElementById('app')); 9 | -------------------------------------------------------------------------------- /app/src/components/ErrorAlert/index.module.scss: -------------------------------------------------------------------------------- 1 | .errorAlert { 2 | position: relative; 3 | margin-left: auto; 4 | margin-right: auto; 5 | max-width: 500px; 6 | } 7 | 8 | .error { 9 | max-width: 960px; 10 | } 11 | 12 | .closeButton { 13 | position: absolute; 14 | right: 5px; 15 | } 16 | -------------------------------------------------------------------------------- /config/generators/container/actions.js.hbs: -------------------------------------------------------------------------------- 1 | import { 2 | {{ uppercase name }}_DEFAULT_ACTION, 3 | } from './constants'; 4 | 5 | // {{ camelCase name }}defaultAction :: None -> {Action} 6 | export const {{ camelCase name }}DefaultAction = () => ({ 7 | type: {{ uppercase name }}_DEFAULT_ACTION, 8 | }); 9 | -------------------------------------------------------------------------------- /app/src/components/AppFooter/index.module.scss: -------------------------------------------------------------------------------- 1 | .appFooter { 2 | background: #fbfbfb; 3 | padding: 50px; 4 | title { 5 | color: black !important; 6 | } 7 | } 8 | 9 | .flexOne { 10 | flex: 1; 11 | } 12 | 13 | $darker-purple: #501EB4; 14 | .footerAnchor { 15 | color: $darker-purple !important; 16 | } 17 | -------------------------------------------------------------------------------- /app/src/containers/AddReviewContainer/index.module.scss: -------------------------------------------------------------------------------- 1 | .addReview { 2 | 3 | } 4 | 5 | .addReviewFooter { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center 9 | } 10 | 11 | .button { 12 | padding: 20px 50px; 13 | margin: 40px; 14 | } 15 | 16 | .errorBox { 17 | max-width: 500px; 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "es6": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "rules": { 11 | "func-names": 0, 12 | "eol-last": 0 13 | }, 14 | "plugins": [ 15 | "react", 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /config/generators/utils/trimTemplateFile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const trimTemplateFile = (template) => { 4 | // Loads the template file and trims the whitespace and then returns the content as a string. 5 | return fs.readFileSync(template, 'utf8').replace(/\s*$/, ''); 6 | }; 7 | 8 | module.exports = trimTemplateFile; 9 | -------------------------------------------------------------------------------- /app/src/components/RestaurantHours/index.module.scss: -------------------------------------------------------------------------------- 1 | .restaurantHours { 2 | padding: 5px; 3 | font-family: 'Open Sans'; 4 | } 5 | 6 | .list { 7 | min-width: 500px; 8 | @media screen and (max-width: 700px) { 9 | min-width: 300px; 10 | } 11 | @media screen and (max-width: 500px) { 12 | min-width: 200px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/containers/LandingContainer/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import Landing from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/src/pages/SingleRestaurantPage/README.md: -------------------------------------------------------------------------------- 1 | ## SingleRestaurantPage 2 | A top level page container that corresponds to a route by the same name. 3 | 4 | ### Route Parameters 5 | An paramaters that might be part of the route. 6 | 7 | ### Example Usage 8 | 9 | ```js 10 | 11 | ``` 12 | 13 | 14 | ### Other Information 15 | -------------------------------------------------------------------------------- /app/src/containers/AddReviewContainer/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import AddReview from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/src/components/AppFooter/README.md: -------------------------------------------------------------------------------- 1 | ## AppFooter Component 2 | A component that shows up as a footer at the bottom of the page. It shows information about the project and also social sharing links. 3 | 4 | The component is completely presentational and is stateless. No props are passed in. 5 | 6 | ### Example 7 | 8 | ```js 9 | 10 | ``` 11 | -------------------------------------------------------------------------------- /app/src/containers/FullReviewModalContainer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FullReviewModal } from 'components'; 3 | 4 | class FullReviewModalContainer extends Component { 5 | render() { 6 | return ( 7 | 8 | ); 9 | } 10 | } 11 | 12 | export default FullReviewModalContainer; 13 | -------------------------------------------------------------------------------- /app/utils/a11y.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_PAGE_TITLE = 'Restaurant Reviewer'; 2 | 3 | export function updatePageTitle(title) { 4 | if (document) { 5 | if (title) { 6 | document.title = `${title} | ${DEFAULT_PAGE_TITLE}`; 7 | } else { 8 | document.title = DEFAULT_PAGE_TITLE; 9 | } 10 | } 11 | } 12 | 13 | export default { updatePageTitle }; 14 | -------------------------------------------------------------------------------- /config/generators/component/test.js.hbs: -------------------------------------------------------------------------------- 1 | import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /config/generators/container/test.js.hbs: -------------------------------------------------------------------------------- 1 | import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/src/components/App/README.md: -------------------------------------------------------------------------------- 1 | ## App Component 2 | Top level Application component that sits above other components. 3 | 4 | ### Props 5 | 6 | | Prop | Type | Default | Possible Values 7 | | ------------- | -------- | ----------- | --------------------------------------------- 8 | | **children** | Element | | Any children react components 9 | -------------------------------------------------------------------------------- /app/src/containers/RestaurantsGridContainer/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import RestaurantsGrid from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/src/components/FilterMenu/index.module.scss: -------------------------------------------------------------------------------- 1 | .anchor { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .listItem { 7 | padding: 0 !important; 8 | color: #8C50FF; 9 | } 10 | 11 | .anchorSelected { 12 | background-color: #8C50FF; 13 | color: white !important; 14 | &:focus { 15 | background-color: #8C50FF !important; 16 | color: white !important; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/containers/SingleRestaurantContainer/index.module.scss: -------------------------------------------------------------------------------- 1 | .containerCenter { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: calc(100vh - 100px); 7 | } 8 | 9 | .noneFound { 10 | text-align: center; 11 | } 12 | 13 | .noPad { 14 | padding-top: 0 !important; 15 | padding-bottom: 0 !important; 16 | } 17 | -------------------------------------------------------------------------------- /app/src/containers/index.js: -------------------------------------------------------------------------------- 1 | /* Assemble all containers for export */ 2 | export FullReviewModalContainer from './FullReviewModalContainer'; 3 | export AddReviewContainer from './AddReviewContainer'; 4 | export SingleRestaurantContainer from './SingleRestaurantContainer'; 5 | export RestaurantsGridContainer from './RestaurantsGridContainer'; 6 | export LandingContainer from './LandingContainer'; 7 | -------------------------------------------------------------------------------- /app/src/containers/LandingContainer/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import landingReducer from '../reducer'; 3 | 4 | const initialState = { 5 | // Initial State goes here! 6 | }; 7 | 8 | describe('landingReducer', () => { 9 | it('returns the initial state', () => { 10 | expect( 11 | landingReducer(undefined, {}) 12 | ).toEqual(initialState); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /config/generators/utils/componentNameCheck.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const pageComponents = fs.readdirSync('app/src/components'); 4 | const pageContainers = fs.readdirSync('app/src/containers'); 5 | const components = pageComponents.concat(pageContainers); 6 | 7 | const componentNameCheck = (component) => 8 | components.indexOf(component) >= 0; 9 | 10 | module.exports = componentNameCheck; 11 | -------------------------------------------------------------------------------- /app/src/containers/AddReviewContainer/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import addReviewReducer from '../reducer'; 3 | 4 | const initialState = { 5 | // Initial State goes here! 6 | }; 7 | 8 | describe('addReviewReducer', () => { 9 | it('returns the initial state', () => { 10 | expect( 11 | addReviewReducer(undefined, {}) 12 | ).toEqual(initialState); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /app/src/components/BannerHeader/index.module.scss: -------------------------------------------------------------------------------- 1 | .bannerHeader { 2 | box-shadow: 0 2px 4px 0 rgba(167,156,142,0.52); 3 | margin-bottom: 100px; 4 | position: relative; 5 | background-color: #0A64A0; 6 | } 7 | 8 | .banner { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | flex-direction: column; 13 | height: 100%; 14 | padding: 30px 0; 15 | color: white; 16 | } 17 | -------------------------------------------------------------------------------- /app/src/pages/NotFoundPage/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | width: 100% 4 | }; 5 | 6 | .footerCenter { 7 | width: 100%; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | margin-top: 60px; 12 | } 13 | 14 | .sectionHeader { 15 | margin-top: 40px; 16 | } 17 | 18 | .putInMiddle { 19 | height: 100%; 20 | display: flex; 21 | justify-content: center; 22 | } 23 | -------------------------------------------------------------------------------- /app/src/containers/RestaurantsGridContainer/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import restaurantsGridReducer from '../reducer'; 3 | 4 | const initialState = { 5 | // Initial State goes here! 6 | }; 7 | 8 | describe('restaurantsGridReducer', () => { 9 | it('returns the initial state', () => { 10 | expect( 11 | restaurantsGridReducer(undefined, {}) 12 | ).toEqual(initialState); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /config/generators/container/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {{ camelCase name }}Reducer from '../reducer'; 3 | 4 | const initialState = { 5 | // Initial State goes here! 6 | }; 7 | 8 | describe('{{ camelCase name }}Reducer', () => { 9 | it('returns the initial state', () => { 10 | expect( 11 | {{ camelCase name }}Reducer(undefined, {}) 12 | ).toEqual(initialState); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /config/generators/page/index.js.hbs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import styles from './index.module.scss'; 4 | 5 | 6 | // Pages map directly to Routes, i.e. one page equals on Route 7 | 8 | const {{ properCase name }}Page = (props) => ( 9 |
10 | Hello from {{ properCase name }}Page ! 11 |
12 | ); 13 | 14 | export default cssModules({{ properCase name }}Page, styles); 15 | -------------------------------------------------------------------------------- /app/src/components/NoRestaurantsFound/README.md: -------------------------------------------------------------------------------- 1 | ## NoRestaurantsFound Component 2 | A component that ... 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **filter** | String | | Object that represents the currently set filter. 15 | -------------------------------------------------------------------------------- /app/src/components/SronlyContent/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styles from './index.module.scss'; 3 | import cssModules from 'react-css-modules'; 4 | 5 | const SrOnlyContent = ({ 6 | children, 7 | }) => ( 8 | 9 | {children} 10 | 11 | ); 12 | 13 | SrOnlyContent.propTypes = { 14 | children: PropTypes.node.isRequired, 15 | }; 16 | 17 | export default cssModules(SrOnlyContent, styles); 18 | -------------------------------------------------------------------------------- /app/src/components/LoadingIndicator/README.md: -------------------------------------------------------------------------------- 1 | ## LoadingIndicator Component 2 | A component that shows a loading indicator. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **isLoading** | Bool | | Whether or not the indicator is currently animating 15 | -------------------------------------------------------------------------------- /app/src/components/SronlyContent/README.md: -------------------------------------------------------------------------------- 1 | ## SrOnlyContent Component 2 | A component that is a screen reader only element. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | {children} 9 | 10 | ``` 11 | 12 | ### Props 13 | 14 | | Prop | Type | Default | Possible Values 15 | | ------------- | -------- | ----------- | --------------------------------------------- 16 | | **children** | Node | | React node children content 17 | -------------------------------------------------------------------------------- /app/src/components/AddButton/README.md: -------------------------------------------------------------------------------- 1 | ## AddButton Component 2 | A component that acts as a button to add items. Contains a + icon. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **onAdd** | Func | | A callback for when the button is clicked. 15 | -------------------------------------------------------------------------------- /config/generators/container/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | import { 2 | {{ uppercase name }}_DEFAULT_ACTION, 3 | } from './constants'; 4 | 5 | const initialState = { 6 | // Initial State goes here! 7 | }; 8 | 9 | const {{ camelCase name }}Reducer = 10 | (state = initialState, action) => { 11 | switch (action.type) { 12 | case DEFAULT_ACTION: 13 | return state; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default {{ camelCase name }}Reducer; 20 | -------------------------------------------------------------------------------- /config/generators/component/README.md.hbs: -------------------------------------------------------------------------------- 1 | ## {{ properCase name }} Component 2 | A component that ... 3 | 4 | ### Example 5 | 6 | ```js 7 | <{{ properCase name }} /> 8 | ``` 9 | 10 | {{#if wantPropTypes}} 11 | ### Props 12 | 13 | | Prop | Type | Default | Possible Values 14 | | ------------- | -------- | ----------- | --------------------------------------------- 15 | | **myProp** | String | | Any string value 16 | 17 | {{/if}} 18 | 19 | ### Other Information 20 | -------------------------------------------------------------------------------- /app/utils/fixLongText.js: -------------------------------------------------------------------------------- 1 | const chunkString = (str, length) => str.match(new RegExp('.{1,' + length + '}', 'g')); 2 | 3 | const fixLongText = (text) => { 4 | const textParts = text.split(' '); 5 | const newTextParts = []; 6 | textParts.forEach((item) => { 7 | if (item.length >= 30) { 8 | newTextParts.push(chunkString(item, 15).join(' ')); 9 | } else { 10 | newTextParts.push(item); 11 | } 12 | }); 13 | return newTextParts.join(' '); 14 | }; 15 | 16 | export default fixLongText; 17 | -------------------------------------------------------------------------------- /config/generators/container/README.md.hbs: -------------------------------------------------------------------------------- 1 | ## {{ properCase name }}Container 2 | A container that does ... 3 | 4 | ### Example Usage 5 | 6 | ```js 7 | <{{ properCase name }}Container /> 8 | ``` 9 | 10 | {{#if wantPropTypes}} 11 | ### Props 12 | 13 | | Prop | Type | Default | Possible Values 14 | | ------------- | -------- | ----------- | --------------------------------------------- 15 | | **myProp** | String | | Any string value 16 | 17 | {{/if}} 18 | 19 | ### Other Information 20 | -------------------------------------------------------------------------------- /app/src/containers/LandingContainer/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | landingDefaultAction, 4 | } from '../actions'; 5 | import { 6 | LANDING_DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('Landing actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: LANDING_DEFAULT_ACTION, 14 | }; 15 | expect(landingDefaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/src/components/CodeBlock/README.md: -------------------------------------------------------------------------------- 1 | ## CodeBlock Component 2 | A component that shows as a code block. 3 | 4 | ### Example 5 | 6 | ```js 7 | 12 | ``` 13 | 14 | ### Props 15 | 16 | | Prop | Type | Default | Possible Values 17 | | ------------- | -------- | ----------- | --------------------------------------------- 18 | | **codeBlock** | String | | Any string value to be shown as code in a code block. 19 | -------------------------------------------------------------------------------- /app/src/containers/AddReviewContainer/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | addReviewDefaultAction, 4 | } from '../actions'; 5 | import { 6 | ADDREVIEW_DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('AddReview actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: ADDREVIEW_DEFAULT_ACTION, 14 | }; 15 | expect(addReviewDefaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/src/components/RestaurantHoursListItem/README.md: -------------------------------------------------------------------------------- 1 | ## RestaurantHoursListItem Component 2 | A component that shows one list item with restaurant hours. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **day** | String | | Any string value 15 | | **hours** | String | | Any string value 16 | -------------------------------------------------------------------------------- /app/src/components/RestaurantPanel/index.module.scss: -------------------------------------------------------------------------------- 1 | .restaurantPanel { 2 | position: relative; 3 | margin: 0 auto; 4 | width: 90%; 5 | max-width: 750px; 6 | } 7 | 8 | .panel { 9 | background: #fff; 10 | box-shadow: 0 2px 4px 0 rgba(167,156,142,0.52); 11 | width: 100%; 12 | margin-bottom: 80px; 13 | position: relative; 14 | } 15 | 16 | .featureImage { 17 | span { 18 | align-items: center; 19 | } 20 | } 21 | 22 | .textWrapper { 23 | margin: 40px; 24 | text-align: center; 25 | font-size: 18px; 26 | font-family: 'Open Sans'; 27 | } 28 | -------------------------------------------------------------------------------- /app/src/containers/RestaurantsGridContainer/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | restaurantsGridDefaultAction, 4 | } from '../actions'; 5 | import { 6 | RESTAURANTSGRID_DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('RestaurantsGrid actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: RESTAURANTSGRID_DEFAULT_ACTION, 14 | }; 15 | expect(restaurantsGridDefaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/src/components/FilterRestaurants/index.module.scss: -------------------------------------------------------------------------------- 1 | .filterRestaurants { 2 | width: 100%; 3 | } 4 | 5 | .menuWrapper { 6 | nav { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | width: 100%; 11 | } 12 | @media screen and (max-width: 900px) { 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | flex-direction: column; 17 | } 18 | } 19 | 20 | .footerContainer { 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | width: 100%; 25 | margin-top: 20px; 26 | } 27 | -------------------------------------------------------------------------------- /config/generators/container/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | {{ camelCase name }}DefaultAction, 4 | } from '../actions'; 5 | import { 6 | {{ uppercase name }}_DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('{{ properCase name }} actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: {{ uppercase name }}_DEFAULT_ACTION, 14 | }; 15 | expect({{ camelCase name }}DefaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/src/containers/AddReviewContainer/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | TOGGLE_ADD_REVIEW, 3 | ADD_REVIEW_INVALID, 4 | CLEAR_ADD_REVIEW_ERRORS, 5 | } from './constants'; 6 | 7 | // toggleAddReview :: None -> {Action} 8 | export const toggleAddReview = () => ({ 9 | type: TOGGLE_ADD_REVIEW, 10 | }); 11 | 12 | // addReviewInvalid :: Error -> {Action} 13 | export const addReviewInvalid = (error) => ({ 14 | type: ADD_REVIEW_INVALID, 15 | error, 16 | }); 17 | 18 | // clearAddReviewErrors :: None -> {Action} 19 | export const clearAddReviewErrors = () => ({ 20 | type: CLEAR_ADD_REVIEW_ERRORS, 21 | }); 22 | -------------------------------------------------------------------------------- /app/src/components/BannerHeader/README.md: -------------------------------------------------------------------------------- 1 | ## BannerHeader Component 2 | A component that shows up as a toplevel banner for a page, almost like a hero, but only 100px high. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **heading** | String | | Any string value 15 | 16 | 17 | ### Other Information 18 | The background is set as whatever the main color for the theme is. 19 | -------------------------------------------------------------------------------- /app/src/components/HeroCarousel/README.md: -------------------------------------------------------------------------------- 1 | ## HeroCarousel Component 2 | A component that takes an array of restaurants with images and makes a half-height image caraousel hero with an item for each restaurant. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **restaurants** | Array | | Arrays containing an object with a src and a caption attribute for each restaurant to be shown. 15 | -------------------------------------------------------------------------------- /app/src/pages/AboutPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import styles from './index.module.scss'; 4 | import { AppFooter, AboutInfo } from 'components'; 5 | import { updatePageTitle } from 'utils/a11y'; 6 | 7 | class AboutPage extends Component { 8 | componentDidMount() { 9 | updatePageTitle('About Page'); 10 | } 11 | render() { 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default cssModules(AboutPage, styles); 22 | -------------------------------------------------------------------------------- /config/generators/page/README.md.hbs: -------------------------------------------------------------------------------- 1 | ## {{ properCase name }}Page 2 | A top level page container that corresponds to a route by the same name. 3 | 4 | ### Route Parameters 5 | An paramaters that might be part of the route. 6 | 7 | ### Example Usage 8 | 9 | ```js 10 | <{{ properCase name }}Page /> 11 | ``` 12 | 13 | {{#if wantPropTypes}} 14 | ### Props 15 | 16 | | Prop | Type | Default | Possible Values 17 | | ------------- | -------- | ----------- | --------------------------------------------- 18 | | **myProp** | String | | Any string value 19 | 20 | {{/if}} 21 | 22 | ### Other Information 23 | -------------------------------------------------------------------------------- /app/src/containers/AddReviewContainer/validation/index.js: -------------------------------------------------------------------------------- 1 | import * as validation from 'utils/validator'; 2 | import memoize from 'lru-memoize'; 3 | 4 | const nameInput = [ 5 | validation.valueRequired, 6 | ]; 7 | 8 | 9 | const textInput = [ 10 | validation.valueRequired, 11 | ]; 12 | 13 | const ratingInput = [ 14 | validation.isAtLeast(1), 15 | validation.isAtMost(5), 16 | validation.valueRequired, 17 | ]; 18 | 19 | const formValidation = validation.createValidator({ 20 | nameInput, 21 | textInput, 22 | ratingInput, 23 | }); 24 | 25 | const validator = memoize(10)(formValidation); 26 | export default validator; 27 | -------------------------------------------------------------------------------- /app/src/components/AboutInfo/index.module.scss: -------------------------------------------------------------------------------- 1 | .aboutInfo { 2 | min-height: 400px; 3 | } 4 | 5 | .paddedHeader { 6 | padding: 50px; 7 | } 8 | 9 | .avatar { 10 | width: 200px; 11 | height: 200px; 12 | box-shadow: 0 0 10px rgba(0,0,0,.5); 13 | padding: 4px; 14 | line-height: 1.42857143; 15 | background-color: #fff; 16 | border: 1px solid #ddd; 17 | border-radius: 50%; 18 | } 19 | 20 | .boogie { 21 | box-shadow: 0 0 8px rgba(0,0,0,.5); 22 | border-radius: 10px; 23 | display: block; 24 | max-width: 100%; 25 | height: auto; 26 | @media screen and (min-width: 1400px) { 27 | max-width: 1200px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/components/RestaurantHours/README.md: -------------------------------------------------------------------------------- 1 | ## RestaurantHours Component 2 | A component that shows a section with hours that the restaurant is open. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **restaurant** | Object | | An object representing a single restaurant. 15 | 16 | 17 | ### Other Information 18 | Presentational component that uses the built in grommet accordion components. 19 | -------------------------------------------------------------------------------- /config/testing/test-bundler.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | 5 | // If we need to use Chai, we'll have already chaiEnzyme loaded 6 | import chai from 'chai'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | import chaiJsx from 'chai-jsx'; 9 | 10 | chai.use(chaiEnzyme()); 11 | chai.use(chaiJsx); 12 | 13 | // Include all .js files under `app`, except app.js, reducers.js, and routes.js. 14 | // This is for isparta code coverage 15 | const context = require.context( 16 | '../../app/src', 17 | true, 18 | /([^\a]+).test\.js$/ 19 | ); 20 | context.keys().forEach(context); 21 | -------------------------------------------------------------------------------- /app/src/components/ErrorAlert/README.md: -------------------------------------------------------------------------------- 1 | ## ErrorAlert Component 2 | A component that shows alert notifications, given an input array of errors. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **errors** | Array | | An array of errors with corresponding messages. 15 | 16 | 17 | ### Other Information 18 | Currently only showing as critical errors, although there are multiple types of errors. 19 | 20 | See: http://grommet.github.io/docs/notification 21 | -------------------------------------------------------------------------------- /app/src/components/CodeBlock/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styles from './index.module.scss'; 3 | import cssModules from 'react-css-modules'; 4 | import Box from 'grommet/components/Box'; 5 | 6 | const CodeBlock = ({ 7 | codeBlock, 8 | }) => ( 9 | 10 | 11 |
12 |         
13 |           {codeBlock}
14 |         
15 |       
16 |
17 |
18 | ); 19 | 20 | CodeBlock.propTypes = { 21 | codeBlock: PropTypes.node.isRequired, 22 | }; 23 | 24 | export default cssModules(CodeBlock, styles); 25 | -------------------------------------------------------------------------------- /app/src/components/ReviewGrid/index.module.scss: -------------------------------------------------------------------------------- 1 | .reviewGrid { 2 | display: flex; 3 | flex-wrap: wrap; 4 | background: #f5f5f5; 5 | padding: 50px; 6 | @media screen and (max-width: 768px) { 7 | padding: 0; 8 | } 9 | } 10 | 11 | .headline { 12 | width: 100%; 13 | margin-top: 50px; 14 | } 15 | 16 | .container { 17 | padding-bottom: 0 !important; 18 | } 19 | 20 | .responsive { 21 | flex-basis: calc(25% - 24px); 22 | @media screen and (max-width: 1600px) { 23 | flex-basis: calc(33.33% - 24px); 24 | } 25 | @media screen and (max-width: 1200px) { 26 | flex-basis: calc(50% - 24px); 27 | } 28 | @media screen and (max-width: 768px) { 29 | flex-basis: calc(100%); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/components/RestaurantInfo/index.module.scss: -------------------------------------------------------------------------------- 1 | .cardInfo { 2 | margin: 0; 3 | overflow: hidden; 4 | padding: 30px; 5 | text-align: center; 6 | font-family: 'Open Sans'; 7 | font-size: 1.2rem; 8 | color: rgb(107, 107, 107); 9 | p { 10 | margin-bottom: 10px; 11 | margin-top: 0; 12 | line-height: 100%; 13 | } 14 | @media screen and (max-width: 768px) { 15 | padding: 10px; 16 | } 17 | } 18 | 19 | .type { 20 | font-size: 1.3rem; 21 | font-weight: 500; 22 | color: black; 23 | margin: 0; 24 | font-style: italic; 25 | } 26 | 27 | .paragraph { 28 | display: flex; 29 | width: 100%; 30 | justify-content: space-between; 31 | margin-left: 30px; 32 | margin-right: 30px; 33 | } 34 | -------------------------------------------------------------------------------- /app/src/containers/SingleRestaurantContainer/constants.js: -------------------------------------------------------------------------------- 1 | export const REVIEWS_LOAD_INITIATION = 'REVIEWS_LOAD_INITIATION'; 2 | export const REVIEWS_LOAD_SUCCESS = 'REVIEWS_LOAD_SUCCESS'; 3 | export const REVIEWS_LOAD_FAILURE = 'REVIEWS_LOAD_FAILURE'; 4 | export const ADD_REVIEW_INITIATION = 'ADD_REVIEW_INITIATION'; 5 | export const ADD_REVIEW_SUCCESS = 'ADD_REVIEW_SUCCESS'; 6 | export const ADD_REVIEW_FAILURE = 'ADD_REVIEW_FAILURE'; 7 | export const REVIEWS_ERRORS = 'REVIEWS_ERRORS'; 8 | export const OPEN_FULL_REVIEW = 'OPEN_FULL_REVIEW'; 9 | export const CLOSE_FULL_REVIEW = 'CLOSE_FULL_REVIEW'; 10 | export const LOAD_INITIAL_REVIEWS = 'LOAD_INITIAL_REVIEWS'; 11 | export const TOGGLE_RESTAURANT_HOURS = 'TOGGLE_RESTAURANT_HOURS'; 12 | -------------------------------------------------------------------------------- /app/src/components/FilterHeading/README.md: -------------------------------------------------------------------------------- 1 | ## FilterHeading Component 2 | A component that shows a heading with the currently selected filters. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **filters** | Object | | An Object whose keys map to set filters, i.e. ratingFilter 15 | | **isHidden** | Boolean | | Boolean value for whether the heading is shown or not. 16 | | **isFiltered** | Boolean | | A boolean value to determine if there is a set filter 17 | -------------------------------------------------------------------------------- /app/src/containers/AddReviewContainer/README.md: -------------------------------------------------------------------------------- 1 | ## AddReviewContainer 2 | A higher-order container that surrounds the add review form. 3 | 4 | ### Example Usage 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | 11 | ### Props 12 | 13 | | Prop | Type | Default | Possible Values 14 | | ------------- | -------- | ----------- | --------------------------------------------- 15 | | **isAddingReview** | Bool | | Whether you are currently adding a review or not. 16 | | **fields** | Object | | Redux form fields 17 | | **hasFab** | Bool | | Whether the container should show a fab button to add a review. 18 | | **resetForm** | Function | | A callback function to reset the form's state. 19 | -------------------------------------------------------------------------------- /app/src/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import { reducer as formReducer } from 'redux-form'; 4 | 5 | // Import all of your reducers here: 6 | import featured from './containers/LandingContainer/reducer'; 7 | import restaurants from './containers/RestaurantsGridContainer/reducer'; 8 | import singleRestaurant from './containers/singleRestaurantContainer/reducer'; 9 | import addReview from './containers/AddReviewContainer/reducer'; 10 | 11 | const rootReducer = combineReducers({ 12 | // Apply all of the reducers here. 13 | featured, 14 | restaurants, 15 | singleRestaurant, 16 | addReview, 17 | routing: routerReducer, 18 | form: formReducer, 19 | }); 20 | 21 | export default rootReducer; 22 | -------------------------------------------------------------------------------- /config/generators/component/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | {{#if wantPropTypes}} 2 | import React, { PropTypes } from 'react'; 3 | {{else}} 4 | import React from 'react'; 5 | {{/if}} 6 | 7 | {{#if wantSCSSModules}} 8 | import styles from './index.module.scss'; 9 | import cssModules from 'react-css-modules'; 10 | {{/if}} 11 | 12 | const {{ properCase name }} = (props) => ( 13 | {{#if wantSCSSModules}} 14 |
15 | {{else}} 16 |
17 | {{/if}} 18 |
19 | ); 20 | 21 | {{#if wantPropTypes}} 22 | {{ properCase name }}.propTypes = { 23 | 24 | }; 25 | {{/if}} 26 | 27 | {{#if wantSCSSModules}} 28 | export default cssModules({{ properCase name }}, styles); 29 | {{else}} 30 | export default {{ properCase name }}; 31 | {{/if}} 32 | -------------------------------------------------------------------------------- /app/src/containers/RestaurantsGridContainer/constants.js: -------------------------------------------------------------------------------- 1 | export const RESTAURANTS_LOADING_INITIATION = 'RESTAURANTS_LOADING_INITIATION'; 2 | export const RESTAURANTS_LOADING_SUCCESS = 'RESTAURANTS_LOADING_SUCCESS'; 3 | export const RESTAURANTS_LOADING_FAILURE = 'RESTAURANTS_LOADING_FAILURE'; 4 | export const RESTAURANT_CATEGORIES = 'RESTAURANT_CATEGORIES'; 5 | export const CLEAR_RESTAURANT_ERRORS = 'CLEAR_RESTAURANT_ERRORS'; 6 | export const RESTAURANT_LOCATIONS = 'RESTAURANT_LOCATIONS'; 7 | export const SET_FILTER_LOCATION = 'SET_FILTER_LOCATION'; 8 | export const SET_FILTER_RATING = 'SET_FILTER_RATING'; 9 | export const SET_FILTER_CATEGORY = 'SET_FILTER_CATEGORY'; 10 | export const APPLY_RESTAURANTS_FILTERS = 'APPLY_RESTAURANTS_FILTERS'; 11 | export const CLEAR_RESTAURANTS_FILTERS = 'CLEAR_RESTAURANTS_FILTERS'; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | npm-debug.log 4 | .DS_Store 5 | ======= 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | -------------------------------------------------------------------------------- /app/src/components/AddButton/index.module.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | font-size: 1.6rem; 3 | stroke: white !important; 4 | fill: white !important; 5 | } 6 | 7 | .fabButton { 8 | stroke: white; 9 | } 10 | 11 | .fabContainer { 12 | z-index: 1000; 13 | position: fixed; 14 | bottom: 0; 15 | right: 0; 16 | margin: 1.5em; 17 | } 18 | 19 | .fab { 20 | background-color: #DC2878; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | border-radius: 50%; 25 | height: 70px; 26 | width: 70px; 27 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 28 | transition: all 0.3s cubic-bezier(.25,.8,.25,1); 29 | &:hover { 30 | box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); 31 | } 32 | title { 33 | color: black !important; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/components/FullReviewModal/README.md: -------------------------------------------------------------------------------- 1 | ## FullReviewModal Component 2 | A component that shows a review in a full screen modal layer. 3 | 4 | ### Example 5 | 6 | ```js 7 | 12 | ``` 13 | isOpen, 14 | onToggleClose, 15 | review, 16 | ### Props 17 | 18 | | Prop | Type | Default | Possible Values 19 | | ------------- | -------- | ----------- | --------------------------------------------- 20 | | **isOpen** | Bool | | Whether the layer is open, or just nonexistant. 21 | | **onToggleClose** | Function | | A function to use to close the element. 22 | | **review** | Object | | An object representing one review item. 23 | 24 | 25 | ### Other Information 26 | -------------------------------------------------------------------------------- /config/generators/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const componentGenerator = require('./component/index.js'); 3 | const containerGenerator = require('./container/index.js'); 4 | const pagesGenerator = require('./page/index.js'); 5 | 6 | module.exports = (plop) => { 7 | plop.setGenerator('component', componentGenerator); 8 | plop.setGenerator('container', containerGenerator); 9 | plop.setGenerator('page', pagesGenerator); 10 | plop.addHelper('uppercase', (text) => { 11 | return text.toUpperCase(); 12 | }); 13 | plop.addHelper('directory', (comp) => { 14 | try { 15 | fs.accessSync(`app/src/containers/${comp}`, fs.F_OK); 16 | return `containers/${comp}`; 17 | } catch (e) { 18 | return `components/${comp}`; 19 | } 20 | }); 21 | plop.addHelper('curly', (object, open) => (open ? '{' : '}')); 22 | }; 23 | -------------------------------------------------------------------------------- /app/src/containers/FullReviewModalContainer/README.md: -------------------------------------------------------------------------------- 1 | ## FullReviewModalContainer Component 2 | A higher order container for showing a full screen modal for a review. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **review** | Object | | An object representing a review (see below). 15 | 16 | 17 | ### Other Information 18 | Higher order component that sits on top of the component for a fullscreen modal review. 19 | 20 | Review Model 21 | ```js 22 | const myReview = { 23 | id: 0, 24 | text: "blah blah blah", 25 | rating: 5, 26 | person: "Ryan Collins", 27 | date: "1/12/2016", 28 | }; 29 | ``` 30 | -------------------------------------------------------------------------------- /app/src/components/ReviewGrid/README.md: -------------------------------------------------------------------------------- 1 | ## ReviewGrid Component 2 | A component that shows a grid of reviews for a single restaurant object. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **reviews** | Array | | An array of reviews for the restaurant. 15 | | **onClickReview** | Function | | A callback function called when a review is clicked. 16 | 17 | ### Other Information 18 | Presentational only component, used in the single restaurant page. 19 | 20 | See the [API repository](https://github.com/RyanCCollins/restaurant-reviewer-api) for more info on the model. 21 | -------------------------------------------------------------------------------- /app/src/pages/LandingPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import styles from './index.module.scss'; 4 | /* eslint-disable*/ // Containers is an alias, so no file is found 5 | import { 6 | LandingContainer, 7 | RestaurantsGridContainer, 8 | } from 'containers'; 9 | import { 10 | AppFooter, 11 | } from 'components'; 12 | import { updatePageTitle } from 'utils/a11y'; 13 | 14 | /* eslint-enable */ 15 | class LandingPage extends Component { 16 | componentDidMount() { 17 | updatePageTitle('Home Page'); 18 | } 19 | render() { 20 | return ( 21 |
22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default cssModules(LandingPage, styles); 31 | -------------------------------------------------------------------------------- /app/src/pages/SingleRestaurantPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import styles from './index.module.scss'; 4 | import { 5 | SingleRestaurantContainer, 6 | } from 'containers'; 7 | import { 8 | AppFooter, 9 | } from 'components'; 10 | import { updatePageTitle } from 'utils/a11y'; 11 | 12 | class SingleRestaurantPage extends Component { 13 | componentDidMount() { 14 | updatePageTitle('Restaurant Details'); 15 | } 16 | render() { 17 | return ( 18 |
19 | 20 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | SingleRestaurantPage.propTypes = { 27 | location: PropTypes.object.isRequired, 28 | }; 29 | 30 | export default cssModules(SingleRestaurantPage, styles); 31 | -------------------------------------------------------------------------------- /app/src/components/RestaurantHoursListItem/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:57:06-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:49-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import ListItem from 'grommet/components/ListItem'; 14 | 15 | const RestaurantHoursListItem = ({ 16 | day, 17 | hours, 18 | }) => ( 19 | 20 | {day} 21 | {hours} 22 | 23 | ); 24 | 25 | RestaurantHoursListItem.propTypes = { 26 | day: PropTypes.string.isRequired, 27 | hours: PropTypes.string.isRequired, 28 | }; 29 | 30 | export default RestaurantHoursListItem; 31 | -------------------------------------------------------------------------------- /app/src/components/ReviewSrOnly/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:58:22-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:19-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | 13 | import React, { PropTypes } from 'react'; 14 | import styles from './index.module.scss'; 15 | import cssModules from 'react-css-modules'; 16 | 17 | const ReviewSronly = ({ 18 | review, 19 | }) => ( 20 | 21 | 22 | {`${review.total_stars} out of 5 stars.`} 23 | 24 | 25 | ); 26 | 27 | ReviewSronly.propTypes = { 28 | review: PropTypes.object.isRequired, 29 | }; 30 | 31 | export default cssModules(ReviewSronly, styles); 32 | -------------------------------------------------------------------------------- /app/src/containers/AddReviewContainer/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | TOGGLE_ADD_REVIEW, 3 | ADD_REVIEW_INVALID, 4 | CLEAR_ADD_REVIEW_ERRORS, 5 | } from './constants'; 6 | 7 | const initialState = { 8 | isAddingReview: false, 9 | error: undefined, 10 | }; 11 | 12 | const addReviewReducer = 13 | (state = initialState, action) => { 14 | switch (action.type) { 15 | case TOGGLE_ADD_REVIEW: 16 | return Object.assign({}, state, { 17 | isAddingReview: !state.isAddingReview, 18 | }); 19 | case ADD_REVIEW_INVALID: 20 | return Object.assign({}, state, { 21 | error: action.error, 22 | }); 23 | case CLEAR_ADD_REVIEW_ERRORS: 24 | return Object.assign({}, state, { 25 | error: undefined, 26 | }); 27 | default: 28 | return state; 29 | } 30 | }; 31 | 32 | export default addReviewReducer; 33 | -------------------------------------------------------------------------------- /app/src/containers/LandingContainer/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOAD_IMAGES_SUCCESS, 3 | LOAD_IMAGES_INITIATION, 4 | } from './constants'; 5 | 6 | // loadImagesInitiation :: None -> {Action} 7 | const loadImagesInitiation = () => ({ 8 | type: LOAD_IMAGES_INITIATION, 9 | }); 10 | 11 | // loadImagesSuccess :: None -> {Action} 12 | const loadImagesSuccess = () => ({ 13 | type: LOAD_IMAGES_SUCCESS, 14 | }); 15 | 16 | // fakeDelay :: None -> Promise 17 | const fakeDelay = () => 18 | new Promise((resolve) => { 19 | setTimeout(() => { 20 | resolve('Wooha!!'); 21 | }, 2000); 22 | }); 23 | 24 | // loadImagesAsync :: None -> Function -> {Action} 25 | export const loadImagesAsync = () => 26 | (dispatch) => { 27 | dispatch(loadImagesInitiation()); 28 | fakeDelay() // Fake asynchronicity. 29 | .then(() => dispatch( 30 | loadImagesSuccess() 31 | ) 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /app/src/components/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:54:33-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:40-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import 'styles/styles.scss'; 14 | import { Navbar } from 'components'; 15 | import AppComponent from 'grommet/components/app'; 16 | 17 | const App = (props) => ( 18 | 19 | 20 |
21 | {React.cloneElement(props.children, props)} 22 |
23 |
24 | ); 25 | 26 | App.propTypes = { 27 | children: PropTypes.node.isRequired, 28 | location: PropTypes.object.isRequired, 29 | }; 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /app/src/components/RestaurantGridItem/README.md: -------------------------------------------------------------------------------- 1 | ## RestaurantGridItem Component 2 | A component that represents a single item in the restaurant grid. 3 | 4 | ### Example 5 | 6 | ```js 7 | ... 8 | {restaurants.map((rest,index) => 9 | 14 | )} 15 | ``` 16 | 17 | ### Props 18 | 19 | | Prop | Type | Default | Possible Values 20 | | ------------- | -------- | ----------- | --------------------------------------------- 21 | | **restaurant** | Object | | A single restaurant object represening the view of the details for an item. 22 | | **onViewDetails** | Function | | A callback function called when the view detail button for a single item is pressed. 23 | 24 | ### Other Information 25 | Acts only as a presentational component. 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Restaurant Reviewer 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/components/ReviewSrOnly/README.md: -------------------------------------------------------------------------------- 1 | ## ReviewSRonly Component 2 | A component that Allows for a screen reader only overview of the review. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **review** | Object | | An object representing a single review (see below for details). 15 | 16 | ### Other Information 17 | Although not always best practice to make things screen reader only, in this case, it makes sense because it keeps the focus cycling through the modal and gives the end user something to focus on when viewing a full review. 18 | 19 | ```js 20 | const myReview = { 21 | id: 0, 22 | text: "blah blah blah", 23 | rating: 5, 24 | person: "Ryan Collins", 25 | date: "1/12/2016", 26 | }; 27 | ``` 28 | -------------------------------------------------------------------------------- /app/src/components/StarRating/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import ReactStars from 'react-stars'; 3 | import styles from './index.module.scss'; 4 | import cssModules from 'react-css-modules'; 5 | 6 | const StarRating = ({ 7 | value, 8 | label, 9 | editable, 10 | onEdit, 11 | }) => ( 12 |
13 | 25 |
26 | ); 27 | 28 | StarRating.propTypes = { 29 | value: PropTypes.number.isRequired, 30 | label: PropTypes.string.isRequired, 31 | editable: PropTypes.bool.isRequired, 32 | onEdit: PropTypes.func, 33 | }; 34 | 35 | export default cssModules(StarRating, styles); 36 | -------------------------------------------------------------------------------- /config/generators/component/es6class.js.hbs: -------------------------------------------------------------------------------- 1 | {{#if wantPropTypes}} 2 | import React, { PropTypes, Component } from 'react'; 3 | {{else}} 4 | import React, { Component } from 'react'; 5 | {{/if}} 6 | {{#if wantSCSSModules}} 7 | import styles from './index.module.scss'; 8 | import cssModules from 'react-css-modules'; 9 | {{/if}} 10 | 11 | class {{ properCase name }} extends Component { // eslint-disable-line react/prefer-stateless-function 12 | render() { 13 | return ( 14 | {{#if wantSCSSModules}} 15 |
16 | {{else}} 17 |
18 | {{/if}} 19 |
20 | ); 21 | } 22 | } 23 | 24 | {{#if wantPropTypes}} 25 | {{ properCase name }}.propTypes = { 26 | 27 | }; 28 | {{/if}} 29 | 30 | {{#if wantSCSSModules}} 31 | export default cssModules({{ properCase name }}, styles); 32 | {{else}} 33 | export default {{ properCase name }}; 34 | {{/if}} 35 | -------------------------------------------------------------------------------- /app/src/components/FilterMenu/README.md: -------------------------------------------------------------------------------- 1 | ## FilterMenu Component 2 | A component that shows a menu to select a filter list item. 3 | 4 | ### Example 5 | 6 | ```js 7 | 13 | ``` 14 | 15 | ### Props 16 | 17 | | Prop | Type | Default | Possible Values 18 | | ------------- | -------- | ----------- | --------------------------------------------- 19 | | **menuItems** | Array | | An array containing strings, which are selectable items 20 | | **onSelectItem** | Func | | A function called when selecting an item. 21 | | **label** | String | | String label for the component 22 | | **selectedItem** | Object | | The item that is currently selected. 23 | 24 | ### Other Information 25 | This component is presentational and does not hold any state. 26 | -------------------------------------------------------------------------------- /app/src/components/LoadingIndicator/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:56:12-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:08-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | 16 | const LoadingIndicator = ({ 17 | isLoading, 18 | }) => ( 19 |
20 | {isLoading && 21 | 22 |
23 |

Loading...

24 | 25 | } 26 |
27 | ); 28 | 29 | LoadingIndicator.propTypes = { 30 | isLoading: PropTypes.bool, 31 | }; 32 | 33 | export default cssModules(LoadingIndicator, styles); 34 | -------------------------------------------------------------------------------- /app/src/components/RestaurantInfo/README.md: -------------------------------------------------------------------------------- 1 | ## RestaurantInfo Component 2 | A component that shows the restaurants info, following specifications for project. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **restaurant** | Object | | An object describing a restaurant. 15 | 16 | 17 | ### Other Information 18 | Does not error check for bad data. Is a presentational component ONLY. 19 | 20 | ```js 21 | const myRestaurant = { 22 | id: 1, 23 | name: "The New Magnum Café", 24 | address: "Jalan M Husni Thamrin 1", 25 | city: "Jakarta Pusat", 26 | state:"Daerah Khusus Ibukota Jakarta", 27 | zip:"23435", 28 | country: "id", 29 | phone: "(021) 23580055", 30 | website: "http://mymagnum.co.id/", 31 | }; 32 | ``` 33 | -------------------------------------------------------------------------------- /app/src/components/StarRating/README.md: -------------------------------------------------------------------------------- 1 | ## StarRating Component 2 | A component that shows as a star rating. 3 | 4 | ### Example 5 | 6 | ```js 7 | 12 | ``` 13 | 14 | ### Props 15 | 16 | | Prop | Type | Default | Possible Values 17 | | ------------- | -------- | ----------- | --------------------------------------------- 18 | | **value** | Number | | The numeric value, i.e. number of stars filled in. 19 | | **label** | String | | A label that acts as an a11y name 20 | | **editable** | Boolean | | Whether the star rating should be editable 21 | | **onEdit** | Function | | Optional callback function for when the rating is changed. 22 | 23 | ### Other Information 24 | The component can be editable, or not editable. It should have an onEdit function passed in as a prop if you wish to subscribe to changes. 25 | -------------------------------------------------------------------------------- /server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Restaurant Reviewer 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, IndexRoute } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | import store, { history } from './store'; 5 | /* eslint-disable */ 6 | import App from 'components/App'; 7 | import * as Pages from 'pages'; 8 | /* eslint-enable */ 9 | 10 | const routes = ( 11 | 12 | { 15 | document.getElementById('content').focus(); 16 | window.scrollTo(0, 0); 17 | }} /* eslint-enable */ 18 | history={history} 19 | > 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | export default routes; 31 | -------------------------------------------------------------------------------- /app/src/components/SingleRestaurant/README.md: -------------------------------------------------------------------------------- 1 | ## SingleRestaurant Component 2 | A component that represents a single restaurant, showing a panel of information for the restaurant and a header with the restaurants name. 3 | 4 | ### Example 5 | 6 | ```js 7 | 8 | ``` 9 | 10 | ### Props 11 | 12 | | Prop | Type | Default | Possible Values 13 | | ------------- | -------- | ----------- | --------------------------------------------- 14 | | **restaurant** | Object | | An object representing a restaurant (see below). 15 | 16 | 17 | ### Other Information 18 | Presentational component to show a single restaurant. 19 | 20 | ```js 21 | const myRestaurant = { 22 | id: 1, 23 | name: "The New Magnum Café", 24 | address: "Jalan M Husni Thamrin 1", 25 | city: "Jakarta Pusat", 26 | state:"Daerah Khusus Ibukota Jakarta", 27 | zip:"23435", 28 | country: "id", 29 | phone: "(021) 23580055", 30 | website: "http://mymagnum.co.id/", 31 | }; 32 | ``` 33 | -------------------------------------------------------------------------------- /app/src/components/RestaurantPanel/README.md: -------------------------------------------------------------------------------- 1 | ## RestaurantPanel Component 2 | A presentational component that represents information for a single restaurant, to be shown on the single restaurant page. 3 | 4 | ### Example 5 | 6 | ```js 7 | 10 | ``` 11 | 12 | ### Props 13 | 14 | | Prop | Type | Default | Possible Values 15 | | ------------- | -------- | ----------- | --------------------------------------------- 16 | | **restaurant** | Object | | A restaurant to show information about. 17 | 18 | 19 | ### Other Information 20 | Presentational component only. Transforms a restaurant model object as show below. 21 | 22 | ```js 23 | const myRestaurant = { 24 | id: 1, 25 | name: "The New Magnum Café", 26 | address: "Jalan M Husni Thamrin 1", 27 | city: "Jakarta Pusat", 28 | state:"Daerah Khusus Ibukota Jakarta", 29 | zip:"23435", 30 | country: "id", 31 | phone: "(021) 23580055", 32 | website: "http://mymagnum.co.id/", 33 | }; 34 | ``` 35 | -------------------------------------------------------------------------------- /app/src/components/BannerHeader/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:54:40-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:35-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import Heading from 'grommet/components/heading'; 16 | import sanityCheck from 'utils/sanityCheck'; 17 | 18 | const BannerHeader = ({ 19 | heading, 20 | children, 21 | }) => ( 22 |
23 |
24 | 25 | {heading} 26 | 27 | {sanityCheck(children) && children} 28 |
29 |
30 | ); 31 | 32 | BannerHeader.propTypes = { 33 | heading: PropTypes.string.isRequired, 34 | children: PropTypes.node, 35 | }; 36 | 37 | export default cssModules(BannerHeader, styles); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan Collins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/src/components/AddReviewForm/README.md: -------------------------------------------------------------------------------- 1 | ## AddReviewForm Component 2 | A component that acts as a form for creating a review. 3 | 4 | ### Example 5 | 6 | ```js 7 | 12 | ``` 13 | 14 | ### Props 15 | 16 | | Prop | Type | Default | Possible Values 17 | | ------------- | -------- | ----------- | --------------------------------------------- 18 | | **onSubmitReview** | Function | | A function to handle submitting a new review 19 | | **onClear** | Function | | A function to handle clearing of the form 20 | | **nameInput** | Object | | A form field created by redux form 21 | | **ratingInput** | Object | | A form field created by redux form 22 | | **textInput** | Object | | A form field created by redux form 23 | 24 | ### Other Information 25 | Acts mostly as a stateless functional component, although the complexity required a few internal functions. Sits beneath a container component that connects it to the redux form and to the submission / API functions. 26 | -------------------------------------------------------------------------------- /app/src/components/RestaurantGrid/README.md: -------------------------------------------------------------------------------- 1 | ## RestaurantGrid Component 2 | A presentational component that shows a grid of restaurants. 3 | 4 | ### Example 5 | 6 | ```js 7 | 11 | ``` 12 | 13 | ### Props 14 | 15 | | Prop | Type | Default | Possible Values 16 | | ------------- | -------- | ----------- | --------------------------------------------- 17 | | **restaurants** | Array | | An Array of restaurants to map to the grid items 18 | | **onViewDetails** | Function | | A function to call when one item is tapped to view details for that item. 19 | 20 | 21 | ### Other Information 22 | Presentational only component with no state. 23 | 24 | ```js 25 | const myRestaurants = [ 26 | { 27 | id: 1, 28 | name: "The New Magnum Café", 29 | address: "Jalan M Husni Thamrin 1", 30 | city: "Jakarta Pusat", 31 | state:"Daerah Khusus Ibukota Jakarta", 32 | zip:"23435", 33 | country: "id", 34 | phone: "(021) 23580055", 35 | website: "http://mymagnum.co.id/", 36 | }, 37 | { 38 | ... 39 | } 40 | ] 41 | ``` 42 | -------------------------------------------------------------------------------- /app/src/components/FilterHeading/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styles from './index.module.scss'; 3 | import cssModules from 'react-css-modules'; 4 | import Heading from 'grommet/components/Heading'; 5 | 6 | const pluralize = (x, ratingFilter) => 7 | parseInt(ratingFilter, 10) === 1 ? x : `${x}s`; 8 | 9 | const FilterHeading = ({ 10 | filters, 11 | isHidden, 12 | isFiltered, 13 | }) => ( 14 |
15 | 16 | {isFiltered ? 'Filtered By' : 'Selected Filters'} 17 | 18 | 19 | Category: {filters.categoryFilter}{', '} 20 | Location: {filters.locationFilter}{', '} 21 | Rating: {`${filters.ratingFilter} 22 | ${filters.ratingFilter === 'All' ? '' : pluralize('Star', filters.ratingFilter)} 23 | `} 24 | 25 | 26 |
27 | ); 28 | 29 | FilterHeading.propTypes = { 30 | filters: PropTypes.object.isRequired, 31 | isHidden: PropTypes.bool.isRequired, 32 | isFiltered: PropTypes.bool.isRequired, 33 | }; 34 | 35 | export default cssModules(FilterHeading, styles); 36 | -------------------------------------------------------------------------------- /app/src/components/NoRestaurantsFound/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styles from './index.module.scss'; 3 | import cssModules from 'react-css-modules'; 4 | import Heading from 'grommet/components/Heading'; 5 | import Paragraph from 'grommet/components/paragraph'; 6 | import Section from 'grommet/components/section'; 7 | import { CodeBlock } from 'components'; 8 | 9 | const NoRestaurantsFound = ({ 10 | filter, 11 | }) => ( 12 |
13 | 19 | No Restaurants Found 20 | 21 | 22 | Sorry, but the set filter did not return any results. 23 | 24 | 33 |
34 | ); 35 | 36 | NoRestaurantsFound.propTypes = { 37 | filter: PropTypes.object, 38 | }; 39 | 40 | export default cssModules(NoRestaurantsFound, styles); 41 | -------------------------------------------------------------------------------- /app/src/components/RestaurantReview/README.md: -------------------------------------------------------------------------------- 1 | ## RestaurantReview Component 2 | A component that shows a review for a restaurant. Mapped over to show a list of reviews in a grid. 3 | 4 | ### Example 5 | 6 | ```js 7 | ... 8 | {myReviews.map((item, i) => 9 | 10 | )} 11 | ... 12 | ``` 13 | review, 14 | onReviewClick, 15 | 16 | ### Props 17 | 18 | | Prop | Type | Default | Possible Values 19 | | ------------- | -------- | ----------- | --------------------------------------------- 20 | | **review** | Object | | A single review item to show a review for the restaurant (see details below) 21 | | **onReviewClick** | Function | | A callback function to call when a review is clicked 22 | 23 | 24 | ### Other Information 25 | Presentational only component. Shows a review with a star rating (1-5), text, name of reviewer and date of review. 26 | 27 | ```js 28 | const myReview = { 29 | id: 0, 30 | text: "blah blah blah", 31 | rating: 5, 32 | person: "Ryan Collins", 33 | date: "1/12/2016", 34 | }; 35 | ``` 36 | 37 | The model for reviews is further detailed in the [restaurant reviewer API repo](https://github.com/RyanCCollins/restaurant-reviewer-api). 38 | -------------------------------------------------------------------------------- /app/src/components/FilterRestaurants/README.md: -------------------------------------------------------------------------------- 1 | ## FilterRestaurants Component 2 | A component that shows two filter buttons to allow filtering of the restaurant lists. 3 | 4 | ### Example 5 | 6 | ```js 7 | 13 | ``` 14 | 15 | ### Props 16 | 17 | | Prop | Type | Default | Possible Values 18 | | ------------- | -------- | ----------- | --------------------------------------------- 19 | | **locations** | String | | An array of location objects containing a value and id for filtering 20 | | **ratings** | String | | An array of rating objects containing a value and id for filtering 21 | | **onFilterLocations** | Function | | A function to call when a location filter has been selected 22 | | **onFilterRatings** | Function | | A function to call when a rating filter has been selected 23 | 24 | locations, 25 | ratings, 26 | onFilterLocations, 27 | onFilterRatings, 28 | 29 | ### Other Information 30 | In some ways is a higher order component, although there is no internal state so left as a component vs. container. 31 | -------------------------------------------------------------------------------- /app/src/pages/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import styles from './index.module.scss'; 4 | import { Link } from 'react-router'; 5 | import Button from 'grommet/components/Button'; 6 | import Heading from 'grommet/components/Heading'; 7 | import Article from 'grommet/components/Article'; 8 | import Section from 'grommet/components/Section'; 9 | import Footer from 'grommet/components/Footer'; 10 | 11 | const NotFound = () => ( 12 |
13 |
14 | 15 | Page Not Found 16 | 17 |
18 | 19 | We're sorry, but there was nothing we could do. 😳 20 | 21 |
22 | 23 | 29 | 30 |
31 |
32 |
33 |
34 | ); 35 | 36 | export default cssModules(NotFound, styles); 37 | -------------------------------------------------------------------------------- /app/src/components/AddButton/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:54:23-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:54-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import Add from 'grommet/components/icons/base/Add'; 16 | import Button from 'grommet/components/button'; 17 | 18 | const AddButton = ({ 19 | onAdd, 20 | }) => ( 21 |
22 |
23 |
37 |
38 | ); 39 | 40 | AddButton.propTypes = { 41 | onAdd: PropTypes.func.isRequired, 42 | }; 43 | 44 | export default cssModules(AddButton, styles); 45 | -------------------------------------------------------------------------------- /app/src/components/index.js: -------------------------------------------------------------------------------- 1 | /* Assemble all components for export */ 2 | export StarRating from './StarRating'; 3 | export AboutInfo from './AboutInfo'; 4 | export AppFooter from './AppFooter'; 5 | export RestaurantHoursListItem from './RestaurantHoursListItem'; 6 | export SrOnlyContent from './SronlyContent'; 7 | export FilterHeading from './FilterHeading'; 8 | export CodeBlock from './CodeBlock'; 9 | export NoRestaurantsFound from './NoRestaurantsFound'; 10 | export FilterMenu from './FilterMenu'; 11 | export FilterRestaurants from './FilterRestaurants'; 12 | export ReviewSrOnly from './ReviewSrOnly'; 13 | export ErrorAlert from './ErrorAlert'; 14 | export FullReviewModal from './FullReviewModal'; 15 | export RestaurantHours from './RestaurantHours'; 16 | export RestaurantInfo from './RestaurantInfo'; 17 | export RestaurantGridItem from './RestaurantGridItem'; 18 | export AddButton from './AddButton'; 19 | export ReviewGrid from './ReviewGrid'; 20 | export AddReviewForm from './AddReviewForm'; 21 | export BannerHeader from './BannerHeader'; 22 | export SingleRestaurant from './SingleRestaurant'; 23 | export RestaurantReview from './RestaurantReview'; 24 | export RestaurantPanel from './RestaurantPanel'; 25 | export Navbar from './Navbar'; 26 | export RestaurantGrid from './RestaurantGrid'; 27 | export LoadingIndicator from './LoadingIndicator'; 28 | export HeroCarousel from './HeroCarousel'; 29 | export App from './App'; 30 | -------------------------------------------------------------------------------- /app/src/components/RestaurantReview/index.module.scss: -------------------------------------------------------------------------------- 1 | .restaurantReview { 2 | font-size: 1.3rem; 3 | font-family: 'Open Sans'; 4 | font-weight: 200; 5 | color: #666; 6 | margin: 10px 0; 7 | text-align: center; 8 | } 9 | 10 | .name { 11 | margin-bottom: 5px; 12 | font-size: 1.6rem; 13 | text-align: center; 14 | } 15 | 16 | .header { 17 | @media screen and (max-width: 768px) { 18 | font-size: 1.6rem; 19 | } 20 | } 21 | 22 | .date { 23 | color: #262626 !important; 24 | font-size: 1em !important; 25 | position: absolute; 26 | bottom: 10px; 27 | right: 20px; 28 | padding: 0; 29 | z-index: 5; 30 | margin-bottom: 0 !important; 31 | } 32 | 33 | .reviewParagraph { 34 | text-align: left; 35 | font-size: 1.3rem !important; 36 | margin-top: 12px !important; 37 | } 38 | 39 | .box { 40 | margin: 12px; 41 | padding: 2.143em; 42 | height: 330px; 43 | padding: 1.5em; 44 | z-index: 1; 45 | cursor: pointer; 46 | position: relative; 47 | background: #FFFFFF; 48 | border: 1px solid #dbe2e8; 49 | color: #2e3d49; 50 | box-shadow: 0px 2px 4px 0px rgba(46,60,73,0.2); 51 | &:after { 52 | background: rgba(255,255,255,0.5); 53 | background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, #fff 50%, #fff 100%); 54 | width: 100%; 55 | height: 66px; 56 | content: ''; 57 | display: block; 58 | position: absolute; 59 | bottom: 0; 60 | left: 0; 61 | } 62 | } 63 | 64 | .content { 65 | overflow:hidden; 66 | } 67 | -------------------------------------------------------------------------------- /config/generators/container/index.js.hbs: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | {{#if wantActionsAndReducer}} 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | import * as {{ properCase name }}ActionCreators from './actions'; 6 | {{/if}} 7 | {{#if wantSCSSModules}} 8 | import cssModules from 'react-css-modules'; 9 | import styles from './index.module.scss'; 10 | {{/if}} 11 | 12 | class {{ properCase name }} extends Component { // eslint-disable-line react/prefer-stateless-function 13 | render() { 14 | return ( 15 | {{#if wantSCSSModules}} 16 |
17 | {{else}} 18 |
19 | {{/if}} 20 |
21 | ); 22 | } 23 | } 24 | 25 | {{#if wantActionsAndReducer}} 26 | // mapStateToProps :: {State} -> {Props} 27 | const mapStateToProps = (state) => ({ 28 | // myProp: state.myProp, 29 | }); 30 | 31 | // mapDispatchToProps :: Dispatch -> {Action} 32 | const mapDispatchToProps = (dispatch) => ({ 33 | actions: bindActionCreators( 34 | {{ properCase name }}ActionCreators, 35 | dispatch 36 | ), 37 | }); 38 | {{/if}} 39 | 40 | {{#if wantSCSSModules}} 41 | const Container = cssModules({{ properCase name }}, styles); 42 | {{else}} 43 | const Container = {{ properCase name }}; 44 | {{/if}} 45 | 46 | {{#if wantActionsAndReducer}} 47 | export default connect( 48 | mapStateToProps, 49 | mapDispatchToProps 50 | )(Container); 51 | {{else}} 52 | export default Container; 53 | {{/if}} 54 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const isDeveloping = process.env.NODE_ENV !== 'production'; 3 | const port = isDeveloping ? 8016 : process.env.PORT; 4 | const path = require('path'); 5 | const express = require('express'); 6 | const app = express(); 7 | 8 | if (isDeveloping) { 9 | const webpack = require('webpack'); 10 | const webpackMiddleware = require('webpack-dev-middleware'); 11 | const webpackHotMiddleware = require('webpack-hot-middleware'); 12 | const config = require('../webpack.config.babel.js'); 13 | const compiler = webpack(config); 14 | const middleware = webpackMiddleware(compiler, { 15 | publicPath: config.output.publicPath, 16 | contentBase: 'src', 17 | stats: { 18 | colors: true, 19 | hash: false, 20 | timings: true, 21 | chunks: false, 22 | chunkModules: false, 23 | modules: false 24 | } 25 | }); 26 | app.use(middleware); 27 | app.use(webpackHotMiddleware(compiler)); 28 | app.get('*', (req, res) => { 29 | res.write(middleware.fileSystem.readFileSync(path.join(__dirname, 'public/index.html'))); 30 | res.end(); 31 | }); 32 | } else { 33 | app.use(express.static(__dirname + '/public')); 34 | app.get('*', (req, res) => { 35 | res.sendFile(path.join(__dirname, 'public/index.html')); 36 | }); 37 | } 38 | 39 | app.listen(port, '0.0.0.0', (err) => { 40 | if (err) { 41 | console.warn(err) 42 | } 43 | console.info('==> 🌎 Listening on port %s. Open up http://0.0.0.0:%s/ in your browser.', port, port); 44 | }); 45 | /* eslint-enable */ 46 | -------------------------------------------------------------------------------- /app/src/components/SingleRestaurant/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:58:40-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:10-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import { 16 | RestaurantPanel, 17 | BannerHeader, 18 | } from 'components'; 19 | import Section from 'grommet/components/Section'; 20 | import Heading from 'grommet/components/Heading'; 21 | 22 | const SingleRestaurant = ({ 23 | restaurant, 24 | onExpandHours, 25 | hoursAreExpanded, 26 | }) => ( 27 |
28 | 29 | 30 | {`Average ${restaurant.average_rating} out of 5 Stars`} 31 | 32 | 33 |
34 | 39 |
40 |
41 | ); 42 | 43 | SingleRestaurant.propTypes = { 44 | restaurant: PropTypes.object.isRequired, 45 | hoursAreExpanded: PropTypes.bool.isRequired, 46 | onExpandHours: PropTypes.func.isRequired, 47 | }; 48 | 49 | export default cssModules(SingleRestaurant, styles); 50 | -------------------------------------------------------------------------------- /app/src/containers/LandingContainer/README.md: -------------------------------------------------------------------------------- 1 | ## LandingContainer 2 | A connect container that wraps over the landing page components 3 | 4 | ### Example Usage 5 | 6 | ```js 7 | 8 | // Props and actions are connected through redux, not passed into the component 9 | ``` 10 | 11 | ### Props 12 | 13 | | Prop | Type | Default | Possible Values 14 | | ------------- | -------- | ----------- | --------------------------------------------- 15 | | **restaurants** | Array | | Restaurants, which are provided by the Redux Store. 16 | | **isLoading** | Bool | | Whether the component is loading data or not. 17 | | **errors** | Array | | An array of errors, if there are any. 18 | | **actions** | Object | | Connected actions to mutate state through redux 19 | 20 | ### Other Information 21 | Higher order connected container, which gets props through the redux store. 22 | 23 | #### Models: 24 | 25 | Restaurants: 26 | ```js 27 | const myRestaurants = [ 28 | { 29 | id: 1, 30 | name: "The New Magnum Café", 31 | address: "Jalan M Husni Thamrin 1", 32 | city: "Jakarta Pusat", 33 | state:"Daerah Khusus Ibukota Jakarta", 34 | zip:"23435", 35 | country: "id", 36 | phone: "(021) 23580055", 37 | website: "http://mymagnum.co.id/", 38 | }, 39 | { 40 | ... 41 | } 42 | ]; 43 | ``` 44 | 45 | Errors: 46 | ``` 47 | const errors = [ 48 | { 49 | id: 0, 50 | message: 'Error message number 1', 51 | }, 52 | { 53 | id: 1, 54 | message: 'Error message number 2', 55 | } 56 | ]; 57 | ``` 58 | -------------------------------------------------------------------------------- /config/testing/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../webpack/webpack.test.babel'); 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | const path = require('path'); 4 | 5 | module.exports = (config) => { 6 | config.set({ 7 | frameworks: ['mocha'], 8 | reporters: ['coverage', 'mocha'], 9 | browsers: process.env.TRAVIS // eslint-disable-line no-nested-ternary 10 | ? ['ChromeTravis'] 11 | : process.env.APPVEYOR 12 | ? ['IE'] : ['Chrome'], 13 | 14 | autoWatch: false, 15 | singleRun: true, 16 | 17 | client: { 18 | mocha: { 19 | grep: argv.grep, 20 | }, 21 | }, 22 | 23 | files: [ 24 | { 25 | pattern: './test-bundler.js', 26 | watched: false, 27 | served: true, 28 | included: true, 29 | }, 30 | ], 31 | 32 | preprocessors: { 33 | ['./test-bundler.js']: [ 34 | 'webpack', 35 | 'sourcemap', 36 | ], 37 | }, 38 | 39 | webpack: webpackConfig, 40 | 41 | // make Webpack bundle generation quiet 42 | webpackMiddleware: { 43 | noInfo: true, 44 | stats: 'errors-only', 45 | }, 46 | 47 | customLaunchers: { 48 | ChromeTravis: { 49 | base: 'Chrome', 50 | flags: ['--no-sandbox'], 51 | }, 52 | }, 53 | 54 | coverageReporter: { 55 | dir: path.join(process.cwd(), 'coverage'), 56 | reporters: [ 57 | { type: 'lcov', subdir: 'lcov' }, 58 | { type: 'html', subdir: 'html' }, 59 | { type: 'text-summary' }, 60 | ], 61 | }, 62 | failOnEmptyTestSuite: false, 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /app/src/components/FullReviewModal/index.module.scss: -------------------------------------------------------------------------------- 1 | .fullReviewModal { 2 | font-size: 1.3rem; 3 | font-family: 'Open Sans'; 4 | font-weight: 200; 5 | color: #666; 6 | margin: 10px 0; 7 | text-align: center; 8 | } 9 | 10 | .name { 11 | margin-bottom: 5px; 12 | font-size: 1.6rem; 13 | text-align: center; 14 | } 15 | 16 | .header { 17 | display: flex; 18 | justify-content: center; 19 | } 20 | 21 | .date { 22 | background-color: #fff; 23 | padding: 10px; 24 | } 25 | 26 | .dateWrapper { 27 | text-align: center; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | 33 | .reviewParagraph { 34 | text-align: left; 35 | font-size: 1.3rem; 36 | } 37 | 38 | .dateDivider { 39 | width: 70%; 40 | height: 12px; 41 | border-bottom: 1px solid rgb(130, 130, 130); 42 | text-align: center; 43 | } 44 | 45 | .box { 46 | margin: 12px; 47 | padding: 2.143em; 48 | height: 330px; 49 | padding: 1.5em; 50 | z-index: 1; 51 | cursor: pointer; 52 | position: relative; 53 | background: #FFFFFF; 54 | border: 1px solid #dbe2e8; 55 | color: #2e3d49; 56 | box-shadow: 0px 2px 4px 0px rgba(46,60,73,0.2); 57 | } 58 | 59 | .content { 60 | overflow:hidden; 61 | } 62 | 63 | .quote { 64 | font-size: 2rem; 65 | @media screen and (max-width: 1100px) { 66 | font-size: 1.9rem; 67 | } 68 | @media screen and (max-width: 768px) { 69 | font-size: 1.8rem; 70 | } 71 | @media screen and (max-width: 600px) { 72 | font-size: 1.6rem; 73 | } 74 | } 75 | 76 | .center { 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | height: 100%; 81 | } 82 | -------------------------------------------------------------------------------- /app/src/components/RestaurantGrid/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:56:38-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:59-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import { RestaurantGridItem, FilterHeading } from 'components'; 16 | import Section from 'grommet/components/Section'; 17 | import filterIsSet from 'utils/filter'; 18 | 19 | const RestaurantGrid = ({ 20 | restaurants, 21 | onViewDetails, 22 | currentFilter, 23 | isFiltered, 24 | }) => ( 25 |
26 | 31 |
32 | {restaurants.map((restaurant, i) => 33 | 39 | )} 40 |
41 |
42 | ); 43 | 44 | RestaurantGrid.propTypes = { 45 | restaurants: PropTypes.array.isRequired, 46 | onViewDetails: PropTypes.func.isRequired, 47 | currentFilter: PropTypes.object, 48 | isFiltered: PropTypes.bool.isRequired, 49 | }; 50 | 51 | export default cssModules(RestaurantGrid, styles); 52 | -------------------------------------------------------------------------------- /app/src/components/Navbar/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:56:27-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:04-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | // Grommet must be imported component by component (i.e. no destructuring) 16 | import Header from 'grommet/components/Header'; 17 | import Title from 'grommet/components/Title'; 18 | import Menu from 'grommet/components/Menu'; 19 | import { Link } from 'react-router'; 20 | 21 | const Navbar = () => ( 22 |
23 | 24 | <Link to="/"> 25 | <img 26 | className={styles.logo} 27 | src="https://github.com/RyanCCollins/cdn/blob/master/restaurant-reviewer/a11ylogo.png?raw=true" 28 | alt="A11y Written in rainbow colors" 29 | /> 30 | </Link> 31 | 32 | 38 | 39 | Home 40 | 41 | 42 | About 43 | 44 | 45 |
46 | ); 47 | 48 | export default cssModules(Navbar, styles); 49 | -------------------------------------------------------------------------------- /app/src/components/HeroCarousel/index.module.scss: -------------------------------------------------------------------------------- 1 | .heroCarousel { 2 | max-height: 50vh; 3 | width: 100%; 4 | position: relative; 5 | @media screen and (max-width: 631px) { 6 | height: 100vh; 7 | } 8 | @media screen and (max-width: 768px) { 9 | height: 80vh; 10 | } 11 | } 12 | 13 | .carousel { 14 | max-height: 50vh; 15 | position: relative; 16 | @media screen and (max-width: 631px) { 17 | height: 100vh; 18 | } 19 | @media screen and (max-width: 768px) { 20 | height: 80vh; 21 | } 22 | } 23 | 24 | .overlay { 25 | position: absolute; 26 | width: 100%; 27 | max-width: 100%; 28 | box-sizing: border-box; 29 | height: 100%; 30 | color: white; 31 | bottom: 0; 32 | left: 50; 33 | right: 50; 34 | line-height: 10px; 35 | z-index: 1; 36 | display: flex; 37 | flex-direction: column; 38 | align-items: center; 39 | justify-content: center; 40 | } 41 | 42 | .headline { 43 | background-color: rgba(0, 0, 0, 0.5); 44 | text-shadow: 2px 2px 0px #865CD6; 45 | color: #ffffff; 46 | padding: 20px; 47 | } 48 | 49 | .subHeadline { 50 | font-style: italic; 51 | } 52 | 53 | .link { 54 | color: white !important; 55 | } 56 | 57 | .itemType { 58 | font-style: italic; 59 | font-size: 1.3rem; 60 | } 61 | 62 | .image { 63 | max-width:100% !important; 64 | height:auto; 65 | display:block; 66 | min-height: 50vh; 67 | @media screen and (max-height: 631px) { 68 | min-height: 100vh; 69 | } 70 | @media screen and (max-width: 768px) { 71 | min-height: 80vh; 72 | } 73 | } 74 | 75 | .rowOne{ 76 | display: flex; 77 | width: 100%; 78 | flex-wrap: nowrap; 79 | } 80 | 81 | .itemCaption { 82 | font-size: 1.3rem; 83 | } 84 | -------------------------------------------------------------------------------- /app/src/components/ReviewGrid/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:58:13-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:24-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import { RestaurantReview } from 'components'; 16 | import Heading from 'grommet/components/heading'; 17 | import Section from 'grommet/components/section'; 18 | import Tiles from 'grommet/components/tiles'; 19 | 20 | const ReviewGrid = ({ 21 | reviews, 22 | onClickReview, 23 | }) => ( 24 |
25 |
26 | 27 | Reviews 28 | 29 | 37 | {reviews.map((item, i) => 38 |
39 | 43 |
44 | )} 45 |
46 |
47 |
48 | ); 49 | 50 | ReviewGrid.propTypes = { 51 | reviews: PropTypes.array.isRequired, 52 | onClickReview: PropTypes.func.isRequired, 53 | }; 54 | 55 | export default cssModules(ReviewGrid, styles); 56 | -------------------------------------------------------------------------------- /app/src/components/HeroCarousel/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:55:57-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:12-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes, Component } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import Carousel from 'grommet/components/Carousel'; 16 | import Headline from 'grommet/components/Headline'; 17 | import Heading from 'grommet/components/Heading'; 18 | 19 | class HeroCarousel extends Component { 20 | render() { 21 | const { 22 | restaurants, 23 | } = this.props; 24 | return ( 25 |
26 |
27 | 28 | Restaurant Reviewer 29 | 30 | 31 | Accessibility App 32 | 33 |
34 | 35 | {restaurants.map((item, index) => 36 |
37 | {item.caption} 42 |
43 | )} 44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | HeroCarousel.propTypes = { 51 | restaurants: PropTypes.array.isRequired, 52 | }; 53 | 54 | export default cssModules(HeroCarousel, styles); 55 | -------------------------------------------------------------------------------- /app/src/components/ErrorAlert/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:54:53-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:32-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import Notification from 'grommet/components/notification'; 16 | import CloseIcon from 'grommet/components/icons/base/Close'; 17 | import Button from 'grommet/components/button'; 18 | import Section from 'grommet/components/Section'; 19 | 20 | const ErrorAlert = ({ 21 | errors, 22 | onClose, 23 | }) => ( 24 |
25 | {errors.length > 0 && errors.map((error, i) => 26 |
27 |
28 | 36 |
37 | 46 |
47 | )} 48 |
49 | ); 50 | 51 | ErrorAlert.propTypes = { 52 | errors: PropTypes.array, 53 | onClose: PropTypes.func.isRequired, 54 | }; 55 | 56 | export default cssModules(ErrorAlert, styles); 57 | -------------------------------------------------------------------------------- /app/src/containers/SingleRestaurantContainer/README.md: -------------------------------------------------------------------------------- 1 | ## SingleRestaurantContainer 2 | A container for single restaurants, loading info and reviews for it. 3 | 4 | ### Example Usage 5 | 6 | ```js 7 | 8 | // Props are connected through redux, not passed into the container. 9 | ``` 10 | 11 | ### Props 12 | 13 | | Prop | Type | Default | Possible Values 14 | | ------------- | -------- | ----------- | --------------------------------------------- 15 | | **selectedReviewId** | Number | | The currently selected review, if any. 16 | | **errors** | Array | | Errors for the single restaurant container 17 | | **isLoading** | Boolean | | Whether the container is loading its data 18 | | **restaurants** | Array | | An array of restaurants 19 | | **params** | Object | | React router params object 20 | | **actions** | Object | | Redux actions object 21 | | **addReviewData** | Object | | Data for adding a review, if applicable 22 | 23 | 24 | ### Other Information 25 | Higher order component that is loaded by the SingleRestaurantPage, connected to redux store. 26 | 27 | #### Models 28 | 29 | Restaurants: 30 | ```js 31 | const myRestaurants = [ 32 | { 33 | id: 1, 34 | name: "The New Magnum Café", 35 | address: "Jalan M Husni Thamrin 1", 36 | city: "Jakarta Pusat", 37 | state:"Daerah Khusus Ibukota Jakarta", 38 | zip:"23435", 39 | country: "id", 40 | phone: "(021) 23580055", 41 | website: "http://mymagnum.co.id/", 42 | }, 43 | { 44 | ... 45 | } 46 | ] 47 | ``` 48 | 49 | Errors: 50 | ```js 51 | const errors = [ 52 | { 53 | id: 0, 54 | message: 'My message', 55 | }, 56 | { 57 | ... 58 | } 59 | ] 60 | ``` 61 | -------------------------------------------------------------------------------- /app/src/components/RestaurantPanel/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:57:28-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:39-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import Image from 'grommet/components/image'; 16 | import Section from 'grommet/components/section'; 17 | import { RestaurantInfo, RestaurantHours } from 'components'; 18 | import Heading from 'grommet/components/heading'; 19 | import Article from 'grommet/components/Article'; 20 | 21 | const RestaurantPanel = ({ 22 | restaurant, 23 | hoursAreExpanded, 24 | onExpandHours, 25 | }) => ( 26 |
27 |
28 | {`${restaurant.name} 34 |
35 | 36 | {`About ${restaurant.name}`} 37 | 38 | 39 | 44 |
45 |
46 |
47 | ); 48 | 49 | RestaurantPanel.propTypes = { 50 | restaurant: PropTypes.object.isRequired, 51 | hoursAreExpanded: PropTypes.bool.isRequired, 52 | onExpandHours: PropTypes.func.isRequired, 53 | }; 54 | 55 | export default cssModules(RestaurantPanel, styles); 56 | -------------------------------------------------------------------------------- /app/src/components/RestaurantReview/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:57:36-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:34-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import ReactStars from 'react-stars'; 16 | import { StarRating } from 'components'; 17 | import Heading from 'grommet/components/heading'; 18 | import Paragraph from 'grommet/components/paragraph'; 19 | import Box from 'grommet/components/box'; 20 | import Article from 'grommet/components/article'; 21 | import fixDate from 'utils/fixDate'; 22 | import fixLongText from 'utils/fixLongText'; 23 | 24 | const RestaurantReview = ({ 25 | review, 26 | onReviewClick, 27 | }) => ( 28 | onReviewClick(review.id)} // eslint-disable-line react/jsx-no-bind 35 | > 36 | 37 | {review.person} 38 | 39 |
40 | 45 | 46 | {fixLongText(review.text)} 47 | 48 |
49 |

{fixDate(review.date)}

50 |
51 | ); 52 | 53 | RestaurantReview.propTypes = { 54 | review: PropTypes.object.isRequired, 55 | onReviewClick: PropTypes.func.isRequired, 56 | }; 57 | 58 | export default cssModules(RestaurantReview, styles); 59 | -------------------------------------------------------------------------------- /app/src/containers/LandingContainer/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOAD_IMAGES_SUCCESS, 3 | LOAD_IMAGES_INITIATION, 4 | } from './constants'; 5 | 6 | const initialState = { 7 | isLoading: false, 8 | errors: [], 9 | restaurants: [], 10 | }; 11 | 12 | const seedData = [ 13 | { 14 | src: 'https://github.com/RyanCCollins/cdn/blob/master/restaurant-reviewer/restaurantone.jpg?raw=true', 15 | caption: 'Awesome Restaurant One', 16 | name: 'Name One', 17 | rating: 4, 18 | type: 'Japanese', 19 | }, 20 | { 21 | src: 'https://github.com/RyanCCollins/cdn/blob/master/restaurant-reviewer/restauranttwo.jpeg?raw=true', 22 | caption: 'Awesome Restaurant Two', 23 | name: 'Name Two', 24 | rating: 3, 25 | type: 'Japanese', 26 | }, 27 | { 28 | src: 'https://github.com/RyanCCollins/cdn/blob/master/restaurant-reviewer/restaurantthree.jpeg?raw=true', 29 | caption: 'Awesome Restaurant Three', 30 | name: 'Name Three', 31 | rating: 4, 32 | type: 'Sushi', 33 | }, 34 | { 35 | src: 'https://github.com/RyanCCollins/cdn/blob/master/restaurant-reviewer/restaurantfour.jpg?raw=true', 36 | caption: 'Awesome Restaurant Four', 37 | name: 'Name Four', 38 | rating: 3, 39 | type: 'Japanese', 40 | }, 41 | { 42 | src: 'https://github.com/RyanCCollins/cdn/blob/master/restaurant-reviewer/restaurantfive.jpg?raw=true', 43 | caption: 'Awesome Restaurant Five', 44 | name: 'Name Five', 45 | rating: 4, 46 | type: 'Japanese', 47 | }, 48 | ]; 49 | 50 | const featured = (state = initialState, action) => { 51 | switch (action.type) { 52 | case LOAD_IMAGES_INITIATION: 53 | return Object.assign({}, state, { 54 | isLoading: true, 55 | }); 56 | case LOAD_IMAGES_SUCCESS: 57 | return Object.assign({}, state, { 58 | isLoading: false, 59 | restaurants: seedData, // So bad, I know, but this will fake the image data 60 | }); 61 | default: 62 | return state; 63 | } 64 | }; 65 | 66 | export default featured; 67 | -------------------------------------------------------------------------------- /app/src/components/RestaurantGridItem/index.module.scss: -------------------------------------------------------------------------------- 1 | .imageWrapper { 2 | display: flex; 3 | align-items: center; 4 | position: relative; 5 | flex-wrap: wrap; 6 | overflow: hidden; 7 | flex-direction: column; 8 | padding: 50px; 9 | text-decoration: none; 10 | background: #fff; 11 | a { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | width: 100%; 16 | height: 228px; 17 | } 18 | a:focus { 19 | border-color: #c3a4fe; 20 | box-shadow: 0 0 1px 1px #c3a4fe; 21 | outline-offset: -2px 22 | } 23 | } 24 | 25 | .contents { 26 | speak: none; 27 | opacity: 1 !important; 28 | text-align: center; 29 | color: black; 30 | padding: 20px; 31 | min-height: 250px; 32 | letter-spacing: 20px; 33 | font-size: 1.6rem; 34 | overflow: hidden; 35 | min-height: 130px; 36 | h3 { 37 | font-family: 'Raleway'; 38 | font-size: 1.3rem; 39 | } 40 | @media screen and (max-width: 870px) { 41 | letter-spacing: 10px; 42 | font-size: 1.4rem; 43 | padding: 10px; 44 | } 45 | @media screen and (max-width: 340px) { 46 | padding: 0; 47 | font-size: 1.2rem; 48 | } 49 | } 50 | 51 | .panel { 52 | display: inline-block; 53 | background: #ffffff; 54 | min-height: 400px; 55 | max-height: 500px; 56 | width: calc(33.33% - 40px); 57 | margin: 20px; 58 | box-shadow:0px 0px 5px 5px #C9C9C9; 59 | -webkit-box-shadow:2px 2px 5px 5x #C9C9C9; 60 | -moz-box-shadow:2px 2px 5px 5px #C9C9C9; 61 | @media screen and (max-width: 1200px) { 62 | width: calc(50% - 40px); 63 | } 64 | @media screen and (max-width: 768px) { 65 | width: calc(100% - 40px); 66 | } 67 | } 68 | 69 | .link { 70 | cursor: pointer; 71 | } 72 | 73 | .cardImage { 74 | position: absolute; 75 | height: auto; 76 | top: 0; 77 | left: 0; 78 | width: 100%; 79 | z-index: 0; 80 | opacity: 0.1; 81 | transition: all .6s ease-in-out; 82 | &:hover { 83 | opacity: 1.0; 84 | transform: scale(1.3); 85 | } 86 | } 87 | 88 | .footer { 89 | display: flex; 90 | justify-content: center; 91 | } 92 | -------------------------------------------------------------------------------- /app/src/containers/LandingContainer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import * as LandingActionCreators from './actions'; 5 | import cssModules from 'react-css-modules'; 6 | import styles from './index.module.scss'; 7 | import Section from 'grommet/components/Section'; 8 | import { 9 | HeroCarousel, 10 | LoadingIndicator, 11 | } from 'components'; 12 | 13 | 14 | class Landing extends Component { // eslint-disable-line react/prefer-stateless-function 15 | constructor() { 16 | super(); 17 | this.handleLoading = this.handleLoading.bind(this); 18 | } 19 | componentDidMount() { 20 | this.handleLoading(); 21 | } 22 | handleLoading() { 23 | const { 24 | loadImagesAsync, 25 | } = this.props.actions; 26 | loadImagesAsync(); 27 | } 28 | render() { 29 | const { 30 | restaurants, 31 | isLoading, 32 | } = this.props; 33 | return ( 34 |
35 | {!isLoading ? 36 | 37 | : 38 | 39 | } 40 |
41 | ); 42 | } 43 | } 44 | 45 | Landing.propTypes = { 46 | restaurants: PropTypes.array.isRequired, 47 | isLoading: PropTypes.bool.isRequired, 48 | errors: PropTypes.array.isRequired, 49 | actions: PropTypes.object.isRequired, 50 | }; 51 | 52 | // mapStateToProps :: {State} -> {Props} 53 | const mapStateToProps = (state) => ({ 54 | restaurants: state.featured.restaurants, 55 | isLoading: state.featured.isLoading, 56 | errors: state.featured.errors, 57 | }); 58 | 59 | // mapDispatchToProps :: Dispatch -> {Action} 60 | const mapDispatchToProps = (dispatch) => ({ 61 | actions: bindActionCreators( 62 | LandingActionCreators, 63 | dispatch 64 | ), 65 | }); 66 | 67 | const Container = cssModules(Landing, styles); 68 | 69 | export default connect( 70 | mapStateToProps, 71 | mapDispatchToProps 72 | )(Container); 73 | -------------------------------------------------------------------------------- /app/src/components/FullReviewModal/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:55:39-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:16-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | 13 | import React, { PropTypes } from 'react'; 14 | import styles from './index.module.scss'; 15 | import cssModules from 'react-css-modules'; 16 | import Header from 'grommet/components/header'; 17 | import Paragraph from 'grommet/components/paragraph'; 18 | import Layer from 'grommet/components/layer'; 19 | import Section from 'grommet/components/section'; 20 | import { ReviewSrOnly, StarRating } from 'components'; 21 | 22 | const FullReviewModal = ({ 23 | isOpen, 24 | onToggleClose, 25 | review, 26 | }) => ( 27 |
28 | {isOpen ? 29 | 56 | : 57 |
60 | ); 61 | 62 | FullReviewModal.propTypes = { 63 | isOpen: PropTypes.bool.isRequired, 64 | onToggleClose: PropTypes.func.isRequired, 65 | review: PropTypes.object, 66 | }; 67 | 68 | export default cssModules(FullReviewModal, styles); 69 | -------------------------------------------------------------------------------- /app/src/components/FilterMenu/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:55:00-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:28-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes, Component } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import Menu from 'grommet/components/Menu'; 16 | import Filter from 'grommet/components/icons/base/Filter'; 17 | import Anchor from 'grommet/components/Anchor'; 18 | import uuid from 'node-uuid'; 19 | 20 | const itemsAreEqual = (item, value) => 21 | item === value || item === value.split(' ')[0]; 22 | 23 | const randomId = () => uuid.v1(); 24 | 25 | const parseLabel = (label) => 26 | label.toLowerCase().split(' ').join('-'); 27 | 28 | class FilterMenu extends Component { 29 | render() { 30 | const { 31 | menuItems, 32 | onSelectItem, 33 | label, 34 | selectedItem, 35 | } = this.props; 36 | return ( 37 | 43 | } 44 | closeOnClick={false} 45 | label={label} 46 | className={styles.filterMenu} 47 | a11yTitle={label} 48 | a11yTitleId={`${parseLabel(label)}-${randomId()}`} 49 | pad="medium" 50 | dropAlign={{ left: 'left', top: 'bottom' }} 51 | > 52 | {menuItems.map((item, i) => 53 | // eslint-disable-line react/jsx-no-bind 60 | onSelectItem({ value: item.value }) 61 | } 62 | > 63 | {item.value} 64 | 65 | )} 66 | 67 | ); 68 | } 69 | } 70 | 71 | FilterMenu.propTypes = { 72 | menuItems: PropTypes.array.isRequired, 73 | onSelectItem: PropTypes.func.isRequired, 74 | label: PropTypes.string.isRequired, 75 | selectedItem: PropTypes.string, 76 | }; 77 | 78 | export default cssModules(FilterMenu, styles); 79 | -------------------------------------------------------------------------------- /config/generators/page/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Page Generator 3 | */ 4 | const componentNameCheck = require('../utils/componentNameCheck'); 5 | const trimTemplateFile = require('../utils/trimTemplateFile'); 6 | 7 | module.exports = { 8 | description: 'Add a page', 9 | prompts: [ 10 | { 11 | type: 'input', 12 | name: 'name', 13 | message: 'What is the name of the page component?', 14 | default: 'About', 15 | validate: (value) => { 16 | if ((/.+/).test(value)) { 17 | return componentNameCheck(value) ? 18 | 'That component already exists. Please choose another name for your page component.' : true; 19 | } 20 | return 'The name is required.'; 21 | }, 22 | }, { 23 | type: 'input', 24 | name: 'path', 25 | message: 'Enter the path of the page component.', 26 | default: '/about', 27 | validate: value => { 28 | if ((/.+/).test(value)) { 29 | return true; 30 | } 31 | 32 | return 'path is required'; 33 | }, 34 | }, 35 | ], 36 | 37 | 38 | actions: data => { 39 | // Generate index.js and index.module.scss 40 | const actions = [{ 41 | type: 'add', 42 | path: '../../app/src/pages/{{properCase name}}Page/index.js', 43 | templateFile: './page/index.js.hbs', 44 | abortOnFail: true, 45 | }, { 46 | type: 'add', 47 | path: '../../app/src/pages/{{properCase name}}Page/index.module.scss', 48 | templateFile: './page/index.module.scss.hbs', 49 | abortOnFail: true, 50 | }]; 51 | 52 | // Add the route to the routes.js file above the error route 53 | // automatic export in root index.js 54 | // TODO smarter route adding 55 | actions.push({ 56 | type: 'modify', 57 | path: '../../app/src/routes.js', 58 | pattern: /()/g, 59 | template: trimTemplateFile('config/generators/page/route.js.hbs'), 60 | }, { 61 | type: 'modify', 62 | path: '../../app/src/pages/index.js', 63 | pattern: /(\/\* Assemble all pages for export \*\/)/g, 64 | template: trimTemplateFile('config/generators/page/export.js.hbs'), 65 | }); 66 | 67 | // README.md 68 | actions.push({ 69 | type: 'add', 70 | path: '../../app/src/pages/{{properCase name}}Page/README.md', 71 | templateFile: './page/README.md.hbs', 72 | abortOnFail: true, 73 | }); 74 | 75 | return actions; 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /app/src/components/RestaurantInfo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:57:18-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:45-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | // No ES6 desructuring because of grommet's performance issues 16 | import Paragraph from 'grommet/components/Paragraph'; 17 | import LocationPin from 'grommet/components/icons/base/LocationPin'; 18 | import LinkIcon from 'grommet/components/icons/base/Link'; 19 | import ContactUs from 'grommet/components/icons/base/ContactUs'; 20 | import Box from 'grommet/components/box'; 21 | import Anchor from 'grommet/components/Anchor'; 22 | import Java from 'grommet/components/icons/base/Java'; 23 | 24 | const RestaurantInfo = ({ 25 | restaurant, 26 | i, 27 | }) => ( 28 |
29 | 30 | 31 | 35 | 36 | {restaurant.type.name} 37 | 38 | 39 | 40 | 44 | {' '} 45 | {restaurant.phone} 46 | 47 | 48 | 52 | 53 | {restaurant.city}, {restaurant.state} {restaurant.country} 54 | 55 | 56 | 57 | 58 | 59 | {restaurant.website.length > 30 ? 'Visit Website' : restaurant.website} 60 | 61 | 62 | 63 |
64 | ); 65 | 66 | RestaurantInfo.propTypes = { 67 | restaurant: PropTypes.object.isRequired, 68 | i: PropTypes.number.isRequired, 69 | }; 70 | 71 | export default cssModules(RestaurantInfo, styles); 72 | -------------------------------------------------------------------------------- /app/src/components/RestaurantHours/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:57:06-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:49-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import List from 'grommet/components/list'; 16 | import Contract from 'grommet/components/icons/base/contract'; 17 | import Expand from 'grommet/components/icons/base/expand'; 18 | import Button from 'grommet/components/Button'; 19 | import Heading from 'grommet/components/Heading'; 20 | import Section from 'grommet/components/section'; 21 | import Box from 'grommet/components/Box'; 22 | import Animate from 'grommet/components/Animate'; 23 | import { RestaurantHoursListItem } from 'components'; 24 | 25 | const daysOfWeek = [ 26 | 'monday', 27 | 'tuesday', 28 | 'wednesday', 29 | 'thursday', 30 | 'friday', 31 | 'saturday', 32 | 'sunday', 33 | ]; 34 | 35 | const RestaurantHours = ({ 36 | restaurant, 37 | onExpandHours, 38 | isExpanded, 39 | }) => ( 40 |
41 | 42 | Hours of Operation 43 | 44 |
73 | ); 74 | 75 | RestaurantHours.propTypes = { 76 | restaurant: PropTypes.object.isRequired, 77 | isExpanded: PropTypes.bool.isRequired, 78 | onExpandHours: PropTypes.func.isRequired, 79 | }; 80 | 81 | export default cssModules(RestaurantHours, styles); 82 | -------------------------------------------------------------------------------- /app/src/containers/SingleRestaurantContainer/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | REVIEWS_LOAD_INITIATION, 3 | REVIEWS_LOAD_SUCCESS, 4 | REVIEWS_LOAD_FAILURE, 5 | ADD_REVIEW_INITIATION, 6 | ADD_REVIEW_SUCCESS, 7 | ADD_REVIEW_FAILURE, 8 | REVIEWS_ERRORS, 9 | OPEN_FULL_REVIEW, 10 | CLOSE_FULL_REVIEW, 11 | LOAD_INITIAL_REVIEWS, 12 | TOGGLE_RESTAURANT_HOURS, 13 | } from './constants'; 14 | 15 | const initialState = { 16 | singleRestaurant: { 17 | reviews: [], 18 | errors: [], 19 | isLoading: false, 20 | selectedRestaurant: null, 21 | selectedReviewId: null, 22 | hoursAreExpanded: false, 23 | }, 24 | }; 25 | 26 | const singleRestaurant = 27 | (state = initialState, action) => { 28 | switch (action.type) { 29 | case REVIEWS_LOAD_INITIATION: 30 | return Object.assign({}, state, { 31 | isLoading: true, 32 | selectedRestaurant: action.selectedRestaurant, 33 | }); 34 | case REVIEWS_LOAD_SUCCESS: 35 | return Object.assign({}, state, { 36 | isLoading: false, 37 | reviews: action.reviews, 38 | }); 39 | case REVIEWS_LOAD_FAILURE: 40 | return Object.assign({}, state, { 41 | isLoading: false, 42 | errors: [...action.error], 43 | }); 44 | case ADD_REVIEW_INITIATION: 45 | return Object.assign({}, state, { 46 | isLoading: true, 47 | }); 48 | case ADD_REVIEW_SUCCESS: 49 | return Object.assign({}, state, { 50 | isLoading: false, 51 | reviews: [ 52 | action.review, 53 | ...state.selectedRestaurant.reviews, 54 | ], 55 | }); 56 | case ADD_REVIEW_FAILURE: 57 | return Object.assign({}, state, { 58 | isLoading: false, 59 | errors: [...action.error], 60 | }); 61 | case REVIEWS_ERRORS: 62 | return Object.assign({}, state, { 63 | errors: action.errors, 64 | }); 65 | case OPEN_FULL_REVIEW: 66 | return Object.assign({}, state, { 67 | selectedReviewId: action.review, 68 | }); 69 | case CLOSE_FULL_REVIEW: 70 | return Object.assign({}, state, { 71 | selectedReviewId: null, 72 | }); 73 | case LOAD_INITIAL_REVIEWS: 74 | return Object.assign({}, state, { 75 | reviews: action.reviews, 76 | }); 77 | case TOGGLE_RESTAURANT_HOURS: 78 | return Object.assign({}, state, { 79 | hoursAreExpanded: !state.hoursAreExpanded 80 | }); 81 | default: 82 | return state; 83 | } 84 | }; 85 | 86 | export default singleRestaurant; 87 | -------------------------------------------------------------------------------- /app/src/components/RestaurantGridItem/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:56:57-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T19:59:54-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import Heading from 'grommet/components/Heading'; 16 | import Article from 'grommet/components/article'; 17 | import { Link } from 'react-router'; 18 | import { 19 | RestaurantInfo, 20 | SrOnlyContent, 21 | StarRating, 22 | } from 'components'; 23 | import Button from 'grommet/components/button'; 24 | import Footer from 'grommet/components/footer'; 25 | import Information from 'grommet/components/icons/base/information'; 26 | 27 | const RestaurantGridItem = ({ 28 | restaurant, 29 | onViewDetails, 30 | i, 31 | }) => ( 32 |
33 |
34 |
35 | 40 | {restaurant.name} 41 | 42 | 47 | 48 | {`${restaurant.name} 53 | 54 |
55 |
56 | 57 |
58 |
73 |
74 | ); 75 | 76 | RestaurantGridItem.propTypes = { 77 | restaurant: PropTypes.object.isRequired, 78 | onViewDetails: PropTypes.func.isRequired, 79 | i: PropTypes.number.isRequired, 80 | }; 81 | 82 | export default cssModules(RestaurantGridItem, styles); 83 | -------------------------------------------------------------------------------- /config/generators/component/index.js: -------------------------------------------------------------------------------- 1 | const componentNameCheck = require('../utils/componentNameCheck'); 2 | const trimTemplateFile = require('../utils/trimTemplateFile'); 3 | 4 | module.exports = { 5 | description: 'Add a component to the app', 6 | prompts: [ 7 | { 8 | type: 'list', 9 | name: 'type', 10 | message: 'Select the type of component', 11 | default: 'Stateless Function', 12 | choices: () => ['ES6 Class', 'Stateless Function'], 13 | }, 14 | { 15 | type: 'input', 16 | name: 'name', 17 | message: 'What is the name of the component?', 18 | default: 'Button', 19 | validate: (value) => { 20 | if ((/.+/).test(value)) { 21 | return componentNameCheck(value) ? 'That component already exists.' : true; 22 | } 23 | return 'The name is required.'; 24 | }, 25 | }, 26 | { 27 | type: 'confirm', 28 | name: 'wantSCSSModules', 29 | default: true, 30 | message: 'Should the component use SCSS Modules?', 31 | }, 32 | { 33 | type: 'confirm', 34 | name: 'wantPropTypes', 35 | default: true, 36 | message: 'Should the component have PropTypes?', 37 | }, 38 | ], 39 | actions: (data) => { 40 | const actions = [{ 41 | type: 'add', 42 | path: '../../app/src/components/{{properCase name}}/index.js', 43 | templateFile: data.type === 'ES6 Class' ? 44 | './component/es6class.js.hbs' : './component/stateless.js.hbs', 45 | abortOnFail: true, 46 | }, { 47 | type: 'add', 48 | path: '../../app/src/components/{{properCase name}}/tests/index.test.js', 49 | templateFile: './component/test.js.hbs', 50 | abortOnFail: true, 51 | }]; 52 | 53 | // If they want a CSS file, add styles.css 54 | if (data.wantSCSSModules) { 55 | actions.push({ 56 | type: 'add', 57 | path: '../../app/src/components/{{properCase name}}/index.module.scss', 58 | templateFile: './component/styles.scss.hbs', 59 | abortOnFail: true, 60 | }); 61 | } 62 | 63 | // README.md 64 | actions.push({ 65 | type: 'add', 66 | path: '../../app/src/components/{{properCase name}}/README.md', 67 | templateFile: './component/README.md.hbs', 68 | abortOnFail: true, 69 | }); 70 | 71 | // Add container export to index.js in container root folder 72 | actions.push({ 73 | type: 'modify', 74 | path: '../../app/src/components/index.js', 75 | pattern: /(\/\* Assemble all components for export \*\/)/g, 76 | template: trimTemplateFile('config/generators/component/export.js.hbs'), 77 | }); 78 | 79 | return actions; 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /app/src/containers/RestaurantsGridContainer/README.md: -------------------------------------------------------------------------------- 1 | ## RestaurantsGridContainer 2 | A connected container that wraps around the restaurant grid on the landing page. 3 | 4 | ### Example Usage 5 | 6 | ```js 7 | 8 | // Props are connected through redux, not passed in. 9 | ``` 10 | 11 | restaurants: state.restaurants.items, 12 | selectedFilterIndex: state.restaurants.selectedFilterIndex, 13 | isLoading: state.restaurants.isLoading, 14 | errors: state.restaurants.errors, 15 | categories: state.restaurants.categories, 16 | locations: state.restaurants.locations, 17 | ratings: state.restaurants.ratings, 18 | 19 | ### Props 20 | 21 | | Prop | Type | Default | Possible Values 22 | | ------------- | -------- | ----------- | --------------------------------------------- 23 | | **restaurants** | Array | | An Array of restaurants to map to the grid items 24 | | **selectedFilterIndex** | String | | Indicates that a filter is selected for an item for cuisine type. 25 | | **isLoading** | Boolean | | Whether the container is loading its data 26 | | **errors** | Array | | An array of errors, if applicable 27 | | **categories** | Array | | Restaurant cuisine categories 28 | | **locations** | Array | | Unique locations of restaurants, used to filter by location 29 | | **ratings** | Array | | Unique ratings of restaurants, used to filter by rating 30 | 31 | 32 | ### Other Information 33 | Connected container component that connects to the redux store for the restaurant grid. 34 | 35 | #### Models 36 | 37 | Restaurants: 38 | ```js 39 | const myRestaurants = [ 40 | { 41 | id: 1, 42 | name: "The New Magnum Café", 43 | address: "Jalan M Husni Thamrin 1", 44 | city: "Jakarta Pusat", 45 | state:"Daerah Khusus Ibukota Jakarta", 46 | zip:"23435", 47 | country: "id", 48 | phone: "(021) 23580055", 49 | website: "http://mymagnum.co.id/", 50 | }, 51 | { 52 | ... 53 | } 54 | ] 55 | ``` 56 | 57 | Errors: 58 | ```js 59 | const errors = [ 60 | { 61 | id: 0, 62 | message: 'My message', 63 | }, 64 | { 65 | ... 66 | } 67 | ] 68 | ``` 69 | 70 | Categories: 71 | ```js 72 | const categories = [ 73 | { 74 | 0:"All" 75 | }, 76 | { 77 | 1:"Cafe" 78 | } 79 | ]; 80 | ``` 81 | 82 | Locations: 83 | ```js 84 | const locations = [ 85 | { 86 | id: 0, 87 | value: 'Brooklyn, NY', 88 | }, 89 | { 90 | id: 1, 91 | value: 'Jakarta', 92 | } 93 | ]; 94 | ``` 95 | 96 | Ratings: 97 | ```js 98 | const ratings = [ 99 | { 100 | id: 0, 101 | value: 'All', 102 | }, 103 | { 104 | id: 1, 105 | value: '1 Star', 106 | } 107 | ]; 108 | ``` 109 | -------------------------------------------------------------------------------- /app/src/components/AppFooter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.module.scss'; 3 | import cssModules from 'react-css-modules'; 4 | import Footer from 'grommet/components/Footer'; 5 | import Box from 'grommet/components/Box'; 6 | import Heading from 'grommet/components/Heading'; 7 | import SocialShare from 'grommet/components/SocialShare'; 8 | 9 | const AppFooter = () => ( 10 | 83 | ); 84 | 85 | export default cssModules(AppFooter, styles); 86 | -------------------------------------------------------------------------------- /app/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { syncHistoryWithStore } from 'react-router-redux'; 3 | import thunk from 'redux-thunk'; 4 | import { browserHistory } from 'react-router'; 5 | import createLogger from 'redux-logger'; 6 | import promiseMiddleware from 'redux-promise-middleware'; 7 | import rootReducer from './reducers'; 8 | 9 | export const initialState = { 10 | featured: { 11 | isLoading: false, 12 | errors: [], 13 | restaurants: [], 14 | }, 15 | restaurants: { 16 | isLoading: false, 17 | items: [], 18 | filteredItems: [], 19 | restaurants: [], 20 | categoryFilter: 'All', 21 | ratingFilter: 'All', 22 | locationFilter: 'All', 23 | appliedFilter: { 24 | isApplied: false, 25 | }, 26 | errors: [], 27 | categories: [], 28 | locations: [], 29 | ratings: [ 30 | { 31 | id: 0, 32 | value: 'All', 33 | }, 34 | { 35 | id: 1, 36 | value: '1 Star', 37 | }, 38 | { 39 | id: 2, 40 | value: '2 Star', 41 | }, 42 | { 43 | id: 3, 44 | value: '3 Star', 45 | }, 46 | { 47 | id: 4, 48 | value: '4 Star', 49 | }, 50 | { 51 | id: 5, 52 | value: '5 Star', 53 | }, 54 | ], 55 | }, 56 | singleRestaurant: { 57 | reviews: [], 58 | errors: [], 59 | isLoading: false, 60 | selectedRestaurant: null, 61 | selectedReviewId: null, 62 | hoursAreExpanded: false, 63 | }, 64 | addReview: { 65 | isAddingReview: false, 66 | error: undefined, 67 | }, 68 | }; 69 | 70 | /* Commonly used middlewares and enhancers */ 71 | /* See: http://redux.js.org/docs/advanced/Middleware.html*/ 72 | const loggerMiddleware = createLogger(); 73 | const middlewares = [thunk, promiseMiddleware(), loggerMiddleware]; 74 | 75 | /* Everyone should use redux dev tools */ 76 | /* https://github.com/gaearon/redux-devtools */ 77 | /* https://medium.com/@meagle/understanding-87566abcfb7a */ 78 | const enhancers = []; 79 | const devToolsExtension = window.devToolsExtension; 80 | if (typeof devToolsExtension === 'function') { 81 | enhancers.push(devToolsExtension()); 82 | } 83 | 84 | const composedEnhancers = compose( 85 | applyMiddleware(...middlewares), 86 | ...enhancers 87 | ); 88 | 89 | /* Hopefully by now you understand what a store is and how redux uses them, 90 | * But if not, take a look at: https://github.com/reactjs/redux/blob/master/docs/api/createStore.md 91 | * And https://egghead.io/lessons/javascript-redux-implementing-store-from-scratch 92 | */ 93 | const store = createStore( 94 | rootReducer, 95 | initialState, 96 | composedEnhancers, 97 | ); 98 | 99 | /* See: https://github.com/reactjs/react-router-redux/issues/305 */ 100 | export const history = syncHistoryWithStore(browserHistory, store); 101 | 102 | /* Hot reloading of reducers. How futuristic!! */ 103 | if (module.hot) { 104 | module.hot.accept('./reducers', () => { 105 | /*eslint-disable */ // Allow require 106 | const nextRootReducer = require('./reducers').default; 107 | /*eslint-enable */ 108 | store.replaceReducer(nextRootReducer); 109 | }); 110 | } 111 | 112 | export default store; 113 | -------------------------------------------------------------------------------- /config/webpack/webpack.test.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TEST WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const modules = [ 8 | 'app/src', 9 | 'node_modules', 10 | ]; 11 | 12 | const ROOT_PATH = path.resolve(__dirname); 13 | 14 | module.exports = { 15 | devtool: 'inline-source-map', 16 | isparta: { 17 | babel: { 18 | presets: ['es2015', 'react', 'stage-0'], 19 | }, 20 | }, 21 | module: { 22 | // Some libraries don't like being run through babel. 23 | // If they gripe, put them here. 24 | noParse: [ 25 | /node_modules(\\|\/)sinon/, 26 | /node_modules(\\|\/)acorn/, 27 | ], 28 | preLoaders: [ 29 | { test: /\.js$/, 30 | loader: 'isparta', 31 | include: path.resolve('app/src'), 32 | }, 33 | ], 34 | loaders: [ 35 | { test: /\.json$/, loader: 'json-loader' }, 36 | { test: /\.css$/, loader: 'null-loader' }, 37 | { 38 | test: /\.module\.scss$/, 39 | loader: 'null-loader', 40 | }, 41 | { 42 | test: /\.scss$/, 43 | exclude: /\.module\.scss$/, 44 | loader: 'null-loader', 45 | }, 46 | 47 | // sinon.js--aliased for enzyme--expects/requires global vars. 48 | // imports-loader allows for global vars to be injected into the module. 49 | // See https://github.com/webpack/webpack/issues/304 50 | { test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 51 | loader: 'imports?define=>false,require=>false', 52 | }, 53 | { test: /\.js$/, 54 | loader: 'babel', 55 | exclude: [/node_modules/], 56 | }, 57 | { test: /\.jpe?g$|\.gif$|\.png$|\.svg$/i, 58 | loader: 'null-loader', 59 | }, 60 | ], 61 | }, 62 | 63 | plugins: [ 64 | 65 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 66 | // inside your code for any environment checks; UglifyJS will automatically 67 | // drop any unreachable code. 68 | new webpack.DefinePlugin({ 69 | 'process.env': { 70 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 71 | }, 72 | })], 73 | 74 | // Some node_modules pull in Node-specific dependencies. 75 | // Since we're running in a browser we have to stub them out. See: 76 | // https://webpack.github.io/docs/configuration.html#node 77 | // https://github.com/webpack/node-libs-browser/tree/master/mock 78 | // https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520 79 | node: { 80 | fs: 'empty', 81 | child_process: 'empty', 82 | net: 'empty', 83 | tls: 'empty', 84 | }, 85 | 86 | // required for enzyme to work properly 87 | externals: { 88 | jsdom: 'window', 89 | 'react/addons': true, 90 | 'react/lib/ExecutionEnvironment': true, 91 | 'react/lib/ReactContext': 'window', 92 | }, 93 | resolve: { 94 | modulesDirectories: modules, 95 | modules, 96 | alias: { 97 | // required for enzyme to work properly 98 | sinon: 'sinon/pkg/sinon', 99 | components: path.resolve(ROOT_PATH, '../../app/src/components'), 100 | containers: path.resolve(ROOT_PATH, '../../app/src/containers'), 101 | pages: path.resolve(ROOT_PATH, '../../app/src/pages'), 102 | }, 103 | }, 104 | }; 105 | -------------------------------------------------------------------------------- /app/src/components/FilterRestaurants/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:55:07-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:23-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import React, { PropTypes } from 'react'; 13 | import styles from './index.module.scss'; 14 | import cssModules from 'react-css-modules'; 15 | import { FilterMenu } from 'components'; 16 | import Section from 'grommet/components/Section'; 17 | import Menu from 'grommet/components/Menu'; 18 | import Button from 'grommet/components/button'; 19 | import Close from 'grommet/components/icons/base/Close'; 20 | import Footer from 'grommet/components/Footer'; 21 | import shouldBeEnabled from 'utils/filter'; 22 | 23 | const FilterRestaurants = ({ 24 | locations, 25 | ratings, 26 | filter, 27 | onFilterLocations, 28 | onFilterRatings, 29 | isFiltering, 30 | onClearFilter, 31 | categories, 32 | onFilterCategories, 33 | onApplyFilters, 34 | }) => ( 35 |
38 |
39 | 45 | ({ id, value: item }))} 47 | onSelectItem={onFilterCategories} 48 | selectedItem={filter.categoryFilter} 49 | label="Filter by Category" 50 | /> 51 | 57 | 63 | 64 |
65 | {isFiltering ? 66 |
84 |
85 |
86 | ); 87 | 88 | FilterRestaurants.propTypes = { 89 | locations: PropTypes.array.isRequired, 90 | ratings: PropTypes.array.isRequired, 91 | filter: PropTypes.object.isRequired, 92 | onFilterLocations: PropTypes.func.isRequired, 93 | onFilterRatings: PropTypes.func.isRequired, 94 | isFiltering: PropTypes.bool.isRequired, 95 | onClearFilter: PropTypes.func.isRequired, 96 | categories: PropTypes.array.isRequired, 97 | onFilterCategories: PropTypes.func.isRequired, 98 | onApplyFilters: PropTypes.func.isRequired, 99 | }; 100 | 101 | export default cssModules(FilterRestaurants, styles); 102 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import path from 'path'; 3 | import HtmlwebpackPlugin from 'html-webpack-plugin'; 4 | import NpmInstallPlugin from 'npm-install-webpack-plugin'; 5 | const ROOT_PATH = path.resolve(__dirname); 6 | 7 | const env = process.env.NODE_ENV || 'development'; 8 | const PORT = process.env.PORT || 1337; 9 | const HOST = '0.0.0.0'; // Set to localhost if need be. 10 | const URL = `http://${HOST}:${PORT}` 11 | 12 | 13 | module.exports = { 14 | devtool: process.env.NODE_ENV === 'production' ? '' : 'source-map', 15 | entry: [ 16 | path.resolve(ROOT_PATH,'app/src/index') 17 | ], 18 | module: { 19 | preLoaders: [ 20 | { 21 | test: /\.jsx?$/, 22 | loaders: process.env.NODE_ENV === 'production' ? [] : ['eslint'], 23 | include: path.resolve(ROOT_PATH, './app') 24 | } 25 | ], 26 | loaders: [{ 27 | test: /\.jsx?$/, 28 | exclude: /node_modules/, 29 | loaders: ['react-hot', 'babel'] 30 | }, 31 | { 32 | test: /\.svg$/, 33 | loader: 'babel!svg-react' 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json' 38 | }, 39 | { 40 | test: /\.(jpe?g|png|gif|svg)$/i, 41 | loaders: [ 42 | 'file?hash=sha512&digest=hex&name=[hash].[ext]', 43 | 'image-webpack?bypassOnDebug&optimizationLevel=7&interlaced=false' 44 | ] 45 | }, 46 | { 47 | test: /\.module\.scss$/, 48 | loaders: [ 49 | 'style', 50 | 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', 51 | 'resolve-url', 52 | 'sass' 53 | ] 54 | }, 55 | { 56 | test: /\.scss$/, 57 | exclude: /\.module\.scss$/, 58 | loaders: ["style", "css", "sass"] 59 | }, 60 | { 61 | test: /\.css$/, 62 | loader: "style-loader!css-loader" 63 | }, 64 | { 65 | test: /\.woff(2)?(\?v=[0-9].[0-9].[0-9])?$/, 66 | loader: "url-loader?mimetype=application/font-woff" 67 | }, 68 | { 69 | test: /\.(ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, 70 | loader: "file-loader?name=[name].[ext]" 71 | }, 72 | { 73 | test: /\.(jpg|png)$/, 74 | loader: 'file?name=[path][name].[hash].[ext]' 75 | } 76 | ] 77 | }, 78 | resolve: { 79 | extensions: ['', '.js', '.jsx'], 80 | alias: { 81 | components: path.resolve(ROOT_PATH, 'app/src/components'), 82 | containers: path.resolve(ROOT_PATH, 'app/src/containers'), 83 | pages: path.resolve(ROOT_PATH, 'app/src/pages'), 84 | styles: path.resolve(ROOT_PATH, 'app/styles'), 85 | utils: path.resolve(ROOT_PATH, 'app/utils'), 86 | }, 87 | }, 88 | output: { 89 | path: process.env.NODE_ENV === 'production' ? 90 | path.resolve(ROOT_PATH, 'server/public') : path.resolve(ROOT_PATH, 'app/build'), 91 | publicPath: '/', 92 | filename: 'bundle.js', 93 | }, 94 | devServer: { 95 | contentBase: path.resolve(ROOT_PATH, 'app/build'), 96 | historyApiFallback: true, 97 | hot: true, 98 | inline: true, 99 | progress: true, 100 | // Constants defined above take care of logic 101 | // For setting host and port 102 | host: HOST, 103 | port: PORT 104 | }, 105 | plugins: [ 106 | new webpack.HotModuleReplacementPlugin(), 107 | new NpmInstallPlugin(), 108 | new HtmlwebpackPlugin({ 109 | title: 'Restaurant Reviewer', 110 | template: 'index.html' 111 | }) 112 | ] 113 | }; 114 | -------------------------------------------------------------------------------- /config/generators/container/index.js: -------------------------------------------------------------------------------- 1 | const componentNameCheck = require('../utils/componentNameCheck'); 2 | const trimTemplateFile = require('../utils/trimTemplateFile'); 3 | 4 | module.exports = { 5 | description: 'Add a container component', 6 | prompts: [ 7 | { 8 | type: 'input', 9 | name: 'name', 10 | message: 'What should it be called?', 11 | default: 'Scalable', 12 | validate: value => { 13 | if ((/.+/).test(value)) { 14 | return componentNameCheck(value) ? 'A container with this name already exists' : true; 15 | } 16 | 17 | return 'The name is required'; 18 | }, 19 | }, 20 | { 21 | type: 'confirm', 22 | name: 'wantSCSSModules', 23 | default: true, 24 | message: 'Does it need styling?', 25 | }, 26 | { 27 | type: 'confirm', 28 | name: 'wantActionsAndReducer', 29 | default: true, 30 | message: 'Do you want actions/constants/reducer for this container?', 31 | }, 32 | ], 33 | actions: (data) => { 34 | const actions = [{ 35 | type: 'add', 36 | path: '../../app/src/containers/{{properCase name}}Container/index.js', 37 | templateFile: './container/index.js.hbs', 38 | abortOnFail: true, 39 | }, { 40 | type: 'add', 41 | path: '../../app/src/containers/{{properCase name}}Container/tests/index.test.js', 42 | templateFile: './container/test.js.hbs', 43 | abortOnFail: true, 44 | }]; 45 | 46 | // Add container export to index.js in container root folder 47 | actions.push({ 48 | type: 'modify', 49 | path: '../../app/src/containers/index.js', 50 | pattern: /(\/\* Assemble all containers for export \*\/)/g, 51 | template: trimTemplateFile('config/generators/container/export.js.hbs'), 52 | }); 53 | 54 | if (data.wantSCSSModules) { 55 | actions.push({ 56 | type: 'add', 57 | path: '../../app/src/containers/{{properCase name}}Container/index.module.scss', 58 | templateFile: './container/styles.scss.hbs', 59 | abortOnFail: true, 60 | }); 61 | } 62 | 63 | // If they want actions and a reducer, generate actions.js, constants.js, 64 | // reducer.js and the corresponding tests for actions and the reducer 65 | if (data.wantActionsAndReducer) { 66 | // Actions 67 | actions.push({ 68 | type: 'add', 69 | path: '../../app/src/containers/{{properCase name}}Container/actions.js', 70 | templateFile: './container/actions.js.hbs', 71 | abortOnFail: true, 72 | }); 73 | actions.push({ 74 | type: 'add', 75 | path: '../../app/src/containers/{{properCase name}}Container/tests/actions.test.js', 76 | templateFile: './container/actions.test.js.hbs', 77 | abortOnFail: true, 78 | }); 79 | 80 | // README.md 81 | actions.push({ 82 | type: 'add', 83 | path: '../../app/src/containers/{{properCase name}}Container/README.md', 84 | templateFile: './container/README.md.hbs', 85 | abortOnFail: true, 86 | }); 87 | 88 | // Constants 89 | actions.push({ 90 | type: 'add', 91 | path: '../../app/src/containers/{{properCase name}}Container/constants.js', 92 | templateFile: './container/constants.js.hbs', 93 | abortOnFail: true, 94 | }); 95 | 96 | // Reducer 97 | actions.push({ 98 | type: 'add', 99 | path: '../../app/src/containers/{{properCase name}}Container/reducer.js', 100 | templateFile: './container/reducer.js.hbs', 101 | abortOnFail: true, 102 | }); 103 | actions.push({ 104 | type: 'add', 105 | path: '../../app/src/containers/{{properCase name}}Container/tests/reducer.test.js', 106 | templateFile: './container/reducer.test.js.hbs', 107 | abortOnFail: true, 108 | }); 109 | } 110 | 111 | return actions; 112 | }, 113 | }; 114 | -------------------------------------------------------------------------------- /app/src/containers/RestaurantsGridContainer/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RESTAURANTS_LOADING_INITIATION, 3 | RESTAURANTS_LOADING_SUCCESS, 4 | RESTAURANTS_LOADING_FAILURE, 5 | RESTAURANT_CATEGORIES, 6 | CLEAR_RESTAURANT_ERRORS, 7 | RESTAURANT_LOCATIONS, 8 | SET_FILTER_LOCATION, 9 | SET_FILTER_RATING, 10 | SET_FILTER_CATEGORY, 11 | APPLY_RESTAURANTS_FILTERS, 12 | CLEAR_RESTAURANTS_FILTERS, 13 | } from './constants'; 14 | 15 | const initialState = { 16 | items: [], 17 | filteredItems: [], 18 | errors: [], 19 | isLoading: false, 20 | categoryFilter: 'All', 21 | ratingFilter: 'All', 22 | locationFilter: 'All', 23 | appliedFilter: { 24 | isApplied: false, 25 | }, 26 | categories: [], 27 | locations: [], 28 | ratings: [ 29 | { 30 | id: 0, 31 | value: 'All', 32 | }, 33 | { 34 | id: 1, 35 | value: '1 Star', 36 | }, 37 | { 38 | id: 2, 39 | value: '2 Star', 40 | }, 41 | { 42 | id: 3, 43 | value: '3 Star', 44 | }, 45 | { 46 | id: 4, 47 | value: '4 Star', 48 | }, 49 | { 50 | id: 5, 51 | value: '5 Star', 52 | }, 53 | ], 54 | }; 55 | 56 | const filteredItems = (state = [], action, previousState) => { 57 | switch (action.type) { 58 | case APPLY_RESTAURANTS_FILTERS: 59 | return previousState.items.filter(i => 60 | previousState.categoryFilter !== 'All' ? 61 | i.type.name === previousState.categoryFilter : true 62 | ).filter(i => 63 | previousState.ratingFilter !== 'All' ? 64 | i.average_rating === parseInt(previousState.ratingFilter, 10) : true 65 | ).filter(i => 66 | previousState.locationFilter !== 'All' ? 67 | i.city === previousState.locationFilter : true 68 | ); 69 | default: 70 | return state; 71 | } 72 | }; 73 | 74 | const restaurants = (state = initialState, action) => { 75 | switch (action.type) { 76 | case RESTAURANTS_LOADING_INITIATION: 77 | return Object.assign({}, state, { 78 | isLoading: true, 79 | }); 80 | case RESTAURANTS_LOADING_SUCCESS: 81 | return Object.assign({}, state, { 82 | isLoading: false, 83 | items: action.restaurants, 84 | filteredItems: action.restaurants, 85 | }); 86 | case RESTAURANT_CATEGORIES: 87 | return Object.assign({}, state, { 88 | categories: action.categories, 89 | }); 90 | case RESTAURANT_LOCATIONS: 91 | return Object.assign({}, state, { 92 | locations: action.locations.map((i, index) => ({ 93 | id: index, 94 | value: i, 95 | })), 96 | }); 97 | case RESTAURANTS_LOADING_FAILURE: 98 | return Object.assign({}, state, { 99 | isLoading: false, 100 | errors: [...state.errors, action.error], 101 | }); 102 | case CLEAR_RESTAURANT_ERRORS: 103 | return Object.assign({}, state, { 104 | errors: [], 105 | }); 106 | case SET_FILTER_LOCATION: 107 | return Object.assign({}, state, { 108 | locationFilter: action.location, 109 | }); 110 | case SET_FILTER_RATING: 111 | return Object.assign({}, state, { 112 | ratingFilter: action.rating, 113 | }); 114 | case SET_FILTER_CATEGORY: 115 | return Object.assign({}, state, { 116 | categoryFilter: action.category, 117 | }); 118 | case APPLY_RESTAURANTS_FILTERS: 119 | return Object.assign({}, state, { 120 | filteredItems: filteredItems(state.filteredItems, action, state), 121 | appliedFilter: { 122 | isApplied: true, 123 | }, 124 | }); 125 | case CLEAR_RESTAURANTS_FILTERS: 126 | return Object.assign({}, state, { 127 | locationFilter: 'All', 128 | ratingFilter: 'All', 129 | categoryFilter: 'All', 130 | filteredItems: state.items, 131 | appliedFilter: { 132 | isApplied: false, 133 | }, 134 | }); 135 | default: 136 | return state; 137 | } 138 | }; 139 | 140 | export default restaurants; 141 | -------------------------------------------------------------------------------- /app/src/containers/RestaurantsGridContainer/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | RESTAURANTS_LOADING_INITIATION, 3 | RESTAURANTS_LOADING_SUCCESS, 4 | RESTAURANTS_LOADING_FAILURE, 5 | RESTAURANT_CATEGORIES, 6 | RESTAURANT_LOCATIONS, 7 | CLEAR_RESTAURANT_ERRORS, 8 | SET_FILTER_LOCATION, 9 | SET_FILTER_RATING, 10 | SET_FILTER_CATEGORY, 11 | APPLY_RESTAURANTS_FILTERS, 12 | CLEAR_RESTAURANTS_FILTERS, 13 | } from './constants'; 14 | import uniq from 'lodash/uniq'; 15 | const baseUrl = 'https://restaurant-reviewer-api.herokuapp.com/api/v1/'; 16 | const restaurantUrl = `${baseUrl}restaurants`; 17 | import fetch from 'isomorphic-fetch'; 18 | 19 | const headers = new Headers({ 20 | 'content-type': 'application/json', 21 | }); 22 | 23 | const options = { 24 | method: 'GET', 25 | headers, 26 | mode: 'no-cors', 27 | }; 28 | 29 | // loadRestaurantsInitiation :: None -> {Action} 30 | const loadRestaurantsInitiation = () => ({ 31 | type: RESTAURANTS_LOADING_INITIATION, 32 | }); 33 | 34 | // loadRestaurantsSuccess :: [JSON] -> {Action} 35 | const loadRestaurantsSuccess = (restaurants) => ({ 36 | type: RESTAURANTS_LOADING_SUCCESS, 37 | restaurants, 38 | }); 39 | 40 | // loadRestaurantCategories :: [String] -> {Action} 41 | const loadRestaurantCategories = (categories) => ({ 42 | type: RESTAURANT_CATEGORIES, 43 | categories, 44 | }); 45 | 46 | // loadRestaurantLocations :: [String] -> {Action} 47 | const loadRestaurantLocations = (locations) => ({ 48 | type: RESTAURANT_LOCATIONS, 49 | locations, 50 | }); 51 | 52 | // loadRestaurantsFailure :: Error -> {Action} 53 | const loadRestaurantsFailure = (error) => ({ 54 | type: RESTAURANTS_LOADING_FAILURE, 55 | error, 56 | }); 57 | 58 | export const clearRestaurantErrors = () => ({ 59 | type: CLEAR_RESTAURANT_ERRORS, 60 | }); 61 | 62 | // loadRestaurants :: None -> Dispatch Func -> Action Data : Error 63 | export const loadRestaurants = () => 64 | (dispatch) => { 65 | dispatch(loadRestaurantsInitiation()); 66 | fetch(restaurantUrl) 67 | .then(res => { 68 | console.log('Received a response from the server: ', res); 69 | return res.json(); 70 | }) 71 | .then(data => { 72 | const { 73 | restaurants, 74 | } = data; 75 | const staticCategories = ['All']; 76 | const dynamicCategories = uniq(restaurants.map(i => i.type.name)); 77 | const finalCategories = [...staticCategories, ...dynamicCategories]; 78 | dispatch( 79 | loadRestaurantCategories(finalCategories) 80 | ); 81 | return restaurants; 82 | }) 83 | .then(restaurants => { 84 | const staticLocations = ['All']; 85 | const dynamicLocations = uniq(restaurants.map(i => i.city)); 86 | const finalLocations = [ 87 | ...staticLocations, 88 | ...dynamicLocations, 89 | ]; 90 | dispatch( 91 | loadRestaurantLocations(finalLocations) 92 | ); 93 | return restaurants; 94 | }) 95 | .then(restaurants => { 96 | dispatch( 97 | loadRestaurantsSuccess(restaurants) 98 | ); 99 | }) 100 | .catch(err => { 101 | const error = { 102 | message: `An error occurred while loading data from server: ${err}`, 103 | }; 104 | dispatch( 105 | loadRestaurantsFailure(error) 106 | ); 107 | }); 108 | }; 109 | 110 | // setFilterLocation :: String -> {Action} 111 | export const setFilterLocation = (location) => ({ 112 | type: SET_FILTER_LOCATION, 113 | location, 114 | }); 115 | 116 | // setFilterRating :: String -> {Action} 117 | export const setFilterRating = (rating) => ({ 118 | type: SET_FILTER_RATING, 119 | rating, 120 | }); 121 | 122 | // setFilterCategory :: String -> {Action} 123 | export const setFilterCategory = (category) => ({ 124 | type: SET_FILTER_CATEGORY, 125 | category, 126 | }); 127 | 128 | // applyFilter :: None -> {Action} 129 | export const applyRestaurantsFilter = () => ({ 130 | type: APPLY_RESTAURANTS_FILTERS, 131 | }); 132 | 133 | // clearRestaurantsFilters :: None -> {Action} 134 | export const clearRestaurantsFilters = () => ({ 135 | type: CLEAR_RESTAURANTS_FILTERS, 136 | }); 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Restaurant Reviewer](https://github.com/RyanCCollins/cdn/blob/master/restaurant-reviewer/main.jpg?raw=true) 2 | 3 | 4 | ## Restaurant Reviewer 5 | 6 | A proof of concept restaurant review application built with a focus on accessibility. 7 | 8 | Accesses JSON served from a [Ruby on Rails API](https://github.com/RyanCCollins/restaurant-reviewer-api) containing restaurant information (including name, a photograph, address, cuisine type and operating hours) as well as JSON containing review information for each restaurant (name of reviewer, date of review, 5-star rating and comments). Includes an application header, and a menu providing multiple ways to filter the restaurants (by cuisine, by location, etc). When viewing a specific restaurant, current reviews are displayed along with a form for the user to submit their own review. 9 | 10 | ## UX & Accessibility Features 11 | This app integrates [grommet](https://github.com/grommet/grommet), the world's most advanced UX framework. It implements many accessibility best practices, including the usage of semantic elements, skip links, proper focus handling, aria attributes, et. al. Color selection and contrast is made with accessibility in mind. 12 | 13 | The design is focused on UX and a11y best practices, incorporating responsive design and proper styling for accessible single page applications. All images utilize alt attributes to provide meaningful information to non-sighted and other users. Interactive elements handle focus successfully, including modals. The project completely passes the Chrome Accessibility Audit tests, making it a joy to use from a screen reader, with the keyboard, zoomed in, or any other way the user desires. 14 | 15 | ### Getting Started 16 | The application requires npm v3.8.8 and node v4.2.4. Errors may occur if you are using other versions. For details on setting node and npm permissions and using node version manager to install specific node versions, please see [this gist](https://gist.github.com/RyanCCollins/69443f0ff1f7725d305d). 17 | 18 | ### Installing 19 | 20 | To install the dependencies, run 21 | ``` 22 | npm run setup 23 | ``` 24 | 25 | Note: there is a script to install webpack and other dependencies globally. This will be run when you run `npm run setup`. If you need to install the global packages manually, make sure to run both `npm install` and also `npm run install-globals`. 26 | 27 | To build the bundle.js and serve with webpack-dev-server, run: 28 | ``` 29 | npm run start 30 | ``` 31 | 32 | The project will be served from: `http://0.0.0.0:1337/` 33 | 34 | ## Serve production bundle.js 35 | 36 | Included in the repo is a pre-built bundle.js, which is located within the /server directory. To serve the bundle without running a build process, please run: 37 | ``` 38 | npm run serve:bundle 39 | ``` 40 | 41 | and browse the served bundle at: `http://0.0.0.0:1337` 42 | 43 | The project uses the [Scalable React Boilerplate Project](https://github.com/RyanCCollins/scalable-react-boilerplate), so please reference it for more useful scripts. 44 | 45 | ## Built With 46 | - [React](https://facebook.github.io/react/) 47 | - [Redux](http://redux.js.org/docs/introduction/) 48 | - [Grommet](http://grommet.io) 49 | - [Scalable React Boilerplate](https://github.com/RyanCCollins/scalable-react-boilerplate) 50 | - [CSS Modules](https://github.com/css-modules/css-modules) 51 | 52 | ## Authors 53 | * **Ryan Collins** 54 | 55 | ## License 56 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 57 | 58 | ## Troubleshooting 59 | If you run into troubles installing the app's dependencies, you should ensure that you are using the following NPM and Node versions. 60 | ``` 61 | "node": "4.2.4", 62 | "npm": "3.8.8" 63 | ``` 64 | 65 | [NVM](https://github.com/creationix/nvm) can be used to change your NodeJS version. 66 | 67 | ## Screen Shots 68 | ![Main Page](https://github.com/RyanCCollins/cdn/blob/master/portfolio-image-gallery-images/restaurant-reviewer-swnd/mainpage.png?raw=true) 69 | ![Single Restaurant](https://github.com/RyanCCollins/cdn/blob/master/portfolio-image-gallery-images/restaurant-reviewer-swnd/singlerestaurantmain.png?raw=true) 70 | ![Reviews](https://github.com/RyanCCollins/cdn/blob/master/portfolio-image-gallery-images/restaurant-reviewer-swnd/reviews.png?raw=true) 71 | -------------------------------------------------------------------------------- /app/src/containers/SingleRestaurantContainer/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | REVIEWS_LOAD_INITIATION, 3 | REVIEWS_LOAD_SUCCESS, 4 | REVIEWS_LOAD_FAILURE, 5 | ADD_REVIEW_INITIATION, 6 | ADD_REVIEW_SUCCESS, 7 | ADD_REVIEW_FAILURE, 8 | REVIEWS_ERRORS, 9 | OPEN_FULL_REVIEW, 10 | CLOSE_FULL_REVIEW, 11 | LOAD_INITIAL_REVIEWS, 12 | TOGGLE_RESTAURANT_HOURS, 13 | } from './constants'; 14 | const baseUrl = 'https://restaurant-reviewer-api.herokuapp.com/api/v1/'; 15 | const reviewsUrl = (restaurantId) => `${baseUrl}restaurants/${restaurantId}/reviews/`; 16 | import fetch from 'isomorphic-fetch'; 17 | 18 | const headers = new Headers({ 19 | 'content-type': 'application/json', 20 | 'Access-Control-Allow-Origin': '*', 21 | }); 22 | 23 | // closeFullReview :: None -> {Action} 24 | export const closeFullReview = () => ({ 25 | type: CLOSE_FULL_REVIEW, 26 | }); 27 | 28 | // openFullReview :: {Object} -> {Action} 29 | export const openFullReview = (review) => ({ 30 | type: OPEN_FULL_REVIEW, 31 | review, 32 | }); 33 | 34 | // loadReviewsInitiation :: None -> {Action} 35 | const loadReviewsInitiation = (selectedRestaurant) => ({ 36 | type: REVIEWS_LOAD_INITIATION, 37 | selectedRestaurant, 38 | }); 39 | 40 | // loadReviewsSuccess :: [JSON] -> {Action} 41 | const loadReviewsSuccess = (reviews) => ({ 42 | type: REVIEWS_LOAD_SUCCESS, 43 | reviews, 44 | }); 45 | 46 | // loadReviewsFailure :: JSON -> {Action} 47 | const loadReviewsFailure = (error) => ({ 48 | type: REVIEWS_LOAD_FAILURE, 49 | error, 50 | }); 51 | 52 | // reviewsErrors :: [Obj] -> {Action} 53 | export const reviewsErrors = (errors) => ({ 54 | type: REVIEWS_ERRORS, 55 | errors, 56 | }); 57 | 58 | // loadInitialReviews :: Array -> {Action} 59 | const loadInitialReviews = (reviews) => ({ 60 | type: LOAD_INITIAL_REVIEWS, 61 | reviews, 62 | }); 63 | 64 | // loadCachedReviews :: Object -> Function -> {Action} 65 | export const loadCachedReviews = (selectedRestaurant) => 66 | (dispatch) => { 67 | dispatch( 68 | loadReviewsInitiation(selectedRestaurant) 69 | ); 70 | const reviewPromise = new Promise((resolve) => { 71 | setTimeout(() => { 72 | resolve(selectedRestaurant.reviews); 73 | }, 3000); 74 | }); 75 | reviewPromise.then(reviews => { 76 | dispatch( 77 | loadInitialReviews(reviews) 78 | ); 79 | return reviews; 80 | }).then(reviews => { 81 | dispatch( 82 | loadReviewsSuccess(reviews) 83 | ); 84 | }).catch(err => { 85 | dispatch( 86 | loadReviewsFailure(err) 87 | ); 88 | }); 89 | }; 90 | 91 | // addReviewInitiation :: None -> {Action} 92 | const addReviewInitiation = () => ({ 93 | type: ADD_REVIEW_INITIATION, 94 | }); 95 | 96 | // addReviewSuccess :: JSON -> {Action} 97 | const addReviewSuccess = (review) => ({ 98 | type: ADD_REVIEW_SUCCESS, 99 | review, 100 | }); 101 | 102 | // addReviewFailure :: JSON -> {Action} 103 | const addReviewFailure = (error) => ({ 104 | type: ADD_REVIEW_FAILURE, 105 | error, 106 | }); 107 | 108 | // addReviewData :: JSON -> {Obj} 109 | const addReviewData = (body) => ({ 110 | method: 'POST', 111 | headers, 112 | body: JSON.stringify(body), 113 | }); 114 | 115 | // encodeReview :: {Object} -> Int -> {Object} 116 | const encodeReview = (review, restaurant) => ({ 117 | review: { 118 | total_stars: review.rating, 119 | text: review.text, 120 | restaurant_id: restaurant.id, 121 | person_attributes: { 122 | name: review.name, 123 | }, 124 | }, 125 | }); 126 | 127 | export const handleReviewError = (error) => 128 | (dispatch) => { 129 | dispatch( 130 | addReviewFailure(error) 131 | ); 132 | }; 133 | 134 | // submitReview :: Int -> JSON -> Func -> Res JSON : Error 135 | export const submitReview = (review, restaurant) => 136 | (dispatch) => { 137 | dispatch( 138 | addReviewInitiation() 139 | ); 140 | fetch( 141 | reviewsUrl(restaurant.id), 142 | addReviewData( 143 | encodeReview(review, restaurant) 144 | ) 145 | ) 146 | .then(res => res.json()) 147 | .then(data => { 148 | const { 149 | review, 150 | } = data; 151 | setTimeout(() => { 152 | dispatch( 153 | addReviewSuccess(review) 154 | ); 155 | }, 2000); 156 | return data; 157 | }) 158 | .catch(error => { 159 | dispatch( 160 | addReviewFailure(error) 161 | ); 162 | }); 163 | }; 164 | 165 | // toggleRestaurantHours :: None -> {Action} 166 | export const toggleRestaurantHours = () => ({ 167 | type: TOGGLE_RESTAURANT_HOURS, 168 | }); 169 | -------------------------------------------------------------------------------- /app/utils/validator.js: -------------------------------------------------------------------------------- 1 | /* Note: used this as a starting place to create the higher order functions 2 | * https://github.com/rszczypka/swd-p1-meetup/blob/master/common/validation.js 3 | */ 4 | 5 | const isIntegerRE = /^\+?(0|[1-9]\d*)$/; 6 | const numberRE = /^(?=.*[0-9]).+$/; 7 | const twoWordsRE = /^[a-z]([-']?[a-z]+)*( [a-z]([-']?[a-z]+)*)+$/; 8 | const lowercaseRE = /^(?=.*[a-z]).+$/; 9 | const uppercaseRE = /^(?=.*[A-Z]).+$/; 10 | const specialCharRE = /^(?=.*[_\W]).+$/; 11 | const emailRE = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 12 | 13 | 14 | /** 15 | * @function join 16 | * @description takes a collection of validation rules, joining into an array 17 | * @param [function] - an array of functions to call to validate 18 | * @param value - the value to validate 19 | * @param data - the data to validate 20 | * @return error - the first error returned from the validation function 21 | */ 22 | const join = (rules) => 23 | (value, data) => 24 | rules.map(rule => 25 | rule(value, data)).filter(error => 26 | !!error 27 | )[0]; 28 | 29 | const noValue = value => 30 | value === undefined || 31 | value === null || 32 | value === ''; 33 | 34 | /** 35 | * @function validateWithRE 36 | * @description - takes a regular expression and a message and returns a function that will return 37 | * The message if the re does not pass with the value passed into the curried function. 38 | * @param {RegExp} = the regular expression to be used to test the value. 39 | * @param String - the message value to return upon failure 40 | * @return Function - a function that takes a value and returns a string message if the RE fails. 41 | */ 42 | const validateWithRE = (RE, message) => 43 | (value) => { 44 | if (!RE.test(value)) { 45 | return message; 46 | } 47 | return undefined; 48 | }; 49 | 50 | export const minLength = (minimum) => 51 | (value) => { 52 | if (!noValue(value) && value.length < minimum) { 53 | return `Value must contain at least ${minimum} characters`; 54 | } 55 | return undefined; 56 | }; 57 | 58 | export const maxLength = (maximum) => 59 | (value) => { 60 | if (!noValue(value) && value.length > maximum) { 61 | return `Value must be no more than ${maximum} characters in length`; 62 | } 63 | return undefined; 64 | }; 65 | 66 | export const valueRequired = (value) => { 67 | if (noValue(value)) { 68 | return 'Value Required'; 69 | } 70 | return undefined; 71 | }; 72 | 73 | export const isAtLeast = (min) => 74 | (value) => { 75 | if (value < min) { return `${value} must be no less than ${min}`; } 76 | return undefined; 77 | }; 78 | 79 | export const isAtMost = (max) => 80 | (value) => { 81 | if (value > max) { return `${value} must be no greater than ${max}`; } 82 | return undefined; 83 | }; 84 | 85 | export const containsSpecialChar = (value) => 86 | value && 87 | validateWithRE( 88 | specialCharRE, 'Must contain 1 special character.' 89 | )(value); 90 | 91 | export const isInteger = (value) => 92 | value && 93 | validateWithRE( 94 | isIntegerRE, 95 | 'Must be an integer value.' 96 | )(value); 97 | 98 | export const containsNumber = (value) => 99 | value && 100 | validateWithRE( 101 | numberRE, 102 | 'Must Contain at least one number' 103 | )(value); 104 | 105 | export const containsLowercase = (value) => 106 | value && 107 | validateWithRE( 108 | lowercaseRE, 109 | 'Must contain at least one lowercase letter.' 110 | )(value); 111 | 112 | export const containsUppercase = (value) => 113 | value && 114 | validateWithRE( 115 | uppercaseRE, 116 | 'Must contain at least one uppercase letter' 117 | )(value); 118 | 119 | export const containsTwoWords = (value) => 120 | value && 121 | validateWithRE( 122 | twoWordsRE, 123 | 'Must contain two words, i.e. full name.' 124 | )(value ? value.toLowerCase() : ''); 125 | 126 | export const isEmail = (value) => 127 | value && 128 | validateWithRE( 129 | emailRE, 130 | 'Must be a valid email address.' 131 | )(value); 132 | 133 | export const createValidator = (validationRules) => 134 | (data = {}) => { 135 | const errors = {}; 136 | Object.keys(validationRules).forEach((key) => { 137 | const rule = join([].concat(validationRules[key])); 138 | const error = rule(data[key], data); 139 | if (error) { 140 | errors[key] = error; 141 | } 142 | }); 143 | return errors; 144 | }; 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restaurant-reviewer", 3 | "version": "1.0.1", 4 | "description": "An UX / A11y focused Restaurant Review App", 5 | "main": "index.js", 6 | "babel": { 7 | "presets": [ 8 | "es2015", 9 | "react", 10 | "stage-0" 11 | ] 12 | }, 13 | "scripts": { 14 | "test": "cross-env NODE_ENV=test karma start config/testing/karma.conf.js --single-run", 15 | "test:watch": "npm run test -- --auto-watch --no-single-run", 16 | "test:firefox": "npm run test -- --browsers Firefox", 17 | "test:safari": "npm run test -- --browsers Safari", 18 | "test:ie": "npm run test -- --browsers IE", 19 | "coveralls": "cat ./coverage/lcov/lcov.info | coveralls", 20 | "build": "webpack", 21 | "dev": "webpack-dev-server", 22 | "generate": "plop --plopfile config/generators/index.js", 23 | "generator": "npm run generate", 24 | "generate:component": "plop --plopfile config/generators/index.js component", 25 | "generate:container": "plop --plopfile config/generators/index.js container", 26 | "generate:page": "plop --plopfile config/generators/index.js page", 27 | "lint": "eslint . --ext .js --ext .jsx; exit 0", 28 | "deploy": "NODE_ENV=production webpack -p", 29 | "start": "npm run dev", 30 | "clean": "rm -rf app/dist app/build", 31 | "setup": "npm install && npm run install-globals", 32 | "install-globals": "npm install -g webpack webpack-dev-server", 33 | "serve:bundle": "NODE_ENV=production PORT=1337 node server.js" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/RyanCCollins/restaurant-reviewer.git" 38 | }, 39 | "keywords": [ 40 | "boilerplate", 41 | "redux", 42 | "react", 43 | "webpack", 44 | "sass", 45 | "css modules" 46 | ], 47 | "engines": { 48 | "node": "4.2.4", 49 | "npm": "3.8.8" 50 | }, 51 | "author": "Ryan Collins", 52 | "license": "MIT", 53 | "bugs": { 54 | "url": "https://github.com/RyanCCollins/restaurant-reviewer/issues" 55 | }, 56 | "homepage": "https://github.com/RyanCCollins/restaurant-reviewer#readme", 57 | "dependencies": { 58 | "actions": "^1.3.0", 59 | "assemble": "^0.3.83", 60 | "autoprefixer-loader": "^3.2.0", 61 | "babel-core": "^6.3.15", 62 | "babel-loader": "^6.2.0", 63 | "babel-preset-es2015": "^6.3.13", 64 | "babel-preset-react": "^6.3.13", 65 | "babel-preset-stage-0": "^6.3.13", 66 | "components": "^0.1.0", 67 | "css-loader": "^0.23.0", 68 | "expect": "^1.20.2", 69 | "express": "^4.14.0", 70 | "file-loader": "^0.9.0", 71 | "foundation-sites": "^6.2.3", 72 | "grommet": "https://github.com/ryanccollins/grommet/tarball/stable", 73 | "grunt": "^0.4.5", 74 | "history": "^1.14.0", 75 | "html-webpack-plugin": "^2.7.1", 76 | "image-webpack-loader": "^2.0.0", 77 | "immutable": "^3.7.5", 78 | "isomorphic-fetch": "^2.2.0", 79 | "isparta": "^4.0.0", 80 | "json-loader": "^0.5.4", 81 | "lodash": "^4.14.1", 82 | "lru-memoize": "^1.0.1", 83 | "node-sass": "^3.4.2", 84 | "node-uuid": "^1.4.7", 85 | "react": "^15.1.0", 86 | "react-addons-css-transition-group": "^15.2.1", 87 | "react-dom": "^15.0.1", 88 | "react-foundation": "^0.6.8", 89 | "react-intl": "^2.1.3", 90 | "react-redux": "^4.4.5", 91 | "react-router": "^2.3.0", 92 | "react-router-redux": "^4.0.4", 93 | "react-stars": "^2.1.0", 94 | "redux": "^3.5.2", 95 | "redux-form": "^5.2.5", 96 | "redux-logger": "^2.6.1", 97 | "redux-promise-middleware": "^3.2.0", 98 | "redux-thunk": "^1.0.0", 99 | "resolve-url-loader": "^1.4.4", 100 | "sass-loader": "^3.1.2", 101 | "style-loader": "^0.13.0", 102 | "styles": "^0.2.1", 103 | "svg-react-loader": "^0.3.6", 104 | "utils": "^0.3.1", 105 | "webpack": "^1.12.9" 106 | }, 107 | "devDependencies": { 108 | "babel-eslint": "^5.0.0-beta4", 109 | "babel-preset-es2015": "^6.9.0", 110 | "chai": "^3.4.1", 111 | "chai-enzyme": "^0.5.0", 112 | "chai-immutable": "^1.5.3", 113 | "cross-env": "^2.0.0", 114 | "enzyme": "^2.4.1", 115 | "eslint": "^1.10.3", 116 | "eslint-config-airbnb": "^4.0.0", 117 | "eslint-loader": "^1.1.1", 118 | "eslint-plugin-react": "^3.11.3", 119 | "flow-bin": "^0.31.1", 120 | "jsdom": "^7.2.0", 121 | "karma": "1.1.1", 122 | "karma-chrome-launcher": "1.0.1", 123 | "karma-coverage": "1.1.1", 124 | "karma-firefox-launcher": "1.0.0", 125 | "karma-ie-launcher": "1.0.0", 126 | "karma-mocha": "1.1.1", 127 | "karma-mocha-reporter": "2.0.4", 128 | "karma-safari-launcher": "1.0.0", 129 | "karma-sourcemap-loader": "0.3.7", 130 | "karma-webpack": "1.7.0", 131 | "mocha": "^2.3.4", 132 | "npm-install-webpack-plugin": "^4.0.3", 133 | "plop": "1.5.0", 134 | "react-css-modules": "^3.7.6", 135 | "react-hot-loader": "^1.3.0", 136 | "redux-devtools": "^3.0.1", 137 | "webpack-dev-server": "^1.14.0", 138 | "webpack-hot-middleware": "^2.10.0" 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/src/components/AddReviewForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * By Ryan Collins 3 | * @Date: 2016-08-16T19:54:09-04:00 4 | * @Email: admin@ryancollins.io 5 | * @Last modified time: 2016-08-16T20:00:48-04:00 6 | * @License: All rights reserved. 7 | 8 | This source code is licensed under the MIT license found in the 9 | LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | 13 | import React, { PropTypes, Component } from 'react'; 14 | import styles from './index.module.scss'; 15 | import cssModules from 'react-css-modules'; 16 | import Form from 'grommet/components/form'; 17 | import FormField from 'grommet/components/formfield'; 18 | import FormFields from 'grommet/components/formfields'; 19 | import Footer from 'grommet/components/footer'; 20 | import Button from 'grommet/components/button'; 21 | import NumberInput from 'grommet/components/numberinput'; 22 | import ReactStars from 'react-stars'; 23 | import Menu from 'grommet/components/menu'; 24 | 25 | const itemInvalid = (item) => 26 | item.touched && item.error; 27 | 28 | class AddReviewForm extends Component { 29 | constructor() { 30 | super(); 31 | this.handleSubmitReview = this.handleSubmitReview.bind(this); 32 | this.validateReview = this.validateReview.bind(this); 33 | this.formIsInvalid = this.formIsInvalid.bind(this); 34 | } 35 | formIsInvalid() { 36 | const { 37 | ratingInput, 38 | nameInput, 39 | textInput, 40 | } = this.props; 41 | return itemInvalid(ratingInput) || 42 | itemInvalid(nameInput) || 43 | itemInvalid(textInput); 44 | } 45 | validateReview(review) { 46 | const length = Object.keys(review).length; 47 | return Object 48 | .keys(review) 49 | .filter(i => { 50 | if (typeof review[i] === 'number') { 51 | return review[i] >= 1 && review[i] <= 5; 52 | } 53 | return review[i].length > 0; 54 | }) 55 | .length === length; 56 | } 57 | handleSubmitReview() { 58 | const { 59 | nameInput, 60 | ratingInput, 61 | textInput, 62 | onSubmitReview, 63 | onSubmitReviewInvalid, 64 | } = this.props; 65 | const review = { 66 | name: nameInput.value, 67 | rating: parseInt(ratingInput.value, 10) || 0, 68 | text: textInput.value, 69 | }; 70 | if (this.validateReview(review)) { 71 | onSubmitReview(review); 72 | } else { 73 | onSubmitReviewInvalid(); 74 | } 75 | } 76 | render() { 77 | const { 78 | onSubmitReview, 79 | nameInput, 80 | ratingInput, 81 | textInput, 82 | onClear, 83 | } = this.props; 84 | return ( 85 |
86 | 87 | 93 | 101 | 102 | 108 |