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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
34 |
35 |
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 |
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 |
36 |
37 |
38 | {review.person}
39 |
40 |
41 |
42 | {review.date}
43 |
44 |
45 |
50 |
51 | {review.text}
52 |
53 |
54 |
55 |
56 | :
57 |
58 | }
59 |
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 |
48 | :
49 |
50 | }
51 | label={isExpanded ? ' Hide Details' : ' Show Details'}
52 | plain
53 | onClick={onExpandHours}
54 | />
55 |
60 |
61 |
62 | {daysOfWeek.map((item, index) =>
63 |
68 | )}
69 |
70 |
71 |
72 |
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 |
53 |
54 |
55 |
56 |
57 |
58 | onViewDetails(restaurant.id)} // eslint-disable-line react/jsx-no-bind
63 | icon={
64 |
70 | }
71 | />
72 |
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 | }
69 | label="Clear Filters"
70 | plain
71 | onClick={onClearFilter}
72 | />
73 | :
74 |
82 | }
83 |
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 | 
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 | 
69 | 
70 | 
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 |
149 | );
150 | }
151 | }
152 |
153 | AddReviewForm.propTypes = {
154 | nameInput: PropTypes.object.isRequired,
155 | textInput: PropTypes.object.isRequired,
156 | ratingInput: PropTypes.object.isRequired,
157 | onSubmitReview: PropTypes.func.isRequired,
158 | onClear: PropTypes.func.isRequired,
159 | onSubmitReviewInvalid: PropTypes.func.isRequired,
160 | };
161 |
162 | export default cssModules(AddReviewForm, styles);
163 |
--------------------------------------------------------------------------------
/app/src/containers/AddReviewContainer/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { bindActionCreators } from 'redux';
4 | import * as AddReviewActionCreators from './actions';
5 | import cssModules from 'react-css-modules';
6 | import styles from './index.module.scss';
7 | import validation from './validation/index';
8 | import {
9 | AddReviewForm,
10 | AddButton,
11 | ErrorAlert,
12 | } from 'components';
13 | import { reduxForm } from 'redux-form';
14 | import Footer from 'grommet/components/footer';
15 | import Layer from 'grommet/components/layer';
16 | import Box from 'grommet/components/box';
17 | import Button from 'grommet/components/button';
18 | import Menu from 'grommet/components/menu';
19 | import Section from 'grommet/components/Section';
20 |
21 | export const addReviewFields = [
22 | 'nameInput',
23 | 'textInput',
24 | 'ratingInput',
25 | ];
26 |
27 | class AddReview extends Component { // eslint-disable-line react/prefer-stateless-function
28 | constructor() {
29 | super();
30 | this.handleToggleModal = this.handleToggleModal.bind(this);
31 | this.handleSubmitReview = this.handleSubmitReview.bind(this);
32 | this.handleClear = this.handleClear.bind(this);
33 | this.handleReviewInvalid = this.handleReviewInvalid.bind(this);
34 | this.handleClearErrors = this.handleClearErrors.bind(this);
35 | }
36 | handleToggleModal() {
37 | const {
38 | actions,
39 | isAddingReview,
40 | } = this.props;
41 | if (isAddingReview) {
42 | document.getElementById('app').classList.remove('no-scroll');
43 | this.handleClear();
44 | } else {
45 | document.getElementById('app').classList.add('no-scroll');
46 | }
47 | actions.toggleAddReview();
48 | }
49 | handleReviewInvalid() {
50 | const {
51 | addReviewInvalid,
52 | } = this.props.actions;
53 | addReviewInvalid({
54 | message: 'The review was invalid. Please correct and try again',
55 | });
56 | }
57 | handleSubmitReview(review) {
58 | const {
59 | onSubmitReview,
60 | } = this.props;
61 | onSubmitReview(review);
62 | this.handleToggleModal();
63 | }
64 | handleClear() {
65 | const {
66 | resetForm,
67 | } = this.props;
68 | resetForm();
69 | }
70 | handleClearErrors() {
71 | const {
72 | actions,
73 | } = this.props;
74 | actions.clearAddReviewErrors();
75 | }
76 | render() {
77 | const {
78 | isAddingReview,
79 | fields,
80 | hasFab,
81 | resetForm,
82 | error,
83 | } = this.props;
84 | return (
85 |
86 | {isAddingReview ?
87 |
93 |
94 | {error != null && // eslint-disable-line
95 |
99 |
103 |
104 | }
105 |
106 |
112 |
113 |
114 |
115 | :
116 |
117 |
118 |
124 |
125 | {hasFab &&
126 |
127 | }
128 |
129 | }
130 |
131 | );
132 | }
133 | }
134 |
135 | AddReview.propTypes = {
136 | isAddingReview: PropTypes.bool.isRequired,
137 | actions: PropTypes.object.isRequired,
138 | fields: PropTypes.object.isRequired,
139 | onSubmitReview: PropTypes.func.isRequired,
140 | hasFab: PropTypes.bool.isRequired,
141 | resetForm: PropTypes.func.isRequired,
142 | error: PropTypes.object,
143 | };
144 |
145 | // mapStateToProps :: {State} -> {Props}
146 | const mapStateToProps = (state) => ({
147 | isAddingReview: state.addReview.isAddingReview,
148 | error: state.addReview.error,
149 | });
150 |
151 | // mapDispatchToProps :: Dispatch -> {Action}
152 | const mapDispatchToProps = (dispatch) => ({
153 | actions: bindActionCreators(
154 | AddReviewActionCreators,
155 | dispatch
156 | ),
157 | });
158 |
159 | const Container = cssModules(AddReview, styles);
160 |
161 | const ConnectedContainer = connect(
162 | mapStateToProps,
163 | mapDispatchToProps
164 | )(Container);
165 |
166 | export default reduxForm({
167 | form: 'addReview',
168 | fields: addReviewFields,
169 | validate: validation,
170 | })(ConnectedContainer);
171 |
--------------------------------------------------------------------------------
/app/src/containers/SingleRestaurantContainer/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { bindActionCreators } from 'redux';
4 | import * as SingleRestaurantActionCreators from './actions';
5 | import cssModules from 'react-css-modules';
6 | import styles from './index.module.scss';
7 | import {
8 | SingleRestaurant,
9 | ReviewGrid,
10 | LoadingIndicator,
11 | ErrorAlert,
12 | } from 'components';
13 | import { AddReviewContainer, FullReviewModalContainer } from 'containers';
14 | import Section from 'grommet/components/section';
15 |
16 | class SingleRestaurantContainer extends Component {
17 | constructor(props) {
18 | super(props);
19 | this.handleLoadingOfRestaurant = this.handleLoadingOfRestaurant.bind(this);
20 | this.handleSubmitReview = this.handleSubmitReview.bind(this);
21 | this.handleCloseReview = this.handleCloseReview.bind(this);
22 | this.handleOpenReview = this.handleOpenReview.bind(this);
23 | this.handleToggleHours = this.handleToggleHours.bind(this);
24 | }
25 | componentDidMount() {
26 | this.handleLoadingOfRestaurant();
27 | }
28 | handleLoadingOfRestaurant() {
29 | const {
30 | restaurants,
31 | params,
32 | } = this.props;
33 | const itemId = parseInt(params.id, 10);
34 | const selectedRestaurant = restaurants.filter(item => item.id === itemId)[0];
35 | if (!selectedRestaurant) {
36 | const {
37 | router,
38 | } = this.context;
39 | router.push('/');
40 | }
41 | const {
42 | loadCachedReviews,
43 | } = this.props.actions;
44 | loadCachedReviews(selectedRestaurant);
45 | }
46 | handleSubmitReview(review) {
47 | const {
48 | actions,
49 | selectedRestaurant,
50 | } = this.props;
51 | actions.submitReview(review, selectedRestaurant);
52 | }
53 | handleCloseReview() {
54 | const {
55 | actions,
56 | } = this.props;
57 | document.getElementById('app').classList.remove('no-scroll');
58 | actions.closeFullReview();
59 | }
60 | handleOpenReview(id) {
61 | const {
62 | actions,
63 | } = this.props;
64 | document.getElementById('app').classList.add('no-scroll');
65 | actions.openFullReview(id);
66 | }
67 | handleToggleHours() {
68 | const {
69 | toggleRestaurantHours,
70 | } = this.props.actions;
71 | toggleRestaurantHours();
72 | }
73 | render() {
74 | const {
75 | selectedReviewId,
76 | isLoading,
77 | errors,
78 | selectedRestaurant,
79 | reviews,
80 | hoursAreExpanded,
81 | } = this.props;
82 | return (
83 |
84 | {selectedRestaurant ?
85 |
86 |
91 |
96 | {errors.length > 0 &&
97 |
98 | }
99 | {isLoading ?
100 |
101 | :
102 |
103 |
107 |
111 | item.id === selectedReviewId
112 | )[0]}
113 | />
114 |
115 | }
116 |
117 | :
118 |
119 |
No Restaurant Found
120 | {'Going back home where it\'s safe!'}
121 |
122 | }
123 |
124 | );
125 | }
126 | }
127 |
128 | SingleRestaurantContainer.propTypes = {
129 | selectedReviewId: PropTypes.number,
130 | errors: PropTypes.array,
131 | isLoading: PropTypes.bool.isRequired,
132 | restaurants: PropTypes.array.isRequired,
133 | reviews: PropTypes.array.isRequired,
134 | params: PropTypes.object.isRequired,
135 | actions: PropTypes.object.isRequired,
136 | addReviewData: PropTypes.object,
137 | selectedRestaurant: PropTypes.object.isRequired,
138 | hoursAreExpanded: PropTypes.bool.isRequired,
139 | };
140 |
141 | SingleRestaurantContainer.contextTypes = {
142 | router: PropTypes.object.isRequired,
143 | };
144 |
145 | // mapStateToProps :: {State} -> {Props}
146 | const mapStateToProps = (state) => ({
147 | errors: state.singleRestaurant.errors,
148 | isLoading: state.singleRestaurant.isLoading,
149 | restaurants: state.restaurants.items,
150 | reviews: state.singleRestaurant.reviews,
151 | addReviewData: state.form.addReview,
152 | selectedReviewId: state.singleRestaurant.selectedReviewId,
153 | selectedRestaurant: state.singleRestaurant.selectedRestaurant,
154 | hoursAreExpanded: state.singleRestaurant.hoursAreExpanded,
155 | });
156 |
157 | // mapDispatchToProps :: Dispatch -> {Action}
158 | const mapDispatchToProps = (dispatch) => ({
159 | actions: bindActionCreators(
160 | SingleRestaurantActionCreators,
161 | dispatch
162 | ),
163 | });
164 |
165 | const Container = cssModules(SingleRestaurantContainer, styles);
166 |
167 | export default connect(
168 | mapStateToProps,
169 | mapDispatchToProps
170 | )(Container);
171 |
--------------------------------------------------------------------------------
/app/src/components/AboutInfo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './index.module.scss';
3 | import cssModules from 'react-css-modules';
4 | import { Link } from 'react-router';
5 | import Hero from 'grommet/components/hero';
6 | import Heading from 'grommet/components/heading';
7 | import Box from 'grommet/components/Box';
8 | import Card from 'grommet/components/Card';
9 | import Section from 'grommet/components/Section';
10 | import Article from 'grommet/components/Article';
11 | import Markdown from 'grommet/components/Markdown';
12 | import Footer from 'grommet/components/Footer';
13 | import Button from 'grommet/components/Button';
14 | import Image from 'grommet/components/Image';
15 | import Tiles from 'grommet/components/Tiles';
16 | import Tile from 'grommet/components/Tile';
17 |
18 | const AboutInfo = () => (
19 |
20 |
24 |
25 | Restaurant Reviewer
26 |
27 |
28 |
29 |
30 |
31 |
38 |
44 |
45 |
53 |
70 | }
71 | />
72 |
73 |
74 |
75 |
76 |
77 | Made With These Great Technologies
78 |
79 |
80 |
81 |
82 |
86 |
87 |
88 |
92 |
93 |
94 |
98 |
99 |
100 |
104 |
105 |
106 |
107 |
108 |
109 | About the Developer
110 |
111 |
119 |
124 |
132 |
133 | Ryan Collins
134 |
135 |
136 | Web Techologist, Full Stack Engineer
137 |
138 |
139 | Experienced engineer specializing in implementing
140 | cutting-edge technologies
141 |
142 | in a multitude of domains,
143 | including Front End web, UI / UX, et. al.
144 |
145 |
146 |
147 |
148 |
149 |
157 |
158 | a} // eslint-disable-line react/jsx-no-bind
160 | a11yTitle="Go Home"
161 | >
162 | Take me home
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | );
171 |
172 | export default cssModules(AboutInfo, styles);
173 |
--------------------------------------------------------------------------------