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 (
+
+
+
+ );
+};
+
+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.
+ *
+ *
+ *
+ */
+ <>
+
+ 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
+
+
+
+
+ Pro
+
+
+
+
+);
+
+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 (
+
+ {value === index && children}
+
+ );
+}
+
+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 = () => (
+
+ {/*
+ */}
+
+);
+
+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 (
+
+ );
+};
+
+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 }) => (
+
+ )}
+
+ >
+ );
+};
+
+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 }) => (
+
+ )}
+
+ >
+ );
+};
+
+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 (
+
+ }
+ onClick={googleHandler}
+ >
+ {!matchDownSM && 'Google'}
+
+ }
+ onClick={twitterHandler}
+ >
+ {!matchDownSM && 'Twitter'}
+
+ }
+ onClick={facebookHandler}
+ >
+ {!matchDownSM && 'Facebook'}
+
+
+ );
+};
+
+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
+
+
+
+ setSlot('month')}
+ color={slot === 'month' ? 'primary' : 'secondary'}
+ variant={slot === 'month' ? 'outlined' : 'text'}
+ >
+ Month
+
+ setSlot('week')}
+ color={slot === 'week' ? 'primary' : 'secondary'}
+ variant={slot === 'week' ? 'outlined' : 'text'}
+ >
+ Week
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Need Help?
+
+
+
+
+
+
+ >
+ );
+};
+
+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,
+ }
+ }
+ }
+})