From 40381472c0b426343ce115174125e575deb6ec94 Mon Sep 17 00:00:00 2001 From: youvovk Date: Mon, 4 Nov 2019 22:03:44 +0200 Subject: [PATCH 1/9] first --- package.json | 8 +- public/images/Apple.svg | 9 ++ public/images/Google.svg | 9 ++ public/images/Group (4).svg | 5 + public/images/Logo_uber.svg | 4 + public/images/fb.svg | 4 + .../images/if_icon-ios7-arrow-back_211686.svg | 5 + public/images/input.svg | 4 + public/images/instagram.svg | 3 + public/images/search.svg | 3 + public/images/twitter.svg | 3 + public/images/world.svg | 5 + public/index.html | 2 + src/App.css | 1 - src/App.js | 28 +++- src/App.scss | 6 + src/components/Error/Error.js | 27 ++++ src/components/Error/Error.scss | 20 +++ src/components/Error/index.js | 0 src/components/Footer/Footer.js | 120 ++++++++++++++++++ src/components/Footer/Footer.scss | 84 ++++++++++++ src/components/Footer/index.js | 0 src/components/Header/Header.js | 69 ++++++++++ src/components/Header/Header.scss | 36 ++++++ src/components/Header/index.js | 0 src/components/Input/Input.js | 83 ++++++++++++ src/components/Input/Input.scss | 30 +++++ src/components/Input/index.js | 0 src/components/Loader/Loader.js | 13 ++ src/components/Loader/Loader.scss | 72 +++++++++++ src/components/Loader/index.js | 0 .../RestaurantCard/RestaurantCard.js | 42 ++++++ .../RestaurantCard/RestaurantCard.scss | 33 +++++ src/components/RestaurantCard/index.js | 0 .../RestaurantsListPage.js | 72 +++++++++++ .../RestaurantsListPage.scss | 5 + src/components/RestaurantsListPage/index.js | 27 ++++ src/components/Select/Select.js | 56 ++++++++ src/components/Select/Select.scss | 36 ++++++ src/components/Select/index.js | 0 src/index.js | 4 +- src/store/actions.js | 35 +++++ src/store/index.js | 11 ++ src/store/rootReducer.js | 48 +++++++ src/store/selectors.js | 26 ++++ src/styles/extends.scss | 17 +++ src/styles/index.scss | 31 +++++ src/styles/utils.scss | 2 + src/styles/var.scss | 1 + 49 files changed, 1089 insertions(+), 10 deletions(-) create mode 100644 public/images/Apple.svg create mode 100644 public/images/Google.svg create mode 100644 public/images/Group (4).svg create mode 100644 public/images/Logo_uber.svg create mode 100644 public/images/fb.svg create mode 100644 public/images/if_icon-ios7-arrow-back_211686.svg create mode 100644 public/images/input.svg create mode 100644 public/images/instagram.svg create mode 100644 public/images/search.svg create mode 100644 public/images/twitter.svg create mode 100644 public/images/world.svg delete mode 100644 src/App.css create mode 100644 src/App.scss create mode 100644 src/components/Error/Error.js create mode 100644 src/components/Error/Error.scss create mode 100644 src/components/Error/index.js create mode 100644 src/components/Footer/Footer.js create mode 100644 src/components/Footer/Footer.scss create mode 100644 src/components/Footer/index.js create mode 100644 src/components/Header/Header.js create mode 100644 src/components/Header/Header.scss create mode 100644 src/components/Header/index.js create mode 100644 src/components/Input/Input.js create mode 100644 src/components/Input/Input.scss create mode 100644 src/components/Input/index.js create mode 100644 src/components/Loader/Loader.js create mode 100644 src/components/Loader/Loader.scss create mode 100644 src/components/Loader/index.js create mode 100644 src/components/RestaurantCard/RestaurantCard.js create mode 100644 src/components/RestaurantCard/RestaurantCard.scss create mode 100644 src/components/RestaurantCard/index.js create mode 100644 src/components/RestaurantsListPage/RestaurantsListPage.js create mode 100644 src/components/RestaurantsListPage/RestaurantsListPage.scss create mode 100644 src/components/RestaurantsListPage/index.js create mode 100644 src/components/Select/Select.js create mode 100644 src/components/Select/Select.scss create mode 100644 src/components/Select/index.js create mode 100644 src/store/actions.js create mode 100644 src/store/index.js create mode 100644 src/store/rootReducer.js create mode 100644 src/store/selectors.js create mode 100644 src/styles/extends.scss create mode 100644 src/styles/index.scss create mode 100644 src/styles/utils.scss create mode 100644 src/styles/var.scss diff --git a/package.json b/package.json index 7bd01de..7a590b4 100755 --- a/package.json +++ b/package.json @@ -7,11 +7,17 @@ "author": "Mate Academy", "license": "GPL-3.0", "dependencies": { + "classnames": "^2.2.6", + "node-sass": "^4.13.0", "prop-types": "^15.7.2", "react": "^16.8.6", "react-dom": "^16.8.6", + "react-redux": "^7.1.1", "react-router-dom": "^5.0.1", - "react-scripts": "3.0.1" + "react-scripts": "3.0.1", + "redux": "^4.0.4", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" }, "devDependencies": { "@mate-academy/eslint-config-react": "*", diff --git a/public/images/Apple.svg b/public/images/Apple.svg new file mode 100644 index 0000000..9196a5b --- /dev/null +++ b/public/images/Apple.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/Google.svg b/public/images/Google.svg new file mode 100644 index 0000000..8b29bb9 --- /dev/null +++ b/public/images/Google.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/Group (4).svg b/public/images/Group (4).svg new file mode 100644 index 0000000..7c3e6a0 --- /dev/null +++ b/public/images/Group (4).svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/Logo_uber.svg b/public/images/Logo_uber.svg new file mode 100644 index 0000000..7418753 --- /dev/null +++ b/public/images/Logo_uber.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/fb.svg b/public/images/fb.svg new file mode 100644 index 0000000..c3894d1 --- /dev/null +++ b/public/images/fb.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/if_icon-ios7-arrow-back_211686.svg b/public/images/if_icon-ios7-arrow-back_211686.svg new file mode 100644 index 0000000..473a5e2 --- /dev/null +++ b/public/images/if_icon-ios7-arrow-back_211686.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/input.svg b/public/images/input.svg new file mode 100644 index 0000000..a2f570f --- /dev/null +++ b/public/images/input.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/instagram.svg b/public/images/instagram.svg new file mode 100644 index 0000000..9c8b7bd --- /dev/null +++ b/public/images/instagram.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/search.svg b/public/images/search.svg new file mode 100644 index 0000000..7e37c73 --- /dev/null +++ b/public/images/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/twitter.svg b/public/images/twitter.svg new file mode 100644 index 0000000..22565b3 --- /dev/null +++ b/public/images/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/world.svg b/public/images/world.svg new file mode 100644 index 0000000..953d851 --- /dev/null +++ b/public/images/world.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/index.html b/public/index.html index 4269d67..4c1cbc8 100644 --- a/public/index.html +++ b/public/index.html @@ -4,6 +4,8 @@ React Uber eats + +
diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 8b13789..0000000 --- a/src/App.css +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/App.js b/src/App.js index d494a99..9767703 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,24 @@ import React from 'react'; -import './App.css'; +import { Provider } from 'react-redux'; -const App = () => ( -
-

React Uber eats

-
-); +import { + RestaurantsListPage, +} from './components/RestaurantsListPage/index'; + +import './App.scss'; -export default App; +import { store } from './store'; +import { Header } from './components/Header/Header'; +import { Footer } from './components/Footer/Footer'; + +export const App = () => ( + +
+
+
+ +
+
+
+ +); diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 0000000..b3af063 --- /dev/null +++ b/src/App.scss @@ -0,0 +1,6 @@ +.page { + position: relative; + min-height: 100vh; + padding-top: 40px; + margin-bottom: 40px; +} diff --git a/src/components/Error/Error.js b/src/components/Error/Error.js new file mode 100644 index 0000000..e156726 --- /dev/null +++ b/src/components/Error/Error.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Error.scss'; + +export const Error = (props) => { + const { message } = props; + + return ( +
+

+ {message} +

+ + + Go to Home + +
+ ) +}; + +Error.propTypes = { + message: PropTypes.string, +}; + +Error.defaultProps = { + message: 'Sorry, something went wrong', +}; diff --git a/src/components/Error/Error.scss b/src/components/Error/Error.scss new file mode 100644 index 0000000..9b53d5c --- /dev/null +++ b/src/components/Error/Error.scss @@ -0,0 +1,20 @@ +@import '../../styles/utils.scss'; + +.error { + @extend %absolute-center; + flex-flow: column nowrap; + + &__text { + font-size: 30px; + font-weight: 500; + } + + &__link { + font-size: 24px; + color: #5eb707; + + &:hover { + text-decoration: underline; + } + } +} diff --git a/src/components/Error/index.js b/src/components/Error/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js new file mode 100644 index 0000000..af0be88 --- /dev/null +++ b/src/components/Footer/Footer.js @@ -0,0 +1,120 @@ +import React from 'react'; +import { Select } from '../Select/Select'; +import './Footer.scss'; + +export const Footer = () => ( +
+
+
+
+
+ Uber Eats + + + +
+ + + + + Sign In + +
+
+
+ ); + } +} diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss new file mode 100644 index 0000000..adab643 --- /dev/null +++ b/src/components/Header/Header.scss @@ -0,0 +1,36 @@ +@import "../../styles/extends.scss"; + +.header { + position: sticky; + z-index: 1; + top: 0; + padding: 16px 0; + + background-color: #fff; + + &__inner { + @extend %flex-row-center; + } + + &__delivery-info { + @extend %flex-row-center; + + margin-left: 7vw; + } + + &__search { + margin-left: auto; + } + + &__link { + margin-left: 40px; + padding: 13px 0; + + white-space: nowrap; + color: #1f1f1f; + + &:hover { + text-decoration: underline; + } + } +} diff --git a/src/components/Header/index.js b/src/components/Header/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Input/Input.js b/src/components/Input/Input.js new file mode 100644 index 0000000..341bc7c --- /dev/null +++ b/src/components/Input/Input.js @@ -0,0 +1,83 @@ +import React, { PureComponent, createRef } from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import './Input.scss'; + +export class Input extends PureComponent { + state = { + isFocused: false, + }; + + inputRef = createRef(); + + handleFocus = () => this.setState({ isFocused: true }); + + handleBlur = () => this.setState({ isFocused: false }); + + focus = () => this.inputRef.current.focus(); + + render() { + const { + iconUrl, + value, + onChange, + type, + placeholder, + name, + className, + } = this.props; + + const { isFocused } = this.state; + + const rootClass = cx('control', { + 'control--focused': isFocused, + [className]: !!className, + }); + + return ( +
+ {!!iconUrl && ( + {placeholder} + )} + + +
+ ); + } +} + +Input.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + type: PropTypes.string, + placeholder: PropTypes.string, + iconUrl: PropTypes.string, + className: PropTypes.string, +}; + +Input.defaultProps = { + type: 'text', + placeholder: '', + iconUrl: '', + className: '', +}; diff --git a/src/components/Input/Input.scss b/src/components/Input/Input.scss new file mode 100644 index 0000000..9e228fc --- /dev/null +++ b/src/components/Input/Input.scss @@ -0,0 +1,30 @@ +@import "../../styles/extends.scss"; + +.control { + $border-color: #e0e0e0; + @extend %flex-row-center; + + padding: 11px 16px; + + border: 1px solid $border-color; + transition: border-color 300ms; + + &--focused { + border-color: darken($border-color, 20%); + } + + &__icon { + width: 14px; + height: 14px; + margin-right: 10px; + } + + &__input { + max-height: 24px; + line-height: 24px; + } + + & + & { + margin-left: 20px; + } +} diff --git a/src/components/Input/index.js b/src/components/Input/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Loader/Loader.js b/src/components/Loader/Loader.js new file mode 100644 index 0000000..13db96a --- /dev/null +++ b/src/components/Loader/Loader.js @@ -0,0 +1,13 @@ +import React from 'react'; +import './Loader.scss'; + +export const Loader = () => ( +
+
+
+
+
+
+
+
+); diff --git a/src/components/Loader/Loader.scss b/src/components/Loader/Loader.scss new file mode 100644 index 0000000..ed26208 --- /dev/null +++ b/src/components/Loader/Loader.scss @@ -0,0 +1,72 @@ +@import '../../styles/utils.scss'; + +.loader-container { + @extend %absolute-center; +} + +.lds-ellipsis { + display: inline-block; + position: relative; + width: 64px * 2; + height: 64px * 2; +} + +.lds-ellipsis div { + position: absolute; + top: 27px * 2; + width: 11px * 2; + height: 11px * 2; + border-radius: 50%; + background: #5eb707; + animation-timing-function: cubic-bezier(0, 1, 1, 0); +} + +.lds-ellipsis div:nth-child(1) { + left: 6px * 2; + animation: lds-ellipsis1 0.6s infinite; +} + +.lds-ellipsis div:nth-child(2) { + left: 6px * 2; + animation: lds-ellipsis2 0.6s infinite; +} + +.lds-ellipsis div:nth-child(3) { + left: 26px * 2; + animation: lds-ellipsis2 0.6s infinite; +} + +.lds-ellipsis div:nth-child(4) { + left: 45px * 2; + animation: lds-ellipsis3 0.6s infinite; +} + +@keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + + 100% { + transform: scale(1); + } +} + +@keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + + 100% { + transform: scale(0); + } +} + +@keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + + 100% { + transform: translate(19px * 2, 0); + } +} diff --git a/src/components/Loader/index.js b/src/components/Loader/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/RestaurantCard/RestaurantCard.js b/src/components/RestaurantCard/RestaurantCard.js new file mode 100644 index 0000000..4841f9e --- /dev/null +++ b/src/components/RestaurantCard/RestaurantCard.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './RestaurantCard.scss'; + +// heroImageUrl, title, categories, etaRange.errorMessage, uuid + +export const RestaurantsCard = (props) => { + const { + imageUrl, + title, + categories, + etaRange, + uuid, + } = props; + + return ( +
+ {title} +

{title}

+
+ {categories.join(' • ')} +
+
+ {etaRange} +
+
+ ); +}; + +RestaurantsCard.propTypes = { + imageUrl: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + categories: PropTypes.arrayOf(PropTypes.string), + etaRange: PropTypes.string, + //uuid: PropTypes.string.isRequired, +}; + +RestaurantsCard.defaultProps = { + categories: [], + etaRange: '', +}; diff --git a/src/components/RestaurantCard/RestaurantCard.scss b/src/components/RestaurantCard/RestaurantCard.scss new file mode 100644 index 0000000..3b1add1 --- /dev/null +++ b/src/components/RestaurantCard/RestaurantCard.scss @@ -0,0 +1,33 @@ +.restaurant-card { + $height: 367px; + + height: $height; + width: 100%; + + cursor: pointer; + + &__img { + height: $height * 0.7; + width: 100%; + object-fit: cover; + } + + &__title { + margin: 12px 0 4px; + } + + &__categories { + font-size: 14px; + color: #757575; + + margin-bottom: 4px; + } + + &__eta { + font-size: 14px; + padding: 2px 8px; + + display: inline-block; + background-color: #f5f5f5; + } +} diff --git a/src/components/RestaurantCard/index.js b/src/components/RestaurantCard/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/RestaurantsListPage/RestaurantsListPage.js b/src/components/RestaurantsListPage/RestaurantsListPage.js new file mode 100644 index 0000000..ec3fce0 --- /dev/null +++ b/src/components/RestaurantsListPage/RestaurantsListPage.js @@ -0,0 +1,72 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { Loader } from '../Loader/Loader'; +import { Error } from '../Error/Error'; +import { RestaurantsCard } from '../RestaurantCard/RestaurantCard'; + +import './RestaurantsListPage.scss'; + +const DEFAULT_ETA_RANGE = '20 - 30 min'; + +export class RestaurantsListPage extends Component { + componentDidMount() { + const { loadRestaurants } = this.props; + + loadRestaurants(); + } + + render() { + const { + restaurantsData, + error, + isLoading, + } = this.props; + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( +
+ {restaurantsData.map((restaurant) => { + const { + uuid, + title, + heroImageUrl, + categories, + etaRange, + } = restaurant; + + return ( + + ); + })} +
+ ); + } +} + +RestaurantsListPage.propTypes = { + restaurantsData: PropTypes.arrayOf(PropTypes.shape({})), + loadRestaurants: PropTypes.func.isRequired, + isLoading: PropTypes.bool, + error: PropTypes.string, +}; + +RestaurantsListPage.defaultProps = { + restaurantsData: [], + isLoading: false, + error: null, +}; diff --git a/src/components/RestaurantsListPage/RestaurantsListPage.scss b/src/components/RestaurantsListPage/RestaurantsListPage.scss new file mode 100644 index 0000000..a87ec4f --- /dev/null +++ b/src/components/RestaurantsListPage/RestaurantsListPage.scss @@ -0,0 +1,5 @@ +.restaurants-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(348px, 1fr)); + gap: 40px 20px; +} diff --git a/src/components/RestaurantsListPage/index.js b/src/components/RestaurantsListPage/index.js new file mode 100644 index 0000000..294f5f7 --- /dev/null +++ b/src/components/RestaurantsListPage/index.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { RestaurantsListPage } from './RestaurantsListPage'; +import { loadRestaurants } from '../../store/actions'; +import { + selectorRestaurants, + selectIsLoading, + selectRestaurantsListError, +} from '../../store/selectors'; + +function mapState2Props(state) { + return { + restaurantsData: selectorRestaurants(state), + isLoading: selectIsLoading(state), + error: selectRestaurantsListError(state), + }; +} + +const mapDispatch2Props = { + loadRestaurants, +}; + +const Enhanced = connect( + mapState2Props, + mapDispatch2Props, +)(RestaurantsListPage); + +export { Enhanced as RestaurantsListPage }; diff --git a/src/components/Select/Select.js b/src/components/Select/Select.js new file mode 100644 index 0000000..7759b34 --- /dev/null +++ b/src/components/Select/Select.js @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Select.scss'; + +export const Select = (props) => { + const { + name, + value, + onSelect, + options, + iconUrl, + } = props; + + return ( +
+ + {!!iconUrl && ( + select icon + )} + + arrow down +
+ ); +}; + +Select.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onSelect: PropTypes.func, + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string, + label: PropTypes.string, + })), + iconUrl: PropTypes.string, +}; + +Select.defaultProps = { + options: [], + iconUrl: '', + onSelect: () => {}, +}; diff --git a/src/components/Select/Select.scss b/src/components/Select/Select.scss new file mode 100644 index 0000000..d44853a --- /dev/null +++ b/src/components/Select/Select.scss @@ -0,0 +1,36 @@ +.select { + position: relative; + + &__input { + -moz-appearance: none; + -webkit-appearance: none; + + color: #fff; + + position: relative; + padding: 10px 40px; + + background-color: transparent; + border: 1px solid #979797; + border-radius: 0; + cursor: pointer; + } + + &__icon { + position: absolute; + top: 12px; + left: 10px; + + width: 20px; + height: 20px; + } + + &__arrow { + position: absolute; + top: 20px; + right: 10px; + + width: 14px; + height: 8px; + } +} diff --git a/src/components/Select/index.js b/src/components/Select/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/index.js b/src/index.js index b597a44..4a2cf0e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import App from './App'; +import { App } from './App'; + +import './styles/index.scss'; ReactDOM.render(, document.getElementById('root')); diff --git a/src/store/actions.js b/src/store/actions.js new file mode 100644 index 0000000..d2ab202 --- /dev/null +++ b/src/store/actions.js @@ -0,0 +1,35 @@ +export const ACTION_TYPES = { + SAVE_RESTAURANTS: 'SAVE_RESTAURANTS', + SET_LOAD_RESTAURANTS_ERROR: 'SET_LOAD_RESTAURANTS_ERROR', + START_LOADING: 'START_LOADING', + STOP_LOADING: 'STOP_LOADING', + +}; + +const saveRestaurants = data => ({ + type: ACTION_TYPES.SAVE_RESTAURANTS, + payload: data, +}); + +const setRestaurantsError = error => ({ + type: ACTION_TYPES.SET_LOAD_RESTAURANTS_ERROR, + payload: error, +}); + +const startLoading = () => ({ + type: ACTION_TYPES.START_LOADING, +}); + +const stopLoading = () => ({ + type: ACTION_TYPES.STOP_LOADING, +}); + +export const loadRestaurants = () => (dispatch) => { + dispatch(startLoading()); + + fetch('https://mate-uber-eats-api.herokuapp.com/api/v1/restaurants') + .then(res => res.json()) + .then(({ data }) => dispatch(saveRestaurants(data))) + .catch(error => dispatch(setRestaurantsError(error.massage))) + .finally(() => dispatch(stopLoading())); +}; diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..8cbd8dc --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,11 @@ +import { createStore, compose, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import { rootReducer } from './rootReducer'; + +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +const middlewares = [thunk]; + +export const store = createStore( + rootReducer, + composeEnhancers(applyMiddleware(...middlewares)) +); diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js new file mode 100644 index 0000000..1b8259d --- /dev/null +++ b/src/store/rootReducer.js @@ -0,0 +1,48 @@ +import { ACTION_TYPES } from './actions'; + +const initialStore = { + restaurantsListData: null, + isLoading: false, + error: null, +}; + +export function rootReducer(state = initialStore, action) { + switch (action.type) { + case ACTION_TYPES.SAVE_RESTAURANTS: { + const { payload } = action; + + return { + ...state, + error: null, + restaurantsListData: payload, + }; + } + + case ACTION_TYPES.SET_LOAD_RESTAURANTS_ERROR: { + const { payload } = action; + + return { + ...state, + error: payload, + restaurantsListData: null, + }; + } + + case ACTION_TYPES.START_LOADING: { + return { + ...state, + isLoading: true, + }; + } + + case ACTION_TYPES.STOP_LOADING: { + return { + ...state, + isLoading: false, + }; + } + + default: + return state; + } +} diff --git a/src/store/selectors.js b/src/store/selectors.js new file mode 100644 index 0000000..83b038c --- /dev/null +++ b/src/store/selectors.js @@ -0,0 +1,26 @@ +import { createSelector } from 'reselect'; + +const rootSelector = state => state; + +export const selectorRestaurants = createSelector( + rootSelector, + ({ restaurantsListData }) => { + if (!restaurantsListData) { + return []; + } + + const { feedItems, storesMap } = restaurantsListData; + + return feedItems.map(({ uuid }) => storesMap[uuid]); + } +); + +export const selectRestaurantsListError = createSelector( + rootSelector, + ({ error }) => error, +); + +export const selectIsLoading = createSelector( + rootSelector, + ({ isLoading }) => isLoading, +); diff --git a/src/styles/extends.scss b/src/styles/extends.scss new file mode 100644 index 0000000..e89b780 --- /dev/null +++ b/src/styles/extends.scss @@ -0,0 +1,17 @@ +%flex-row-center { + display: flex; + flex-flow: row nowrap; + align-items: center; +} + +%absolute-center { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..b57ccb9 --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,31 @@ +*, +::after, +::before { + box-sizing: border-box; +} + +body, +button, +a, +select, +input { + font-family: Roboto, sans-serif; + font-size: 16px; + line-height: 1.5; +} + +input { + border: none; + background: transparent; + outline: none; +} + +a { + text-decoration: none; +} + +.content { + max-width: 1160px; + padding: 0 34px; + margin: 0 auto; +} diff --git a/src/styles/utils.scss b/src/styles/utils.scss new file mode 100644 index 0000000..9674568 --- /dev/null +++ b/src/styles/utils.scss @@ -0,0 +1,2 @@ +@import './var.scss'; +@import './extends.scss'; diff --git a/src/styles/var.scss b/src/styles/var.scss new file mode 100644 index 0000000..c06a551 --- /dev/null +++ b/src/styles/var.scss @@ -0,0 +1 @@ +$color-green: #5eb707; From 3f2932cfa73d9f45c3c184c7caa19407199901b6 Mon Sep 17 00:00:00 2001 From: youvovk Date: Sun, 10 Nov 2019 18:40:46 +0200 Subject: [PATCH 2/9] add RestaurantPage --- package.json | 5 +- public/images/close.svg | 3 + public/images/location.svg | 4 + public/images/restaurant-hero.svg | 9 ++ public/images/search.svg | 4 +- src/App.js | 42 +++-- src/AppEnhansed.js | 17 ++ src/components/Error/Error.js | 2 +- src/components/Footer/Footer.scss | 58 ++++++- src/components/Header/Header.js | 116 ++++++++++++-- src/components/Header/Header.scss | 69 ++++++++- src/components/Input/Input.js | 64 +++++--- src/components/Input/Input.scss | 32 +++- src/components/Loader/Loader.js | 2 +- .../RestaurantCard/RestaurantCard.js | 25 +-- .../RestaurantCard/RestaurantCard.scss | 12 +- .../RestaurantPage/RestaurantPage.js | 97 ++++++++++++ .../RestaurantPage/RestaurantPage.scss | 146 ++++++++++++++++++ src/components/RestaurantPage/index.js | 27 ++++ .../RestaurantsListPage.scss | 16 +- src/components/Select/Select.js | 10 +- src/index.js | 15 +- src/store/actions.js | 18 +++ src/store/rootReducer.js | 11 ++ src/store/selectors.js | 14 ++ src/styles/index.scss | 16 ++ src/styles/var.scss | 7 + 27 files changed, 759 insertions(+), 82 deletions(-) create mode 100644 public/images/close.svg create mode 100644 public/images/location.svg create mode 100644 public/images/restaurant-hero.svg create mode 100644 src/AppEnhansed.js create mode 100644 src/components/RestaurantPage/RestaurantPage.js create mode 100644 src/components/RestaurantPage/RestaurantPage.scss create mode 100644 src/components/RestaurantPage/index.js diff --git a/package.json b/package.json index 7a590b4..1d75913 100755 --- a/package.json +++ b/package.json @@ -13,11 +13,12 @@ "react": "^16.8.6", "react-dom": "^16.8.6", "react-redux": "^7.1.1", - "react-router-dom": "^5.0.1", + "react-router-dom": "^5.1.2", "react-scripts": "3.0.1", "redux": "^4.0.4", "redux-thunk": "^2.3.0", - "reselect": "^4.0.0" + "reselect": "^4.0.0", + "uuid": "^3.3.3" }, "devDependencies": { "@mate-academy/eslint-config-react": "*", diff --git a/public/images/close.svg b/public/images/close.svg new file mode 100644 index 0000000..b536414 --- /dev/null +++ b/public/images/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/location.svg b/public/images/location.svg new file mode 100644 index 0000000..81764e6 --- /dev/null +++ b/public/images/location.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/restaurant-hero.svg b/public/images/restaurant-hero.svg new file mode 100644 index 0000000..c8eeb1d --- /dev/null +++ b/public/images/restaurant-hero.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/search.svg b/public/images/search.svg index 7e37c73..baeba4c 100644 --- a/public/images/search.svg +++ b/public/images/search.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/App.js b/src/App.js index 9767703..7883daf 100644 --- a/src/App.js +++ b/src/App.js @@ -1,24 +1,40 @@ import React from 'react'; -import { Provider } from 'react-redux'; +import { Route } from 'react-router-dom'; import { RestaurantsListPage, } from './components/RestaurantsListPage/index'; +import { + RestaurantPage, +} from './components/RestaurantPage/index'; import './App.scss'; -import { store } from './store'; import { Header } from './components/Header/Header'; import { Footer } from './components/Footer/Footer'; -export const App = () => ( - -
-
-
- -
-
-
- -); +const uuidv1 = require('uuid/v1'); + +export const App = ({ restaurantsData }) => { + + return ( + <> +
+
+
+ + + +
+
+
+ + ); +}; diff --git a/src/AppEnhansed.js b/src/AppEnhansed.js new file mode 100644 index 0000000..9baa103 --- /dev/null +++ b/src/AppEnhansed.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { App } from './App'; +import { + selectorRestaurants, +} from './store/selectors'; + +function mapState2Props(state) { + return { + restaurantsData: selectorRestaurants(state), + }; +} + +const Enhanced = connect( + mapState2Props, +)(App); + +export { Enhanced as App }; diff --git a/src/components/Error/Error.js b/src/components/Error/Error.js index e156726..8b884a5 100644 --- a/src/components/Error/Error.js +++ b/src/components/Error/Error.js @@ -15,7 +15,7 @@ export const Error = (props) => { Go to Home
- ) + ); }; Error.propTypes = { diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss index a738eb1..db18df9 100644 --- a/src/components/Footer/Footer.scss +++ b/src/components/Footer/Footer.scss @@ -1,9 +1,13 @@ -@import "../../styles//extends.scss"; +@import "../../styles//utils.scss"; .footer { padding: 64px 0; background-color: #262626; + @media (max-width: $break-point-tablet) { + padding: 40px 0; + } + &__top-part { margin-bottom: 40px; padding-bottom: 40px; @@ -20,10 +24,21 @@ @extend %flex-row-center; margin-bottom: 40px; + + @media (max-width: $break-point-tablet) { + flex-direction: column; + align-items: flex-start; + } } &__mobile-app { display: inline-block; + + @media (max-width: $break-point-tablet) { + display: block; + margin-bottom: 20px; + } + &:first-of-type { margin-right: 20px; } @@ -31,27 +46,47 @@ &__logo { margin-right: 40px; + + @media (max-width: $break-point-tablet) { + margin-bottom: 40px; + } } &__top-part, &__bottom-part { @extend %flex-row-center; align-items: flex-start; + + @media (max-width: $break-point-phone-lg) { + flex-direction: column; + } } &__copyright, &__main { flex-basis: 45vw; padding-right: 40px; + + @media (max-width: $break-point-phone-lg) { + flex-basis: initial; + } } &__top-links:last-of-type { margin-left: 10vw; + + @media (max-width: $break-point-phone-lg) { + margin-left: 0; + } } &__copyright { color: #fff; font-size: 14px; + + @media (max-width: $break-point-phone-lg) { + order: 1; + } } &__bottom-links { @@ -59,10 +94,25 @@ margin-bottom: 40px; + @media (max-width: $break-point-phone-lg) { + flex-direction: column; + align-items: flex-start; + margin-top: 20px; + order: 1; + } + .links__link:not(:last-of-type) { margin-right: 40px; } } + + &__misc { + @media (max-width: $break-point-phone-lg) { + flex-direction: column; + display: flex; + margin-top: 20px; + } + } } .links { @@ -79,6 +129,12 @@ } } +.social { + &__link { + margin-right: 20px; + } +} + .social__link { margin-right: 20px; } diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 4fbd552..bb59ed0 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -8,6 +8,8 @@ export class Header extends Component { address: '', time: '', search: '', + isMobileSearchVisible: false, + isMobileDeliveryInfoVisible: false, }; handleChange = ({ target }) => { @@ -16,18 +18,41 @@ export class Header extends Component { }); }; + toggleSearch = () => this.setState(({ isMobileSearchVisible }) => ({ + isMobileSearchVisible: !isMobileSearchVisible, + isMobileDeliveryInfoVisible: false, + })); + + toggleDeliveryInfo = () => this.setState(({ + isMobileDeliveryInfoVisible, + }) => ({ + isMobileDeliveryInfoVisible: !isMobileDeliveryInfoVisible, + isMobileSearchVisible: false, + })); + + closeMobile = () => this.setState({ + isMobileSearchVisible: false, + isMobileDeliveryInfoVisible: false, + }); + render() { const { address, time, search, + isMobileSearchVisible, + isMobileDeliveryInfoVisible, } = this.state; return (
); diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss index adab643..60fcc14 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.scss @@ -1,9 +1,11 @@ -@import "../../styles/extends.scss"; +@import "../../styles/utils.scss"; .header { position: sticky; z-index: 1; top: 0; + + width: 100%; padding: 16px 0; background-color: #fff; @@ -12,14 +14,31 @@ @extend %flex-row-center; } + &__logo { + height: 24px; + width: auto; + + @media (max-width: $break-point-phone) { + height: 14px; + } + } + &__delivery-info { @extend %flex-row-center; margin-left: 7vw; + + @media (max-width: $break-point-tablet) { + display: none; + } } &__search { margin-left: auto; + + @media (max-width: $break-point-tablet) { + display: none; + } } &__link { @@ -33,4 +52,52 @@ text-decoration: underline; } } + + &__toggle-buttons { + display: none; + margin-left: auto; + + cursor: pointer; + + @media (max-width: $break-point-tablet) { + display: flex; + } + } + + &__toggle-btn { + display: flex; + align-items: center; + padding: 11px 5px; + + cursor: pointer; + border: 1px solid transparent; + + &:not(:last-of-type) { + margin-right: 20px; + } + + &:focus { + border: 1px solid $color-gray; + } + } +} + +.mobile-controls { + display: none; + position: relative; + + padding-top: 30px; + + @media (max-width: $break-point-tablet) { + display: flex; + } + + &__close { + position: absolute; + + top: 10px; + right: 0; + + cursor: pointer; + } } diff --git a/src/components/Input/Input.js b/src/components/Input/Input.js index 341bc7c..e63956a 100644 --- a/src/components/Input/Input.js +++ b/src/components/Input/Input.js @@ -26,41 +26,51 @@ export class Input extends PureComponent { placeholder, name, className, + isSmall, + label, } = this.props; const { isFocused } = this.state; - const rootClass = cx('control', { - 'control--focused': isFocused, + const inputWrappeClass = cx('control__input-wrapper', { + 'control__input-wrapper--focused': isFocused, [className]: !!className, }); + const inputClass = cx('control_input', { + 'control_input--small': isSmall, + 'control_input--time': type === 'time', + }); + return ( -
- {!!iconUrl && ( - {placeholder} +
+ ); } } @@ -73,6 +83,8 @@ Input.propTypes = { placeholder: PropTypes.string, iconUrl: PropTypes.string, className: PropTypes.string, + isSmall: PropTypes.bool, + label: PropTypes.string, }; Input.defaultProps = { @@ -80,4 +92,6 @@ Input.defaultProps = { placeholder: '', iconUrl: '', className: '', + label: '', + isSmall: true, }; diff --git a/src/components/Input/Input.scss b/src/components/Input/Input.scss index 9e228fc..3a24418 100644 --- a/src/components/Input/Input.scss +++ b/src/components/Input/Input.scss @@ -1,16 +1,17 @@ -@import "../../styles/extends.scss"; +@import "../../styles/utils.scss"; .control { - $border-color: #e0e0e0; - @extend %flex-row-center; + &__input-wrapper { + @extend %flex-row-center; - padding: 11px 16px; + padding: 11px 16px; - border: 1px solid $border-color; - transition: border-color 300ms; + border: 1px solid $color-gray; + transition: border-color 300ms; + } &--focused { - border-color: darken($border-color, 20%); + border-color: darken($color-gray, 20%); } &__icon { @@ -22,6 +23,23 @@ &__input { max-height: 24px; line-height: 24px; + + &--time { + min-width: 75px; + } + + &--small { + @media (max-width: $break-point-tablet-lg) { + max-width: 75px; + } + } + } + + &__label { + font-size: 14px; + color: #626262; + + margin-bottom: 8px; } & + & { diff --git a/src/components/Loader/Loader.js b/src/components/Loader/Loader.js index 13db96a..f999695 100644 --- a/src/components/Loader/Loader.js +++ b/src/components/Loader/Loader.js @@ -4,7 +4,7 @@ import './Loader.scss'; export const Loader = () => (
-
+
diff --git a/src/components/RestaurantCard/RestaurantCard.js b/src/components/RestaurantCard/RestaurantCard.js index 4841f9e..0864594 100644 --- a/src/components/RestaurantCard/RestaurantCard.js +++ b/src/components/RestaurantCard/RestaurantCard.js @@ -1,10 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; import './RestaurantCard.scss'; -// heroImageUrl, title, categories, etaRange.errorMessage, uuid - export const RestaurantsCard = (props) => { const { imageUrl, @@ -15,16 +14,18 @@ export const RestaurantsCard = (props) => { } = props; return ( -
- {title} -

{title}

-
- {categories.join(' • ')} -
-
- {etaRange} + +
+ {title} +

{title}

+
+ {categories.join(' • ')} +
+
+ {etaRange} +
-
+ ); }; @@ -33,7 +34,7 @@ RestaurantsCard.propTypes = { title: PropTypes.string.isRequired, categories: PropTypes.arrayOf(PropTypes.string), etaRange: PropTypes.string, - //uuid: PropTypes.string.isRequired, + uuid: PropTypes.string.isRequired, }; RestaurantsCard.defaultProps = { diff --git a/src/components/RestaurantCard/RestaurantCard.scss b/src/components/RestaurantCard/RestaurantCard.scss index 3b1add1..667c8af 100644 --- a/src/components/RestaurantCard/RestaurantCard.scss +++ b/src/components/RestaurantCard/RestaurantCard.scss @@ -1,13 +1,19 @@ +@import './../../styles/utils.scss'; + .restaurant-card { - $height: 367px; + --height: 367px; - height: $height; + height: var(--height); width: 100%; cursor: pointer; + @media (max-width: $break-point-phone) { + --height: 330px; + } + &__img { - height: $height * 0.7; + height: calc(var(--height) * 0.7); width: 100%; object-fit: cover; } diff --git a/src/components/RestaurantPage/RestaurantPage.js b/src/components/RestaurantPage/RestaurantPage.js new file mode 100644 index 0000000..ec4ac8c --- /dev/null +++ b/src/components/RestaurantPage/RestaurantPage.js @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './RestaurantPage.scss'; + +export class RestaurantPage extends Component { + componentDidMount() { + const { loadRestaurant, match } = this.props; + + loadRestaurant(match.params.id); + } + + render() { + console.log(this.props.restaurantData) + const { restaurantData: { + categories, + title, + location, + sectionsMap, + entitiesMap, + heroImageUrls, + priceBucket, + }, + } = this.props; + + const entitiesMapToArray = entitiesMap && Object.entries(entitiesMap); + + return ( +
+
+ + + +
+ {sectionsMap && Object.entries(sectionsMap).map((item) => { + + return ( +
+

{item[1].title}

+
+ {item[1].itemUuids.map((itemMenu) => { + const foundItem = entitiesMapToArray.find(elem => elem[0] === itemMenu); + + return ( +
+
+

{foundItem[1].title}

+

{foundItem[1].description}

+

{`${priceBucket}${foundItem[1].price}`}

+
+ +
+ +
+
+ ); + })} +
+
+ ); + })} +
+
+ ); + } +} + +RestaurantPage.propTypes = { + categories: PropTypes.arrayOf().isRequired, + title: PropTypes.string.isRequired, + location: PropTypes.string.isRequired, + sectionsMap: PropTypes.shape({}).isRequired, + entitiesMap: PropTypes.shape({}).isRequired, + heroImageUrls: PropTypes.arrayOf().isRequired, + priceBucket: PropTypes.string.isRequired, +}; + +RestaurantPage.defaultProps = { + +}; diff --git a/src/components/RestaurantPage/RestaurantPage.scss b/src/components/RestaurantPage/RestaurantPage.scss new file mode 100644 index 0000000..382f6ae --- /dev/null +++ b/src/components/RestaurantPage/RestaurantPage.scss @@ -0,0 +1,146 @@ +.restaurant{ + &-title { + font-weight: 500; + font-size: 20px; + color: #000; + + margin-bottom: 20px; + } + + &__menu { + + &-items { + display: flex; + overflow-x: scroll; + + margin: 30px 0 40px; + } + } + + &__items { + margin-bottom: 40px; + } +} + +.menu { + &__item { + + &:last-child { + margin-right: 0; + } + + &-btn { + position: relative; + height: 42px; + margin-right: 24px; + + font-size: 14px; + color: #000; + + white-space: nowrap; + transition: color 300ms; + + &:hover { + color: #5FB709; + } + + &:hover::after { + content: ''; + position: absolute; + height: 1px; + width: 100%; + background-color: #000; + bottom: 0; + left: 0; + + animation: menu-hover-animation 0.3s; + } + } + } +} + +.container { + --item-width: 348px; + + display: grid; + grid-template-columns: repeat(auto-fill, minmax(348px, 1fr)); + gap: 40px 20px; +} + +.item { + display: flex; + justify-content: space-between; + height: 158px; + + box-sizing: border-box; + + border: 1px solid #e0e0e0; + + &__left { + display: flex; + flex-direction: column; + justify-content: space-between; + + padding: 0 16px; + } + + &__title { + margin-bottom: 4px; + padding-top: 16px; + + font-size: 16px; + font-weight: 500; + line-height: 21px; + + color: #000; + } + + &__description { + display: inline-block; + + overflow: hidden; + text-overflow: ellipsis; + + margin-bottom: 17px; + + font-size: 14px; + line-height: 21px; + + color: #626262; + } + + &__price { + + + padding-bottom: 16px; + + font-size: 16px; + font-weight: 500; + line-height: 21px; + + color: #000; + } + + &__img { + max-width: 156px; + height: 158px; + } +} + +@keyframes menu-hover-animation { + 0% { + width: 0%; + } + 25% { + width: 25%; + } + 50% { + width: 50%; + } + 75% { + width: 75%; + } + 100% { + width: 100%; + } +} diff --git a/src/components/RestaurantPage/index.js b/src/components/RestaurantPage/index.js new file mode 100644 index 0000000..6f6ff2c --- /dev/null +++ b/src/components/RestaurantPage/index.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { RestaurantPage } from './RestaurantPage'; +import { loadRestaurant } from '../../store/actions'; +import { + selectorRestaurant, + selectIsLoading, + selectRestaurantsListError, +} from '../../store/selectors'; + +function mapState2Props(state) { + return { + restaurantData: selectorRestaurant(state), + }; +} + +const mapDispatch2Props = (dispatch) => { + return ({ + loadRestaurant: uuid => dispatch(loadRestaurant(uuid)), + }); +}; + +const Enhanced = connect( + mapState2Props, + mapDispatch2Props, +)(RestaurantPage); + +export { Enhanced as RestaurantPage }; diff --git a/src/components/RestaurantsListPage/RestaurantsListPage.scss b/src/components/RestaurantsListPage/RestaurantsListPage.scss index a87ec4f..e193d12 100644 --- a/src/components/RestaurantsListPage/RestaurantsListPage.scss +++ b/src/components/RestaurantsListPage/RestaurantsListPage.scss @@ -1,5 +1,19 @@ +@import '../../styles/utils.scss'; + .restaurants-list { + --card-width: 348px; + display: grid; - grid-template-columns: repeat(auto-fill, minmax(348px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(--card-width, 1fr)); gap: 40px 20px; + + @media (max-width: $break-point-tablet-lg) { + --card-width: 340px; + + row-gap: 20px; + } + + @media (max-width: $break-point-phone) { + --card-width: 280px; + } } diff --git a/src/components/Select/Select.js b/src/components/Select/Select.js index 7759b34..3cc5f80 100644 --- a/src/components/Select/Select.js +++ b/src/components/Select/Select.js @@ -2,6 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import './Select.scss'; +const uuidv1 = require('uuid/v1'); + export const Select = (props) => { const { name, @@ -19,8 +21,12 @@ export const Select = (props) => { onChange={onSelect} className="select__input" > - {options.map(({ value: optionValue, label }) => ( - ))} diff --git a/src/index.js b/src/index.js index 4a2cf0e..ab657f2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,18 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { App } from './App'; +import { Provider } from 'react-redux'; +import { HashRouter } from 'react-router-dom'; +import { App } from './AppEnhansed'; + +import { store } from './store'; import './styles/index.scss'; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + + + + + , + document.getElementById('root') +); diff --git a/src/store/actions.js b/src/store/actions.js index d2ab202..11c4da0 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -1,11 +1,17 @@ export const ACTION_TYPES = { SAVE_RESTAURANTS: 'SAVE_RESTAURANTS', + SAVE_RESTAURANT: 'SAVE_RESTAURANT', SET_LOAD_RESTAURANTS_ERROR: 'SET_LOAD_RESTAURANTS_ERROR', START_LOADING: 'START_LOADING', STOP_LOADING: 'STOP_LOADING', }; +const saveRestaurant = data => ({ + type: ACTION_TYPES.SAVE_RESTAURANT, + payload: data, +}); + const saveRestaurants = data => ({ type: ACTION_TYPES.SAVE_RESTAURANTS, payload: data, @@ -33,3 +39,15 @@ export const loadRestaurants = () => (dispatch) => { .catch(error => dispatch(setRestaurantsError(error.massage))) .finally(() => dispatch(stopLoading())); }; + +export const loadRestaurant = uuid => (dispatch) => { + dispatch(startLoading()); + + fetch(`https://mate-uber-eats-api.herokuapp.com/api/v1/restaurants/${uuid}`) + .then(res => res.json()) + .then(({ data }) => { + dispatch(saveRestaurant(data)); + }) + .catch(error => dispatch(setRestaurantsError(error.massage))) + .finally(() => dispatch(stopLoading())); +}; diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js index 1b8259d..2602a07 100644 --- a/src/store/rootReducer.js +++ b/src/store/rootReducer.js @@ -4,6 +4,7 @@ const initialStore = { restaurantsListData: null, isLoading: false, error: null, + restaurantData: null, }; export function rootReducer(state = initialStore, action) { @@ -18,6 +19,16 @@ export function rootReducer(state = initialStore, action) { }; } + case ACTION_TYPES.SAVE_RESTAURANT: { + const { payload } = action; + + return { + ...state, + error: null, + restaurantData: payload, + }; + } + case ACTION_TYPES.SET_LOAD_RESTAURANTS_ERROR: { const { payload } = action; diff --git a/src/store/selectors.js b/src/store/selectors.js index 83b038c..acef457 100644 --- a/src/store/selectors.js +++ b/src/store/selectors.js @@ -15,6 +15,20 @@ export const selectorRestaurants = createSelector( } ); +export const selectorRestaurant = createSelector( + rootSelector, + ({ restaurantData }) => { + if (!restaurantData) { + return []; + } + + const { feedItems, storesMap } = restaurantData; + + //return feedItems.map(({ uuid }) => storesMap[uuid]); + return restaurantData; + } +); + export const selectRestaurantsListError = createSelector( rootSelector, ({ error }) => error, diff --git a/src/styles/index.scss b/src/styles/index.scss index b57ccb9..11ec9d2 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,3 +1,5 @@ +@import './utils.scss'; + *, ::after, ::before { @@ -24,8 +26,22 @@ a { text-decoration: none; } +button { + border: none; + background: none; + outline: none; +} + .content { max-width: 1160px; padding: 0 34px; margin: 0 auto; + + @media (max-width: $break-point-phone) { + padding: 0 20px; + } + + @media (min-width: $break-point-large) { + max-width: 1520px; + } } diff --git a/src/styles/var.scss b/src/styles/var.scss index c06a551..1607bc5 100644 --- a/src/styles/var.scss +++ b/src/styles/var.scss @@ -1 +1,8 @@ $color-green: #5eb707; +$color-gray: #e0e0e0; + +$break-point-large: 1281px; +$break-point-tablet-lg: 956px; +$break-point-phone-lg: 620px; +$break-point-tablet: 768px; +$break-point-phone: 550px; From 713be003764d7c6de79d1d3fb16c46ac0375ea19 Mon Sep 17 00:00:00 2001 From: youvovk Date: Sun, 10 Nov 2019 22:50:21 +0200 Subject: [PATCH 3/9] add RestaurantPage --- src/App.js | 2 +- .../RestaurantPage/RestaurantPage.js | 124 +++++++++++------- .../RestaurantPage/RestaurantPage.scss | 11 +- .../RestaurantsListPage.js | 42 +++--- 4 files changed, 110 insertions(+), 69 deletions(-) diff --git a/src/App.js b/src/App.js index 7883daf..4b164dc 100644 --- a/src/App.js +++ b/src/App.js @@ -21,7 +21,7 @@ export const App = ({ restaurantsData }) => { <>
-
+
{ + let foundItem = false; + + item[1].itemUuids.forEach((itemMenu) => { + if (array.some(elem => elem[0] === itemMenu)) { + foundItem = true; + } + }); + + return foundItem; +}; + export class RestaurantPage extends Component { componentDidMount() { const { loadRestaurant, match } = this.props; @@ -11,7 +23,6 @@ export class RestaurantPage extends Component { } render() { - console.log(this.props.restaurantData) const { restaurantData: { categories, title, @@ -22,60 +33,79 @@ export class RestaurantPage extends Component { priceBucket, }, } = this.props; - + const hero = heroImageUrls && heroImageUrls[heroImageUrls.length - 1].url; const entitiesMapToArray = entitiesMap && Object.entries(entitiesMap); return (
-
+
+
+ +
+
+ +
+ - -
- {sectionsMap && Object.entries(sectionsMap).map((item) => { - - return ( -
-

{item[1].title}

-
- {item[1].itemUuids.map((itemMenu) => { - const foundItem = entitiesMapToArray.find(elem => elem[0] === itemMenu); - - return ( -
-
-

{foundItem[1].title}

-

{foundItem[1].description}

-

{`${priceBucket}${foundItem[1].price}`}

-
+
+

{item[1].title}

+
+ {item[1].itemUuids.map((itemMenu) => { + const foundItem = entitiesMapToArray.find(elem => elem[0] === itemMenu); -
- + return ( +
+
+

+ {foundItem && foundItem[1].title} +

+

+ {foundItem && foundItem[1].description} +

+

+ {`${priceBucket.length > 2 ? priceBucket.replace(/[^0-9]/, '') : priceBucket} ${foundItem && foundItem[1].price}`} +

+
+ +
+ +
-
- ); - })} -
-
- ); - })} + ); + })} +
+
+ ); + })} +
); @@ -89,9 +119,9 @@ RestaurantPage.propTypes = { sectionsMap: PropTypes.shape({}).isRequired, entitiesMap: PropTypes.shape({}).isRequired, heroImageUrls: PropTypes.arrayOf().isRequired, - priceBucket: PropTypes.string.isRequired, + priceBucket: PropTypes.string, }; RestaurantPage.defaultProps = { - + priceBucket: '', }; diff --git a/src/components/RestaurantPage/RestaurantPage.scss b/src/components/RestaurantPage/RestaurantPage.scss index 382f6ae..b2a543e 100644 --- a/src/components/RestaurantPage/RestaurantPage.scss +++ b/src/components/RestaurantPage/RestaurantPage.scss @@ -70,6 +70,7 @@ .item { display: flex; justify-content: space-between; + height: 158px; box-sizing: border-box; @@ -81,6 +82,8 @@ flex-direction: column; justify-content: space-between; + width: 50%; + padding: 0 16px; } @@ -121,9 +124,15 @@ color: #000; } + &-right { + display: flex; + } + &__img { max-width: 156px; - height: 158px; + height: 155px; + + align-self: center; } } diff --git a/src/components/RestaurantsListPage/RestaurantsListPage.js b/src/components/RestaurantsListPage/RestaurantsListPage.js index ec3fce0..8f0cc28 100644 --- a/src/components/RestaurantsListPage/RestaurantsListPage.js +++ b/src/components/RestaurantsListPage/RestaurantsListPage.js @@ -32,27 +32,29 @@ export class RestaurantsListPage extends Component { } return ( -
- {restaurantsData.map((restaurant) => { - const { - uuid, - title, - heroImageUrl, - categories, - etaRange, - } = restaurant; +
+
+ {restaurantsData.map((restaurant) => { + const { + uuid, + title, + heroImageUrl, + categories, + etaRange, + } = restaurant; - return ( - - ); - })} + return ( + + ); + })} +
); } From 6e7e2829fe7227cd36ff75a66954ac349a94d727 Mon Sep 17 00:00:00 2001 From: youvovk Date: Mon, 11 Nov 2019 00:00:58 +0200 Subject: [PATCH 4/9] add fix media --- .../RestaurantsListPage/RestaurantsListPage.scss | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/RestaurantsListPage/RestaurantsListPage.scss b/src/components/RestaurantsListPage/RestaurantsListPage.scss index e193d12..1972844 100644 --- a/src/components/RestaurantsListPage/RestaurantsListPage.scss +++ b/src/components/RestaurantsListPage/RestaurantsListPage.scss @@ -1,19 +1,17 @@ @import '../../styles/utils.scss'; .restaurants-list { - --card-width: 348px; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(--card-width, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(348px, 1fr)); gap: 40px 20px; @media (max-width: $break-point-tablet-lg) { - --card-width: 340px; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); row-gap: 20px; } @media (max-width: $break-point-phone) { - --card-width: 280px; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } } From 438414eb952c358847580624f257a397a99fa49b Mon Sep 17 00:00:00 2001 From: youvovk Date: Mon, 11 Nov 2019 20:36:30 +0200 Subject: [PATCH 5/9] add hero title --- .../RestaurantPage/RestaurantPage.js | 15 ++++++- .../RestaurantPage/RestaurantPage.scss | 42 +++++++++++++++++++ src/styles/index.scss | 2 + 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/components/RestaurantPage/RestaurantPage.js b/src/components/RestaurantPage/RestaurantPage.js index 6b0e71c..3c00d7d 100644 --- a/src/components/RestaurantPage/RestaurantPage.js +++ b/src/components/RestaurantPage/RestaurantPage.js @@ -39,8 +39,19 @@ export class RestaurantPage extends Component { return (
-
- +
+ +
+ +
+
+

{title}

+

{categories}

+
+

{location && location.address}

+ More info +
+
diff --git a/src/components/RestaurantPage/RestaurantPage.scss b/src/components/RestaurantPage/RestaurantPage.scss index b2a543e..829619f 100644 --- a/src/components/RestaurantPage/RestaurantPage.scss +++ b/src/components/RestaurantPage/RestaurantPage.scss @@ -1,3 +1,45 @@ +.restaurant-page { + &__img-wrapper { + height: 380px; + width: 100%; + overflow: hidden; + } + + &__img { + width: 100%; + } + + &__title { + position: absolute; + top: -250px; + + background-color: #fff; + } + + .title { + font-size: 36px; + line-height: 150%; + color: #1d1d1d; + } + + .categories { + font-size: 14px; + line-height: 21px; + color: #757575; + } + + .location { + font-size: 14px; + line-height: 150%; + color: #000; + + &-wrapper { + display: flex; + justify-content: space-between; + } + } +} + .restaurant{ &-title { font-weight: 500; diff --git a/src/styles/index.scss b/src/styles/index.scss index 11ec9d2..02fe443 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -33,6 +33,8 @@ button { } .content { + position: relative; + max-width: 1160px; padding: 0 34px; margin: 0 auto; From 8c5d6c53475f2412e04350bb33b909e1da24cdaf Mon Sep 17 00:00:00 2001 From: youvovk Date: Sun, 1 Dec 2019 15:49:08 +0200 Subject: [PATCH 6/9] add modal window --- src/App.js | 10 ++++-- src/AppEnhansed.js | 1 + src/components/Header/Header.scss | 2 +- src/components/ModalWindow/ModalWindow.js | 14 ++++++++ src/components/ModalWindow/ModalWindow.scss | 23 +++++++++++++ src/components/ModalWindow/index.js | 0 .../RestaurantPage/RestaurantPage.js | 32 ++++++++++------- .../RestaurantPage/RestaurantPage.scss | 34 +++++++++++++++++-- src/components/RestaurantPage/index.js | 3 +- src/components/index.js | 0 src/store/actions.js | 7 +++- src/store/rootReducer.js | 8 +++++ 12 files changed, 115 insertions(+), 19 deletions(-) create mode 100644 src/components/ModalWindow/ModalWindow.js create mode 100644 src/components/ModalWindow/ModalWindow.scss create mode 100644 src/components/ModalWindow/index.js create mode 100644 src/components/index.js diff --git a/src/App.js b/src/App.js index 4b164dc..0454cd9 100644 --- a/src/App.js +++ b/src/App.js @@ -7,6 +7,7 @@ import { import { RestaurantPage, } from './components/RestaurantPage/index'; +import { ModalWindow } from './components/ModalWindow/ModalWindow'; import './App.scss'; @@ -15,10 +16,15 @@ import { Footer } from './components/Footer/Footer'; const uuidv1 = require('uuid/v1'); -export const App = ({ restaurantsData }) => { - +export const App = ({ restaurantsData, openedModalWindow }) => { return ( <> + {openedModalWindow && ( + + {/* */} + + )} +
diff --git a/src/AppEnhansed.js b/src/AppEnhansed.js index 9baa103..1606dda 100644 --- a/src/AppEnhansed.js +++ b/src/AppEnhansed.js @@ -7,6 +7,7 @@ import { function mapState2Props(state) { return { restaurantsData: selectorRestaurants(state), + openedModalWindow: state.openedModalWindow, }; } diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss index 60fcc14..90558dd 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.scss @@ -2,7 +2,7 @@ .header { position: sticky; - z-index: 1; + z-index: 2; top: 0; width: 100%; diff --git a/src/components/ModalWindow/ModalWindow.js b/src/components/ModalWindow/ModalWindow.js new file mode 100644 index 0000000..10ba3c3 --- /dev/null +++ b/src/components/ModalWindow/ModalWindow.js @@ -0,0 +1,14 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './ModalWindow.scss'; + +export const ModalWindow = () => { + return ( +
+
+ +
+
+ ); +} diff --git a/src/components/ModalWindow/ModalWindow.scss b/src/components/ModalWindow/ModalWindow.scss new file mode 100644 index 0000000..f515bfd --- /dev/null +++ b/src/components/ModalWindow/ModalWindow.scss @@ -0,0 +1,23 @@ +.modal-window { + &__container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0,0,0,.9); + position: fixed; + top: 0; + bottom: 0; + z-index: 4; + padding-top: 80px; + } + + &__content { + width: 716px; + height: 100%; + overflow-y: scroll; + margin: 0 auto 80px; + background-color: #fff; + } +} diff --git a/src/components/ModalWindow/index.js b/src/components/ModalWindow/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/RestaurantPage/RestaurantPage.js b/src/components/RestaurantPage/RestaurantPage.js index 3c00d7d..f43b1f9 100644 --- a/src/components/RestaurantPage/RestaurantPage.js +++ b/src/components/RestaurantPage/RestaurantPage.js @@ -23,7 +23,9 @@ export class RestaurantPage extends Component { } render() { - const { restaurantData: { + const { + openModalWindow, + restaurantData: { categories, title, location, @@ -31,8 +33,8 @@ export class RestaurantPage extends Component { entitiesMap, heroImageUrls, priceBucket, - }, - } = this.props; + }, + } = this.props; const hero = heroImageUrls && heroImageUrls[heroImageUrls.length - 1].url; const entitiesMapToArray = entitiesMap && Object.entries(entitiesMap); @@ -41,18 +43,24 @@ export class RestaurantPage extends Component {
-
- -
@@ -93,7 +101,7 @@ export class RestaurantPage extends Component { const foundItem = entitiesMapToArray.find(elem => elem[0] === itemMenu); return ( -
+
openModalWindow()}>

{foundItem && foundItem[1].title} diff --git a/src/components/RestaurantPage/RestaurantPage.scss b/src/components/RestaurantPage/RestaurantPage.scss index 829619f..90585f4 100644 --- a/src/components/RestaurantPage/RestaurantPage.scss +++ b/src/components/RestaurantPage/RestaurantPage.scss @@ -3,6 +3,7 @@ height: 380px; width: 100%; overflow: hidden; + position: relative; } &__img { @@ -11,15 +12,34 @@ &__title { position: absolute; - top: -250px; + top: 25%; + left: 0; + + padding: 24px 40px 24px 98px; + + z-index: 1; background-color: #fff; + + @media (min-width: 1500px) { + padding-left: 264px; + } + + &:nth-child(even), + &:nth-child(odd) { + padding-bottom: 18px; + } + } + + .page__title_indent { + margin-bottom: 18px; } .title { font-size: 36px; line-height: 150%; color: #1d1d1d; + font-weight: 500; } .categories { @@ -35,7 +55,17 @@ &-wrapper { display: flex; - justify-content: space-between; + color: #000; + } + + &__info { + font-size: 14px; + color: #247a00; + } + + &__dot { + color: #000; + margin: 0 5px; } } } diff --git a/src/components/RestaurantPage/index.js b/src/components/RestaurantPage/index.js index 6f6ff2c..69bf5de 100644 --- a/src/components/RestaurantPage/index.js +++ b/src/components/RestaurantPage/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { RestaurantPage } from './RestaurantPage'; -import { loadRestaurant } from '../../store/actions'; +import { loadRestaurant, openModalWindow } from '../../store/actions'; import { selectorRestaurant, selectIsLoading, @@ -16,6 +16,7 @@ function mapState2Props(state) { const mapDispatch2Props = (dispatch) => { return ({ loadRestaurant: uuid => dispatch(loadRestaurant(uuid)), + openModalWindow: () => dispatch(openModalWindow()), }); }; diff --git a/src/components/index.js b/src/components/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/store/actions.js b/src/store/actions.js index 11c4da0..7ec98ab 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -4,9 +4,14 @@ export const ACTION_TYPES = { SET_LOAD_RESTAURANTS_ERROR: 'SET_LOAD_RESTAURANTS_ERROR', START_LOADING: 'START_LOADING', STOP_LOADING: 'STOP_LOADING', - + OPEN_MODAL_WINDOW: 'OPEN_MODAL_WINDOW', }; +export const openModalWindow = data => ({ + type: ACTION_TYPES.OPEN_MODAL_WINDOW, + payload: data, +}); + const saveRestaurant = data => ({ type: ACTION_TYPES.SAVE_RESTAURANT, payload: data, diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js index 2602a07..72b6ccd 100644 --- a/src/store/rootReducer.js +++ b/src/store/rootReducer.js @@ -5,10 +5,18 @@ const initialStore = { isLoading: false, error: null, restaurantData: null, + openedModalWindow: false, }; export function rootReducer(state = initialStore, action) { switch (action.type) { + case ACTION_TYPES.OPEN_MODAL_WINDOW: { + return { + ...state, + openedModalWindow: true, + }; + } + case ACTION_TYPES.SAVE_RESTAURANTS: { const { payload } = action; From 3b19e2bc9cd1c1e5ce6751dcddc30c91406a5a0f Mon Sep 17 00:00:00 2001 From: youvovk Date: Sun, 1 Dec 2019 16:28:43 +0200 Subject: [PATCH 7/9] add modal window --- src/components/ModalWindow/ModalWindow.js | 28 ++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/ModalWindow/ModalWindow.js b/src/components/ModalWindow/ModalWindow.js index 10ba3c3..ca85887 100644 --- a/src/components/ModalWindow/ModalWindow.js +++ b/src/components/ModalWindow/ModalWindow.js @@ -6,8 +6,34 @@ import './ModalWindow.scss'; export const ModalWindow = () => { return (
-
+
+ +

STAR CHEESE Burger

+

Cheddar cheese, Boston lettuce, tomatoes, onion, pickles and firm sauce 275 g.

+
+

Double Meat

+

Choose up to 1

+
+
+
+ +

Cheddar

+
+

+£0.68

+
+

Special instructions

+ + +
+
+

1

+
+
+

Add 1 to order

+

139,00 UAH

+
+
); From 94b2931c0f00741b923c2684bd1ab9965248d479 Mon Sep 17 00:00:00 2001 From: youvovk Date: Sun, 1 Dec 2019 21:39:24 +0200 Subject: [PATCH 8/9] add modal window content --- public/images/modal-img.svg | 9 ++ src/components/ModalWindow/ModalWindow.js | 41 ++--- src/components/ModalWindow/ModalWindow.scss | 168 ++++++++++++++++++++ 3 files changed, 200 insertions(+), 18 deletions(-) create mode 100644 public/images/modal-img.svg diff --git a/public/images/modal-img.svg b/public/images/modal-img.svg new file mode 100644 index 0000000..4a97e14 --- /dev/null +++ b/public/images/modal-img.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/ModalWindow/ModalWindow.js b/src/components/ModalWindow/ModalWindow.js index ca85887..9bbbc11 100644 --- a/src/components/ModalWindow/ModalWindow.js +++ b/src/components/ModalWindow/ModalWindow.js @@ -7,31 +7,36 @@ export const ModalWindow = () => { return (
- -

STAR CHEESE Burger

-

Cheddar cheese, Boston lettuce, tomatoes, onion, pickles and firm sauce 275 g.

-
-

Double Meat

-

Choose up to 1

+ +
+

STAR CHEESE Burger

+

Cheddar cheese, Boston lettuce, tomatoes, onion, pickles and firm sauce 275 g.

-
-
- + +
+

Double Meat

+

Choose up to 1

+
+
+
+

Cheddar

-

+£0.68

+

+£0.68

-

Special instructions

- +

Special instructions

+