├── .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 |
32 | {cartList.map((cartItem, idx) => (
33 | -
34 |
38 |
39 | ))}
40 |
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 |
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 |
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 |
57 | );
58 | }
59 | }
60 |
61 | render() {
62 | return (
63 |
72 | );
73 | }
74 | }
75 |
76 | export default Footer;
77 |
--------------------------------------------------------------------------------
/src/app/components/Footer/style.css:
--------------------------------------------------------------------------------
1 | .normal {
2 | color: #777;
3 | padding: 10px 15px;
4 | height: 20px;
5 | text-align: center;
6 | border-top: 1px solid #e6e6e6;
7 | }
8 |
9 | .normal:before {
10 | content: '';
11 | position: absolute;
12 | right: 0;
13 | bottom: 0;
14 | left: 0;
15 | height: 50px;
16 | overflow: hidden;
17 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
18 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
19 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
20 | }
21 |
22 | .filters {
23 | margin: 0;
24 | padding: 0;
25 | list-style: none;
26 | position: absolute;
27 | right: 0;
28 | left: 0;
29 | }
30 |
31 | .filters li {
32 | display: inline;
33 | }
34 |
35 | .filters li a {
36 | color: inherit;
37 | margin: 3px;
38 | padding: 3px 7px;
39 | text-decoration: none;
40 | border: 1px solid transparent;
41 | border-radius: 3px;
42 | }
43 |
44 | .filters li a.selected,
45 | .filters li a:hover {
46 | border-color: rgba(175, 47, 47, 0.1);
47 | }
48 |
49 | .filters li a.selected {
50 | border-color: rgba(175, 47, 47, 0.2);
51 | }
52 |
53 | .count {
54 | float: left;
55 | text-align: left;
56 | }
57 |
58 | .count strong {
59 | font-weight: 300;
60 | }
61 |
62 | .clearCompleted,
63 | html .clearCompleted:active {
64 | float: right;
65 | position: relative;
66 | line-height: 20px;
67 | text-decoration: none;
68 | cursor: pointer;
69 | visibility: hidden;
70 | position: relative;
71 | }
72 |
73 | .clearCompleted::after {
74 | visibility: visible;
75 | content: 'Clear completed';
76 | position: absolute;
77 | right: 0;
78 | white-space: nowrap;
79 | }
80 |
81 | .clearCompleted:hover::after {
82 | text-decoration: underline;
83 | }
84 |
85 | @media (max-width: 430px) {
86 | .normal {
87 | height: 50px;
88 | }
89 | .filters {
90 | bottom: 10px;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/app/components/Forms/ImageUpload.tsx:
--------------------------------------------------------------------------------
1 | // import * as React from 'react';
2 |
3 | // export interface UploadImageProps {}
4 |
5 | // export default class UploadImage extends React.Component {
6 | // state = {
7 | // loading: true,
8 | // uploading: false,
9 | // images: [],
10 | // files: []
11 | // };
12 |
13 | // fileChangedHandler = (e) => {
14 | // const errs = [];
15 | // const files = Array.from(e.target.files);
16 | // if (files.length > 3) {
17 | // const msg = 'Only 3 images can be uploaded at a time';
18 | // console.log(msg);
19 | // }
20 | // this.setState({ files });
21 | // };
22 |
23 | // uploadHandler = (e) => {
24 | // const formData = new FormData();
25 | // const types = ['image/png', 'image/jpeg', 'image/gif'];
26 | // this.state.files.forEach((file, i) => {
27 | // if (types.every((type) => file.type !== type)) {
28 | // errs.push(`'${file.type}' is not a supported format`);
29 | // }
30 |
31 | // if (file.size > 150000) {
32 | // errs.push(`'${file.name}' is too large, please pick a smaller file`);
33 | // }
34 |
35 | // formData.append(i, file);
36 | // });
37 |
38 | // fetch(`http://localhost:1337/image-upload`, {
39 | // method: 'POST',
40 | // body: formData
41 | // });
42 | // };
43 | // public render() {
44 | // return (
45 | //
46 | //
50 | //
51 | // );
52 | // }
53 | // }
54 |
--------------------------------------------------------------------------------
/src/app/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { inject, observer } from 'mobx-react';
3 |
4 | import { TodoTextInput } from 'app/components/TodoTextInput';
5 | import { TodoModel } from 'app/models/TodoModel';
6 | import { RouterStore } from 'app/stores';
7 |
8 | import * as styles from './style.css';
9 | import { STORE_ROUTER, STORE_CATALOG, STORE_CART } from 'app/constants';
10 | export interface HeaderProps {
11 | addTodo?: (todo: Partial) => any;
12 | routerStore?: any;
13 | }
14 |
15 | export interface HeaderState {
16 | /* empty */
17 | }
18 | // @inject(STORE_ROUTER, STORE_CATALOG, STORE_CART)
19 | @observer
20 | export class Header extends React.Component {
21 | render() {
22 | console.log('this.props header');
23 | console.log(this.props);
24 | const { routerStore } = this.props;
25 | const { location, push, goBack } = routerStore;
26 |
27 | return (
28 |
52 | );
53 | }
54 | }
55 |
56 | export default Header;
57 |
58 | // render() {
59 | // return (
60 | //
61 | // Todos
62 | //
67 | //
68 | // );
69 | // }
70 |
--------------------------------------------------------------------------------
/src/app/components/Header/style.css:
--------------------------------------------------------------------------------
1 | .leftMenu {
2 | max-width: 200px;
3 | background: #191919;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/components/Menus/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Input, Menu, Button, Icon } from 'semantic-ui-react';
4 |
5 | import { inject, observer } from 'mobx-react';
6 | import { RouterStore, CatalogStore, CartStore, UIStore } from 'app/stores';
7 | import { STORE_ROUTER, STORE_CATALOG, STORE_UI } from 'app/constants';
8 |
9 | @inject(STORE_ROUTER, STORE_UI)
10 | @observer
11 | export default class Navigation extends React.Component {
12 | getActiveItem = () => {
13 | const { pathname } = this.props[STORE_ROUTER].history.location;
14 | const activeItem = pathname.replace(/^\/+/g, '');
15 | return activeItem;
16 | };
17 | handleItemClick = (e, { name, href }) => {
18 | e.preventDefault();
19 | this.setState({ activeItem: name });
20 | this.props[STORE_ROUTER].history.push(href);
21 | };
22 | toggleSidebar = (e, { name, href }) => {
23 | e.preventDefault();
24 | this.setState({ activeItem: name });
25 | this.props[STORE_UI].showCart = !this.props[STORE_UI].showCart;
26 | };
27 | render() {
28 | const { pathname } = this.props[STORE_ROUTER].location;
29 | // const activeItem = pathname.replace(/^\/+/g, ''); // removes slash from /pathname
30 | const activeItem = pathname.split('/')[1];
31 | return (
32 |
77 | );
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/app/components/Menus/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { inject, observer } from 'mobx-react';
3 | import { RouterStore, CatalogStore, CartStore, UIStore } from 'app/stores';
4 | import { STORE_ROUTER, STORE_CATALOG, STORE_UI } from 'app/constants';
5 | import { Icon, Menu, Sidebar } from 'semantic-ui-react';
6 |
7 | // interface IProps {
8 | // uiStore: UIStore;
9 | // }
10 |
11 | @inject(STORE_UI)
12 | @observer
13 | export default class SidebarExample extends React.Component {
14 | handleHideClick = () => (this.props[STORE_UI].showCart = false);
15 | handleShowClick = () => (this.props[STORE_UI].showCart = true);
16 |
17 | render() {
18 | const cartVisibility = this.props[STORE_UI].showCart;
19 |
20 | return (
21 |
31 |
32 |
33 | Home
34 |
35 |
36 |
37 | Games
38 |
39 |
40 |
41 | Channels
42 |
43 |
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/components/Product/CreateProduct.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export interface CreateProductProps {}
4 |
5 | export default class CreateProduct extends React.Component<
6 | CreateProductProps,
7 | any
8 | > {
9 | state = {
10 | name: '',
11 | price: '',
12 | imageUrl: '',
13 | description: '',
14 | categoryId: '',
15 | categoryName: ''
16 | };
17 | inputChange = (e, key) => {
18 | let state = this.state;
19 | state[key] = e.target.value;
20 | this.setState(state);
21 | };
22 | onSubmit = (e) => {
23 | e.preventDefault();
24 | let product = {
25 | name: this.state.name,
26 | price: this.state.price,
27 | imageUrl: this.state.imageUrl,
28 | description: this.state.description
29 | // categoryName: this.props.match.params.categoryName
30 | };
31 | let toReturn = this.validateProduct(product);
32 | if (toReturn) return;
33 | // products.create(product).then(() => {
34 | // this.props.createNotification('success', 'Product created');
35 | // this.props.history.goBack();
36 | // });
37 | };
38 | validateProduct(product) {
39 | if (product.name.length < 3) {
40 | // this.props.createNotification(
41 | // 'error',
42 | // 'Name must be at least 3 symbols long'
43 | // );
44 | return true;
45 | }
46 | if (product.description.length < 15) {
47 | // this.props.createNotification(
48 | // 'error',
49 | // 'Description must be at least 15 symbols long'
50 | // );
51 | return true;
52 | }
53 | }
54 | public render() {
55 | return (
56 |
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/app/components/Product/ProductCard.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Card, Icon, Image, Header } from 'semantic-ui-react';
3 | import { ProductModel } from 'app/models';
4 | import { observer, inject } from 'mobx-react';
5 | import { STORE_ROUTER, STORE_CART } from 'app/constants';
6 |
7 | interface ProductCardProps {
8 | product: ProductModel;
9 | history?: any;
10 | }
11 |
12 | const ProductCard: React.SFC = (props) => {
13 | const { name, brand, category, price, imageUrl, id } = props.product;
14 | const handleAddToCart = (e) => {
15 | e.preventDefault();
16 | e.stopPropagation();
17 | props[STORE_CART].addToCart(props.product);
18 | };
19 | const handleClick = (e) => {
20 | e.preventDefault();
21 | props[STORE_ROUTER].history.push(`/catalog/product/${id}`);
22 | };
23 | return (
24 |
25 |
26 |
27 | {brand.name}
28 |
29 |
30 |
31 |
32 |
33 | {price} EUR
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default inject(STORE_CART, STORE_ROUTER)(observer(ProductCard));
41 | // export default withRouter(ProductCard);
42 |
--------------------------------------------------------------------------------
/src/app/components/Product/ProductDetails.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { inject, observer } from 'mobx-react';
3 | import { STORE_CATALOG, STORE_ROUTER, STORE_CART } from 'app/constants';
4 | import { Loader } from 'semantic-ui-react';
5 |
6 | @inject(STORE_CATALOG, STORE_ROUTER)
7 | @observer
8 | export default class ProductDetails extends React.Component {
9 | componentWillMount() {
10 | const { id } = this.props.match.params;
11 | const { getProduct, findProductById, products } = this.props[STORE_CATALOG];
12 |
13 | if (products.length <= 0) {
14 | getProduct(id);
15 | } else {
16 | findProductById(id);
17 | }
18 | }
19 |
20 | render() {
21 | const { selectedProduct } = this.props[STORE_CATALOG];
22 | return !selectedProduct ? (
23 |
24 | ) : (
25 |
26 |

27 |
{selectedProduct.category_rel.name}
28 |
{selectedProduct.brand.name}
29 |
{selectedProduct.name}
30 |
{selectedProduct.price}
31 |
{selectedProduct.description}
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/components/Product/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { inject, observer } from 'mobx-react';
3 |
4 | import { STORE_ROUTER, STORE_CATALOG, STORE_CART } from 'app/constants';
5 | import { Grid, Image, Container } from 'semantic-ui-react';
6 | import ProductCard from 'app/components/Product/ProductCard';
7 |
8 | @inject(STORE_CATALOG)
9 | @observer
10 | export class ProductList extends React.Component {
11 | componentWillMount() {
12 | this.props[STORE_CATALOG].getProductsList();
13 | }
14 |
15 | public render() {
16 | return (
17 |
18 |
19 | {this.props[STORE_CATALOG].products.map((product) => (
20 |
21 |
22 |
23 | ))}
24 |
25 |
26 | );
27 | }
28 | }
29 | // handleAddToCart = (e) => {
30 | // e.preventDefault();
31 | // this.props.cart.addToCart(this.props.product);
32 | // };
33 | // handleDetailsView = (e, clickedProduct) => {
34 | // e.preventDefault();
35 | // this.props.catalog.selectedProduct = clickedProduct;
36 | // this.props.router.history.push(`/products/${clickedProduct.id}`);
37 | // };
38 |
--------------------------------------------------------------------------------
/src/app/components/Product/style.css:
--------------------------------------------------------------------------------
1 | .buttonCardAction {
2 | padding: 0.4em 0.6em;
3 | background: rgb(255, 235, 59);
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/components/TodoTextInput/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as classNames from 'classnames';
3 | import * as style from './style.css';
4 |
5 | export interface TodoTextInputProps {
6 | text?: string;
7 | placeholder?: string;
8 | newTodo?: boolean;
9 | editing?: boolean;
10 | onSave: (text: string) => any;
11 | }
12 |
13 | export interface TodoTextInputState {
14 | text: string;
15 | }
16 |
17 | export class TodoTextInput extends React.Component<
18 | TodoTextInputProps,
19 | TodoTextInputState
20 | > {
21 | constructor(props?: TodoTextInputProps, context?: any) {
22 | super(props, context);
23 | this.state = {
24 | text: this.props.text || ''
25 | };
26 | }
27 |
28 | private handleSubmit = (e) => {
29 | const text = e.target.value.trim();
30 | if (e.which === 13) {
31 | this.props.onSave(text);
32 | if (this.props.newTodo) {
33 | this.setState({ text: '' });
34 | }
35 | }
36 | };
37 |
38 | private handleChange = (e) => {
39 | this.setState({ text: e.target.value });
40 | };
41 |
42 | private handleBlur = (e) => {
43 | const text = e.target.value.trim();
44 | if (!this.props.newTodo) {
45 | this.props.onSave(text);
46 | }
47 | };
48 |
49 | render() {
50 | const classes = classNames(
51 | {
52 | [style.edit]: this.props.editing,
53 | [style.new]: this.props.newTodo
54 | },
55 | style.normal
56 | );
57 |
58 | return (
59 |
69 | );
70 | }
71 | }
72 |
73 | export default TodoTextInput;
74 |
--------------------------------------------------------------------------------
/src/app/components/TodoTextInput/style.css:
--------------------------------------------------------------------------------
1 | .new,
2 | .edit {
3 | position: relative;
4 | margin: 0;
5 | width: 100%;
6 | font-size: 24px;
7 | font-family: inherit;
8 | font-weight: inherit;
9 | line-height: 1.4em;
10 | border: 0;
11 | outline: none;
12 | color: inherit;
13 | padding: 6px;
14 | border: 1px solid #999;
15 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
16 | box-sizing: border-box;
17 | font-smoothing: antialiased;
18 | }
19 |
20 | .new {
21 | padding: 16px 16px 16px 60px;
22 | border: none;
23 | background: rgba(0, 0, 0, 0.003);
24 | box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/components/_unsorted/HOCs.tsx:
--------------------------------------------------------------------------------
1 | // const NewComponent = (BaseComponent) => {
2 | // // ... create new component from old one and update
3 | // return UpdatedComponent
4 | // }
5 |
6 | // https://levelup.gitconnected.com/understanding-react-higher-order-components-by-example-95e8c47c8006
7 |
8 | import * as React from 'react';
9 |
10 | // Example 1
11 | const higherOrderComponent = (WrappedComponent) => {
12 | class HOC extends React.Component {
13 | render() {
14 | return ;
15 | }
16 | }
17 |
18 | return HOC;
19 | };
20 | const MyComponent = () => mycomponent
;
21 | const SimpleHOC = higherOrderComponent(MyComponent);
22 |
23 | // Example 2
24 | const withStorage = (WrappedComponent) => {
25 | class HOC extends React.Component {
26 | state = {
27 | localStorageAvailable: false
28 | };
29 |
30 | componentDidMount() {
31 | this.checkLocalStorageExists();
32 | }
33 |
34 | checkLocalStorageExists() {
35 | const testKey = 'test';
36 |
37 | try {
38 | localStorage.setItem(testKey, testKey);
39 | localStorage.removeItem(testKey);
40 | this.setState({ localStorageAvailable: true });
41 | } catch (e) {
42 | this.setState({ localStorageAvailable: false });
43 | }
44 | }
45 |
46 | load = (key) => {
47 | if (this.state.localStorageAvailable) {
48 | return localStorage.getItem(key);
49 | }
50 |
51 | return null;
52 | };
53 |
54 | save = (key, data) => {
55 | if (this.state.localStorageAvailable) {
56 | localStorage.setItem(key, data);
57 | }
58 | };
59 |
60 | remove = (key) => {
61 | if (this.state.localStorageAvailable) {
62 | localStorage.removeItem(key);
63 | }
64 | };
65 |
66 | render() {
67 | return (
68 |
74 | );
75 | }
76 | }
77 |
78 | return HOC;
79 | };
80 |
81 | export default withStorage;
82 |
--------------------------------------------------------------------------------
/src/app/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Footer';
2 | export * from './Header';
3 | export * from './TodoItem';
4 | export * from './TodoList';
5 | export * from './TodoTextInput';
6 |
--------------------------------------------------------------------------------
/src/app/constants/catalog.ts:
--------------------------------------------------------------------------------
1 | export const CATALOG_SOME_CONST = 'test const catalog';
2 |
--------------------------------------------------------------------------------
/src/app/constants/index.ts:
--------------------------------------------------------------------------------
1 | export * from './stores';
2 | export * from './todos';
3 | export * from './catalog';
4 | export * from './urls';
5 | export const API_BASE_URL = 'http://localhost:1337/';
6 |
--------------------------------------------------------------------------------
/src/app/constants/stores.ts:
--------------------------------------------------------------------------------
1 | export const STORE_TODO = 'todo';
2 | export const STORE_ROUTER = 'router';
3 | export const STORE_CATALOG = 'catalog';
4 | export const STORE_CART = 'cart';
5 | export const STORE_UI = 'ui';
6 |
--------------------------------------------------------------------------------
/src/app/constants/todos.ts:
--------------------------------------------------------------------------------
1 | export enum TodoFilter {
2 | ALL = 0,
3 | ACTIVE,
4 | COMPLETED
5 | }
6 |
7 | export const TODO_FILTER_TYPES = [
8 | TodoFilter.ALL,
9 | TodoFilter.ACTIVE,
10 | TodoFilter.COMPLETED
11 | ];
12 |
13 | export const TODO_FILTER_TITLES = {
14 | [TodoFilter.ALL]: 'All',
15 | [TodoFilter.ACTIVE]: 'Active',
16 | [TodoFilter.COMPLETED]: 'Completed'
17 | };
18 |
19 | export const TODO_FILTER_LOCATION_HASH = {
20 | [TodoFilter.ALL]: '#',
21 | [TodoFilter.ACTIVE]: '#active',
22 | [TodoFilter.COMPLETED]: '#completed'
23 | };
24 |
--------------------------------------------------------------------------------
/src/app/constants/urls.ts:
--------------------------------------------------------------------------------
1 | // export const REGISTER_URL = 'http://localhost:1337/users/register'
2 | // export const LOGIN_URL = 'http://localhost:1337/users/login'
3 | // export const USERS_URL = 'http://localhost:1337/users'
4 | export const CATEGORIES_URL = 'http://localhost:1337/categories';
5 | export const PRODUCTS_URL = 'http://localhost:1337/products';
6 | export const BRANDS_URL = 'http://localhost:1337/brands';
7 | export const PRODUCTS_BY_CATEGORY_URL =
8 | 'http://localhost:1337/products?category=';
9 | export const PRODUCT_GET_BY_ID_URL = 'http://localhost:1337/products/';
10 | // export const PRODUCT_PROMO_URL = 'http://localhost:1337/api/products/promo';
11 | // export const PRODUCT_ARRAY_URL = 'http://localhost:1337/api/products/array=';
12 | // export const PRODUCT_NEW_URL = 'http://localhost:1337/api/products/new';
13 | // export const COMMENTS_PRODUCT_URL = 'http://localhost:1337/api/comments/';
14 | // export const CREATE_COMMENT_URL = 'http://localhost:1337/api/comments';
15 | // export const DELETE_COMMENT_URL =
16 | // 'http://localhost:1337/api/comments/commentId=';
17 | // export const UPDATE_COMMENT_URL = 'http://localhost:1337/api/comments/';
18 | // export const CREATE_PRODUCT_URL = 'http://localhost:1337/api/products';
19 | // export const UPDATE_PRODUCT_URL = 'http://localhost:1337/api/products';
20 |
--------------------------------------------------------------------------------
/src/app/containers/Root/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export class Root extends React.Component {
4 | renderDevTool() {
5 | if (process.env.NODE_ENV !== 'production') {
6 | const DevTools = require('mobx-react-devtools').default;
7 | return ;
8 | }
9 | }
10 |
11 | render() {
12 | return (
13 |
14 | {this.props.children}
15 | {this.renderDevTool()}
16 |
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/containers/ShopApp/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as style from './style.css';
3 | import { inject, observer } from 'mobx-react';
4 | import { RouteComponentProps } from 'react-router';
5 | import { withRouter } from 'react-router-dom';
6 | import { Switch, Route, Redirect } from 'react-router';
7 | import { toJS } from 'mobx';
8 | import { ProductList } from 'app/components/Product';
9 | import { Cart } from 'app/components/Cart';
10 |
11 | import { RouterStore, CatalogStore, CartStore } from 'app/stores';
12 | import { STORE_ROUTER, STORE_CATALOG, STORE_CART } from 'app/constants';
13 | import { Input, Menu } from 'semantic-ui-react';
14 | import Navigation from 'app/components/Menus/Navigation';
15 | import SidebarExample from 'app/components/Menus/Sidebar';
16 |
17 | export interface ShopAppProps extends RouteComponentProps {}
18 |
19 | export class ShopApp extends React.Component<{}, {}> {
20 | render() {
21 | // const routerStore = this.props[STORE_ROUTER] as RouterStore;
22 | // const catalogStore = this.props[STORE_CATALOG] as CatalogStore;
23 | // const cartStore = toJS(this.props[STORE_CART]) as CartStore;
24 | // const { push } = this.props.history;
25 | return (
26 |
27 |
28 |
29 | {this.props.children}
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/containers/ShopApp/style.css:
--------------------------------------------------------------------------------
1 | /* @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500');
2 |
3 | html,
4 | body {
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | button {
10 | margin: 0;
11 | padding: 0;
12 | border: 0;
13 | background: none;
14 | font-size: 100%;
15 | vertical-align: baseline;
16 | font-family: inherit;
17 | font-weight: inherit;
18 | color: inherit;
19 | appearance: none;
20 | font-smoothing: antialiased;
21 | }
22 |
23 | body {
24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
25 | line-height: 1.4em;
26 | background: #f5f5f5;
27 | color: #4d4d4d;
28 |
29 | -webkit-font-smoothing: antialiased;
30 | -moz-font-smoothing: antialiased;
31 | -ms-font-smoothing: antialiased;
32 | font-smoothing: antialiased;
33 | font-weight: 300;
34 | }
35 |
36 | button,
37 | input[type='checkbox'] {
38 | outline: none;
39 | }
40 |
41 | input::-webkit-input-placeholder {
42 | font-style: italic;
43 | font-weight: 300;
44 | color: #e6e6e6;
45 | }
46 |
47 | input::-moz-placeholder {
48 | font-style: italic;
49 | font-weight: 300;
50 | color: #e6e6e6;
51 | }
52 |
53 | input::input-placeholder {
54 | font-style: italic;
55 | font-weight: 300;
56 | color: #e6e6e6;
57 | }
58 |
59 | h1 {
60 | color: rgba(175, 47, 47, 0.15);
61 | -webkit-text-rendering: optimizeLegibility;
62 | -moz-text-rendering: optimizeLegibility;
63 | -ms-text-rendering: optimizeLegibility;
64 | text-rendering: optimizeLegibility;
65 | } */
66 |
--------------------------------------------------------------------------------
/src/app/containers/TodoApp/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as style from './style.css';
3 | import { inject, observer } from 'mobx-react';
4 | import { RouteComponentProps } from 'react-router';
5 | import { Header } from 'app/components/Header';
6 | import { TodoList } from 'app/components/TodoList';
7 | import { Footer } from 'app/components/Footer';
8 | import { TodoStore, RouterStore } from 'app/stores';
9 | import {
10 | STORE_TODO,
11 | STORE_ROUTER,
12 | TODO_FILTER_LOCATION_HASH,
13 | TodoFilter
14 | } from 'app/constants';
15 |
16 | export interface TodoAppProps extends RouteComponentProps {
17 | /** MobX Stores will be injected via @inject() **/
18 | // [STORE_ROUTER]: RouterStore;
19 | // [STOURE_TODO]: TodoStore;
20 | }
21 |
22 | export interface TodoAppState {
23 | filter: TodoFilter;
24 | }
25 |
26 | @inject(STORE_TODO, STORE_ROUTER)
27 | @observer
28 | export class TodoApp extends React.Component {
29 | constructor(props: TodoAppProps, context: any) {
30 | super(props, context);
31 | this.state = { filter: TodoFilter.ALL };
32 | }
33 |
34 | componentWillMount() {
35 | this.checkLocationChange();
36 | }
37 |
38 | componentWillReceiveProps(nextProps: TodoAppProps, nextContext: any) {
39 | this.checkLocationChange();
40 | }
41 |
42 | checkLocationChange() {
43 | const router = this.props[STORE_ROUTER] as RouterStore;
44 | const filter = Object.keys(TODO_FILTER_LOCATION_HASH)
45 | .map((key) => Number(key) as TodoFilter)
46 | .find(
47 | (filter) => TODO_FILTER_LOCATION_HASH[filter] === router.location.hash
48 | );
49 | this.setState({ filter });
50 | }
51 |
52 | private handleFilter = (filter: TodoFilter) => {
53 | const router = this.props[STORE_ROUTER] as RouterStore;
54 | const currentHash = router.location.hash;
55 | const nextHash = TODO_FILTER_LOCATION_HASH[filter];
56 | if (currentHash !== nextHash) {
57 | router.replace(nextHash);
58 | }
59 | };
60 |
61 | getFilteredTodo(filter: TodoFilter) {
62 | const todoStore = this.props[STORE_TODO] as TodoStore;
63 | switch (filter) {
64 | case TodoFilter.ACTIVE:
65 | return todoStore.activeTodos;
66 | case TodoFilter.COMPLETED:
67 | return todoStore.completedTodos;
68 | default:
69 | return todoStore.todos;
70 | }
71 | }
72 |
73 | render() {
74 | const todoStore = this.props[STORE_TODO] as TodoStore;
75 | const { children } = this.props;
76 | const { filter } = this.state;
77 | const filteredTodos = this.getFilteredTodo(filter);
78 |
79 | const footer = todoStore.todos.length && (
80 |
87 | );
88 |
89 | return (
90 |
91 |
92 |
98 | {footer}
99 | {children}
100 |
101 | );
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/app/containers/TodoApp/style.css:
--------------------------------------------------------------------------------
1 | /* @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500');
2 |
3 | html,
4 | body {
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | button {
10 | margin: 0;
11 | padding: 0;
12 | border: 0;
13 | background: none;
14 | font-size: 100%;
15 | vertical-align: baseline;
16 | font-family: inherit;
17 | font-weight: inherit;
18 | color: inherit;
19 | appearance: none;
20 | font-smoothing: antialiased;
21 | }
22 |
23 | body {
24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
25 | line-height: 1.4em;
26 | background: #f5f5f5;
27 | color: #4d4d4d;
28 | min-width: 230px;
29 | max-width: 550px;
30 | margin: 0 auto;
31 | -webkit-font-smoothing: antialiased;
32 | -moz-font-smoothing: antialiased;
33 | -ms-font-smoothing: antialiased;
34 | font-smoothing: antialiased;
35 | font-weight: 300;
36 | }
37 |
38 | button,
39 | input[type='checkbox'] {
40 | outline: none;
41 | }
42 |
43 | .normal {
44 | background: #fff;
45 | margin: 200px 0 40px 0;
46 | position: relative;
47 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
48 | }
49 |
50 | .normal input::-webkit-input-placeholder {
51 | font-style: italic;
52 | font-weight: 300;
53 | color: #e6e6e6;
54 | }
55 |
56 | .normal input::-moz-placeholder {
57 | font-style: italic;
58 | font-weight: 300;
59 | color: #e6e6e6;
60 | }
61 |
62 | .normal input::input-placeholder {
63 | font-style: italic;
64 | font-weight: 300;
65 | color: #e6e6e6;
66 | }
67 |
68 | .normal h1 {
69 | position: absolute;
70 | top: -155px;
71 | width: 100%;
72 | font-size: 100px;
73 | font-weight: 100;
74 | text-align: center;
75 | color: rgba(175, 47, 47, 0.15);
76 | -webkit-text-rendering: optimizeLegibility;
77 | -moz-text-rendering: optimizeLegibility;
78 | -ms-text-rendering: optimizeLegibility;
79 | text-rendering: optimizeLegibility;
80 | } */
81 |
--------------------------------------------------------------------------------
/src/app/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { hot } from 'react-hot-loader';
3 | import { Router, Route, Switch, Redirect } from 'react-router';
4 |
5 | import { inject, observer } from 'mobx-react';
6 | import { toJS } from 'mobx';
7 | import { RouterStore, CatalogStore, CartStore } from 'app/stores';
8 | import { STORE_ROUTER, STORE_CATALOG, STORE_CART } from 'app/constants';
9 | import { Root } from 'app/containers/Root';
10 | import { ShopApp } from 'app/containers/ShopApp';
11 | import { ProductList } from 'app/components/Product';
12 | import ProductDetails from 'app/components/Product/ProductDetails';
13 | import { Cart } from 'app/components/Cart';
14 | import { Catalog } from 'app/components/Catalog/CatalogView';
15 | import CatalogV2 from 'app/components/Catalog/Catalog';
16 |
17 | import './styles/index.scss';
18 |
19 | let loggedIn = true;
20 | const App = hot(module)(({ history }) => (
21 |
22 |
23 |
24 |
25 |
29 | loggedIn ? :
30 | }
31 | />
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | ));
41 | export { App };
42 |
43 | @inject(STORE_CATALOG, STORE_CART)
44 | @observer
45 | class Main extends React.Component {
46 | render() {
47 | const cart = this.props[STORE_CART] as CartStore;
48 |
49 | return (
50 |
51 |
52 |
53 |
54 | );
55 | }
56 | }
57 |
58 | const LoginComponent = () => LoginComponent
;
59 |
60 | // const App = hot(module)(({ history }) => {}
61 | // export default hot(module)(App);
62 |
--------------------------------------------------------------------------------
/src/app/mock.json:
--------------------------------------------------------------------------------
1 | {
2 | "sneakers": [
3 | {
4 | "_id": 1,
5 | "category_id": 3,
6 | "category_name": "Sneakers",
7 | "brand": "New Balance",
8 | "name": "M998TCC",
9 | "price": 80.95,
10 | "imageUrl": "/assets/img/M998TCC_600x400.jpg",
11 | "description": "The New Balance 247 is part of the lifestyle segment by the brand from Boston. The model takes New Balance heritage and turns it into a new modern sneaker that adds athletic style to your everyday look. With its sock-like bootie-style fit and lightweight tan brown pigskin leather upper the New Balance 274 combines comfort with performance. The REVlite cushioning technology in the white midsole as well as the gum outsole finish of this shoe that can easily be your next office sneaker.",
12 | "description_list": [
13 | "Made In The USA",
14 | "ABZORB Sole Unit",
15 | "Navy/Ocean Blue/Grey",
16 | "3M Reflective Detailing ",
17 | "Rubber Outsole"
18 | ]
19 | },
20 | {
21 | "_id": 2,
22 | "category_id": 4,
23 | "category_name": "Summer Shoes",
24 | "brand": "New Balance",
25 | "name": "MS247LA",
26 | "price": 80.95,
27 | "imageUrl": "/assets/img/MS247LA_1_1280x.jpg",
28 | "description": "The New Balance 247 is part of the lifestyle segment by the brand from Boston. The model takes New Balance heritage and turns it into a new modern sneaker that adds athletic style to your everyday look. With its sock-like bootie-style fit and lightweight tan brown pigskin leather upper the New Balance 274 combines comfort with performance. The REVlite cushioning technology in the white midsole as well as the gum outsole finish of this shoe that can easily be your next office sneaker.",
29 | "description_list": [
30 | "Made In The USA",
31 | "ABZORB Sole Unit",
32 | "Navy/Ocean Blue/Grey",
33 | "3M Reflective Detailing ",
34 | "Rubber Outsole"
35 | ]
36 | },
37 | {
38 | "_id": 3,
39 | "category_id": 4,
40 | "category_name": "Summer Shoes",
41 | "brand": "New Balance",
42 | "name": "MSX90RCC",
43 | "price": 59.4,
44 | "imageUrl": "/assets/img/MSX90RCC_1_1280x.jpg",
45 | "description": "New for 2018 New Balance seamlessly blend the old and new in classic NB fashion, with their latest offering. The X90 lands here in a clean and simple execution offering something for everyone. Following on from the one piece style we've already seen, these newest iterations come in a re-constructed upper with a mix of suede and mesh. These X90's feature 3M reflective details, a REVlite sole unit and a gum outsole.",
46 | "description_list": [
47 | "Re-Construced Version",
48 | "REVLite Sole Unit",
49 | "Blue/Grey Colourway",
50 | "Suede & Mesh Upper",
51 | "Gum Outsole"
52 | ]
53 | }
54 | ],
55 | "categories": [
56 | {
57 | "id": 1,
58 | "parentId": null,
59 | "parentName": "",
60 | "name": "ROOT",
61 | "displayName": "ROOT"
62 | },
63 | {
64 | "id": 2,
65 | "parentId": 1,
66 | "parentName": "ROOT",
67 | "name": "Sneakers",
68 | "displayName": "Sneakers"
69 | },
70 | {
71 | "id": 3,
72 | "parentId": 2,
73 | "parentName": "Sneakers",
74 | "name": "Running Sneakers",
75 | "displayName": "Running Sneakers"
76 | },
77 | {
78 | "id": 4,
79 | "parentId": 2,
80 | "parentName": "Sneakers",
81 | "name": "Casual Sneakers",
82 | "displayName": "Casual Sneakers"
83 | },
84 | {
85 | "id": 5,
86 | "parentId": 2,
87 | "parentName": "Sneakers",
88 | "name": "Leather Sneakers",
89 | "displayName": "Leather Sneakers"
90 | }
91 | ]
92 | }
93 |
--------------------------------------------------------------------------------
/src/app/models/ProductModel.ts:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 | import uuidv1 from 'uuid/v1';
3 | // import { PRODUCT_STATUSES } from '../constants/catalog';
4 |
5 | export class ProductModel {
6 | id: string;
7 | @observable public name: string;
8 | @observable public category: string;
9 | @observable public brand: string;
10 | @observable public price: number;
11 | @observable public imageUrl?: string;
12 | @observable public description?: string;
13 |
14 | constructor(product) {
15 | this.id = ProductModel.generateId();
16 | this.name = product.name;
17 | this.category = product.category;
18 | this.brand = product.brand;
19 | this.price = product.price;
20 | this.imageUrl = product.imageUrl;
21 | this.description = product.description;
22 | }
23 |
24 | static nextId = 1;
25 | static generateId() {
26 | return uuidv1();
27 | }
28 | // handleItemClick = (product: product): void => {
29 | // this.ProductController.selectedProduct = product
30 | // this.ProductController.productAction = 'edit'
31 | // }
32 | }
33 |
34 | export default ProductModel;
35 |
--------------------------------------------------------------------------------
/src/app/models/TodoModel.ts:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | export class TodoModel {
4 | readonly id: number;
5 | @observable public text: string;
6 | @observable public completed: boolean;
7 |
8 | constructor(text: string, completed: boolean = false) {
9 | this.id = TodoModel.generateId();
10 | this.text = text;
11 | this.completed = completed;
12 | }
13 |
14 | static nextId = 1;
15 | static generateId() {
16 | return this.nextId++;
17 | }
18 | }
19 |
20 | export default TodoModel;
21 |
--------------------------------------------------------------------------------
/src/app/models/catalog.model.ts:
--------------------------------------------------------------------------------
1 | interface Catalog {
2 | categories: any;
3 | brands: [];
4 | products: [];
5 | }
6 |
7 | export default Catalog;
8 |
--------------------------------------------------------------------------------
/src/app/models/category.model.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Model for a Category, just defines lightweight the shape of data.
3 | */
4 | // https://github.com/atroppmann/shoppu/blob/f21688604fb41b54c87e2ca551621d81a36384b9/app/javascript/packs/models/record.model.ts
5 | interface Category {
6 | id: number;
7 | displayName: string;
8 | name: string;
9 | productsCount: number;
10 | parentId: number;
11 | parentName: string;
12 | }
13 |
14 | export default Category;
15 |
--------------------------------------------------------------------------------
/src/app/models/counter.model.ts:
--------------------------------------------------------------------------------
1 | export interface CounterComponentProps {
2 | value: number;
3 | increment: () => void;
4 | decrement: () => void;
5 | canDecrement: boolean;
6 | canIncrement: boolean;
7 | styles?: {
8 | decrementButton?: string;
9 | incrementButton?: string;
10 | valueContainer?: string;
11 | }
12 | }
13 | // https://github.com/Mercateo/counter-component-with-react-mobx-fela/blob/master/src/index.tsx
--------------------------------------------------------------------------------
/src/app/models/index.ts:
--------------------------------------------------------------------------------
1 | import TodoModel from './TodoModel';
2 | // import ProductModel from './ProductModel';
3 | export { TodoModel };
4 | import { observable, action, computed } from 'mobx';
5 | import uuidv1 from 'uuid/v1';
6 |
7 | export interface IProduct {
8 | id: string;
9 | name: string;
10 | category: any;
11 | brand: any;
12 | price: number;
13 | imageUrl?: string;
14 | description?: string;
15 | }
16 |
17 | export class ProductModel implements IProduct {
18 | id: string;
19 | @observable public name: string;
20 | @observable public category: any;
21 | @observable public brand: any;
22 | @observable public price: number;
23 | @observable public imageUrl?: string;
24 | @observable public description?: string;
25 |
26 | constructor(product) {
27 | this.id = ProductModel.generateId();
28 | this.name = product.name;
29 | this.category = product.category;
30 | this.brand = product.brand;
31 | this.price = product.price;
32 | this.imageUrl = product.imageUrl;
33 | this.description = product.description;
34 | }
35 |
36 | static nextId = 1;
37 | static generateId() {
38 | return uuidv1();
39 | }
40 | }
41 |
42 | export interface ICartItem {
43 | item: ProductModel;
44 | qty: number;
45 | }
46 |
47 | export class CartItemModel {
48 | @observable item?: ProductModel;
49 | @observable qty?: number;
50 | constructor(item, qty = 1) {
51 | this.item = item;
52 | this.qty = qty;
53 | }
54 | @computed
55 | get totalPrice() {
56 | return (this.item.price * this.qty).toFixed(2);
57 | }
58 | @action setQty = (quantity) => (this.qty = quantity);
59 | @action incQty = () => this.qty++;
60 | @action decQty = () => this.qty--;
61 | }
62 |
63 | export default ProductModel;
64 |
--------------------------------------------------------------------------------
/src/app/models/product.model.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Model for a Product, just defines lightweight the shape of data.
3 | */
4 |
5 | interface Product {
6 | id: number;
7 | displayName: string;
8 | categoryId: number;
9 | categoryName: string;
10 | name: string;
11 | price: number;
12 | currency: string;
13 | displayCurrency: string;
14 | }
15 |
16 | export default Product;
17 |
--------------------------------------------------------------------------------
/src/app/stores/CartStore.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/Jackyzm/react-app-ts/blob/master/src/stores/List/FakeList.ts
2 | import { observable, action, computed, autorun } from 'mobx';
3 | import { ProductModel, CartItemModel } from 'app/models';
4 | // import { getCatalogList } from '../utils/api';
5 | import * as _ from 'lodash';
6 |
7 | export class CartStore {
8 | @observable cartItems = observable.map();
9 | @observable loading = false;
10 |
11 | @computed
12 | get count() {
13 | return this.cartItems.size;
14 | }
15 |
16 | @computed
17 | get subTotal() {
18 | let cartItem = this.cartItems.values();
19 | let subtotal = _.sumBy(cartItem, 'item.price');
20 | return subtotal.toFixed(2);
21 | }
22 |
23 | @action addToCart = (product) => {
24 | let cartItem;
25 | const { id } = product;
26 | const { name } = product;
27 | if (this.cartItems.has(id)) {
28 | cartItem = this.cartItems.get(id);
29 | cartItem.incQty();
30 | } else {
31 | this.cartItems.set(name, new CartItemModel(product));
32 | }
33 | };
34 |
35 | @action removeFromCart = (id) => {
36 | this.cartItems.delete(id);
37 | };
38 | @action clearCart = () => {
39 | this.cartItems.clear;
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/stores/CatalogStore.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/Jackyzm/react-app-ts/blob/master/src/stores/List/FakeList.ts
2 | // https://github.com/StoyanTodorinov/js-web-projects/blob/cae069f1efd300850e8a733e9f26fcb6ada883a2/ReactJS/armory-react/src/fetcher/products.js
3 | import { observable, action, toJS } from 'mobx';
4 | import Client from 'app/utils/api/client';
5 | import { ProductModel, CartItemModel } from 'app/models';
6 |
7 | export class CatalogStore {
8 | api: any;
9 | @observable loading: boolean = false;
10 | @observable categories: [] = [];
11 | @observable brands: [] = [];
12 | @observable products: ProductModel[] = [];
13 | @observable selectedProduct: {} | null = null;
14 | @observable productsInCategory: ProductModel[] = [];
15 |
16 | constructor() {
17 | this.api = new Client({});
18 | }
19 |
20 | @action getProductsList = (filter) => {
21 | this.loading = true;
22 | this.api.products.list(filter).then((data) => {
23 | this.products = toJS(data);
24 | this.loading = false;
25 | });
26 | };
27 | @action getProduct = (id) => {
28 | this.loading = true;
29 | this.api.products.item(id).then((data) => {
30 | this.selectedProduct = toJS(data);
31 | this.loading = false;
32 | });
33 | };
34 | @action getProductsOfCategory = (filter) => {
35 | this.loading = true;
36 | this.api.categories.list(filter).then((data) => {
37 | this.productsInCategory = toJS(data[0].products_in_category);
38 | this.loading = false;
39 | });
40 | };
41 | @action getCategoriesList = (filter) => {
42 | this.loading = true;
43 | this.api.categories.list(filter).then((data) => {
44 | this.categories = toJS(data);
45 | this.loading = false;
46 | });
47 | };
48 | @action getBrandsList = (filter) => {
49 | this.loading = true;
50 | this.api.brands.list(filter).then((data) => {
51 | this.brands = toJS(data);
52 | this.loading = false;
53 | });
54 | };
55 | @action findProductById = (id) => {
56 | this.selectedProduct = this.products.find((item) => item.id === id);
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/stores/FilterStore.ts:
--------------------------------------------------------------------------------
1 | // // https://github.com/codempireio/shopApp/blob/32a617c9856136a41189311e43a5ae4953357cc7/src/features/stores/filterStore.ts
2 |
3 | // import { observable, action } from 'mobx';
4 | // import { findCategoryIndex } from '../shared';
5 | // import { categoriesKeys } from '../filter/categoriesKeys';
6 |
7 | // export default class ObservableStore implements IFilterStore {
8 | // constructor(public root: RootStore) {}
9 |
10 | // @observable categoriesFilter: Category[] = [];
11 | // @observable tab: string = '';
12 |
13 | // get listOfCategory() {
14 | // return this.categoriesFilter;
15 | // }
16 |
17 | // get filterTab() {
18 | // return this.tab;
19 | // }
20 |
21 | // @action
22 | // setFilterTab = (tab: string) => {
23 | // this.tab = tab;
24 | // };
25 |
26 | // @action
27 | // clearFilters = () => {
28 | // this.categoriesFilter = [];
29 | // };
30 |
31 | // @action
32 | // public addNewCategory = (category: string, subCutegory: string) => {
33 | // const categoryIndex: number = findCategoryIndex(
34 | // this.categoriesFilter,
35 | // category
36 | // );
37 | // if (this.categoriesFilter[categoryIndex]) {
38 | // const newCategoriesFilter: Category[] = [...this.categoriesFilter];
39 | // newCategoriesFilter[categoryIndex].subCategories.push(subCutegory);
40 | // this.categoriesFilter = [...newCategoriesFilter];
41 | // } else {
42 | // this.categoriesFilter = [
43 | // ...this.categoriesFilter,
44 | // { category, subCategories: [subCutegory] }
45 | // ];
46 | // }
47 | // };
48 |
49 | // @action
50 | // public removeCategory = (category: string, subCutegory: string) => {
51 | // const categoryIndex: number = findCategoryIndex(
52 | // this.categoriesFilter,
53 | // category
54 | // );
55 | // const subCategoryLength: number = this.categoriesFilter[categoryIndex]
56 | // .subCategories.length;
57 | // const newCategoriesFilter: Category[] = [...this.categoriesFilter];
58 | // if (subCategoryLength === 1) {
59 | // newCategoriesFilter.splice(categoryIndex, 1);
60 | // }
61 | // if (subCategoryLength > 1) {
62 | // const newSubcategiries: string[] = newCategoriesFilter[
63 | // categoryIndex
64 | // ].subCategories.filter(
65 | // (_subCutegory: string) => _subCutegory !== subCutegory
66 | // );
67 | // newCategoriesFilter[categoryIndex].subCategories = [...newSubcategiries];
68 | // }
69 | // this.categoriesFilter = [...newCategoriesFilter];
70 | // };
71 |
72 | // @action
73 | // public selectAllSubCategories = (category: string) => {
74 | // const categoryIndex: number = findCategoryIndex(
75 | // this.categoriesFilter,
76 | // category
77 | // );
78 | // const subCategories: string[] = categoriesKeys.find(
79 | // (_category: Category) => _category.category === category
80 | // ).subCategories;
81 | // if (this.categoriesFilter[categoryIndex]) {
82 | // const newCategoriesFilter: Category[] = [...this.categoriesFilter];
83 | // newCategoriesFilter[categoryIndex].subCategories = [...subCategories];
84 | // this.categoriesFilter = [...newCategoriesFilter];
85 | // } else {
86 | // this.categoriesFilter = [
87 | // ...this.categoriesFilter,
88 | // { category, subCategories: [...subCategories] }
89 | // ];
90 | // }
91 | // };
92 | // }
93 |
--------------------------------------------------------------------------------
/src/app/stores/RouterStore.ts:
--------------------------------------------------------------------------------
1 | import { History } from 'history';
2 | import {
3 | RouterStore as BaseRouterStore,
4 | syncHistoryWithStore
5 | } from 'mobx-react-router';
6 | import { observable } from 'mobx';
7 |
8 | export class RouterStore extends BaseRouterStore {
9 | @observable history: any = null;
10 | constructor(history?: History) {
11 | super();
12 | if (history) {
13 | this.history = syncHistoryWithStore(history, this);
14 | }
15 | }
16 | }
17 |
18 | export default RouterStore;
19 |
--------------------------------------------------------------------------------
/src/app/stores/TodoStore.ts:
--------------------------------------------------------------------------------
1 | import { observable, computed, action } from 'mobx';
2 | import { TodoModel } from 'app/models';
3 |
4 | export class TodoStore {
5 | constructor(fixtures: TodoModel[]) {
6 | this.todos = fixtures;
7 | }
8 |
9 | @observable public todos: Array;
10 |
11 | @computed
12 | get activeTodos() {
13 | return this.todos.filter((todo) => !todo.completed);
14 | }
15 |
16 | @computed
17 | get completedTodos() {
18 | return this.todos.filter((todo) => todo.completed);
19 | }
20 |
21 | @action
22 | addTodo = (item: Partial): void => {
23 | this.todos.push(new TodoModel(item.text, item.completed));
24 | };
25 |
26 | @action
27 | editTodo = (id: number, data: Partial): void => {
28 | this.todos = this.todos.map((todo) => {
29 | if (todo.id === id) {
30 | if (typeof data.completed == 'boolean') {
31 | todo.completed = data.completed;
32 | }
33 | if (typeof data.text == 'string') {
34 | todo.text = data.text;
35 | }
36 | }
37 | return todo;
38 | });
39 | };
40 |
41 | @action
42 | deleteTodo = (id: number): void => {
43 | this.todos = this.todos.filter((todo) => todo.id !== id);
44 | };
45 |
46 | @action
47 | completeAll = (): void => {
48 | this.todos = this.todos.map((todo) => ({ ...todo, completed: true }));
49 | };
50 |
51 | @action
52 | clearCompleted = (): void => {
53 | this.todos = this.todos.filter((todo) => !todo.completed);
54 | };
55 | }
56 |
57 | export default TodoStore;
58 |
--------------------------------------------------------------------------------
/src/app/stores/UIStore.ts:
--------------------------------------------------------------------------------
1 | import { observable, computed, action } from 'mobx';
2 |
3 | export class UIStore {
4 | public rootStore;
5 | @observable public showMobileNavigation = false;
6 | @observable public showCart = false;
7 | @observable public loading = false;
8 | @observable public error: string;
9 |
10 | constructor(rootStore) {
11 | this.rootStore = rootStore;
12 | this.showCart = false;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/stores/createStore.ts:
--------------------------------------------------------------------------------
1 | import { History } from 'history';
2 | import { ProductModel } from 'app/models';
3 | import { CatalogStore } from './CatalogStore';
4 | import { RouterStore } from './RouterStore';
5 | import { CartStore } from './CartStore';
6 | import { UIStore } from './UIStore';
7 | import {
8 | STORE_ROUTER,
9 | STORE_CATALOG,
10 | STORE_CART,
11 | STORE_UI
12 | } from 'app/constants';
13 |
14 | export function createStores(
15 | history: History,
16 | defaultProducts?: ProductModel[]
17 | ) {
18 | const routerStore = new RouterStore(history);
19 | const catalogStore = new CatalogStore();
20 | const cartStore = new CartStore();
21 | const uiStore = new UIStore(this);
22 | return {
23 | [STORE_ROUTER]: routerStore,
24 | [STORE_CATALOG]: catalogStore,
25 | [STORE_CART]: cartStore,
26 | [STORE_UI]: uiStore
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/stores/index.ts:
--------------------------------------------------------------------------------
1 | export * from './TodoStore';
2 | export * from './RouterStore';
3 | export * from './createStore';
4 | export * from './CatalogStore';
5 | export * from './CartStore';
6 | export * from './UIStore';
7 |
--------------------------------------------------------------------------------
/src/app/styles/index.scss:
--------------------------------------------------------------------------------
1 | // 1. Your custom variables and variable overwrites.
2 | $global-link-color: #da7d02;
3 |
4 | $font-stack: Helvetica, sans-serif;
5 |
6 | // https://hallister.github.io/semantic-react/#Button
7 | $myCustomFonts_01: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
8 | $primary-color: #333;
9 | $secondary-color: #1b1c1d;
10 |
11 | body {
12 | font: 100% $font-stack;
13 | color: $primary-color;
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/utils/api.ts:
--------------------------------------------------------------------------------
1 | // import qs from 'qs';
2 | import * as urls from 'app/constants/urls';
3 | export async function getProductsList() {
4 | let products = [];
5 | await fetch(urls.PRODUCTS_URL)
6 | .then((res) => res.json())
7 | .catch((error) => console.error('Error:', error))
8 | .then((response) => {
9 | products = response;
10 | });
11 | return products;
12 | }
13 |
14 | export async function brandsFindOne(id) {
15 | console.log(id);
16 | let brand;
17 | await fetch(`http://localhost:1337/brands/${id}`)
18 | .then((res) => res.json())
19 | .then((data) => {
20 | console.log(data);
21 | brand = data;
22 | })
23 | .catch((error) => console.error('Error in brandsFindOne:', error));
24 | return brand;
25 | }
26 | export async function categoriesFindOne(id) {
27 | let category;
28 | await fetch(`http://localhost:1337/categories/${id}`)
29 | .then((res) => res.json())
30 | .then((data) => {
31 | console.log(data);
32 | category = data;
33 | })
34 | .catch((error) => console.error('Error:', error));
35 | return category;
36 | }
37 |
38 | export async function getProductsByCategory(categoryName) {
39 | return new Promise((resolve) => {
40 | fetch(`http://localhost:1337/products?category_rel=${categoryName}`)
41 | .then((res) => res.json())
42 | .then((data) => {
43 | console.log(data);
44 | return resolve(data);
45 | })
46 | .catch((error) => console.error('Error:', error));
47 | });
48 | }
49 |
50 | // export async function getProductById(productId) {
51 | // let product = {};
52 | // await fetch(urls.PRODUCT_GET_BY_ID_URL + productId)
53 | // .then((res) => res.json())
54 | // .catch((error) => console.error('Error:', error))
55 | // .then((response) => {
56 | // product = response;
57 | // console.log(product);
58 | // });
59 | // return product;
60 | // }
61 | export const getProductById = (id): Promise<{}> => {
62 | return new Promise((resolve) => {
63 | return fetch(urls.PRODUCT_GET_BY_ID_URL + id)
64 | .then((response) => response.json())
65 | .then((data) => resolve(data))
66 | .catch((error) => {
67 | console.log('An error occurred:', error);
68 | });
69 | });
70 | };
71 | export async function getCategoriesList() {
72 | let categories = [];
73 | await fetch(urls.CATEGORIES_URL)
74 | .then((res) => res.json())
75 | .catch((error) => console.error('Error:', error))
76 | .then((response) => {
77 | categories = response;
78 | });
79 | return categories;
80 | }
81 |
82 | export async function getBrandsList() {
83 | let brands = [];
84 | await fetch(urls.BRANDS_URL)
85 | .then((res) => res.json())
86 | .then((response) => {
87 | brands = response;
88 | })
89 | .catch((error) => console.error('Error:', error));
90 |
91 | return brands;
92 | }
93 |
--------------------------------------------------------------------------------
/src/app/utils/api/apiClient.ts:
--------------------------------------------------------------------------------
1 | export default class ApiClient {
2 | baseUrl;
3 | token;
4 | constructor(options) {
5 | this.baseUrl = options.baseUrl;
6 | this.token = options.token;
7 | }
8 |
9 | static returnStatusAndJson = (response) => {
10 | // response.status (number) - HTTP response code in the 100–599 range
11 | // response.statusText (String) - Status text as reported by the server, e.g. "Unauthorized"
12 | // response.ok (boolean) - True if status is HTTP 2xx
13 | // response.headers (Headers)
14 | // response.url (String)
15 |
16 | return response
17 | .json()
18 | .then((json) => ({ status: response.status, json: json }))
19 | .catch(() => ({ status: response.status, json: null }));
20 | };
21 | getConfig = (method, data) => {
22 | let config = {
23 | body: null,
24 | credentials: 'same-origin',
25 | method: method,
26 | headers: {
27 | 'Content-Type': 'application/json',
28 | 'Accept-Encoding': 'gzip, deflate',
29 | Authorization: 'Bearer ' + this.token
30 | }
31 | };
32 |
33 | if (data) {
34 | config.body = JSON.stringify(data);
35 | }
36 | return config;
37 | };
38 | get = (endpoint, filter = '') => {
39 | return fetch(this.baseUrl + endpoint + filter)
40 | .then((response) => response.json())
41 | .then((data) => data);
42 | };
43 |
44 | post = (endpoint, data) => {
45 | return fetch(this.baseUrl + endpoint, this.getConfig('post', data))
46 | .then((response) => response.json())
47 | .then((data) => data);
48 | };
49 |
50 | put = (endpoint, data) => {
51 | return fetch(this.baseUrl + endpoint, this.getConfig('put', data)).then(
52 | ApiClient.returnStatusAndJson
53 | );
54 | };
55 |
56 | delete = (endpoint) => {
57 | return fetch(this.baseUrl + endpoint, this.getConfig('delete')).then(
58 | ApiClient.returnStatusAndJson
59 | );
60 | };
61 |
62 | postFormData = (endpoint) => {
63 | // var input = document.querySelector('input[type="file"]');
64 | let input;
65 | let formData = new FormData();
66 | for (const file of input.files) {
67 | formData.append('files', file, file.name);
68 | }
69 | return fetch(this.baseUrl + endpoint, {
70 | method: 'POST',
71 | headers: {
72 | 'Content-Type': 'multipart/form-data',
73 | Accept: 'application/json',
74 | Authorization: 'AUTH TOKEN'
75 | },
76 | body: formData
77 | });
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/src/app/utils/api/brands.ts:
--------------------------------------------------------------------------------
1 | export default class BrandsApi {
2 | client: any;
3 | constructor(client) {
4 | this.client = client;
5 | }
6 |
7 | list(filter) {
8 | return this.client.get('/brands', filter);
9 | }
10 |
11 | // retrieve(id) {
12 | // return this.client.get(`/brands/${id}`);
13 | // }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/utils/api/categories.ts:
--------------------------------------------------------------------------------
1 | export default class CategoriesApi {
2 | client: any;
3 | constructor(client) {
4 | this.client = client;
5 | }
6 |
7 | list(filter) {
8 | return this.client.get('/categories', filter);
9 | }
10 |
11 | // retrieve(id) {
12 | // return this.client.get(`/categories/${id}`);
13 | // }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/utils/api/client.ts:
--------------------------------------------------------------------------------
1 | // import CategoriesApi0 from 'app/utils/api/categories0';
2 | import CategoriesApi from 'app/utils/api/categories';
3 | // import ProductsApi0 from 'app/utils/api/products0';
4 | import ProductsApi from 'app/utils/api/products';
5 | import BrandsApi from 'app/utils/api/brands';
6 | import ApiClient from 'app/utils/api/apiClient';
7 |
8 | // import { ApiClient } from 'app/utils/api/client';
9 | interface IProps {
10 | baseUrl: string;
11 | client: any;
12 | }
13 |
14 | class Client {
15 | baseUrl;
16 |
17 | apiBaseUrl;
18 | apiTest;
19 | products;
20 | categories;
21 | brands;
22 | constructor(options) {
23 | if (!options) {
24 | options = {};
25 | }
26 |
27 | this.apiBaseUrl = options.apiBaseUrl || 'http://localhost:1337';
28 | const apiClientTest = new ApiClient({ baseUrl: this.apiBaseUrl });
29 | this.products = new ProductsApi(apiClientTest);
30 | this.categories = new CategoriesApi(apiClientTest);
31 | this.brands = new BrandsApi(apiClientTest);
32 | }
33 | }
34 | export default Client;
35 |
--------------------------------------------------------------------------------
/src/app/utils/api/products.ts:
--------------------------------------------------------------------------------
1 | export default class ProductsApi {
2 | client: any;
3 | constructor(client) {
4 | this.client = client;
5 | }
6 |
7 | list(filter) {
8 | return this.client.get('/products', filter);
9 | }
10 | item(id) {
11 | return this.client.get(`/products/${id}`);
12 | }
13 | // create(data) {
14 | // return this.client.post(`/categories`, data);
15 | // }
16 |
17 | // update(id, data) {
18 | // return this.client.put(`/categories/${id}`, data);
19 | // }
20 |
21 | // delete(id) {
22 | // return this.client.delete(`/categories/${id}`);
23 | // }
24 |
25 | // uploadImage(categoryId, formData) {
26 | // return this.client.postFormData(
27 | // `/categories/${categoryId}/image`,
28 | // formData
29 | // );
30 | // }
31 |
32 | // deleteImage(id) {
33 | // return this.client.delete(`/categories/${id}/image`);
34 | // }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/utils/firebase.ts:
--------------------------------------------------------------------------------
1 | // import * as firebase from 'firebase';
2 |
3 | // export const API_KEY = process.env.FIREBASE_API_KEY;
4 | // export const PROJECT_ID = process.env.FIREBASE_PROJECT_ID;
5 | // export const AUTH_DOMAIN = process.env.FIREBASE_AUTH_DOMAIN;
6 | // export const DATABASE_URL = process.env.FIREBASE_DATABASE_URL;
7 | // export const STORAGE_BUCKET = process.env.FIREBASE_STORAGE_BUCKET;
8 | // export const MESSAGING_SENDER_ID = process.env.FIREBASE_MESSAGING_SENDER_ID;
9 |
10 | // const config = {
11 | // apiKey: API_KEY,
12 | // projectId: PROJECT_ID,
13 | // authDomain: AUTH_DOMAIN,
14 | // databaseURL: DATABASE_URL,
15 | // storageBucket: STORAGE_BUCKET,
16 | // messagingSenderId: MESSAGING_SENDER_ID
17 | // };
18 |
19 | // firebase.initializeApp(config);
20 |
--------------------------------------------------------------------------------
/src/app/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | // removes a value from array
2 | export function arrayRemove(arr, value) {
3 | return arr.filter(function(ele) {
4 | return ele != value;
5 | });
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/utils/helpers/format-money.ts:
--------------------------------------------------------------------------------
1 | export const formatMoneyNative = (
2 | num: number,
3 | currency: string = 'RUB'
4 | ): number | string => {
5 | const formatter = new Intl.NumberFormat('ru-RU', {
6 | style: 'currency',
7 | currency,
8 | maximumSignificantDigits: 2
9 | });
10 |
11 | return num && formatter.format(num);
12 | };
13 |
14 | export const formatMoney = (
15 | num: number,
16 | penny: number = 2,
17 | breaking: boolean = true
18 | ): number | string => {
19 | let value =
20 | num &&
21 | parseFloat(num.toString())
22 | .toFixed(penny)
23 | .replace(/\d(?=(\d{3})+\.)/g, '$& ')
24 | .replace(/\./g, ',')
25 | .replace(/,0{2}/g, '');
26 |
27 | if (!breaking) {
28 | value = value && value.replace(/\s/g, ' ');
29 | }
30 |
31 | return (num && value) || 0;
32 | };
33 |
--------------------------------------------------------------------------------
/src/app/utils/helpers/is-active.ts:
--------------------------------------------------------------------------------
1 | export const isActive = (match?: any, location?: Location) => {
2 | if (!match) {
3 | return false;
4 | }
5 |
6 | return match && location && location.pathname === match.url;
7 | };
8 |
--------------------------------------------------------------------------------
/src/app/utils/helpers/is-email.ts:
--------------------------------------------------------------------------------
1 | export const isEmail = (str: string) => {
2 | const re = new RegExp(
3 | // tslint:disable-next-line
4 | /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\d-A-Za-z]+\.)+[A-Za-z]{2,}))$/
5 | );
6 |
7 | return str && !re.test(str[name]);
8 | };
9 |
--------------------------------------------------------------------------------
/src/app/utils/helpers/is-phone.ts:
--------------------------------------------------------------------------------
1 | export const isPhone = (phone: string): boolean =>
2 | !!(phone && phone.replace(/[\s()+\-A-Z_a-z]+/gm, '').length === 11);
3 |
--------------------------------------------------------------------------------
/src/app/utils/request.ts:
--------------------------------------------------------------------------------
1 | // import { notification } from 'antd';
2 | import * as history from 'history';
3 | // https://github.com/denipotapo/react-app-ts/blob/master/src/utils/request.ts
4 |
5 | const myHistory = history.createHashHistory();
6 |
7 | const codeMessage = {
8 | 200: '服务器成功返回请求的数据。',
9 | 201: '新建或修改数据成功。',
10 | 202: '一个请求已经进入后台排队(异步任务)。',
11 | 204: '删除数据成功。',
12 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
13 | 401: '用户没有权限(令牌、用户名、密码错误)。',
14 | 403: '用户得到授权,但是访问是被禁止的。',
15 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
16 | 406: '请求的格式不可得。',
17 | 410: '请求的资源被永久删除,且不会再得到的。',
18 | 422: '当创建一个对象时,发生一个验证错误。',
19 | 500: '服务器发生错误,请检查服务器。',
20 | 502: '网关错误。',
21 | 503: '服务不可用,服务器暂时过载或维护。',
22 | 504: '网关超时。'
23 | };
24 |
25 | function checkStatus(response) {
26 | if (response.status >= 200 && response.status < 300) {
27 | return response;
28 | }
29 | const errortext = codeMessage[response.status] || response.statusText;
30 | // notification.error({
31 | // message: `请求错误 ${response.status}: ${response.url}`,
32 | // description: errortext,
33 | // });
34 | const error = new Error(errortext);
35 | error.name = response.status;
36 | error.message = response;
37 | throw error;
38 | }
39 |
40 | /**
41 | * Requests a URL, returning a promise.
42 | *
43 | * @param {string} url The URL we want to request
44 | * @param {object} [options] The options we want to pass to "fetch"
45 | * @return {object} An object containing either "data" or "err"
46 | */
47 | interface IMyOptionObj {
48 | method?: string;
49 | body?: any;
50 | headers?: any;
51 | credentials?: any;
52 | }
53 |
54 | export default function request(url: string, options: object) {
55 | const defaultOptions = {
56 | credentials: 'include'
57 | };
58 | const newOptions: IMyOptionObj = { ...defaultOptions, ...options };
59 | if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
60 | if (!(newOptions.body instanceof FormData)) {
61 | newOptions.headers = {
62 | Accept: 'application/json',
63 | 'Content-Type': 'application/json; charset=utf-8',
64 | ...newOptions.headers
65 | };
66 | newOptions.body = JSON.stringify(newOptions.body);
67 | } else {
68 | // newOptions.body is FormData
69 | newOptions.headers = {
70 | Accept: 'application/json',
71 | ...newOptions.headers
72 | };
73 | }
74 | }
75 |
76 | return fetch(url, newOptions)
77 | .then(checkStatus)
78 | .then((response) => {
79 | if (newOptions.method === 'DELETE' || response.status === 204) {
80 | return Promise.resolve(response.text());
81 | }
82 | return Promise.resolve(response.json());
83 | })
84 | .catch((err) => {
85 | const status = err.name;
86 | if (status === 401) {
87 | myHistory.push('/user/login');
88 | return;
89 | }
90 | if (status === 403) {
91 | myHistory.push('/exception/403');
92 | return;
93 | }
94 | if (status <= 504 && status >= 500) {
95 | myHistory.push('/exception/500');
96 | return;
97 | }
98 | if (status >= 404 && status < 422) {
99 | myHistory.push('/exception/404');
100 | }
101 | });
102 | }
103 |
--------------------------------------------------------------------------------
/src/assets/img/M998TCC_600x400.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spac3unit/ecommerce-mobx-typescript/d5380b1c3bf520b810abc403adc8e29de67b1978/src/assets/img/M998TCC_600x400.jpg
--------------------------------------------------------------------------------
/src/assets/img/MS247LA_1_1280x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spac3unit/ecommerce-mobx-typescript/d5380b1c3bf520b810abc403adc8e29de67b1978/src/assets/img/MS247LA_1_1280x.jpg
--------------------------------------------------------------------------------
/src/assets/img/MSX90RCC_1_1280x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spac3unit/ecommerce-mobx-typescript/d5380b1c3bf520b810abc403adc8e29de67b1978/src/assets/img/MSX90RCC_1_1280x.jpg
--------------------------------------------------------------------------------
/src/assets/img/mj810211nbs_40.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spac3unit/ecommerce-mobx-typescript/d5380b1c3bf520b810abc403adc8e29de67b1978/src/assets/img/mj810211nbs_40.jpg
--------------------------------------------------------------------------------
/src/assets/img/nb-MSX90_small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spac3unit/ecommerce-mobx-typescript/d5380b1c3bf520b810abc403adc8e29de67b1978/src/assets/img/nb-MSX90_small.jpg
--------------------------------------------------------------------------------
/src/assets/img/nb-WSX90_small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spac3unit/ecommerce-mobx-typescript/d5380b1c3bf520b810abc403adc8e29de67b1978/src/assets/img/nb-WSX90_small.jpg
--------------------------------------------------------------------------------
/src/assets/img/new_balance-ML840NTB-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spac3unit/ecommerce-mobx-typescript/d5380b1c3bf520b810abc403adc8e29de67b1978/src/assets/img/new_balance-ML840NTB-1.jpg
--------------------------------------------------------------------------------
/src/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shopping App @React Mobx TS
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
33 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import { configure } from 'mobx';
4 |
5 | import { Provider } from 'mobx-react';
6 | import { createBrowserHistory } from 'history';
7 |
8 | import { TodoModel, ProductModel } from 'app/models';
9 | import { createStores } from 'app/stores';
10 | import { App } from 'app';
11 |
12 | import * as mockData from 'app/mock.json';
13 |
14 | // enable MobX strict mode
15 | // configure({ enforceActions: 'always' });
16 |
17 | //default items
18 | const defaultTodos = [];
19 | const defaultProducts = [];
20 | // const defaultProducts: ProductModel[] = mockData.default.sneakers.map(
21 | // (product) => new ProductModel(product)
22 | // );
23 |
24 | const history = createBrowserHistory();
25 | const rootStore = createStores(history, defaultProducts);
26 |
27 | // render react DOM
28 | ReactDOM.render(
29 |
30 |
31 | ,
32 | document.getElementById('root')
33 | );
34 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "target": "es5",
5 | "jsx": "react",
6 | "module": "es6",
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "declaration": false,
11 | "noImplicitAny": false,
12 | "noImplicitReturns": false,
13 | "noUnusedLocals": false,
14 | "removeComments": true,
15 | "strictNullChecks": false,
16 | "outDir": "build",
17 | "lib": ["es6", "es7", "dom"],
18 | "baseUrl": "src",
19 | "paths": {
20 | "app/*": ["./app/*"]
21 | }
22 | },
23 | "exclude": ["dist", "build", "node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/types/global.d.ts:
--------------------------------------------------------------------------------
1 | /** Global definitions for developement **/
2 |
3 | // for style loader
4 | declare module '*.css' {
5 | const styles: any;
6 | export = styles;
7 | }
8 | declare module '*.json' {
9 | const value: any;
10 | export default value;
11 | }
12 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | // variables
5 | var isProduction = process.argv.indexOf('-p') >= 0;
6 | var sourcePath = path.join(__dirname, './src');
7 | var outPath = path.join(__dirname, './dist');
8 |
9 | // plugins
10 | var HtmlWebpackPlugin = require('html-webpack-plugin');
11 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
12 | var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
13 |
14 | module.exports = {
15 | context: sourcePath,
16 | entry: {
17 | main: './main.tsx'
18 | },
19 | output: {
20 | path: outPath,
21 | filename: 'bundle.js',
22 | chunkFilename: '[chunkhash].js',
23 | publicPath: '/'
24 | },
25 | target: 'web',
26 | resolve: {
27 | extensions: ['.js', '.ts', '.tsx'],
28 | // Fix webpack's default behavior to not load packages with jsnext:main module
29 | // (jsnext:main directs not usually distributable es6 format, but es6 sources)
30 | mainFields: ['module', 'browser', 'main'],
31 | alias: {
32 | app: path.resolve(__dirname, 'src/app/')
33 | }
34 | },
35 | module: {
36 | rules: [
37 | // .ts, .tsx
38 | {
39 | test: /\.tsx?$/,
40 | use: isProduction
41 | ? 'ts-loader'
42 | : ['babel-loader?plugins=react-hot-loader/babel', 'ts-loader']
43 | },
44 | // css
45 | {
46 | test: /\.css$/,
47 | use: ExtractTextPlugin.extract({
48 | fallback: 'style-loader',
49 | use: [
50 | {
51 | loader: 'css-loader',
52 | query: {
53 | modules: true,
54 | sourceMap: !isProduction,
55 | importLoaders: 1,
56 | localIdentName: '[local]__[hash:base64:5]'
57 | }
58 | },
59 | {
60 | loader: 'postcss-loader',
61 | options: {
62 | ident: 'postcss',
63 | plugins: [
64 | require('postcss-import')({ addDependencyTo: webpack }),
65 | require('postcss-url')(),
66 | require('postcss-cssnext')(),
67 | require('precss')(),
68 | require('postcss-reporter')(),
69 | require('postcss-browser-reporter')({
70 | disabled: isProduction
71 | })
72 | ]
73 | }
74 | }
75 | ]
76 | })
77 | },
78 | {
79 | test: /\.scss$/,
80 | use: [
81 | { loader: 'style-loader' },
82 | { loader: 'css-loader' },
83 | { loader: 'sass-loader' }
84 | ]
85 | },
86 | // static assets
87 | { test: /\.html$/, use: 'html-loader' },
88 | { test: /\.png$/, use: 'url-loader?limit=10000' },
89 | { test: /\.jpg$/, use: 'file-loader' }
90 | ]
91 | },
92 | optimization: {
93 | splitChunks: {
94 | name: true,
95 | cacheGroups: {
96 | commons: {
97 | chunks: 'initial',
98 | minChunks: 2
99 | },
100 | vendors: {
101 | test: /[\\/]node_modules[\\/]/,
102 | chunks: 'all',
103 | priority: -10
104 | }
105 | }
106 | },
107 | runtimeChunk: true
108 | },
109 | plugins: [
110 | new WebpackCleanupPlugin(),
111 | new ExtractTextPlugin({
112 | filename: 'styles.css',
113 | disable: !isProduction
114 | }),
115 | new HtmlWebpackPlugin({
116 | template: 'assets/index.html'
117 | })
118 | ],
119 | devServer: {
120 | contentBase: sourcePath,
121 | hot: true,
122 | inline: true,
123 | historyApiFallback: {
124 | disableDotRule: true
125 | },
126 | stats: 'minimal'
127 | },
128 | devtool: 'cheap-module-eval-source-map',
129 | node: {
130 | // workaround for webpack-dev-server issue
131 | // https://github.com/webpack/webpack-dev-server/issues/60#issuecomment-103411179
132 | fs: 'empty',
133 | net: 'empty'
134 | }
135 | };
136 |
--------------------------------------------------------------------------------