├── .DS_Store ├── .gitignore ├── package-lock.json ├── package.json ├── public └── index.html └── src ├── actions └── index.js ├── components ├── app-header │ ├── app-header.js │ ├── app-header.scss │ ├── index.js │ └── shopping-cart-solid.svg ├── app │ ├── app.js │ ├── food-bg.jpg │ └── index.js ├── cart-table │ ├── cart-table.js │ ├── cart-table.scss │ └── index.js ├── error-boundary │ ├── error-boundary.js │ └── index.js ├── error │ ├── error.js │ └── index.js ├── hoc │ ├── index.js │ └── with-resto-service.js ├── menu-list-item │ ├── index.js │ ├── menu-list-item.js │ └── menu-list-item.scss ├── menu-list │ ├── index.js │ ├── menu-list.js │ └── menu-list.scss ├── pages │ ├── cart-page.js │ ├── index.js │ └── main-page.js ├── resto-service-context │ ├── index.js │ └── resto-service-context.js └── spinner │ ├── index.js │ └── spinner.js ├── db.json ├── index.js ├── index.scss ├── reducers └── index.js ├── services └── resto-service.js └── store.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrey-kudinov/react-redux-food-app/ac31bc9011b077c09cf9ef5175993ddb21e2298e/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-food-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "json-server": "^0.16.3", 7 | "node-sass": "^4.12.0", 8 | "prop-types": "^15.7.2", 9 | "react": "^16.8.6", 10 | "react-dom": "^16.8.6", 11 | "react-redux": "^7.1.0", 12 | "react-router-dom": "^5.0.1", 13 | "react-scripts": "3.0.1", 14 | "redux": "^4.0.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React App 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | const menuLoaded = (newMenu) => { 2 | return { 3 | type: "MENU_LOADED", 4 | payload: newMenu, 5 | }; 6 | }; 7 | 8 | const menuRequested = () => { 9 | return { 10 | type: "MENU_REQUESTED", 11 | }; 12 | }; 13 | 14 | const addedToCart = (id) => { 15 | return { 16 | type: "ITEM_ADD_TO_CART", 17 | payload: id, 18 | }; 19 | }; 20 | 21 | const deleteFromCart = (id) => { 22 | return { 23 | type: "ITEM_REMOVE_FROM_CART", 24 | payload: id, 25 | }; 26 | }; 27 | 28 | const changeTotal = (value) => { 29 | return { 30 | type: "CHANGE_TOTAL", 31 | payload: value, 32 | }; 33 | }; 34 | 35 | const loadTotal = () => { 36 | return { 37 | type: "LOAD_TOTAL", 38 | }; 39 | }; 40 | 41 | export { menuLoaded, menuRequested, addedToCart, deleteFromCart, changeTotal, loadTotal }; 42 | -------------------------------------------------------------------------------- /src/components/app-header/app-header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import cartIcon from "./shopping-cart-solid.svg"; 3 | import { loadTotal } from "../../actions"; 4 | import { connect } from "react-redux"; 5 | import { Link } from "react-router-dom"; 6 | import "./app-header.scss"; 7 | 8 | class AppHeader extends Component { 9 | componentDidMount() { 10 | this.props.loadTotal(); 11 | } 12 | render() { 13 | const { total } = this.props; 14 | return ( 15 |
16 | 17 | Menu 18 | 19 | 20 | Total: {total} $ 21 | 22 |
23 | ); 24 | } 25 | } 26 | const mapStateToProps = ({ total }) => { 27 | return { 28 | total, 29 | }; 30 | }; 31 | 32 | const mapDispatchToProps = { 33 | loadTotal, 34 | }; 35 | 36 | export default connect(mapStateToProps, mapDispatchToProps)(AppHeader); 37 | -------------------------------------------------------------------------------- /src/components/app-header/app-header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | background-color: rgba(0,0,0, .5); 3 | height: 60px; 4 | display: flex; 5 | justify-content: flex-end; 6 | align-items: center; 7 | padding: 0 50px; 8 | &__link { 9 | display: flex; 10 | margin-right: 40px; 11 | font-size: 18px; 12 | color: #fff; 13 | text-decoration: none; 14 | } 15 | &__cart { 16 | width: 20px; 17 | margin-right: 10px; 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/app-header/index.js: -------------------------------------------------------------------------------- 1 | import AppHeader from './app-header'; 2 | 3 | export default AppHeader; -------------------------------------------------------------------------------- /src/components/app-header/shopping-cart-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/app/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MainPage, CartPage } from "../pages"; 3 | import AppHeader from "../app-header"; 4 | 5 | import Background from "./food-bg.jpg"; 6 | import { Route, Switch } from "react-router"; 7 | 8 | const App = () => { 9 | return ( 10 |
14 | 15 | 16 | 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/components/app/food-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrey-kudinov/react-redux-food-app/ac31bc9011b077c09cf9ef5175993ddb21e2298e/src/components/app/food-bg.jpg -------------------------------------------------------------------------------- /src/components/app/index.js: -------------------------------------------------------------------------------- 1 | import App from './app'; 2 | 3 | export default App; -------------------------------------------------------------------------------- /src/components/cart-table/cart-table.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./cart-table.scss"; 3 | import { connect } from "react-redux"; 4 | import { deleteFromCart, changeTotal } from "../../actions"; 5 | 6 | const CartTable = ({ items, deleteFromCart, changeTotal }) => { 7 | return ( 8 | <> 9 |
Ваш заказ:
10 |
11 | {items.map((item) => { 12 | const { title, price, url, id, count } = item; 13 | return ( 14 |
15 | {title} 16 |
{title}
17 |
{price}$
18 |
19 | {count > 1 ? `${count} items` : `${count} item`} 20 |
21 |
{ 24 | deleteFromCart(id); 25 | changeTotal(-price); 26 | }} 27 | > 28 | × 29 |
30 |
31 | ); 32 | })} 33 |
34 | 35 | ); 36 | }; 37 | 38 | const mapStateToProps = ({ items }) => { 39 | return { 40 | items, 41 | }; 42 | }; 43 | 44 | const mapDispatchToProps = { 45 | deleteFromCart, 46 | changeTotal, 47 | }; 48 | 49 | export default connect(mapStateToProps, mapDispatchToProps)(CartTable); 50 | -------------------------------------------------------------------------------- /src/components/cart-table/cart-table.scss: -------------------------------------------------------------------------------- 1 | .cart { 2 | padding-top: 120px; 3 | padding-bottom: 50px; 4 | min-height: 100vh; 5 | &__title { 6 | font-size: 36px; 7 | color: #fff; 8 | text-align: center; 9 | font-weight: 300; 10 | } 11 | &__list { 12 | width: 60%; 13 | margin: 0 auto; 14 | } 15 | &__item { 16 | position: relative; 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: center; 20 | margin-top: 40px; 21 | border: 1px solid rgba(256,256,256, .2); 22 | background-color: rgba(0,0,0,.7); 23 | box-shadow: 0 0 20px rgba(256,256,256, .1); 24 | &-title, &-price { 25 | width: 250px; 26 | text-align: center; 27 | color: #fff; 28 | font-size: 24px; 29 | } 30 | &-img { 31 | height: 125px; 32 | width: 250px; 33 | object-fit: cover; 34 | } 35 | } 36 | &__close { 37 | position: absolute; 38 | width: 25px; 39 | height: 25px; 40 | text-align: center; 41 | top: -10px; 42 | right: -10px; 43 | color: #fff; 44 | font-size: 20px; 45 | border-radius: 100%; 46 | border: 1px solid #fff; 47 | cursor: pointer; 48 | } 49 | } -------------------------------------------------------------------------------- /src/components/cart-table/index.js: -------------------------------------------------------------------------------- 1 | import CartTable from './cart-table'; 2 | 3 | export default CartTable; -------------------------------------------------------------------------------- /src/components/error-boundary/error-boundary.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Error from "../error"; 3 | 4 | export default class ErrorBoundary extends Component { 5 | state = { 6 | error: false, 7 | }; 8 | 9 | componentDidCatch() { 10 | this.setState({ error: true }); 11 | } 12 | 13 | render() { 14 | if (this.state.error) { 15 | return 16 | } 17 | 18 | return this.props.children; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/error-boundary/index.js: -------------------------------------------------------------------------------- 1 | import ErrorBoundary from './error-boundary'; 2 | 3 | export default ErrorBoundary; -------------------------------------------------------------------------------- /src/components/error/error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Error = () => { 4 | return
Error
5 | } 6 | 7 | export default Error; -------------------------------------------------------------------------------- /src/components/error/index.js: -------------------------------------------------------------------------------- 1 | import Error from './error'; 2 | 3 | export default Error; -------------------------------------------------------------------------------- /src/components/hoc/index.js: -------------------------------------------------------------------------------- 1 | import WithRestoService from './with-resto-service'; 2 | 3 | export default WithRestoService; -------------------------------------------------------------------------------- /src/components/hoc/with-resto-service.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RestoServiceContext from "../resto-service-context"; 3 | 4 | const WithRestoService = () => (Wrapper) => { 5 | return (props) => { 6 | return ( 7 | 8 | {(RestoService) => { 9 | return ; 10 | }} 11 | 12 | ); 13 | }; 14 | }; 15 | 16 | export default WithRestoService; 17 | -------------------------------------------------------------------------------- /src/components/menu-list-item/index.js: -------------------------------------------------------------------------------- 1 | import MenuListItem from './menu-list-item'; 2 | export default MenuListItem; -------------------------------------------------------------------------------- /src/components/menu-list-item/menu-list-item.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./menu-list-item.scss"; 3 | 4 | const MenuListItem = ({ menuItem, onAddToCart }) => { 5 | const { title, price, url, category } = menuItem; 6 | return ( 7 |
  • 8 |
    {title}
    9 | {title} 10 |
    11 | Category: {category} 12 |
    13 |
    14 | Price: {price}$ 15 |
    16 | 17 |
  • 18 | ); 19 | }; 20 | 21 | export default MenuListItem; 22 | -------------------------------------------------------------------------------- /src/components/menu-list-item/menu-list-item.scss: -------------------------------------------------------------------------------- 1 | .menu { 2 | &__item { 3 | width: 350px; 4 | min-height: 400px; 5 | background-color: #fff; 6 | box-shadow: 5px 5px 45px rgba(256,256,256,.1); 7 | border-radius: 8px; 8 | padding: 20px; 9 | margin-right: 40px; 10 | margin-bottom: 40px; 11 | } 12 | &__title { 13 | font-size: 22px; 14 | margin-bottom: 20px; 15 | text-align: center; 16 | } 17 | &__img { 18 | width: calc(100% + 40px); 19 | transform: translateX(-20px); 20 | height: 240px; 21 | object-fit: cover; 22 | margin-bottom: 20px; 23 | } 24 | &__category, &__price { 25 | font-size: 18px; 26 | font-weight: 300; 27 | margin-bottom: 10px; 28 | span { 29 | font-weight: 700; 30 | } 31 | } 32 | &__btn { 33 | width: 150px; 34 | height: 40px; 35 | border: none; 36 | background-color: #29a745; 37 | border-radius: 4px; 38 | color: #fff; 39 | font-size: 16px; 40 | cursor: pointer; 41 | } 42 | } -------------------------------------------------------------------------------- /src/components/menu-list/index.js: -------------------------------------------------------------------------------- 1 | import MenuList from './menu-list'; 2 | export default MenuList; -------------------------------------------------------------------------------- /src/components/menu-list/menu-list.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import MenuListItem from "../menu-list-item"; 3 | import { connect } from "react-redux"; 4 | import WithRestoService from "../hoc"; 5 | import { 6 | menuLoaded, 7 | menuRequested, 8 | addedToCart, 9 | changeTotal, 10 | } from "../../actions"; 11 | import Spinner from "../spinner"; 12 | 13 | import "./menu-list.scss"; 14 | 15 | class MenuList extends Component { 16 | componentDidMount() { 17 | this.props.menuRequested(); 18 | 19 | const { RestoService } = this.props; 20 | RestoService.getMenuItems().then((res) => this.props.menuLoaded(res)); 21 | } 22 | 23 | render() { 24 | const { menuItems, loading, addedToCart, changeTotal } = this.props; 25 | 26 | if (loading) { 27 | return ; 28 | } 29 | 30 | return ( 31 | 45 | ); 46 | } 47 | } 48 | 49 | const mapStateToProps = (state) => { 50 | return { 51 | menuItems: state.menu, 52 | loading: state.loading, 53 | }; 54 | }; 55 | 56 | // const mapDispatchToProps = (dispatch) => { 57 | // return { 58 | // menuLoaded: (newMenu) => { 59 | // // dispatch({ 60 | // // type: "MENU_LOADED", 61 | // // payload: newMenu, 62 | // // }); 63 | // dispatch(menuLoaded(newMenu)) 64 | // }, 65 | // }; 66 | // }; 67 | 68 | const mapDispatchToProps = { menuLoaded, menuRequested, addedToCart, changeTotal }; 69 | 70 | export default WithRestoService()( 71 | connect(mapStateToProps, mapDispatchToProps)(MenuList) 72 | ); 73 | -------------------------------------------------------------------------------- /src/components/menu-list/menu-list.scss: -------------------------------------------------------------------------------- 1 | .menu { 2 | &__list { 3 | min-height: 100vh; 4 | padding: 50px; 5 | display: flex; 6 | justify-content: flex-start; 7 | align-items: flex-start; 8 | flex-wrap: wrap; 9 | } 10 | } -------------------------------------------------------------------------------- /src/components/pages/cart-page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CartTable from '../cart-table'; 3 | 4 | const CartPage = () => { 5 | return ( 6 |
    7 | 8 |
    9 | ) 10 | } 11 | 12 | export default CartPage; -------------------------------------------------------------------------------- /src/components/pages/index.js: -------------------------------------------------------------------------------- 1 | import MainPage from './main-page'; 2 | import CartPage from './cart-page'; 3 | 4 | export { 5 | MainPage, 6 | CartPage 7 | }; -------------------------------------------------------------------------------- /src/components/pages/main-page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuList from '../menu-list'; 3 | 4 | const MainPage = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default MainPage; 11 | -------------------------------------------------------------------------------- /src/components/resto-service-context/index.js: -------------------------------------------------------------------------------- 1 | import RestoServiceContext from './resto-service-context'; 2 | 3 | export default RestoServiceContext; -------------------------------------------------------------------------------- /src/components/resto-service-context/resto-service-context.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const RestoServiceContext = React.createContext(); 4 | 5 | export default RestoServiceContext; -------------------------------------------------------------------------------- /src/components/spinner/index.js: -------------------------------------------------------------------------------- 1 | import Spinner from './spinner'; 2 | 3 | export default Spinner; -------------------------------------------------------------------------------- /src/components/spinner/spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Spinner = () => { 4 | return
    loading...
    5 | } 6 | 7 | export default Spinner; -------------------------------------------------------------------------------- /src/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [ 3 | { 4 | "title": "Cesar salad", 5 | "price": 12, 6 | "url": "https://static.1000.menu/img/content/21458/-salat-cezar-s-kr-salat-cezar-s-krevetkami-s-maionezom_1501173720_1_max.jpg", 7 | "category": "salads", 8 | "id": 1 9 | }, 10 | { 11 | "title": "Pizza Margherita", 12 | "price": 10, 13 | "url": "https://www.pizzanapoletana.org/struttura/pagine_bicolor/mobile/decalogo_avpn_1.jpg", 14 | "category": "pizza", 15 | "id": 2 16 | }, 17 | { 18 | "title": "Pizza Napoletana", 19 | "price": 13, 20 | "url": "https://www.pizzanapoletana.org/struttura/pagine_bicolor/mobile/decalogo_avpn_1.jpg", 21 | "category": "pizza", 22 | "id": 3 23 | }, 24 | { 25 | "title": "Greece salad", 26 | "price": 8, 27 | "url": "https://assets.epicurious.com/photos/576454fb42e4a5ed66d1df6b/master/pass/greek-salad.jpg", 28 | "category": "salads", 29 | "id": 4 30 | }, 31 | { 32 | "title": "Cowboy Steak", 33 | "price": 25, 34 | "url": "https://i.cbc.ca/1.4491288.1516208229!/fileImage/httpImage/image.jpg_gen/derivatives/16x9_780/cowboysteak.jpg", 35 | "category": "meat", 36 | "id": 5 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./components/app"; 4 | import { Provider } from "react-redux"; 5 | import { BrowserRouter as Router } from "react-router-dom"; 6 | import ErrorBoundary from "./components/error-boundary"; 7 | import RestoService from "./services/resto-service"; 8 | import RestoServiceContext from "./components/resto-service-context"; 9 | import store from "./store"; 10 | 11 | import "./index.scss"; 12 | 13 | const restoService = new RestoService(); 14 | 15 | ReactDOM.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | , 25 | document.getElementById("root") 26 | ); 27 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | body { 7 | font-family: 'Roboto', sans-serif; 8 | } -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | menu: [], 3 | loading: true, 4 | items: [], 5 | total: 0, 6 | }; 7 | 8 | const reducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case "MENU_LOADED": 11 | return { 12 | ...state, 13 | menu: action.payload, 14 | loading: false, 15 | }; 16 | case "MENU_REQUESTED": 17 | return { 18 | ...state, 19 | menu: state.menu, 20 | loading: true, 21 | }; 22 | 23 | case "ITEM_ADD_TO_CART": 24 | const id = action.payload; 25 | const itemsArr = state.items.filter((item) => item.id === id); 26 | if (!itemsArr.length) { 27 | const item = state.menu.find((item) => item.id === id); 28 | const newItem = { 29 | title: item.title, 30 | price: item.price, 31 | url: item.url, 32 | id: item.id, 33 | count: 1, 34 | }; 35 | return { 36 | ...state, 37 | items: [...state.items, newItem], 38 | }; 39 | } 40 | 41 | const item = itemsArr[0]; 42 | item.count = itemsArr.length + item.count; 43 | const itemIdx = state.items.findIndex((item) => item.id === id); 44 | return { 45 | ...state, 46 | items: [ 47 | ...state.items.slice(0, itemIdx), 48 | ...state.items.slice(itemIdx + 1), 49 | item, 50 | ], 51 | }; 52 | 53 | case "ITEM_REMOVE_FROM_CART": 54 | const idx = action.payload; 55 | const itemIndex = state.items.findIndex((item) => item.id === idx); 56 | 57 | const itemRemove = state.items.find((item) => item.id === idx); 58 | if (itemRemove.count > 1) { 59 | itemRemove.count--; 60 | return { 61 | ...state, 62 | items: [...state.items], 63 | }; 64 | } 65 | return { 66 | ...state, 67 | items: [ 68 | ...state.items.slice(0, itemIndex), 69 | ...state.items.slice(itemIndex + 1), 70 | ], 71 | }; 72 | case "CHANGE_TOTAL": 73 | const total = state.total + action.payload; 74 | return { 75 | ...state, 76 | total, 77 | }; 78 | case "LOAD_TOTAL": 79 | return { 80 | ...state, 81 | }; 82 | default: 83 | return state; 84 | } 85 | }; 86 | 87 | export default reducer; 88 | -------------------------------------------------------------------------------- /src/services/resto-service.js: -------------------------------------------------------------------------------- 1 | export default class RestoService { 2 | _apiBase = "http://localhost:3000"; 3 | 4 | async getResource(url) { 5 | const res = await fetch(`${this._apiBase}${url}`); 6 | 7 | if (!res.ok) { 8 | throw new Error(`Could not fetch ${url}, received ${res.status}`); 9 | } 10 | return await res.json(); 11 | } 12 | 13 | async getMenuItems() { 14 | return await this.getResource("/menu/"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import reducer from "./reducers"; 3 | 4 | const store = createStore(reducer); 5 | 6 | export default store; 7 | --------------------------------------------------------------------------------