diff --git a/.stylelintrc.js b/.stylelintrc.js index f3a4e74..d5fdb09 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,4 +1,4 @@ module.exports = { extends: "@mate-academy/stylelint-config", - rules: {} + rules: {}, }; diff --git a/README.md b/README.md index 84c7a15..85b7b99 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,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 @@ -57,7 +57,7 @@ Base API url: https://mate-uber-eats-api.herokuapp.com/api/v1/ - open basket sidebar when click on busket button, close on click outside or close icon - add item to basket when click submit in MenuItemModal - show list added items in basket - - add ability to change item count, remove item + - add ability to change item count, remove item - when user click edit on item - open MenuItemModal with additional remove button. After submit, edit current item instead of add new one - clear basket when user click order button 5. Restore user session after close-open tab @@ -69,27 +69,27 @@ 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://OlehTereshchuk.github.io/react_uber-eats/) - 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. diff --git a/package.json b/package.json index 7bd01de..2068948 100755 --- a/package.json +++ b/package.json @@ -7,16 +7,21 @@ "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.3", "react-router-dom": "^5.0.1", - "react-scripts": "3.0.1" + "react-scripts": "^3.3.0", + "redux": "^4.0.5", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" }, "devDependencies": { "@mate-academy/eslint-config-react": "*", "@mate-academy/stylelint-config": "*", - "eslint": "^5.16.0", "gh-pages": "^2.1.1", "husky": "^1.3.1", "lint-staged": "^8.1.5", diff --git a/public/images/appstore.png b/public/images/appstore.png new file mode 100644 index 0000000..6c2ff05 Binary files /dev/null and b/public/images/appstore.png differ 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/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/public/images/delivery.svg b/public/images/delivery.svg new file mode 100644 index 0000000..ea10186 --- /dev/null +++ b/public/images/delivery.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/facebook.svg b/public/images/facebook.svg new file mode 100644 index 0000000..22565b3 --- /dev/null +++ b/public/images/facebook.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/google-play.png b/public/images/google-play.png new file mode 100644 index 0000000..b988c6f Binary files /dev/null and b/public/images/google-play.png differ diff --git a/public/images/instagram.svg b/public/images/instagram.svg new file mode 100644 index 0000000..7c3e6a0 --- /dev/null +++ b/public/images/instagram.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/logo-footer.svg b/public/images/logo-footer.svg new file mode 100644 index 0000000..c3894d1 --- /dev/null +++ b/public/images/logo-footer.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/logo.svg b/public/images/logo.svg new file mode 100644 index 0000000..3124de7 --- /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..ea09167 --- /dev/null +++ b/public/images/place.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..9c8b7bd --- /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..b8806c7 --- /dev/null +++ b/public/images/world.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/index.html b/public/index.html index 4269d67..ef75689 100644 --- a/public/index.html +++ b/public/index.html @@ -3,6 +3,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..89be8ac 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,19 @@ import React from 'react'; -import './App.css'; +import Header from './components/Header'; +import Footer from './components/Footer'; +import RestaurantsListPage from './components/RestaurantsListPage'; +import './styles/Page.scss'; const App = () => ( -
-

React Uber eats

-
+ <> +
+
+
+ +
+
+
+ ); export default App; diff --git a/src/components/Error.js b/src/components/Error.js new file mode 100644 index 0000000..daa5ba4 --- /dev/null +++ b/src/components/Error.js @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import '../styles/Error.scss'; + +const Error = ({ message }) => ( +
+

{message}

+ Go to home +
+); + +Error.propTypes = { + message: PropTypes.string, +}; + +Error.defaultProps = { + message: 'Sorry, something went wrong', +}; + +export default Error; diff --git a/src/components/Footer.js b/src/components/Footer.js new file mode 100644 index 0000000..7c180a5 --- /dev/null +++ b/src/components/Footer.js @@ -0,0 +1,80 @@ +import React from 'react'; +import Select from './Select'; +import '../styles/Footer.scss'; + +const Footer = () => ( +
+
+
+
+
+ Uber Eats + + +
+ +
+ +
+ +
+ + + +
+ + Sign in +
+ + {(isMobileDeliveryVisible || isMobileSearchVisible) && ( +
+ {isMobileSearchVisible && ( + + )} + + {isMobileDeliveryVisible && ( + <> + + + + )} + + +
+ )} +
+
+ ); +}; + +export default Header; diff --git a/src/components/Input.js b/src/components/Input.js new file mode 100644 index 0000000..fc69950 --- /dev/null +++ b/src/components/Input.js @@ -0,0 +1,80 @@ +import React, { useState, createRef } from 'react'; +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import '../styles/Input.scss'; + +const Input = ({ + iconUrl, value, onChange, placeholder, name, className, type, isSmall, label, +}) => { + const [isFocused, setFocused] = useState(false); + + const handleFocus = () => setFocused(true); + const handleBlur = () => setFocused(false); + const inputRef = createRef(); + + const inputWrapperClass = cx('control__input-wrapper', { + 'control__input-wrapper--focused': isFocused, + [className]: className, + }); + + const inputClass = cx({ + 'control__input--small': isSmall, + 'control__input--time': type === 'time', + }); + + return ( + // eslint-disable-next-line + + ); +}; + +Input.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + className: PropTypes.string, + iconUrl: PropTypes.string, + placeholder: PropTypes.string, + isSmall: PropTypes.bool, + label: PropTypes.string, + type: PropTypes.string, +}; + +Input.defaultProps = { + type: '', + label: '', + className: '', + iconUrl: '', + placeholder: '', + isSmall: true, +}; + +export default Input; diff --git a/src/components/Loader.js b/src/components/Loader.js new file mode 100644 index 0000000..3d17287 --- /dev/null +++ b/src/components/Loader.js @@ -0,0 +1,24 @@ +import React from 'react'; +import '../styles/Loader.scss'; + +const Loader = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +); + +export default Loader; diff --git a/src/components/RestaurantCard.js b/src/components/RestaurantCard.js new file mode 100644 index 0000000..93356aa --- /dev/null +++ b/src/components/RestaurantCard.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import '../styles/RestaurantCard.scss'; + +const RestaurantCard = ({ imageUrl, title, categories, etaRange, uuid }) => ( +
+ {title} +

{title}

+
+ {categories.join(' • ')} +
+
{etaRange}
+
+); + +RestaurantCard.propTypes = { + imageUrl: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + categories: PropTypes.arrayOf(PropTypes.string), + etaRange: PropTypes.string, + uuid: PropTypes.string.isRequired, +}; + +RestaurantCard.defaultProps = { + categories: [], + etaRange: '', +}; + +export default RestaurantCard; diff --git a/src/components/RestaurantsListPage.js b/src/components/RestaurantsListPage.js new file mode 100644 index 0000000..c643ce4 --- /dev/null +++ b/src/components/RestaurantsListPage.js @@ -0,0 +1,72 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import RestaurantCard from './RestaurantCard'; +import { + loadData, getRestaurants, getIsLoading, getError, +} from '../store/store'; +import '../styles/RestaurantsList.scss'; +import Loader from './Loader'; +import Error from './Error'; + +const DEFAULT_ETA_RANGE = '20 - 30 min'; + +const RestaurantsListPage = ({ + restaurants, loadDataFromServer, isLoading, error, +}) => { + useEffect(() => { + loadDataFromServer(); + }, [loadDataFromServer]); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( +
+ {restaurants.map(( + { heroImageUrl, title, categories, etaRange, uuid }, + ) => ( + + ))} +
+ ); +}; + +const mapStateToProps = state => ({ + restaurants: getRestaurants(state), + isLoading: getIsLoading(state), + error: getError(state), +}); + +const mapMethodsToProps = { + loadDataFromServer: loadData, +}; + +RestaurantsListPage.propTypes = { + restaurants: PropTypes.arrayOf(PropTypes.object), + loadDataFromServer: PropTypes.func.isRequired, + error: PropTypes.string, + isLoading: PropTypes.bool, +}; + +RestaurantsListPage.defaultProps = { + restaurants: [], + error: null, + isLoading: false, +}; + +export default connect(mapStateToProps, mapMethodsToProps)(RestaurantsListPage); diff --git a/src/components/Select.js b/src/components/Select.js new file mode 100644 index 0000000..f0106e0 --- /dev/null +++ b/src/components/Select.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import '../styles/Select.scss'; + +const Select = ({ name, value, onSelect, options, iconUrl }) => ( +
+ + {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: '', +}; + +export default Select; diff --git a/src/index.js b/src/index.js index b597a44..f1c1672 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,13 @@ import React from 'react'; +import { Provider } from 'react-redux'; import ReactDOM from 'react-dom'; +import store from './store/store'; import App from './App'; +import './styles/default.scss'; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + + + , + document.getElementById('root'), +); diff --git a/src/store/errorReducer.js b/src/store/errorReducer.js new file mode 100644 index 0000000..ca021c5 --- /dev/null +++ b/src/store/errorReducer.js @@ -0,0 +1,17 @@ +const LOAD_ERROR = 'LOAD_ERROR'; + +export const setLoadError = error => ({ + type: LOAD_ERROR, + error, +}); + +const errorReducer = (error = null, action) => { + switch (action.type) { + case LOAD_ERROR: + return action.error; + default: + return error; + } +}; + +export default errorReducer; diff --git a/src/store/loadDataFromServer.js b/src/store/loadDataFromServer.js new file mode 100644 index 0000000..971b93a --- /dev/null +++ b/src/store/loadDataFromServer.js @@ -0,0 +1,7 @@ +const URL = 'https://mate-uber-eats-api.herokuapp.com/api/v1/restaurants'; + +export const loadDataFromServer = async() => { + const response = await fetch(URL); + + return response.json(); +}; diff --git a/src/store/loadingReducer.js b/src/store/loadingReducer.js new file mode 100644 index 0000000..73680a0 --- /dev/null +++ b/src/store/loadingReducer.js @@ -0,0 +1,23 @@ +const START_LOADING = 'START_LOADING'; +const STOP_LOADING = 'STOP_LOADING'; + +export const startLoading = () => ({ + type: START_LOADING, +}); + +export const stopLoading = () => ({ + type: STOP_LOADING, +}); + +const loadingReducer = (isLoading = false, action) => { + switch (action.type) { + case START_LOADING: + return true; + case STOP_LOADING: + return false; + default: + return isLoading; + } +}; + +export default loadingReducer; diff --git a/src/store/restaurantsReducer.js b/src/store/restaurantsReducer.js new file mode 100644 index 0000000..d50296f --- /dev/null +++ b/src/store/restaurantsReducer.js @@ -0,0 +1,17 @@ +const SAVE_RESTAURANTS = 'SAVE_RESTAURANTS'; + +export const saveRestaurants = restaurants => ({ + type: SAVE_RESTAURANTS, + restaurants, +}); + +const restaurantsReducer = (restaurants = null, action) => { + switch (action.type) { + case SAVE_RESTAURANTS: + return action.restaurants; + default: + return restaurants; + } +}; + +export default restaurantsReducer; diff --git a/src/store/store.js b/src/store/store.js new file mode 100644 index 0000000..46974f5 --- /dev/null +++ b/src/store/store.js @@ -0,0 +1,43 @@ +import { createStore, applyMiddleware, combineReducers } from 'redux'; +import thunk from 'redux-thunk'; +import { loadDataFromServer } from './loadDataFromServer'; +import restaurantsReducer, { saveRestaurants } from './restaurantsReducer'; +import loadingReducer, { startLoading, stopLoading } from './loadingReducer'; +import errorReducer, { setLoadError } from './errorReducer'; + +export const loadData = () => async(dispatch) => { + dispatch(startLoading()); + + try { + const restaurants = await loadDataFromServer(); + + dispatch(saveRestaurants(restaurants.data)); + } catch (e) { + dispatch(setLoadError(e.message)); + } + + dispatch(stopLoading()); +}; + +const rootReducer = combineReducers({ + restaurants: restaurantsReducer, + isLoading: loadingReducer, + error: errorReducer, +}); + +export const getRestaurants = (state) => { + if (!state.restaurants) { + return []; + } + + const { feedItems, storesMap } = state.restaurants; + + return feedItems.map(({ uuid }) => storesMap[uuid]); +}; + +export const getIsLoading = state => state.isLoading; +export const getError = state => state.error; + +const store = createStore(rootReducer, applyMiddleware(thunk)); + +export default store; diff --git a/src/styles/Error.scss b/src/styles/Error.scss new file mode 100644 index 0000000..955a701 --- /dev/null +++ b/src/styles/Error.scss @@ -0,0 +1,20 @@ +@import 'extends'; + +.error { + @extend %absolute-center; + flex-flow: column nowrap; + + &__text { + font-size: 30px; + font-weight: 500; + } + + &__link { + font-size: 24px; + + &:hover { + text-decoration: underline; + color: #5eb707; + } + } + } diff --git a/src/styles/Footer.scss b/src/styles/Footer.scss new file mode 100644 index 0000000..9be3409 --- /dev/null +++ b/src/styles/Footer.scss @@ -0,0 +1,143 @@ +@import "extends"; +@import "variables"; + +.footer { + background-color: #262626; + padding: 64px 0; + + @media (max-width: $breakpoint-tablet) { + padding: 40px 0; + } + + &__main { + display: flex; + flex-flow: column nowrap; + justify-content: space-between; + } + + &__top-part { + border-bottom: 1px solid #c4c4c4; + margin-bottom: 40px; + padding-bottom: 40px; + } + + &__logo { + margin-right: 40px; + + @media (max-width: $breakpoint-tablet) { + margin-bottom: 40px; + } + } + + &__logo-container { + @extend %flex-row; + margin-bottom: 40px; + + @media (max-width: $breakpoint-tablet) { + flex-direction: column; + align-items: flex-start; + } + } + + &__top-part, + &__bottom-part { + @extend %flex-row; + align-items: flex-start; + + @media (max-width: $breakpoint-phone-lg) { + flex-direction: column; + } + } + + &__main, + &__copyright { + flex-basis: 45vw; + padding-right: 40px; + + @media (max-width: $breakpoint-tablet) { + margin-left: 0; + flex-basis: initial; + } + } + + &__copyright { + color: white; + font-size: 14px; + + @media (max-width: $breakpoint-phone-lg) { + order: 1; + } + } + + &__mobile-app { + display: inline-block; + + @media (max-width: $breakpoint-tablet) { + display: block; + margin-bottom: 20px; + } + + &:first-of-type { + margin-right: 20px; + + @media (max-width: $breakpoint-tablet) { + margin-right: 0; + margin-bottom: 20px; + } + } + } + + &__top-links { + &:last-of-type { + margin-left: 10vw; + + @media (max-width: $breakpoint-phone-lg) { + margin-left: 0; + } + } + } + + &__bottom-links { + @extend %flex-row; + margin-bottom: 40px; + + .links__link:not(:last-of-type) { + margin-right: 40px; + } + + @media (max-width: $breakpoint-phone-lg) { + flex-direction: column; + align-items: flex-start; + margin-top: 20px; + order: 1; + } + } + + &__misc { + @media (max-width: $breakpoint-phone-lg) { + display: flex; + flex-direction: column; + margin-top: 20px; + } + } +} + +.links { + &__link { + display: block; + margin-bottom: 16px; + font-weight: 500; + color: white; + white-space: nowrap; + + &:hover { + text-decoration: underline; + } + } +} + +.social { + &__link { + margin-right: 20px; + } +} diff --git a/src/styles/Header.scss b/src/styles/Header.scss new file mode 100644 index 0000000..c467e06 --- /dev/null +++ b/src/styles/Header.scss @@ -0,0 +1,97 @@ +@import "extends"; +@import "variables"; + +.header { + position: sticky; + top: 0; + z-index: 1; + background-color: white; + padding: 16px 0; + + &__inner { + @extend %flex-row; + } + + &__logo { + height: 24px; + width: auto; + + @media (max-width: $breakpoint-phone) { + height: 14px; + } + } + + &__delivery-info { + @extend %flex-row; + margin-left: 7vw; + + @media (max-width: $breakpoint-tablet) { + display: none; + } + } + + &__search { + margin-left: auto; + + @media (max-width: $breakpoint-tablet) { + display: none; + } + } + + &__link { + margin-left: 40px; + padding: 12px 0; + color: #1f1f1f; + white-space: nowrap; + + &:hover { + text-decoration: underline; + } + } + + &__toggle-buttons { + display: none; + margin-left: auto; + + @media (max-width: $breakpoint-tablet) { + display: flex; + } + } + + &__toggle-btn { + display: flex; + align-items: center; + border: 1px solid transparent; + padding: 11px 5px; + cursor: pointer; + + &:not(:last-of-type) { + margin-right: 20px; + } + + &:focus { + border-color: #e0e0e0; + } + + &--place { + width: 16px; + height: 16px; + } + } +} + +.mobile-controls { + display: none; + position: relative; + padding-top: 30px; + + @media (max-width: $breakpoint-tablet) { + display: flex; + } + + &__close { + position: absolute; + top: 10px; + right: 0; + } +} diff --git a/src/styles/Input.scss b/src/styles/Input.scss new file mode 100644 index 0000000..fecbf82 --- /dev/null +++ b/src/styles/Input.scss @@ -0,0 +1,47 @@ +@import "extends"; +@import "variables"; + +.control { + + &__input-wrapper { + @extend %flex-row; + 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; + + &--time { + max-width: 75px; + } + + &--small { + @media (max-width: $breakpoint-tablet-lg) { + max-width: 75px; + } + } + } + + &__label { + font-size: 14px; + margin-bottom: 8px; + color: #626262; + } + + & + & { + margin-left: 20px; + } +} diff --git a/src/styles/Loader.scss b/src/styles/Loader.scss new file mode 100644 index 0000000..1b50b0a --- /dev/null +++ b/src/styles/Loader.scss @@ -0,0 +1,102 @@ +@import 'extends'; + +.loader-container { + @extend %absolute-center; +} + +.lds-default { + display: inline-block; + position: relative; + width: 80px * 2; + height: 80px * 2; +} + +.lds-default div { + position: absolute; + width: 6px * 2; + height: 6px * 2; + background: #5eb707; + border-radius: 50%; + animation: lds-default 1.2s linear infinite; +} + +.lds-default div:nth-child(1) { + animation-delay: 0s; + top: 37px * 2; + left: 66px * 2; +} + +.lds-default div:nth-child(2) { + animation-delay: -0.1s; + top: 22px * 2; + left: 62px * 2; +} + +.lds-default div:nth-child(3) { + animation-delay: -0.2s; + top: 11px * 2; + left: 52px * 2; +} + +.lds-default div:nth-child(4) { + animation-delay: -0.3s; + top: 7px * 2; + left: 37px * 2; +} + +.lds-default div:nth-child(5) { + animation-delay: -0.4s; + top: 11px * 2; + left: 22px * 2; +} + +.lds-default div:nth-child(6) { + animation-delay: -0.5s; + top: 22px * 2; + left: 11px * 2; +} + +.lds-default div:nth-child(7) { + animation-delay: -0.6s; + top: 37px * 2; + left: 7px * 2; +} + +.lds-default div:nth-child(8) { + animation-delay: -0.7s; + top: 52px * 2; + left: 11px * 2; +} + +.lds-default div:nth-child(9) { + animation-delay: -0.8s; + top: 62px * 2; + left: 22px * 2; +} + +.lds-default div:nth-child(10) { + animation-delay: -0.9s; + top: 66px * 2; + left: 37px * 2; +} + +.lds-default div:nth-child(11) { + animation-delay: -1s; + top: 62px * 2; + left: 52px * 2; +} + +.lds-default div:nth-child(12) { + animation-delay: -1.1s; + top: 52px * 2; + left: 62px * 2; +} + +@keyframes lds-default { + 0%, 20%, 80%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.5); + } +} diff --git a/src/styles/Page.scss b/src/styles/Page.scss new file mode 100644 index 0000000..4a63ea4 --- /dev/null +++ b/src/styles/Page.scss @@ -0,0 +1,6 @@ +.page { + position: relative; + padding-top: 40px; + min-height: 100vh; + margin-bottom: 40px; +} diff --git a/src/styles/RestaurantCard.scss b/src/styles/RestaurantCard.scss new file mode 100644 index 0000000..a163d16 --- /dev/null +++ b/src/styles/RestaurantCard.scss @@ -0,0 +1,37 @@ +@import "variables"; + +.restaurant-card { + --height: 367px; + + height: var(--height); + width: 100%; + cursor: pointer; + + @media (max-width: $breakpoint-phone) { + --height: 330px; + } + + &__img { + height: calc(var(--height) * 0.7); + width: 100%; + object-fit: cover; + } + + &__title { + margin: 12px 0 4px; + } + + &__categories { + font-size: 14px; + color: #757575; + margin-bottom: 4px; + } + + &__eta { + display: inline-block; + font-size: 14px; + font-weight: 700; + padding: 2px 8px; + background-color: #f5f5f5; + } +} diff --git a/src/styles/RestaurantsList.scss b/src/styles/RestaurantsList.scss new file mode 100644 index 0000000..0339e02 --- /dev/null +++ b/src/styles/RestaurantsList.scss @@ -0,0 +1,17 @@ +@import "variables"; + +.restaurants-list { + --card-width: 348px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--card-width), 1fr)); + gap: 40px 20px; + + @media (max-width: $breakpoint-tablet-lg) { + --card-width: 340px; + row-gap: 20px; + } + + @media (max-width: $breakpoint-phone) { + --card-width: 280px; + } +} diff --git a/src/styles/Select.scss b/src/styles/Select.scss new file mode 100644 index 0000000..d51c967 --- /dev/null +++ b/src/styles/Select.scss @@ -0,0 +1,33 @@ +.select { + position: relative; + + &__input { + -moz-appearance: none; + -webkit-appearance: none; + + background-color: transparent; + border: 1px solid #979797; + border-radius: 0; + padding: 10px 40px; + color: white; + cursor: pointer; + } + + &__icon { + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 10px; + width: 20px; + height: 20px; + } + + &__arrow { + position: absolute; + top: 50%; + right: 10px; + width: 14px; + height: 8px; + transform: translateY(-50%); + } +} diff --git a/src/styles/default.scss b/src/styles/default.scss new file mode 100644 index 0000000..e2b1e8f --- /dev/null +++ b/src/styles/default.scss @@ -0,0 +1,47 @@ +@import "variables"; + +*, +::before, +::after { + box-sizing: border-box; +} + +body, +input, +a, +select, +button { + font-family: Roboto, sans-serif; + font-size: 16px; + line-height: 1.5; +} + +input { + border: none; + outline: none; + background-color: transparent; +} + +a { + text-decoration: none; +} + +button { + border: none; + background: none; + outline: none; +} + +.content { + max-width: 1160px; + padding: 0 34px; + margin: 0 auto; + + @media (max-width: $breakpoint-phone) { + padding: 0 20px; + } + + @media (min-width: $breakpoint-large) { + max-width: 1520px; + } +} diff --git a/src/styles/extends.scss b/src/styles/extends.scss new file mode 100644 index 0000000..15deefc --- /dev/null +++ b/src/styles/extends.scss @@ -0,0 +1,16 @@ +%flex-row { + 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/variables.scss b/src/styles/variables.scss new file mode 100644 index 0000000..0ea9a9c --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,7 @@ +$breakpoint-large: 1281px; +$breakpoint-tablet-lg: 976px; +$breakpoint-tablet: 768px; +$breakpoint-phone-lg: 620px; +$breakpoint-phone: 550px; +$border-color: #e0e0e0; +