From 8a879c670ededc9967b73c090feef8043db754d5 Mon Sep 17 00:00:00 2001 From: igaryok Date: Tue, 15 Oct 2019 14:31:38 +0300 Subject: [PATCH 01/34] part solution --- .eslintrc.js | 3 + package.json | 8 +- public/images/app-store.png | Bin 0 -> 3658 bytes public/images/arrow-down.svg | 3 + public/images/fb.svg | 3 + public/images/google-play.png | Bin 0 -> 4109 bytes public/images/ig.svg | 5 + public/images/logo-white.svg | 4 + public/images/logo.svg | 4 + public/images/place.svg | 4 + public/images/search.svg | 3 + public/images/tw.svg | 3 + public/images/world.svg | 3 + public/index.html | 10 ++ src/App.css | 1 - src/App.js | 19 ++- src/App.scss | 6 + src/components/Error/Error.js | 27 +++++ src/components/Error/Error.scss | 20 ++++ src/components/Error/index.js | 3 + src/components/Footer/Footer.js | 110 ++++++++++++++++++ src/components/Footer/Footer.scss | 86 ++++++++++++++ src/components/Footer/index.js | 3 + src/components/Header/Header.js | 88 ++++++++++++++ src/components/Header/Header.scss | 66 +++++++++++ src/components/Header/index.js | 3 + src/components/Input/Input.js | 81 +++++++++++++ src/components/Input/Input.scss | 34 ++++++ src/components/Input/index.js | 3 + src/components/Loader/Loader.js | 21 ++++ src/components/Loader/Loader.scss | 105 +++++++++++++++++ src/components/Loader/index.js | 3 + .../RestaurantCard/RestaurantCard.js | 39 +++++++ .../RestaurantCard/RestaurantCard.scss | 34 ++++++ src/components/RestaurantCard/index.js | 3 + .../RestaurantListPage.scss | 5 + .../RestaurantsListPage.js | 70 +++++++++++ src/components/RestaurantsListPage/index.js | 25 ++++ src/components/Select/Select.js | 60 ++++++++++ src/components/Select/Select.scss | 40 +++++++ src/components/Select/index.js | 3 + src/index.js | 6 +- src/store/actions.js | 33 ++++++ src/store/index.js | 11 ++ src/store/rootReducer.js | 48 ++++++++ src/store/selector.js | 26 +++++ src/styles/extends.scss | 17 +++ src/styles/index.scss | 37 ++++++ src/styles/utils.scss | 2 + src/styles/variables.scss | 5 + 50 files changed, 1189 insertions(+), 7 deletions(-) create mode 100644 public/images/app-store.png create mode 100644 public/images/arrow-down.svg create mode 100644 public/images/fb.svg create mode 100644 public/images/google-play.png create mode 100644 public/images/ig.svg create mode 100644 public/images/logo-white.svg create mode 100644 public/images/logo.svg create mode 100644 public/images/place.svg create mode 100644 public/images/search.svg create mode 100644 public/images/tw.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/RestaurantListPage.scss create mode 100644 src/components/RestaurantsListPage/RestaurantsListPage.js 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/selector.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/variables.scss diff --git a/.eslintrc.js b/.eslintrc.js index b377e14..83d6f4b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,6 @@ module.exports = { extends: '@mate-academy/eslint-config-react', + "rules": { + "import/prefer-default-export": 0, + }, }; diff --git a/package.json b/package.json index 7bd01de..d537744 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.12.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/app-store.png b/public/images/app-store.png new file mode 100644 index 0000000000000000000000000000000000000000..4c1ddae8277dfe0e40641beb52a726c5a614deb1 GIT binary patch literal 3658 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 literal 0 HcmV?d00001 diff --git a/public/images/arrow-down.svg b/public/images/arrow-down.svg new file mode 100644 index 0000000..2576a2a --- /dev/null +++ b/public/images/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/fb.svg b/public/images/fb.svg new file mode 100644 index 0000000..22565b3 --- /dev/null +++ b/public/images/fb.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/google-play.png b/public/images/google-play.png new file mode 100644 index 0000000000000000000000000000000000000000..72e527dbf1b8e82d56b0f91cc756c6ca217a5457 GIT binary patch literal 4109 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 + + + + diff --git a/public/images/logo-white.svg b/public/images/logo-white.svg new file mode 100644 index 0000000..c3894d1 --- /dev/null +++ b/public/images/logo-white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/logo.svg b/public/images/logo.svg new file mode 100644 index 0000000..7418753 --- /dev/null +++ b/public/images/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/place.svg b/public/images/place.svg new file mode 100644 index 0000000..a2f570f --- /dev/null +++ b/public/images/place.svg @@ -0,0 +1,4 @@ + + + + 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/tw.svg b/public/images/tw.svg new file mode 100644 index 0000000..9c8b7bd --- /dev/null +++ b/public/images/tw.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/world.svg b/public/images/world.svg new file mode 100644 index 0000000..b8806c7 --- /dev/null +++ b/public/images/world.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/index.html b/public/index.html index 4269d67..bda5938 100644 --- a/public/index.html +++ b/public/index.html @@ -3,6 +3,16 @@ + + 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..11a3c2d 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,21 @@ import React from 'react'; -import './App.css'; +import { Provider } from 'react-redux'; +import './App.scss'; +import { store } from './store'; +import { RestaurantsListPage } from './components/RestaurantsListPage'; +import Header from './components/Header'; +import Footer from './components/Footer'; const App = () => ( -
-

React Uber eats

-
+ +
+
+
+ +
+
+
+ ); export default App; diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 0000000..44896fc --- /dev/null +++ b/src/App.scss @@ -0,0 +1,6 @@ +.page { + position: relative; + padding-top: 40px; + margin-bottom: 40px; + min-height: 100vh; +} diff --git a/src/components/Error/Error.js b/src/components/Error/Error.js new file mode 100644 index 0000000..ec4ee1c --- /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..fbd3c17 --- /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: $color-green; + + &:hover { + text-decoration: underline; + } + } +} diff --git a/src/components/Error/index.js b/src/components/Error/index.js new file mode 100644 index 0000000..4b5c243 --- /dev/null +++ b/src/components/Error/index.js @@ -0,0 +1,3 @@ +import { Error } from './Error'; + +export default Error; diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js new file mode 100644 index 0000000..135322c --- /dev/null +++ b/src/components/Footer/Footer.js @@ -0,0 +1,110 @@ +import React from 'react'; +import Select from '../Select'; +import './Footer.scss'; + +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..089499d --- /dev/null +++ b/src/components/Header/Header.scss @@ -0,0 +1,66 @@ +@import "../../styles/utils.scss"; + +.header { + position: sticky; + top: 0; + z-index: 1; + padding: 16px 0; + + background: #fff; + + &__inner { + @extend %flex-row-center; + } + + &__delivery-info { + @extend %flex-row-center; + + margin-left: 7vw; + + @media (max-width: $breack-point-tablet) { + display: none; + } + } + + &__search { + margin-left: auto; + + @media (max-width: $breack-point-tablet) { + display: none; + } + } + + &__link { + margin-left: 40px; + padding: 12px 0; + + white-space: nowrap; + color: #1f1f1f; + + &:hover { + text-decoration: underline; + } + } + + &__toogle-buttons { + display: none; + margin-left: auto; + + @media (max-width: $breack-point-tablet) { + display: flex; + } + } + + &__toogle-btn { + padding: 12px 0; + cursor: pointer; + + &:not(:last-of-type) { + margin-right: 20px; + } + + &:focus { + border: 1px solid $color-gray; + } + } +} diff --git a/src/components/Header/index.js b/src/components/Header/index.js new file mode 100644 index 0000000..130dd06 --- /dev/null +++ b/src/components/Header/index.js @@ -0,0 +1,3 @@ +import { Header } from './Header'; + +export default Header; diff --git a/src/components/Input/Input.js b/src/components/Input/Input.js new file mode 100644 index 0000000..517ac82 --- /dev/null +++ b/src/components/Input/Input.js @@ -0,0 +1,81 @@ +import React, { PureComponent, createRef } from 'react'; +import cx from 'classnames'; +import PropTypes from 'prop-types'; +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, + iconUrl: PropTypes.string, + type: PropTypes.string, + placeholder: PropTypes.string, + className: PropTypes.string, +}; + +Input.defaultProps = { + iconUrl: '', + type: 'text', + placeholder: '', + className: '', +}; diff --git a/src/components/Input/Input.scss b/src/components/Input/Input.scss new file mode 100644 index 0000000..a0dd60a --- /dev/null +++ b/src/components/Input/Input.scss @@ -0,0 +1,34 @@ +@import "../../styles/utils.scss"; + +.control { + @extend %flex-row-center; + + padding: 11px 16px; + border: 1px solid $color-gray; + transition: border-color 300ms; + + &--focused { + border-color: darken($color-gray, 20); + } + + &__icon { + width: 14px; + height: 14px; + + margin-right: 10px; + } + + &__input { + max-height: 24px; + line-height: 24px; + + @media (max-width: $breack-point-tablet-lg) { + max-width: 75px; + } + + } + + & + & { + margin-left: 20px; + } +} diff --git a/src/components/Input/index.js b/src/components/Input/index.js new file mode 100644 index 0000000..0f8213f --- /dev/null +++ b/src/components/Input/index.js @@ -0,0 +1,3 @@ +import { Input } from './Input'; + +export default Input; diff --git a/src/components/Loader/Loader.js b/src/components/Loader/Loader.js new file mode 100644 index 0000000..e07f821 --- /dev/null +++ b/src/components/Loader/Loader.js @@ -0,0 +1,21 @@ +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..126bfe4 --- /dev/null +++ b/src/components/Loader/Loader.scss @@ -0,0 +1,105 @@ +@import '../../styles/utils.scss'; + +.loader-container { + @extend %absolute-center; +} + +.lds-default { + display: inline-block; + position: relative; + width: 64px * 2; + height: 64px * 2; +} + +.lds-default div { + position: absolute; + width: 5px * 2; + height: 5px * 2; + background: $color-green; + border-radius: 50%; + animation: lds-default 1.2s linear infinite; +} + +.lds-default div:nth-child(1) { + animation-delay: 0s; + top: 29px * 2; + left: 53px * 2; +} + +.lds-default div:nth-child(2) { + animation-delay: -0.1s; + top: 18px * 2; + left: 50px * 2; +} + +.lds-default div:nth-child(3) { + animation-delay: -0.2s; + top: 9px * 2; + left: 41px * 2; +} + +.lds-default div:nth-child(4) { + animation-delay: -0.3s; + top: 6px * 2; + left: 29px * 2; +} + +.lds-default div:nth-child(5) { + animation-delay: -0.4s; + top: 9px * 2; + left: 18px * 2; +} + +.lds-default div:nth-child(6) { + animation-delay: -0.5s; + top: 18px * 2; + left: 9px * 2; +} + +.lds-default div:nth-child(7) { + animation-delay: -0.6s; + top: 29px * 2; + left: 6px * 2; +} + +.lds-default div:nth-child(8) { + animation-delay: -0.7s; + top: 41px * 2; + left: 9px * 2; +} + +.lds-default div:nth-child(9) { + animation-delay: -0.8s; + top: 50px * 2; + left: 18px * 2; +} + +.lds-default div:nth-child(10) { + animation-delay: -0.9s; + top: 53px * 2; + left: 29px * 2; +} + +.lds-default div:nth-child(11) { + animation-delay: -1s; + top: 50px * 2; + left: 41px * 2; +} + +.lds-default div:nth-child(12) { + animation-delay: -1.1s; + top: 41px * 2; + left: 50px * 2; +} + +@keyframes lds-default { + 0%, + 20%, + 80%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.5); + } +} diff --git a/src/components/Loader/index.js b/src/components/Loader/index.js new file mode 100644 index 0000000..e7231c2 --- /dev/null +++ b/src/components/Loader/index.js @@ -0,0 +1,3 @@ +import { Loader } from './Loader'; + +export default Loader; diff --git a/src/components/RestaurantCard/RestaurantCard.js b/src/components/RestaurantCard/RestaurantCard.js new file mode 100644 index 0000000..dd0aaf1 --- /dev/null +++ b/src/components/RestaurantCard/RestaurantCard.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './RestaurantCard.scss'; + +export const RestaurantCard = (props) => { + const { + imageUrl, + title, + categories, + etaRange, + // uuid, + } = props; + + return ( +
+ {title} +

{title}

+
+ {categories.join(' • ')} +
+
+ {etaRange} +
+
+ ); +}; + +RestaurantCard.propTypes = { + // uuid: PropTypes.string.isRequired, + imageUrl: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + categories: PropTypes.arrayOf(PropTypes.string), + etaRange: PropTypes.string, +}; + +RestaurantCard.defaultProps = { + categories: [], + etaRange: '', +}; diff --git a/src/components/RestaurantCard/RestaurantCard.scss b/src/components/RestaurantCard/RestaurantCard.scss new file mode 100644 index 0000000..d51810b --- /dev/null +++ b/src/components/RestaurantCard/RestaurantCard.scss @@ -0,0 +1,34 @@ +.restaurant-card { + $height: 367px; + height: $height; + width: 100%; + + &__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; + + display: inline-block; + padding: 2px 8px; + + background-color: #f5f5f5; + } + + cursor: pointer; +} diff --git a/src/components/RestaurantCard/index.js b/src/components/RestaurantCard/index.js new file mode 100644 index 0000000..41f249e --- /dev/null +++ b/src/components/RestaurantCard/index.js @@ -0,0 +1,3 @@ +import { RestaurantCard } from './RestaurantCard'; + +export default RestaurantCard; diff --git a/src/components/RestaurantsListPage/RestaurantListPage.scss b/src/components/RestaurantsListPage/RestaurantListPage.scss new file mode 100644 index 0000000..9cd09e1 --- /dev/null +++ b/src/components/RestaurantsListPage/RestaurantListPage.scss @@ -0,0 +1,5 @@ +.restaurant-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(348px, 1fr)); + gap: 40px 20px; +} diff --git a/src/components/RestaurantsListPage/RestaurantsListPage.js b/src/components/RestaurantsListPage/RestaurantsListPage.js new file mode 100644 index 0000000..4ee1f01 --- /dev/null +++ b/src/components/RestaurantsListPage/RestaurantsListPage.js @@ -0,0 +1,70 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import RestaurantCard from '../RestaurantCard'; +import './RestaurantListPage.scss'; +import Loader from '../Loader'; +import Error from '../Error'; + +const DEFAULT_ETA_RANGE = '20 - 30 min'; + +export class RestaurantsListPage extends Component { + componentDidMount() { + const { loadRestaurants } = this.props; + + loadRestaurants(); + } + + render() { + const { + restaurantsListData, + error, + isLoading, + } = this.props; + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( +
+ {restaurantsListData.map((restoraunt) => { + const { + uuid, + title, + heroImageUrl, + categories, + etaRange, + } = restoraunt; + + return ( + + ); + })} +
+ ); + } +} + +RestaurantsListPage.propTypes = { + restaurantsListData: PropTypes.arrayOf(PropTypes.shape()), + loadRestaurants: PropTypes.func.isRequired, + error: PropTypes.string, + isLoading: PropTypes.bool, +}; + +RestaurantsListPage.defaultProps = { + restaurantsListData: [], + error: null, + isLoading: false, +}; diff --git a/src/components/RestaurantsListPage/index.js b/src/components/RestaurantsListPage/index.js new file mode 100644 index 0000000..591eebc --- /dev/null +++ b/src/components/RestaurantsListPage/index.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { RestaurantsListPage } from './RestaurantsListPage'; +import { loadRestaurants } from '../../store/actions'; +import { + selectRestaurantsList, + selectRestaurantsListError, + selectIsLoading, +} from '../../store/selector'; + +const mapState2Props = state => ({ + restaurantsListData: selectRestaurantsList(state), + error: selectRestaurantsListError(state), + isLoading: selectIsLoading(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..59dcafd --- /dev/null +++ b/src/components/Select/Select.js @@ -0,0 +1,60 @@ +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 = { + onSelect: () => {}, + options: [], + iconUrl: '', +}; diff --git a/src/components/Select/Select.scss b/src/components/Select/Select.scss new file mode 100644 index 0000000..f7a9334 --- /dev/null +++ b/src/components/Select/Select.scss @@ -0,0 +1,40 @@ +.select { + position: relative; + + &__input { + -moz-appearance: none; + -webkit-appearance: none; + + color: #fff; + + padding: 10px 40px; + + background-color: #262626; + border: 1px solid #979797; + border-radius: 0; + cursor: pointer; + + // option { + // background-color: transparent; + // } + } + + + &__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..cad5b21 --- /dev/null +++ b/src/components/Select/index.js @@ -0,0 +1,3 @@ +import { Select } from './Select'; + +export default Select; diff --git a/src/index.js b/src/index.js index b597a44..7e8df49 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +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 new file mode 100644 index 0000000..1698a34 --- /dev/null +++ b/src/store/actions.js @@ -0,0 +1,33 @@ +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(response => response.json()) + .then(({ data }) => dispatch(saveRestaurants(data))) + .catch(error => dispatch(setRestaurantsError(error.message))) + .finally(() => dispatch(stopLoading())); +}; diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..e6b904f --- /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..ed1015a --- /dev/null +++ b/src/store/rootReducer.js @@ -0,0 +1,48 @@ +import { ACTION_TYPES } from './actions'; + +const initialState = { + restaurantsListData: null, + isLoading: false, + error: null, +}; + +export function rootReducer(state = initialState, 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/selector.js b/src/store/selector.js new file mode 100644 index 0000000..f2e85bc --- /dev/null +++ b/src/store/selector.js @@ -0,0 +1,26 @@ +import { createSelector } from 'reselect'; + +const rootSelector = state => state; + +export const selectRestaurantsList = 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..b3b26fb --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,37 @@ +*, +::after, +::before { + box-sizing: border-box; +} + +body, +button, +a, +select, +input { + font-family: Roboto, sans-serif; + font-size: 16px; + line-height: 1.6; +} + +input { + border: 0; + background: transparent; + outline: none; +} + +a { + text-decoration: none; +} + +button { + border: none; + background: none; + outline: 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..8e9efb3 --- /dev/null +++ b/src/styles/utils.scss @@ -0,0 +1,2 @@ +@import "./variables.scss"; +@import "./extends.scss"; diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 0000000..449e586 --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,5 @@ +$color-green: #5eb707; +$color-gray: #e0e0e0; + +$breack-point-tablet-lg: 956px; +$breack-point-tablet: 768px; From b6ef4a7025700924fc326bb5c1def24a8fbb74b0 Mon Sep 17 00:00:00 2001 From: igaryok Date: Wed, 16 Oct 2019 15:09:03 +0300 Subject: [PATCH 02/34] part solution --- public/images/close.svg | 3 + src/components/Footer/Footer.scss | 52 +++++++++- src/components/Header/Header.js | 94 ++++++++++++++++++- src/components/Header/Header.scss | 35 ++++++- src/components/Input/Input.js | 67 ++++++++----- src/components/Input/Input.scss | 26 ++++- .../RestaurantCard/RestaurantCard.scss | 17 +++- .../RestaurantListPage.scss | 17 +++- src/styles/index.scss | 10 ++ src/styles/variables.scss | 3 + 10 files changed, 282 insertions(+), 42 deletions(-) create mode 100644 public/images/close.svg diff --git a/public/images/close.svg b/public/images/close.svg new file mode 100644 index 0000000..c784d0c --- /dev/null +++ b/public/images/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss index 43f914a..4bc5de1 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: $breack-point-tablet) { + padding: 40px 0; + } + &__top-part { margin-bottom: 40px; padding-bottom: 40px; @@ -20,11 +24,21 @@ @extend %flex-row-center; margin-bottom: 40px; + + @media (max-width: $breack-point-tablet) { + flex-direction: column; + align-items: flex-start; + } } &__mobile-app { display: inline-block; + @media (max-width: $breack-point-tablet) { + display: block; + margin-bottom: 20px; + } + &:first-of-type { margin-right: 20px; } @@ -32,24 +46,39 @@ &__logo { margin-right: 40px; + + @media (max-width: $breack-point-tablet) { + margin-bottom: 40px; + } } &__top-part, &__bottom-part { @extend %flex-row-center; - align-items: flex-start; + + @media (max-width: $breack-point-phone-lg) { + flex-direction: column; + } } &__main, &__copyright { flex-basis: 45vw; padding-right: 40px; + + @media (max-width: $breack-point-phone-lg) { + flex-basis: initial; + } } &__copyright { font-size: 14px; color: #fff; + + @media (max-width: $breack-point-phone-lg) { + order: 1; + } } &__bottom-links { @@ -57,6 +86,13 @@ margin-bottom: 40px; + @media (max-width: $breack-point-phone-lg) { + flex-direction: column; + align-items: flex-start; + margin-top: 20px; + order: 1; + } + .links__link:not(:last-of-type) { margin-right: 40px; } @@ -64,6 +100,18 @@ &__top-links:last-of-type { margin-left: 10vw; + + @media (max-width: $breack-point-phone-lg) { + margin-left: 0; + } + } + + &__misc { + @media (max-width: $breack-point-phone-lg) { + display: flex; + flex-direction: column; + margin-top: 20px; + } } } diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 61cd244..0662841 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -7,6 +7,8 @@ export class Header extends Component { address: '', time: '', search: '', + isMobileSearchVisible: false, + isMobileDeliveryInfoVisible: false, } handleChange = ({ target }) => { @@ -15,18 +17,43 @@ export class Header extends Component { }); } + toggleSearch = () => this.setState(({ isMobileSearchVisible }) => ({ + isMobileSearchVisible: !isMobileSearchVisible, + isMobileDeliveryInfoVisible: false, + })); + + toggleDeliveryInfo = () => this.setState((prevState) => { + const { isMobileDeliveryInfoVisible } = prevState; + + return ({ + isMobileDeliveryInfoVisible: !isMobileDeliveryInfoVisible, + isMobileSearchVisible: false, + }); + }); + + closeMobile = () => this.setState({ + isMobileDeliveryInfoVisible: false, + isMobileSearchVisible: false, + }); + render() { const { address, time, search, + isMobileDeliveryInfoVisible, + isMobileSearchVisible, } = this.state; return (
- Uber Eats + Uber Eats
- -
+ + {(isMobileSearchVisible || isMobileDeliveryInfoVisible) && ( +
+ {isMobileSearchVisible && ( + + )} + + {isMobileDeliveryInfoVisible && ( + <> + + + + + )} + + + +
+ )}
); diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss index 089499d..93910ad 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.scss @@ -4,6 +4,8 @@ position: sticky; top: 0; z-index: 1; + + width: 100%; padding: 16px 0; background: #fff; @@ -12,6 +14,15 @@ @extend %flex-row-center; } + &__logo { + height: 24px; + width: auto; + + @media (max-width: $breack-point-phone) { + height: 14px; + } + } + &__delivery-info { @extend %flex-row-center; @@ -52,15 +63,35 @@ } &__toogle-btn { - padding: 12px 0; + 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; + border-color: $color-gray; } } } + +.mobile-controls { + display: none; + position: relative; + padding-top: 30px; + + @media (max-width: $breack-point-tablet) { + display: flex; + } + + &__close { + position: absolute; + top: 10px; + right: 0; + } +} diff --git a/src/components/Input/Input.js b/src/components/Input/Input.js index 517ac82..a95e52c 100644 --- a/src/components/Input/Input.js +++ b/src/components/Input/Input.js @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ import React, { PureComponent, createRef } from 'react'; import cx from 'classnames'; import PropTypes from 'prop-types'; @@ -25,40 +26,52 @@ export class Input extends PureComponent { placeholder, name, className, + isSmall, + label, } = this.props; const { isFocused } = this.state; - const rootClass = cx('control', { - 'control--focused': isFocused, + const inputWrapperClass = 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} +
+ ); } } @@ -71,6 +84,8 @@ Input.propTypes = { type: PropTypes.string, placeholder: PropTypes.string, className: PropTypes.string, + isSmall: PropTypes.bool, + label: PropTypes.string, }; Input.defaultProps = { @@ -78,4 +93,6 @@ Input.defaultProps = { type: 'text', placeholder: '', className: '', + isSmall: true, + label: '', }; diff --git a/src/components/Input/Input.scss b/src/components/Input/Input.scss index a0dd60a..b3e3c94 100644 --- a/src/components/Input/Input.scss +++ b/src/components/Input/Input.scss @@ -1,11 +1,15 @@ @import "../../styles/utils.scss"; .control { - @extend %flex-row-center; - padding: 11px 16px; - border: 1px solid $color-gray; - transition: border-color 300ms; + &__input-wrapper{ + @extend %flex-row-center; + + padding: 11px 16px; + border: 1px solid $color-gray; + transition: border-color 300ms; + } + &--focused { border-color: darken($color-gray, 20); @@ -22,10 +26,22 @@ max-height: 24px; line-height: 24px; - @media (max-width: $breack-point-tablet-lg) { + &--time { max-width: 75px; } + &--small { + @media (max-width: $breack-point-tablet-lg) { + max-width: 75px; + } + } + } + + &__label { + font-size: 14px; + color: #626262; + + margin-bottom: 8px; } & + & { diff --git a/src/components/RestaurantCard/RestaurantCard.scss b/src/components/RestaurantCard/RestaurantCard.scss index d51810b..ac37e56 100644 --- a/src/components/RestaurantCard/RestaurantCard.scss +++ b/src/components/RestaurantCard/RestaurantCard.scss @@ -1,10 +1,19 @@ +@import "../../styles/utils.scss"; + .restaurant-card { - $height: 367px; - height: $height; + --height: 367px; + + height: var(--height); width: 100%; + cursor: pointer; + + @media (max-width: $breack-point-phone) { + --height: 330px; + } + &__img { - height: $height * 0.7; + height: calc(var(--height) * 0.7); width: 100%; object-fit: cover; @@ -26,7 +35,7 @@ display: inline-block; padding: 2px 8px; - + background-color: #f5f5f5; } diff --git a/src/components/RestaurantsListPage/RestaurantListPage.scss b/src/components/RestaurantsListPage/RestaurantListPage.scss index 9cd09e1..84ec0ce 100644 --- a/src/components/RestaurantsListPage/RestaurantListPage.scss +++ b/src/components/RestaurantsListPage/RestaurantListPage.scss @@ -1,5 +1,20 @@ +@import "../../styles/utils.scss"; + .restaurant-list { + --card-width: 348px; + display: grid; - grid-template-columns: repeat(auto-fill, minmax(348px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(var(--card-width), 1fr)); gap: 40px 20px; + + @media (max-width: $breack-point-tablet-lg) { + --card-width: 340px; + + row-gap: 20px; + } + + @media (max-width: $breack-point-phone) { + --card-width: 280px; + } + } diff --git a/src/styles/index.scss b/src/styles/index.scss index b3b26fb..b2bdace 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,3 +1,5 @@ +@import "./utils.scss"; + *, ::after, ::before { @@ -34,4 +36,12 @@ button { max-width: 1160px; padding: 0 34px; margin: 0 auto; + + @media (max-width: $breack-point-phone) { + padding: 0 20px; + } + + @media (min-width: $breack-point-large) { + max-width: 1520px; + } } diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 449e586..30f92c4 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -1,5 +1,8 @@ $color-green: #5eb707; $color-gray: #e0e0e0; +$breack-point-large: 1281px; $breack-point-tablet-lg: 956px; $breack-point-tablet: 768px; +$breack-point-phone-lg: 620px; +$breack-point-phone: 550px; From 1db94851c8632ed689c84c6043eadf5a52cf7497 Mon Sep 17 00:00:00 2001 From: igaryok Date: Mon, 21 Oct 2019 22:33:52 +0300 Subject: [PATCH 03/34] partial solution --- src/App.js | 11 +- src/components/CardItem/CardItem.js | 36 +++++++ src/components/CardItem/CardItem.scss | 33 ++++++ src/components/CardItem/index.js | 15 +++ src/components/Header/Header.js | 12 ++- src/components/Header/Header.scss | 4 + .../RestaurantPage/RestaurantPage.js | 101 +++++++++++++++++ .../RestaurantPage/RestaurantPage.scss | 102 ++++++++++++++++++ src/components/RestaurantPage/index.js | 23 ++++ .../RestaurantsListPage.js | 45 ++++---- src/components/Section/Section.js | 24 +++++ src/components/Section/Section.scss | 16 +++ src/components/Section/index.js | 15 +++ src/components/Select/Select.js | 4 +- src/store/actions.js | 15 +++ src/store/rootReducer.js | 13 +++ src/store/selector.js | 58 ++++++++++ src/styles/index.scss | 1 + 18 files changed, 498 insertions(+), 30 deletions(-) create mode 100644 src/components/CardItem/CardItem.js create mode 100644 src/components/CardItem/CardItem.scss create mode 100644 src/components/CardItem/index.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 create mode 100644 src/components/Section/Section.js create mode 100644 src/components/Section/Section.scss create mode 100644 src/components/Section/index.js diff --git a/src/App.js b/src/App.js index 11a3c2d..da7a0c3 100644 --- a/src/App.js +++ b/src/App.js @@ -1,18 +1,23 @@ import React from 'react'; +import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { Provider } from 'react-redux'; import './App.scss'; import { store } from './store'; import { RestaurantsListPage } from './components/RestaurantsListPage'; import Header from './components/Header'; import Footer from './components/Footer'; +import { RestaurantPage } from './components/RestaurantPage'; const App = () => (
-
- -
+ + + + + +
diff --git a/src/components/CardItem/CardItem.js b/src/components/CardItem/CardItem.js new file mode 100644 index 0000000..1c16dcc --- /dev/null +++ b/src/components/CardItem/CardItem.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './CardItem.scss'; + +export const CardItem = ({ + title, + imageUrl, + description, + price, + restaurantCurency, +}) => ( +
+
+
+

{title}

+

{description}

+
+

{`${restaurantCurency}${price}`}

+
+ {imageUrl && {title}} +
+); + +CardItem.propTypes = { + title: PropTypes.string.isRequired, + imageUrl: PropTypes.string, + description: PropTypes.string, + price: PropTypes.string.isRequired, + restaurantCurency: PropTypes.string, +}; + +CardItem.defaultProps = { + imageUrl: '', + description: '', + restaurantCurency: '', +}; diff --git a/src/components/CardItem/CardItem.scss b/src/components/CardItem/CardItem.scss new file mode 100644 index 0000000..290eed9 --- /dev/null +++ b/src/components/CardItem/CardItem.scss @@ -0,0 +1,33 @@ +.card { + display: flex; + flex-flow: row nowrap; + box-sizing: border-box; + height: 160px; + + line-height: 21px; + border: 1px solid #e0e0e0; + + &__img { + width: 50%; + } + + &__description { + display: flex; + flex-flow: column nowrap; + justify-content: space-between; + box-sizing: border-box; + padding: 16px; + + &-wrapper { + overflow-y: auto; + max-height: 90px; + } + + &-info { + font-size: 14px; + color: #626262; + margin-top: 4px; + + } + } +} diff --git a/src/components/CardItem/index.js b/src/components/CardItem/index.js new file mode 100644 index 0000000..4afc887 --- /dev/null +++ b/src/components/CardItem/index.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import { CardItem } from './CardItem'; +import { + selectRestaurantCurency, +} from '../../store/selector'; + +const mapState2Props = state => ({ + restaurantCurency: selectRestaurantCurency(state), +}); + +const Enhanced = connect( + mapState2Props, +)(CardItem); + +export { Enhanced as CardItem }; diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 0662841..3081336 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -49,11 +49,13 @@ export class Header extends Component {
- Uber Eats + + Uber Eats +
; + } + + const heroImageUrl = heroImageUrls[heroImageUrls.length - 1].url; + + const heroImageUrlSrcSet = heroImageUrls.reduce((acc, item, index) => { + let str = `${item.url} ${item.width}w`; + + (index !== heroImageUrls.length - 1) && (str = `${str},`); + + return `${acc}${str}`; + }, ''); + + return ( + <> +
+ {title} +
+

{title}

+
+ {categories.join(' • ')} +
+
+ {etaRange || DEFAULT_ETA_RANGE} +
+
+ {`${location.address} • `} + More info +
+
+
+
+ + + {restaurantSections.map(item =>
)} +
+ + ); + } +} + +RestaurantPage.propTypes = { + loadRestaurantPage: PropTypes.func.isRequired, + restaurantPageData: PropTypes.shape(), + restaurantSections: PropTypes.arrayOf(PropTypes.shape()), + match: PropTypes.shape().isRequired, +}; + +RestaurantPage.defaultProps = { + restaurantPageData: [], + restaurantSections: [], +}; diff --git a/src/components/RestaurantPage/RestaurantPage.scss b/src/components/RestaurantPage/RestaurantPage.scss new file mode 100644 index 0000000..2675ce9 --- /dev/null +++ b/src/components/RestaurantPage/RestaurantPage.scss @@ -0,0 +1,102 @@ +.hero { + position: relative; + height: 380px; + margin-top: -40px; + margin-bottom: 30px; + + overflow: hidden; + + &__img { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 100%; + } +} + +.title { + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + + display: flex; + flex-direction: column; + align-items: flex-start; + + font-size: 14px; + + background: #fff; + padding-left: 100px; + padding-right: 40px; + padding-top: 24px; + padding-bottom: 24px; + + &__name { + color: #1d1d1d; + font-size: 36px; + font-weight: 500; + + margin-bottom: 16px; + } + + &__categories { + color: #757575; + + margin-bottom: 16px; + } + + &__eta { + width: 86px; + background-color: #f5f5f5; + + margin-bottom: 18px; + } + + &__more { + color: #247a00; + font-size: inherit; + } +} + +.restoraunt-menu { + display: flex; + justify-content: space-between; + + overflow-x: auto; + overflow-anchor: none; + position: sticky; + top: 0; + + font-size: 14px; + height: 40px; + + margin-bottom: 40px; + white-space: nowrap; + + &__item { + font-size: 14px; + line-height: 21px; + position: relative; + + &:not(:last-of-type) { + margin-right: 20px; + } + + &::after { + content: ""; + height: 1px; + background-color: #000; + width: 100%; + display: none; + position: absolute; + bottom: 0; + } + + &:hover::after { + display: block; + } + + } +} + diff --git a/src/components/RestaurantPage/index.js b/src/components/RestaurantPage/index.js new file mode 100644 index 0000000..1f414dc --- /dev/null +++ b/src/components/RestaurantPage/index.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { RestaurantPage } from './RestaurantPage'; +import { loadRestaurantPage } from '../../store/actions'; +import { + selectRestaurantPageData, + selectResorauntSections, +} from '../../store/selector'; + +const mapState2Props = state => ({ + restaurantPageData: selectRestaurantPageData(state), + restaurantSections: selectResorauntSections(state), +}); + +const mapDispatch2Props = { + loadRestaurantPage, +}; + +const Enhanced = connect( + mapState2Props, + mapDispatch2Props +)(RestaurantPage); + +export { Enhanced as RestaurantPage }; diff --git a/src/components/RestaurantsListPage/RestaurantsListPage.js b/src/components/RestaurantsListPage/RestaurantsListPage.js index 4ee1f01..ef4e876 100644 --- a/src/components/RestaurantsListPage/RestaurantsListPage.js +++ b/src/components/RestaurantsListPage/RestaurantsListPage.js @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import RestaurantCard from '../RestaurantCard'; import './RestaurantListPage.scss'; @@ -30,27 +31,31 @@ export class RestaurantsListPage extends Component { } return ( -
- {restaurantsListData.map((restoraunt) => { - const { - uuid, - title, - heroImageUrl, - categories, - etaRange, - } = restoraunt; +
+
+ {restaurantsListData.map((restoraunt) => { + const { + uuid, + title, + heroImageUrl, + categories, + etaRange, + } = restoraunt; - return ( - - ); - })} + return ( + + + + ); + })} +
); } diff --git a/src/components/Section/Section.js b/src/components/Section/Section.js new file mode 100644 index 0000000..8a3a76e --- /dev/null +++ b/src/components/Section/Section.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Section.scss'; +import { CardItem } from '../CardItem'; + +export const Section = ({ category, restaurantItems }) => { + const { title, uuid } = category; + const items = restaurantItems + .find(item => item.uuid === category.uuid).itemUuids; + + return ( + <> +

{title}

+
+ {items.map(item => )} +
+ + ); +}; + +Section.propTypes = { + category: PropTypes.shape().isRequired, + restaurantItems: PropTypes.arrayOf(PropTypes.shape()).isRequired, +}; diff --git a/src/components/Section/Section.scss b/src/components/Section/Section.scss new file mode 100644 index 0000000..471bf2a --- /dev/null +++ b/src/components/Section/Section.scss @@ -0,0 +1,16 @@ +.section { + --card-width: 348px; + + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--card-width), 1fr)); + gap: 40px 20px; + + margin-bottom: 40px; +} + +.title-categories { + font-size: 20px; + font-weight: 500; + + margin-bottom: 20px; +} diff --git a/src/components/Section/index.js b/src/components/Section/index.js new file mode 100644 index 0000000..6d8745c --- /dev/null +++ b/src/components/Section/index.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import { Section } from './Section'; +import { + selectRestorauntItems, +} from '../../store/selector'; + +const mapState2Props = state => ({ + restaurantItems: selectRestorauntItems(state), +}); + +const Enhanced = connect( + mapState2Props, +)(Section); + +export { Enhanced as Section }; diff --git a/src/components/Select/Select.js b/src/components/Select/Select.js index 59dcafd..3a4eb70 100644 --- a/src/components/Select/Select.js +++ b/src/components/Select/Select.js @@ -15,12 +15,12 @@ export const Select = (props) => {
Date: Tue, 22 Oct 2019 15:28:51 +0300 Subject: [PATCH 08/34] added demo --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index dfe2840..5a132c0 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@ Base API url: https://mate-uber-eats-api.herokuapp.com/api/v1/ - use `redux-thunk` to handle async actions - create separate component for restaurant card - use https://mate-uber-eats-api.herokuapp.com/api/v1/restaurants endpoint to fetch data - - accept optional query parameter `location`: `london|kyiv` - - `RestaurantCard` should be clickable fully (not only the image) + - accept optional query parameter `location`: `london|kyiv` + - `RestaurantCard` should be clickable fully (not only the image) 2. Implement basic header markup 3. Implement footer 4. Add functionality and styles for mobile and tablet versions - `Location`, `Deliver now` and `Search` should be replaced whith buttons on the small screens. - Block with inputs should apper below the header after click on the button. + Block with inputs should apper below the header after click on the button. 5. Implement `RestaurantPage` - use `uuid` in the URL - fetch data from https://mate-uber-eats-api.herokuapp.com/api/v1/restaurants/:uuid @@ -37,29 +37,31 @@ Base API url: https://mate-uber-eats-api.herokuapp.com/api/v1/ ## Workflow - Fork the repository with task -- Clone forked repository +- Clone forked repository ```bash git clone git@github.com:/.git ``` - Run `npm install` to install dependencies. - Then develop -## Development mode +## Development mode - Run `npm start` to start development server on `http://localhost:3000` - When you run server the command line window will no longer be available for - writing commands until you stop server (`ctrl + c`). All other commands you + When you run server the command line window will no longer be available for + writing commands until you stop server (`ctrl + c`). All other commands you need to run in new command line window. - Follow [HTML, CSS styleguide](https://mate-academy.github.io/style-guides/htmlcss.html) - Follow [the simplified JS styleguide](https://mate-academy.github.io/style-guides/javascript-standard-modified) - run `npm run lint` to check code style -- When you finished add correct `homepage` to `package.json` and run `npm run deploy` +- When you finished add correct `homepage` to `package.json` and run `npm run deploy` - Add links to your demo in readme.md. - - `[DEMO LINK](https://.github.io//)` - this will be a + - `[DEMO LINK](https://.github.io//)` - this will be a link to your index.html - Commit and push all recent changes. -- Create `Pull Request` from forked repo `()` to original repo +- Create `Pull Request` from forked repo `()` to original repo (`master`). - Add a link at `PR` to Google Spreadsheets. ## Project structure You should be writing your code in `src/` directory. + +-[DEMO](https://igaryok.github.io/react_uber-eats/) From 77ba95aad2a0f711264c83a887b02d9258fb55d9 Mon Sep 17 00:00:00 2001 From: igaryok Date: Tue, 22 Oct 2019 16:27:03 +0300 Subject: [PATCH 09/34] Update src/components/RestaurantPage/index.js Co-Authored-By: Iryna Holovko <33497431+irenhh@users.noreply.github.com> --- src/components/RestaurantPage/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RestaurantPage/index.js b/src/components/RestaurantPage/index.js index 1f414dc..e6f9d2d 100644 --- a/src/components/RestaurantPage/index.js +++ b/src/components/RestaurantPage/index.js @@ -16,7 +16,7 @@ const mapDispatch2Props = { }; const Enhanced = connect( - mapState2Props, + mapStateToProps, mapDispatch2Props )(RestaurantPage); From bdd4f8c770cfb487f32946244573821f588709b9 Mon Sep 17 00:00:00 2001 From: igaryok Date: Tue, 22 Oct 2019 16:27:42 +0300 Subject: [PATCH 10/34] Update src/components/CardItem/index.js Co-Authored-By: Iryna Holovko <33497431+irenhh@users.noreply.github.com> --- src/components/CardItem/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CardItem/index.js b/src/components/CardItem/index.js index 4afc887..6747e7d 100644 --- a/src/components/CardItem/index.js +++ b/src/components/CardItem/index.js @@ -4,7 +4,7 @@ import { selectRestaurantCurency, } from '../../store/selector'; -const mapState2Props = state => ({ +const mapStateToProps = state => ({ restaurantCurency: selectRestaurantCurency(state), }); From 0744373726c1ff826379637ddac65962fce1f958 Mon Sep 17 00:00:00 2001 From: igaryok Date: Wed, 23 Oct 2019 18:41:03 +0300 Subject: [PATCH 11/34] partial solution --- src/components/CardItem/index.js | 2 +- src/components/Header/Header.js | 6 +++--- src/components/Header/Header.scss | 3 ++- src/components/RestaurantCard/RestaurantCard.scss | 6 +++++- src/components/RestaurantPage/RestaurantPage.js | 1 + src/components/RestaurantPage/index.js | 2 +- src/styles/index.scss | 3 ++- 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/CardItem/index.js b/src/components/CardItem/index.js index 6747e7d..4afc887 100644 --- a/src/components/CardItem/index.js +++ b/src/components/CardItem/index.js @@ -4,7 +4,7 @@ import { selectRestaurantCurency, } from '../../store/selector'; -const mapStateToProps = state => ({ +const mapState2Props = state => ({ restaurantCurency: selectRestaurantCurency(state), }); diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 9b8d654..09c0501 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -111,12 +111,12 @@ export class Header extends Component {
- Sign In - +
diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss index 0430e18..f848222 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.scss @@ -85,6 +85,7 @@ position: relative; padding-top: 30px; + @media (max-width: $breack-point-tablet) { display: flex; } @@ -97,5 +98,5 @@ } .control { - margin-left: auto; + // margin-left: auto; } diff --git a/src/components/RestaurantCard/RestaurantCard.scss b/src/components/RestaurantCard/RestaurantCard.scss index ac37e56..053cd2c 100644 --- a/src/components/RestaurantCard/RestaurantCard.scss +++ b/src/components/RestaurantCard/RestaurantCard.scss @@ -39,5 +39,9 @@ background-color: #f5f5f5; } - cursor: pointer; + transition: transform 500ms; + + &:hover { + transform: scale(1.1,1.1); + } } diff --git a/src/components/RestaurantPage/RestaurantPage.js b/src/components/RestaurantPage/RestaurantPage.js index cd7bac9..01de5a6 100644 --- a/src/components/RestaurantPage/RestaurantPage.js +++ b/src/components/RestaurantPage/RestaurantPage.js @@ -12,6 +12,7 @@ export class RestaurantPage extends PureComponent { const { match: { params } } = this.props; loadRestaurantPage(params.uuid); + window.scrollTo(0, 0); } render() { diff --git a/src/components/RestaurantPage/index.js b/src/components/RestaurantPage/index.js index e6f9d2d..1f414dc 100644 --- a/src/components/RestaurantPage/index.js +++ b/src/components/RestaurantPage/index.js @@ -16,7 +16,7 @@ const mapDispatch2Props = { }; const Enhanced = connect( - mapStateToProps, + mapState2Props, mapDispatch2Props )(RestaurantPage); diff --git a/src/styles/index.scss b/src/styles/index.scss index ee2a470..8922856 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -16,7 +16,8 @@ input { line-height: 1.6; } -input { +input, +select { border: 0; background: transparent; outline: none; From 443d96b6b87bb490734dad397b0cffd117838f64 Mon Sep 17 00:00:00 2001 From: igaryok Date: Wed, 23 Oct 2019 21:22:37 +0300 Subject: [PATCH 12/34] partial solution --- package.json | 3 +- src/components/CardItem/CardItem.js | 2 +- src/components/Error/Error.js | 1 - src/components/Header/Header.js | 1 - src/components/Header/Header.scss | 2 +- src/components/Input/Input.js | 4 +- .../RestaurantCard/RestaurantCard.js | 2 - .../RestaurantPage/RestaurantPage.js | 46 ++++++++++++++----- src/components/Section/Section.js | 15 ++++-- src/store/actions.js | 6 ++- src/store/selector.js | 8 +--- 11 files changed, 58 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index d537744..ebce6b5 100755 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "react-scripts": "3.0.1", "redux": "^4.0.4", "redux-thunk": "^2.3.0", - "reselect": "^4.0.0" + "reselect": "^4.0.0", + "uuidv4": "^5.0.1" }, "devDependencies": { "@mate-academy/eslint-config-react": "*", diff --git a/src/components/CardItem/CardItem.js b/src/components/CardItem/CardItem.js index 1c16dcc..4f2f76d 100644 --- a/src/components/CardItem/CardItem.js +++ b/src/components/CardItem/CardItem.js @@ -25,7 +25,7 @@ CardItem.propTypes = { title: PropTypes.string.isRequired, imageUrl: PropTypes.string, description: PropTypes.string, - price: PropTypes.string.isRequired, + price: PropTypes.number.isRequired, restaurantCurency: PropTypes.string, }; diff --git a/src/components/Error/Error.js b/src/components/Error/Error.js index ec4ee1c..23be283 100644 --- a/src/components/Error/Error.js +++ b/src/components/Error/Error.js @@ -13,7 +13,6 @@ export const Error = (props) => { Go to Home -
); }; diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 09c0501..caa6389 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -169,7 +169,6 @@ export class Header extends Component { alt="search icon" /> -
)}
diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss index f848222..3c7af23 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.scss @@ -98,5 +98,5 @@ } .control { - // margin-left: auto; + margin-left: auto; } diff --git a/src/components/Input/Input.js b/src/components/Input/Input.js index a95e52c..f70032d 100644 --- a/src/components/Input/Input.js +++ b/src/components/Input/Input.js @@ -1,4 +1,3 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ import React, { PureComponent, createRef } from 'react'; import cx from 'classnames'; import PropTypes from 'prop-types'; @@ -42,7 +41,7 @@ export class Input extends PureComponent { }); return ( -
); } } +const restorauntShape = { + title: PropTypes.string, + uuid: PropTypes.string, + categories: PropTypes.arrayOf(PropTypes.string), + etaRange: PropTypes.string, + location: PropTypes.shape({ + address: PropTypes.string, + city: PropTypes.string, + }), +}; + RestaurantPage.propTypes = { loadRestaurantPage: PropTypes.func.isRequired, - restaurantPageData: PropTypes.shape(), - restaurantSections: PropTypes.arrayOf(PropTypes.shape()), - match: PropTypes.shape().isRequired, + restaurantPageData: PropTypes.shape(restorauntShape), + restaurantSections: PropTypes.arrayOf(PropTypes.shape({ + uuid: PropTypes.string, + tittle: PropTypes.string, + })), + match: PropTypes.shape({ + params: PropTypes.shape({ + uuid: PropTypes.string, + }), + }).isRequired, }; RestaurantPage.defaultProps = { - restaurantPageData: [], + restaurantPageData: null, restaurantSections: [], }; diff --git a/src/components/Section/Section.js b/src/components/Section/Section.js index 8a3a76e..9aa60f2 100644 --- a/src/components/Section/Section.js +++ b/src/components/Section/Section.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import uuidGen from 'uuidv4'; import './Section.scss'; import { CardItem } from '../CardItem'; @@ -12,13 +13,21 @@ export const Section = ({ category, restaurantItems }) => { <>

{title}

- {items.map(item => )} + {items.map(item => )}
); }; Section.propTypes = { - category: PropTypes.shape().isRequired, - restaurantItems: PropTypes.arrayOf(PropTypes.shape()).isRequired, + category: PropTypes.shape({ + uuid: PropTypes.string, + tittle: PropTypes.string, + }).isRequired, + restaurantItems: PropTypes.arrayOf(PropTypes.shape({ + title: PropTypes.string, + imageUrl: PropTypes.string, + description: PropTypes.string, + price: PropTypes.number, + })).isRequired, }; diff --git a/src/store/actions.js b/src/store/actions.js index 4a169cd..2414512 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -1,3 +1,5 @@ +const BASE_URL = 'https://mate-uber-eats-api.herokuapp.com/api/v1/restaurants/'; + export const ACTION_TYPES = { SAVE_RESTAURANTS: 'SAVE_RESTAURANTS', SAVE_RESTAURANT_PAGE: 'SAVE_RESTAURANT_PAGE', @@ -31,7 +33,7 @@ const stopLoading = () => ({ export const loadRestaurants = () => (dispatch) => { dispatch(startLoading()); - fetch('https://mate-uber-eats-api.herokuapp.com/api/v1/restaurants') + fetch(BASE_URL) .then(response => response.json()) .then(({ data }) => dispatch(saveRestaurants(data))) .catch(error => dispatch(setRestaurantsError(error.message))) @@ -40,7 +42,7 @@ export const loadRestaurants = () => (dispatch) => { export const loadRestaurantPage = id => (dispatch) => { dispatch(startLoading()); - fetch(`https://mate-uber-eats-api.herokuapp.com/api/v1/restaurants/${id}`) + fetch(`${BASE_URL}${id}`) .then(response => response.json()) .then(({ data }) => dispatch(saveRestaurantPage(data))) .catch(error => dispatch(setRestaurantsError(error.message))) diff --git a/src/store/selector.js b/src/store/selector.js index 3bbcde0..8070529 100644 --- a/src/store/selector.js +++ b/src/store/selector.js @@ -53,13 +53,7 @@ export const selectEtaRange = ( export const selectRestaurantPageData = createSelector( rootSelector, - ({ restaurantPageData }) => { - if (!restaurantPageData) { - return []; - } - - return restaurantPageData; - } + ({ restaurantPageData }) => restaurantPageData ); export const selectRestaurantCurency = createSelector( From c44a7f1450494e241a6befe6e68abac7c87654e1 Mon Sep 17 00:00:00 2001 From: igaryok Date: Wed, 23 Oct 2019 23:41:28 +0300 Subject: [PATCH 13/34] partial solution --- src/components/Header/Header.js | 25 +++++++++---------- src/components/Header/Header.scss | 22 ++++++++++++++++ src/components/Input/Input.js | 7 +----- src/components/Input/Input.scss | 6 ++++- .../RestaurantPage/RestaurantPage.js | 2 +- .../RestaurantPage/RestaurantPage.scss | 12 ++++----- .../RestaurantsListPage.js | 1 + src/styles/variables.scss | 1 + 8 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index caa6389..e1c3d28 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -111,12 +111,11 @@ export class Header extends Component {
- Sign In - +
@@ -137,23 +136,23 @@ export class Header extends Component { {isMobileDeliveryInfoVisible && ( <> diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss index 3c7af23..4597a9f 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.scss @@ -50,6 +50,7 @@ &:hover { text-decoration: underline; + cursor: pointer; } } @@ -90,6 +91,10 @@ display: flex; } + @media (max-width: $breack-point-phone) { + justify-content: center; + } + &__close { position: absolute; top: 10px; @@ -99,4 +104,21 @@ .control { margin-left: auto; + + @media (max-width: $breack-point-tablet) { + margin-left: 0; + } + + @media (max-width: $breack-point-phone) { + width: 100%; + &:not(:last-of-type) { + width: 25%; + } + } + + @media (max-width: $breack-point-phone-sm) { + &:not(:first-of-type) { + max-width: 195px; + } + } } diff --git a/src/components/Input/Input.js b/src/components/Input/Input.js index f70032d..f99e1f8 100644 --- a/src/components/Input/Input.js +++ b/src/components/Input/Input.js @@ -1,4 +1,4 @@ -import React, { PureComponent, createRef } from 'react'; +import React, { PureComponent } from 'react'; import cx from 'classnames'; import PropTypes from 'prop-types'; import './Input.scss'; @@ -8,14 +8,10 @@ export class Input extends PureComponent { isFocused: false, } - inputRef = createRef(); - handleFocus = () => this.setState({ isFocused: true }); handleBlur = () => this.setState({ isFocused: false }); - focus = () => this.inputRef.current.focus(); - render() { const { iconUrl, @@ -60,7 +56,6 @@ export class Input extends PureComponent {
{`${location.address} • `} - More info + More info
diff --git a/src/components/RestaurantPage/RestaurantPage.scss b/src/components/RestaurantPage/RestaurantPage.scss index 2675ce9..38056d2 100644 --- a/src/components/RestaurantPage/RestaurantPage.scss +++ b/src/components/RestaurantPage/RestaurantPage.scss @@ -56,6 +56,8 @@ &__more { color: #247a00; font-size: inherit; + + cursor: pointer; } } @@ -64,9 +66,6 @@ justify-content: space-between; overflow-x: auto; - overflow-anchor: none; - position: sticky; - top: 0; font-size: 14px; height: 40px; @@ -86,15 +85,16 @@ &::after { content: ""; height: 1px; + width: 0; background-color: #000; - width: 100%; - display: none; + display: block; position: absolute; bottom: 0; + transition: 500ms; } &:hover::after { - display: block; + width: 100%; } } diff --git a/src/components/RestaurantsListPage/RestaurantsListPage.js b/src/components/RestaurantsListPage/RestaurantsListPage.js index ef4e876..f7e946a 100644 --- a/src/components/RestaurantsListPage/RestaurantsListPage.js +++ b/src/components/RestaurantsListPage/RestaurantsListPage.js @@ -13,6 +13,7 @@ export class RestaurantsListPage extends Component { const { loadRestaurants } = this.props; loadRestaurants(); + window.scrollTo(0, 0); } render() { diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 30f92c4..054ab01 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -6,3 +6,4 @@ $breack-point-tablet-lg: 956px; $breack-point-tablet: 768px; $breack-point-phone-lg: 620px; $breack-point-phone: 550px; +$breack-point-phone-sm: 400px; From e567c7bd15a600473c9369fc970cfe86d95310cb Mon Sep 17 00:00:00 2001 From: igaryok Date: Thu, 24 Oct 2019 09:57:24 +0300 Subject: [PATCH 14/34] Update src/components/Section/index.js Co-Authored-By: Iryna Holovko <33497431+irenhh@users.noreply.github.com> --- src/components/Section/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Section/index.js b/src/components/Section/index.js index 6d8745c..1df7911 100644 --- a/src/components/Section/index.js +++ b/src/components/Section/index.js @@ -4,7 +4,7 @@ import { selectRestorauntItems, } from '../../store/selector'; -const mapState2Props = state => ({ +const mapStateToProps = state => ({ restaurantItems: selectRestorauntItems(state), }); From f71bfb8bb583a2d0c1912ffc9c24d963334840de Mon Sep 17 00:00:00 2001 From: igaryok Date: Thu, 24 Oct 2019 10:10:52 +0300 Subject: [PATCH 15/34] partial solution --- src/components/CardItem/index.js | 4 ++-- src/components/RestaurantPage/index.js | 8 ++++---- src/components/Section/index.js | 2 +- src/components/Select/Select.scss | 4 ---- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/CardItem/index.js b/src/components/CardItem/index.js index 4afc887..24ab6bd 100644 --- a/src/components/CardItem/index.js +++ b/src/components/CardItem/index.js @@ -4,12 +4,12 @@ import { selectRestaurantCurency, } from '../../store/selector'; -const mapState2Props = state => ({ +const mapStateToProps = state => ({ restaurantCurency: selectRestaurantCurency(state), }); const Enhanced = connect( - mapState2Props, + mapStateToProps, )(CardItem); export { Enhanced as CardItem }; diff --git a/src/components/RestaurantPage/index.js b/src/components/RestaurantPage/index.js index 1f414dc..116b7c9 100644 --- a/src/components/RestaurantPage/index.js +++ b/src/components/RestaurantPage/index.js @@ -6,18 +6,18 @@ import { selectResorauntSections, } from '../../store/selector'; -const mapState2Props = state => ({ +const mapStateToProps = state => ({ restaurantPageData: selectRestaurantPageData(state), restaurantSections: selectResorauntSections(state), }); -const mapDispatch2Props = { +const mapDispatchToProps = { loadRestaurantPage, }; const Enhanced = connect( - mapState2Props, - mapDispatch2Props + mapStateToProps, + mapDispatchToProps )(RestaurantPage); export { Enhanced as RestaurantPage }; diff --git a/src/components/Section/index.js b/src/components/Section/index.js index 1df7911..fe79e06 100644 --- a/src/components/Section/index.js +++ b/src/components/Section/index.js @@ -9,7 +9,7 @@ const mapStateToProps = state => ({ }); const Enhanced = connect( - mapState2Props, + mapStateToProps, )(Section); export { Enhanced as Section }; diff --git a/src/components/Select/Select.scss b/src/components/Select/Select.scss index f7a9334..1898b00 100644 --- a/src/components/Select/Select.scss +++ b/src/components/Select/Select.scss @@ -13,10 +13,6 @@ border: 1px solid #979797; border-radius: 0; cursor: pointer; - - // option { - // background-color: transparent; - // } } From 55a9b2533360777c456cb4e98284afe6996ef963 Mon Sep 17 00:00:00 2001 From: igaryok Date: Thu, 24 Oct 2019 11:14:58 +0300 Subject: [PATCH 16/34] partial solution --- src/components/Footer/Footer.scss | 4 +++- src/components/RestaurantPage/RestaurantPage.scss | 4 ++-- src/styles/index.scss | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss index 4bc5de1..ba1b041 100644 --- a/src/components/Footer/Footer.scss +++ b/src/components/Footer/Footer.scss @@ -123,8 +123,10 @@ color: #fff; white-space: nowrap; + transition: color 300ms; + &:hover { - text-decoration: underline; + color: $color-green; } } } diff --git a/src/components/RestaurantPage/RestaurantPage.scss b/src/components/RestaurantPage/RestaurantPage.scss index 38056d2..50d903f 100644 --- a/src/components/RestaurantPage/RestaurantPage.scss +++ b/src/components/RestaurantPage/RestaurantPage.scss @@ -82,7 +82,7 @@ margin-right: 20px; } - &::after { + &:after { content: ""; height: 1px; width: 0; @@ -93,7 +93,7 @@ transition: 500ms; } - &:hover::after { + &:hover:after { width: 100%; } diff --git a/src/styles/index.scss b/src/styles/index.scss index 8922856..3bc36f7 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -26,6 +26,7 @@ select { a { text-decoration: none; color: #000; + outline: none; } button { From a904cccafe202b8383572f2fd5cb4bbe79e5dd93 Mon Sep 17 00:00:00 2001 From: igaryok Date: Sat, 26 Oct 2019 14:56:04 +0300 Subject: [PATCH 17/34] partial solution --- .../RestaurantPage/RestaurantPage.js | 38 ++++++--- .../RestaurantPage/RestaurantPage.scss | 82 +++++++++++++++++-- 2 files changed, 103 insertions(+), 17 deletions(-) diff --git a/src/components/RestaurantPage/RestaurantPage.js b/src/components/RestaurantPage/RestaurantPage.js index de52f03..d94fbcd 100644 --- a/src/components/RestaurantPage/RestaurantPage.js +++ b/src/components/RestaurantPage/RestaurantPage.js @@ -57,18 +57,34 @@ export class RestaurantPage extends PureComponent { src={heroImageUrl} alt={title} /> -
-

{title}

-
- {categories.join(' • ')} -
-
- {etaRange || DEFAULT_ETA_RANGE} -
-
- {`${location.address} • `} - More info + +
+
+
+

{title}

+
+ {categories.join(' • ')} +
+
+ {etaRange || DEFAULT_ETA_RANGE} +
+
+ {`${location.address} • `} + More info +
+
+
+
+
+ +
+

{title}

+
+ {categories.join(' • ')} +
+
+ {etaRange || DEFAULT_ETA_RANGE}
diff --git a/src/components/RestaurantPage/RestaurantPage.scss b/src/components/RestaurantPage/RestaurantPage.scss index 50d903f..82957c4 100644 --- a/src/components/RestaurantPage/RestaurantPage.scss +++ b/src/components/RestaurantPage/RestaurantPage.scss @@ -1,3 +1,5 @@ +@import '../../styles/utils.scss'; + .hero { position: relative; height: 380px; @@ -6,6 +8,10 @@ overflow: hidden; + @media (max-width: $breack-point-tablet-lg) { + margin-bottom: 90px; + } + &__img { position: absolute; top: 50%; @@ -15,19 +21,18 @@ } .title { - position: absolute; - top: 50%; - left: 0; - transform: translateY(-50%); - display: flex; flex-direction: column; align-items: flex-start; font-size: 14px; + @media (max-width: $breack-point-tablet-lg) { + display: none; + } + + width: fit-content; background: #fff; - padding-left: 100px; padding-right: 40px; padding-top: 24px; padding-bottom: 24px; @@ -100,3 +105,68 @@ } } +.title-mobile { + display: none; + + position: absolute; + + width: 575px; + + // display: flex; + flex-direction: column; + align-items: flex-start; + + font-size: 14px; + + background: #fff; + padding-left: 62px; + padding-top: 24px; + padding-bottom: 24px; + + &__name { + color: #1d1d1d; + font-size: 28px; + font-weight: 500; + + margin-bottom: 16px; + } + + &__categories { + color: #757575; + + margin-bottom: 16px; + } + + &__eta { + width: 86px; + background-color: #f5f5f5; + + margin-bottom: 18px; + } +} + +.wrapper-title { + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + width: 100%; + background: transparent; +} + +.left-margin-block { + position: absolute; + background: #fff; + height: 100%; + width: 30%; + top: 0; + z-index: -1; + + @media (min-width: 3500px) { + width: 40%; + } + + @media (min-width: 7200px) { + width: 50%; + } +} From dd4f6c41e89455fb10c4f54173608080eb8b1659 Mon Sep 17 00:00:00 2001 From: igaryok Date: Sat, 26 Oct 2019 19:23:44 +0300 Subject: [PATCH 18/34] partial solution - hero of restoran page --- .../RestaurantPage/RestaurantPage.js | 2 + .../RestaurantPage/RestaurantPage.scss | 44 +++++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/components/RestaurantPage/RestaurantPage.js b/src/components/RestaurantPage/RestaurantPage.js index d94fbcd..a199255 100644 --- a/src/components/RestaurantPage/RestaurantPage.js +++ b/src/components/RestaurantPage/RestaurantPage.js @@ -76,6 +76,7 @@ export class RestaurantPage extends PureComponent {
+
@@ -87,6 +88,7 @@ export class RestaurantPage extends PureComponent { {etaRange || DEFAULT_ETA_RANGE}
+
+ + {restaurantSections + .map(item =>
) + } +
+ + ); +}; const restorauntShape = { title: PropTypes.string, diff --git a/src/store/selector.js b/src/store/selector.js index 8070529..55c4c78 100644 --- a/src/store/selector.js +++ b/src/store/selector.js @@ -31,14 +31,14 @@ export const selectResorauntSections = createSelector( export const selectRestorauntItems = createSelector( rootSelector, selectResorauntSections, - ({ restaurantPageData }, resorauntSections) => { + ({ restaurantPageData }, restorauntSections) => { if (!restaurantPageData) { return []; } const { entitiesMap } = restaurantPageData; - return resorauntSections + return restorauntSections .map(item => ({ ...item, itemUuids: item.itemUuids.map(uid => entitiesMap[uid]), From f591389900cb74dba045dd38e2e955fb041a2792 Mon Sep 17 00:00:00 2001 From: igaryok Date: Tue, 29 Oct 2019 22:20:18 +0200 Subject: [PATCH 23/34] partial solution - regactoring --- package.json | 3 +- public/images/button-close.svg | 3 + public/images/button-minus.svg | 4 + public/images/button-plus.svg | 5 + public/images/no_image.png | Bin 0 -> 18632 bytes src/App.js | 26 ----- src/components/App/App.js | 29 ++++++ src/{ => components/App}/App.scss | 0 src/components/App/index.js | 18 ++++ src/components/CardItem/CardItem.js | 38 ++++++-- src/components/CardItem/CardItem.scss | 22 ++++- src/components/CardItem/index.js | 7 ++ src/components/Error/Error.js | 4 +- src/components/Footer/Footer.js | 2 + src/components/Header/Header.js | 2 + src/components/Input/Input.js | 1 + src/components/Loader/Loader.js | 1 + src/components/Order/Order.js | 84 ++++++++++++++++ src/components/Order/Order.scss | 92 ++++++++++++++++++ src/components/Order/index.js | 29 ++++++ .../RestaurantCard/RestaurantCard.js | 1 + .../RestaurantPage/RestaurantPage.js | 4 +- .../RestaurantPage/RestaurantPage.scss | 4 + src/components/RestaurantPage/index.js | 2 + .../RestaurantsListPage.js | 4 +- src/components/RestaurantsListPage/index.js | 1 + src/components/Section/Section.js | 7 +- src/components/Section/index.js | 1 + src/components/Select/Select.js | 1 + src/index.js | 14 ++- src/store/actions.js | 28 +++++- src/store/index.js | 1 + src/store/rootReducer.js | 21 ++++ src/store/selector.js | 16 +++ 34 files changed, 424 insertions(+), 51 deletions(-) create mode 100644 public/images/button-close.svg create mode 100644 public/images/button-minus.svg create mode 100644 public/images/button-plus.svg create mode 100644 public/images/no_image.png delete mode 100644 src/App.js create mode 100644 src/components/App/App.js rename src/{ => components/App}/App.scss (100%) create mode 100644 src/components/App/index.js create mode 100644 src/components/Order/Order.js create mode 100644 src/components/Order/Order.scss create mode 100644 src/components/Order/index.js diff --git a/package.json b/package.json index ebce6b5..d537744 100755 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "react-scripts": "3.0.1", "redux": "^4.0.4", "redux-thunk": "^2.3.0", - "reselect": "^4.0.0", - "uuidv4": "^5.0.1" + "reselect": "^4.0.0" }, "devDependencies": { "@mate-academy/eslint-config-react": "*", diff --git a/public/images/button-close.svg b/public/images/button-close.svg new file mode 100644 index 0000000..ed1a1b8 --- /dev/null +++ b/public/images/button-close.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/button-minus.svg b/public/images/button-minus.svg new file mode 100644 index 0000000..8798241 --- /dev/null +++ b/public/images/button-minus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/button-plus.svg b/public/images/button-plus.svg new file mode 100644 index 0000000..7007058 --- /dev/null +++ b/public/images/button-plus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/no_image.png b/public/images/no_image.png new file mode 100644 index 0000000000000000000000000000000000000000..73cc6df92e9720a1b229079f329a7f0eb8cc3ce4 GIT binary patch literal 18632 zcmV*GKxw~;P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 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 ( - - -
-
- - - - -
-