diff --git a/.circleci/config.yml b/.circleci/config.yml index cb873be..fca431a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,6 +23,18 @@ jobs: name: set Go path command: echo 'export PATH=$PATH:/usr/local/go/bin' >> $BASH_ENV + - run: + name: install Node 1/3 + command: curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh + + - run: + name: install Node 2/3 + command: sudo bash ./nodesource_setup.sh + + - run: + name: install Node 3/3 + command: sudo apt-get install -y nodejs + - run: name: Install GuPM command: curl -fsSL https://azukaar.github.io/GuPM/install.sh | bash @@ -36,6 +48,11 @@ jobs: - run: name: Install dependencies command: ~/.gupm/gupm/g make + + - run: + name: Build UI + command: g vite build + - run: name: Build Linux (ARM) command: ~/.gupm/gupm/g ci/build linux arm64 diff --git a/.gitignore b/.gitignore index 7aec21b..0c7870e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,10 @@ localcert.crt localcert.key .vite dev.json +static .bin client/dist +client/.vite config_dev.json tests todo.txt diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..19c7bdb --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16 \ No newline at end of file diff --git a/client/TEMPLATE LICENSE b/client/TEMPLATE LICENSE new file mode 100644 index 0000000..453768a --- /dev/null +++ b/client/TEMPLATE LICENSE @@ -0,0 +1,25 @@ +Built from original template from CodedThemes. +The template was distributed with the licence. +This licence does not cover the changes made to the template: + +MIT License + +Copyright (c) 2022 CodedThemes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..44adf8a --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Cosmos + + +
+ + + diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 0000000..5f2615d --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,18 @@ +// project import +import Routes from './routes'; +import ThemeCustomization from './themes'; +import ScrollTop from './components/ScrollTop'; +// ==============================|| APP - THEME, ROUTER, LOCAL ||============================== // + +const App = () => { + + return ( + + + + + + ) +} + +export default App; diff --git a/client/src/App.test.jsx b/client/src/App.test.jsx new file mode 100644 index 0000000..414b21c --- /dev/null +++ b/client/src/App.test.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/client/src/api/authentication.jsx b/client/src/api/authentication.jsx new file mode 100644 index 0000000..821f5f3 --- /dev/null +++ b/client/src/api/authentication.jsx @@ -0,0 +1,26 @@ + +function login(values) { + return fetch('/cosmos/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(values) + }) + .then((res) => res.json()) +} + +function me() { + return fetch('/cosmos/api/me/', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then((res) => res.json()) +} + +export { + login, + me +}; \ No newline at end of file diff --git a/client/src/api/index.jsx b/client/src/api/index.jsx new file mode 100644 index 0000000..97dd64a --- /dev/null +++ b/client/src/api/index.jsx @@ -0,0 +1,4 @@ +import * as auth from './authentication.jsx'; +export { + auth +}; \ No newline at end of file diff --git a/client/src/assets/images/auth/AuthBackground.jsx b/client/src/assets/images/auth/AuthBackground.jsx new file mode 100644 index 0000000..b05f6ae --- /dev/null +++ b/client/src/assets/images/auth/AuthBackground.jsx @@ -0,0 +1,18 @@ +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Box } from '@mui/material'; + +import logo from '../../../../../logo.png'; + +// ==============================|| AUTH BLUR BACK SVG ||============================== // + +const AuthBackground = () => { + const theme = useTheme(); + return ( + + Cosmos + + ); +}; + +export default AuthBackground; diff --git a/client/src/assets/images/icons/discord(1).svg b/client/src/assets/images/icons/discord(1).svg new file mode 100644 index 0000000..4b74773 --- /dev/null +++ b/client/src/assets/images/icons/discord(1).svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/images/icons/discord.svg b/client/src/assets/images/icons/discord.svg new file mode 100644 index 0000000..4b74773 --- /dev/null +++ b/client/src/assets/images/icons/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/images/icons/facebook.svg b/client/src/assets/images/icons/facebook.svg new file mode 100644 index 0000000..6d4fd87 --- /dev/null +++ b/client/src/assets/images/icons/facebook.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/images/icons/google.svg b/client/src/assets/images/icons/google.svg new file mode 100644 index 0000000..bd30fd9 --- /dev/null +++ b/client/src/assets/images/icons/google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/src/assets/images/icons/twitter.svg b/client/src/assets/images/icons/twitter.svg new file mode 100644 index 0000000..f868d36 --- /dev/null +++ b/client/src/assets/images/icons/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/images/users/avatar-1.png b/client/src/assets/images/users/avatar-1.png new file mode 100644 index 0000000..6f83344 Binary files /dev/null and b/client/src/assets/images/users/avatar-1.png differ diff --git a/client/src/assets/images/users/avatar-2.png b/client/src/assets/images/users/avatar-2.png new file mode 100644 index 0000000..2f3f309 Binary files /dev/null and b/client/src/assets/images/users/avatar-2.png differ diff --git a/client/src/assets/images/users/avatar-3.png b/client/src/assets/images/users/avatar-3.png new file mode 100644 index 0000000..6024c00 Binary files /dev/null and b/client/src/assets/images/users/avatar-3.png differ diff --git a/client/src/assets/images/users/avatar-4.png b/client/src/assets/images/users/avatar-4.png new file mode 100644 index 0000000..c4447ee Binary files /dev/null and b/client/src/assets/images/users/avatar-4.png differ diff --git a/client/src/assets/images/users/avatar-group.png b/client/src/assets/images/users/avatar-group.png new file mode 100644 index 0000000..9b08b0c Binary files /dev/null and b/client/src/assets/images/users/avatar-group.png differ diff --git a/client/src/assets/third-party/apex-chart.css b/client/src/assets/third-party/apex-chart.css new file mode 100644 index 0000000..94ccd2b --- /dev/null +++ b/client/src/assets/third-party/apex-chart.css @@ -0,0 +1,4 @@ +.apexcharts-legend-series .apexcharts-legend-marker { + left: -4px !important; + top: 2px !important; +} diff --git a/client/src/components/@extended/AnimateButton.jsx b/client/src/components/@extended/AnimateButton.jsx new file mode 100644 index 0000000..19c465e --- /dev/null +++ b/client/src/components/@extended/AnimateButton.jsx @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; + +// third-party +import { motion } from 'framer-motion'; + +// ==============================|| ANIMATION BUTTON ||============================== // + +export default function AnimateButton({ children, type }) { + switch (type) { + case 'rotate': // only available in paid version + case 'slide': // only available in paid version + case 'scale': // only available in paid version + default: + return ( + + {children} + + ); + } +} + +AnimateButton.propTypes = { + children: PropTypes.node, + type: PropTypes.oneOf(['slide', 'scale', 'rotate']) +}; + +AnimateButton.defaultProps = { + type: 'scale' +}; diff --git a/client/src/components/@extended/Breadcrumbs.jsx b/client/src/components/@extended/Breadcrumbs.jsx new file mode 100644 index 0000000..613639b --- /dev/null +++ b/client/src/components/@extended/Breadcrumbs.jsx @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import { useEffect, useState } from 'react'; +import { Link, useLocation } from 'react-router-dom'; + +// material-ui +import MuiBreadcrumbs from '@mui/material/Breadcrumbs'; +import { Grid, Typography } from '@mui/material'; + +// project imports +import MainCard from '../MainCard'; + +// ==============================|| BREADCRUMBS ||============================== // + +const Breadcrumbs = ({ navigation, title, ...others }) => { + const location = useLocation(); + const [main, setMain] = useState(); + const [item, setItem] = useState(); + + // set active item state + const getCollapse = (menu) => { + if (menu.children) { + menu.children.filter((collapse) => { + if (collapse.type && collapse.type === 'collapse') { + getCollapse(collapse); + } else if (collapse.type && collapse.type === 'item') { + if (location.pathname === collapse.url) { + setMain(menu); + setItem(collapse); + } + } + return false; + }); + } + }; + + useEffect(() => { + navigation?.items?.map((menu) => { + if (menu.type && menu.type === 'group') { + getCollapse(menu); + } + return false; + }); + }); + + // only used for component demo breadcrumbs + if (location.pathname === '/breadcrumbs') { + location.pathname = '/dashboard/analytics'; + } + + let mainContent; + let itemContent; + let breadcrumbContent = ; + let itemTitle = ''; + + // collapse item + if (main && main.type === 'collapse') { + mainContent = ( + + {main.title} + + ); + } + + // items + if (item && item.type === 'item') { + itemTitle = item.title; + itemContent = ( + + {itemTitle} + + ); + + // main + if (item.breadcrumbs !== false) { + breadcrumbContent = ( + + + + + + Home + + {mainContent} + {itemContent} + + + {title && ( + + {item.title} + + )} + + + ); + } + } + + return breadcrumbContent; +}; + +Breadcrumbs.propTypes = { + navigation: PropTypes.object, + title: PropTypes.bool +}; + +export default Breadcrumbs; diff --git a/client/src/components/@extended/Dot.jsx b/client/src/components/@extended/Dot.jsx new file mode 100644 index 0000000..5f8d700 --- /dev/null +++ b/client/src/components/@extended/Dot.jsx @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Box } from '@mui/material'; + +const Dot = ({ color, size }) => { + const theme = useTheme(); + let main; + switch (color) { + case 'secondary': + main = theme.palette.secondary.main; + break; + case 'error': + main = theme.palette.error.main; + break; + case 'warning': + main = theme.palette.warning.main; + break; + case 'info': + main = theme.palette.info.main; + break; + case 'success': + main = theme.palette.success.main; + break; + case 'primary': + default: + main = theme.palette.primary.main; + } + + return ( + + ); +}; + +Dot.propTypes = { + color: PropTypes.string, + size: PropTypes.number +}; + +export default Dot; diff --git a/client/src/components/@extended/Transitions.jsx b/client/src/components/@extended/Transitions.jsx new file mode 100644 index 0000000..1b5f6be --- /dev/null +++ b/client/src/components/@extended/Transitions.jsx @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import { forwardRef } from 'react'; + +// material-ui +import { Fade, Box, Grow } from '@mui/material'; + +// ==============================|| TRANSITIONS ||============================== // + +const Transitions = forwardRef(({ children, position, type, ...others }, ref) => { + let positionSX = { + transformOrigin: '0 0 0' + }; + + switch (position) { + case 'top-right': + case 'top': + case 'bottom-left': + case 'bottom-right': + case 'bottom': + case 'top-left': + default: + positionSX = { + transformOrigin: '0 0 0' + }; + break; + } + + return ( + + {type === 'grow' && ( + + {children} + + )} + {type === 'fade' && ( + + {children} + + )} + + ); +}); + +Transitions.propTypes = { + children: PropTypes.node, + type: PropTypes.oneOf(['grow', 'fade', 'collapse', 'slide', 'zoom']), + position: PropTypes.oneOf(['top-left', 'top-right', 'top', 'bottom-left', 'bottom-right', 'bottom']) +}; + +Transitions.defaultProps = { + type: 'grow', + position: 'top-left' +}; + +export default Transitions; diff --git a/client/src/components/Loadable.jsx b/client/src/components/Loadable.jsx new file mode 100644 index 0000000..554555d --- /dev/null +++ b/client/src/components/Loadable.jsx @@ -0,0 +1,15 @@ +import { Suspense } from 'react'; + +// project import +import Loader from './Loader'; + +// ==============================|| LOADABLE - LAZY LOADING ||============================== // + +const Loadable = (Component) => (props) => + ( + }> + + + ); + +export default Loadable; diff --git a/client/src/components/Loader.jsx b/client/src/components/Loader.jsx new file mode 100644 index 0000000..43e79c1 --- /dev/null +++ b/client/src/components/Loader.jsx @@ -0,0 +1,25 @@ +// material-ui +import { styled } from '@mui/material/styles'; +import LinearProgress from '@mui/material/LinearProgress'; + +// loader style +const LoaderWrapper = styled('div')(({ theme }) => ({ + position: 'fixed', + top: 0, + left: 0, + zIndex: 2001, + width: '100%', + '& > * + *': { + marginTop: theme.spacing(2) + } +})); + +// ==============================|| Loader ||============================== // + +const Loader = () => ( + + + +); + +export default Loader; diff --git a/client/src/components/Logo/Logo.jsx b/client/src/components/Logo/Logo.jsx new file mode 100644 index 0000000..5b812b1 --- /dev/null +++ b/client/src/components/Logo/Logo.jsx @@ -0,0 +1,26 @@ +// material-ui +import { useTheme } from '@mui/material/styles'; +import { fontWeight } from '@mui/system'; + +import logo from '../../../../logo.png'; + +// ==============================|| LOGO SVG ||============================== // + +const Logo = () => { + const theme = useTheme(); + + return ( + /** + * if you want to use image instead of svg uncomment following, and comment out element. + * + * Mantis + * + */ + <> + Cosmos + Cosmos + + ); +}; + +export default Logo; diff --git a/client/src/components/Logo/index.jsx b/client/src/components/Logo/index.jsx new file mode 100644 index 0000000..11d53df --- /dev/null +++ b/client/src/components/Logo/index.jsx @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; + +// material-ui +import { ButtonBase } from '@mui/material'; + +// project import +import Logo from './Logo'; +import config from '../../config'; + +// ==============================|| MAIN LOGO ||============================== // + +const LogoSection = ({ sx, to }) => ( + + + +); + +LogoSection.propTypes = { + sx: PropTypes.object, + to: PropTypes.string +}; + +export default LogoSection; diff --git a/client/src/components/MainCard.jsx b/client/src/components/MainCard.jsx new file mode 100644 index 0000000..b47b179 --- /dev/null +++ b/client/src/components/MainCard.jsx @@ -0,0 +1,109 @@ +import PropTypes from 'prop-types'; +import { forwardRef } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material'; + +// project import +import Highlighter from './third-party/Highlighter'; + +// header style +const headerSX = { + p: 2.5, + '& .MuiCardHeader-action': { m: '0px auto', alignSelf: 'center' } +}; + +// ==============================|| CUSTOM - MAIN CARD ||============================== // + +const MainCard = forwardRef( + ( + { + border = true, + boxShadow, + children, + content = true, + contentSX = {}, + darkTitle, + divider = true, + elevation, + secondary, + shadow, + sx = {}, + title, + codeHighlight, + ...others + }, + ref + ) => { + const theme = useTheme(); + boxShadow = theme.palette.mode === 'dark' ? boxShadow || true : boxShadow; + + return ( + + {/* card header and action */} + {!darkTitle && title && ( + + )} + {darkTitle && title && ( + {title}} action={secondary} /> + )} + + {/* content & header divider */} + {title && divider && } + + {/* card content */} + {content && {children}} + {!content && children} + + {/* card footer - clipboard & highlighter */} + {codeHighlight && ( + <> + + + {children} + + + )} + + ); + } +); + +MainCard.propTypes = { + border: PropTypes.bool, + boxShadow: PropTypes.bool, + contentSX: PropTypes.object, + darkTitle: PropTypes.bool, + divider: PropTypes.bool, + elevation: PropTypes.number, + secondary: PropTypes.node, + shadow: PropTypes.string, + sx: PropTypes.object, + title: PropTypes.string, + codeHighlight: PropTypes.bool, + content: PropTypes.bool, + children: PropTypes.node +}; + +export default MainCard; diff --git a/client/src/components/ScrollTop.jsx b/client/src/components/ScrollTop.jsx new file mode 100644 index 0000000..809ac4d --- /dev/null +++ b/client/src/components/ScrollTop.jsx @@ -0,0 +1,26 @@ +import PropTypes from 'prop-types'; +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +// ==============================|| NAVIGATION - SCROLL TO TOP ||============================== // + +const ScrollTop = ({ children }) => { + const location = useLocation(); + const { pathname } = location; + + useEffect(() => { + window.scrollTo({ + top: 0, + left: 0, + behavior: 'smooth' + }); + }, [pathname]); + + return children || null; +}; + +ScrollTop.propTypes = { + children: PropTypes.node +}; + +export default ScrollTop; diff --git a/client/src/components/cards/AuthFooter.jsx b/client/src/components/cards/AuthFooter.jsx new file mode 100644 index 0000000..f7996d9 --- /dev/null +++ b/client/src/components/cards/AuthFooter.jsx @@ -0,0 +1,22 @@ +// material-ui +import { useMediaQuery, Container, Link, Typography, Stack } from '@mui/material'; + +// ==============================|| FOOTER - AUTHENTICATION ||============================== // + +const AuthFooter = () => { + const matchDownSM = useMediaQuery((theme) => theme.breakpoints.down('sm')); + + return ( + + + + + ); +}; + +export default AuthFooter; diff --git a/client/src/components/cards/statistics/AnalyticEcommerce.jsx b/client/src/components/cards/statistics/AnalyticEcommerce.jsx new file mode 100644 index 0000000..7d1bb5f --- /dev/null +++ b/client/src/components/cards/statistics/AnalyticEcommerce.jsx @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; + +// material-ui +import { Box, Chip, Grid, Stack, Typography } from '@mui/material'; + +// project import +import MainCard from '../../../components/MainCard'; + +// assets +import { RiseOutlined, FallOutlined } from '@ant-design/icons'; + +// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== // + +const AnalyticEcommerce = ({ color, title, count, percentage, isLoss, extra }) => ( + + + + {title} + + + + + {count} + + + {percentage && ( + + + {!isLoss && } + {isLoss && } + + } + label={`${percentage}%`} + sx={{ ml: 1.25, pl: 1 }} + size="small" + /> + + )} + + + + + You made an extra{' '} + + {extra} + {' '} + this year + + + +); + +AnalyticEcommerce.propTypes = { + color: PropTypes.string, + title: PropTypes.string, + count: PropTypes.string, + percentage: PropTypes.number, + isLoss: PropTypes.bool, + extra: PropTypes.oneOfType([PropTypes.node, PropTypes.string]) +}; + +AnalyticEcommerce.defaultProps = { + color: 'primary' +}; + +export default AnalyticEcommerce; diff --git a/client/src/components/third-party/Highlighter.jsx b/client/src/components/third-party/Highlighter.jsx new file mode 100644 index 0000000..410eddc --- /dev/null +++ b/client/src/components/third-party/Highlighter.jsx @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; +import { useState } from 'react'; + +// material-ui +import { Box, CardActions, Collapse, Divider, IconButton, Tooltip } from '@mui/material'; + +// third-party +import { CopyToClipboard } from 'react-copy-to-clipboard'; +import reactElementToJSXString from 'react-element-to-jsx-string'; + +// project import +import SyntaxHighlight from '../../utils/SyntaxHighlight'; + +// assets +import { CodeOutlined, CopyOutlined } from '@ant-design/icons'; + +// ==============================|| CLIPBOARD & HIGHLIGHTER ||============================== // + +const Highlighter = ({ children }) => { + const [highlight, setHighlight] = useState(false); + + return ( + + + + + + + + + + + + + setHighlight(!highlight)} + > + + + + + + + {highlight && ( + + {reactElementToJSXString(children, { + showFunctions: true, + showDefaultProps: false, + maxInlineAttributesLineLength: 100 + })} + + )} + + + ); +}; + +Highlighter.propTypes = { + children: PropTypes.node +}; + +export default Highlighter; diff --git a/client/src/components/third-party/SimpleBar.jsx b/client/src/components/third-party/SimpleBar.jsx new file mode 100644 index 0000000..2d715d3 --- /dev/null +++ b/client/src/components/third-party/SimpleBar.jsx @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; + +// material-ui +import { alpha, styled } from '@mui/material/styles'; +import { Box } from '@mui/material'; + +// third-party +import SimpleBar from 'simplebar-react'; +import { BrowserView, MobileView } from 'react-device-detect'; + +// root style +const RootStyle = styled(BrowserView)({ + flexGrow: 1, + height: '100%', + overflow: 'hidden' +}); + +// scroll bar wrapper +const SimpleBarStyle = styled(SimpleBar)(({ theme }) => ({ + maxHeight: '100%', + '& .simplebar-scrollbar': { + '&:before': { + backgroundColor: alpha(theme.palette.grey[500], 0.48) + }, + '&.simplebar-visible:before': { + opacity: 1 + } + }, + '& .simplebar-track.simplebar-vertical': { + width: 10 + }, + '& .simplebar-track.simplebar-horizontal .simplebar-scrollbar': { + height: 6 + }, + '& .simplebar-mask': { + zIndex: 'inherit' + } +})); + +// ==============================|| SIMPLE SCROLL BAR ||============================== // + +export default function SimpleBarScroll({ children, sx, ...other }) { + return ( + <> + + + {children} + + + + + {children} + + + + ); +} + +SimpleBarScroll.propTypes = { + children: PropTypes.node, + sx: PropTypes.object +}; diff --git a/client/src/config.jsx b/client/src/config.jsx new file mode 100644 index 0000000..f2f6a7d --- /dev/null +++ b/client/src/config.jsx @@ -0,0 +1,19 @@ +// ==============================|| THEME CONFIG ||============================== // + +const config = { + defaultPath: '/dashboard/default', + fontFamily: `'Public Sans', sans-serif`, + i18n: 'en', + miniDrawer: false, + container: true, + mode: 'light', + presetColor: 'default', + themeDirection: 'ltr' +}; + +export default config; +export const drawerWidth = 260; + +export const twitterColor = '#1DA1F2'; +export const facebookColor = '#3b5998'; +export const linkedInColor = '#0e76a8'; diff --git a/client/src/index.jsx b/client/src/index.jsx new file mode 100644 index 0000000..7168d81 --- /dev/null +++ b/client/src/index.jsx @@ -0,0 +1,36 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +// scroll bar +import 'simplebar/src/simplebar.css'; + +// third-party +import { Provider as ReduxProvider } from 'react-redux'; + +// apex-chart +import './assets/third-party/apex-chart.css'; + +// project import +import App from './App'; +import { store } from './store'; +import reportWebVitals from './reportWebVitals'; + +// ==============================|| MAIN - REACT DOM RENDER ||============================== // + +const container = document.getElementById('root'); +const root = createRoot(container); // createRoot(container!) if you use TypeScript +root.render( + + + + + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/client/src/isLoggedIn.jsx b/client/src/isLoggedIn.jsx new file mode 100644 index 0000000..4f90d15 --- /dev/null +++ b/client/src/isLoggedIn.jsx @@ -0,0 +1,13 @@ + +import * as API from './api'; +import { useEffect } from 'react'; + +const isLoggedIn = () => useEffect(() => { + API.auth.me().then((data) => { + if(data.status != 'OK') { + window.location.href = '/login'; + } + }); +}); + +export default isLoggedIn; \ No newline at end of file diff --git a/client/src/layout/MainLayout/Drawer/DrawerContent/NavCard.jsx b/client/src/layout/MainLayout/Drawer/DrawerContent/NavCard.jsx new file mode 100644 index 0000000..f89b0cc --- /dev/null +++ b/client/src/layout/MainLayout/Drawer/DrawerContent/NavCard.jsx @@ -0,0 +1,31 @@ +// material-ui +import { Button, CardMedia, Link, Stack, Typography } from '@mui/material'; + +// project import +import MainCard from '../../../../components/MainCard'; + +// assets +import avatar from '../../../../assets/images/users/avatar-group.png'; +import AnimateButton from '../../../../components/@extended/AnimateButton'; + +// ==============================|| DRAWER CONTENT - NAVIGATION CARD ||============================== // + +const NavCard = () => ( + + + + Cosmos Pro + + Checkout pro features + + + + + + + +); + +export default NavCard; diff --git a/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavGroup.jsx b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavGroup.jsx new file mode 100644 index 0000000..9c001af --- /dev/null +++ b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavGroup.jsx @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; + +// material-ui +import { Box, List, Typography } from '@mui/material'; + +// project import +import NavItem from './NavItem'; + +// ==============================|| NAVIGATION - LIST GROUP ||============================== // + +const NavGroup = ({ item }) => { + const menu = useSelector((state) => state.menu); + const { drawerOpen } = menu; + + const navCollapse = item.children?.map((menuItem) => { + switch (menuItem.type) { + case 'collapse': + return ( + + collapse - only available in paid version + + ); + case 'item': + return ; + default: + return ( + + Fix - Group Collapse or Items + + ); + } + }); + + return ( + + + {item.title} + + {/* only available in paid version */} + + ) + } + sx={{ mb: drawerOpen ? 1.5 : 0, py: 0, zIndex: 0 }} + > + {navCollapse} + + ); +}; + +NavGroup.propTypes = { + item: PropTypes.object +}; + +export default NavGroup; diff --git a/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx new file mode 100644 index 0000000..2be4088 --- /dev/null +++ b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx @@ -0,0 +1,146 @@ +import PropTypes from 'prop-types'; +import { forwardRef, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography } from '@mui/material'; + +// project import +import { activeItem } from '../../../../../store/reducers/menu'; + +// ==============================|| NAVIGATION - LIST ITEM ||============================== // + +const NavItem = ({ item, level }) => { + const theme = useTheme(); + const dispatch = useDispatch(); + const menu = useSelector((state) => state.menu); + const { drawerOpen, openItem } = menu; + + let itemTarget = '_self'; + if (item.target) { + itemTarget = '_blank'; + } + + let listItemProps = { component: forwardRef((props, ref) => ) }; + if (item?.external) { + listItemProps = { component: 'a', href: item.url, target: itemTarget }; + } + + const itemHandler = (id) => { + dispatch(activeItem({ openItem: [id] })); + }; + + const Icon = item.icon; + const itemIcon = item.icon ? : false; + + const isSelected = openItem.findIndex((id) => id === item.id) > -1; + + // active menu item on page load + useEffect(() => { + const currentIndex = document.location.pathname + .toString() + .split('/') + .findIndex((id) => id === item.id); + if (currentIndex > -1) { + dispatch(activeItem({ openItem: [item.id] })); + } + // eslint-disable-next-line + }, []); + + const textColor = 'text.primary'; + const iconSelectedColor = 'primary.main'; + + return ( + itemHandler(item.id)} + selected={isSelected} + sx={{ + zIndex: 1201, + pl: drawerOpen ? `${level * 28}px` : 1.5, + py: !drawerOpen && level === 1 ? 1.25 : 1, + ...(drawerOpen && { + '&:hover': { + bgcolor: 'primary.lighter' + }, + '&.Mui-selected': { + bgcolor: 'primary.lighter', + borderRight: `2px solid ${theme.palette.primary.main}`, + color: iconSelectedColor, + '&:hover': { + color: iconSelectedColor, + bgcolor: 'primary.lighter' + } + } + }), + ...(!drawerOpen && { + '&:hover': { + bgcolor: 'transparent' + }, + '&.Mui-selected': { + '&:hover': { + bgcolor: 'transparent' + }, + bgcolor: 'transparent' + } + }) + }} + > + {itemIcon && ( + + {itemIcon} + + )} + {(drawerOpen || (!drawerOpen && level !== 1)) && ( + + {item.title} + + } + /> + )} + {(drawerOpen || (!drawerOpen && level !== 1)) && item.chip && ( + {item.chip.avatar}} + /> + )} + + ); +}; + +NavItem.propTypes = { + item: PropTypes.object, + level: PropTypes.number +}; + +export default NavItem; diff --git a/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/index.jsx b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/index.jsx new file mode 100644 index 0000000..9c1bb8c --- /dev/null +++ b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/index.jsx @@ -0,0 +1,27 @@ +// material-ui +import { Box, Typography } from '@mui/material'; + +// project import +import NavGroup from './NavGroup'; +import menuItem from '../../../../../menu-items'; + +// ==============================|| DRAWER CONTENT - NAVIGATION ||============================== // + +const Navigation = () => { + const navGroups = menuItem.items.map((item) => { + switch (item.type) { + case 'group': + return ; + default: + return ( + + Fix - Navigation Group + + ); + } + }); + + return {navGroups}; +}; + +export default Navigation; diff --git a/client/src/layout/MainLayout/Drawer/DrawerContent/index.jsx b/client/src/layout/MainLayout/Drawer/DrawerContent/index.jsx new file mode 100644 index 0000000..3c539aa --- /dev/null +++ b/client/src/layout/MainLayout/Drawer/DrawerContent/index.jsx @@ -0,0 +1,22 @@ +// project import +import NavCard from './NavCard'; +import Navigation from './Navigation'; +import SimpleBar from '../../../../components/third-party/SimpleBar'; + +// ==============================|| DRAWER CONTENT ||============================== // + +const DrawerContent = () => ( + + + {/* */} + +); + +export default DrawerContent; diff --git a/client/src/layout/MainLayout/Drawer/DrawerHeader/DrawerHeaderStyled.jsx b/client/src/layout/MainLayout/Drawer/DrawerHeader/DrawerHeaderStyled.jsx new file mode 100644 index 0000000..057251c --- /dev/null +++ b/client/src/layout/MainLayout/Drawer/DrawerHeader/DrawerHeaderStyled.jsx @@ -0,0 +1,15 @@ +// material-ui +import { styled } from '@mui/material/styles'; +import { Box } from '@mui/material'; + +// ==============================|| DRAWER HEADER - STYLED ||============================== // + +const DrawerHeaderStyled = styled(Box, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({ + ...theme.mixins.toolbar, + display: 'flex', + alignItems: 'center', + justifyContent: open ? 'flex-start' : 'center', + paddingLeft: theme.spacing(open ? 3 : 0) +})); + +export default DrawerHeaderStyled; diff --git a/client/src/layout/MainLayout/Drawer/DrawerHeader/index.jsx b/client/src/layout/MainLayout/Drawer/DrawerHeader/index.jsx new file mode 100644 index 0000000..dcc5b00 --- /dev/null +++ b/client/src/layout/MainLayout/Drawer/DrawerHeader/index.jsx @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Stack, Chip } from '@mui/material'; + +// project import +import DrawerHeaderStyled from './DrawerHeaderStyled'; +import Logo from '../../../../components/Logo'; +import {version} from '../../../../../../gupm.json'; + +// ==============================|| DRAWER HEADER ||============================== // + +const DrawerHeader = ({ open }) => { + const theme = useTheme(); + + return ( + + + + + + + ); +}; + +DrawerHeader.propTypes = { + open: PropTypes.bool +}; + +export default DrawerHeader; diff --git a/client/src/layout/MainLayout/Drawer/MiniDrawerStyled.jsx b/client/src/layout/MainLayout/Drawer/MiniDrawerStyled.jsx new file mode 100644 index 0000000..c574c54 --- /dev/null +++ b/client/src/layout/MainLayout/Drawer/MiniDrawerStyled.jsx @@ -0,0 +1,47 @@ +// material-ui +import { styled } from '@mui/material/styles'; +import Drawer from '@mui/material/Drawer'; + +// project import +import { drawerWidth } from '../../../config'; + +const openedMixin = (theme) => ({ + width: drawerWidth, + borderRight: `1px solid ${theme.palette.divider}`, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen + }), + overflowX: 'hidden', + boxShadow: 'none' +}); + +const closedMixin = (theme) => ({ + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen + }), + overflowX: 'hidden', + width: 0, + borderRight: 'none', + boxShadow: theme.customShadows.z1 +}); + +// ==============================|| DRAWER - MINI STYLED ||============================== // + +const MiniDrawerStyled = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({ + width: drawerWidth, + flexShrink: 0, + whiteSpace: 'nowrap', + boxSizing: 'border-box', + ...(open && { + ...openedMixin(theme), + '& .MuiDrawer-paper': openedMixin(theme) + }), + ...(!open && { + ...closedMixin(theme), + '& .MuiDrawer-paper': closedMixin(theme) + }) +})); + +export default MiniDrawerStyled; diff --git a/client/src/layout/MainLayout/Drawer/index.jsx b/client/src/layout/MainLayout/Drawer/index.jsx new file mode 100644 index 0000000..e0225b0 --- /dev/null +++ b/client/src/layout/MainLayout/Drawer/index.jsx @@ -0,0 +1,66 @@ +import PropTypes from 'prop-types'; +import { useMemo } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Box, Drawer, useMediaQuery } from '@mui/material'; + +// project import +import DrawerHeader from './DrawerHeader'; +import DrawerContent from './DrawerContent'; +import MiniDrawerStyled from './MiniDrawerStyled'; +import { drawerWidth } from '../../../config'; + +// ==============================|| MAIN LAYOUT - DRAWER ||============================== // + +const MainDrawer = ({ open, handleDrawerToggle, window }) => { + const theme = useTheme(); + const matchDownMD = useMediaQuery(theme.breakpoints.down('lg')); + + // responsive drawer container + const container = window !== undefined ? () => window().document.body : undefined; + + // header content + const drawerContent = useMemo(() => , []); + const drawerHeader = useMemo(() => , [open]); + + return ( + + {!matchDownMD ? ( + + {drawerHeader} + {drawerContent} + + ) : ( + + {open && drawerHeader} + {open && drawerContent} + + )} + + ); +}; + +MainDrawer.propTypes = { + open: PropTypes.bool, + handleDrawerToggle: PropTypes.func, + window: PropTypes.object +}; + +export default MainDrawer; diff --git a/client/src/layout/MainLayout/Header/AppBarStyled.jsx b/client/src/layout/MainLayout/Header/AppBarStyled.jsx new file mode 100644 index 0000000..b34e929 --- /dev/null +++ b/client/src/layout/MainLayout/Header/AppBarStyled.jsx @@ -0,0 +1,26 @@ +// material-ui +import { styled } from '@mui/material/styles'; +import AppBar from '@mui/material/AppBar'; + +// project import +import { drawerWidth } from '../../../config'; + +// ==============================|| HEADER - APP BAR STYLED ||============================== // + +const AppBarStyled = styled(AppBar, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({ + zIndex: theme.zIndex.drawer + 1, + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen + }), + ...(open && { + marginLeft: drawerWidth, + width: `calc(100% - ${drawerWidth}px)`, + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen + }) + }) +})); + +export default AppBarStyled; diff --git a/client/src/layout/MainLayout/Header/HeaderContent/MobileSection.jsx b/client/src/layout/MainLayout/Header/HeaderContent/MobileSection.jsx new file mode 100644 index 0000000..e5db651 --- /dev/null +++ b/client/src/layout/MainLayout/Header/HeaderContent/MobileSection.jsx @@ -0,0 +1,102 @@ +import { useEffect, useRef, useState } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { AppBar, Box, ClickAwayListener, IconButton, Paper, Popper, Toolbar } from '@mui/material'; + +// project import +import Search from './Search'; +import Profile from './Profile'; +import Transitions from '../../../../components/@extended/Transitions'; + +// assets +import { MoreOutlined } from '@ant-design/icons'; + +// ==============================|| HEADER CONTENT - MOBILE ||============================== // + +const MobileSection = () => { + const theme = useTheme(); + + const [open, setOpen] = useState(false); + const anchorRef = useRef(null); + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + + setOpen(false); + }; + + const prevOpen = useRef(open); + useEffect(() => { + if (prevOpen.current === true && open === false) { + anchorRef.current.focus(); + } + + prevOpen.current = open; + }, [open]); + + return ( + <> + + + + + + + {({ TransitionProps }) => ( + + + + + + + + + + + + + )} + + + ); +}; + +export default MobileSection; diff --git a/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx b/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx new file mode 100644 index 0000000..c397c1e --- /dev/null +++ b/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx @@ -0,0 +1,278 @@ +import { useRef, useState } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { + Avatar, + Badge, + Box, + ClickAwayListener, + Divider, + IconButton, + List, + ListItemButton, + ListItemAvatar, + ListItemText, + ListItemSecondaryAction, + Paper, + Popper, + Typography, + useMediaQuery +} from '@mui/material'; + +// project import +import MainCard from '../../../../components/MainCard'; +import Transitions from '../../../../components/@extended/Transitions'; +// assets +import { BellOutlined, CloseOutlined, GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons'; + +// sx styles +const avatarSX = { + width: 36, + height: 36, + fontSize: '1rem' +}; + +const actionSX = { + mt: '6px', + ml: 1, + top: 'auto', + right: 'auto', + alignSelf: 'flex-start', + + transform: 'none' +}; + +// ==============================|| HEADER CONTENT - NOTIFICATION ||============================== // + +const Notification = () => { + const theme = useTheme(); + const matchesXs = useMediaQuery(theme.breakpoints.down('md')); + + const anchorRef = useRef(null); + const [open, setOpen] = useState(false); + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + setOpen(false); + }; + + const iconBackColorOpen = 'grey.300'; + const iconBackColor = 'grey.100'; + + return ( + + + + + + + + {({ TransitionProps }) => ( + + + + + + + } + > + + + + + + + + + It's{' '} + + Cristina danny's + {' '} + birthday today. + + } + secondary="2 min ago" + /> + + + 3:00 AM + + + + + + + + + + + + + Aida Burg + {' '} + commented your post. + + } + secondary="5 August" + /> + + + 6:00 PM + + + + + + + + + + + + Your Profile is Complete   + + 60% + {' '} + + } + secondary="7 hours ago" + /> + + + 2:45 PM + + + + + + + + C + + + + + Cristina Danny + {' '} + invited to join{' '} + + Meeting. + + + } + secondary="Daily scrum meeting time" + /> + + + 9:10 PM + + + + + + + View All + + } + /> + + + + + + + )} + + + ); +}; + +export default Notification; diff --git a/client/src/layout/MainLayout/Header/HeaderContent/Profile/ProfileTab.jsx b/client/src/layout/MainLayout/Header/HeaderContent/Profile/ProfileTab.jsx new file mode 100644 index 0000000..99242cd --- /dev/null +++ b/client/src/layout/MainLayout/Header/HeaderContent/Profile/ProfileTab.jsx @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import { useState } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; + +// assets +import { EditOutlined, ProfileOutlined, LogoutOutlined, UserOutlined, WalletOutlined } from '@ant-design/icons'; + +// ==============================|| HEADER PROFILE - PROFILE TAB ||============================== // + +const ProfileTab = ({ handleLogout }) => { + const theme = useTheme(); + + const [selectedIndex, setSelectedIndex] = useState(0); + const handleListItemClick = (event, index) => { + setSelectedIndex(index); + }; + + return ( + + handleListItemClick(event, 0)}> + + + + + + handleListItemClick(event, 1)}> + + + + + + + handleListItemClick(event, 3)}> + + + + + + handleListItemClick(event, 4)}> + + + + + + + + + + + + + ); +}; + +ProfileTab.propTypes = { + handleLogout: PropTypes.func +}; + +export default ProfileTab; diff --git a/client/src/layout/MainLayout/Header/HeaderContent/Profile/SettingTab.jsx b/client/src/layout/MainLayout/Header/HeaderContent/Profile/SettingTab.jsx new file mode 100644 index 0000000..5f7a79b --- /dev/null +++ b/client/src/layout/MainLayout/Header/HeaderContent/Profile/SettingTab.jsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; + +// assets +import { CommentOutlined, LockOutlined, QuestionCircleOutlined, UserOutlined, UnorderedListOutlined } from '@ant-design/icons'; + +// ==============================|| HEADER PROFILE - SETTING TAB ||============================== // + +const SettingTab = () => { + const theme = useTheme(); + + const [selectedIndex, setSelectedIndex] = useState(0); + const handleListItemClick = (event, index) => { + setSelectedIndex(index); + }; + + return ( + + handleListItemClick(event, 0)}> + + + + + + handleListItemClick(event, 1)}> + + + + + + handleListItemClick(event, 2)}> + + + + + + handleListItemClick(event, 3)}> + + + + + + handleListItemClick(event, 4)}> + + + + + + + ); +}; + +export default SettingTab; diff --git a/client/src/layout/MainLayout/Header/HeaderContent/Profile/index.jsx b/client/src/layout/MainLayout/Header/HeaderContent/Profile/index.jsx new file mode 100644 index 0000000..245a0b0 --- /dev/null +++ b/client/src/layout/MainLayout/Header/HeaderContent/Profile/index.jsx @@ -0,0 +1,212 @@ +import PropTypes from 'prop-types'; +import { useRef, useState } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { + Avatar, + Box, + ButtonBase, + CardContent, + ClickAwayListener, + Grid, + IconButton, + Paper, + Popper, + Stack, + Tab, + Tabs, + Typography +} from '@mui/material'; + +// project import +import MainCard from '../../../../../components/MainCard'; +import Transitions from '../../../../../components/@extended/Transitions'; +import ProfileTab from './ProfileTab'; +import SettingTab from './SettingTab'; + +// assets +import avatar1 from '../../../../../assets/images/users/avatar-1.png'; +import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; + +// tab panel wrapper +function TabPanel({ children, value, index, ...other }) { + return ( + + ); +} + +TabPanel.propTypes = { + children: PropTypes.node, + index: PropTypes.any.isRequired, + value: PropTypes.any.isRequired +}; + +function a11yProps(index) { + return { + id: `profile-tab-${index}`, + 'aria-controls': `profile-tabpanel-${index}` + }; +} + +// ==============================|| HEADER CONTENT - PROFILE ||============================== // + +const Profile = () => { + const theme = useTheme(); + + const handleLogout = async () => { + // logout + }; + + const anchorRef = useRef(null); + const [open, setOpen] = useState(false); + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + setOpen(false); + }; + + const [value, setValue] = useState(0); + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + const iconBackColorOpen = 'grey.300'; + + return ( + + + + + John Doe + + + + {({ TransitionProps }) => ( + + {open && ( + + + + + + + + + + John Doe + + UI/UX Designer + + + + + + + + + + + + {open && ( + <> + + + } + label="Profile" + {...a11yProps(0)} + /> + } + label="Setting" + {...a11yProps(1)} + /> + + + + + + + + + + )} + + + + )} + + )} + + + ); +}; + +export default Profile; diff --git a/client/src/layout/MainLayout/Header/HeaderContent/Search.jsx b/client/src/layout/MainLayout/Header/HeaderContent/Search.jsx new file mode 100644 index 0000000..55eb11e --- /dev/null +++ b/client/src/layout/MainLayout/Header/HeaderContent/Search.jsx @@ -0,0 +1,30 @@ +// material-ui +import { Box, FormControl, InputAdornment, OutlinedInput } from '@mui/material'; + +// assets +import { SearchOutlined } from '@ant-design/icons'; + +// ==============================|| HEADER CONTENT - SEARCH ||============================== // + +const Search = () => ( + + {/* + + + + } + aria-describedby="header-search-text" + inputProps={{ + 'aria-label': 'weight' + }} + placeholder="Ctrl + K" + /> + */} + +); + +export default Search; diff --git a/client/src/layout/MainLayout/Header/HeaderContent/index.jsx b/client/src/layout/MainLayout/Header/HeaderContent/index.jsx new file mode 100644 index 0000000..9c65b12 --- /dev/null +++ b/client/src/layout/MainLayout/Header/HeaderContent/index.jsx @@ -0,0 +1,28 @@ +// material-ui +import { Box, IconButton, Link, useMediaQuery } from '@mui/material'; +import { GithubOutlined } from '@ant-design/icons'; + +// project import +import Search from './Search'; +import Profile from './Profile'; +import Notification from './Notification'; +import MobileSection from './MobileSection'; + +// ==============================|| HEADER - CONTENT ||============================== // + +const HeaderContent = () => { + const matchesXs = useMediaQuery((theme) => theme.breakpoints.down('md')); + + return ( + <> + {!matchesXs && } + {matchesXs && } + + + {!matchesXs && } + {matchesXs && } + + ); +}; + +export default HeaderContent; diff --git a/client/src/layout/MainLayout/Header/index.jsx b/client/src/layout/MainLayout/Header/index.jsx new file mode 100644 index 0000000..f18513d --- /dev/null +++ b/client/src/layout/MainLayout/Header/index.jsx @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { AppBar, IconButton, Toolbar, useMediaQuery } from '@mui/material'; + +// project import +import AppBarStyled from './AppBarStyled'; +import HeaderContent from './HeaderContent'; + +// assets +import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; + +// ==============================|| MAIN LAYOUT - HEADER ||============================== // + +const Header = ({ open, handleDrawerToggle }) => { + const theme = useTheme(); + const matchDownMD = useMediaQuery(theme.breakpoints.down('lg')); + + const iconBackColor = 'grey.100'; + const iconBackColorOpen = 'grey.200'; + + // common header + const mainHeader = ( + + + {!open ? : } + + + + ); + + // app-bar params + const appBar = { + position: 'fixed', + color: 'inherit', + elevation: 0, + sx: { + borderBottom: `1px solid ${theme.palette.divider}` + // boxShadow: theme.customShadows.z1 + } + }; + + return ( + <> + {!matchDownMD ? ( + + {mainHeader} + + ) : ( + {mainHeader} + )} + + ); +}; + +Header.propTypes = { + open: PropTypes.bool, + handleDrawerToggle: PropTypes.func +}; + +export default Header; diff --git a/client/src/layout/MainLayout/index.jsx b/client/src/layout/MainLayout/index.jsx new file mode 100644 index 0000000..a6640ef --- /dev/null +++ b/client/src/layout/MainLayout/index.jsx @@ -0,0 +1,60 @@ +import { useEffect, useState } from 'react'; +import { Outlet } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Box, Toolbar, useMediaQuery } from '@mui/material'; + +// project import +import Drawer from './Drawer'; +import Header from './Header'; +import navigation from '../../menu-items'; +import Breadcrumbs from '../../components/@extended/Breadcrumbs'; + +// types +import { openDrawer } from '../../store/reducers/menu'; + +// ==============================|| MAIN LAYOUT ||============================== // + +const MainLayout = () => { + const theme = useTheme(); + const matchDownLG = useMediaQuery(theme.breakpoints.down('xl')); + const dispatch = useDispatch(); + + const { drawerOpen } = useSelector((state) => state.menu); + + // drawer toggler + const [open, setOpen] = useState(drawerOpen); + const handleDrawerToggle = () => { + setOpen(!open); + dispatch(openDrawer({ drawerOpen: !open })); + }; + + // set media wise responsive drawer + useEffect(() => { + setOpen(!matchDownLG); + dispatch(openDrawer({ drawerOpen: !matchDownLG })); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [matchDownLG]); + + useEffect(() => { + if (open !== drawerOpen) setOpen(drawerOpen); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [drawerOpen]); + + return ( + +
+ + + + + + + + ); +}; + +export default MainLayout; diff --git a/client/src/layout/MinimalLayout/index.jsx b/client/src/layout/MinimalLayout/index.jsx new file mode 100644 index 0000000..d3fc724 --- /dev/null +++ b/client/src/layout/MinimalLayout/index.jsx @@ -0,0 +1,11 @@ +import { Outlet } from 'react-router-dom'; + +// ==============================|| MINIMAL LAYOUT ||============================== // + +const MinimalLayout = () => ( + <> + + +); + +export default MinimalLayout; diff --git a/client/src/main.css b/client/src/main.css new file mode 100644 index 0000000..e69de29 diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000..60801e6 --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import './main.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + +
Hello
+
+) diff --git a/client/src/menu-items/dashboard.jsx b/client/src/menu-items/dashboard.jsx new file mode 100644 index 0000000..ba3f25d --- /dev/null +++ b/client/src/menu-items/dashboard.jsx @@ -0,0 +1,27 @@ +// assets +import { HomeOutlined } from '@ant-design/icons'; + +// icons +const icons = { + HomeOutlined +}; + +// ==============================|| MENU ITEMS - DASHBOARD ||============================== // + +const dashboard = { + id: 'group-dashboard', + title: 'Navigation', + type: 'group', + children: [ + { + id: 'home', + title: 'Home', + type: 'item', + url: '/', + icon: icons.HomeOutlined, + breadcrumbs: false + } + ] +}; + +export default dashboard; diff --git a/client/src/menu-items/index.jsx b/client/src/menu-items/index.jsx new file mode 100644 index 0000000..e5b5365 --- /dev/null +++ b/client/src/menu-items/index.jsx @@ -0,0 +1,12 @@ +// project import +import pages from './pages'; +import dashboard from './dashboard'; +import support from './support'; + +// ==============================|| MENU ITEMS ||============================== // + +const menuItems = { + items: [dashboard, pages, support] +}; + +export default menuItems; diff --git a/client/src/menu-items/pages.jsx b/client/src/menu-items/pages.jsx new file mode 100644 index 0000000..ca4f92b --- /dev/null +++ b/client/src/menu-items/pages.jsx @@ -0,0 +1,42 @@ +// assets +import { ProfileOutlined, SettingOutlined, NodeExpandOutlined} from '@ant-design/icons'; + +// icons +const icons = { + NodeExpandOutlined, + ProfileOutlined, + SettingOutlined +}; + +// ==============================|| MENU ITEMS - EXTRA PAGES ||============================== // + +const pages = { + id: 'management', + title: 'Management', + type: 'group', + children: [ + { + id: 'proxy', + title: 'Proxy Routes', + type: 'item', + url: '/config/proxy', + icon: icons.NodeExpandOutlined, + }, + { + id: 'users', + title: 'Manage Users', + type: 'item', + url: '/config/users', + icon: icons.ProfileOutlined, + }, + { + id: 'config', + title: 'Configuration', + type: 'item', + url: '/config/general', + icon: icons.SettingOutlined, + } + ] +}; + +export default pages; diff --git a/client/src/menu-items/support.jsx b/client/src/menu-items/support.jsx new file mode 100644 index 0000000..a1a31f1 --- /dev/null +++ b/client/src/menu-items/support.jsx @@ -0,0 +1,48 @@ +// assets +import { GithubOutlined, QuestionOutlined } from '@ant-design/icons'; +import DiscordOutlined from '../assets/images/icons/discord.svg' + +// ==============================|| MENU ITEMS - SAMPLE PAGE & DOCUMENTATION ||============================== // + +const DiscordOutlinedIcon = (props) => { + return ( + Discord + ); +}; + +const support = { + id: 'support', + title: 'Support', + type: 'group', + children: [ + { + id: 'discord', + title: 'Discord', + type: 'item', + url: 'https://discord.com/invite/PwMWwsrwHA', + icon: DiscordOutlinedIcon, + external: true, + target: true + }, + { + id: 'github', + title: 'Github', + type: 'item', + url: 'https://github.com/azukaar/Cosmos-Server', + icon: GithubOutlined, + external: true, + target: true + }, + { + id: 'documentation', + title: 'Documentation', + type: 'item', + url: 'https://github.com/azukaar/Cosmos-Server/wiki', + icon: QuestionOutlined, + external: true, + target: true + } + ] +}; + +export default support; diff --git a/client/src/pages/authentication/AuthCard.jsx b/client/src/pages/authentication/AuthCard.jsx new file mode 100644 index 0000000..0607bad --- /dev/null +++ b/client/src/pages/authentication/AuthCard.jsx @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; + +// material-ui +import { Box } from '@mui/material'; + +// project import +import MainCard from '../../components/MainCard'; + +// ==============================|| AUTHENTICATION - CARD WRAPPER ||============================== // + +const AuthCard = ({ children, ...other }) => ( + *': { + flexGrow: 1, + flexBasis: '50%' + } + }} + content={false} + {...other} + border={false} + boxShadow + shadow={(theme) => theme.customShadows.z1} + > + {children} + +); + +AuthCard.propTypes = { + children: PropTypes.node +}; + +export default AuthCard; diff --git a/client/src/pages/authentication/AuthWrapper.jsx b/client/src/pages/authentication/AuthWrapper.jsx new file mode 100644 index 0000000..cdcea5a --- /dev/null +++ b/client/src/pages/authentication/AuthWrapper.jsx @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; + +// material-ui +import { Box, Grid } from '@mui/material'; + +// project import +import AuthCard from './AuthCard'; +import Logo from '../../components/Logo'; +import AuthFooter from '../../components/cards/AuthFooter'; + +// assets +import AuthBackground from '../../assets/images/auth/AuthBackground'; + +// ==============================|| AUTHENTICATION - WRAPPER ||============================== // + +const AuthWrapper = ({ children }) => ( + + + + + + + + + + {children} + + + + + + + + +); + +AuthWrapper.propTypes = { + children: PropTypes.node +}; + +export default AuthWrapper; diff --git a/client/src/pages/authentication/Login.jsx b/client/src/pages/authentication/Login.jsx new file mode 100644 index 0000000..fed18e9 --- /dev/null +++ b/client/src/pages/authentication/Login.jsx @@ -0,0 +1,30 @@ +import { Link } from 'react-router-dom'; + +// material-ui +import { Grid, Stack, Typography } from '@mui/material'; + +// project import +import AuthLogin from './auth-forms/AuthLogin'; +import AuthWrapper from './AuthWrapper'; + +// ================================|| LOGIN ||================================ // + +const Login = () => ( + + + + + Login + {/* + Don't have an account? + */} + + + + + + + +); + +export default Login; diff --git a/client/src/pages/authentication/Register.jsx b/client/src/pages/authentication/Register.jsx new file mode 100644 index 0000000..a22df0a --- /dev/null +++ b/client/src/pages/authentication/Register.jsx @@ -0,0 +1,30 @@ +import { Link } from 'react-router-dom'; + +// material-ui +import { Grid, Stack, Typography } from '@mui/material'; + +// project import +import FirebaseRegister from './auth-forms/AuthRegister'; +import AuthWrapper from './AuthWrapper'; + +// ================================|| REGISTER ||================================ // + +const Register = () => ( + + + + + Sign up + + Already have an account? + + + + + + + + +); + +export default Register; diff --git a/client/src/pages/authentication/auth-forms/AuthLogin.jsx b/client/src/pages/authentication/auth-forms/AuthLogin.jsx new file mode 100644 index 0000000..424dc14 --- /dev/null +++ b/client/src/pages/authentication/auth-forms/AuthLogin.jsx @@ -0,0 +1,226 @@ +import React, { useEffect } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; + +// material-ui +import { + Button, + Checkbox, + Divider, + FormControlLabel, + FormHelperText, + Grid, + Link, + IconButton, + InputAdornment, + InputLabel, + OutlinedInput, + Stack, + Typography, + Alert +} from '@mui/material'; + +import * as API from "../../../api"; + +// third party +import * as Yup from 'yup'; +import { Formik } from 'formik'; + +// project import +import FirebaseSocial from './FirebaseSocial'; +import AnimateButton from '../../../components/@extended/AnimateButton'; + +// assets +import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; + +// ============================|| FIREBASE - LOGIN ||============================ // + +const AuthLogin = () => { + const [checked, setChecked] = React.useState(false); + + const [showPassword, setShowPassword] = React.useState(false); + const handleClickShowPassword = () => { + setShowPassword(!showPassword); + }; + + const handleMouseDownPassword = (event) => { + event.preventDefault(); + }; + + // TODO: Add ?notlogged=1 and ?invalid=1 to check for errors + // TODO: Extract ?redirect= to redirect to a specific page after login + const urlSearchParams = new URLSearchParams(window.location.search); + const notLogged = urlSearchParams.get('notlogged') == 1; + const invalid = urlSearchParams.get('invalid') == 1; + const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/'; + + useEffect(() => { + API.auth.me().then((data) => { + if(data.status == 'OK') { + window.location.href = redirectTo; + } + }); + }); + + return ( + <> + { notLogged && + You need to be logged in to access this +
+
} + + { invalid && + You have been disconnected. Please login to continue +
+
} + { + try { + API.auth.login(values).then((data) => { + if(data.status == 'error') { + setStatus({ success: false }); + if(data.code == 'UL001') { + setErrors({ submit: 'Wrong nickname or password. Try again or try resetting your password' }); + } else if (data.code == 'UL002') { + setErrors({ submit: 'You have not yet registered your account. You should have an invite link in your emails. If you need a new one, contact your administrator.' }); + } else if(data.status == 'error') { + setErrors({ submit: 'Unexpected error. Try again later.' }); + } + setSubmitting(false); + return; + } else { + setStatus({ success: true }); + setSubmitting(false); + window.location.href = redirectTo; + } + }) + } catch (err) { + setStatus({ success: false }); + setErrors({ submit: err.message }); + setSubmitting(false); + } + }} + > + {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => ( +
+ + + + Nickname + + {touched.nickname && errors.nickname && ( + + {errors.nickname} + + )} + + + + + Password + + + {showPassword ? : } + + + } + placeholder="Enter your password" + /> + {touched.password && errors.password && ( + + {errors.password} + + )} + + + + + + setChecked(event.target.checked)} + name="checked" + color="primary" + size="small" + /> + } + label={Keep me sign in} + /> + + Forgot Password? + + + + {errors.submit && ( + + {errors.submit} + + )} + + + + + + {/* + + Login with + + + + + */} + +
+ )} +
+ + ); +}; + +export default AuthLogin; diff --git a/client/src/pages/authentication/auth-forms/AuthRegister.jsx b/client/src/pages/authentication/auth-forms/AuthRegister.jsx new file mode 100644 index 0000000..d905875 --- /dev/null +++ b/client/src/pages/authentication/auth-forms/AuthRegister.jsx @@ -0,0 +1,271 @@ +import { useEffect, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; + +// material-ui +import { + Box, + Button, + Divider, + FormControl, + FormHelperText, + Grid, + Link, + IconButton, + InputAdornment, + InputLabel, + OutlinedInput, + Stack, + Typography +} from '@mui/material'; + +// third party +import * as Yup from 'yup'; +import { Formik } from 'formik'; + +// project import +import FirebaseSocial from './FirebaseSocial'; +import AnimateButton from '../../../components/@extended/AnimateButton'; +import { strengthColor, strengthIndicator } from '../../../utils/password-strength'; + +// assets +import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; + +// ============================|| FIREBASE - REGISTER ||============================ // + +const AuthRegister = () => { + const [level, setLevel] = useState(); + const [showPassword, setShowPassword] = useState(false); + const handleClickShowPassword = () => { + setShowPassword(!showPassword); + }; + + const handleMouseDownPassword = (event) => { + event.preventDefault(); + }; + + const changePassword = (value) => { + const temp = strengthIndicator(value); + setLevel(strengthColor(temp)); + }; + + useEffect(() => { + changePassword(''); + }, []); + + return ( + <> + { + try { + setStatus({ success: false }); + setSubmitting(false); + } catch (err) { + console.error(err); + setStatus({ success: false }); + setErrors({ submit: err.message }); + setSubmitting(false); + } + }} + > + {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => ( +
+ + + + First Name* + + {touched.firstname && errors.firstname && ( + + {errors.firstname} + + )} + + + + + Last Name* + + {touched.lastname && errors.lastname && ( + + {errors.lastname} + + )} + + + + + Company + + {touched.company && errors.company && ( + + {errors.company} + + )} + + + + + Email Address* + + {touched.email && errors.email && ( + + {errors.email} + + )} + + + + + Password + { + handleChange(e); + changePassword(e.target.value); + }} + endAdornment={ + + + {showPassword ? : } + + + } + placeholder="******" + inputProps={{}} + /> + {touched.password && errors.password && ( + + {errors.password} + + )} + + + + + + + + + {level?.label} + + + + + + + + By Signing up, you agree to our   + + Terms of Service + +   and   + + Privacy Policy + + + + {errors.submit && ( + + {errors.submit} + + )} + + + + + + + + Sign up with + + + + + + +
+ )} +
+ + ); +}; + +export default AuthRegister; diff --git a/client/src/pages/authentication/auth-forms/FirebaseSocial.jsx b/client/src/pages/authentication/auth-forms/FirebaseSocial.jsx new file mode 100644 index 0000000..05761a9 --- /dev/null +++ b/client/src/pages/authentication/auth-forms/FirebaseSocial.jsx @@ -0,0 +1,66 @@ +// material-ui +import { useTheme } from '@mui/material/styles'; +import { useMediaQuery, Button, Stack } from '@mui/material'; + +// assets +import Google from '../../../assets/images/icons/google.svg'; +import Twitter from '../../../assets/images/icons/twitter.svg'; +import Facebook from '../../../assets/images/icons/facebook.svg'; + +// ==============================|| FIREBASE - SOCIAL BUTTON ||============================== // + +const FirebaseSocial = () => { + const theme = useTheme(); + const matchDownSM = useMediaQuery(theme.breakpoints.down('sm')); + + const googleHandler = async () => { + // login || singup + }; + + const twitterHandler = async () => { + // login || singup + }; + + const facebookHandler = async () => { + // login || singup + }; + + return ( + + + + + + ); +}; + +export default FirebaseSocial; diff --git a/client/src/pages/components-overview/AntIcons.jsx b/client/src/pages/components-overview/AntIcons.jsx new file mode 100644 index 0000000..6cdbd0b --- /dev/null +++ b/client/src/pages/components-overview/AntIcons.jsx @@ -0,0 +1,24 @@ +// material-ui +import { styled } from '@mui/material/styles'; + +// project import +import ComponentSkeleton from './ComponentSkeleton'; +import MainCard from '../../components/MainCard'; + +// styles +const IFrameWrapper = styled('iframe')(() => ({ + height: 'calc(100vh - 210px)', + border: 'none' +})); + +// ============================|| ANT ICONS ||============================ // + +const AntIcons = () => ( + + + + + +); + +export default AntIcons; diff --git a/client/src/pages/components-overview/Color.jsx b/client/src/pages/components-overview/Color.jsx new file mode 100644 index 0000000..5a62b2d --- /dev/null +++ b/client/src/pages/components-overview/Color.jsx @@ -0,0 +1,141 @@ +import PropTypes from 'prop-types'; + +// material-ui +import { Box, Card, Grid, Stack, Typography } from '@mui/material'; + +// project import +import MainCard from '../../components/MainCard'; +import ComponentSkeleton from './ComponentSkeleton'; + +// ===============================|| COLOR BOX ||=============================== // + +function ColorBox({ bgcolor, title, data, dark, main }) { + return ( + <> + + + {title && ( + + + {data && ( + + {data.label} + {data.color} + + )} + + + + {title} + + + + )} + + + + ); +} + +ColorBox.propTypes = { + bgcolor: PropTypes.string, + title: PropTypes.string, + data: PropTypes.object.isRequired, + dark: PropTypes.bool, + main: PropTypes.bool +}; + +// ===============================|| COMPONENT - COLOR ||=============================== // + +const ComponentColor = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default ComponentColor; diff --git a/client/src/pages/components-overview/ComponentSkeleton.jsx b/client/src/pages/components-overview/ComponentSkeleton.jsx new file mode 100644 index 0000000..999dc03 --- /dev/null +++ b/client/src/pages/components-overview/ComponentSkeleton.jsx @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import { useEffect, useState } from 'react'; + +// material-ui +import { Grid, Skeleton, Stack } from '@mui/material'; + +// project import +import MainCard from '../../components/MainCard'; + +// ===============================|| COMPONENT - SKELETON ||=============================== // + +const ComponentSkeleton = ({ children }) => { + const [isLoading, setLoading] = useState(true); + useEffect(() => { + setLoading(false); + }, []); + + const skeletonCard = ( + } + secondary={} + > + + + + + + + + ); + + return ( + <> + {isLoading && ( + + + {skeletonCard} + + + {skeletonCard} + + + {skeletonCard} + + + {skeletonCard} + + + )} + {!isLoading && children} + + ); +}; + +ComponentSkeleton.propTypes = { + children: PropTypes.node +}; + +export default ComponentSkeleton; diff --git a/client/src/pages/components-overview/Shadow.jsx b/client/src/pages/components-overview/Shadow.jsx new file mode 100644 index 0000000..e0c3481 --- /dev/null +++ b/client/src/pages/components-overview/Shadow.jsx @@ -0,0 +1,152 @@ +import PropTypes from 'prop-types'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Grid, Stack, Typography } from '@mui/material'; + +// project import +import MainCard from '../../components/MainCard'; +import ComponentSkeleton from './ComponentSkeleton'; + +// ===============================|| SHADOW BOX ||=============================== // + +function ShadowBox({ shadow }) { + return ( + + + boxShadow + {shadow} + + + ); +} + +ShadowBox.propTypes = { + shadow: PropTypes.string.isRequired +}; + +// ===============================|| CUSTOM - SHADOW BOX ||=============================== // + +function CustomShadowBox({ shadow, label, color, bgcolor }) { + return ( + + + + {label} + + + + ); +} + +CustomShadowBox.propTypes = { + shadow: PropTypes.string.isRequired, + color: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + bgcolor: PropTypes.string.isRequired +}; + +// ============================|| COMPONENT - SHADOW ||============================ // + +const ComponentShadow = () => { + const theme = useTheme(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ComponentShadow; diff --git a/client/src/pages/components-overview/Typography.jsx b/client/src/pages/components-overview/Typography.jsx new file mode 100644 index 0000000..71ee3c1 --- /dev/null +++ b/client/src/pages/components-overview/Typography.jsx @@ -0,0 +1,263 @@ +// material-ui +import { Breadcrumbs, Divider, Grid, Link, Stack, Typography } from '@mui/material'; + +// project import +import ComponentSkeleton from './ComponentSkeleton'; +import MainCard from '../../components/MainCard'; + +// ==============================|| COMPONENTS - TYPOGRAPHY ||============================== // + +const ComponentTypography = () => ( + + + + + + + Inter + Font Family + + Regular + Medium + Bold + + + + + + H1 Heading + + Size: 38px + Weight: Bold + Line Height: 46px + + + + H2 Heading + + Size: 30px + Weight: Bold + Line Height: 38px + + + + H3 Heading + + Size: 24px + Weight: Regular & Bold + Line Height: 32px + + + + H4 Heading + + Size: 20px + Weight: Bold + Line Height: 28px + + + + H5 Heading + + Size: 16px + Weight: Regular & Medium & Bold + Line Height: 24px + + + + H6 Heading / Subheading + + Size: 14px + Weight: Regular + Line Height: 22px + + + + + <> + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Size: 14px + Weight: Regular + Line Height: 22px + + + + + <> + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Size: 12px + Weight: Regular + Line Height: 20px + + + + + <> + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Size: 14px + Weight: Medium + Line Height: 22px + + + + + <> + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Size: 12px + Weight: Medium + Line Height: 20px + + + + + + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Size: 12px + Weight: Regular + Line Height: 20px + + + + + + + + + <> + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + + <> + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Size: 12px + Weight: Regular + Line Height: 20px + + + + + + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Size: 12px + Weight: Regular + Line Height: 20px + + + + + + www.mantis.com + + Size: 12px + Weight: Regular + Line Height: 20px + + + + + <> + + This is textPrimary text color. + + + This is textSecondary text color. + + + This is primary text color. + + + This is secondary text color. + + + This is success text color. + + + This is warning text color. + + + This is error text color. + + + + + <> + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. + + + Size: 14px + Weight: Regular + Line Height: 22px + + + + + <> + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. + + + Size: 14px + Weight: Italic Regular & Italic Bold + Line Height: 22px + + + + + + + +); + +export default ComponentTypography; diff --git a/client/src/pages/dashboard/IncomeAreaChart.jsx b/client/src/pages/dashboard/IncomeAreaChart.jsx new file mode 100644 index 0000000..b7acc54 --- /dev/null +++ b/client/src/pages/dashboard/IncomeAreaChart.jsx @@ -0,0 +1,121 @@ +import PropTypes from 'prop-types'; +import { useState, useEffect } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; + +// third-party +import ReactApexChart from 'react-apexcharts'; + +// chart options +const areaChartOptions = { + chart: { + height: 450, + type: 'area', + toolbar: { + show: false + } + }, + dataLabels: { + enabled: false + }, + stroke: { + curve: 'smooth', + width: 2 + }, + grid: { + strokeDashArray: 0 + } +}; + +// ==============================|| INCOME AREA CHART ||============================== // + +const IncomeAreaChart = ({ slot }) => { + const theme = useTheme(); + + const { primary, secondary } = theme.palette.text; + const line = theme.palette.divider; + + const [options, setOptions] = useState(areaChartOptions); + + useEffect(() => { + setOptions((prevState) => ({ + ...prevState, + colors: [theme.palette.primary.main, theme.palette.primary[700]], + xaxis: { + categories: + slot === 'month' + ? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + labels: { + style: { + colors: [ + secondary, + secondary, + secondary, + secondary, + secondary, + secondary, + secondary, + secondary, + secondary, + secondary, + secondary, + secondary + ] + } + }, + axisBorder: { + show: true, + color: line + }, + tickAmount: slot === 'month' ? 11 : 7 + }, + yaxis: { + labels: { + style: { + colors: [secondary] + } + } + }, + grid: { + borderColor: line + }, + tooltip: { + theme: 'light' + } + })); + }, [primary, secondary, line, theme, slot]); + + const [series, setSeries] = useState([ + { + name: 'Page Views', + data: [0, 86, 28, 115, 48, 210, 136] + }, + { + name: 'Sessions', + data: [0, 43, 14, 56, 24, 105, 68] + } + ]); + + useEffect(() => { + setSeries([ + { + name: 'Page Views', + data: slot === 'month' ? [76, 85, 101, 98, 87, 105, 91, 114, 94, 86, 115, 35] : [31, 40, 28, 51, 42, 109, 100] + }, + { + name: 'Sessions', + data: slot === 'month' ? [110, 60, 150, 35, 60, 36, 26, 45, 65, 52, 53, 41] : [11, 32, 45, 32, 34, 52, 41] + } + ]); + }, [slot]); + + return ; +}; + +IncomeAreaChart.propTypes = { + slot: PropTypes.string +}; + +export default IncomeAreaChart; diff --git a/client/src/pages/dashboard/MonthlyBarChart.jsx b/client/src/pages/dashboard/MonthlyBarChart.jsx new file mode 100644 index 0000000..10cd0ad --- /dev/null +++ b/client/src/pages/dashboard/MonthlyBarChart.jsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; + +// third-party +import ReactApexChart from 'react-apexcharts'; + +// chart options +const barChartOptions = { + chart: { + type: 'bar', + height: 365, + toolbar: { + show: false + } + }, + plotOptions: { + bar: { + columnWidth: '45%', + borderRadius: 4 + } + }, + dataLabels: { + enabled: false + }, + xaxis: { + categories: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: { + show: false + }, + grid: { + show: false + } +}; + +// ==============================|| MONTHLY BAR CHART ||============================== // + +const MonthlyBarChart = () => { + const theme = useTheme(); + + const { primary, secondary } = theme.palette.text; + const info = theme.palette.info.light; + + const [series] = useState([ + { + data: [80, 95, 70, 42, 65, 55, 78] + } + ]); + + const [options, setOptions] = useState(barChartOptions); + + useEffect(() => { + setOptions((prevState) => ({ + ...prevState, + colors: [info], + xaxis: { + labels: { + style: { + colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary] + } + } + }, + tooltip: { + theme: 'light' + } + })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [primary, info, secondary]); + + return ( +
+ +
+ ); +}; + +export default MonthlyBarChart; diff --git a/client/src/pages/dashboard/OrdersTable.jsx b/client/src/pages/dashboard/OrdersTable.jsx new file mode 100644 index 0000000..a9a1781 --- /dev/null +++ b/client/src/pages/dashboard/OrdersTable.jsx @@ -0,0 +1,224 @@ +import PropTypes from 'prop-types'; +import { useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; + +// material-ui +import { Box, Link, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material'; + +// third-party +import NumberFormat from 'react-number-format'; + +// project import +import Dot from '../../components/@extended/Dot'; + +function createData(trackingNo, name, fat, carbs, protein) { + return { trackingNo, name, fat, carbs, protein }; +} + +const rows = [ + createData(84564564, 'Camera Lens', 40, 2, 40570), + createData(98764564, 'Laptop', 300, 0, 180139), + createData(98756325, 'Mobile', 355, 1, 90989), + createData(98652366, 'Handset', 50, 1, 10239), + createData(13286564, 'Computer Accessories', 100, 1, 83348), + createData(86739658, 'TV', 99, 0, 410780), + createData(13256498, 'Keyboard', 125, 2, 70999), + createData(98753263, 'Mouse', 89, 2, 10570), + createData(98753275, 'Desktop', 185, 1, 98063), + createData(98753291, 'Chair', 100, 0, 14001) +]; + +function descendingComparator(a, b, orderBy) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function getComparator(order, orderBy) { + return order === 'desc' ? (a, b) => descendingComparator(a, b, orderBy) : (a, b) => -descendingComparator(a, b, orderBy); +} + +function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) { + return order; + } + return a[1] - b[1]; + }); + return stabilizedThis.map((el) => el[0]); +} + +// ==============================|| ORDER TABLE - HEADER CELL ||============================== // + +const headCells = [ + { + id: 'trackingNo', + align: 'left', + disablePadding: false, + label: 'Tracking No.' + }, + { + id: 'name', + align: 'left', + disablePadding: true, + label: 'Product Name' + }, + { + id: 'fat', + align: 'right', + disablePadding: false, + label: 'Total Order' + }, + { + id: 'carbs', + align: 'left', + disablePadding: false, + + label: 'Status' + }, + { + id: 'protein', + align: 'right', + disablePadding: false, + label: 'Total Amount' + } +]; + +// ==============================|| ORDER TABLE - HEADER ||============================== // + +function OrderTableHead({ order, orderBy }) { + return ( + + + {headCells.map((headCell) => ( + + {headCell.label} + + ))} + + + ); +} + +OrderTableHead.propTypes = { + order: PropTypes.string, + orderBy: PropTypes.string +}; + +// ==============================|| ORDER TABLE - STATUS ||============================== // + +const OrderStatus = ({ status }) => { + let color; + let title; + + switch (status) { + case 0: + color = 'warning'; + title = 'Pending'; + break; + case 1: + color = 'success'; + title = 'Approved'; + break; + case 2: + color = 'error'; + title = 'Rejected'; + break; + default: + color = 'primary'; + title = 'None'; + } + + return ( + + + {title} + + ); +}; + +OrderStatus.propTypes = { + status: PropTypes.number +}; + +// ==============================|| ORDER TABLE ||============================== // + +export default function OrderTable() { + const [order] = useState('asc'); + const [orderBy] = useState('trackingNo'); + const [selected] = useState([]); + + const isSelected = (trackingNo) => selected.indexOf(trackingNo) !== -1; + + return ( + + + + + + {stableSort(rows, getComparator(order, orderBy)).map((row, index) => { + const isItemSelected = isSelected(row.trackingNo); + const labelId = `enhanced-table-checkbox-${index}`; + + return ( + + + + {row.trackingNo} + + + {row.name} + {row.fat} + + + + + + + + ); + })} + +
+
+
+ ); +} diff --git a/client/src/pages/dashboard/ReportAreaChart.jsx b/client/src/pages/dashboard/ReportAreaChart.jsx new file mode 100644 index 0000000..ed91490 --- /dev/null +++ b/client/src/pages/dashboard/ReportAreaChart.jsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; + +// third-party +import ReactApexChart from 'react-apexcharts'; + +// chart options +const areaChartOptions = { + chart: { + height: 340, + type: 'line', + toolbar: { + show: false + } + }, + dataLabels: { + enabled: false + }, + stroke: { + curve: 'smooth', + width: 1.5 + }, + grid: { + strokeDashArray: 4 + }, + xaxis: { + type: 'datetime', + categories: [ + '2018-05-19T00:00:00.000Z', + '2018-06-19T00:00:00.000Z', + '2018-07-19T01:30:00.000Z', + '2018-08-19T02:30:00.000Z', + '2018-09-19T03:30:00.000Z', + '2018-10-19T04:30:00.000Z', + '2018-11-19T05:30:00.000Z', + '2018-12-19T06:30:00.000Z' + ], + labels: { + format: 'MMM' + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: { + show: false + }, + tooltip: { + x: { + format: 'MM' + } + } +}; + +// ==============================|| REPORT AREA CHART ||============================== // + +const ReportAreaChart = () => { + const theme = useTheme(); + + const { primary, secondary } = theme.palette.text; + const line = theme.palette.divider; + + const [options, setOptions] = useState(areaChartOptions); + + useEffect(() => { + setOptions((prevState) => ({ + ...prevState, + colors: [theme.palette.warning.main], + xaxis: { + labels: { + style: { + colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary, secondary] + } + } + }, + grid: { + borderColor: line + }, + tooltip: { + theme: 'light' + }, + legend: { + labels: { + colors: 'grey.500' + } + } + })); + }, [primary, secondary, line, theme]); + + const [series] = useState([ + { + name: 'Series 1', + data: [58, 115, 28, 83, 63, 75, 35, 55] + } + ]); + + return ; +}; + +export default ReportAreaChart; diff --git a/client/src/pages/dashboard/SalesColumnChart.jsx b/client/src/pages/dashboard/SalesColumnChart.jsx new file mode 100644 index 0000000..beb789b --- /dev/null +++ b/client/src/pages/dashboard/SalesColumnChart.jsx @@ -0,0 +1,148 @@ +import { useEffect, useState } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; + +// third-party +import ReactApexChart from 'react-apexcharts'; + +// chart options +const columnChartOptions = { + chart: { + type: 'bar', + height: 430, + toolbar: { + show: false + } + }, + plotOptions: { + bar: { + columnWidth: '30%', + borderRadius: 4 + } + }, + dataLabels: { + enabled: false + }, + stroke: { + show: true, + width: 8, + colors: ['transparent'] + }, + xaxis: { + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] + }, + yaxis: { + title: { + text: '$ (thousands)' + } + }, + fill: { + opacity: 1 + }, + tooltip: { + y: { + formatter(val) { + return `$ ${val} thousands`; + } + } + }, + legend: { + show: true, + fontFamily: `'Public Sans', sans-serif`, + offsetX: 10, + offsetY: 10, + labels: { + useSeriesColors: false + }, + markers: { + width: 16, + height: 16, + radius: '50%', + offsexX: 2, + offsexY: 2 + }, + itemMargin: { + horizontal: 15, + vertical: 50 + } + }, + responsive: [ + { + breakpoint: 600, + options: { + yaxis: { + show: false + } + } + } + ] +}; + +// ==============================|| SALES COLUMN CHART ||============================== // + +const SalesColumnChart = () => { + const theme = useTheme(); + + const { primary, secondary } = theme.palette.text; + const line = theme.palette.divider; + + const warning = theme.palette.warning.main; + const primaryMain = theme.palette.primary.main; + const successDark = theme.palette.success.dark; + + const [series] = useState([ + { + name: 'Net Profit', + data: [180, 90, 135, 114, 120, 145] + }, + { + name: 'Revenue', + data: [120, 45, 78, 150, 168, 99] + } + ]); + + const [options, setOptions] = useState(columnChartOptions); + + useEffect(() => { + setOptions((prevState) => ({ + ...prevState, + colors: [warning, primaryMain], + xaxis: { + labels: { + style: { + colors: [secondary, secondary, secondary, secondary, secondary, secondary] + } + } + }, + yaxis: { + labels: { + style: { + colors: [secondary] + } + } + }, + grid: { + borderColor: line + }, + tooltip: { + theme: 'light' + }, + legend: { + position: 'top', + horizontalAlign: 'right', + labels: { + colors: 'grey.500' + } + } + })); + }, [primary, secondary, line, warning, primaryMain, successDark]); + + return ( +
+ +
+ ); +}; + +export default SalesColumnChart; diff --git a/client/src/pages/dashboard/index.jsx b/client/src/pages/dashboard/index.jsx new file mode 100644 index 0000000..f21b8c8 --- /dev/null +++ b/client/src/pages/dashboard/index.jsx @@ -0,0 +1,358 @@ +import { useState } from 'react'; + +// material-ui +import { + Avatar, + AvatarGroup, + Box, + Button, + Grid, + List, + ListItemAvatar, + ListItemButton, + ListItemSecondaryAction, + ListItemText, + MenuItem, + Stack, + TextField, + Typography, + Alert +} from '@mui/material'; + +// project import +import OrdersTable from './OrdersTable'; +import IncomeAreaChart from './IncomeAreaChart'; +import MonthlyBarChart from './MonthlyBarChart'; +import ReportAreaChart from './ReportAreaChart'; +import SalesColumnChart from './SalesColumnChart'; +import MainCard from '../../components/MainCard'; +import AnalyticEcommerce from '../../components/cards/statistics/AnalyticEcommerce'; + +// assets +import { GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons'; +import avatar1 from '../../assets/images/users/avatar-1.png'; +import avatar2 from '../../assets/images/users/avatar-2.png'; +import avatar3 from '../../assets/images/users/avatar-3.png'; +import avatar4 from '../../assets/images/users/avatar-4.png'; +import isLoggedIn from '../../isLoggedIn'; + +// avatar style +const avatarSX = { + width: 36, + height: 36, + fontSize: '1rem' +}; + +// action style +const actionSX = { + mt: 0.75, + ml: 1, + top: 'auto', + right: 'auto', + alignSelf: 'flex-start', + transform: 'none' +}; + +// sales report status +const status = [ + { + value: 'today', + label: 'Today' + }, + { + value: 'month', + label: 'This Month' + }, + { + value: 'year', + label: 'This Year' + } +]; + +// ==============================|| DASHBOARD - DEFAULT ||============================== // + +const DashboardDefault = () => { + const [value, setValue] = useState('today'); + const [slot, setSlot] = useState('week'); + + isLoggedIn(); + + return ( + <> +
+ Dashboard implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord! +
+
+ + {/* row 1 */} + + Dashboard + + + + + + + + + + + + + + + + + {/* row 2 */} + + + + Unique Visitor + + + + + + + + + + + + + + + + + + Income Overview + + + + + + + + This Week Statistics + + $7,650 + + + + + + + {/* row 3 */} + + + + Recent Orders + + + + + + + + + + + Analytics Report + + + + + + + + +45.14% + + + + 0.58% + + + + Low + + + + + + + {/* row 4 */} + + + + Sales Report + + + setValue(e.target.value)} + sx={{ '& .MuiInputBase-input': { py: 0.5, fontSize: '0.875rem' } }} + > + {status.map((option) => ( + + {option.label} + + ))} + + + + + + + Net Profit + + $1560 + + + + + + + + Transaction History + + + + + + + + + + + + Order #002434} secondary="Today, 2:00 AM" /> + + + + + $1,430 + + + 78% + + + + + + + + + + + Order #984947} + secondary="5 August, 1:45 PM" + /> + + + + + $302 + + + 8% + + + + + + + + + + + Order #988784} secondary="7 hours ago" /> + + + + + $682 + + + 16% + + + + + + + + + + + + + Help & Support Chat + + + Typical replay within 5 min + + + + + + + + + + + + + + + + + +
+ + ); +}; + +export default DashboardDefault; diff --git a/client/src/pages/extra-pages/SamplePage.jsx b/client/src/pages/extra-pages/SamplePage.jsx new file mode 100644 index 0000000..2cf4192 --- /dev/null +++ b/client/src/pages/extra-pages/SamplePage.jsx @@ -0,0 +1,20 @@ +// material-ui +import { Typography } from '@mui/material'; + +// project import +import MainCard from '../../components/MainCard'; + +// ==============================|| SAMPLE PAGE ||============================== // + +const SamplePage = () => ( + + + Lorem ipsum dolor sit amen, consenter nipissing eli, sed do elusion tempos incident ut laborers et doolie magna alissa. Ut enif + ad minim venice, quin nostrum exercitation illampu laborings nisi ut liquid ex ea commons construal. Duos aube grue dolor in + reprehended in voltage veil esse colum doolie eu fujian bulla parian. Exceptive sin ocean cuspidate non president, sunk in culpa + qui officiate descent molls anim id est labours. + + +); + +export default SamplePage; diff --git a/client/src/react-app-env.d.jsx b/client/src/react-app-env.d.jsx new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/client/src/react-app-env.d.jsx @@ -0,0 +1 @@ +/// diff --git a/client/src/reportWebVitals.jsx b/client/src/reportWebVitals.jsx new file mode 100644 index 0000000..17f8ad1 --- /dev/null +++ b/client/src/reportWebVitals.jsx @@ -0,0 +1,13 @@ +const reportWebVitals = (onPerfEntry) => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/client/src/routes/LoginRoutes.jsx b/client/src/routes/LoginRoutes.jsx new file mode 100644 index 0000000..ffd0031 --- /dev/null +++ b/client/src/routes/LoginRoutes.jsx @@ -0,0 +1,28 @@ +import { lazy } from 'react'; + +// project import +import Loadable from '../components/Loadable'; +import MinimalLayout from '../layout/MinimalLayout'; + +// render - login +const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login'))); +const AuthRegister = Loadable(lazy(() => import('../pages/authentication/Register'))); + +// ==============================|| AUTH ROUTING ||============================== // + +const LoginRoutes = { + path: '/', + element: , + children: [ + { + path: 'login', + element: + }, + { + path: 'register', + element: + } + ] +}; + +export default LoginRoutes; diff --git a/client/src/routes/MainRoutes.jsx b/client/src/routes/MainRoutes.jsx new file mode 100644 index 0000000..bc228cf --- /dev/null +++ b/client/src/routes/MainRoutes.jsx @@ -0,0 +1,61 @@ +import { lazy } from 'react'; + +// project import +import Loadable from '../components/Loadable'; +import MainLayout from '../layout/MainLayout'; + +// render - dashboard +const DashboardDefault = Loadable(lazy(() => import('../pages/dashboard'))); + +// render - sample page +const SamplePage = Loadable(lazy(() => import('../pages/extra-pages/SamplePage'))); + +// render - utilities +const Typography = Loadable(lazy(() => import('../pages/components-overview/Typography'))); +const Color = Loadable(lazy(() => import('../pages/components-overview/Color'))); +const Shadow = Loadable(lazy(() => import('../pages/components-overview/Shadow'))); +const AntIcons = Loadable(lazy(() => import('../pages/components-overview/AntIcons'))); + +// ==============================|| MAIN ROUTING ||============================== // + +const MainRoutes = { + path: '/', + element: , + children: [ + { + path: '/', + element: + }, + { + path: 'color', + element: + }, + { + path: 'dashboard', + children: [ + { + path: 'default', + element: + } + ] + }, + { + path: 'sample-page', + element: + }, + { + path: 'shadow', + element: + }, + { + path: 'typography', + element: + }, + { + path: 'icons/ant', + element: + } + ] +}; + +export default MainRoutes; diff --git a/client/src/routes/index.jsx b/client/src/routes/index.jsx new file mode 100644 index 0000000..4c82fe3 --- /dev/null +++ b/client/src/routes/index.jsx @@ -0,0 +1,11 @@ +import { useRoutes } from 'react-router-dom'; + +// project import +import LoginRoutes from './LoginRoutes'; +import MainRoutes from './MainRoutes'; + +// ==============================|| ROUTING RENDER ||============================== // + +export default function ThemeRoutes() { + return useRoutes([MainRoutes, LoginRoutes]); +} diff --git a/client/src/setupTests.jsx b/client/src/setupTests.jsx new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/client/src/setupTests.jsx @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/client/src/store/index.jsx b/client/src/store/index.jsx new file mode 100644 index 0000000..736fe07 --- /dev/null +++ b/client/src/store/index.jsx @@ -0,0 +1,15 @@ +// third-party +import { configureStore } from '@reduxjs/toolkit'; + +// project import +import reducers from './reducers'; + +// ==============================|| REDUX TOOLKIT - MAIN STORE ||============================== // + +const store = configureStore({ + reducer: reducers +}); + +const { dispatch } = store; + +export { store, dispatch }; diff --git a/client/src/store/reducers/actions.jsx b/client/src/store/reducers/actions.jsx new file mode 100644 index 0000000..d5a0eb8 --- /dev/null +++ b/client/src/store/reducers/actions.jsx @@ -0,0 +1,4 @@ +// action - account reducer +export const LOGIN = '@auth/LOGIN'; +export const LOGOUT = '@auth/LOGOUT'; +export const REGISTER = '@auth/REGISTER'; diff --git a/client/src/store/reducers/index.jsx b/client/src/store/reducers/index.jsx new file mode 100644 index 0000000..e5c3261 --- /dev/null +++ b/client/src/store/reducers/index.jsx @@ -0,0 +1,11 @@ +// third-party +import { combineReducers } from 'redux'; + +// project import +import menu from './menu'; + +// ==============================|| COMBINE REDUCERS ||============================== // + +const reducers = combineReducers({ menu }); + +export default reducers; diff --git a/client/src/store/reducers/menu.jsx b/client/src/store/reducers/menu.jsx new file mode 100644 index 0000000..00aba2e --- /dev/null +++ b/client/src/store/reducers/menu.jsx @@ -0,0 +1,38 @@ +// types +import { createSlice } from '@reduxjs/toolkit'; + +// initial state +const initialState = { + openItem: ['dashboard'], + openComponent: 'buttons', + drawerOpen: false, + componentDrawerOpen: true +}; + +// ==============================|| SLICE - MENU ||============================== // + +const menu = createSlice({ + name: 'menu', + initialState, + reducers: { + activeItem(state, action) { + state.openItem = action.payload.openItem; + }, + + activeComponent(state, action) { + state.openComponent = action.payload.openComponent; + }, + + openDrawer(state, action) { + state.drawerOpen = action.payload.drawerOpen; + }, + + openComponentDrawer(state, action) { + state.componentDrawerOpen = action.payload.componentDrawerOpen; + } + } +}); + +export default menu.reducer; + +export const { activeItem, activeComponent, openDrawer, openComponentDrawer } = menu.actions; diff --git a/client/src/themes/index.jsx b/client/src/themes/index.jsx new file mode 100644 index 0000000..4ef49ce --- /dev/null +++ b/client/src/themes/index.jsx @@ -0,0 +1,64 @@ +import PropTypes from 'prop-types'; +import { useMemo } from 'react'; + +// material-ui +import { CssBaseline, StyledEngineProvider } from '@mui/material'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + +// project import +import Palette from './palette'; +import Typography from './typography'; +import CustomShadows from './shadows'; +import componentsOverride from './overrides'; + +// ==============================|| DEFAULT THEME - MAIN ||============================== // + +export default function ThemeCustomization({ children }) { + const theme = Palette('light', 'default'); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const themeTypography = Typography(`'Public Sans', sans-serif`); + const themeCustomShadows = useMemo(() => CustomShadows(theme), [theme]); + + const themeOptions = useMemo( + () => ({ + breakpoints: { + values: { + xs: 0, + sm: 768, + md: 1024, + lg: 1266, + xl: 1536 + } + }, + direction: 'ltr', + mixins: { + toolbar: { + minHeight: 60, + paddingTop: 8, + paddingBottom: 8 + } + }, + palette: theme.palette, + customShadows: themeCustomShadows, + typography: themeTypography + }), + [theme, themeTypography, themeCustomShadows] + ); + + const themes = createTheme(themeOptions); + themes.components = componentsOverride(themes); + + return ( + + + + {children} + + + ); +} + +ThemeCustomization.propTypes = { + children: PropTypes.node +}; diff --git a/client/src/themes/overrides/Badge.jsx b/client/src/themes/overrides/Badge.jsx new file mode 100644 index 0000000..21e8a8c --- /dev/null +++ b/client/src/themes/overrides/Badge.jsx @@ -0,0 +1,15 @@ +// ==============================|| OVERRIDES - BADGE ||============================== // + +export default function Badge(theme) { + return { + MuiBadge: { + styleOverrides: { + standard: { + minWidth: theme.spacing(2), + height: theme.spacing(2), + padding: theme.spacing(0.5) + } + } + } + }; +} diff --git a/client/src/themes/overrides/Button.jsx b/client/src/themes/overrides/Button.jsx new file mode 100644 index 0000000..3cf97db --- /dev/null +++ b/client/src/themes/overrides/Button.jsx @@ -0,0 +1,28 @@ +// ==============================|| OVERRIDES - BUTTON ||============================== // + +export default function Button(theme) { + const disabledStyle = { + '&.Mui-disabled': { + backgroundColor: theme.palette.grey[200] + } + }; + + return { + MuiButton: { + defaultProps: { + disableElevation: true + }, + styleOverrides: { + root: { + fontWeight: 400 + }, + contained: { + ...disabledStyle + }, + outlined: { + ...disabledStyle + } + } + } + }; +} diff --git a/client/src/themes/overrides/CardContent.jsx b/client/src/themes/overrides/CardContent.jsx new file mode 100644 index 0000000..2edc544 --- /dev/null +++ b/client/src/themes/overrides/CardContent.jsx @@ -0,0 +1,16 @@ +// ==============================|| OVERRIDES - CARD CONTENT ||============================== // + +export default function CardContent() { + return { + MuiCardContent: { + styleOverrides: { + root: { + padding: 20, + '&:last-child': { + paddingBottom: 20 + } + } + } + } + }; +} diff --git a/client/src/themes/overrides/Checkbox.jsx b/client/src/themes/overrides/Checkbox.jsx new file mode 100644 index 0000000..8120949 --- /dev/null +++ b/client/src/themes/overrides/Checkbox.jsx @@ -0,0 +1,13 @@ +// ==============================|| OVERRIDES - CHECKBOX ||============================== // + +export default function Checkbox(theme) { + return { + MuiCheckbox: { + styleOverrides: { + root: { + color: theme.palette.secondary[300] + } + } + } + }; +} diff --git a/client/src/themes/overrides/Chip.jsx b/client/src/themes/overrides/Chip.jsx new file mode 100644 index 0000000..db0a0bb --- /dev/null +++ b/client/src/themes/overrides/Chip.jsx @@ -0,0 +1,40 @@ +// ==============================|| OVERRIDES - CHIP ||============================== // + +export default function Chip(theme) { + return { + MuiChip: { + styleOverrides: { + root: { + borderRadius: 4, + '&:active': { + boxShadow: 'none' + } + }, + sizeLarge: { + fontSize: '1rem', + height: 40 + }, + light: { + color: theme.palette.primary.main, + backgroundColor: theme.palette.primary.lighter, + borderColor: theme.palette.primary.light, + '&.MuiChip-lightError': { + color: theme.palette.error.main, + backgroundColor: theme.palette.error.lighter, + borderColor: theme.palette.error.light + }, + '&.MuiChip-lightSuccess': { + color: theme.palette.success.main, + backgroundColor: theme.palette.success.lighter, + borderColor: theme.palette.success.light + }, + '&.MuiChip-lightWarning': { + color: theme.palette.warning.main, + backgroundColor: theme.palette.warning.lighter, + borderColor: theme.palette.warning.light + } + } + } + } + }; +} diff --git a/client/src/themes/overrides/IconButton.jsx b/client/src/themes/overrides/IconButton.jsx new file mode 100644 index 0000000..648dba4 --- /dev/null +++ b/client/src/themes/overrides/IconButton.jsx @@ -0,0 +1,28 @@ +// ==============================|| OVERRIDES - ICON BUTTON ||============================== // + +export default function IconButton(theme) { + return { + MuiIconButton: { + styleOverrides: { + root: { + borderRadius: 4 + }, + sizeLarge: { + width: theme.spacing(5.5), + height: theme.spacing(5.5), + fontSize: '1.25rem' + }, + sizeMedium: { + width: theme.spacing(4.5), + height: theme.spacing(4.5), + fontSize: '1rem' + }, + sizeSmall: { + width: theme.spacing(3.75), + height: theme.spacing(3.75), + fontSize: '0.75rem' + } + } + } + }; +} diff --git a/client/src/themes/overrides/InputLabel.jsx b/client/src/themes/overrides/InputLabel.jsx new file mode 100644 index 0000000..d2300a4 --- /dev/null +++ b/client/src/themes/overrides/InputLabel.jsx @@ -0,0 +1,25 @@ +// ==============================|| OVERRIDES - INPUT LABEL ||============================== // + +export default function InputLabel(theme) { + return { + MuiInputLabel: { + styleOverrides: { + root: { + color: theme.palette.grey[600] + }, + outlined: { + lineHeight: '0.8em', + '&.MuiInputLabel-sizeSmall': { + lineHeight: '1em' + }, + '&.MuiInputLabel-shrink': { + background: theme.palette.background.paper, + padding: '0 8px', + marginLeft: -6, + lineHeight: '1.4375em' + } + } + } + } + }; +} diff --git a/client/src/themes/overrides/LinearProgress.jsx b/client/src/themes/overrides/LinearProgress.jsx new file mode 100644 index 0000000..ffbe1ef --- /dev/null +++ b/client/src/themes/overrides/LinearProgress.jsx @@ -0,0 +1,17 @@ +// ==============================|| OVERRIDES - LINER PROGRESS ||============================== // + +export default function LinearProgress() { + return { + MuiLinearProgress: { + styleOverrides: { + root: { + height: 6, + borderRadius: 100 + }, + bar: { + borderRadius: 100 + } + } + } + }; +} diff --git a/client/src/themes/overrides/Link.jsx b/client/src/themes/overrides/Link.jsx new file mode 100644 index 0000000..16279df --- /dev/null +++ b/client/src/themes/overrides/Link.jsx @@ -0,0 +1,11 @@ +// ==============================|| OVERRIDES - LINK ||============================== // + +export default function Link() { + return { + MuiLink: { + defaultProps: { + underline: 'hover' + } + } + }; +} diff --git a/client/src/themes/overrides/ListItemIcon.jsx b/client/src/themes/overrides/ListItemIcon.jsx new file mode 100644 index 0000000..9001ab6 --- /dev/null +++ b/client/src/themes/overrides/ListItemIcon.jsx @@ -0,0 +1,13 @@ +// ==============================|| OVERRIDES - LIST ITEM ICON ||============================== // + +export default function ListItemIcon() { + return { + MuiListItemIcon: { + styleOverrides: { + root: { + minWidth: 24 + } + } + } + }; +} diff --git a/client/src/themes/overrides/OutlinedInput.jsx b/client/src/themes/overrides/OutlinedInput.jsx new file mode 100644 index 0000000..96709f2 --- /dev/null +++ b/client/src/themes/overrides/OutlinedInput.jsx @@ -0,0 +1,47 @@ +// material-ui +import { alpha } from '@mui/material/styles'; + +// ==============================|| OVERRIDES - OUTLINED INPUT ||============================== // + +export default function OutlinedInput(theme) { + return { + MuiOutlinedInput: { + styleOverrides: { + input: { + padding: '10.5px 14px 10.5px 12px' + }, + notchedOutline: { + borderColor: theme.palette.grey[300] + }, + root: { + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.primary.light + }, + '&.Mui-focused': { + boxShadow: `0 0 0 2px ${alpha(theme.palette.primary.main, 0.2)}`, + '& .MuiOutlinedInput-notchedOutline': { + border: `1px solid ${theme.palette.primary.light}` + } + }, + '&.Mui-error': { + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.error.light + }, + '&.Mui-focused': { + boxShadow: `0 0 0 2px ${alpha(theme.palette.error.main, 0.2)}`, + '& .MuiOutlinedInput-notchedOutline': { + border: `1px solid ${theme.palette.error.light}` + } + } + } + }, + inputSizeSmall: { + padding: '7.5px 8px 7.5px 12px' + }, + inputMultiline: { + padding: 0 + } + } + } + }; +} diff --git a/client/src/themes/overrides/Tab.jsx b/client/src/themes/overrides/Tab.jsx new file mode 100644 index 0000000..dbd899f --- /dev/null +++ b/client/src/themes/overrides/Tab.jsx @@ -0,0 +1,14 @@ +// ==============================|| OVERRIDES - TAB ||============================== // + +export default function Tab(theme) { + return { + MuiTab: { + styleOverrides: { + root: { + minHeight: 46, + color: theme.palette.text.primary + } + } + } + }; +} diff --git a/client/src/themes/overrides/TableCell.jsx b/client/src/themes/overrides/TableCell.jsx new file mode 100644 index 0000000..581a364 --- /dev/null +++ b/client/src/themes/overrides/TableCell.jsx @@ -0,0 +1,20 @@ +// ==============================|| OVERRIDES - TABLE CELL ||============================== // + +export default function TableCell(theme) { + return { + MuiTableCell: { + styleOverrides: { + root: { + fontSize: '0.875rem', + padding: 12, + borderColor: theme.palette.divider + }, + head: { + fontWeight: 600, + paddingTop: 20, + paddingBottom: 20 + } + } + } + }; +} diff --git a/client/src/themes/overrides/Tabs.jsx b/client/src/themes/overrides/Tabs.jsx new file mode 100644 index 0000000..8e906cc --- /dev/null +++ b/client/src/themes/overrides/Tabs.jsx @@ -0,0 +1,13 @@ +// ==============================|| OVERRIDES - TABS ||============================== // + +export default function Tabs() { + return { + MuiTabs: { + styleOverrides: { + vertical: { + overflow: 'visible' + } + } + } + }; +} diff --git a/client/src/themes/overrides/Typography.jsx b/client/src/themes/overrides/Typography.jsx new file mode 100644 index 0000000..9c95a37 --- /dev/null +++ b/client/src/themes/overrides/Typography.jsx @@ -0,0 +1,13 @@ +// ==============================|| OVERRIDES - TYPOGRAPHY ||============================== // + +export default function Typography() { + return { + MuiTypography: { + styleOverrides: { + gutterBottom: { + marginBottom: 12 + } + } + } + }; +} diff --git a/client/src/themes/overrides/index.jsx b/client/src/themes/overrides/index.jsx new file mode 100644 index 0000000..e5e39e7 --- /dev/null +++ b/client/src/themes/overrides/index.jsx @@ -0,0 +1,41 @@ +// third-party +import { merge } from 'lodash'; + +// project import +import Badge from './Badge'; +import Button from './Button'; +import CardContent from './CardContent'; +import Checkbox from './Checkbox'; +import Chip from './Chip'; +import IconButton from './IconButton'; +import InputLabel from './InputLabel'; +import LinearProgress from './LinearProgress'; +import Link from './Link'; +import ListItemIcon from './ListItemIcon'; +import OutlinedInput from './OutlinedInput'; +import Tab from './Tab'; +import TableCell from './TableCell'; +import Tabs from './Tabs'; +import Typography from './Typography'; + +// ==============================|| OVERRIDES - MAIN ||============================== // + +export default function ComponentsOverrides(theme) { + return merge( + Button(theme), + Badge(theme), + CardContent(), + Checkbox(theme), + Chip(theme), + IconButton(theme), + InputLabel(theme), + LinearProgress(), + Link(), + ListItemIcon(), + OutlinedInput(theme), + Tab(theme), + TableCell(theme), + Tabs(), + Typography() + ); +} diff --git a/client/src/themes/palette.jsx b/client/src/themes/palette.jsx new file mode 100644 index 0000000..7a1b2f3 --- /dev/null +++ b/client/src/themes/palette.jsx @@ -0,0 +1,60 @@ +// material-ui +import { createTheme } from '@mui/material/styles'; + +// third-party +import { presetPalettes } from '@ant-design/colors'; + +// project import +import ThemeOption from './theme'; + +// ==============================|| DEFAULT THEME - PALETTE ||============================== // + +const Palette = (mode) => { + const colors = presetPalettes; + + const greyPrimary = [ + '#ffffff', + '#fafafa', + '#f5f5f5', + '#f0f0f0', + '#d9d9d9', + '#bfbfbf', + '#8c8c8c', + '#595959', + '#262626', + '#141414', + '#000000' + ]; + const greyAscent = ['#fafafa', '#bfbfbf', '#434343', '#1f1f1f']; + const greyConstant = ['#fafafb', '#e6ebf1']; + + colors.grey = [...greyPrimary, ...greyAscent, ...greyConstant]; + + const paletteColor = ThemeOption(colors); + + return createTheme({ + palette: { + mode, + common: { + black: '#000', + white: '#fff' + }, + ...paletteColor, + text: { + primary: paletteColor.grey[700], + secondary: paletteColor.grey[500], + disabled: paletteColor.grey[400] + }, + action: { + disabled: paletteColor.grey[300] + }, + divider: paletteColor.grey[200], + background: { + paper: paletteColor.grey[0], + default: paletteColor.grey.A50 + } + } + }); +}; + +export default Palette; diff --git a/client/src/themes/shadows.jsx b/client/src/themes/shadows.jsx new file mode 100644 index 0000000..712551e --- /dev/null +++ b/client/src/themes/shadows.jsx @@ -0,0 +1,13 @@ +// material-ui +import { alpha } from '@mui/material/styles'; + +// ==============================|| DEFAULT THEME - CUSTOM SHADOWS ||============================== // + +const CustomShadows = (theme) => ({ + button: `0 2px #0000000b`, + text: `0 -1px 0 rgb(0 0 0 / 12%)`, + z1: `0px 2px 8px ${alpha(theme.palette.grey[900], 0.15)}` + // only available in paid version +}); + +export default CustomShadows; diff --git a/client/src/themes/theme/index.jsx b/client/src/themes/theme/index.jsx new file mode 100644 index 0000000..03347aa --- /dev/null +++ b/client/src/themes/theme/index.jsx @@ -0,0 +1,92 @@ +// ==============================|| PRESET THEME - THEME SELECTOR ||============================== // + +const Theme = (colors) => { + const { blue, red, gold, cyan, green, grey } = colors; + const greyColors = { + 0: grey[0], + 50: grey[1], + 100: grey[2], + 200: grey[3], + 300: grey[4], + 400: grey[5], + 500: grey[6], + 600: grey[7], + 700: grey[8], + 800: grey[9], + 900: grey[10], + A50: grey[15], + A100: grey[11], + A200: grey[12], + A400: grey[13], + A700: grey[14], + A800: grey[16] + }; + const contrastText = '#fff'; + + return { + primary: { + lighter: '#C8A2C8', + 100: '#B785B7', + 200: '#B785B7', + light: '#A668A6', + 400: '#A668A6', + main: '#8D538D', + dark: '#704270', + 700: '#533153', + darker:'#362036', + 900: '#1A0E1A', + contrastText + }, + secondary: { + lighter: greyColors[100], + 100: greyColors[100], + 200: greyColors[200], + light: greyColors[300], + 400: greyColors[400], + main: greyColors[500], + 600: greyColors[600], + dark: greyColors[700], + 800: greyColors[800], + darker: greyColors[900], + A100: greyColors[0], + A200: greyColors.A400, + A300: greyColors.A700, + contrastText: greyColors[0] + }, + error: { + lighter: red[0], + light: red[2], + main: red[4], + dark: red[7], + darker: red[9], + contrastText + }, + warning: { + lighter: gold[0], + light: gold[3], + main: gold[5], + dark: gold[7], + darker: gold[9], + contrastText: greyColors[100] + }, + info: { + lighter: cyan[0], + light: cyan[3], + main: cyan[5], + dark: cyan[7], + darker: cyan[9], + contrastText + }, + success: { + lighter: green[0], + light: green[3], + main: green[5], + dark: green[7], + darker: green[9], + contrastText + }, + grey: greyColors + }; +}; + +export default Theme; diff --git a/client/src/themes/typography.jsx b/client/src/themes/typography.jsx new file mode 100644 index 0000000..448825f --- /dev/null +++ b/client/src/themes/typography.jsx @@ -0,0 +1,71 @@ +// ==============================|| DEFAULT THEME - TYPOGRAPHY ||============================== // + +const Typography = (fontFamily) => ({ + htmlFontSize: 16, + fontFamily, + fontWeightLight: 300, + fontWeightRegular: 400, + fontWeightMedium: 500, + fontWeightBold: 600, + h1: { + fontWeight: 600, + fontSize: '2.375rem', + lineHeight: 1.21 + }, + h2: { + fontWeight: 600, + fontSize: '1.875rem', + lineHeight: 1.27 + }, + h3: { + fontWeight: 600, + fontSize: '1.5rem', + lineHeight: 1.33 + }, + h4: { + fontWeight: 600, + fontSize: '1.25rem', + lineHeight: 1.4 + }, + h5: { + fontWeight: 600, + fontSize: '1rem', + lineHeight: 1.5 + }, + h6: { + fontWeight: 400, + fontSize: '0.875rem', + lineHeight: 1.57 + }, + caption: { + fontWeight: 400, + fontSize: '0.75rem', + lineHeight: 1.66 + }, + body1: { + fontSize: '0.875rem', + lineHeight: 1.57 + }, + body2: { + fontSize: '0.75rem', + lineHeight: 1.66 + }, + subtitle1: { + fontSize: '0.875rem', + fontWeight: 600, + lineHeight: 1.57 + }, + subtitle2: { + fontSize: '0.75rem', + fontWeight: 500, + lineHeight: 1.66 + }, + overline: { + lineHeight: 1.66 + }, + button: { + textTransform: 'capitalize' + } +}); + +export default Typography; diff --git a/client/src/utils/SyntaxHighlight.jsx b/client/src/utils/SyntaxHighlight.jsx new file mode 100644 index 0000000..a2f0088 --- /dev/null +++ b/client/src/utils/SyntaxHighlight.jsx @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types'; + +// third-party +import SyntaxHighlighter from 'react-syntax-highlighter'; +import { a11yDark } from 'react-syntax-highlighter/dist/esm/styles/hljs'; + +// ==============================|| CODE HIGHLIGHTER ||============================== // + +export default function SyntaxHighlight({ children, ...others }) { + return ( + + {children} + + ); +} + +SyntaxHighlight.propTypes = { + children: PropTypes.node +}; diff --git a/client/src/utils/password-strength.jsx b/client/src/utils/password-strength.jsx new file mode 100644 index 0000000..51e812d --- /dev/null +++ b/client/src/utils/password-strength.jsx @@ -0,0 +1,29 @@ +// has number +const hasNumber = (number) => new RegExp(/[0-9]/).test(number); + +// has mix of small and capitals +const hasMixed = (number) => new RegExp(/[a-z]/).test(number) && new RegExp(/[A-Z]/).test(number); + +// has special chars +const hasSpecial = (number) => new RegExp(/[!#@$%^&*)(+=._-]/).test(number); + +// set color based on password strength +export const strengthColor = (count) => { + if (count < 2) return { label: 'Poor', color: 'error.main' }; + if (count < 3) return { label: 'Weak', color: 'warning.main' }; + if (count < 4) return { label: 'Normal', color: 'warning.dark' }; + if (count < 5) return { label: 'Good', color: 'success.main' }; + if (count < 6) return { label: 'Strong', color: 'success.dark' }; + return { label: 'Poor', color: 'error.main' }; +}; + +// password strength indicator +export const strengthIndicator = (number) => { + let strengths = 0; + if (number.length > 5) strengths += 1; + if (number.length > 7) strengths += 1; + if (hasNumber(number)) strengths += 1; + if (hasSpecial(number)) strengths += 1; + if (hasMixed(number)) strengths += 1; + return strengths; +}; diff --git a/dockerfile b/dockerfile index 76766a3..55d383b 100644 --- a/dockerfile +++ b/dockerfile @@ -5,6 +5,7 @@ FROM debian WORKDIR /app COPY build/cosmos . +COPY static . VOLUME /config diff --git a/dockerfile.arm64 b/dockerfile.arm64 index 48e1d0d..b479a6f 100644 --- a/dockerfile.arm64 +++ b/dockerfile.arm64 @@ -5,6 +5,7 @@ FROM amd64/debian WORKDIR /app COPY build/cosmos . +COPY static . VOLUME /config diff --git a/gupm.json b/gupm.json index c9394af..f2efa72 100644 --- a/gupm.json +++ b/gupm.json @@ -20,13 +20,71 @@ "go://github.com/joho/godotenv": "master", "go://github.com/lib/pq": "master", "go://github.com/pquerna/ffjson": "master", + "go://github.com/roberthodgen/spa-server": "master", "go://go.mongodb.org/mongo-driver": "master", - "go://gopkg.in/ffmt.v1": "v1.5.6" + "go://gopkg.in/ffmt.v1": "v1.5.6", + "npm://@ant-design/colors": "^6.0.0", + "npm://@ant-design/icons": "^4.7.0", + "npm://@babel/core": "^7.19.1", + "npm://@babel/eslint-parser": "^7.19.1", + "npm://@emotion/cache": "^11.10.3", + "npm://@emotion/react": "^11.10.4", + "npm://@emotion/styled": "^11.10.4", + "npm://@esbuild/linux-x64": "0.16.17", + "npm://@mui/lab": "^5.0.0-alpha.100", + "npm://@mui/material": "^5.10.6", + "npm://@reduxjs/toolkit": "^1.8.5", + "npm://@testing-library/jest-dom": "^5.16.5", + "npm://@testing-library/react": "^13.4.0", + "npm://@testing-library/user-event": "^14.4.3", + "npm://@vitejs/plugin-react": "3.1.0", + "npm://apexcharts": "^3.35.5", + "npm://eslint": "^8.23.1", + "npm://eslint-config-airbnb-typescript": "^17.0.0", + "npm://eslint-config-prettier": "^8.5.0", + "npm://eslint-config-react-app": "7.0.1", + "npm://eslint-import-resolver-typescript": "3.5.1", + "npm://eslint-plugin-flowtype": "^8.0.3", + "npm://eslint-plugin-import": "^2.26.0", + "npm://eslint-plugin-jsx-a11y": "6.6.1", + "npm://eslint-plugin-prettier": "^4.2.1", + "npm://eslint-plugin-react": "^7.31.8", + "npm://eslint-plugin-react-hooks": "4.6.0", + "npm://express": "4.18.2", + "npm://express-ws": "5.0.2", + "npm://formik": "^2.2.9", + "npm://framer-motion": "^7.3.6", + "npm://history": "^5.3.0", + "npm://lodash": "^4.17.21", + "npm://prettier": "2.7.1", + "npm://prop-types": "^15.8.1", + "npm://react": "^18.2.0", + "npm://react-apexcharts": "^1.4.0", + "npm://react-copy-to-clipboard": "^5.1.0", + "npm://react-device-detect": "^2.2.2", + "npm://react-dom": "^18.2.0", + "npm://react-draggable": "^4.4.5", + "npm://react-element-to-jsx-string": "^15.0.0", + "npm://react-number-format": "^4.9.4", + "npm://react-perfect-scrollbar": "^1.5.8", + "npm://react-redux": "^8.0.4", + "npm://react-router": "^6.4.1", + "npm://react-router-dom": "^6.4.1", + "npm://react-scripts": "^5.0.1", + "npm://react-syntax-highlighter": "^15.5.0", + "npm://react-window": "^1.8.7", + "npm://redux": "^4.2.0", + "npm://simplebar": "^5.3.8", + "npm://simplebar-react": "^2.4.1", + "npm://typescript": "4.8.3", + "npm://vite": "4.1.4", + "npm://web-vitals": "^3.0.2", + "npm://yup": "^0.32.11" }, "defaultProvider": "go" }, "description": "Cosmos Server", "name": "cosmos-server", - "version": "0.0.1", + "version": "0.0.2", "wrapInstallFolder": "src" } \ No newline at end of file diff --git a/readme.md b/readme.md index aeadf26..a8c3652 100644 --- a/readme.md +++ b/readme.md @@ -10,12 +10,13 @@ Disclaimer: Cosmos is still in early Alpha stage, please be careful when you use Looking for a **secure** and **robust** way to run your **self-hosted applications**? With **Cosmos**, you can take control of your data and privacy without sacrificing security and stability. -Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex** or **HomeAssistant**, Cosmos is the perfect solution to secure it all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box. +Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex**, **HomeAssistant** or even a blog, Cosmos is the perfect solution to secure it all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box. * **Authentication** Connect to all your application with the same account, including strong security and multi-factor authentication * **Automatic HTTPS** certificates provision * **Anti-bot** protections such as Captcha and IP rate limiting * **Anti-DDOS** protections such as variable timeouts/throttling, IP rate limiting and IP blacklisting + * **Proper user management** to invite your friends and family to your applications without awkardly sharing credentials. Let them request a password change with an email rather than having you unlock their account manually! And a **lot more planned features** are coming! diff --git a/src/httpServer.go b/src/httpServer.go index 9215fd9..734d774 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -11,9 +11,11 @@ import ( "regexp" "time" "encoding/json" + "os" "github.com/go-chi/chi/middleware" "github.com/go-chi/httprate" "crypto/tls" + spa "github.com/roberthodgen/spa-server" ) var serverPortHTTP = "" @@ -150,24 +152,25 @@ func StartServer() { utils.Log("Saved new Auth ED25519 certificate") } - router := proxy.BuildFromConfig(config.ProxyConfig) + router := mux.NewRouter().StrictSlash(true) router.Use(middleware.Recoverer) router.Use(middleware.Logger) router.Use(tokenMiddleware) router.Use(utils.SetSecurityHeaders) + + srapi := router.PathPrefix("/cosmos").Subrouter() - srapi := router.PathPrefix("/api").Subrouter() + srapi.HandleFunc("/api/login", user.UserLogin) + srapi.HandleFunc("/api/logout", user.UserLogout) + srapi.HandleFunc("/api/register", user.UserRegister) + srapi.HandleFunc("/api/invite", user.UserResendInviteLink) + srapi.HandleFunc("/api/me", user.Me) - srapi.HandleFunc("/login", user.UserLogin) - srapi.HandleFunc("/logout", user.UserLogout) - srapi.HandleFunc("/register", user.UserRegister) - srapi.HandleFunc("/invite", user.UserResendInviteLink) + srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute) + srapi.HandleFunc("/api/users", user.UsersRoute) - srapi.HandleFunc("/users/{nickname}", user.UsersIdRoute) - srapi.HandleFunc("/users", user.UsersRoute) - - srapi.Use(utils.AcceptHeader("application/json")) + // srapi.Use(utils.AcceptHeader("*/*")) srapi.Use(utils.CORSHeader(utils.GetMainConfig().HTTPConfig.Hostname)) srapi.Use(utils.MiddlewareTimeout(5 * time.Second)) srapi.Use(httprate.Limit(20, 1*time.Minute, @@ -179,6 +182,16 @@ func StartServer() { return }), )) + + pwd,_ := os.Getwd() + utils.Log("Starting in " + pwd) + if _, err := os.Stat(pwd + "/static"); os.IsNotExist(err) { + utils.Fatal("Static folder not found at " + pwd + "/static", err) + } + fs := spa.SpaHandler(pwd + "/static", "index.html") + router.PathPrefix("/").Handler(fs) + + router = proxy.BuildFromConfig(router, config.ProxyConfig) if tlsCert != "" && tlsKey != "" { utils.Log("TLS certificate exist, starting HTTPS servers and redirecting HTTP to HTTPS") diff --git a/src/proxy/buildFromConfig.go b/src/proxy/buildFromConfig.go index 0020d1d..8c9659d 100644 --- a/src/proxy/buildFromConfig.go +++ b/src/proxy/buildFromConfig.go @@ -6,8 +6,7 @@ import ( "../utils" ) -func BuildFromConfig(config utils.ProxyConfig) *mux.Router { - router := mux.NewRouter().StrictSlash(true) +func BuildFromConfig(router *mux.Router, config utils.ProxyConfig) *mux.Router { router.HandleFunc("/_health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) diff --git a/src/proxy/routerGen.go b/src/proxy/routerGen.go index 3ef5a0e..57677f8 100644 --- a/src/proxy/routerGen.go +++ b/src/proxy/routerGen.go @@ -21,9 +21,11 @@ func RouterGen(route utils.Route, router *mux.Router, destination *httputil.Reve if(route.UsePathPrefix) { origin = origin.PathPrefix(route.PathPrefix) + } + + if(route.UsePathPrefix && route.StripPathPrefix) { realDestination = http.StripPrefix(route.PathPrefix, destination) } - timeout := route.Timeout if(timeout == 0) { diff --git a/src/user/get.go b/src/user/get.go index 1a2fa35..eef542a 100644 --- a/src/user/get.go +++ b/src/user/get.go @@ -10,6 +10,10 @@ import ( func UserGet(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) nickname := utils.Sanitize(vars["nickname"]) + + if nickname == "" && req.Header.Get("x-cosmos-user") != "" { + nickname = req.Header.Get("x-cosmos-user") + } if AdminOrItselfOnly(w, req, nickname) != nil { return diff --git a/src/user/me.go b/src/user/me.go new file mode 100644 index 0000000..f68fb1d --- /dev/null +++ b/src/user/me.go @@ -0,0 +1,17 @@ +package user + +import ( + "net/http" + "../utils" +) + + +func Me(w http.ResponseWriter, req *http.Request) { + if (req.Method == "GET") { + UserGet(w, req) + } else { + utils.Error("UserRoute: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/user/token.go b/src/user/token.go index 3697f1e..b144212 100644 --- a/src/user/token.go +++ b/src/user/token.go @@ -165,7 +165,8 @@ func loggedInOnly(w http.ResponseWriter, req *http.Request) error { if !isUserLoggedIn || userNickname == "" { utils.Error("LoggedInOnly: User is not logged in", nil) - http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound) + //http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound) + utils.HTTPError(w, "User not logged in", http.StatusUnauthorized, "HTTP004") return errors.New("User not logged in") } @@ -180,13 +181,14 @@ func AdminOnly(w http.ResponseWriter, req *http.Request) error { if !isUserLoggedIn || userNickname == "" { utils.Error("AdminOnly: User is not logged in", nil) - http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound) + //http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound) + utils.HTTPError(w, "User not logged in", http.StatusUnauthorized, "HTTP004") return errors.New("User not logged in") } if isUserLoggedIn && !isUserAdmin { utils.Error("AdminOnly: User is not admin", nil) - utils.HTTPError(w, "Unauthorized", http.StatusUnauthorized, "HTTP002") + utils.HTTPError(w, "User unauthorized", http.StatusUnauthorized, "HTTP005") return errors.New("User not Admin") } @@ -201,13 +203,13 @@ func AdminOrItselfOnly(w http.ResponseWriter, req *http.Request, nickname string if !isUserLoggedIn || userNickname == "" { utils.Error("AdminOrItselfOnly: User is not logged in", nil) - http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound) + utils.HTTPError(w, "User not logged in", http.StatusUnauthorized, "HTTP004") return errors.New("User not logged in") } if nickname != userNickname && !isUserAdmin { utils.Error("AdminOrItselfOnly: User is not admin", nil) - utils.HTTPError(w, "Unauthorized", http.StatusUnauthorized, "HTTP002") + utils.HTTPError(w, "User unauthorized", http.StatusUnauthorized, "HTTP005") return errors.New("User not Admin") } diff --git a/src/utils/db.go b/src/utils/db.go index 555cbe1..4babe41 100644 --- a/src/utils/db.go +++ b/src/utils/db.go @@ -14,7 +14,7 @@ var client *mongo.Client func DB() { Log("Connecting to the database...") - uri := os.Getenv("MONGODB") + "/?retryWrites=true&w=majority" + uri := MainConfig.MongoDB + "/?retryWrites=true&w=majority" var err error diff --git a/src/utils/log.go b/src/utils/log.go index b5cd2c9..a73906e 100644 --- a/src/utils/log.go +++ b/src/utils/log.go @@ -37,14 +37,22 @@ func Warn(message string) { func Error(message string, err error) { ll := LoggingLevelLabels[GetMainConfig().LoggingLevel] + errStr := "" + if err != nil { + errStr = err.Error() + } if ll <= ERROR { - log.Println(Red + "[ERROR] " + message + " : " + err.Error() + Reset) + log.Println(Red + "[ERROR] " + message + " : " + errStr + Reset) } } func Fatal(message string, err error) { ll := LoggingLevelLabels[GetMainConfig().LoggingLevel] + errStr := "" + if err != nil { + errStr = err.Error() + } if ll <= ERROR { - log.Fatal(Red + "[FATAL] " + message + " : " + err.Error() + Reset) + log.Fatal(Red + "[FATAL] " + message + " : " + errStr + Reset) } } diff --git a/src/utils/types.go b/src/utils/types.go index 71f76ed..1496355 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -51,6 +51,7 @@ type User struct { type Config struct { LoggingLevel LoggingLevel `validate:"oneof=DEBUG INFO WARNING ERROR"` + MongoDB string HTTPConfig HTTPConfig } @@ -85,4 +86,6 @@ type Route struct { ThrottlePerMinute int SPAMode bool CORSOrigin string + StripPathPrefix bool + Static bool } \ No newline at end of file diff --git a/src/utils/utils.go b/src/utils/utils.go index a3b9ac6..255c439 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -114,6 +114,9 @@ func LoadBaseMainConfig(config Config){ if os.Getenv("LOG_LEVEL") != "" { MainConfig.LoggingLevel = (LoggingLevel)(os.Getenv("LOG_LEVEL")) } + if os.Getenv("MONGODB") != "" { + MainConfig.MongoDB = os.Getenv("MONGODB") + } } func GetMainConfig() Config { diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..12c14e3 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + root: 'client', + build: { + outDir: '../static', + }, + server: { + proxy: { + '/cosmos/api': { + target: 'https://localhost:8443', + secure: false, + } + } + } +})