├── .editorconfig
├── .env
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── @types
│ ├── action.d.ts
│ ├── api.d.ts
│ ├── files.d.ts
│ ├── product.d.ts
│ ├── reducer.d.ts
│ └── user.d.ts
├── App
│ ├── App.actions.ts
│ ├── App.constants.ts
│ ├── App.reducer.ts
│ └── App.tsx
├── apis
│ ├── product.api.ts
│ └── user.api.ts
├── assets
│ ├── fonts
│ │ ├── OpenSans-Bold.ttf
│ │ ├── OpenSans-BoldItalic.ttf
│ │ ├── OpenSans-ExtraBold.ttf
│ │ ├── OpenSans-ExtraBoldItalic.ttf
│ │ ├── OpenSans-Italic.ttf
│ │ ├── OpenSans-Light.ttf
│ │ ├── OpenSans-LightItalic.ttf
│ │ ├── OpenSans-Regular.ttf
│ │ ├── OpenSans-SemiBold.ttf
│ │ └── OpenSans-SemiBoldItalic.ttf
│ ├── images
│ │ ├── home.svg
│ │ ├── list.svg
│ │ └── open-menu.svg
│ └── scss
│ │ └── index.scss
├── components
│ ├── Header
│ │ ├── Header.styles.ts
│ │ └── Header.tsx
│ ├── Loading
│ │ └── Loading.tsx
│ └── SideNav
│ │ ├── SideNav.styles.ts
│ │ └── SideNav.tsx
├── constants
│ ├── paths.ts
│ └── styles.ts
├── guards
│ └── AuthenticatedGuard.tsx
├── helpers
│ └── string.ts
├── hooks
│ └── usePrevious.tsx
├── index.tsx
├── layouts
│ └── MainLayout.tsx
├── logo.svg
├── pages
│ ├── Home
│ │ └── Home.tsx
│ ├── Login
│ │ ├── Login.actions.ts
│ │ ├── Login.constants.ts
│ │ ├── Login.reducer.ts
│ │ ├── Login.styles.ts
│ │ ├── Login.thunks.ts
│ │ └── Login.tsx
│ └── Product
│ │ ├── ProductItem
│ │ ├── ProductItem.actions.ts
│ │ ├── ProductItem.constants.ts
│ │ ├── ProductItem.reducer.ts
│ │ ├── ProductItem.thunks.ts
│ │ └── ProductItem.tsx
│ │ └── ProductList
│ │ ├── ProductList.actions.ts
│ │ ├── ProductList.constants.ts
│ │ ├── ProductList.reducer.ts
│ │ ├── ProductList.styles.ts
│ │ ├── ProductList.thunks.ts
│ │ └── ProductList.tsx
├── react-app-env.d.ts
├── reducer
│ └── reducer.ts
├── routes
│ ├── HomeRoutes.tsx
│ ├── LoginRoutes.tsx
│ ├── ProductRoutes.tsx
│ └── routes.tsx
├── serviceWorker.ts
├── setupTests.ts
└── store
│ └── store.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | indent_size = 2
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | EXTEND_ESLINT=true
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /src/serviceWorker.ts
2 | /src/setupTests.ts
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app", "prettier"],
3 | "plugins": ["react", "prettier"],
4 | "rules": {
5 | "prettier/prettier": [
6 | "warn",
7 | {
8 | "arrowParens": "avoid",
9 | "semi": false,
10 | "trailingComma": "none",
11 | "endOfLine": "lf",
12 | "tabWidth": 2,
13 | "printWidth": 80,
14 | "useTabs": false
15 | }
16 | ],
17 | "no-console": "warn"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "semi": false,
4 | "trailingComma": "none",
5 | "endOfLine": "lf",
6 | "tabWidth": 2,
7 | "printWidth": 80,
8 | "useTabs": false
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "akamud.vscode-theme-onedark",
4 | "esbenp.prettier-vscode",
5 | "alefragnani.Bookmarks",
6 | "CoenraadS.bracket-pair-colorizer",
7 | "cssho.vscode-svgviewer",
8 | "dbaeumer.vscode-eslint",
9 | "dsznajder.es7-react-js-snippets",
10 | "eamodio.gitlens",
11 | "EditorConfig.EditorConfig",
12 | "formulahendry.auto-close-tag",
13 | "formulahendry.auto-rename-tag",
14 | "jpoissonnier.vscode-styled-components",
15 | "PKief.material-icon-theme",
16 | "streetsidesoftware.code-spell-checker",
17 | "ritwickdey.LiveServer",
18 | "riazxrazor.html-to-jsx"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "editor.formatOnSave": true,
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.eslint": true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cấu trúc thư mục React tối ưu
2 |
3 | ## Login
4 |
5 | - username: admin
6 | - password: 123
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "React-Folder-Structure",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "bootstrap": "5.0.0-alpha1",
7 | "immer": "^9.0.1",
8 | "react": "^17.0.2",
9 | "react-dom": "^17.0.2",
10 | "react-redux": "^7.2.3",
11 | "react-router-dom": "^5.2.0",
12 | "react-scripts": "4.0.3",
13 | "redux": "^4.0.5",
14 | "redux-thunk": "^2.3.0",
15 | "styled-components": "^5.2.3",
16 | "typescript": "~4.2.3"
17 | },
18 | "devDependencies": {
19 | "@testing-library/jest-dom": "^5.11.10",
20 | "@testing-library/react": "^11.2.6",
21 | "@testing-library/user-event": "^13.1.1",
22 | "@types/jest": "^26.0.22",
23 | "@types/node": "^14.14.37",
24 | "@types/react": "^17.0.3",
25 | "@types/react-dom": "^17.0.3",
26 | "@types/react-redux": "^7.1.16",
27 | "@types/react-router-dom": "^5.1.7",
28 | "@types/redux": "^3.6.31",
29 | "@types/redux-thunk": "^2.1.32",
30 | "@types/styled-components": "^5.1.9",
31 | "eslint-config-prettier": "^8.1.0",
32 | "eslint-plugin-prettier": "^3.3.1",
33 | "node-sass": "^5.0.0",
34 | "prettier": "^2.2.1"
35 | },
36 | "scripts": {
37 | "start": "react-scripts start",
38 | "build": "react-scripts build",
39 | "test": "react-scripts test",
40 | "eject": "react-scripts eject",
41 | "lint": "eslint --ext js,jsx,ts,tsx src/",
42 | "lint:fix": "eslint --fix --ext js,jsx,ts,tsx src/",
43 | "prettier": "prettier --check \"src/**/(*.tsx|*.ts|*.jsx|*.js|*.scss|*.css)\"",
44 | "prettier:fix": "prettier --write \"src/**/(*.tsx|*.ts|*.jsx|*.js|*.scss|*.css)\""
45 | },
46 | "eslintConfig": {
47 | "extends": "react-app"
48 | },
49 | "browserslist": {
50 | "production": [
51 | ">0.2%",
52 | "not dead",
53 | "not op_mini all"
54 | ],
55 | "development": [
56 | "last 1 chrome version",
57 | "last 1 firefox version",
58 | "last 1 safari version"
59 | ]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/@types/action.d.ts:
--------------------------------------------------------------------------------
1 | interface ActionRedux {
2 | types: string
3 | payload?: any
4 | }
5 |
--------------------------------------------------------------------------------
/src/@types/api.d.ts:
--------------------------------------------------------------------------------
1 | interface Res {
2 | data: any
3 | message: string
4 | }
5 |
--------------------------------------------------------------------------------
/src/@types/files.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | declare namespace NodeJS {
6 | interface ProcessEnv {
7 | readonly NODE_ENV: "development" | "production" | "test"
8 | readonly PUBLIC_URL: string
9 | }
10 | }
11 |
12 | declare module "*.bmp" {
13 | const src: string
14 | export default src
15 | }
16 |
17 | declare module "*.gif" {
18 | const src: string
19 | export default src
20 | }
21 |
22 | declare module "*.jpg" {
23 | const src: string
24 | export default src
25 | }
26 |
27 | declare module "*.jpeg" {
28 | const src: string
29 | export default src
30 | }
31 |
32 | declare module "*.png" {
33 | const src: string
34 | export default src
35 | }
36 |
37 | declare module "*.webp" {
38 | const src: string
39 | export default src
40 | }
41 |
42 | declare module "*.svg" {
43 | import * as React from "react"
44 |
45 | export const ReactComponent: React.FunctionComponent<
46 | React.SVGProps & { title?: string }
47 | >
48 |
49 | const src: string
50 | export default src
51 | }
52 |
53 | declare module "*.module.css" {
54 | const classes: { readonly [key: string]: string }
55 | export default classes
56 | }
57 |
58 | declare module "*.module.scss" {
59 | const classes: { readonly [key: string]: string }
60 | export default classes
61 | }
62 |
63 | declare module "*.module.sass" {
64 | const classes: { readonly [key: string]: string }
65 | export default classes
66 | }
67 |
--------------------------------------------------------------------------------
/src/@types/product.d.ts:
--------------------------------------------------------------------------------
1 | interface Product {
2 | id: string
3 | name: string
4 | quantity: number
5 | price: number
6 | }
7 |
8 | interface ResGetProductApi extends Res {
9 | data: {
10 | products: Product[]
11 | }
12 | }
13 |
14 | interface ResGetProduct extends ActionRedux {
15 | payload: ResGetProductApi
16 | }
17 |
18 | interface ResGetProductItemApi extends Res {
19 | data: {
20 | product: Product
21 | }
22 | }
23 |
24 | interface ResGetProductItem extends ActionRedux {
25 | payload: ResGetProductItemApi
26 | }
27 |
--------------------------------------------------------------------------------
/src/@types/reducer.d.ts:
--------------------------------------------------------------------------------
1 | import rootReducer from "src/reducer/reducer"
2 |
3 | declare global {
4 | type AppState = ReturnType
5 | }
6 |
--------------------------------------------------------------------------------
/src/@types/user.d.ts:
--------------------------------------------------------------------------------
1 | interface ReqLogin {
2 | username: string
3 | password: string
4 | }
5 | interface ResLoginApi extends Res {
6 | data: {
7 | access_token: string
8 | }
9 | }
10 |
11 | interface ResLogin extends ActionRedux {}
12 |
--------------------------------------------------------------------------------
/src/App/App.actions.ts:
--------------------------------------------------------------------------------
1 | import * as types from "./App.constants"
2 |
3 | export const logout = () => ({
4 | type: types.LOGOUT
5 | })
6 |
7 | export const toggleSideNav = () => ({
8 | type: types.CLOSE_SIDE_NAV
9 | })
10 |
--------------------------------------------------------------------------------
/src/App/App.constants.ts:
--------------------------------------------------------------------------------
1 | export const LOGOUT = "app/LOGOUT"
2 | export const CLOSE_SIDE_NAV = "app/CLOSE_SIDE_NAV"
3 |
--------------------------------------------------------------------------------
/src/App/App.reducer.ts:
--------------------------------------------------------------------------------
1 | import * as types from "./App.constants"
2 | import { LOGIN_SUCCESS } from "src/pages/Login/Login.constants"
3 | import produce from "immer"
4 |
5 | const initialState = {
6 | isAuthenticated: false,
7 | closeSideNav: false
8 | }
9 |
10 | export const AppReducer = (state = initialState, action) =>
11 | produce(state, draft => {
12 | switch (action.type) {
13 | case types.LOGOUT:
14 | localStorage.removeItem("token")
15 | draft.isAuthenticated = false
16 | break
17 | case LOGIN_SUCCESS:
18 | draft.isAuthenticated = true
19 | break
20 | case types.CLOSE_SIDE_NAV:
21 | draft.closeSideNav = !state.closeSideNav
22 | break
23 | default:
24 | return state
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/src/App/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import Routes from "src/routes/routes"
3 |
4 | function App() {
5 | return
6 | }
7 |
8 | export default App
9 |
--------------------------------------------------------------------------------
/src/apis/product.api.ts:
--------------------------------------------------------------------------------
1 | const mockProducts = [
2 | {
3 | id: "1",
4 | name: "Iphone",
5 | quantity: 100,
6 | price: 27000000
7 | },
8 | {
9 | id: "2",
10 | name: "Samsung",
11 | quantity: 28,
12 | price: 22000000
13 | },
14 | {
15 | id: "3",
16 | name: "Nokia",
17 | quantity: 10,
18 | price: 15000000
19 | },
20 | {
21 | id: "4",
22 | name: "Sony",
23 | quantity: 44,
24 | price: 25000000
25 | }
26 | ]
27 |
28 | export const getProductListApi = (): Promise =>
29 | new Promise((resolve, reject) => {
30 | setTimeout(() => {
31 | resolve({
32 | data: {
33 | products: mockProducts
34 | },
35 | message: "Lấy sản phẩm thành công"
36 | })
37 | }, 100)
38 | })
39 |
40 | export const getProductItemApi = (id: string): Promise =>
41 | new Promise((resolve, reject) => {
42 | setTimeout(() => {
43 | const product = mockProducts.find(product => product.id === id)
44 | if (product) {
45 | resolve({
46 | data: {
47 | product
48 | },
49 | message: "Lấy sản phẩm thành công"
50 | })
51 | } else {
52 | reject(new Error("Không tìm thấy sản phẩm"))
53 | }
54 | }, 100)
55 | })
56 |
--------------------------------------------------------------------------------
/src/apis/user.api.ts:
--------------------------------------------------------------------------------
1 | export const loginApi = ({
2 | username,
3 | password
4 | }: ReqLogin): Promise =>
5 | new Promise((resolve, reject) => {
6 | setTimeout(() => {
7 | if (username === "admin" && password === "123") {
8 | resolve({
9 | data: {
10 | access_token: "82jdu82193yh90sad83hxfgsd"
11 | },
12 | message: "Login thành công"
13 | })
14 | } else {
15 | reject(new Error("Login thất bại"))
16 | }
17 | }, 100)
18 | })
19 |
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-Bold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-BoldItalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-ExtraBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-Italic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-LightItalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-SemiBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duthanhduoc/React-Folder-Structure/0b61c55ec356dd7815a7f3daea840c5ba418f583/src/assets/fonts/OpenSans-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/src/assets/images/home.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/images/list.svg:
--------------------------------------------------------------------------------
1 |
2 |
31 |
--------------------------------------------------------------------------------
/src/assets/images/open-menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/scss/index.scss:
--------------------------------------------------------------------------------
1 | @import "node_modules/bootstrap/scss/functions";
2 | @import "node_modules/bootstrap/scss/variables";
3 | @import "node_modules/bootstrap/scss/mixins";
4 | @import "node_modules/bootstrap/scss/reboot";
5 | @import "node_modules/bootstrap/scss/containers";
6 | @import "node_modules/bootstrap/scss/grid";
7 | @import "node_modules/bootstrap/scss/utilities";
8 | @import "node_modules/bootstrap/scss/utilities/api";
9 | @import "node_modules/bootstrap/scss/forms";
10 | @import "node_modules/bootstrap/scss/buttons";
11 | @import "node_modules/bootstrap/scss/tables";
12 |
13 | @font-face {
14 | font-family: "Open Sans";
15 | src: url(../fonts/OpenSans-Light.ttf);
16 | font-weight: 300;
17 | }
18 | @font-face {
19 | font-family: "Open Sans";
20 | src: url(../fonts/OpenSans-LightItalic.ttf);
21 | font-weight: 300;
22 | font-style: italic;
23 | }
24 | @font-face {
25 | font-family: "Open Sans";
26 | src: url(../fonts/OpenSans-Regular.ttf);
27 | font-weight: 400;
28 | }
29 | @font-face {
30 | font-family: "Open Sans";
31 | src: url(../fonts/OpenSans-Italic.ttf);
32 | font-weight: 400;
33 | font-style: italic;
34 | }
35 | @font-face {
36 | font-family: "Open Sans";
37 | src: url(../fonts/OpenSans-SemiBold.ttf);
38 | font-weight: 600;
39 | }
40 | @font-face {
41 | font-family: "Open Sans";
42 | src: url(../fonts/OpenSans-SemiBoldItalic.ttf);
43 | font-weight: 600;
44 | font-style: italic;
45 | }
46 | @font-face {
47 | font-family: "Open Sans";
48 | src: url(../fonts/OpenSans-Bold.ttf);
49 | font-weight: 700;
50 | }
51 | @font-face {
52 | font-family: "Open Sans";
53 | src: url(../fonts/OpenSans-BoldItalic.ttf);
54 | font-weight: 700;
55 | font-style: italic;
56 | }
57 | @font-face {
58 | font-family: "Open Sans";
59 | src: url(../fonts/OpenSans-ExtraBold.ttf);
60 | font-weight: 800;
61 | }
62 | @font-face {
63 | font-family: "Open Sans";
64 | src: url(../fonts/OpenSans-ExtraBoldItalic.ttf);
65 | font-weight: 800;
66 | font-style: italic;
67 | }
68 |
69 | body {
70 | font-family: "Open Sans", sans-serif;
71 | font-weight: 400;
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/Header/Header.styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const LogoutIcon = styled.span``
4 |
--------------------------------------------------------------------------------
/src/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react"
2 | import { connect, ConnectedProps } from "react-redux"
3 | import { logout, toggleSideNav } from "src/App/App.actions"
4 | import { useHistory } from "react-router-dom"
5 | import { LogoutIcon } from "./Header.styles"
6 | import { PATH } from "src/constants/paths"
7 |
8 | const mapStateToProps = state => ({})
9 |
10 | const mapDispatchToProps = {
11 | logout,
12 | toggleSideNav
13 | }
14 |
15 | const connector = connect(mapStateToProps, mapDispatchToProps)
16 |
17 | interface Props extends ConnectedProps {}
18 |
19 | const Header = (props: Props) => {
20 | const { logout, toggleSideNav } = props
21 | const history = useHistory()
22 | const handleLogout = () => {
23 | logout()
24 | history.push(PATH.LOGIN)
25 | }
26 | useEffect(() => {}, [history])
27 |
28 | return (
29 |
30 |
59 |
60 | Logout
61 |
62 |
63 | )
64 | }
65 |
66 | export default connector(Header)
67 |
--------------------------------------------------------------------------------
/src/components/Loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | export default function Loading() {
4 | return loading...
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/SideNav/SideNav.styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { BREAKPOINT, COLOR } from "src/constants/styles"
3 |
4 | export const Footer = styled.div`
5 | padding: 0 30px;
6 | `
7 |
8 | export const Menu = styled.ul`
9 | padding: 0;
10 | transition: 0.3s;
11 | li {
12 | font-size: 16px;
13 | > a {
14 | padding: 10px 30px;
15 | display: flex;
16 | align-items: center;
17 | color: white;
18 | border-bottom: 1px solid rgba(255, 255, 255, 0.1);
19 | text-decoration: none;
20 | img {
21 | width: 20px;
22 | height: auto;
23 | display: inline-block;
24 | margin-right: 10px;
25 | }
26 | &.active {
27 | background: ${COLOR.BLUE};
28 | }
29 | }
30 | }
31 | `
32 |
33 | export const Nav = styled.nav`
34 | min-width: 270px;
35 | max-width: 270px;
36 | background: #3e64ff;
37 | color: #fff;
38 | transition: all 0.3s;
39 | &.close {
40 | min-width: 80px;
41 | max-width: 80px;
42 | text-align: center;
43 | ${Menu} {
44 | li > a span {
45 | display: none;
46 | }
47 | }
48 | ${Footer} {
49 | display: none;
50 | }
51 | }
52 | @media (max-width: ${BREAKPOINT.MD - 1}px) {
53 | min-width: 80px;
54 | max-width: 80px;
55 | text-align: center;
56 | ${Menu} {
57 | li > a span {
58 | display: none;
59 | }
60 | }
61 | ${Footer} {
62 | display: none;
63 | }
64 | &.close {
65 | margin-left: -80px;
66 | }
67 | }
68 | `
69 |
70 | export const Logo = styled.a`
71 | display: block;
72 | color: #fff;
73 | font-weight: 900;
74 | padding: 10px 30px;
75 | transition: 0.3s;
76 | `
77 |
--------------------------------------------------------------------------------
/src/components/SideNav/SideNav.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { NavLink } from "react-router-dom"
3 | import { Footer, Logo, Menu, Nav } from "./SideNav.styles"
4 | import { PATH } from "src/constants/paths"
5 | import home from "src/assets/images/home.svg"
6 | import list from "src/assets/images/list.svg"
7 | import { connect, ConnectedProps } from "react-redux"
8 |
9 | const mapStateToProps = state => ({
10 | closeSideNav: state.app.closeSideNav
11 | })
12 |
13 | const mapDispatchToProps = {}
14 |
15 | const connector = connect(mapStateToProps, mapDispatchToProps)
16 |
17 | interface Props extends ConnectedProps {}
18 |
19 | function SideNav(props: Props) {
20 | const { closeSideNav } = props
21 | return (
22 |
62 | )
63 | }
64 |
65 | export default connector(SideNav)
66 |
--------------------------------------------------------------------------------
/src/constants/paths.ts:
--------------------------------------------------------------------------------
1 | export const PATH = {
2 | HOME: "/",
3 | PRODUCT: "/product",
4 | LOGIN: "/login"
5 | }
6 |
--------------------------------------------------------------------------------
/src/constants/styles.ts:
--------------------------------------------------------------------------------
1 | export const BREAKPOINT = {
2 | SM: 576,
3 | MD: 768,
4 | LG: 992,
5 | XL: 1200,
6 | XXL: 1400
7 | }
8 |
9 | export const COLOR = {
10 | BLUE: "#0d6efd"
11 | }
12 |
--------------------------------------------------------------------------------
/src/guards/AuthenticatedGuard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import {
3 | Route,
4 | RouteProps,
5 | Redirect,
6 | RouteComponentProps
7 | } from "react-router-dom"
8 | import { connect } from "react-redux"
9 |
10 | interface ReduxProps {
11 | isAuthenticated: boolean
12 | }
13 | interface Props extends ReduxProps, RouteProps {
14 | component: React.ComponentType
15 | }
16 |
17 | function AuthenticatedGuard(props: Props) {
18 | const { isAuthenticated, component: Component, ...rest } = props
19 | return (
20 | {
23 | if (!isAuthenticated && !localStorage.getItem("token")) {
24 | return
25 | }
26 | return
27 | }}
28 | />
29 | )
30 | }
31 |
32 | const mapStateToProps = state => ({
33 | isAuthenticated: state.app.isAuthenticated
34 | })
35 |
36 | const mapDispatchToProps = {}
37 |
38 | export default connect(mapStateToProps, mapDispatchToProps)(AuthenticatedGuard)
39 |
--------------------------------------------------------------------------------
/src/helpers/string.ts:
--------------------------------------------------------------------------------
1 | export const handlePrice = (value: string | number) =>
2 | Number(value).toLocaleString("en") + " đ"
3 |
--------------------------------------------------------------------------------
/src/hooks/usePrevious.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from "react"
2 |
3 | export function usePrevious(value) {
4 | const ref = useRef()
5 | useEffect(() => {
6 | ref.current = value
7 | })
8 | return ref.current
9 | }
10 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import "src/assets/scss/index.scss"
4 | import App from "./App/App"
5 | import * as serviceWorker from "./serviceWorker"
6 | import { Provider } from "react-redux"
7 | import { store } from "./store/store"
8 |
9 | ReactDOM.render(
10 |
11 |
12 |
13 |
14 | ,
15 | document.getElementById("root")
16 | )
17 |
18 | // If you want your app to work offline and load faster, you can change
19 | // unregister() to register() below. Note this comes with some pitfalls.
20 | // Learn more about service workers: https://bit.ly/CRA-PWA
21 | serviceWorker.unregister()
22 |
--------------------------------------------------------------------------------
/src/layouts/MainLayout.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react"
2 | import Header from "src/components/Header/Header"
3 | import SideNav from "src/components/SideNav/SideNav"
4 |
5 | interface Props {
6 | children: ReactNode
7 | }
8 |
9 | export default function MainLayout(props: Props) {
10 | const { children } = props
11 | return (
12 |
13 |
14 |
15 |
16 | {children}
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import MainLayout from "src/layouts/MainLayout"
3 | export default function Home() {
4 | return (
5 |
6 | Home
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/Login/Login.actions.ts:
--------------------------------------------------------------------------------
1 | import * as types from "./Login.constants"
2 |
3 | export const loginRequested = () => ({
4 | type: types.LOGIN_REQUESTED
5 | })
6 |
7 | export const loginSuccess = payload => ({
8 | type: types.LOGIN_SUCCESS,
9 | payload
10 | })
11 |
12 | export const loginFailed = payload => ({
13 | type: types.LOGIN_FAILED,
14 | payload
15 | })
16 |
--------------------------------------------------------------------------------
/src/pages/Login/Login.constants.ts:
--------------------------------------------------------------------------------
1 | export const LOGIN_REQUESTED = "views/login/LOGIN_REQUESTED"
2 | export const LOGIN_SUCCESS = "views/login/LOGIN_SUCCESS"
3 | export const LOGIN_FAILED = "views/login/LOGIN_FAILED"
4 |
--------------------------------------------------------------------------------
/src/pages/Login/Login.reducer.ts:
--------------------------------------------------------------------------------
1 | import * as types from "./Login.constants"
2 | import produce from "immer"
3 |
4 | const initialState = {
5 | loading: false
6 | }
7 |
8 | export const loginReducer = (state = initialState, action) =>
9 | produce(state, draft => {
10 | switch (action.type) {
11 | case types.LOGIN_REQUESTED:
12 | draft.loading = true
13 | break
14 | case types.LOGIN_SUCCESS:
15 | draft.loading = false
16 | break
17 | case types.LOGIN_FAILED:
18 | draft.loading = false
19 | break
20 | default:
21 | return state
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/src/pages/Login/Login.styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const Title = styled.h1`
4 | margin-bottom: 1rem;
5 | `
6 |
--------------------------------------------------------------------------------
/src/pages/Login/Login.thunks.ts:
--------------------------------------------------------------------------------
1 | import { loginApi } from "src/apis/user.api"
2 | import * as actions from "./Login.actions"
3 |
4 | export const login = (payload: ReqLogin) => dispatch => {
5 | dispatch(actions.loginRequested())
6 | return loginApi(payload)
7 | .then(res => {
8 | localStorage.setItem("token", res.data.access_token)
9 | return dispatch(actions.loginSuccess(res))
10 | })
11 | .catch(err => Promise.reject(dispatch(actions.loginFailed(err))))
12 | }
13 |
--------------------------------------------------------------------------------
/src/pages/Login/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react"
2 | import { connect, ConnectedProps } from "react-redux"
3 | import { login } from "./Login.thunks"
4 | import { Title } from "./Login.styles"
5 | import { useHistory } from "react-router-dom"
6 | import { PATH } from "src/constants/paths"
7 |
8 | const mapStateToProps = state => ({
9 | loading: state.loading
10 | })
11 |
12 | const mapDispatchToProps = {
13 | login
14 | }
15 |
16 | const connector = connect(mapStateToProps, mapDispatchToProps)
17 |
18 | interface Props extends ConnectedProps {}
19 |
20 | const Login = (props: Props) => {
21 | const { login, loading } = props
22 | const [username, setUsername] = useState("")
23 | const [password, setPassword] = useState("")
24 | const [error, setError] = useState("")
25 | const history = useHistory()
26 | const handleUsername = (event: React.ChangeEvent) => {
27 | setUsername(event.target.value)
28 | }
29 |
30 | const handlePassword = (event: React.ChangeEvent) => {
31 | setPassword(event.target.value)
32 | }
33 |
34 | const submit = async (event: React.FormEvent) => {
35 | event.preventDefault()
36 | if (!loading) {
37 | const payload = { username, password }
38 | login(payload)
39 | .then(res => {
40 | history.push(PATH.HOME)
41 | })
42 | .catch(err => {
43 | setError(err.payload.message)
44 | })
45 | }
46 | }
47 |
48 | return (
49 |
77 | )
78 | }
79 |
80 | export default connector(Login)
81 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductItem/ProductItem.actions.ts:
--------------------------------------------------------------------------------
1 | import * as types from "./ProductItem.constants"
2 |
3 | export const getProductItemRequested = () => ({
4 | type: types.GET_PRODUCT_ITEM_REQUESTED
5 | })
6 |
7 | export const getProductItemSuccess = (payload: ResGetProductItemApi) => ({
8 | type: types.GET_PRODUCT_ITEM_SUCCESS,
9 | payload
10 | })
11 |
12 | export const getProductItemFailed = payload => ({
13 | type: types.GET_PRODUCT_ITEM_FAILED,
14 | payload
15 | })
16 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductItem/ProductItem.constants.ts:
--------------------------------------------------------------------------------
1 | export const GET_PRODUCT_ITEM_REQUESTED =
2 | "views/Product/ProductItem/GET_PRODUCT_ITEM_REQUESTED"
3 | export const GET_PRODUCT_ITEM_SUCCESS =
4 | "views/Product/ProductItem/GET_PRODUCT_ITEM_SUCCESS"
5 | export const GET_PRODUCT_ITEM_FAILED =
6 | "views/Product/ProductItem/GET_PRODUCT_ITEM_FAILED"
7 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductItem/ProductItem.reducer.ts:
--------------------------------------------------------------------------------
1 | import * as types from "./ProductItem.constants"
2 | import produce from "immer"
3 |
4 | const initialState = {
5 | loading: false,
6 | productItem: null as Product | null
7 | }
8 |
9 | export const productItemReducer = (state = initialState, action) =>
10 | produce(state, draft => {
11 | switch (action.type) {
12 | case types.GET_PRODUCT_ITEM_REQUESTED:
13 | draft.loading = true
14 | draft.productItem = null
15 | break
16 | case types.GET_PRODUCT_ITEM_SUCCESS:
17 | draft.loading = false
18 | draft.productItem = action.payload.data.product
19 | break
20 | case types.GET_PRODUCT_ITEM_FAILED:
21 | draft.loading = false
22 | break
23 | default:
24 | return state
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductItem/ProductItem.thunks.ts:
--------------------------------------------------------------------------------
1 | import * as actions from "./ProductItem.actions"
2 | import { getProductItemApi } from "src/apis/product.api"
3 |
4 | export const getProductItem = (id: string) => dispatch => {
5 | dispatch(actions.getProductItemRequested())
6 | return getProductItemApi(id)
7 | .then(res => dispatch(actions.getProductItemSuccess(res)))
8 | .catch(err => Promise.reject(dispatch(actions.getProductItemFailed(err))))
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductItem/ProductItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react"
2 | import MainLayout from "src/layouts/MainLayout"
3 | import { connect, ConnectedProps } from "react-redux"
4 | import { getProductItem } from "./ProductItem.thunks"
5 | import { useParams } from "react-router-dom"
6 | import { handlePrice } from "src/helpers/string"
7 |
8 | const mapStateToProps = (state: AppState) => ({
9 | productItem: state.productItem.productItem
10 | })
11 |
12 | const mapDispatchToProps = {
13 | getProductItem
14 | }
15 |
16 | const connector = connect(mapStateToProps, mapDispatchToProps)
17 |
18 | interface Props extends ConnectedProps {}
19 |
20 | function ProductItem(props: Props) {
21 | const { productItem, getProductItem } = props
22 | const params: { idProduct: string } = useParams()
23 | useEffect(() => {
24 | const { idProduct } = params
25 | getProductItem(idProduct)
26 | }, [params, getProductItem])
27 | return (
28 |
29 | {productItem && (
30 | <>
31 | {productItem.name}
32 | Price: {handlePrice(productItem.price)}
33 | Quantity: {productItem.quantity}
34 | >
35 | )}
36 |
37 | )
38 | }
39 |
40 | export default connector(ProductItem)
41 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductList/ProductList.actions.ts:
--------------------------------------------------------------------------------
1 | import * as types from "./ProductList.constants"
2 |
3 | export const getProductListRequested = () => ({
4 | type: types.GET_PRODUCT_LIST_REQUESTED
5 | })
6 |
7 | export const getProductListSuccess = payload => {
8 | return {
9 | type: types.GET_PRODUCT_LIST_SUCCESS,
10 | payload
11 | }
12 | }
13 |
14 | export const getProductListFailed = payload => ({
15 | type: types.GET_PRODUCT_LIST_FAILED,
16 | payload
17 | })
18 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductList/ProductList.constants.ts:
--------------------------------------------------------------------------------
1 | export const GET_PRODUCT_LIST_REQUESTED =
2 | "views/ProductList/GET_PRODUCT_LIST_REQUESTED"
3 | export const GET_PRODUCT_LIST_SUCCESS =
4 | "views/ProductList/GET_PRODUCT_LIST_SUCCESS"
5 | export const GET_PRODUCT_LIST_FAILED =
6 | "views/ProductList/GET_PRODUCT_LIST_FAILED"
7 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductList/ProductList.reducer.ts:
--------------------------------------------------------------------------------
1 | import * as types from "./ProductList.constants"
2 | import produce from "immer"
3 |
4 | const initialState = {
5 | loading: false,
6 | productList: [] as Product[]
7 | }
8 |
9 | export const ProductListReducer = (state = initialState, action) =>
10 | produce(state, draft => {
11 | switch (action.type) {
12 | case types.GET_PRODUCT_LIST_REQUESTED:
13 | draft.loading = true
14 | break
15 | case types.GET_PRODUCT_LIST_SUCCESS:
16 | draft.loading = false
17 | draft.productList = action.payload.data.products
18 | break
19 | case types.GET_PRODUCT_LIST_FAILED:
20 | draft.loading = false
21 | break
22 | default:
23 | return state
24 | }
25 | })
26 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductList/ProductList.styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const TableContainer = styled.div`
4 | overflow: auto;
5 | `
6 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductList/ProductList.thunks.ts:
--------------------------------------------------------------------------------
1 | import * as actions from "./ProductList.actions"
2 | import { getProductListApi } from "src/apis/product.api"
3 |
4 | export const getProductList = () => dispatch => {
5 | dispatch(actions.getProductListRequested())
6 | return getProductListApi()
7 | .then(res => dispatch(actions.getProductListSuccess(res)))
8 | .catch(err => Promise.reject(dispatch(actions.getProductListFailed(err))))
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/Product/ProductList/ProductList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react"
2 | import { connect, ConnectedProps } from "react-redux"
3 | import MainLayout from "src/layouts/MainLayout"
4 | import { getProductList } from "./ProductList.thunks"
5 | import { Link } from "react-router-dom"
6 | import { PATH } from "src/constants/paths"
7 | import { handlePrice } from "src/helpers/string"
8 | import { TableContainer } from "./ProductList.styles"
9 |
10 | const mapStateToProps = (state: AppState) => ({
11 | productList: state.productList.productList
12 | })
13 |
14 | const mapDispatchToProps = {
15 | getProductList
16 | }
17 |
18 | const connector = connect(mapStateToProps, mapDispatchToProps)
19 |
20 | interface Props extends ConnectedProps {}
21 | const ProductList = (props: Props) => {
22 | const { getProductList, productList } = props
23 |
24 | useEffect(() => {
25 | getProductList()
26 | }, [getProductList])
27 |
28 | return (
29 |
30 | Product List
31 |
32 |
33 |
34 |
35 | # |
36 | Name |
37 | Quantity |
38 | Price |
39 | Actions |
40 |
41 |
42 |
43 | {productList.map((product, index) => (
44 |
45 | {index + 1} |
46 | {product.name} |
47 | {product.quantity} |
48 | {handlePrice(product.price)} |
49 |
50 |
54 | Detail
55 |
56 | |
57 |
58 | ))}
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
66 | export default connector(ProductList)
67 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reducer/reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux"
2 | import { AppReducer } from "src/App/App.reducer"
3 | import { loginReducer } from "src/pages/Login/Login.reducer"
4 | import { ProductListReducer } from "src/pages/Product/ProductList/ProductList.reducer"
5 | import { productItemReducer } from "src/pages/Product/ProductItem/ProductItem.reducer"
6 |
7 | const rootReducer = combineReducers({
8 | app: AppReducer,
9 | login: loginReducer,
10 | productList: ProductListReducer,
11 | productItem: productItemReducer
12 | })
13 |
14 | export default rootReducer
15 |
--------------------------------------------------------------------------------
/src/routes/HomeRoutes.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy, Suspense } from "react"
2 | import { Switch } from "react-router-dom"
3 | import AuthenticatedGuard from "src/guards/AuthenticatedGuard"
4 | import { PATH } from "src/constants/paths"
5 | import Loading from "src/components/Loading/Loading"
6 | const Home = lazy(() => import("src/pages/Home/Home"))
7 |
8 | export default function HomeRoutes() {
9 | return (
10 |
11 | (
15 | }>
16 |
17 |
18 | )}
19 | />
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/src/routes/LoginRoutes.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy, Suspense } from "react"
2 | import { Route, Switch } from "react-router-dom"
3 | import { PATH } from "src/constants/paths"
4 | import Loading from "src/components/Loading/Loading"
5 | const Login = lazy(() => import("src/pages/Login/Login"))
6 |
7 | export default function LoginRoutes() {
8 | return (
9 |
10 | (
13 | }>
14 |
15 |
16 | )}
17 | />
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/routes/ProductRoutes.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy, Suspense } from "react"
2 | import { Switch } from "react-router-dom"
3 | import AuthenticatedGuard from "../guards/AuthenticatedGuard"
4 | import { PATH } from "src/constants/paths"
5 | import Loading from "src/components/Loading/Loading"
6 | const ProductList = lazy(
7 | () => import("src/pages/Product/ProductList/ProductList")
8 | )
9 | const ProductItem = lazy(
10 | () => import("src/pages/Product/ProductItem/ProductItem")
11 | )
12 | export default function ProductRoutes() {
13 | return (
14 |
15 | (
19 | }>
20 |
21 |
22 | )}
23 | />
24 | (
28 | }>
29 |
30 |
31 | )}
32 | />
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/routes/routes.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { BrowserRouter } from "react-router-dom"
3 | import ProductRoutes from "./ProductRoutes"
4 | import LoginRoutes from "./LoginRoutes"
5 | import HomeRoutes from "./HomeRoutes"
6 |
7 | export default function Routes() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === "localhost" ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === "[::1]" ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | )
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void
26 | }
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
32 | if (publicUrl.origin !== window.location.origin) {
33 | // Our service worker won't work if PUBLIC_URL is on a different origin
34 | // from what our page is served on. This might happen if a CDN is used to
35 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
36 | return
37 | }
38 |
39 | window.addEventListener("load", () => {
40 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
41 |
42 | if (isLocalhost) {
43 | // This is running on localhost. Let's check if a service worker still exists or not.
44 | checkValidServiceWorker(swUrl, config)
45 |
46 | // Add some additional logging to localhost, pointing developers to the
47 | // service worker/PWA documentation.
48 | navigator.serviceWorker.ready.then(() => {
49 | console.log(
50 | "This web app is being served cache-first by a service " +
51 | "worker. To learn more, visit https://bit.ly/CRA-PWA"
52 | )
53 | })
54 | } else {
55 | // Is not localhost. Just register service worker
56 | registerValidSW(swUrl, config)
57 | }
58 | })
59 | }
60 | }
61 |
62 | function registerValidSW(swUrl: string, config?: Config) {
63 | navigator.serviceWorker
64 | .register(swUrl)
65 | .then(registration => {
66 | registration.onupdatefound = () => {
67 | const installingWorker = registration.installing
68 | if (installingWorker == null) {
69 | return
70 | }
71 | installingWorker.onstatechange = () => {
72 | if (installingWorker.state === "installed") {
73 | if (navigator.serviceWorker.controller) {
74 | // At this point, the updated precached content has been fetched,
75 | // but the previous service worker will still serve the older
76 | // content until all client tabs are closed.
77 | console.log(
78 | "New content is available and will be used when all " +
79 | "tabs for this page are closed. See https://bit.ly/CRA-PWA."
80 | )
81 |
82 | // Execute callback
83 | if (config && config.onUpdate) {
84 | config.onUpdate(registration)
85 | }
86 | } else {
87 | // At this point, everything has been precached.
88 | // It's the perfect time to display a
89 | // "Content is cached for offline use." message.
90 | console.log("Content is cached for offline use.")
91 |
92 | // Execute callback
93 | if (config && config.onSuccess) {
94 | config.onSuccess(registration)
95 | }
96 | }
97 | }
98 | }
99 | }
100 | })
101 | .catch(error => {
102 | console.error("Error during service worker registration:", error)
103 | })
104 | }
105 |
106 | function checkValidServiceWorker(swUrl: string, config?: Config) {
107 | // Check if the service worker can be found. If it can't reload the page.
108 | fetch(swUrl, {
109 | headers: { "Service-Worker": "script" }
110 | })
111 | .then(response => {
112 | // Ensure service worker exists, and that we really are getting a JS file.
113 | const contentType = response.headers.get("content-type")
114 | if (
115 | response.status === 404 ||
116 | (contentType != null && contentType.indexOf("javascript") === -1)
117 | ) {
118 | // No service worker found. Probably a different app. Reload the page.
119 | navigator.serviceWorker.ready.then(registration => {
120 | registration.unregister().then(() => {
121 | window.location.reload()
122 | })
123 | })
124 | } else {
125 | // Service worker found. Proceed as normal.
126 | registerValidSW(swUrl, config)
127 | }
128 | })
129 | .catch(() => {
130 | console.log(
131 | "No internet connection found. App is running in offline mode."
132 | )
133 | })
134 | }
135 |
136 | export function unregister() {
137 | if ("serviceWorker" in navigator) {
138 | navigator.serviceWorker.ready
139 | .then(registration => {
140 | registration.unregister()
141 | })
142 | .catch(error => {
143 | console.error(error.message)
144 | })
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "srctesting-library/jest-dom/extend-expect"
6 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from "redux"
2 | import thunk from "redux-thunk"
3 | import rootReducer from "src/reducer/reducer"
4 | const composeEnhancers =
5 | typeof window === "object" &&
6 | process.env.NODE_ENV === "development" &&
7 | (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
8 | ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
9 | : compose
10 | const enhancer = composeEnhancers(applyMiddleware(thunk))
11 | export const store = createStore(rootReducer, enhancer)
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx",
22 | "noImplicitAny": false,
23 | "baseUrl": "."
24 | },
25 | "include": [
26 | "src"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------