├── .gitignore ├── .prettierrc ├── README.md ├── package.json ├── src ├── app │ ├── categoriesKeys.mock │ ├── components │ │ ├── Breadcrumbs │ │ │ └── index.tsx │ │ ├── Cart │ │ │ ├── index.tsx │ │ │ ├── style.css │ │ │ └── styles.scss │ │ ├── Catalog │ │ │ ├── Catalog.tsx │ │ │ ├── CatalogFilterMenu.tsx │ │ │ ├── CatalogSorter.tsx │ │ │ └── CatalogView.tsx │ │ ├── Counter │ │ │ └── index.tsx │ │ ├── Footer │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── Forms │ │ │ └── ImageUpload.tsx │ │ ├── Header │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── Menus │ │ │ ├── Navigation.tsx │ │ │ └── Sidebar.tsx │ │ ├── Product │ │ │ ├── CreateProduct.tsx │ │ │ ├── ProductCard.tsx │ │ │ ├── ProductDetails.tsx │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── TodoTextInput │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── _unsorted │ │ │ └── HOCs.tsx │ │ └── index.ts │ ├── constants │ │ ├── catalog.ts │ │ ├── index.ts │ │ ├── stores.ts │ │ ├── todos.ts │ │ └── urls.ts │ ├── containers │ │ ├── Root │ │ │ └── index.tsx │ │ ├── ShopApp │ │ │ ├── index.tsx │ │ │ └── style.css │ │ └── TodoApp │ │ │ ├── index.tsx │ │ │ └── style.css │ ├── index.tsx │ ├── mock.json │ ├── models │ │ ├── ProductModel.ts │ │ ├── TodoModel.ts │ │ ├── catalog.model.ts │ │ ├── category.model.ts │ │ ├── counter.model.ts │ │ ├── index.ts │ │ └── product.model.ts │ ├── stores │ │ ├── CartStore.ts │ │ ├── CatalogStore.ts │ │ ├── FilterStore.ts │ │ ├── RouterStore.ts │ │ ├── TodoStore.ts │ │ ├── UIStore.ts │ │ ├── createStore.ts │ │ └── index.ts │ ├── styles │ │ └── index.scss │ └── utils │ │ ├── api.ts │ │ ├── api │ │ ├── apiClient.ts │ │ ├── brands.ts │ │ ├── categories.ts │ │ ├── client.ts │ │ └── products.ts │ │ ├── firebase.ts │ │ ├── helpers.ts │ │ ├── helpers │ │ ├── format-money.ts │ │ ├── is-active.ts │ │ ├── is-email.ts │ │ └── is-phone.ts │ │ └── request.ts ├── assets │ ├── img │ │ ├── M998TCC_600x400.jpg │ │ ├── MS247LA_1_1280x.jpg │ │ ├── MSX90RCC_1_1280x.jpg │ │ ├── mj810211nbs_40.jpg │ │ ├── nb-MSX90_small.jpg │ │ ├── nb-WSX90_small.jpg │ │ └── new_balance-ML840NTB-1.jpg │ └── index.html └── main.tsx ├── tsconfig.json ├── types └── global.d.ts ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | .DS_STORE 4 | node_modules 5 | .module-cache 6 | *.log* 7 | build 8 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "singleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ecommerce Shop 2 | 3 | - Front-end: React, Mobx, Typescript 4 | - Back-end: [Strapi](https://github.com/strapi/strapi) 5 | 6 | ## Actions 7 | 8 | - `setItem` - Sets the quantity of an item in the basket. 9 | - `removeItem` - Removes an item from the basket. 10 | - `setAddress` - Sets the address, specifically the country. 11 | - `setDelivery` - Sets the delivery method to be used to deliver the order. 12 | - `loadProducts` - Loads products. 13 | - `loadCountries` - Loads countries. 14 | - `loadDeliveryMethods` - Loads delivery methods. 15 | - `setPaymentOptions` - Sets the payment options and purchase hook. 16 | - `purchase` - Processes order. 17 | - `completed` - Called when payment has been successully processed. 18 | - `refreshCheckout` - (Used internally). 19 | - `setErrors` - Can be used to set errors manually. 20 | 21 | ## Stores 22 | 23 | - `AddressStore` - The current address, specifically the country. 24 | - `BasketStore` - The items currently in the basket. 25 | - `CountriesStore` - The list of all countries. 26 | - `DeliveryMethodsStore` - The available delivery methods for the current address. 27 | - `DeliveryStore` - The currently selected delivery method and it's associated cost. 28 | - `OrderStore` - The current state of the order with totals, adjustments and errors. 29 | - `PaymentOptionsStore` - The payment options. 30 | - `ProductsStore` - The list of all products. 31 | - `CheckoutStore` - (Used internally). 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eshop-react-mobx-typescript", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "eShop at React, MobX, Typescript", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "BROWSER=none webpack-dev-server --mode development --hot --progress --colors --port 3000 ", 10 | "build": "webpack -p --progress --colors", 11 | "prettier": "prettier --write \"src/**/*.{ts,tsx,css}\"" 12 | }, 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/classnames": "^2.2.3", 16 | "@types/node": "^9.4.6", 17 | "@types/react": "^16.6.0", 18 | "@types/react-dom": "^16.0.10", 19 | "@types/react-router": "^4.3.1", 20 | "@types/webpack": "^3.8.8", 21 | "babel-loader": "^7.1.3", 22 | "css-loader": "^1.0.1", 23 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 24 | "file-loader": "^1.1.11", 25 | "html-loader": "^1.0.0-alpha.0", 26 | "html-webpack-plugin": "^3.0.4", 27 | "mobx-react-devtools": "^6.0.3", 28 | "node-sass": "^4.10.0", 29 | "postcss": "^6.0.19", 30 | "postcss-browser-reporter": "^0.5.0", 31 | "postcss-cssnext": "^3.1.0", 32 | "postcss-import": "^11.1.0", 33 | "postcss-loader": "^2.1.1", 34 | "postcss-reporter": "^5.0.0", 35 | "postcss-url": "^7.3.1", 36 | "precss": "^3.1.2", 37 | "prettier": "^1.11.1", 38 | "react-hot-loader": "^4.3.0", 39 | "sass-loader": "^7.1.0", 40 | "style-loader": "^0.20.2", 41 | "ts-loader": "^4.0.0", 42 | "typescript": "^3.1.6", 43 | "url-loader": "^1.0.0-beta.0", 44 | "webpack": "4.19.1", 45 | "webpack-cleanup-plugin": "^0.5.1", 46 | "webpack-cli": "^2.0.10", 47 | "webpack-dev-server": "^3.1.0", 48 | "webpack-hot-middleware": "^2.21.1" 49 | }, 50 | "dependencies": { 51 | "classnames": "^2.2.5", 52 | "mobx": "^5.6.0", 53 | "mobx-react": "^5.4.2", 54 | "mobx-react-router": "^4.0.5", 55 | "qs": "^6.5.2", 56 | "react": "^16.6.0", 57 | "react-dom": "^16.6.0", 58 | "react-router": "^4.3.1", 59 | "react-router-dom": "^4.3.1", 60 | "semantic-ui-react": "^0.83.0", 61 | "uuid": "^3.3.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/categoriesKeys.mock: -------------------------------------------------------------------------------- 1 | // export const categoriesKeys: Category[] = [ 2 | // { 3 | // category: 'jeans', 4 | // subCategories: [ 5 | // 'bootcut-jeans', 6 | // 'classic-jeans', 7 | // 'cropped-jeans', 8 | // 'distressed-jeans', 9 | // 'flare-jeans', 10 | // 'relaxed-jeans', 11 | // 'skinny-jeans', 12 | // 'straight-jeans', 13 | // 'stretch-jeans' 14 | // ] 15 | // }, 16 | // { 17 | // category: 'dresses', 18 | // subCategories: ['cocktail-dresses', 'day-dresses', 'evening-dresses'] 19 | // }, 20 | // { 21 | // category: 'jackets', 22 | // subCategories: [ 23 | // 'vests', 24 | // 'womens-outerwear', 25 | // 'casual-jackets', 26 | // 'denim-jackets', 27 | // 'leather-jackets', 28 | // 'waistcoats', 29 | // 'fur-and-shearling-coats', 30 | // 'leather-and-suede-coats', 31 | // 'blazers' 32 | // ] 33 | // }, 34 | // { 35 | // category: 'knitwear', 36 | // subCategories: [ 37 | // 'cardigans', 38 | // 'cashmere', 39 | // 'crewnecks', 40 | // 'turtlenecks', 41 | // 'v-neck' 42 | // ] 43 | // }, 44 | // { 45 | // category: 'skirts', 46 | // subCategories: ['mini-skirts', 'mid-length-skirts', 'long-skirts'] 47 | // }, 48 | // { 49 | // category: 'tops', 50 | // subCategories: [ 51 | // 'button front tops', 52 | // 'camis', 53 | // 'cashmere tops', 54 | // 'halters', 55 | // 'longsleeve top', 56 | // 'polos', 57 | // 'shortsleeve tops', 58 | // 'sleeveless tops', 59 | // 't-shirts', 60 | // 'tanks', 61 | // 'tunics' 62 | // ] 63 | // }, 64 | // { 65 | // category: 'trousers', 66 | // subCategories: [ 67 | // 'casual', 68 | // 'cropped', 69 | // 'dress', 70 | // 'leggings', 71 | // 'skinny', 72 | // 'wide leg' 73 | // ] 74 | // }, 75 | // { 76 | // category: 'shoes', 77 | // subCategories: [ 78 | // 'boots', 79 | // 'espadrills', 80 | // 'evening shoes', 81 | // 'flats', 82 | // 'heels', 83 | // 'mules & clogs', 84 | // 'platforms', 85 | // 'sandals', 86 | // 'sports shoes', 87 | // 'trainers', 88 | // 'wedges' 89 | // ] 90 | // }, 91 | // { 92 | // category: 'bags', 93 | // subCategories: [ 94 | // 'backpacks', 95 | // 'clutches', 96 | // 'duffels & totes', 97 | // 'evening', 98 | // 'hobos', 99 | // 'purses', 100 | // 'satchels', 101 | // 'shoulder' 102 | // ] 103 | // } 104 | // ]; 105 | -------------------------------------------------------------------------------- /src/app/components/Breadcrumbs/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface BreadcrumbsProps {} 4 | 5 | export default class Breadcrumbs extends React.Component< 6 | BreadcrumbsProps, 7 | any 8 | > { 9 | public render() { 10 | return ( 11 |
12 | {/*

PageName / Category / Subcategory / Product Name

*/} 13 |

Home / Shoes / Running / New Balance / M390s

14 |
15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/components/Cart/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { toJS } from 'mobx'; 4 | import { STORE_CATALOG, STORE_CART } from 'app/constants'; 5 | import { CatalogStore, CartStore } from 'app/stores'; 6 | import * as styles from './style.css'; 7 | import './styles.scss'; 8 | 9 | interface CartProps { 10 | cart?: any; 11 | cartItem?: any; 12 | item?: any; 13 | } 14 | 15 | @observer 16 | export class Cart extends React.Component { 17 | handleClearCart = (e) => { 18 | e.preventDefault(); 19 | this.props.cart.clearCart(); 20 | }; 21 | 22 | public render() { 23 | // const cartStore = this.props[STORE_CART] as CartStore; 24 | const { cart } = this.props; 25 | const cartList = Array.from(cart.cartItems.values()); 26 | 27 | return ( 28 |
29 |
Total Items In Cart: {cart.count}
30 |
Subtotal Price: {cart.subTotal}
31 | 41 |
42 | ); 43 | } 44 | } 45 | 46 | interface ShoppingCartItemViewProps { 47 | cartItem: any; 48 | removeFromCart: (id: number) => void; 49 | } 50 | @observer 51 | class ShoppingCartItemView extends React.Component< 52 | ShoppingCartItemViewProps, 53 | any 54 | > { 55 | render() { 56 | const { id, name, imageUrl } = this.props.cartItem.item; 57 | const { qty, totalPrice, incQty, decQty } = this.props.cartItem; 58 | const { removeFromCart } = this.props; 59 | // const cartStore = this.props[STORE_CART] as CartStore; 60 | return ( 61 |
62 |
63 | 64 |
65 |
66 |
{name}
67 |
qty: {qty}
68 |
totalPrice: {totalPrice}
69 | 77 | 85 | 96 |
97 |
98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/app/components/Cart/style.css: -------------------------------------------------------------------------------- 1 | .buttonSecondary { 2 | background: rgb(66, 184, 221); /* this is a light blue */ 3 | } 4 | .buttonSMALL { 5 | font-size: 85%; 6 | } 7 | .buttonXSMALL { 8 | font-size: 70%; 9 | } 10 | .buttonCardAction { 11 | padding: 0.4em 0.6em; 12 | background: #777777; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/components/Cart/styles.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spac3unit/ecommerce-mobx-typescript/d5380b1c3bf520b810abc403adc8e29de67b1978/src/app/components/Cart/styles.scss -------------------------------------------------------------------------------- /src/app/components/Catalog/Catalog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { STORE_ROUTER, STORE_CATALOG, STORE_CART } from 'app/constants'; 4 | import { Grid, Image, Loader } from 'semantic-ui-react'; 5 | import Breadcrumbs from 'app/components/Breadcrumbs'; 6 | import CatalogFilterMenu from 'app/components/Catalog/CatalogFilterMenu'; 7 | import CatalogSorter from 'app/components/Catalog/CatalogSorter'; 8 | @inject(STORE_CATALOG) 9 | @observer 10 | export default class Catalog extends React.Component { 11 | componentWillMount() { 12 | this.props[STORE_CATALOG].getProductsList(); 13 | this.props[STORE_CATALOG].getProductsOfCategory('?name=Sneakers'); 14 | this.props[STORE_CATALOG].getCategoriesList(); 15 | this.props[STORE_CATALOG].getBrandsList(); 16 | } 17 | 18 | public render() { 19 | const { categories, products, productsInCategory, loading } = this.props[ 20 | STORE_CATALOG 21 | ]; 22 | 23 | return ( 24 |
25 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | class CatalogComponent extends React.Component { 37 | render() { 38 | const { categories, products, productsInCategory, loading } = this.props; 39 | 40 | return loading ? ( 41 | 42 | ) : ( 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | ); 56 | } 57 | } 58 | 59 | const ProductsList = ({ items }) => { 60 | return items.map((p) => ( 61 |

62 |

{p.name}
63 |
{p.price}
64 |
{p.brand}
65 |

66 | )); 67 | }; 68 | -------------------------------------------------------------------------------- /src/app/components/Catalog/CatalogFilterMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Menu } from 'semantic-ui-react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { observable, action, toJS } from 'mobx'; 5 | import { STORE_CATALOG, STORE_ROUTER } from 'app/constants'; 6 | 7 | interface IProps {} 8 | interface IState { 9 | activeItem: any; 10 | } 11 | 12 | @inject(STORE_CATALOG, STORE_ROUTER) 13 | export default class CatalogFilterMenu extends React.Component { 14 | readonly state = { 15 | activeItem: {} 16 | }; 17 | 18 | private handleItemClick = (e, { name, id }) => { 19 | this.setState({ activeItem: name }); 20 | this.props[STORE_CATALOG].getOneCategory(id); 21 | }; 22 | private handleBrandClick = (e, { name, id }) => { 23 | this.setState({ activeItem: name }); 24 | this.props[STORE_CATALOG].getOneBrand(id); 25 | }; 26 | render() { 27 | const { activeItem } = this.state; 28 | const { categories, brands } = this.props[STORE_CATALOG]; 29 | 30 | return ( 31 | 32 | 33 | Categories 34 | 35 | 36 | {categories.map((c) => ( 37 | 44 | ))} 45 | 46 | 47 | 48 | Brands 49 | 50 | 51 | {brands.length > 0 && 52 | brands.map((c) => ( 53 | 60 | ))} 61 | 62 | 63 | 64 | 65 | Gender 66 | 67 | 68 | 73 | 78 | 79 | 80 | 81 | 82 | Colour 83 | 84 | 85 | 90 | Black 91 | 92 | 93 | 98 | White 99 | 100 | 101 | 102 | 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/app/components/Catalog/CatalogSorter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Menu } from 'semantic-ui-react'; 3 | 4 | export interface IProps {} 5 | 6 | export interface IState { 7 | activeItem: any; 8 | } 9 | 10 | export default class CatalogSorter extends React.Component { 11 | constructor(props: IProps) { 12 | super(props); 13 | 14 | this.state = { 15 | activeItem: 'closest' 16 | }; 17 | } 18 | handleItemClick = (e, { name }) => this.setState({ activeItem: name }); 19 | public render() { 20 | const { activeItem } = this.state; 21 | return ( 22 | 23 | Sort By 24 | 29 | 34 | 39 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/components/Catalog/CatalogView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { observable, action, toJS } from 'mobx'; 4 | 5 | import { Grid, Image } from 'semantic-ui-react'; 6 | import { STORE_CATALOG, STORE_ROUTER } from 'app/constants'; 7 | import { Counter } from 'app/components/Counter'; 8 | import CatalogFilterMenu from 'app/components/Catalog/CatalogFilterMenu'; 9 | import CatalogSorter from 'app/components/Catalog/CatalogSorter'; 10 | import Breadcrumbs from 'app/components/Breadcrumbs'; 11 | import Category from '../../models/category.model'; 12 | @inject(STORE_CATALOG, STORE_ROUTER) 13 | @observer 14 | export class Catalog extends React.Component { 15 | baseUrl?: string; 16 | constructor(props) { 17 | super(props); 18 | this.baseUrl = 'http://localhost:1337'; 19 | } 20 | componentWillMount = () => { 21 | this.props[STORE_CATALOG].getCategories(); 22 | this.props[STORE_CATALOG].getBrands(); 23 | this.props[STORE_CATALOG].getProductCategoriesList(); 24 | }; 25 | returnStatusAndJson = (response) => 26 | response.json().then((json) => ({ 27 | json 28 | })); 29 | get(endpoint = '/categories', filter = 'name=Sneakers') { 30 | return fetch(`${this.baseUrl}${endpoint}?${filter}`).then( 31 | this.returnStatusAndJson 32 | ); 33 | } 34 | 35 | public render() { 36 | const { 37 | categoriesList, 38 | productCategoriesList, 39 | selectedCategoryProducts 40 | } = this.props[STORE_CATALOG]; 41 | 42 | return ( 43 |
44 | {/* */} 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {productCategoriesList && ( 53 | 54 | )} 55 | {/* */} 56 | 57 | 58 |
59 | ); 60 | } 61 | } 62 | 63 | const CatalogCategories = ({ categoriesList }) => { 64 | console.log(categoriesList); 65 | 66 | return ( 67 | <> 68 | {categoriesList.map((c, i) => { 69 | return c.products_in_category.map((p) =>
{p.name}
); 70 | })} 71 | 72 | ); 73 | }; 74 | const CatalogBrands = ({ brandsList }) => { 75 | return ( 76 | <>{brandsList && brandsList.map((b) =>
{b.name}
)} 77 | ); 78 | }; 79 | const CategoryProducts = () => {}; 80 | // class CategoriesList extends React.Component { 81 | 82 | // public render() { 83 | // return ( 84 | 85 | // ); 86 | // } 87 | // } 88 | -------------------------------------------------------------------------------- /src/app/components/Counter/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { observable, computed } from 'mobx'; 4 | import { observer } from 'mobx-react'; 5 | 6 | // https://github.com/Mercateo/counter-component-with-react-mobx-fela/blob/master/src/index.tsx 7 | 8 | const MINIMUM = 0; 9 | const MAXIMUM = 10; 10 | 11 | export interface CounterComponentProps { 12 | value: number; 13 | increment: () => void; 14 | decrement: () => void; 15 | canDecrement: boolean; 16 | canIncrement: boolean; 17 | styles?: { 18 | decrementButton?: string; 19 | incrementButton?: string; 20 | valueContainer?: string; 21 | }; 22 | } 23 | 24 | class CounterComponent extends React.Component { 25 | public render() { 26 | const { 27 | value, 28 | decrement, 29 | increment, 30 | canDecrement, 31 | canIncrement 32 | } = this.props; 33 | return ( 34 |
35 | 43 | Value: {value} 44 | 52 |
53 | ); 54 | } 55 | } 56 | 57 | @observer 58 | export class Counter extends React.Component<{}, {}> { 59 | @observable value = 5; 60 | 61 | @computed 62 | get canDecrement() { 63 | return this.value !== MINIMUM; 64 | } 65 | 66 | @computed 67 | get canIncrement() { 68 | return this.value !== MAXIMUM; 69 | } 70 | 71 | decrement = () => { 72 | this.value -= 1; 73 | }; 74 | 75 | increment = () => { 76 | this.value += 1; 77 | }; 78 | 79 | render() { 80 | return ( 81 | 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as classNames from 'classnames'; 3 | import * as style from './style.css'; 4 | import { 5 | TodoFilter, 6 | TODO_FILTER_TITLES, 7 | TODO_FILTER_TYPES 8 | } from 'app/constants'; 9 | 10 | export interface FooterProps { 11 | filter: TodoFilter; 12 | activeCount: number; 13 | completedCount: number; 14 | onChangeFilter: (filter: TodoFilter) => any; 15 | onClearCompleted: () => any; 16 | } 17 | 18 | export interface FooterState { 19 | /* empty */ 20 | } 21 | 22 | export class Footer extends React.Component { 23 | renderTodoCount() { 24 | const { activeCount } = this.props; 25 | const itemWord = activeCount === 1 ? 'item' : 'items'; 26 | 27 | return ( 28 | 29 | {activeCount || 'No'} {itemWord} left 30 | 31 | ); 32 | } 33 | 34 | renderFilterLink(filter: TodoFilter) { 35 | const title = TODO_FILTER_TITLES[filter]; 36 | const { filter: selectedFilter, onChangeFilter } = this.props; 37 | const className = classNames({ 38 | [style.selected]: filter === selectedFilter 39 | }); 40 | 41 | return ( 42 | onChangeFilter(filter)} 46 | > 47 | {title} 48 | 49 | ); 50 | } 51 | 52 | renderClearButton() { 53 | const { completedCount, onClearCompleted } = this.props; 54 | if (completedCount > 0) { 55 | return ( 56 |