├── .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 |

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 |
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 |
32 | {menuItems.map((menuItem) => {
33 | return (
34 | {
38 | addedToCart(menuItem.id);
39 | changeTotal(menuItem.price);
40 | }}
41 | />
42 | );
43 | })}
44 |
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 |
--------------------------------------------------------------------------------