├── .eslintrc ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── README.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico ├── index.html ├── manifest.json └── static │ ├── icons │ ├── ic_flag_de.svg │ ├── ic_flag_en.svg │ ├── ic_flag_fr.svg │ ├── ic_notification_chat.svg │ ├── ic_notification_mail.svg │ ├── ic_notification_package.svg │ ├── ic_notification_shipping.svg │ └── shape-avatar.svg │ ├── illustrations │ ├── BTC_Logo.svg │ ├── illustration_404.svg │ └── illustration_BTC.png │ ├── log.svg │ └── logo.svg ├── src ├── App.js ├── assets │ └── avatar_default.jpg ├── components │ ├── @material-extend │ │ ├── MHidden.js │ │ └── index.js │ ├── ColorManyPicker.js │ ├── ColorPreview.js │ ├── Label.js │ ├── Logo.js │ ├── MenuPopover.js │ ├── NavSection.js │ ├── Page.js │ ├── ScrollToTop.js │ ├── Scrollbar.js │ ├── SearchNotFound.js │ ├── SvgIconStyle.js │ ├── _dashboard │ │ ├── SummaryBox.jsx │ │ ├── app │ │ │ ├── AppCurrentSubject.js │ │ │ ├── AppTasks.js │ │ │ ├── AppTrafficBySite.js │ │ │ ├── LastOrders.js │ │ │ ├── PerformanceOverview.js │ │ │ ├── PositionsRepartition.js │ │ │ ├── TopPerformers.js │ │ │ └── index.js │ │ └── order │ │ │ ├── OrderListHead.js │ │ │ ├── OrderListToolbar.js │ │ │ ├── OrderMoreMenu.js │ │ │ └── index.js │ ├── animate │ │ ├── MotionContainer.js │ │ ├── index.js │ │ └── variants │ │ │ ├── Wrap.js │ │ │ ├── bounce │ │ │ ├── BounceIn.js │ │ │ ├── BounceOut.js │ │ │ └── index.js │ │ │ └── index.js │ └── charts │ │ ├── BaseOptionChart.js │ │ └── index.js ├── config │ └── api-keys.example.js ├── index.js ├── layouts │ ├── LogoOnlyLayout.js │ └── dashboard │ │ ├── AccountPopover.js │ │ ├── BTCprice.jsx │ │ ├── DashboardNavbar.js │ │ ├── DashboardSidebar.js │ │ ├── SidebarConfig.js │ │ ├── Summary.jsx │ │ └── index.js ├── pages │ ├── DashboardApp.js │ ├── OrderHistory.js │ └── Page404.js ├── recoil │ └── atoms │ │ ├── accountAtom.js │ │ ├── incomesAtom.js │ │ ├── index.js │ │ ├── tradesAtom.js │ │ └── updateTimeAtom.js ├── reportWebVitals.js ├── routes.js ├── serviceWorker.js ├── services │ ├── accountServices.js │ ├── incomeServices.js │ ├── tradesServices.js │ └── utils.js ├── setupTests.js ├── theme │ ├── breakpoints.js │ ├── globalStyles.js │ ├── index.js │ ├── overrides │ │ ├── Autocomplete.js │ │ ├── Backdrop.js │ │ ├── Button.js │ │ ├── Card.js │ │ ├── IconButton.js │ │ ├── Input.js │ │ ├── Lists.js │ │ ├── Paper.js │ │ ├── Tooltip.js │ │ ├── Typography.js │ │ └── index.js │ ├── palette.js │ ├── shadows.js │ ├── shape.js │ └── typography.js └── utils │ ├── formatNumber.js │ ├── formatTime.js │ └── functions.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "airbnb", 9 | "prettier", 10 | "plugin:jsx-a11y/recommended", 11 | "plugin:react-hooks/recommended" 12 | ], 13 | "plugins": ["prettier", "react", "react-hooks"], 14 | "parser": "@babel/eslint-parser", 15 | "parserOptions": { 16 | "ecmaVersion": 8, 17 | "requireConfigFile": false, 18 | "ecmaFeatures": { 19 | "experimentalObjectRestSpread": true, 20 | "impliedStrict": true 21 | } 22 | }, 23 | "rules": { 24 | "import": 0, 25 | "max-len": 0, 26 | "no-alert": 0, 27 | "no-shadow": 0, 28 | "no-console": 0, 29 | "comma-dangle": 0, 30 | "import/no-cycle": 0, 31 | "react/prop-types": 0, 32 | "no-return-assign": 0, 33 | "consistent-return": 1, 34 | "no-param-reassign": 0, 35 | "react/display-name": 0, 36 | "no-use-before-define": 0, 37 | "no-underscore-dangle": 0, 38 | "react/button-has-type": 1, 39 | "react/no-children-prop": 0, 40 | "react/forbid-prop-types": 0, 41 | "jsx-a11y/anchor-is-valid": 0, 42 | "react/react-in-jsx-scope": 0, 43 | "react/no-array-index-key": 0, 44 | "react/no-unused-prop-types": 1, 45 | "react/require-default-props": 0, 46 | "react/no-unescaped-entities": 0, 47 | "import/prefer-default-export": 0, 48 | "react/jsx-props-no-spreading": 0, 49 | "react/jsx-filename-extension": 0, 50 | "react/destructuring-assignment": 0, 51 | "import/no-extraneous-dependencies": 0, 52 | "react/jsx-key": 1, 53 | "react-hooks/rules-of-hooks": 2, 54 | "no-unused-vars": [ 55 | 1, 56 | { 57 | "ignoreRestSiblings": false 58 | } 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.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 17 | /.env.local 18 | /.env.development.local 19 | /.env.test.local 20 | /.env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | .eslintcache 27 | 28 | # config 29 | /src/config/api-keys.js 30 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.2.0 2 | 3 | ###### Sep 18, 2021 4 | 5 | - Support MIU v5.0.0 official release 6 | - Upgrade some dependencies to the latest versions 7 | 8 | ###### Aug 18, 2021 9 | 10 | - Update `src/theme/typography.js` 11 | - Upgrade some dependencies to the latest versions 12 | 13 | --- 14 | 15 | ### v1.1.0 16 | 17 | ###### Jul 23, 2021 18 | 19 | - Support MUI v5.0.0-beta.1 20 | - Upgrade some dependencies to the latest versions 21 | 22 | --- 23 | 24 | ### v1.0.0 25 | 26 | ###### Jun 28, 2021 27 | 28 | Initial release. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trading Dashboard 2 | 3 | Screenshot 2021-11-01 at 18 28 41 4 | 5 | ## Getting started 6 | 7 | - Clone this repository. 8 | - Create a fresh new API on Binance, with only read rights. 9 | - In the config folder, edit the `api-keys.example.js` into `api-keys.js` and put your new api key/secret. 10 | - From the root, run `yarn install` or `npm install` (one single time) 11 | - From the root, run `yarn start` or `npm start` 12 | 13 | Currently only Binance and Futures are supported. 14 | 15 | ## API weight usage 16 | 17 | - Account: Fetching account information cost `5` weight 18 | - Trades: The initial fetch cost `7 days * 8 chunks * 5 weight = 280 weight`. After the initial fetch, it will cost max `8 chunks * 5 weight = 40 weight / minute` 19 | - Income: The initial fetch cost `7 days * 3 chunks * 30 weight = 630 weight`. After the initial fetch, it will cost max `3 chunks * 30 weight = 90 weight / minute` 20 | 21 | - Total weight consumption: First load of the page `915 weight`, after that every minute your data is refreshed automatically and consumes `135 weight / minute`. 22 | - Reminder: Binance API allows you to consume up to `1200 weight / minute / IP`. 23 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "baseUrl": "." 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sh-stark/trading-dashboard", 3 | "version": "1.0.1", 4 | "private": false, 5 | "author": "SH Stark", 6 | "scripts": { 7 | "build": "react-scripts build", 8 | "eject": "react-scripts eject", 9 | "lint": "eslint --ext .js,.jsx ./src", 10 | "lint:fix": "eslint --fix --ext .js,.jsx ./src", 11 | "start": "react-scripts start", 12 | "test": "react-scripts test" 13 | }, 14 | "babel": { 15 | "presets": [ 16 | "@babel/preset-react" 17 | ] 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "dependencies": { 38 | "@emotion/react": "11.5.0", 39 | "@emotion/styled": "11.3.0", 40 | "@iconify/icons-eva": "1.1.0", 41 | "@iconify/react": "3.0.1", 42 | "@mui/lab": "5.0.0-alpha.53", 43 | "@mui/material": "5.0.6", 44 | "@mui/utils": "5.0.1", 45 | "@testing-library/jest-dom": "5.15.0", 46 | "apexcharts": "3.29.0", 47 | "axios": "0.24.0", 48 | "crypto": "1.0.1", 49 | "date-fns": "2.25.0", 50 | "faker": "5.5.3", 51 | "formik": "2.2.9", 52 | "framer-motion": "4.1.17", 53 | "history": "5.1.0", 54 | "lodash": "4.17.21", 55 | "moment": "2.29.1", 56 | "numeral": "2.0.6", 57 | "prop-types": "15.7.2", 58 | "react": "17.0.2", 59 | "react-apexcharts": "1.3.9", 60 | "react-dom": "17.0.2", 61 | "react-helmet-async": "1.1.2", 62 | "react-router-dom": "6.0.1", 63 | "react-scripts": "4.0.3", 64 | "react-use-websocket": "2.9.1", 65 | "recoil": "0.5.2", 66 | "simplebar": "5.3.6", 67 | "simplebar-react": "2.3.6", 68 | "web-vitals": "2.1.2" 69 | }, 70 | "devDependencies": { 71 | "@babel/core": "^7.16.0", 72 | "@babel/eslint-parser": "^7.16.0", 73 | "eslint": "^7.32.0", 74 | "eslint-config-airbnb": "18.2.1", 75 | "eslint-config-prettier": "8.3.0", 76 | "eslint-config-react-app": "6.0.0", 77 | "eslint-plugin-flowtype": "5.2.0", 78 | "eslint-plugin-import": "2.25.2", 79 | "eslint-plugin-jsx-a11y": "6.4.1", 80 | "eslint-plugin-prettier": "4.0.0", 81 | "eslint-plugin-react": "7.26.1", 82 | "eslint-plugin-react-hooks": "4.2.0", 83 | "prettier": "2.4.1" 84 | }, 85 | "licence": "MIT" 86 | } 87 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SH-Stark/trading-dashboard/4a272ec5e788902a61ae327107a4204f8605039e/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SH-Stark/trading-dashboard/4a272ec5e788902a61ae327107a4204f8605039e/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SH-Stark/trading-dashboard/4a272ec5e788902a61ae327107a4204f8605039e/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SH-Stark/trading-dashboard/4a272ec5e788902a61ae327107a4204f8605039e/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SH-Stark/trading-dashboard/4a272ec5e788902a61ae327107a4204f8605039e/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SH-Stark/trading-dashboard/4a272ec5e788902a61ae327107a4204f8605039e/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | Trading Dashboard 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Dashboard App", 3 | "name": "Dashboard Binance Futures", 4 | "icons": [ 5 | { 6 | "src": "favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /public/static/icons/ic_flag_de.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/icons/ic_flag_en.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/icons/ic_flag_fr.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/icons/ic_notification_chat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/icons/ic_notification_mail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/icons/ic_notification_package.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/icons/ic_notification_shipping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/icons/shape-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/illustrations/BTC_Logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/illustrations/illustration_404.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 14 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/static/illustrations/illustration_BTC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SH-Stark/trading-dashboard/4a272ec5e788902a61ae327107a4204f8605039e/public/static/illustrations/illustration_BTC.png -------------------------------------------------------------------------------- /public/static/log.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useSetRecoilState } from 'recoil'; 3 | import { format } from 'date-fns'; 4 | 5 | // routes 6 | import Router from './routes'; 7 | // theme 8 | import ThemeConfig from './theme'; 9 | import GlobalStyles from './theme/globalStyles'; 10 | // components 11 | import ScrollToTop from './components/ScrollToTop'; 12 | import { BaseOptionChartStyle } from './components/charts/BaseOptionChart'; 13 | // recoil and services 14 | import { accountAtom, incomesAtom, tradesAtom, updateTimeAtom } from './recoil/atoms'; 15 | import { getTradesOfTheWeek, getUserTradesOfTheDay } from './services/tradesServices'; 16 | import { getIncomesOfTheWeek, getUserIncomesOfTheDay } from './services/incomeServices'; 17 | import { getUserAccount } from './services/accountServices'; 18 | 19 | // ---------------------------------------------------------------------- 20 | 21 | const INTERVAL_FETCH = 60000; // 1 minute 22 | 23 | const App = () => { 24 | const [intervalId, setIntervalId] = useState(); 25 | 26 | const setTradesRecoil = useSetRecoilState(tradesAtom); 27 | const setIncomesRecoil = useSetRecoilState(incomesAtom); 28 | const setAccountRecoil = useSetRecoilState(accountAtom); 29 | const setUpdateTimeRecoil = useSetRecoilState(updateTimeAtom); 30 | 31 | const getDailyData = () => { 32 | getUserAccount().then((account) => setAccountRecoil(account)); 33 | getUserTradesOfTheDay().then((trades) => 34 | setTradesRecoil((oldTrades) => ({ 35 | ...oldTrades, 36 | [format(new Date(), 'MM/dd/yyyy')]: trades 37 | })) 38 | ); 39 | getUserIncomesOfTheDay().then((incomes) => 40 | setIncomesRecoil((oldIncomes) => ({ 41 | ...oldIncomes, 42 | [format(new Date(), 'MM/dd/yyyy')]: incomes 43 | })) 44 | ); 45 | 46 | setUpdateTimeRecoil(new Date()); 47 | console.log('Updating your daily data... time: ', new Date()); 48 | }; 49 | 50 | const periodicallyFetchDailyData = () => { 51 | const id = setInterval(getDailyData, INTERVAL_FETCH); 52 | setIntervalId(id); 53 | }; 54 | 55 | useEffect(() => { 56 | console.log('Fetching your weekly data'); 57 | 58 | getTradesOfTheWeek().then((trades) => setTradesRecoil(trades)); 59 | getIncomesOfTheWeek().then((incomes) => setIncomesRecoil(incomes)); 60 | getUserAccount().then((account) => setAccountRecoil(account)); 61 | setUpdateTimeRecoil(new Date()); 62 | 63 | periodicallyFetchDailyData(); 64 | return () => clearInterval(intervalId); 65 | }, []); 66 | 67 | return ( 68 | 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | 77 | export default App; 78 | -------------------------------------------------------------------------------- /src/assets/avatar_default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SH-Stark/trading-dashboard/4a272ec5e788902a61ae327107a4204f8605039e/src/assets/avatar_default.jpg -------------------------------------------------------------------------------- /src/components/@material-extend/MHidden.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | // material 3 | import { useMediaQuery } from '@mui/material'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | MHidden.propTypes = { 8 | children: PropTypes.node, 9 | width: PropTypes.oneOf([ 10 | 'xsDown', 11 | 'smDown', 12 | 'mdDown', 13 | 'lgDown', 14 | 'xlDown', 15 | 'xsUp', 16 | 'smUp', 17 | 'mdUp', 18 | 'lgUp', 19 | 'xlUp' 20 | ]).isRequired 21 | }; 22 | 23 | export default function MHidden({ width, children }) { 24 | const breakpoint = width.substring(0, 2); 25 | 26 | const hiddenUp = useMediaQuery((theme) => theme.breakpoints.up(breakpoint)); 27 | const hiddenDown = useMediaQuery((theme) => theme.breakpoints.down(breakpoint)); 28 | 29 | if (width.includes('Down')) { 30 | return hiddenDown ? null : children; 31 | } 32 | 33 | if (width.includes('Up')) { 34 | return hiddenUp ? null : children; 35 | } 36 | 37 | return null; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/@material-extend/index.js: -------------------------------------------------------------------------------- 1 | export { default as MHidden } from './MHidden'; 2 | -------------------------------------------------------------------------------- /src/components/ColorManyPicker.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Icon } from '@iconify/react'; 3 | import checkmarkFill from '@iconify/icons-eva/checkmark-fill'; 4 | // material 5 | import { Box, Checkbox } from '@mui/material'; 6 | 7 | // ---------------------------------------------------------------------- 8 | 9 | IconColor.propTypes = { 10 | sx: PropTypes.object 11 | }; 12 | 13 | function IconColor({ sx, ...other }) { 14 | return ( 15 | 26 | theme.transitions.create('all', { 27 | duration: theme.transitions.duration.shortest 28 | }), 29 | ...sx 30 | }} 31 | {...other} 32 | > 33 | 34 | 35 | ); 36 | } 37 | 38 | ColorManyPicker.propTypes = { 39 | colors: PropTypes.array.isRequired, 40 | onChecked: PropTypes.func, 41 | sx: PropTypes.object 42 | }; 43 | 44 | export default function ColorManyPicker({ colors, onChecked, sx, ...other }) { 45 | return ( 46 | 47 | {colors.map((color) => { 48 | const isWhite = color === '#FFFFFF' || color === 'white'; 49 | 50 | return ( 51 | `solid 1px ${theme.palette.divider}` 62 | }) 63 | }} 64 | /> 65 | } 66 | checkedIcon={ 67 | `solid 1px ${theme.palette.divider}`, 82 | boxShadow: (theme) => `4px 4px 8px 0 ${theme.palette.grey[500_24]}`, 83 | '& svg': { width: 12, height: 12, color: 'common.black' } 84 | }) 85 | }} 86 | /> 87 | } 88 | sx={{ 89 | color, 90 | '&:hover': { opacity: 0.72 } 91 | }} 92 | {...other} 93 | /> 94 | ); 95 | })} 96 | 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /src/components/ColorPreview.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | // material 3 | import { alpha, styled } from '@mui/material/styles'; 4 | import { Box, Typography } from '@mui/material'; 5 | 6 | // ---------------------------------------------------------------------- 7 | 8 | const RootStyle = styled(Box)({ 9 | display: 'flex', 10 | alignItems: 'center', 11 | justifyContent: 'flex-end' 12 | }); 13 | 14 | const IconStyle = styled('div')(({ theme }) => ({ 15 | marginLeft: -4, 16 | borderRadius: '50%', 17 | width: theme.spacing(2), 18 | height: theme.spacing(2), 19 | border: `solid 2px ${theme.palette.background.paper}`, 20 | boxShadow: `inset -1px 1px 2px ${alpha(theme.palette.common.black, 0.24)}` 21 | })); 22 | 23 | // ---------------------------------------------------------------------- 24 | 25 | ColorPreview.propTypes = { 26 | colors: PropTypes.array.isRequired, 27 | limit: PropTypes.number 28 | }; 29 | 30 | export default function ColorPreview({ colors, limit = 3, ...other }) { 31 | const showColor = colors.slice(0, limit); 32 | const moreColor = colors.length - limit; 33 | 34 | return ( 35 | 36 | {showColor.map((color, index) => ( 37 | 38 | ))} 39 | 40 | {colors.length > limit && {`+${moreColor}`}} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Label.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | // material 3 | import { alpha, styled } from '@mui/material/styles'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | const RootStyle = styled('span')(({ theme, ownerState }) => { 8 | const { color, variant } = ownerState; 9 | 10 | const styleFilled = (color) => ({ 11 | color: theme.palette[color].contrastText, 12 | backgroundColor: theme.palette[color].main 13 | }); 14 | 15 | const styleOutlined = (color) => ({ 16 | color: theme.palette[color].main, 17 | backgroundColor: 'transparent', 18 | border: `1px solid ${theme.palette[color].main}` 19 | }); 20 | 21 | const styleGhost = (color) => ({ 22 | color: theme.palette[color].dark, 23 | backgroundColor: alpha(theme.palette[color].main, 0.16) 24 | }); 25 | 26 | return { 27 | height: 22, 28 | minWidth: 22, 29 | lineHeight: 0, 30 | borderRadius: 8, 31 | cursor: 'default', 32 | alignItems: 'center', 33 | whiteSpace: 'nowrap', 34 | display: 'inline-flex', 35 | justifyContent: 'center', 36 | padding: theme.spacing(0, 1), 37 | color: theme.palette.grey[800], 38 | fontSize: theme.typography.pxToRem(12), 39 | fontFamily: theme.typography.fontFamily, 40 | backgroundColor: theme.palette.grey[300], 41 | fontWeight: theme.typography.fontWeightBold, 42 | 43 | ...(color !== 'default' 44 | ? { 45 | ...(variant === 'filled' && { ...styleFilled(color) }), 46 | ...(variant === 'outlined' && { ...styleOutlined(color) }), 47 | ...(variant === 'ghost' && { ...styleGhost(color) }) 48 | } 49 | : { 50 | ...(variant === 'outlined' && { 51 | backgroundColor: 'transparent', 52 | color: theme.palette.text.primary, 53 | border: `1px solid ${theme.palette.grey[500_32]}` 54 | }), 55 | ...(variant === 'ghost' && { 56 | color: theme.palette.text.secondary, 57 | backgroundColor: theme.palette.grey[500_16] 58 | }) 59 | }) 60 | }; 61 | }); 62 | 63 | // ---------------------------------------------------------------------- 64 | 65 | export default function Label({ color = 'default', variant = 'ghost', children, ...other }) { 66 | return ( 67 | 68 | {children} 69 | 70 | ); 71 | } 72 | 73 | Label.propTypes = { 74 | children: PropTypes.node, 75 | color: PropTypes.oneOf([ 76 | 'default', 77 | 'primary', 78 | 'secondary', 79 | 'info', 80 | 'success', 81 | 'warning', 82 | 'error' 83 | ]), 84 | variant: PropTypes.oneOf(['filled', 'outlined', 'ghost']) 85 | }; 86 | -------------------------------------------------------------------------------- /src/components/Logo.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | // material 3 | import { Box } from '@mui/material'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | Logo.propTypes = { 8 | sx: PropTypes.object 9 | }; 10 | 11 | export default function Logo({ sx }) { 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/MenuPopover.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | // material 3 | import { Popover } from '@mui/material'; 4 | import { alpha, styled } from '@mui/material/styles'; 5 | 6 | // ---------------------------------------------------------------------- 7 | 8 | const ArrowStyle = styled('span')(({ theme }) => ({ 9 | [theme.breakpoints.up('sm')]: { 10 | top: -7, 11 | zIndex: 1, 12 | width: 12, 13 | right: 20, 14 | height: 12, 15 | content: "''", 16 | position: 'absolute', 17 | borderRadius: '0 0 4px 0', 18 | transform: 'rotate(-135deg)', 19 | background: theme.palette.background.paper, 20 | borderRight: `solid 1px ${alpha(theme.palette.grey[500], 0.12)}`, 21 | borderBottom: `solid 1px ${alpha(theme.palette.grey[500], 0.12)}` 22 | } 23 | })); 24 | 25 | // ---------------------------------------------------------------------- 26 | 27 | MenuPopover.propTypes = { 28 | children: PropTypes.node.isRequired, 29 | sx: PropTypes.object 30 | }; 31 | 32 | export default function MenuPopover({ children, sx, ...other }) { 33 | return ( 34 | theme.customShadows.z20, 43 | border: (theme) => `solid 1px ${theme.palette.grey[500_8]}`, 44 | width: 200, 45 | ...sx 46 | } 47 | }} 48 | {...other} 49 | > 50 | 51 | 52 | {children} 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/NavSection.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Icon } from '@iconify/react'; 4 | import { NavLink as RouterLink, matchPath, useLocation } from 'react-router-dom'; 5 | import arrowIosForwardFill from '@iconify/icons-eva/arrow-ios-forward-fill'; 6 | import arrowIosDownwardFill from '@iconify/icons-eva/arrow-ios-downward-fill'; 7 | // material 8 | import { alpha, useTheme, styled } from '@mui/material/styles'; 9 | import { Box, List, Collapse, ListItemText, ListItemIcon, ListItemButton } from '@mui/material'; 10 | 11 | // ---------------------------------------------------------------------- 12 | 13 | const ListItemStyle = styled((props) => )( 14 | ({ theme }) => ({ 15 | ...theme.typography.body2, 16 | height: 48, 17 | position: 'relative', 18 | textTransform: 'capitalize', 19 | paddingLeft: theme.spacing(5), 20 | paddingRight: theme.spacing(2.5), 21 | color: theme.palette.text.secondary, 22 | '&:before': { 23 | top: 0, 24 | right: 0, 25 | width: 3, 26 | bottom: 0, 27 | content: "''", 28 | display: 'none', 29 | position: 'absolute', 30 | borderTopLeftRadius: 4, 31 | borderBottomLeftRadius: 4, 32 | backgroundColor: theme.palette.primary.main 33 | } 34 | }) 35 | ); 36 | 37 | const ListItemIconStyle = styled(ListItemIcon)({ 38 | width: 22, 39 | height: 22, 40 | display: 'flex', 41 | alignItems: 'center', 42 | justifyContent: 'center' 43 | }); 44 | 45 | // ---------------------------------------------------------------------- 46 | 47 | NavItem.propTypes = { 48 | item: PropTypes.object, 49 | active: PropTypes.func 50 | }; 51 | 52 | function NavItem({ item, active }) { 53 | const theme = useTheme(); 54 | const isActiveRoot = active(item.path); 55 | const { title, path, icon, info, children } = item; 56 | const [open, setOpen] = useState(isActiveRoot); 57 | 58 | const handleOpen = () => { 59 | setOpen((prev) => !prev); 60 | }; 61 | 62 | const activeRootStyle = { 63 | color: 'primary.main', 64 | fontWeight: 'fontWeightMedium', 65 | bgcolor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity), 66 | '&:before': { display: 'block' } 67 | }; 68 | 69 | const activeSubStyle = { 70 | color: 'text.primary', 71 | fontWeight: 'fontWeightMedium' 72 | }; 73 | 74 | if (children) { 75 | return ( 76 | <> 77 | 83 | {icon && icon} 84 | 85 | {info && info} 86 | 91 | 92 | 93 | 94 | 95 | {children.map((item) => { 96 | const { title, path } = item; 97 | const isActiveSub = active(path); 98 | 99 | return ( 100 | 108 | 109 | theme.transitions.create('transform'), 120 | ...(isActiveSub && { 121 | transform: 'scale(2)', 122 | bgcolor: 'primary.main' 123 | }) 124 | }} 125 | /> 126 | 127 | 128 | 129 | ); 130 | })} 131 | 132 | 133 | 134 | ); 135 | } 136 | 137 | return ( 138 | 145 | {icon && icon} 146 | 147 | {info && info} 148 | 149 | ); 150 | } 151 | 152 | NavSection.propTypes = { 153 | navConfig: PropTypes.array 154 | }; 155 | 156 | export default function NavSection({ navConfig, ...other }) { 157 | const { pathname } = useLocation(); 158 | const match = (path) => (path ? !!matchPath({ path, end: false }, pathname) : false); 159 | 160 | return ( 161 | 162 | 163 | {navConfig.map((item) => ( 164 | 165 | ))} 166 | 167 | 168 | ); 169 | } 170 | -------------------------------------------------------------------------------- /src/components/Page.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Helmet } from 'react-helmet-async'; 3 | import { forwardRef } from 'react'; 4 | // material 5 | import { Box } from '@mui/material'; 6 | 7 | // ---------------------------------------------------------------------- 8 | 9 | const Page = forwardRef(({ children, title = '', ...other }, ref) => ( 10 | 11 | 12 | {title} 13 | 14 | {children} 15 | 16 | )); 17 | 18 | Page.propTypes = { 19 | children: PropTypes.node.isRequired, 20 | title: PropTypes.string 21 | }; 22 | 23 | export default Page; 24 | -------------------------------------------------------------------------------- /src/components/ScrollToTop.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | // ---------------------------------------------------------------------- 5 | 6 | export default function ScrollToTop() { 7 | const { pathname } = useLocation(); 8 | 9 | useEffect(() => { 10 | window.scrollTo(0, 0); 11 | }, [pathname]); 12 | 13 | return null; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Scrollbar.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import SimpleBarReact from 'simplebar-react'; 3 | // material 4 | import { alpha, styled } from '@mui/material/styles'; 5 | import { Box } from '@mui/material'; 6 | 7 | // ---------------------------------------------------------------------- 8 | 9 | const RootStyle = styled('div')({ 10 | flexGrow: 1, 11 | height: '100%', 12 | overflow: 'hidden' 13 | }); 14 | 15 | const SimpleBarStyle = styled(SimpleBarReact)(({ theme }) => ({ 16 | maxHeight: '100%', 17 | '& .simplebar-scrollbar': { 18 | '&:before': { 19 | backgroundColor: alpha(theme.palette.grey[600], 0.48) 20 | }, 21 | '&.simplebar-visible:before': { 22 | opacity: 1 23 | } 24 | }, 25 | '& .simplebar-track.simplebar-vertical': { 26 | width: 10 27 | }, 28 | '& .simplebar-track.simplebar-horizontal .simplebar-scrollbar': { 29 | height: 6 30 | }, 31 | '& .simplebar-mask': { 32 | zIndex: 'inherit' 33 | } 34 | })); 35 | 36 | // ---------------------------------------------------------------------- 37 | 38 | Scrollbar.propTypes = { 39 | children: PropTypes.node.isRequired, 40 | sx: PropTypes.object 41 | }; 42 | 43 | export default function Scrollbar({ children, sx, ...other }) { 44 | const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( 45 | navigator.userAgent 46 | ); 47 | 48 | if (isMobile) { 49 | return ( 50 | 51 | {children} 52 | 53 | ); 54 | } 55 | 56 | return ( 57 | 58 | 59 | {children} 60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/SearchNotFound.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | // material 3 | import { Paper, Typography } from '@mui/material'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | SearchNotFound.propTypes = { 8 | searchQuery: PropTypes.string 9 | }; 10 | 11 | export default function SearchNotFound({ searchQuery = '', ...other }) { 12 | return ( 13 | 14 | 15 | Not found 16 | 17 | 18 | No results found for   19 | "{searchQuery}". Try checking for typos or using complete words. 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/SvgIconStyle.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Box } from '@mui/material'; 3 | 4 | // ---------------------------------------------------------------------- 5 | 6 | SvgIconStyle.propTypes = { 7 | src: PropTypes.string.isRequired, 8 | color: PropTypes.string, 9 | sx: PropTypes.object 10 | }; 11 | 12 | export default function SvgIconStyle({ src, color = 'inherit', sx }) { 13 | return ( 14 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/_dashboard/SummaryBox.jsx: -------------------------------------------------------------------------------- 1 | // material 2 | import { styled } from '@mui/material/styles'; 3 | import { Card, Typography } from '@mui/material'; 4 | 5 | const SummaryBox = ({ total = 0, color = '', backgroundColor = '', text = '', subText = '' }) => { 6 | const RootStyle = styled(Card)(({ theme }) => ({ 7 | boxShadow: 'none', 8 | textAlign: 'center', 9 | padding: theme.spacing(subText ? 1.5 : 3, 0), 10 | color: theme.palette[color].darker, 11 | backgroundColor: theme.palette[backgroundColor].lighter 12 | })); 13 | 14 | return ( 15 | 16 | {subText && ( 17 | 18 | {subText} 19 | 20 | )} 21 | 22 | {total} 23 | 24 | 25 | {text} 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default SummaryBox; 32 | -------------------------------------------------------------------------------- /src/components/_dashboard/app/AppCurrentSubject.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash'; 2 | import ReactApexChart from 'react-apexcharts'; 3 | // material 4 | import { useTheme, styled } from '@mui/material/styles'; 5 | import { Card, CardHeader } from '@mui/material'; 6 | // 7 | import { BaseOptionChart } from '../../charts'; 8 | 9 | // ---------------------------------------------------------------------- 10 | 11 | const CHART_HEIGHT = 392; 12 | const LEGEND_HEIGHT = 72; 13 | 14 | const ChartWrapperStyle = styled('div')(({ theme }) => ({ 15 | height: CHART_HEIGHT, 16 | marginTop: theme.spacing(2), 17 | '& .apexcharts-canvas svg': { 18 | height: CHART_HEIGHT 19 | }, 20 | '& .apexcharts-canvas svg,.apexcharts-canvas foreignObject': { 21 | overflow: 'visible' 22 | }, 23 | '& .apexcharts-legend': { 24 | height: LEGEND_HEIGHT, 25 | alignContent: 'center', 26 | position: 'relative !important', 27 | borderTop: `solid 1px ${theme.palette.divider}`, 28 | top: `calc(${CHART_HEIGHT - LEGEND_HEIGHT}px) !important` 29 | } 30 | })); 31 | 32 | // ---------------------------------------------------------------------- 33 | 34 | const CHART_DATA = [ 35 | { name: 'Series 1', data: [80, 50, 30, 40, 100, 20] }, 36 | { name: 'Series 2', data: [20, 30, 40, 80, 20, 80] }, 37 | { name: 'Series 3', data: [44, 76, 78, 13, 43, 10] } 38 | ]; 39 | 40 | export default function AppCurrentSubject() { 41 | const theme = useTheme(); 42 | 43 | const chartOptions = merge(BaseOptionChart(), { 44 | stroke: { width: 2 }, 45 | fill: { opacity: 0.48 }, 46 | legend: { floating: true, horizontalAlign: 'center' }, 47 | xaxis: { 48 | categories: ['English', 'History', 'Physics', 'Geography', 'Chinese', 'Math'], 49 | labels: { 50 | style: { 51 | colors: [ 52 | theme.palette.text.secondary, 53 | theme.palette.text.secondary, 54 | theme.palette.text.secondary, 55 | theme.palette.text.secondary, 56 | theme.palette.text.secondary, 57 | theme.palette.text.secondary 58 | ] 59 | } 60 | } 61 | } 62 | }); 63 | 64 | return ( 65 | 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/_dashboard/app/AppTasks.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Form, FormikProvider, useFormik } from 'formik'; 3 | // material 4 | import { 5 | Box, 6 | Card, 7 | Checkbox, 8 | CardHeader, 9 | Typography, 10 | FormControlLabel, 11 | Stack 12 | } from '@mui/material'; 13 | 14 | // ---------------------------------------------------------------------- 15 | 16 | const TASKS = [ 17 | 'Create FireStone Logo', 18 | 'Add SCSS and JS files if required', 19 | 'Stakeholder Meeting', 20 | 'Scoping & Estimations', 21 | 'Sprint Showcase' 22 | ]; 23 | 24 | // ---------------------------------------------------------------------- 25 | 26 | TaskItem.propTypes = { 27 | task: PropTypes.string, 28 | checked: PropTypes.bool, 29 | formik: PropTypes.object 30 | }; 31 | 32 | function TaskItem({ task, checked, formik, ...other }) { 33 | const { getFieldProps } = formik; 34 | 35 | return ( 36 | 37 | 40 | } 41 | label={ 42 | 51 | {task} 52 | 53 | } 54 | /> 55 | 56 | ); 57 | } 58 | 59 | export default function AppTasks() { 60 | const formik = useFormik({ 61 | initialValues: { 62 | checked: [TASKS[2]] 63 | }, 64 | onSubmit: (values) => { 65 | console.log(values); 66 | } 67 | }); 68 | 69 | const { values, handleSubmit } = formik; 70 | 71 | return ( 72 | 73 | 74 | 75 | 76 |
77 | {TASKS.map((task) => ( 78 | 84 | ))} 85 | 86 |
87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/components/_dashboard/app/AppTrafficBySite.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import PropTypes from 'prop-types'; 3 | import { Icon } from '@iconify/react'; 4 | import googleFill from '@iconify/icons-eva/google-fill'; 5 | import twitterFill from '@iconify/icons-eva/twitter-fill'; 6 | import facebookFill from '@iconify/icons-eva/facebook-fill'; 7 | import linkedinFill from '@iconify/icons-eva/linkedin-fill'; 8 | // material 9 | import { Box, Grid, Card, Paper, Typography, CardHeader, CardContent } from '@mui/material'; 10 | // utils 11 | import { fShortenNumber } from '../../../utils/formatNumber'; 12 | 13 | // ---------------------------------------------------------------------- 14 | 15 | const SOCIALS = [ 16 | { 17 | name: 'FaceBook', 18 | value: faker.datatype.number(), 19 | icon: 20 | }, 21 | { 22 | name: 'Google', 23 | value: faker.datatype.number(), 24 | icon: 25 | }, 26 | { 27 | name: 'Linkedin', 28 | value: faker.datatype.number(), 29 | icon: 30 | }, 31 | { 32 | name: 'Twitter', 33 | value: faker.datatype.number(), 34 | icon: 35 | } 36 | ]; 37 | 38 | // ---------------------------------------------------------------------- 39 | 40 | SiteItem.propTypes = { 41 | site: PropTypes.object 42 | }; 43 | 44 | function SiteItem({ site }) { 45 | const { icon, value, name } = site; 46 | 47 | return ( 48 | 49 | 50 | {icon} 51 | {fShortenNumber(value)} 52 | 53 | {name} 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | export default function AppTrafficBySite() { 61 | return ( 62 | 63 | 64 | 65 | 66 | {SOCIALS.map((site) => ( 67 | 68 | ))} 69 | 70 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/_dashboard/app/LastOrders.js: -------------------------------------------------------------------------------- 1 | import { isEmpty, orderBy } from 'lodash'; 2 | import { useRecoilValue } from 'recoil'; 3 | import { memo } from 'react'; 4 | import { format } from 'date-fns'; 5 | 6 | // material 7 | import { Card, Typography, CardHeader, CardContent, CircularProgress } from '@mui/material'; 8 | import { 9 | Timeline, 10 | TimelineItem, 11 | TimelineContent, 12 | TimelineConnector, 13 | TimelineSeparator, 14 | TimelineDot 15 | } from '@mui/lab'; 16 | 17 | // utils 18 | import { fDateTime } from '../../../utils/formatTime'; 19 | import { tradesAtom } from '../../../recoil/atoms'; 20 | 21 | function Trade({ trade, isLast }) { 22 | const { side, time, symbol } = trade; 23 | 24 | const title = `Long ${symbol} ${side === 'BUY' ? 'opened' : 'closed'}`; 25 | 26 | return ( 27 | 28 | 29 | 35 | {isLast ? null : } 36 | 37 | 38 | {title} 39 | 40 | {fDateTime(time)} 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | const LastOrders = () => { 48 | const trades = useRecoilValue(tradesAtom); 49 | const today = format(new Date(), 'MM/dd/yyyy'); 50 | 51 | return ( 52 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | const Content = memo(({ trades }) => { 68 | if (isEmpty(trades)) { 69 | return ; 70 | } 71 | 72 | const orderedTrades = orderBy(trades, ['time'], ['desc']); 73 | const tradesLength = trades.length; 74 | const maxLength = 5; 75 | 76 | return ( 77 | 78 | {orderedTrades.slice(0, maxLength).map((trade, index) => ( 79 | 84 | ))} 85 | 86 | ); 87 | }); 88 | 89 | export default LastOrders; 90 | -------------------------------------------------------------------------------- /src/components/_dashboard/app/PerformanceOverview.js: -------------------------------------------------------------------------------- 1 | import { merge, sum, max, flatten } from 'lodash'; 2 | import { format } from 'date-fns'; 3 | import ReactApexChart from 'react-apexcharts'; 4 | import { useRecoilValue } from 'recoil'; 5 | 6 | // material 7 | import { Card, CardHeader, Box } from '@mui/material'; 8 | // 9 | import { BaseOptionChart } from '../../charts'; 10 | import { fPercent } from '../../../utils/formatNumber'; 11 | import { generateLastNdates } from '../../../utils/formatTime'; 12 | import { incomesAtom, accountAtom } from '../../../recoil/atoms'; 13 | 14 | // ---------------------------------------------------------------------- 15 | 16 | const lastWeekDates = generateLastNdates(7, 'MM/dd/yyyy').reverse(); 17 | 18 | const getIncomeForDate = (incomes, date) => { 19 | const incomeList = incomes[format(new Date(date), 'MM/dd/yyyy')]; 20 | return sum(incomeList?.map((inc) => JSON.parse(inc?.income))); 21 | }; 22 | 23 | const getIncomeOfWeek = (incomes, isRounded = false) => { 24 | const incomeOfWeek = lastWeekDates.map((date) => getIncomeForDate(incomes, date)); 25 | 26 | return isRounded 27 | ? incomeOfWeek.map((inc) => Math.round(inc)) 28 | : incomeOfWeek.map((inc) => parseFloat(inc.toFixed(2))); 29 | }; 30 | 31 | const getBalanceLastWeek = (incomes, balance) => { 32 | let sumIncome = 0; 33 | const balances = []; 34 | 35 | const incomeOfWeek = getIncomeOfWeek(incomes).reverse(); 36 | 37 | incomeOfWeek.forEach((inc, i) => { 38 | sumIncome += inc; 39 | balances.push(Math.round(balance - sumIncome + incomeOfWeek[i])); 40 | }); 41 | 42 | return balances.reverse(); 43 | }; 44 | 45 | const PerformanceOverview = () => { 46 | const incomes = useRecoilValue(incomesAtom); 47 | const account = useRecoilValue(accountAtom); 48 | 49 | const profitLastWeek = sum( 50 | flatten(Object.values(incomes))?.map((inc) => JSON.parse(inc?.income)) 51 | ); 52 | 53 | const { totalCrossWalletBalance = 0 } = account; 54 | const balance = JSON.parse(totalCrossWalletBalance); 55 | const increasePercent = 56 | (balance > 0 && profitLastWeek && (profitLastWeek / (balance - profitLastWeek)) * 100) || 0; 57 | 58 | const balancesOfLastWeek = getBalanceLastWeek(incomes, balance); 59 | const weekIncome = getIncomeOfWeek(incomes, true); 60 | 61 | const chartOptions = merge(BaseOptionChart(), { 62 | stroke: { width: [0, 2, 3] }, 63 | plotOptions: { bar: { columnWidth: '11%', borderRadius: 4 } }, 64 | fill: { type: ['solid', 'gradient', 'solid'] }, 65 | labels: lastWeekDates, 66 | xaxis: { type: 'datetime' }, 67 | yaxis: [ 68 | { 69 | seriesName: 'Income', 70 | opposite: true, 71 | title: 'Income', 72 | min: 0, 73 | max: max(weekIncome) * 2, 74 | forceNiceScale: true 75 | }, 76 | { 77 | seriesName: 'Balance', 78 | min: Math.round(balancesOfLastWeek[0] * 0.99), 79 | max: Math.round(balancesOfLastWeek[balancesOfLastWeek.length - 1] * 1.01), 80 | forceNiceScale: true, 81 | title: 'Balance' 82 | } 83 | ], 84 | tooltip: { 85 | shared: true, 86 | intersect: false, 87 | y: { 88 | formatter: (y) => { 89 | if (typeof y !== 'undefined') { 90 | return `$${y.toFixed(1)}`; 91 | } 92 | return y; 93 | } 94 | } 95 | } 96 | }); 97 | 98 | const CHART_DATA = [ 99 | { 100 | name: 'Income', 101 | type: 'column', 102 | data: weekIncome 103 | }, 104 | { 105 | name: 'Balance', 106 | type: 'area', 107 | data: balancesOfLastWeek 108 | } 109 | ]; 110 | 111 | return ( 112 | 113 | 117 | 118 | 119 | 120 | 121 | ); 122 | }; 123 | 124 | export default PerformanceOverview; 125 | -------------------------------------------------------------------------------- /src/components/_dashboard/app/PositionsRepartition.js: -------------------------------------------------------------------------------- 1 | import { merge, isEmpty } from 'lodash'; 2 | import ReactApexChart from 'react-apexcharts'; 3 | import { useRecoilValue } from 'recoil'; 4 | 5 | // material 6 | import { useTheme, styled } from '@mui/material/styles'; 7 | import { Card, CardHeader, CircularProgress } from '@mui/material'; 8 | // utils 9 | import { fNumber } from '../../../utils/formatNumber'; 10 | // 11 | import { BaseOptionChart } from '../../charts'; 12 | import { accountAtom } from '../../../recoil/atoms'; 13 | 14 | // ---------------------------------------------------------------------- 15 | 16 | const CHART_HEIGHT = 410; 17 | const LEGEND_HEIGHT = 108; 18 | 19 | const ChartWrapperStyle = styled('div')(({ theme }) => ({ 20 | height: CHART_HEIGHT, 21 | marginTop: theme.spacing(3), 22 | '& .apexcharts-canvas svg': { height: CHART_HEIGHT }, 23 | '& .apexcharts-canvas svg,.apexcharts-canvas foreignObject': { 24 | overflow: 'visible' 25 | }, 26 | '& .apexcharts-legend': { 27 | height: LEGEND_HEIGHT, 28 | alignContent: 'center', 29 | position: 'relative !important', 30 | borderTop: `solid 1px ${theme.palette.divider}`, 31 | top: `calc(${CHART_HEIGHT - LEGEND_HEIGHT}px) !important` 32 | } 33 | })); 34 | 35 | // ---------------------------------------------------------------------- 36 | 37 | const PositionsRepartition = () => { 38 | const account = useRecoilValue(accountAtom); 39 | 40 | return ( 41 | 42 | 43 | 44 | {isEmpty(account) ? ( 45 | 46 | ) : ( 47 | 48 | )} 49 | 50 | 51 | ); 52 | }; 53 | const PieChart = ({ accountPositions }) => { 54 | const theme = useTheme(); 55 | 56 | const positions = accountPositions?.filter((pos) => pos.positionAmt > 0 || pos.positionAmt < 0); 57 | 58 | const chartOptions = merge(BaseOptionChart(), { 59 | labels: positions?.map((p) => p.symbol), 60 | stroke: { colors: [theme.palette.background.paper] }, 61 | legend: { floating: true, horizontalAlign: 'center' }, 62 | dataLabels: { enabled: true, dropShadow: { enabled: false } }, 63 | tooltip: { 64 | fillSeriesColor: false, 65 | y: { 66 | formatter: (seriesName) => fNumber(seriesName), 67 | title: { 68 | formatter: (seriesName) => `#${seriesName}` 69 | } 70 | } 71 | }, 72 | plotOptions: { 73 | pie: { donut: { labels: { show: false } } } 74 | } 75 | }); 76 | 77 | return ( 78 | JSON.parse(p.positionInitialMargin))} 81 | options={chartOptions} 82 | height={280} 83 | /> 84 | ); 85 | }; 86 | 87 | export default PositionsRepartition; 88 | -------------------------------------------------------------------------------- /src/components/_dashboard/app/TopPerformers.js: -------------------------------------------------------------------------------- 1 | import { merge, sum, orderBy, flatten } from 'lodash'; 2 | import ReactApexChart from 'react-apexcharts'; 3 | import { useRecoilValue } from 'recoil'; 4 | 5 | // material 6 | import { Box, Card, CardHeader } from '@mui/material'; 7 | // utils 8 | import { fCurrency } from '../../../utils/formatNumber'; 9 | // 10 | import { BaseOptionChart } from '../../charts'; 11 | import { incomesAtom } from '../../../recoil/atoms'; 12 | 13 | // ---------------------------------------------------------------------- 14 | 15 | const TopPerformers = () => { 16 | const incomes = useRecoilValue(incomesAtom); 17 | 18 | const pnlRepartition = {}; 19 | 20 | flatten(Object.values(incomes))?.forEach((inc) => { 21 | pnlRepartition[inc?.symbol] = (pnlRepartition[inc?.symbol] || 0) + JSON.parse(inc?.income); 22 | }); 23 | 24 | const orderedPerformers = orderBy( 25 | Object.keys(pnlRepartition).map((key) => ({ label: key, value: pnlRepartition[key] })), 26 | 'value', 27 | 'desc' 28 | ); 29 | 30 | return ( 31 | 32 | o.value)))} last 7 days`} 35 | /> 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | const Content = ({ orderedPerformers }) => { 44 | const chartOptions = merge(BaseOptionChart(), { 45 | tooltip: { 46 | marker: { show: false }, 47 | y: { 48 | formatter: (seriesName) => fCurrency(seriesName), 49 | title: { 50 | formatter: () => 'Last 7 days: ' 51 | } 52 | } 53 | }, 54 | plotOptions: { 55 | bar: { horizontal: true, barHeight: '28%', borderRadius: 2 } 56 | }, 57 | xaxis: { 58 | categories: orderedPerformers.map((o) => o.label) 59 | } 60 | }); 61 | 62 | const CHART_DATA = [{ data: orderedPerformers.map((o) => o.value) }]; 63 | 64 | return ; 65 | }; 66 | 67 | export default TopPerformers; 68 | -------------------------------------------------------------------------------- /src/components/_dashboard/app/index.js: -------------------------------------------------------------------------------- 1 | export { default as TopPerformers } from './TopPerformers'; 2 | export { default as AppCurrentSubject } from './AppCurrentSubject'; 3 | export { default as PositionsRepartition } from './PositionsRepartition'; 4 | export { default as LastOrders } from './LastOrders'; 5 | export { default as AppTasks } from './AppTasks'; 6 | export { default as AppTrafficBySite } from './AppTrafficBySite'; 7 | export { default as PerformanceOverview } from './PerformanceOverview'; 8 | -------------------------------------------------------------------------------- /src/components/_dashboard/order/OrderListHead.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | // material 3 | import { visuallyHidden } from '@mui/utils'; 4 | import { Box, TableRow, TableCell, TableHead, TableSortLabel } from '@mui/material'; 5 | 6 | // ---------------------------------------------------------------------- 7 | 8 | OrderListHead.propTypes = { 9 | order: PropTypes.oneOf(['asc', 'desc']), 10 | orderBy: PropTypes.string, 11 | headLabel: PropTypes.array, 12 | onRequestSort: PropTypes.func 13 | }; 14 | 15 | export default function OrderListHead({ order, orderBy, headLabel, onRequestSort }) { 16 | const createSortHandler = (property) => (event) => { 17 | onRequestSort(event, property); 18 | }; 19 | 20 | return ( 21 | 22 | 23 | {headLabel.map((headCell) => ( 24 | 29 | 35 | {headCell.label} 36 | {orderBy === headCell.id ? ( 37 | 38 | {order === 'desc' ? 'sorted descending' : 'sorted ascending'} 39 | 40 | ) : null} 41 | 42 | 43 | ))} 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/_dashboard/order/OrderListToolbar.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Icon } from '@iconify/react'; 3 | import searchFill from '@iconify/icons-eva/search-fill'; 4 | 5 | // material 6 | import { styled } from '@mui/material/styles'; 7 | import { Box, Toolbar, Typography, OutlinedInput, InputAdornment } from '@mui/material'; 8 | 9 | // ---------------------------------------------------------------------- 10 | 11 | const RootStyle = styled(Toolbar)(({ theme }) => ({ 12 | height: 96, 13 | display: 'flex', 14 | justifyContent: 'space-between', 15 | padding: theme.spacing(0, 1, 0, 3) 16 | })); 17 | 18 | const SearchStyle = styled(OutlinedInput)(({ theme }) => ({ 19 | width: 240, 20 | transition: theme.transitions.create(['box-shadow', 'width'], { 21 | easing: theme.transitions.easing.easeInOut, 22 | duration: theme.transitions.duration.shorter 23 | }), 24 | '&.Mui-focused': { width: 320, boxShadow: theme.customShadows.z8 }, 25 | '& fieldset': { 26 | borderWidth: `1px !important`, 27 | borderColor: `${theme.palette.grey[500_32]} !important` 28 | } 29 | })); 30 | 31 | // ---------------------------------------------------------------------- 32 | 33 | OrderListToolbar.propTypes = { 34 | numSelected: PropTypes.number, 35 | filterName: PropTypes.string, 36 | onFilterName: PropTypes.func 37 | }; 38 | 39 | export default function OrderListToolbar({ numSelected, filterName, onFilterName }) { 40 | return ( 41 | 0 && { 44 | color: 'primary.main', 45 | bgcolor: 'primary.lighter' 46 | }) 47 | }} 48 | > 49 | {numSelected > 0 ? ( 50 | 51 | {numSelected} selected 52 | 53 | ) : ( 54 | 60 | 61 | 62 | } 63 | /> 64 | )} 65 | 66 | {/* {numSelected > 0 ? ( 67 | 68 | 69 | 70 | 71 | 72 | ) : ( 73 | 74 | 75 | 76 | 77 | 78 | )} */} 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/components/_dashboard/order/OrderMoreMenu.js: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/react'; 2 | import { useRef, useState } from 'react'; 3 | import editFill from '@iconify/icons-eva/edit-fill'; 4 | import { Link as RouterLink } from 'react-router-dom'; 5 | import trash2Outline from '@iconify/icons-eva/trash-2-outline'; 6 | import moreVerticalFill from '@iconify/icons-eva/more-vertical-fill'; 7 | // material 8 | import { Menu, MenuItem, IconButton, ListItemIcon, ListItemText } from '@mui/material'; 9 | 10 | // ---------------------------------------------------------------------- 11 | 12 | export default function OrderMoreMenu() { 13 | const ref = useRef(null); 14 | const [isOpen, setIsOpen] = useState(false); 15 | 16 | return ( 17 | <> 18 | setIsOpen(true)}> 19 | 20 | 21 | 22 | setIsOpen(false)} 26 | PaperProps={{ 27 | sx: { width: 200, maxWidth: '100%' } 28 | }} 29 | anchorOrigin={{ vertical: 'top', horizontal: 'right' }} 30 | transformOrigin={{ vertical: 'top', horizontal: 'right' }} 31 | > 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/_dashboard/order/index.js: -------------------------------------------------------------------------------- 1 | export { default as OrderListHead } from './OrderListHead'; 2 | export { default as OrderListToolbar } from './OrderListToolbar'; 3 | export { default as OrderMoreMenu } from './OrderMoreMenu'; 4 | -------------------------------------------------------------------------------- /src/components/animate/MotionContainer.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { motion } from 'framer-motion'; 3 | // material 4 | import { Box } from '@mui/material'; 5 | // 6 | import { varWrapEnter } from './variants'; 7 | 8 | // ---------------------------------------------------------------------- 9 | 10 | MotionContainer.propTypes = { 11 | open: PropTypes.bool.isRequired, 12 | children: PropTypes.node 13 | }; 14 | 15 | export default function MotionContainer({ open, children, ...other }) { 16 | return ( 17 | 24 | {children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/animate/index.js: -------------------------------------------------------------------------------- 1 | export * from './variants'; 2 | export { default as MotionContainer } from './MotionContainer'; 3 | -------------------------------------------------------------------------------- /src/components/animate/variants/Wrap.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export const varWrapEnter = { 4 | animate: { 5 | transition: { staggerChildren: 0.1 } 6 | } 7 | }; 8 | 9 | export const varWrapExit = { 10 | exit: { 11 | transition: { staggerChildren: 0.1 } 12 | } 13 | }; 14 | 15 | export const varWrapBoth = { 16 | animate: { 17 | transition: { staggerChildren: 0.07, delayChildren: 0.1 } 18 | }, 19 | exit: { 20 | transition: { staggerChildren: 0.05, staggerDirection: -1 } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/animate/variants/bounce/BounceIn.js: -------------------------------------------------------------------------------- 1 | import { 2 | varBounceOut, 3 | varBounceOutUp, 4 | varBounceOutDown, 5 | varBounceOutLeft, 6 | varBounceOutRight 7 | } from './BounceOut'; 8 | 9 | // ---------------------------------------------------------------------- 10 | 11 | const TRANSITION_ENTER = { 12 | duration: 0.72, 13 | ease: [0.43, 0.13, 0.23, 0.96] 14 | }; 15 | 16 | const TRANSITION_EXIT = { 17 | duration: 0.48, 18 | ease: [0.43, 0.13, 0.23, 0.96] 19 | }; 20 | 21 | export const varBounceIn = { 22 | animate: { 23 | scale: [0.3, 1.1, 0.9, 1.03, 0.97, 1], 24 | opacity: [0, 1, 1, 1, 1, 1], 25 | transition: TRANSITION_ENTER 26 | }, 27 | exit: varBounceOut.animate 28 | }; 29 | 30 | export const varBounceInUp = { 31 | animate: { 32 | y: [720, -24, 12, -4, 0], 33 | scaleY: [4, 0.9, 0.95, 0.985, 1], 34 | opacity: [0, 1, 1, 1, 1], 35 | transition: { ...TRANSITION_ENTER } 36 | }, 37 | exit: { ...varBounceOutDown.animate, transition: TRANSITION_EXIT } 38 | }; 39 | 40 | export const varBounceInDown = { 41 | animate: { 42 | y: [-720, 24, -12, 4, 0], 43 | scaleY: [4, 0.9, 0.95, 0.985, 1], 44 | opacity: [0, 1, 1, 1, 1], 45 | transition: TRANSITION_ENTER 46 | }, 47 | exit: { ...varBounceOutUp.animate, transition: TRANSITION_EXIT } 48 | }; 49 | 50 | export const varBounceInLeft = { 51 | animate: { 52 | x: [-720, 24, -12, 4, 0], 53 | scaleX: [3, 1, 0.98, 0.995, 1], 54 | opacity: [0, 1, 1, 1, 1], 55 | transition: TRANSITION_ENTER 56 | }, 57 | exit: { ...varBounceOutLeft.animate, transition: TRANSITION_EXIT } 58 | }; 59 | 60 | export const varBounceInRight = { 61 | animate: { 62 | x: [720, -24, 12, -4, 0], 63 | scaleX: [3, 1, 0.98, 0.995, 1], 64 | opacity: [0, 1, 1, 1, 1], 65 | transition: TRANSITION_ENTER 66 | }, 67 | exit: { ...varBounceOutRight.animate, transition: TRANSITION_EXIT } 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/animate/variants/bounce/BounceOut.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export const varBounceOut = { 4 | animate: { 5 | scale: [0.9, 1.1, 0.3], 6 | opacity: [1, 1, 0] 7 | } 8 | }; 9 | 10 | export const varBounceOutUp = { 11 | animate: { 12 | y: [-12, 24, -720], 13 | scaleY: [0.985, 0.9, 3], 14 | opacity: [1, 1, 0] 15 | } 16 | }; 17 | 18 | export const varBounceOutDown = { 19 | animate: { 20 | y: [12, -24, 720], 21 | scaleY: [0.985, 0.9, 3], 22 | opacity: [1, 1, 0] 23 | } 24 | }; 25 | 26 | export const varBounceOutLeft = { 27 | animate: { 28 | x: [0, 24, -720], 29 | scaleX: [1, 0.9, 2], 30 | opacity: [1, 1, 0] 31 | } 32 | }; 33 | 34 | export const varBounceOutRight = { 35 | animate: { 36 | x: [0, -24, 720], 37 | scaleX: [1, 0.9, 2], 38 | opacity: [1, 1, 0] 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/animate/variants/bounce/index.js: -------------------------------------------------------------------------------- 1 | export * from './BounceIn'; 2 | export * from './BounceOut'; 3 | -------------------------------------------------------------------------------- /src/components/animate/variants/index.js: -------------------------------------------------------------------------------- 1 | export * from './Wrap'; 2 | export * from './bounce'; 3 | -------------------------------------------------------------------------------- /src/components/charts/BaseOptionChart.js: -------------------------------------------------------------------------------- 1 | // material 2 | import { alpha, useTheme } from '@mui/material/styles'; 3 | import { GlobalStyles } from '@mui/material'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | export function BaseOptionChartStyle() { 8 | const theme = useTheme(); 9 | 10 | const background = { 11 | backdropFilter: 'blur(6px)', 12 | WebkitBackdropFilter: 'blur(6px)', // Fix on Mobile 13 | backgroundColor: alpha(theme.palette.background.default, 0.72) 14 | }; 15 | 16 | return ( 17 | 61 | ); 62 | } 63 | 64 | export default function BaseOptionChart() { 65 | const theme = useTheme(); 66 | 67 | const LABEL_TOTAL = { 68 | show: true, 69 | label: 'Total', 70 | color: theme.palette.text.secondary, 71 | ...theme.typography.subtitle2 72 | }; 73 | 74 | const LABEL_VALUE = { 75 | offsetY: 8, 76 | color: theme.palette.text.primary, 77 | ...theme.typography.h3 78 | }; 79 | 80 | return { 81 | // Colors 82 | colors: [ 83 | theme.palette.primary.main, 84 | theme.palette.chart.yellow[0], 85 | theme.palette.chart.blue[0], 86 | theme.palette.chart.violet[0], 87 | theme.palette.chart.green[0], 88 | theme.palette.chart.red[0] 89 | ], 90 | 91 | // Chart 92 | chart: { 93 | toolbar: { show: false }, 94 | zoom: { enabled: false }, 95 | // animations: { enabled: false }, 96 | foreColor: theme.palette.text.disabled, 97 | fontFamily: theme.typography.fontFamily 98 | }, 99 | 100 | // States 101 | states: { 102 | hover: { 103 | filter: { 104 | type: 'lighten', 105 | value: 0.04 106 | } 107 | }, 108 | active: { 109 | filter: { 110 | type: 'darken', 111 | value: 0.88 112 | } 113 | } 114 | }, 115 | 116 | // Fill 117 | fill: { 118 | opacity: 1, 119 | gradient: { 120 | type: 'vertical', 121 | shadeIntensity: 0, 122 | opacityFrom: 0.4, 123 | opacityTo: 0, 124 | stops: [0, 100] 125 | } 126 | }, 127 | 128 | // Datalabels 129 | dataLabels: { enabled: false }, 130 | 131 | // Stroke 132 | stroke: { 133 | width: 3, 134 | curve: 'smooth', 135 | lineCap: 'round' 136 | }, 137 | 138 | // Grid 139 | grid: { 140 | strokeDashArray: 3, 141 | borderColor: theme.palette.divider 142 | }, 143 | 144 | // Xaxis 145 | xaxis: { 146 | axisBorder: { show: false }, 147 | axisTicks: { show: false } 148 | }, 149 | 150 | // Markers 151 | markers: { 152 | size: 0, 153 | strokeColors: theme.palette.background.paper 154 | }, 155 | 156 | // Tooltip 157 | tooltip: { 158 | x: { 159 | show: false 160 | } 161 | }, 162 | 163 | // Legend 164 | legend: { 165 | show: true, 166 | fontSize: 13, 167 | position: 'top', 168 | horizontalAlign: 'right', 169 | markers: { 170 | radius: 12 171 | }, 172 | fontWeight: 500, 173 | itemMargin: { horizontal: 12 }, 174 | labels: { 175 | colors: theme.palette.text.primary 176 | } 177 | }, 178 | 179 | // plotOptions 180 | plotOptions: { 181 | // Bar 182 | bar: { 183 | columnWidth: '28%', 184 | borderRadius: 4 185 | }, 186 | // Pie + Donut 187 | pie: { 188 | donut: { 189 | labels: { 190 | show: true, 191 | value: LABEL_VALUE, 192 | total: LABEL_TOTAL 193 | } 194 | } 195 | }, 196 | // Radialbar 197 | radialBar: { 198 | track: { 199 | strokeWidth: '100%', 200 | background: theme.palette.grey[500_16] 201 | }, 202 | dataLabels: { 203 | value: LABEL_VALUE, 204 | total: LABEL_TOTAL 205 | } 206 | }, 207 | // Radar 208 | radar: { 209 | polygons: { 210 | fill: { colors: ['transparent'] }, 211 | strokeColors: theme.palette.divider, 212 | connectorColors: theme.palette.divider 213 | } 214 | }, 215 | // polarArea 216 | polarArea: { 217 | rings: { 218 | strokeColor: theme.palette.divider 219 | }, 220 | spokes: { 221 | connectorColors: theme.palette.divider 222 | } 223 | } 224 | }, 225 | 226 | // Responsive 227 | responsive: [ 228 | { 229 | // sm 230 | breakpoint: theme.breakpoints.values.sm, 231 | options: { 232 | plotOptions: { bar: { columnWidth: '40%' } } 233 | } 234 | }, 235 | { 236 | // md 237 | breakpoint: theme.breakpoints.values.md, 238 | options: { 239 | plotOptions: { bar: { columnWidth: '32%' } } 240 | } 241 | } 242 | ] 243 | }; 244 | } 245 | -------------------------------------------------------------------------------- /src/components/charts/index.js: -------------------------------------------------------------------------------- 1 | export { default as BaseOptionChart } from './BaseOptionChart'; 2 | -------------------------------------------------------------------------------- /src/config/api-keys.example.js: -------------------------------------------------------------------------------- 1 | export const CONFIGURATION = { 2 | binance: { 3 | key: 'BINANCE_API_KEY', 4 | secret: 'BINANCE_API_SECRET', 5 | exchangeType: 'futures' // spot is not supported yet 6 | }, 7 | profile: { 8 | name: 'John Doe' 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // scroll bar 2 | import 'simplebar/src/simplebar.css'; 3 | 4 | import ReactDOM from 'react-dom'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import { HelmetProvider } from 'react-helmet-async'; 7 | import { RecoilRoot } from 'recoil'; 8 | 9 | // 10 | import App from './App'; 11 | import * as serviceWorker from './serviceWorker'; 12 | import reportWebVitals from './reportWebVitals'; 13 | 14 | // ---------------------------------------------------------------------- 15 | 16 | ReactDOM.render( 17 | 18 | 19 | 20 | 21 | 22 | 23 | , 24 | document.getElementById('root') 25 | ); 26 | 27 | // If you want to enable client cache, register instead. 28 | serviceWorker.unregister(); 29 | 30 | // If you want to start measuring performance in your app, pass a function 31 | // to log results (for example: reportWebVitals(console.log)) 32 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 33 | reportWebVitals(); 34 | -------------------------------------------------------------------------------- /src/layouts/LogoOnlyLayout.js: -------------------------------------------------------------------------------- 1 | import { Link as RouterLink, Outlet } from 'react-router-dom'; 2 | // material 3 | import { styled } from '@mui/material/styles'; 4 | // components 5 | import Logo from '../components/Logo'; 6 | 7 | // ---------------------------------------------------------------------- 8 | 9 | const HeaderStyle = styled('header')(({ theme }) => ({ 10 | top: 0, 11 | left: 0, 12 | lineHeight: 0, 13 | width: '100%', 14 | position: 'absolute', 15 | padding: theme.spacing(3, 3, 0), 16 | [theme.breakpoints.up('sm')]: { 17 | padding: theme.spacing(5, 5, 0) 18 | } 19 | })); 20 | 21 | // ---------------------------------------------------------------------- 22 | 23 | export default function LogoOnlyLayout() { 24 | return ( 25 | <> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/layouts/dashboard/AccountPopover.js: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/react'; 2 | import { useRef, useState } from 'react'; 3 | import homeFill from '@iconify/icons-eva/home-fill'; 4 | import personFill from '@iconify/icons-eva/person-fill'; 5 | import settings2Fill from '@iconify/icons-eva/settings-2-fill'; 6 | import { Link as RouterLink } from 'react-router-dom'; 7 | // material 8 | import { alpha } from '@mui/material/styles'; 9 | import { Button, Box, Divider, MenuItem, Typography, Avatar, IconButton } from '@mui/material'; 10 | // components 11 | import MenuPopover from '../../components/MenuPopover'; 12 | // 13 | import profilePicture from '../../assets/avatar_default.jpg'; 14 | import { CONFIGURATION } from '../../config/api-keys'; 15 | 16 | // ---------------------------------------------------------------------- 17 | 18 | const MENU_OPTIONS = [ 19 | { 20 | label: 'Home', 21 | icon: homeFill, 22 | linkTo: '/' 23 | }, 24 | { 25 | label: 'Profile', 26 | icon: personFill, 27 | linkTo: '#' 28 | }, 29 | { 30 | label: 'Settings', 31 | icon: settings2Fill, 32 | linkTo: '#' 33 | } 34 | ]; 35 | 36 | // ---------------------------------------------------------------------- 37 | 38 | export default function AccountPopover() { 39 | const anchorRef = useRef(null); 40 | const [open, setOpen] = useState(false); 41 | 42 | const handleOpen = () => { 43 | setOpen(true); 44 | }; 45 | const handleClose = () => { 46 | setOpen(false); 47 | }; 48 | 49 | return ( 50 | <> 51 | alpha(theme.palette.grey[900], 0.72) 67 | } 68 | }) 69 | }} 70 | > 71 | 72 | 73 | 74 | 80 | 81 | 82 | {CONFIGURATION.profile.name} 83 | 84 | 85 | Email 86 | 87 | 88 | 89 | 90 | 91 | {MENU_OPTIONS.map((option) => ( 92 | 99 | 108 | 109 | {option.label} 110 | 111 | ))} 112 | 113 | 114 | 117 | 118 | 119 | 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /src/layouts/dashboard/BTCprice.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react'; 2 | import useWebSocket from 'react-use-websocket'; 3 | 4 | import { Box, Typography, Stack, Grid } from '@mui/material'; 5 | import { fCurrency, fPercent } from '../../utils/formatNumber'; 6 | 7 | const socketUrl = 'wss://fstream.binance.com/stream'; 8 | 9 | const BTCprice = () => { 10 | const { sendJsonMessage, lastJsonMessage } = useWebSocket(socketUrl); 11 | 12 | useEffect(() => { 13 | console.log('Opening websocket for BTC data'); 14 | subscribeWebsocket(); 15 | 16 | return () => { 17 | console.log('Closing websocket for BTC data'); 18 | unsubscribeWebsocket(); 19 | }; 20 | }, []); 21 | 22 | const subscribeWebsocket = useCallback( 23 | () => 24 | sendJsonMessage({ 25 | method: 'SUBSCRIBE', 26 | params: ['btcusdt@ticker'], 27 | id: 1 28 | }), 29 | [sendJsonMessage] 30 | ); 31 | 32 | const unsubscribeWebsocket = useCallback( 33 | () => 34 | sendJsonMessage({ 35 | method: 'UNSUBSCRIBE', 36 | params: ['btcusdt@ticker'], 37 | id: 1 38 | }), 39 | [sendJsonMessage] 40 | ); 41 | 42 | return ( 43 | 44 | 54 | 59 | 60 | 61 | 62 | 63 | {fCurrency(Math.round(lastJsonMessage?.data?.c))} 64 | 65 | 66 | 0 ? 'green' : 'red'} variant="overline"> 67 | {fPercent(lastJsonMessage?.data?.P)} 68 | 69 | 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default BTCprice; 77 | -------------------------------------------------------------------------------- /src/layouts/dashboard/DashboardNavbar.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Icon } from '@iconify/react'; 3 | import menu2Fill from '@iconify/icons-eva/menu-2-fill'; 4 | // material 5 | import { alpha, styled } from '@mui/material/styles'; 6 | import { AppBar, Toolbar, IconButton } from '@mui/material'; 7 | // components 8 | import { MHidden } from '../../components/@material-extend'; 9 | // 10 | // import Searchbar from './Searchbar'; 11 | // import AccountPopover from './AccountPopover'; 12 | // import LanguagePopover from './LanguagePopover'; 13 | // import NotificationsPopover from './NotificationsPopover'; 14 | 15 | // ---------------------------------------------------------------------- 16 | 17 | const DRAWER_WIDTH = 280; 18 | 19 | const RootStyle = styled(AppBar)(({ theme }) => ({ 20 | boxShadow: 'none', 21 | backdropFilter: 'blur(6px)', 22 | WebkitBackdropFilter: 'blur(6px)', // Fix on Mobile 23 | backgroundColor: alpha(theme.palette.background.default, 0.72), 24 | [theme.breakpoints.up('lg')]: { 25 | width: `calc(100% - ${DRAWER_WIDTH + 1}px)` 26 | } 27 | })); 28 | 29 | const ToolbarStyle = styled(Toolbar)(({ theme }) => ({ 30 | minHeight: 0, 31 | [theme.breakpoints.up('lg')]: { 32 | minHeight: 0, 33 | padding: theme.spacing(0, 5) 34 | } 35 | })); 36 | 37 | // ---------------------------------------------------------------------- 38 | 39 | DashboardNavbar.propTypes = { 40 | onOpenSidebar: PropTypes.func 41 | }; 42 | 43 | export default function DashboardNavbar({ onOpenSidebar }) { 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/layouts/dashboard/DashboardSidebar.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { useEffect } from 'react'; 3 | import { Link as RouterLink, useLocation } from 'react-router-dom'; 4 | // material 5 | import { styled } from '@mui/material/styles'; 6 | import { Box, Link, Drawer, Typography, Avatar } from '@mui/material'; 7 | // components 8 | import Logo from '../../components/Logo'; 9 | import Scrollbar from '../../components/Scrollbar'; 10 | import NavSection from '../../components/NavSection'; 11 | import { MHidden } from '../../components/@material-extend'; 12 | // 13 | import sidebarConfig from './SidebarConfig'; 14 | import profilePicture from '../../assets/avatar_default.jpg'; 15 | import BTCprice from './BTCprice'; 16 | import { CONFIGURATION } from '../../config/api-keys'; 17 | 18 | // ---------------------------------------------------------------------- 19 | 20 | const DRAWER_WIDTH = 280; 21 | 22 | const RootStyle = styled('div')(({ theme }) => ({ 23 | [theme.breakpoints.up('lg')]: { 24 | flexShrink: 0, 25 | width: DRAWER_WIDTH 26 | } 27 | })); 28 | 29 | const AccountStyle = styled('div')(({ theme }) => ({ 30 | display: 'flex', 31 | alignItems: 'center', 32 | padding: theme.spacing(2, 2.5), 33 | borderRadius: theme.shape.borderRadiusSm, 34 | backgroundColor: theme.palette.grey[200] 35 | })); 36 | 37 | // ---------------------------------------------------------------------- 38 | 39 | DashboardSidebar.propTypes = { 40 | isOpenSidebar: PropTypes.bool, 41 | onCloseSidebar: PropTypes.func 42 | }; 43 | 44 | export default function DashboardSidebar({ isOpenSidebar, onCloseSidebar }) { 45 | const { pathname } = useLocation(); 46 | 47 | useEffect(() => { 48 | if (isOpenSidebar) { 49 | onCloseSidebar(); 50 | } 51 | // eslint-disable-next-line react-hooks/exhaustive-deps 52 | }, [pathname]); 53 | 54 | const renderContent = ( 55 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {CONFIGURATION.profile.name} 74 | 75 | 76 | Crypto Trader 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ); 90 | 91 | return ( 92 | 93 | 94 | 101 | {renderContent} 102 | 103 | 104 | 105 | 106 | 116 | {renderContent} 117 | 118 | 119 | 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /src/layouts/dashboard/SidebarConfig.js: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/react'; 2 | import pieChart2Fill from '@iconify/icons-eva/pie-chart-2-fill'; 3 | import fileTextFill from '@iconify/icons-eva/file-text-fill'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | const getIcon = (name) => ; 8 | 9 | const sidebarConfig = [ 10 | { 11 | title: 'dashboard', 12 | path: '/dashboard/app', 13 | icon: getIcon(pieChart2Fill) 14 | }, 15 | { 16 | title: 'Order history', 17 | path: '/dashboard/orderHistory', 18 | icon: getIcon(fileTextFill) 19 | } 20 | // { 21 | // title: 'login', 22 | // path: '/login', 23 | // icon: getIcon(lockFill) 24 | // }, 25 | // { 26 | // title: 'register', 27 | // path: '/register', 28 | // icon: getIcon(personAddFill) 29 | // }, 30 | // { 31 | // title: 'Not found', 32 | // path: '/404', 33 | // icon: getIcon(alertTriangleFill) 34 | // } 35 | ]; 36 | 37 | export default sidebarConfig; 38 | -------------------------------------------------------------------------------- /src/layouts/dashboard/Summary.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid } from '@mui/material'; 3 | import { sum, flatten } from 'lodash'; 4 | import { useRecoilValue } from 'recoil'; 5 | import { format } from 'date-fns'; 6 | 7 | import SummaryBox from '../../components/_dashboard/SummaryBox'; 8 | import { fCurrency, fNumber } from '../../utils/formatNumber'; 9 | import { tradesAtom, incomesAtom, accountAtom } from '../../recoil/atoms'; 10 | import { getPercentIncrease } from '../../utils/functions'; 11 | 12 | const Summary = () => { 13 | const trades = useRecoilValue(tradesAtom); 14 | const incomes = useRecoilValue(incomesAtom); 15 | const account = useRecoilValue(accountAtom); 16 | 17 | const today = format(new Date(), 'MM/dd/yyyy'); 18 | const incomeToday = incomes[today]; 19 | const tradesToday = trades[today]; 20 | 21 | const profitToday = sum(incomeToday?.map((inc) => JSON.parse(inc?.income))); 22 | const weekProfit = sum(flatten(Object.values(incomes)).map((inc) => JSON.parse(inc?.income))); 23 | 24 | const tradesNumber = {}; 25 | 26 | tradesToday?.forEach((trade) => { 27 | tradesNumber[trade.symbol] = (tradesNumber[trade.symbol] || 0) + 1; 28 | }); 29 | 30 | const sortedPerformersOfToday = Object.entries(tradesNumber).sort( 31 | (prev, next) => prev[1] - next[1] 32 | ); 33 | 34 | return ( 35 | <> 36 | 37 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 62 | 63 | 64 | 70 | 71 | 72 | 78 | 79 | 80 | 86 | 87 | 88 | ); 89 | }; 90 | 91 | export default Summary; 92 | -------------------------------------------------------------------------------- /src/layouts/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | import { Grid, Typography } from '@mui/material'; 4 | import { useRecoilValue } from 'recoil'; 5 | 6 | // material 7 | import { styled } from '@mui/material/styles'; 8 | // 9 | import DashboardNavbar from './DashboardNavbar'; 10 | import DashboardSidebar from './DashboardSidebar'; 11 | import { fDateTime } from '../../utils/formatTime'; 12 | import { updateTimeAtom } from '../../recoil/atoms'; 13 | 14 | // ---------------------------------------------------------------------- 15 | 16 | const APP_BAR_MOBILE = 16; 17 | const APP_BAR_DESKTOP = 8; 18 | 19 | const RootStyle = styled('div')({ 20 | display: 'flex', 21 | minHeight: '100%', 22 | overflow: 'hidden' 23 | }); 24 | 25 | const MainStyle = styled('div')(({ theme }) => ({ 26 | flexGrow: 1, 27 | overflow: 'auto', 28 | minHeight: '100%', 29 | paddingTop: APP_BAR_MOBILE + 24, 30 | paddingBottom: theme.spacing(10), 31 | [theme.breakpoints.up('lg')]: { 32 | paddingTop: APP_BAR_DESKTOP + 24, 33 | paddingLeft: theme.spacing(2), 34 | paddingRight: theme.spacing(2) 35 | } 36 | })); 37 | 38 | // ---------------------------------------------------------------------- 39 | 40 | export default function DashboardLayout() { 41 | const [open, setOpen] = useState(false); 42 | const updateTime = useRecoilValue(updateTimeAtom); 43 | 44 | return ( 45 | 46 | setOpen(true)} /> 47 | 48 | setOpen(false)} /> 49 | 50 | 51 | 52 | 53 | Last update: {fDateTime(updateTime)} 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/pages/DashboardApp.js: -------------------------------------------------------------------------------- 1 | // material 2 | import { Grid, Container } from '@mui/material'; 3 | 4 | // components 5 | import Page from '../components/Page'; 6 | import { 7 | // AppTasks, 8 | // AppNewsUpdate, 9 | LastOrders, 10 | PositionsRepartition, 11 | PerformanceOverview, 12 | // AppTrafficBySite, 13 | // AppCurrentSubject, 14 | TopPerformers 15 | } from '../components/_dashboard/app'; 16 | import Summary from '../layouts/dashboard/Summary'; 17 | 18 | // ---------------------------------------------------------------------- 19 | 20 | const DashboardApp = () => ( 21 | 22 | 23 | {/* 24 | Hi, Welcome back 25 | */} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {/* 46 | 47 | */} 48 | 49 | {/* 50 | 51 | */} 52 | 53 | {/* 54 | 55 | 56 | 57 | 58 | 59 | */} 60 | 61 | 62 | 63 | ); 64 | 65 | export default DashboardApp; 66 | -------------------------------------------------------------------------------- /src/pages/OrderHistory.js: -------------------------------------------------------------------------------- 1 | import { filter, orderBy, flatten } from 'lodash'; 2 | import { useState } from 'react'; 3 | import { useRecoilValue } from 'recoil'; 4 | 5 | // material 6 | import { 7 | Card, 8 | Table, 9 | Stack, 10 | // Avatar, 11 | // Button, 12 | // Checkbox, 13 | TableRow, 14 | TableBody, 15 | TableCell, 16 | Container, 17 | Typography, 18 | TableContainer, 19 | TablePagination 20 | } from '@mui/material'; 21 | // components 22 | import Page from '../components/Page'; 23 | import Label from '../components/Label'; 24 | import Scrollbar from '../components/Scrollbar'; 25 | import SearchNotFound from '../components/SearchNotFound'; 26 | import { OrderListHead, OrderListToolbar } from '../components/_dashboard/order'; 27 | // 28 | 29 | import { fDateTime } from '../utils/formatTime'; 30 | import { fCurrency } from '../utils/formatNumber'; 31 | import { tradesAtom } from '../recoil/atoms'; 32 | 33 | // ---------------------------------------------------------------------- 34 | 35 | const TABLE_HEAD = [ 36 | { id: 'symbol', label: 'Symbol' }, 37 | { id: 'side', label: 'Side' }, 38 | { id: 'time', label: 'Time' }, 39 | { id: 'pnl', label: 'Realized PnL' }, 40 | { id: 'price', label: 'Price' }, 41 | { id: 'quoteQty', label: 'Quote quantity' } 42 | ]; 43 | 44 | // ---------------------------------------------------------------------- 45 | 46 | function descendingComparator(a, b, orderedBy) { 47 | if (b[orderedBy] < a[orderedBy]) { 48 | return -1; 49 | } 50 | if (b[orderedBy] > a[orderedBy]) { 51 | return 1; 52 | } 53 | return 0; 54 | } 55 | 56 | function getComparator(order, orderedBy) { 57 | return order === 'desc' 58 | ? (a, b) => descendingComparator(a, b, orderedBy) 59 | : (a, b) => -descendingComparator(a, b, orderedBy); 60 | } 61 | 62 | function applySortFilter(array, comparator, query) { 63 | const stabilizedThis = array.map((el, index) => [el, index]); 64 | stabilizedThis.sort((a, b) => { 65 | const order = comparator(a[0], b[0]); 66 | if (order !== 0) return order; 67 | return a[1] - b[1]; 68 | }); 69 | if (query) { 70 | return filter( 71 | array, 72 | (_order) => _order.symbol.toLowerCase().indexOf(query.toLowerCase()) !== -1 73 | ); 74 | } 75 | return stabilizedThis.map((el) => el[0]); 76 | } 77 | 78 | const OrderHistory = () => { 79 | const trades = useRecoilValue(tradesAtom); 80 | 81 | const [page, setPage] = useState(0); 82 | const [order, setOrder] = useState('asc'); 83 | const [selected, setSelected] = useState([]); 84 | const [orderedBy, setOrderBy] = useState('name'); 85 | const [filterName, setFilterName] = useState(''); 86 | const [rowsPerPage, setRowsPerPage] = useState(25); 87 | 88 | const orderedTrades = orderBy(flatten(Object.values(trades)), ['time'], ['desc']); 89 | 90 | const handleRequestSort = (event, property) => { 91 | const isAsc = orderedBy === property && order === 'asc'; 92 | setOrder(isAsc ? 'desc' : 'asc'); 93 | setOrderBy(property); 94 | }; 95 | 96 | const handleSelectAllClick = (event) => { 97 | if (event.target.checked) { 98 | const newSelecteds = orderedTrades.map((n) => n.symbol); 99 | setSelected(newSelecteds); 100 | return; 101 | } 102 | setSelected([]); 103 | }; 104 | 105 | const handleChangePage = (event, newPage) => { 106 | setPage(newPage); 107 | }; 108 | 109 | const handleChangeRowsPerPage = (event) => { 110 | setRowsPerPage(parseInt(event.target.value, 10)); 111 | setPage(0); 112 | }; 113 | 114 | const handleFilterByName = (event) => { 115 | setFilterName(event.target.value); 116 | }; 117 | 118 | const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - orderedTrades.length) : 0; 119 | 120 | const filteredOrders = applySortFilter( 121 | orderedTrades, 122 | getComparator(order, orderedBy), 123 | filterName 124 | ); 125 | 126 | const isOrderNotFound = filteredOrders.length === 0; 127 | 128 | return ( 129 | 130 | 131 | 132 | 133 | Order history 134 | 135 | 136 | 137 | 138 | 143 | 144 | 145 | 146 | 147 | 156 | 157 | {filteredOrders 158 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 159 | .map((row) => { 160 | const { id, symbol, buyer, realizedPnl, time, quoteQty, price } = row; 161 | const isItemSelected = selected.indexOf(symbol) !== -1; 162 | 163 | return ( 164 | 172 | 173 | 174 | {symbol} 175 | 176 | 177 | 178 | 181 | 182 | {fDateTime(time)} 183 | {fCurrency(realizedPnl)} 184 | {price} 185 | {quoteQty} 186 | 187 | ); 188 | })} 189 | {emptyRows > 0 && ( 190 | 191 | 192 | 193 | )} 194 | 195 | {isOrderNotFound && ( 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | )} 204 |
205 |
206 |
207 | 208 | 217 |
218 |
219 |
220 | ); 221 | }; 222 | 223 | export default OrderHistory; 224 | -------------------------------------------------------------------------------- /src/pages/Page404.js: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { Link as RouterLink } from 'react-router-dom'; 3 | // material 4 | import { styled } from '@mui/material/styles'; 5 | import { Box, Button, Typography, Container } from '@mui/material'; 6 | // components 7 | import { MotionContainer, varBounceIn } from '../components/animate'; 8 | import Page from '../components/Page'; 9 | 10 | // ---------------------------------------------------------------------- 11 | 12 | const RootStyle = styled(Page)(({ theme }) => ({ 13 | display: 'flex', 14 | minHeight: '100%', 15 | alignItems: 'center', 16 | paddingTop: theme.spacing(15), 17 | paddingBottom: theme.spacing(10) 18 | })); 19 | 20 | // ---------------------------------------------------------------------- 21 | 22 | export default function Page404() { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | Sorry, page not found! 31 | 32 | 33 | 34 | Sorry, we couldn’t find the page you’re looking for. Perhaps you’ve mistyped the URL? 35 | Be sure to check your spelling. 36 | 37 | 38 | 39 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/recoil/atoms/accountAtom.js: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | const accountAtom = atom({ 4 | key: 'accountState', 5 | default: {} 6 | }); 7 | 8 | export default accountAtom; 9 | -------------------------------------------------------------------------------- /src/recoil/atoms/incomesAtom.js: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { generateLastNdates } from '../../utils/formatTime'; 3 | 4 | const generateEmptyIncomes = () => { 5 | const lastWeekDates = generateLastNdates(7, 'MM/dd/yyyy'); 6 | const emptyIncomes = {}; 7 | 8 | lastWeekDates.forEach((d) => { 9 | emptyIncomes[d] = []; 10 | }); 11 | 12 | return emptyIncomes; 13 | }; 14 | 15 | const incomesAtom = atom({ 16 | key: 'incomesState', 17 | default: generateEmptyIncomes() 18 | }); 19 | 20 | export default incomesAtom; 21 | -------------------------------------------------------------------------------- /src/recoil/atoms/index.js: -------------------------------------------------------------------------------- 1 | export { default as tradesAtom } from './tradesAtom'; 2 | export { default as incomesAtom } from './incomesAtom'; 3 | export { default as accountAtom } from './accountAtom'; 4 | export { default as updateTimeAtom } from './updateTimeAtom'; 5 | -------------------------------------------------------------------------------- /src/recoil/atoms/tradesAtom.js: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { generateLastNdates } from '../../utils/formatTime'; 3 | 4 | const generateEmptyTrades = () => { 5 | const lastWeekDates = generateLastNdates(7, 'MM/dd/yyyy'); 6 | const emptyTrades = {}; 7 | 8 | lastWeekDates.forEach((d) => { 9 | emptyTrades[d] = []; 10 | }); 11 | 12 | return emptyTrades; 13 | }; 14 | 15 | const tradesAtom = atom({ 16 | key: 'tradesState', 17 | default: generateEmptyTrades() 18 | }); 19 | 20 | export default tradesAtom; 21 | -------------------------------------------------------------------------------- /src/recoil/atoms/updateTimeAtom.js: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | const updateTimeAtom = atom({ 4 | key: 'updateTimeState', 5 | default: new Date() 6 | }); 7 | 8 | export default updateTimeAtom; 9 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import { Navigate, useRoutes } from 'react-router-dom'; 2 | 3 | // layouts 4 | import DashboardLayout from './layouts/dashboard'; 5 | import LogoOnlyLayout from './layouts/LogoOnlyLayout'; 6 | import DashboardApp from './pages/DashboardApp'; 7 | import OrderHistory from './pages/OrderHistory'; 8 | import NotFound from './pages/Page404'; 9 | 10 | // ---------------------------------------------------------------------- 11 | 12 | export default function Router() { 13 | return useRoutes([ 14 | { 15 | path: '/dashboard', 16 | element: , 17 | children: [ 18 | { element: }, 19 | { 20 | path: 'app', 21 | element: 22 | }, 23 | { path: 'orderHistory', element: } 24 | ] 25 | }, 26 | { 27 | path: '/', 28 | element: , 29 | children: [ 30 | { path: '404', element: }, 31 | { path: '/', element: }, 32 | { path: '*', element: } 33 | ] 34 | }, 35 | { path: '*', element: } 36 | ]); 37 | } 38 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* @ts-nocheck */ 3 | 4 | // This optional code is used to register a service worker. 5 | // register() is not called by default. 6 | 7 | // This lets the app load faster on subsequent visits in production, and gives 8 | // it offline capabilities. However, it also means that developers (and users) 9 | // will only see deployed updates on subsequent visits to a page, after all the 10 | // existing tabs open on the page have been closed, since previously cached 11 | // resources are updated in the background. 12 | 13 | // To learn more about the benefits of this model and instructions on how to 14 | // opt-in, read https://bit.ly/CRA-PWA 15 | 16 | const isLocalhost = Boolean( 17 | window.location.hostname === 'localhost' || 18 | // [::1] is the IPv6 localhost address. 19 | window.location.hostname === '[::1]' || 20 | // 127.0.0.0/8 are considered localhost for IPv4. 21 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 22 | ); 23 | 24 | export function register(config) { 25 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 26 | // The URL constructor is available in all browsers that support SW. 27 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 28 | if (publicUrl.origin !== window.location.origin) { 29 | // Our service worker won't work if PUBLIC_URL is on a different origin 30 | // from what our page is served on. This might happen if a CDN is used to 31 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 32 | return; 33 | } 34 | 35 | window.addEventListener('load', () => { 36 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 37 | 38 | if (isLocalhost) { 39 | // This is running on localhost. Let's check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl, config); 41 | 42 | // Add some additional logging to localhost, pointing developers to the 43 | // service worker/PWA documentation. 44 | navigator.serviceWorker.ready.then(() => { 45 | console.log( 46 | 'This web app is being served cache-first by a service ' + 47 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 48 | ); 49 | }); 50 | } else { 51 | // Is not localhost. Just register service worker 52 | registerValidSW(swUrl, config); 53 | } 54 | }); 55 | } 56 | } 57 | 58 | function registerValidSW(swUrl, config) { 59 | navigator.serviceWorker 60 | .register(swUrl) 61 | .then((registration) => { 62 | registration.onupdatefound = () => { 63 | const installingWorker = registration.installing; 64 | if (installingWorker == null) { 65 | return; 66 | } 67 | installingWorker.onstatechange = () => { 68 | if (installingWorker.state === 'installed') { 69 | if (navigator.serviceWorker.controller) { 70 | // At this point, the updated precached content has been fetched, 71 | // but the previous service worker will still serve the older 72 | // content until all client tabs are closed. 73 | console.log( 74 | 'New content is available and will be used when all ' + 75 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 76 | ); 77 | 78 | // Execute callback 79 | if (config && config.onUpdate) { 80 | config.onUpdate(registration); 81 | } 82 | } else { 83 | // At this point, everything has been precached. 84 | // It's the perfect time to display a 85 | // "Content is cached for offline use." message. 86 | console.log('Content is cached for offline use.'); 87 | 88 | // Execute callback 89 | if (config && config.onSuccess) { 90 | config.onSuccess(registration); 91 | } 92 | } 93 | } 94 | }; 95 | }; 96 | }) 97 | .catch((error) => { 98 | console.error('Error during service worker registration:', error); 99 | }); 100 | } 101 | 102 | function checkValidServiceWorker(swUrl, config) { 103 | // Check if the service worker can be found. If it can't reload the page. 104 | fetch(swUrl, { 105 | headers: { 'Service-Worker': 'script' } 106 | }) 107 | .then((response) => { 108 | // Ensure service worker exists, and that we really are getting a JS file. 109 | const contentType = response.headers.get('content-type'); 110 | if ( 111 | response.status === 404 || 112 | (contentType != null && contentType.indexOf('javascript') === -1) 113 | ) { 114 | // No service worker found. Probably a different app. Reload the page. 115 | navigator.serviceWorker.ready.then((registration) => { 116 | registration.unregister().then(() => { 117 | window.location.reload(); 118 | }); 119 | }); 120 | } else { 121 | // Service worker found. Proceed as normal. 122 | registerValidSW(swUrl, config); 123 | } 124 | }) 125 | .catch(() => { 126 | console.log('No internet connection found. App is running in offline mode.'); 127 | }); 128 | } 129 | 130 | export function unregister() { 131 | if ('serviceWorker' in navigator) { 132 | navigator.serviceWorker.ready 133 | .then((registration) => { 134 | registration.unregister(); 135 | }) 136 | .catch((error) => { 137 | console.error(error.message); 138 | }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/services/accountServices.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { BINANCE_HEADERS, getSignature, getUrl } from './utils'; 4 | 5 | export const getUserAccount = async () => { 6 | const timestamp = Date.now(); 7 | 8 | const params = { 9 | timestamp, 10 | signature: getSignature(`timestamp=${timestamp}`) 11 | }; 12 | 13 | const response = await axios.request({ 14 | url: getUrl('account'), 15 | method: 'get', 16 | params, 17 | headers: BINANCE_HEADERS 18 | }); 19 | 20 | return response.data; 21 | }; 22 | -------------------------------------------------------------------------------- /src/services/incomeServices.js: -------------------------------------------------------------------------------- 1 | import { isEmpty, flatten } from 'lodash'; 2 | import { format } from 'date-fns'; 3 | import axios from 'axios'; 4 | 5 | import { BINANCE_HEADERS, getSignature, getUrl } from './utils'; 6 | import { generateLastNdates } from '../utils/formatTime'; 7 | 8 | const ONE_DAY_TIMESTAMP = 60 * 60 * 24 * 1000; 9 | const CHUNK_NUMBER = 3; 10 | 11 | const generateNChunksRequest = (date) => { 12 | if (isEmpty(date)) return []; 13 | 14 | const timestamp = Date.now(); 15 | const limit = 1000; 16 | const incomeType = 'REALIZED_PNL'; 17 | 18 | const dateRequested = new Date(date).getTime(); 19 | const requests = []; 20 | 21 | for (let i = 1; i <= CHUNK_NUMBER; i += 1) { 22 | const chunkTime = Math.round(ONE_DAY_TIMESTAMP / CHUNK_NUMBER); 23 | 24 | const startTime = dateRequested + (i - 1) * chunkTime; 25 | const endTime = startTime + chunkTime; 26 | 27 | if (startTime <= new Date().getTime()) { 28 | // Add the request only if the startTime is in the past 29 | 30 | const params = { 31 | incomeType, 32 | startTime, 33 | endTime, 34 | limit, 35 | timestamp, 36 | signature: getSignature( 37 | `incomeType=${incomeType}&startTime=${startTime}&endTime=${endTime}&limit=${limit}×tamp=${timestamp}` 38 | ) 39 | }; 40 | 41 | requests.push( 42 | axios.request({ 43 | url: getUrl('income'), 44 | method: 'get', 45 | params, 46 | headers: BINANCE_HEADERS 47 | }) 48 | ); 49 | } 50 | } 51 | 52 | return requests; 53 | }; 54 | 55 | export const getUserIncomesByDate = async (date) => { 56 | if (isEmpty(date)) return []; 57 | 58 | const response = await axios 59 | .all(generateNChunksRequest(date)) 60 | .then(axios.spread((...responses) => flatten(responses.map((res) => res.data)))) 61 | .catch((errors) => { 62 | console.error('Something went wrong when fetching getUserIncomesByDate, error: ', errors); 63 | }); 64 | 65 | return response; 66 | }; 67 | 68 | export const getUserIncomesOfTheDay = () => getUserIncomesByDate(format(new Date(), 'MM/dd/yyyy')); 69 | 70 | export const getIncomesOfTheWeek = async () => { 71 | const lastWeekDates = generateLastNdates(7, 'MM/dd/yyyy'); 72 | const incomes = {}; 73 | 74 | await axios 75 | .all(lastWeekDates.map((day) => getUserIncomesByDate(day))) 76 | .then( 77 | axios.spread((...responses) => { 78 | lastWeekDates.forEach((day, index) => { 79 | incomes[day] = responses[index]; 80 | }); 81 | }) 82 | ) 83 | .catch((errors) => { 84 | console.error('Something went wrong when fetching all the weekly incomes, error: ', errors); 85 | }); 86 | 87 | return incomes; 88 | }; 89 | -------------------------------------------------------------------------------- /src/services/tradesServices.js: -------------------------------------------------------------------------------- 1 | import { isEmpty, flatten } from 'lodash'; 2 | import { format } from 'date-fns'; 3 | import axios from 'axios'; 4 | 5 | import { BINANCE_HEADERS, getSignature, getUrl } from './utils'; 6 | import { generateLastNdates } from '../utils/formatTime'; 7 | 8 | const ONE_DAY_TIMESTAMP = 60 * 60 * 24 * 1000; 9 | const CHUNK_NUMBER = 8; 10 | 11 | const generateNChunksRequest = (date) => { 12 | if (isEmpty(date)) return []; 13 | 14 | const timestamp = Date.now(); 15 | const limit = 1000; 16 | 17 | const dateRequested = new Date(date).getTime(); 18 | const requests = []; 19 | 20 | for (let i = 1; i <= CHUNK_NUMBER; i += 1) { 21 | const chunkTime = Math.round(ONE_DAY_TIMESTAMP / CHUNK_NUMBER); 22 | 23 | const startTime = dateRequested + (i - 1) * chunkTime; 24 | const endTime = startTime + chunkTime; 25 | 26 | if (startTime <= new Date().getTime()) { 27 | // Add the request only if the startTime is in the past 28 | 29 | const params = { 30 | startTime, 31 | endTime, 32 | limit, 33 | timestamp, 34 | signature: getSignature( 35 | `startTime=${startTime}&endTime=${endTime}&limit=${limit}×tamp=${timestamp}` 36 | ) 37 | }; 38 | 39 | requests.push( 40 | axios.request({ 41 | url: getUrl('trades'), 42 | method: 'get', 43 | params, 44 | headers: BINANCE_HEADERS 45 | }) 46 | ); 47 | } 48 | } 49 | 50 | return requests; 51 | }; 52 | 53 | export const getUserTradesByDate = async (date) => { 54 | if (isEmpty(date)) return []; 55 | 56 | const response = await axios 57 | .all(generateNChunksRequest(date)) 58 | .then(axios.spread((...responses) => flatten(responses.map((res) => res.data)))) 59 | .catch((errors) => { 60 | console.error('Something went wrong when fetching getUserTradesByDate, error: ', errors); 61 | }); 62 | 63 | return response; 64 | }; 65 | 66 | export const getUserTradesOfTheDay = () => getUserTradesByDate(format(new Date(), 'MM/dd/yyyy')); 67 | 68 | export const getTradesOfTheWeek = async () => { 69 | const lastWeekDates = generateLastNdates(7, 'MM/dd/yyyy'); 70 | const trades = {}; 71 | 72 | await axios 73 | .all(lastWeekDates.map((day) => getUserTradesByDate(day))) 74 | .then( 75 | axios.spread((...responses) => { 76 | lastWeekDates.forEach((day, index) => { 77 | trades[day] = responses[index]; 78 | }); 79 | }) 80 | ) 81 | .catch((errors) => { 82 | console.error('Something went wrong when fetching all the weekly trades, error: ', errors); 83 | }); 84 | 85 | return trades; 86 | }; 87 | -------------------------------------------------------------------------------- /src/services/utils.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | import { CONFIGURATION } from '../config/api-keys'; 4 | 5 | const BINANCE_URLS = { 6 | spot: { 7 | trades: 'https://api.binance.com/api/v3/myTrades', 8 | income: 'https://api.binance.com/api/v3/account', 9 | account: 'https://api.binance.com/api/v3/account' 10 | }, 11 | futures: { 12 | trades: 'https://fapi.binance.com/fapi/v1/userTrades', 13 | income: 'https://fapi.binance.com/fapi/v1/income', 14 | account: 'https://fapi.binance.com/fapi/v2/account' 15 | } 16 | }; 17 | 18 | export const BINANCE_HEADERS = { 19 | Accept: 'Application/json', 20 | 'X-MBX-APIKEY': CONFIGURATION.binance.key 21 | }; 22 | 23 | export const getSignature = (queryParams) => 24 | crypto.createHmac('sha256', CONFIGURATION.binance.secret).update(queryParams).digest('hex'); 25 | 26 | export const getUrl = (type) => BINANCE_URLS[CONFIGURATION.binance.exchangeType][type]; 27 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 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 '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/theme/breakpoints.js: -------------------------------------------------------------------------------- 1 | const breakpoints = { 2 | values: { 3 | xs: 0, 4 | sm: 600, 5 | md: 900, 6 | lg: 1200, 7 | xl: 1536 8 | } 9 | }; 10 | 11 | export default breakpoints; 12 | -------------------------------------------------------------------------------- /src/theme/globalStyles.js: -------------------------------------------------------------------------------- 1 | // material 2 | import { useTheme } from '@mui/material/styles'; 3 | import { GlobalStyles as GlobalThemeStyles } from '@mui/material'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | export default function GlobalStyles() { 8 | const theme = useTheme(); 9 | 10 | return ( 11 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/theme/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { useMemo } from 'react'; 3 | // material 4 | import { CssBaseline } from '@mui/material'; 5 | import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles'; 6 | // 7 | import shape from './shape'; 8 | import palette from './palette'; 9 | import typography from './typography'; 10 | import componentsOverride from './overrides'; 11 | import shadows, { customShadows } from './shadows'; 12 | 13 | // ---------------------------------------------------------------------- 14 | 15 | ThemeConfig.propTypes = { 16 | children: PropTypes.node 17 | }; 18 | 19 | export default function ThemeConfig({ children }) { 20 | const themeOptions = useMemo( 21 | () => ({ 22 | palette, 23 | shape, 24 | typography, 25 | shadows, 26 | customShadows 27 | }), 28 | [] 29 | ); 30 | 31 | const theme = createTheme(themeOptions); 32 | theme.components = componentsOverride(theme); 33 | 34 | return ( 35 | 36 | 37 | 38 | {children} 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/theme/overrides/Autocomplete.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Autocomplete(theme) { 4 | return { 5 | MuiAutocomplete: { 6 | styleOverrides: { 7 | paper: { 8 | boxShadow: theme.customShadows.z20 9 | } 10 | } 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/theme/overrides/Backdrop.js: -------------------------------------------------------------------------------- 1 | import { alpha } from '@mui/material/styles'; 2 | 3 | // ---------------------------------------------------------------------- 4 | 5 | export default function Backdrop(theme) { 6 | const varLow = alpha(theme.palette.grey[900], 0.48); 7 | const varHigh = alpha(theme.palette.grey[900], 1); 8 | 9 | return { 10 | MuiBackdrop: { 11 | styleOverrides: { 12 | root: { 13 | background: [ 14 | `rgb(22,28,36)`, 15 | `-moz-linear-gradient(75deg, ${varLow} 0%, ${varHigh} 100%)`, 16 | `-webkit-linear-gradient(75deg, ${varLow} 0%, ${varHigh} 100%)`, 17 | `linear-gradient(75deg, ${varLow} 0%, ${varHigh} 100%)` 18 | ], 19 | '&.MuiBackdrop-invisible': { 20 | background: 'transparent' 21 | } 22 | } 23 | } 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/theme/overrides/Button.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Button(theme) { 4 | return { 5 | MuiButton: { 6 | styleOverrides: { 7 | root: { 8 | '&:hover': { 9 | boxShadow: 'none' 10 | } 11 | }, 12 | sizeLarge: { 13 | height: 48 14 | }, 15 | containedInherit: { 16 | color: theme.palette.grey[800], 17 | boxShadow: theme.customShadows.z8, 18 | '&:hover': { 19 | backgroundColor: theme.palette.grey[400] 20 | } 21 | }, 22 | containedPrimary: { 23 | boxShadow: theme.customShadows.primary 24 | }, 25 | containedSecondary: { 26 | boxShadow: theme.customShadows.secondary 27 | }, 28 | outlinedInherit: { 29 | border: `1px solid ${theme.palette.grey[500_32]}`, 30 | '&:hover': { 31 | backgroundColor: theme.palette.action.hover 32 | } 33 | }, 34 | textInherit: { 35 | '&:hover': { 36 | backgroundColor: theme.palette.action.hover 37 | } 38 | } 39 | } 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/theme/overrides/Card.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Card(theme) { 4 | return { 5 | MuiCard: { 6 | styleOverrides: { 7 | root: { 8 | boxShadow: theme.customShadows.z16, 9 | borderRadius: theme.shape.borderRadiusMd, 10 | position: 'relative', 11 | zIndex: 0 // Fix Safari overflow: hidden with border radius 12 | } 13 | } 14 | }, 15 | MuiCardHeader: { 16 | defaultProps: { 17 | titleTypographyProps: { variant: 'h6' }, 18 | subheaderTypographyProps: { variant: 'body2' } 19 | }, 20 | styleOverrides: { 21 | root: { 22 | padding: theme.spacing(3, 3, 0) 23 | } 24 | } 25 | }, 26 | MuiCardContent: { 27 | styleOverrides: { 28 | root: { 29 | padding: theme.spacing(3) 30 | } 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/theme/overrides/IconButton.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function IconButton(theme) { 4 | return { 5 | MuiIconButton: { 6 | variants: [ 7 | { 8 | props: { color: 'default' }, 9 | style: { 10 | '&:hover': { backgroundColor: theme.palette.action.hover } 11 | } 12 | }, 13 | { 14 | props: { color: 'inherit' }, 15 | style: { 16 | '&:hover': { backgroundColor: theme.palette.action.hover } 17 | } 18 | } 19 | ], 20 | 21 | styleOverrides: { 22 | root: {} 23 | } 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/theme/overrides/Input.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Input(theme) { 4 | return { 5 | MuiInputBase: { 6 | styleOverrides: { 7 | root: { 8 | '&.Mui-disabled': { 9 | '& svg': { color: theme.palette.text.disabled } 10 | } 11 | }, 12 | input: { 13 | '&::placeholder': { 14 | opacity: 1, 15 | color: theme.palette.text.disabled 16 | } 17 | } 18 | } 19 | }, 20 | MuiInput: { 21 | styleOverrides: { 22 | underline: { 23 | '&:before': { 24 | borderBottomColor: theme.palette.grey[500_56] 25 | } 26 | } 27 | } 28 | }, 29 | MuiFilledInput: { 30 | styleOverrides: { 31 | root: { 32 | backgroundColor: theme.palette.grey[500_12], 33 | '&:hover': { 34 | backgroundColor: theme.palette.grey[500_16] 35 | }, 36 | '&.Mui-focused': { 37 | backgroundColor: theme.palette.action.focus 38 | }, 39 | '&.Mui-disabled': { 40 | backgroundColor: theme.palette.action.disabledBackground 41 | } 42 | }, 43 | underline: { 44 | '&:before': { 45 | borderBottomColor: theme.palette.grey[500_56] 46 | } 47 | } 48 | } 49 | }, 50 | MuiOutlinedInput: { 51 | styleOverrides: { 52 | root: { 53 | '& .MuiOutlinedInput-notchedOutline': { 54 | borderColor: theme.palette.grey[500_32] 55 | }, 56 | '&.Mui-disabled': { 57 | '& .MuiOutlinedInput-notchedOutline': { 58 | borderColor: theme.palette.action.disabledBackground 59 | } 60 | } 61 | } 62 | } 63 | } 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/theme/overrides/Lists.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Lists(theme) { 4 | return { 5 | MuiListItemIcon: { 6 | styleOverrides: { 7 | root: { 8 | color: 'inherit', 9 | minWidth: 'auto', 10 | marginRight: theme.spacing(2) 11 | } 12 | } 13 | }, 14 | MuiListItemAvatar: { 15 | styleOverrides: { 16 | root: { 17 | minWidth: 'auto', 18 | marginRight: theme.spacing(2) 19 | } 20 | } 21 | }, 22 | MuiListItemText: { 23 | styleOverrides: { 24 | root: { 25 | marginTop: 0, 26 | marginBottom: 0 27 | }, 28 | multiline: { 29 | marginTop: 0, 30 | marginBottom: 0 31 | } 32 | } 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/theme/overrides/Paper.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Paper() { 4 | return { 5 | MuiPaper: { 6 | defaultProps: { 7 | elevation: 0 8 | }, 9 | 10 | styleOverrides: { 11 | root: { 12 | backgroundImage: 'none' 13 | } 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/theme/overrides/Tooltip.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Tooltip(theme) { 4 | return { 5 | MuiTooltip: { 6 | styleOverrides: { 7 | tooltip: { 8 | backgroundColor: theme.palette.grey[800] 9 | }, 10 | arrow: { 11 | color: theme.palette.grey[800] 12 | } 13 | } 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/theme/overrides/Typography.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Typography(theme) { 4 | return { 5 | MuiTypography: { 6 | styleOverrides: { 7 | paragraph: { 8 | marginBottom: theme.spacing(2) 9 | }, 10 | gutterBottom: { 11 | marginBottom: theme.spacing(1) 12 | } 13 | } 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/theme/overrides/index.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash'; 2 | import Card from './Card'; 3 | import Lists from './Lists'; 4 | import Paper from './Paper'; 5 | import Input from './Input'; 6 | import Button from './Button'; 7 | import Tooltip from './Tooltip'; 8 | import Backdrop from './Backdrop'; 9 | import Typography from './Typography'; 10 | import IconButton from './IconButton'; 11 | import Autocomplete from './Autocomplete'; 12 | 13 | // ---------------------------------------------------------------------- 14 | 15 | export default function ComponentsOverrides(theme) { 16 | return merge( 17 | Card(theme), 18 | Lists(theme), 19 | Paper(theme), 20 | Input(theme), 21 | Button(theme), 22 | Tooltip(theme), 23 | Backdrop(theme), 24 | Typography(theme), 25 | IconButton(theme), 26 | Autocomplete(theme) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/theme/palette.js: -------------------------------------------------------------------------------- 1 | import { alpha } from '@mui/material/styles'; 2 | 3 | // ---------------------------------------------------------------------- 4 | 5 | function createGradient(color1, color2) { 6 | return `linear-gradient(to bottom, ${color1}, ${color2})`; 7 | } 8 | 9 | // SETUP COLORS 10 | const GREY = { 11 | 0: '#FFFFFF', 12 | 100: '#F9FAFB', 13 | 200: '#F4F6F8', 14 | 300: '#DFE3E8', 15 | 400: '#C4CDD5', 16 | 500: '#919EAB', 17 | 600: '#637381', 18 | 700: '#454F5B', 19 | 800: '#212B36', 20 | 900: '#161C24', 21 | 500_8: alpha('#919EAB', 0.08), 22 | 500_12: alpha('#919EAB', 0.12), 23 | 500_16: alpha('#919EAB', 0.16), 24 | 500_24: alpha('#919EAB', 0.24), 25 | 500_32: alpha('#919EAB', 0.32), 26 | 500_48: alpha('#919EAB', 0.48), 27 | 500_56: alpha('#919EAB', 0.56), 28 | 500_80: alpha('#919EAB', 0.8) 29 | }; 30 | 31 | const PRIMARY = { 32 | lighter: '#C8FACD', 33 | light: '#5BE584', 34 | main: '#00AB55', 35 | dark: '#007B55', 36 | darker: '#005249', 37 | contrastText: '#fff' 38 | }; 39 | const SECONDARY = { 40 | lighter: '#D6E4FF', 41 | light: '#84A9FF', 42 | main: '#3366FF', 43 | dark: '#1939B7', 44 | darker: '#091A7A', 45 | contrastText: '#fff' 46 | }; 47 | const INFO = { 48 | lighter: '#D0F2FF', 49 | light: '#74CAFF', 50 | main: '#1890FF', 51 | dark: '#0C53B7', 52 | darker: '#04297A', 53 | contrastText: '#fff' 54 | }; 55 | const SUCCESS = { 56 | lighter: '#E9FCD4', 57 | light: '#AAF27F', 58 | main: '#54D62C', 59 | dark: '#229A16', 60 | darker: '#08660D', 61 | contrastText: GREY[800] 62 | }; 63 | const WARNING = { 64 | lighter: '#FFF7CD', 65 | light: '#FFE16A', 66 | main: '#FFC107', 67 | dark: '#B78103', 68 | darker: '#7A4F01', 69 | contrastText: GREY[800] 70 | }; 71 | const ERROR = { 72 | lighter: '#FFE7D9', 73 | light: '#FFA48D', 74 | main: '#FF4842', 75 | dark: '#B72136', 76 | darker: '#7A0C2E', 77 | contrastText: '#fff' 78 | }; 79 | 80 | const GRADIENTS = { 81 | primary: createGradient(PRIMARY.light, PRIMARY.main), 82 | info: createGradient(INFO.light, INFO.main), 83 | success: createGradient(SUCCESS.light, SUCCESS.main), 84 | warning: createGradient(WARNING.light, WARNING.main), 85 | error: createGradient(ERROR.light, ERROR.main) 86 | }; 87 | 88 | const CHART_COLORS = { 89 | violet: ['#826AF9', '#9E86FF', '#D0AEFF', '#F7D2FF'], 90 | blue: ['#2D99FF', '#83CFFF', '#A5F3FF', '#CCFAFF'], 91 | green: ['#2CD9C5', '#60F1C8', '#A4F7CC', '#C0F2DC'], 92 | yellow: ['#FFE700', '#FFEF5A', '#FFF7AE', '#FFF3D6'], 93 | red: ['#FF6C40', '#FF8F6D', '#FFBD98', '#FFF2D4'] 94 | }; 95 | 96 | const palette = { 97 | common: { black: '#000', white: '#fff' }, 98 | primary: { ...PRIMARY }, 99 | secondary: { ...SECONDARY }, 100 | info: { ...INFO }, 101 | success: { ...SUCCESS }, 102 | warning: { ...WARNING }, 103 | error: { ...ERROR }, 104 | grey: GREY, 105 | gradients: GRADIENTS, 106 | chart: CHART_COLORS, 107 | divider: GREY[500_24], 108 | text: { primary: GREY[800], secondary: GREY[600], disabled: GREY[500] }, 109 | background: { paper: '#fff', default: '#fff', neutral: GREY[200] }, 110 | action: { 111 | active: GREY[600], 112 | hover: GREY[500_8], 113 | selected: GREY[500_16], 114 | disabled: GREY[500_80], 115 | disabledBackground: GREY[500_24], 116 | focus: GREY[500_24], 117 | hoverOpacity: 0.08, 118 | disabledOpacity: 0.48 119 | } 120 | }; 121 | 122 | export default palette; 123 | -------------------------------------------------------------------------------- /src/theme/shadows.js: -------------------------------------------------------------------------------- 1 | // material 2 | import { alpha } from '@mui/material/styles'; 3 | import palette from './palette'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | const LIGHT_MODE = palette.grey[500]; 8 | 9 | const createShadow = (color) => { 10 | const transparent1 = alpha(color, 0.2); 11 | const transparent2 = alpha(color, 0.14); 12 | const transparent3 = alpha(color, 0.12); 13 | return [ 14 | 'none', 15 | `0px 2px 1px -1px ${transparent1},0px 1px 1px 0px ${transparent2},0px 1px 3px 0px ${transparent3}`, 16 | `0px 3px 1px -2px ${transparent1},0px 2px 2px 0px ${transparent2},0px 1px 5px 0px ${transparent3}`, 17 | `0px 3px 3px -2px ${transparent1},0px 3px 4px 0px ${transparent2},0px 1px 8px 0px ${transparent3}`, 18 | `0px 2px 4px -1px ${transparent1},0px 4px 5px 0px ${transparent2},0px 1px 10px 0px ${transparent3}`, 19 | `0px 3px 5px -1px ${transparent1},0px 5px 8px 0px ${transparent2},0px 1px 14px 0px ${transparent3}`, 20 | `0px 3px 5px -1px ${transparent1},0px 6px 10px 0px ${transparent2},0px 1px 18px 0px ${transparent3}`, 21 | `0px 4px 5px -2px ${transparent1},0px 7px 10px 1px ${transparent2},0px 2px 16px 1px ${transparent3}`, 22 | `0px 5px 5px -3px ${transparent1},0px 8px 10px 1px ${transparent2},0px 3px 14px 2px ${transparent3}`, 23 | `0px 5px 6px -3px ${transparent1},0px 9px 12px 1px ${transparent2},0px 3px 16px 2px ${transparent3}`, 24 | `0px 6px 6px -3px ${transparent1},0px 10px 14px 1px ${transparent2},0px 4px 18px 3px ${transparent3}`, 25 | `0px 6px 7px -4px ${transparent1},0px 11px 15px 1px ${transparent2},0px 4px 20px 3px ${transparent3}`, 26 | `0px 7px 8px -4px ${transparent1},0px 12px 17px 2px ${transparent2},0px 5px 22px 4px ${transparent3}`, 27 | `0px 7px 8px -4px ${transparent1},0px 13px 19px 2px ${transparent2},0px 5px 24px 4px ${transparent3}`, 28 | `0px 7px 9px -4px ${transparent1},0px 14px 21px 2px ${transparent2},0px 5px 26px 4px ${transparent3}`, 29 | `0px 8px 9px -5px ${transparent1},0px 15px 22px 2px ${transparent2},0px 6px 28px 5px ${transparent3}`, 30 | `0px 8px 10px -5px ${transparent1},0px 16px 24px 2px ${transparent2},0px 6px 30px 5px ${transparent3}`, 31 | `0px 8px 11px -5px ${transparent1},0px 17px 26px 2px ${transparent2},0px 6px 32px 5px ${transparent3}`, 32 | `0px 9px 11px -5px ${transparent1},0px 18px 28px 2px ${transparent2},0px 7px 34px 6px ${transparent3}`, 33 | `0px 9px 12px -6px ${transparent1},0px 19px 29px 2px ${transparent2},0px 7px 36px 6px ${transparent3}`, 34 | `0px 10px 13px -6px ${transparent1},0px 20px 31px 3px ${transparent2},0px 8px 38px 7px ${transparent3}`, 35 | `0px 10px 13px -6px ${transparent1},0px 21px 33px 3px ${transparent2},0px 8px 40px 7px ${transparent3}`, 36 | `0px 10px 14px -6px ${transparent1},0px 22px 35px 3px ${transparent2},0px 8px 42px 7px ${transparent3}`, 37 | `0px 11px 14px -7px ${transparent1},0px 23px 36px 3px ${transparent2},0px 9px 44px 8px ${transparent3}`, 38 | `0px 11px 15px -7px ${transparent1},0px 24px 38px 3px ${transparent2},0px 9px 46px 8px ${transparent3}` 39 | ]; 40 | }; 41 | 42 | const createCustomShadow = (color) => { 43 | const transparent = alpha(color, 0.24); 44 | 45 | return { 46 | z1: `0 1px 2px 0 ${transparent}`, 47 | z8: `0 8px 16px 0 ${transparent}`, 48 | z12: `0 0 2px 0 ${transparent}, 0 12px 24px 0 ${transparent}`, 49 | z16: `0 0 2px 0 ${transparent}, 0 16px 32px -4px ${transparent}`, 50 | z20: `0 0 2px 0 ${transparent}, 0 20px 40px -4px ${transparent}`, 51 | z24: `0 0 4px 0 ${transparent}, 0 24px 48px 0 ${transparent}`, 52 | primary: `0 8px 16px 0 ${alpha(palette.primary.main, 0.24)}`, 53 | secondary: `0 8px 16px 0 ${alpha(palette.secondary.main, 0.24)}`, 54 | info: `0 8px 16px 0 ${alpha(palette.info.main, 0.24)}`, 55 | success: `0 8px 16px 0 ${alpha(palette.success.main, 0.24)}`, 56 | warning: `0 8px 16px 0 ${alpha(palette.warning.main, 0.24)}`, 57 | error: `0 8px 16px 0 ${alpha(palette.error.main, 0.24)}` 58 | }; 59 | }; 60 | 61 | export const customShadows = createCustomShadow(LIGHT_MODE); 62 | 63 | const shadows = createShadow(LIGHT_MODE); 64 | 65 | export default shadows; 66 | -------------------------------------------------------------------------------- /src/theme/shape.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | const shape = { 4 | borderRadius: 8, 5 | borderRadiusSm: 12, 6 | borderRadiusMd: 16 7 | }; 8 | 9 | export default shape; 10 | -------------------------------------------------------------------------------- /src/theme/typography.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | function pxToRem(value) { 4 | return `${value / 16}rem`; 5 | } 6 | 7 | function responsiveFontSizes({ sm, md, lg }) { 8 | return { 9 | '@media (min-width:600px)': { 10 | fontSize: pxToRem(sm) 11 | }, 12 | '@media (min-width:900px)': { 13 | fontSize: pxToRem(md) 14 | }, 15 | '@media (min-width:1200px)': { 16 | fontSize: pxToRem(lg) 17 | } 18 | }; 19 | } 20 | 21 | const FONT_PRIMARY = 'Public Sans, sans-serif'; 22 | 23 | const typography = { 24 | fontFamily: FONT_PRIMARY, 25 | fontWeightRegular: 400, 26 | fontWeightMedium: 600, 27 | fontWeightBold: 700, 28 | h1: { 29 | fontWeight: 700, 30 | lineHeight: 80 / 64, 31 | fontSize: pxToRem(40), 32 | ...responsiveFontSizes({ sm: 52, md: 58, lg: 64 }) 33 | }, 34 | h2: { 35 | fontWeight: 700, 36 | lineHeight: 64 / 48, 37 | fontSize: pxToRem(32), 38 | ...responsiveFontSizes({ sm: 40, md: 44, lg: 48 }) 39 | }, 40 | h3: { 41 | fontWeight: 700, 42 | lineHeight: 1.5, 43 | fontSize: pxToRem(24), 44 | ...responsiveFontSizes({ sm: 26, md: 30, lg: 32 }) 45 | }, 46 | h4: { 47 | fontWeight: 700, 48 | lineHeight: 1.5, 49 | fontSize: pxToRem(20), 50 | ...responsiveFontSizes({ sm: 20, md: 24, lg: 24 }) 51 | }, 52 | h5: { 53 | fontWeight: 700, 54 | lineHeight: 1.5, 55 | fontSize: pxToRem(18), 56 | ...responsiveFontSizes({ sm: 19, md: 20, lg: 20 }) 57 | }, 58 | h6: { 59 | fontWeight: 700, 60 | lineHeight: 28 / 18, 61 | fontSize: pxToRem(17), 62 | ...responsiveFontSizes({ sm: 18, md: 18, lg: 18 }) 63 | }, 64 | subtitle1: { 65 | fontWeight: 600, 66 | lineHeight: 1.5, 67 | fontSize: pxToRem(16) 68 | }, 69 | subtitle2: { 70 | fontWeight: 600, 71 | lineHeight: 22 / 14, 72 | fontSize: pxToRem(14) 73 | }, 74 | body1: { 75 | lineHeight: 1.5, 76 | fontSize: pxToRem(16) 77 | }, 78 | body2: { 79 | lineHeight: 22 / 14, 80 | fontSize: pxToRem(14) 81 | }, 82 | caption: { 83 | lineHeight: 1.5, 84 | fontSize: pxToRem(12) 85 | }, 86 | overline: { 87 | fontWeight: 700, 88 | lineHeight: 1.5, 89 | fontSize: pxToRem(12), 90 | letterSpacing: 1.1, 91 | textTransform: 'uppercase' 92 | }, 93 | button: { 94 | fontWeight: 700, 95 | lineHeight: 24 / 14, 96 | fontSize: pxToRem(14), 97 | textTransform: 'capitalize' 98 | } 99 | }; 100 | 101 | export default typography; 102 | -------------------------------------------------------------------------------- /src/utils/formatNumber.js: -------------------------------------------------------------------------------- 1 | import { replace } from 'lodash'; 2 | import numeral from 'numeral'; 3 | 4 | // ---------------------------------------------------------------------- 5 | 6 | export function fCurrency(number) { 7 | return numeral(number).format(Number.isInteger(number) ? '$0,0' : '$0,0.00'); 8 | } 9 | 10 | export function fPercent(number, f = '0.0%') { 11 | const res = numeral(number / 100).format(f); 12 | const indicator = number > 0 ? '+' : ''; 13 | 14 | return indicator + res; 15 | } 16 | 17 | export function fNumber(number) { 18 | return numeral(number).format(); 19 | } 20 | 21 | export function fShortenNumber(number) { 22 | return replace(numeral(number).format('0.00a'), '.00', ''); 23 | } 24 | 25 | export function fData(number) { 26 | return numeral(number).format('0.0 b'); 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/formatTime.js: -------------------------------------------------------------------------------- 1 | import { format, formatDistanceToNow } from 'date-fns'; 2 | 3 | // ---------------------------------------------------------------------- 4 | 5 | export function fDate(date) { 6 | return format(new Date(date), 'dd MMMM yyyy'); 7 | } 8 | 9 | export function fDateTime(date) { 10 | return format(new Date(date), 'dd MMM yyyy HH:mm'); 11 | } 12 | 13 | export function fDateTimeSuffix(date) { 14 | return format(new Date(date), 'dd/MM/yyyy hh:mm p'); 15 | } 16 | 17 | export function fToNow(date) { 18 | return formatDistanceToNow(new Date(date), { 19 | addSuffix: true 20 | }); 21 | } 22 | 23 | export const isToday = (someDate) => { 24 | const today = new Date(); 25 | return ( 26 | someDate.getDate() === today.getDate() && 27 | someDate.getMonth() === today.getMonth() && 28 | someDate.getFullYear() === today.getFullYear() 29 | ); 30 | }; 31 | 32 | export const generateLastNdates = (n, f = null) => 33 | [...Array(n)].map((_, i) => { 34 | const d = new Date(); 35 | d.setDate(d.getDate() - i); 36 | return f ? format(new Date(d), f) : d; 37 | }); 38 | -------------------------------------------------------------------------------- /src/utils/functions.js: -------------------------------------------------------------------------------- 1 | import { fPercent } from './formatNumber'; 2 | 3 | export const getPercentIncrease = (amount, balance) => { 4 | const changePercent = (amount / (balance - amount)) * 100; 5 | 6 | return fPercent(changePercent, '0.00%'); 7 | }; 8 | --------------------------------------------------------------------------------