From 8e3d59c1b785b31baaa60a532f98b67e7bd33335 Mon Sep 17 00:00:00 2001 From: Anton Karpenko Date: Fri, 23 Apr 2021 02:31:39 +0300 Subject: [PATCH] feat: AppForm component feat: Auth forms feat: Prettier feat: additiona scripts fix: AppButton refactor: AppIcon with additional icons --- .prettierrc.js | 12 + package.json | 6 +- src/components/AppAlert/AppAlert.tsx | 26 +- src/components/AppButton/AppButton.test.tsx | 118 ++++---- src/components/AppButton/AppButton.tsx | 78 ++--- src/components/AppForm/AppForm.tsx | 32 +++ src/components/AppForm/index.tsx | 3 + src/components/AppIcon/AppIcon.tsx | 56 ++-- src/components/AppIcon/index.tsx | 2 +- .../AppIconButton/AppIconButton.tsx | 48 ++-- src/components/AppLink/AppLink.test.tsx | 199 +++++++------ src/components/AppLink/AppLink.tsx | 66 ++--- src/components/AppLink/index.tsx | 2 +- src/components/ErrorBoundary.tsx | 70 ++--- src/components/NotImplemented.tsx | 2 +- src/components/SideBar/SideBar.tsx | 163 ++++++----- src/components/SideBar/SideBarLink.tsx | 34 +-- src/components/SideBar/SideBarNavigation.tsx | 110 ++++---- src/components/SideBar/index.tsx | 4 +- src/components/TopBar/TopBar.tsx | 106 +++---- src/components/TopBar/index.tsx | 4 +- src/components/UserInfo/UserInfo.tsx | 70 ++--- src/components/UserInfo/index.tsx | 4 +- src/components/index.tsx | 3 +- src/index.css | 8 +- src/routes/AppRouter.tsx | 2 +- src/routes/Layout/PrivateLayout.tsx | 178 ++++++------ src/routes/Layout/PublicLayout.tsx | 225 ++++++++------- src/routes/Layout/index.tsx | 2 +- src/routes/PrivateRoutes.tsx | 22 +- src/routes/PublicRoutes.tsx | 27 +- src/routes/Routes.tsx | 38 +-- src/store/AppReducer.ts | 62 ++-- src/store/AppStore.tsx | 41 ++- src/theme.tsx | 68 ++--- src/utils/date.ts | 2 +- src/utils/form.ts | 4 +- src/utils/localStorage.ts | 4 +- src/utils/style.ts | 3 +- src/utils/type.ts | 1 - src/views/About/About.tsx | 221 +++++++-------- src/views/About/index.tsx | 12 +- src/views/Auth/Auth.tsx | 27 ++ src/views/Auth/Login/Email.tsx | 123 ++++++++ src/views/Auth/Login/index.tsx | 17 ++ src/views/Auth/Recovery/Password.tsx | 78 +++++ src/views/Auth/Recovery/index.tsx | 17 ++ src/views/Auth/Signup/ConfirmEmail.tsx | 61 ++++ src/views/Auth/Signup/Signup.tsx | 266 ++++++++++++++++++ src/views/Auth/Signup/index.tsx | 19 ++ src/views/Auth/index.tsx | 22 ++ src/views/NotFound.tsx | 2 +- src/views/NotImplemented.tsx | 48 ++-- src/views/index.tsx | 3 +- yarn.lock | 5 + 55 files changed, 1749 insertions(+), 1077 deletions(-) create mode 100644 .prettierrc.js create mode 100644 src/components/AppForm/AppForm.tsx create mode 100644 src/components/AppForm/index.tsx create mode 100644 src/views/Auth/Auth.tsx create mode 100644 src/views/Auth/Login/Email.tsx create mode 100644 src/views/Auth/Login/index.tsx create mode 100644 src/views/Auth/Recovery/Password.tsx create mode 100644 src/views/Auth/Recovery/index.tsx create mode 100644 src/views/Auth/Signup/ConfirmEmail.tsx create mode 100644 src/views/Auth/Signup/Signup.tsx create mode 100644 src/views/Auth/Signup/index.tsx create mode 100644 src/views/Auth/index.tsx diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..0acd471 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,12 @@ +module.exports = { + printWidth: 120, // max 120 chars in line, code is easy to read + useTabs: false, // use spaces instead of tabs + tabWidth: 2, // "visual width" of of the "tab" + trailingComma: 'es5', // add trailing commas in objects, arrays, etc. + semi: true, // add ; when needed + singleQuote: true, // '' for stings instead of "" + bracketSpacing: true, // import { some } ... instead of import {some} ... + arrowParens: 'always', // braces even for single param in arrow functions (a) => { } + jsxSingleQuote: false, // "" for react props, like in html + jsxBracketSameLine: false, // pretty JSX +}; diff --git a/package.json b/package.json index 050476a..60bdbe2 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,15 @@ "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", - "@types/react-router-dom": "^5.1.7" + "@types/react-router-dom": "^5.1.7", + "prettier": "^2.2.1" }, "scripts": { "start": "react-scripts start", + "dev": "npm install -s && react-scripts start", "build": "react-scripts build", + "type": "tsc", + "format": "node_modules/.bin/prettier src --write", "test": "react-scripts test" }, "eslintConfig": { diff --git a/src/components/AppAlert/AppAlert.tsx b/src/components/AppAlert/AppAlert.tsx index a1c0cfd..89f32e2 100644 --- a/src/components/AppAlert/AppAlert.tsx +++ b/src/components/AppAlert/AppAlert.tsx @@ -6,26 +6,26 @@ const APP_ALERT_SEVERITY = 'info'; // 'error' | 'info'| 'success' | 'warning' const APP_ALERT_VARIANT = 'standard'; // 'filled' | 'outlined' | 'standard' const useStyles = makeStyles((theme: Theme) => ({ - root: { - marginTop: theme.spacing(1), - marginBottom: theme.spacing(1), - }, + root: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + }, })); /** * Application styled Alert component */ const AppAlert: React.FC = ({ - severity = APP_ALERT_SEVERITY, - variant = APP_ALERT_VARIANT, - className, - onClose, - ...restOfProps + severity = APP_ALERT_SEVERITY, + variant = APP_ALERT_VARIANT, + className, + onClose, + ...restOfProps }) => { - const classes = useStyles(); - const classRoot = clsx(classes.root, className); + const classes = useStyles(); + const classRoot = clsx(classes.root, className); - return ; + return ; }; -export default AppAlert; \ No newline at end of file +export default AppAlert; diff --git a/src/components/AppButton/AppButton.test.tsx b/src/components/AppButton/AppButton.test.tsx index f378541..7110a25 100644 --- a/src/components/AppButton/AppButton.test.tsx +++ b/src/components/AppButton/AppButton.test.tsx @@ -9,73 +9,73 @@ import { ColorName } from '../../utils/style'; * @param {boolean} [ignoreClassName] - optional flag to ignore className (color "inherit" doesn't use any class name) */ function testButtonColor(colorName: string, expectedClassName = colorName, ignoreClassName = false) { - it(`supports "${colorName}" color`, async () => { - let text = `${colorName} button`; - await render({text}); + it(`supports "${colorName}" color`, async () => { + let text = `${colorName} button`; + await render({text}); - let span = await screen.getByText(text); // with specific text - expect(span).toBeDefined(); + let span = await screen.getByText(text); // with specific text + expect(span).toBeDefined(); - let button = await span.closest('button'); // parent - - ); + const classes = useStyles(); + const classButton = clsx(classes[color as ColorName], className); + return ( + + + + ); }; -export default AppButton; \ No newline at end of file +export default AppButton; diff --git a/src/components/AppForm/AppForm.tsx b/src/components/AppForm/AppForm.tsx new file mode 100644 index 0000000..9538325 --- /dev/null +++ b/src/components/AppForm/AppForm.tsx @@ -0,0 +1,32 @@ +import { ReactNode, FormHTMLAttributes } from 'react'; +import { Grid } from '@material-ui/core'; +import { Theme, makeStyles } from '@material-ui/core/styles'; +import { formStyle } from '../../utils/style'; + +export const useStyles = makeStyles((theme: Theme) => ({ + formBody: { + ...formStyle(theme), + }, +})); + +interface Props extends FormHTMLAttributes { + children: ReactNode; +} + +/** + * Application styled Form container + */ +const AppForm: React.FC = ({ children, ...resOfProps }) => { + const classes = useStyles(); + return ( +
+ + + {children} + + +
+ ); +}; + +export default AppForm; diff --git a/src/components/AppForm/index.tsx b/src/components/AppForm/index.tsx new file mode 100644 index 0000000..f444fb3 --- /dev/null +++ b/src/components/AppForm/index.tsx @@ -0,0 +1,3 @@ +import AppForm from './AppForm'; + +export { AppForm as default, AppForm }; diff --git a/src/components/AppIcon/AppIcon.tsx b/src/components/AppIcon/AppIcon.tsx index abdbbbe..6647222 100644 --- a/src/components/AppIcon/AppIcon.tsx +++ b/src/components/AppIcon/AppIcon.tsx @@ -17,6 +17,8 @@ import HomeIcon from '@material-ui/icons/Home'; import AccountCircle from '@material-ui/icons/AccountCircle'; import PersonAddIcon from '@material-ui/icons/PersonAdd'; import PersonIcon from '@material-ui/icons/Person'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import NotificationsIcon from '@material-ui/icons/NotificationsOutlined'; /** * How to use: @@ -27,26 +29,28 @@ import PersonIcon from '@material-ui/icons/Person'; * Note: You can use camelCase or UPPERCASE in the component */ const ICONS: Record = { - default: DefaultIcon, - logo: () => ( - - - - ), - close: CloseIcon, - menu: MenuIcon, - settings: SettingsIcon, - visibilityon: VisibilityIcon, - visibilityoff: VisibilityOffIcon, - daynight: DayNightIcon, - night: NightIcon, - day: DayIcon, - search: SearchIcon, - info: InfoIcon, - home: HomeIcon, - account: AccountCircle, - signup: PersonAddIcon, - login: PersonIcon, + default: DefaultIcon, + logo: () => ( + + + + ), + close: CloseIcon, + menu: MenuIcon, + settings: SettingsIcon, + visibilityon: VisibilityIcon, + visibilityoff: VisibilityOffIcon, + daynight: DayNightIcon, + night: NightIcon, + day: DayIcon, + search: SearchIcon, + info: InfoIcon, + home: HomeIcon, + account: AccountCircle, + signup: PersonAddIcon, + login: PersonIcon, + logout: ExitToAppIcon, + notifications: NotificationsIcon, }; /** @@ -55,13 +59,13 @@ const ICONS: Record = { * @param {string} [props.icon] - name of the Icon to render */ interface Props { - name?: string; // Icon's name - icon?: string; // Icon's name alternate prop + name?: string; // Icon's name + icon?: string; // Icon's name alternate prop } const AppIcon: React.FC = ({ name, icon, ...restOfProps }) => { - const iconName = (name || icon || 'default').trim().toLowerCase(); - const ComponentToRender = ICONS[iconName] || DefaultIcon; - return ; + const iconName = (name || icon || 'default').trim().toLowerCase(); + const ComponentToRender = ICONS[iconName] || DefaultIcon; + return ; }; -export default AppIcon; \ No newline at end of file +export default AppIcon; diff --git a/src/components/AppIcon/index.tsx b/src/components/AppIcon/index.tsx index 88c2017..eef6266 100644 --- a/src/components/AppIcon/index.tsx +++ b/src/components/AppIcon/index.tsx @@ -1,3 +1,3 @@ import AppIcon from './AppIcon'; -export { AppIcon as default, AppIcon }; \ No newline at end of file +export { AppIcon as default, AppIcon }; diff --git a/src/components/AppIconButton/AppIconButton.tsx b/src/components/AppIconButton/AppIconButton.tsx index e1f658b..b03eb6c 100644 --- a/src/components/AppIconButton/AppIconButton.tsx +++ b/src/components/AppIconButton/AppIconButton.tsx @@ -6,16 +6,16 @@ import { ColorName, buttonStylesByNames } from '../../utils/style'; import AppIcon from '../AppIcon'; const useStyles = makeStyles((theme: Theme) => ({ - // Add styles for Material UI names 'primary', 'secondary', 'warning', and so on - ...buttonStylesByNames(theme), + // Add styles for Material UI names 'primary', 'secondary', 'warning', and so on + ...buttonStylesByNames(theme), })); function getValidMuiColor(color: string | undefined): PropTypes.Color | undefined { - if (color && ['inherit', 'primary', 'secondary', 'default'].includes(color)) { - return color as PropTypes.Color; - } else { - return undefined; - } + if (color && ['inherit', 'primary', 'secondary', 'default'].includes(color)) { + return color as PropTypes.Color; + } else { + return undefined; + } } /** @@ -23,26 +23,26 @@ function getValidMuiColor(color: string | undefined): PropTypes.Color | undefine * @param {string} [props.icon] - name of Icon to render inside the IconButton */ interface Props extends Omit { - color?: ColorName | 'inherit'; - icon?: string; - // Missing props - component?: React.ElementType; // Could be RouterLink, AppLink, etc. - to?: string; // Link prop - href?: string; // Link prop + color?: ColorName | 'inherit'; + icon?: string; + // Missing props + component?: React.ElementType; // Could be RouterLink, AppLink, etc. + to?: string; // Link prop + href?: string; // Link prop } const AppIconButton: React.FC = ({ color, className, children, icon, title, ...restOfProps }) => { - const classes = useStyles(); - const classButton = clsx(classes[color as ColorName], className); - const colorButton = getValidMuiColor(color); + const classes = useStyles(); + const classButton = clsx(classes[color as ColorName], className); + const colorButton = getValidMuiColor(color); - const renderIcon = () => ( - - - {children} - - ); + const renderIcon = () => ( + + + {children} + + ); - return title ? {renderIcon()} : renderIcon(); + return title ? {renderIcon()} : renderIcon(); }; -export default AppIconButton; \ No newline at end of file +export default AppIconButton; diff --git a/src/components/AppLink/AppLink.test.tsx b/src/components/AppLink/AppLink.test.tsx index 7f0dca0..2dde00b 100644 --- a/src/components/AppLink/AppLink.test.tsx +++ b/src/components/AppLink/AppLink.test.tsx @@ -4,108 +4,107 @@ import AppLink from './AppLink'; import { AppRouter } from '../../routes/'; describe('AppLink component', () => { + it('renders itself', async () => { + const text = 'sample text'; + const url = 'https://example.com/'; + await render( + + {text} + + ); + const link = await screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + }); - it('renders itself', async () => { - const text = 'sample text'; - const url = 'https://example.com/'; - await render( - - {text} - - ); - const link = await screen.getByText(text); - expect(link).toBeDefined(); - expect(link).toHaveAttribute('href', url); - expect(link).toHaveTextContent(text); - }); + it('supports external link', async () => { + const text = 'external link'; + const url = 'https://example.com/'; + await render( + + {text} + + ); + const link = await screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + expect(link).toHaveAttribute('target', '_blank'); // Open external links in new Tab by default + expect(link).toHaveAttribute('rel'); // For links opened in new Tab rel="noreferrer noopener" is required + const rel = (link as any)?.rel; + expect(rel.includes('noreferrer')).toBeTruthy(); // ref="noreferrer" check + expect(rel.includes('noopener')).toBeTruthy(); // rel="noreferrer check + }); - it('supports external link', async () => { - const text = 'external link'; - const url = 'https://example.com/'; - await render( - - {text} - - ); - const link = await screen.getByText(text); - expect(link).toBeDefined(); - expect(link).toHaveAttribute('href', url); - expect(link).toHaveTextContent(text); - expect(link).toHaveAttribute('target', '_blank'); // Open external links in new Tab by default - expect(link).toHaveAttribute('rel'); // For links opened in new Tab rel="noreferrer noopener" is required - const rel = (link as any)?.rel; - expect(rel.includes('noreferrer')).toBeTruthy(); // ref="noreferrer" check - expect(rel.includes('noopener')).toBeTruthy(); // rel="noreferrer check - }); + it('supports internal link', async () => { + const text = 'internal link'; + const url = '/internal-link'; + await render( + + {text} + + ); + const link = await screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + expect(link).not.toHaveAttribute('target'); + expect(link).not.toHaveAttribute('rel'); + }); - it('supports internal link', async () => { - const text = 'internal link'; - const url = '/internal-link'; - await render( - - {text} - - ); - const link = await screen.getByText(text); - expect(link).toBeDefined(); - expect(link).toHaveAttribute('href', url); - expect(link).toHaveTextContent(text); - expect(link).not.toHaveAttribute('target'); - expect(link).not.toHaveAttribute('rel'); - }); + it('supports openInNewTab property', async () => { + // External link with openInNewTab={false} + let text = 'external link in same tab'; + let url = 'https://example.com/'; + await render( + + + {text} + + + ); + let link = await screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + expect(link).not.toHaveAttribute('target'); + expect(link).not.toHaveAttribute('rel'); - it('supports openInNewTab property', async () => { - // External link with openInNewTab={false} - let text = 'external link in same tab'; - let url = 'https://example.com/'; - await render( - - - {text} - - - ); - let link = await screen.getByText(text); - expect(link).toBeDefined(); - expect(link).toHaveAttribute('href', url); - expect(link).toHaveTextContent(text); - expect(link).not.toHaveAttribute('target'); - expect(link).not.toHaveAttribute('rel'); + // Internal link with openInNewTab={true} + text = 'internal link in new tab'; + url = '/internal-link-in-new-tab'; + await render( + + + {text} + + + ); + link = await screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + expect(link).toHaveAttribute('target', '_blank'); // Open links in new Tab + expect(link).toHaveAttribute('rel'); // For links opened in new Tab rel="noreferrer noopener" is required + const rel = (link as any)?.rel; + expect(rel.includes('noreferrer')).toBeTruthy(); // ref="noreferrer" check + expect(rel.includes('noopener')).toBeTruthy(); // rel="noreferrer check + }); - // Internal link with openInNewTab={true} - text = 'internal link in new tab'; - url = '/internal-link-in-new-tab'; - await render( - - - {text} - - - ); - link = await screen.getByText(text); - expect(link).toBeDefined(); - expect(link).toHaveAttribute('href', url); - expect(link).toHaveTextContent(text); - expect(link).toHaveAttribute('target', '_blank'); // Open links in new Tab - expect(link).toHaveAttribute('rel'); // For links opened in new Tab rel="noreferrer noopener" is required - const rel = (link as any)?.rel; - expect(rel.includes('noreferrer')).toBeTruthy(); // ref="noreferrer" check - expect(rel.includes('noopener')).toBeTruthy(); // rel="noreferrer check - }); - - it('supports className property', async () => { - let text = 'internal link with specific class'; - let url = '/internal-link-with-class'; - let className = 'someClassName'; - await render( - - - {text} - - - ); - let link = await screen.getByText(text); - expect(link).toBeDefined(); - expect(link).toHaveClass(className); - }); -}); \ No newline at end of file + it('supports className property', async () => { + let text = 'internal link with specific class'; + let url = '/internal-link-with-class'; + let className = 'someClassName'; + await render( + + + {text} + + + ); + let link = await screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveClass(className); + }); +}); diff --git a/src/components/AppLink/AppLink.tsx b/src/components/AppLink/AppLink.tsx index d352141..f53ced8 100644 --- a/src/components/AppLink/AppLink.tsx +++ b/src/components/AppLink/AppLink.tsx @@ -6,10 +6,10 @@ const DEFAULT_APP_LINK_COLOR = 'textSecondary'; // 'primary' // 'secondary' const DEFAULT_APP_LINK_UNDERLINE = 'hover'; // 'always interface Props extends MuiLinkProps { - children: ReactNode; - to?: string; - href?: string; - openInNewTab?: boolean; + children: ReactNode; + to?: string; + href?: string; + openInNewTab?: boolean; } /** @@ -21,34 +21,34 @@ interface Props extends MuiLinkProps { * @param {boolean} [openInNewTab] - link will be opened in new tab when true */ const AppLink = forwardRef( - ( - { - children, - color = DEFAULT_APP_LINK_COLOR, - underline = DEFAULT_APP_LINK_UNDERLINE, - to, - href, - openInNewTab = Boolean(href), // Open external links in new Tab by default - ...restOfProps - }, - ref - ) => { - const propsToRender = { - color, - underline, - ...(openInNewTab ? { target: '_blank', rel: 'noreferrer noopener' } : {}), - ...restOfProps, - }; - return href ? ( - - {children} - - ) : ( - - {children} - - ); - } + ( + { + children, + color = DEFAULT_APP_LINK_COLOR, + underline = DEFAULT_APP_LINK_UNDERLINE, + to, + href, + openInNewTab = Boolean(href), // Open external links in new Tab by default + ...restOfProps + }, + ref + ) => { + const propsToRender = { + color, + underline, + ...(openInNewTab ? { target: '_blank', rel: 'noreferrer noopener' } : {}), + ...restOfProps, + }; + return href ? ( + + {children} + + ) : ( + + {children} + + ); + } ); -export default AppLink; \ No newline at end of file +export default AppLink; diff --git a/src/components/AppLink/index.tsx b/src/components/AppLink/index.tsx index 4413025..403f1f2 100644 --- a/src/components/AppLink/index.tsx +++ b/src/components/AppLink/index.tsx @@ -1,3 +1,3 @@ import AppLink from './AppLink'; -export { AppLink as default, AppLink }; \ No newline at end of file +export { AppLink as default, AppLink }; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 4702082..fb6ef33 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,12 +1,12 @@ import React, { Component, ErrorInfo } from 'react'; interface Props { - name: string; + name: string; } interface State { - error?: any; - errorInfo?: ErrorInfo; + error?: any; + errorInfo?: ErrorInfo; } /** @@ -14,39 +14,39 @@ interface State { * @param {string} [props.name] - name of the wrapped segment, "Error Boundary" by default */ class ErrorBoundary extends Component { - state: State = { - error: null, - }; + state: State = { + error: null, + }; - componentDidCatch(error: any, errorInfo: ErrorInfo) { - // Catch errors in any components below and re-render with error message - this.setState({ - error: error, - errorInfo: errorInfo, - }); - // We can also log error messages to an error reporting service here - } + componentDidCatch(error: any, errorInfo: ErrorInfo) { + // Catch errors in any components below and re-render with error message + this.setState({ + error: error, + errorInfo: errorInfo, + }); + // We can also log error messages to an error reporting service here + } - render() { - const { errorInfo } = this.state; - if (errorInfo) { - // Error path - const { error } = this.state; - const { name = 'Error Boundary' } = this.props; - return ( -
-

{name} - Something went wrong

-
- {error ? error.toString() : null} -
- {errorInfo?.componentStack} -
-
- ); - } - // Normally, just render children - return this.props.children; - } + render() { + const { errorInfo } = this.state; + if (errorInfo) { + // Error path + const { error } = this.state; + const { name = 'Error Boundary' } = this.props; + return ( +
+

{name} - Something went wrong

+
+ {error ? error.toString() : null} +
+ {errorInfo?.componentStack} +
+
+ ); + } + // Normally, just render children + return this.props.children; + } } -export default ErrorBoundary; \ No newline at end of file +export default ErrorBoundary; diff --git a/src/components/NotImplemented.tsx b/src/components/NotImplemented.tsx index 0bd5b4a..fb99767 100644 --- a/src/components/NotImplemented.tsx +++ b/src/components/NotImplemented.tsx @@ -2,7 +2,7 @@ * Renders "Component is under construction" boilerplate */ const ComponentIsNotImplemented = () => { - return
Component is under construction
; + return
Component is under construction
; }; export default ComponentIsNotImplemented; diff --git a/src/components/SideBar/SideBar.tsx b/src/components/SideBar/SideBar.tsx index ec723a4..1ffd548 100644 --- a/src/components/SideBar/SideBar.tsx +++ b/src/components/SideBar/SideBar.tsx @@ -14,35 +14,35 @@ import { SIDEBAR_WIDTH } from '../../routes/Layout/PrivateLayout'; import { LinkToPage } from '../../utils/type'; const useStyles = makeStyles((theme: Theme) => ({ - root: { - // backgroundColor: theme.palette.white, - display: 'flex', - flexDirection: 'column', - height: '100%', - padding: theme.spacing(2), - }, - paperInDrawer: { - width: SIDEBAR_WIDTH, - [theme.breakpoints.up('md')]: { - marginTop: 64, - height: 'calc(100% - 64px)', - }, - }, - profile: { - marginBottom: theme.spacing(2), - }, - nav: {}, - buttons: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-evenly', - alignItems: 'center', - marginTop: theme.spacing(2), - }, + root: { + // backgroundColor: theme.palette.white, + display: 'flex', + flexDirection: 'column', + height: '100%', + padding: theme.spacing(2), + }, + paperInDrawer: { + width: SIDEBAR_WIDTH, + [theme.breakpoints.up('md')]: { + marginTop: 64, + height: 'calc(100% - 64px)', + }, + }, + profile: { + marginBottom: theme.spacing(2), + }, + nav: {}, + buttons: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-evenly', + alignItems: 'center', + marginTop: theme.spacing(2), + }, })); /** - * Renders SideBar with Menu and User details + * Renders SideBar with Menu and User details * Actually for Authenticated users only, rendered in "Private Layout" * @class SideBar * @param {string} [prop.anchor] - 'left' or 'right' @@ -52,73 +52,68 @@ const useStyles = makeStyles((theme: Theme) => ({ * @param {func} [props.onClose] - called when the Drawer is closing */ interface Props extends Pick { - items: Array; + items: Array; } const SideBar: React.FC = ({ anchor, className, open, variant, items, onClose, ...restOfProps }) => { - const [state, dispatch] = useAppStore(); - const classes = useStyles(); + const [state, dispatch] = useAppStore(); + const classes = useStyles(); - const handleSwitchDarkMode = useCallback(() => { - dispatch({ - type: 'SET_DARK_MODE', - darkMode: !state.darkMode, - payload: !state.darkMode, - }); - }, [state, dispatch]); + const handleSwitchDarkMode = useCallback(() => { + dispatch({ + type: 'SET_DARK_MODE', + darkMode: !state.darkMode, + payload: !state.darkMode, + }); + }, [state, dispatch]); - const handleOnLogout = useCallback(async () => { - // await api.auth.logout(); - dispatch({ type: 'LOG_OUT' }); - }, [dispatch]); + const handleOnLogout = useCallback(async () => { + // await api.auth.logout(); + dispatch({ type: 'LOG_OUT' }); + }, [dispatch]); - const handleAfterLinkClick = useCallback( - (event: React.MouseEvent) => { - if (variant === 'temporary' && typeof onClose === 'function') { - onClose(event, 'backdropClick'); - } - }, - [variant, onClose] - ); + const handleAfterLinkClick = useCallback( + (event: React.MouseEvent) => { + if (variant === 'temporary' && typeof onClose === 'function') { + onClose(event, 'backdropClick'); + } + }, + [variant, onClose] + ); - const drawerClasses = { - // See: https://material-ui.com/api/drawer/#css - paper: classes.paperInDrawer, - }; - const classRoot = clsx(classes.root, className); + const drawerClasses = { + // See: https://material-ui.com/api/drawer/#css + paper: classes.paperInDrawer, + }; + const classRoot = clsx(classes.root, className); - return ( - -
- {state.isAuthenticated /*&& state?.currentUser*/ && ( - <> - - - - )} + return ( + +
+ {state.isAuthenticated /*&& state?.currentUser*/ && ( + <> + + + + )} - - + + -
- - - } - /> - +
+ + } + /> + - {state.isAuthenticated && ( - - )} -
-
- - ); + {state.isAuthenticated && ( + + )} +
+
+
+ ); }; -export default SideBar; \ No newline at end of file +export default SideBar; diff --git a/src/components/SideBar/SideBarLink.tsx b/src/components/SideBar/SideBarLink.tsx index caec9e5..9cabf91 100644 --- a/src/components/SideBar/SideBarLink.tsx +++ b/src/components/SideBar/SideBarLink.tsx @@ -4,28 +4,28 @@ import { NavLink, NavLinkProps } from 'react-router-dom'; import { Theme, makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles((theme: Theme) => ({ - root: { - flexGrow: 1, // takes all width - }, - link: { - color: theme.palette.text.secondary, - }, - linkActive: { - color: theme.palette.text.primary, - }, + root: { + flexGrow: 1, // takes all width + }, + link: { + color: theme.palette.text.secondary, + }, + linkActive: { + color: theme.palette.text.primary, + }, })); /** * Router link with styling to use in SideBar, highlights the current url. */ const SideBarLink = forwardRef(({ className, ...restOfProps }, ref) => { - const classes = useStyles(); - const classLink = clsx(className, classes.link); - return ( -
- -
- ); + const classes = useStyles(); + const classLink = clsx(className, classes.link); + return ( +
+ +
+ ); }); -export default SideBarLink; \ No newline at end of file +export default SideBarLink; diff --git a/src/components/SideBar/SideBarNavigation.tsx b/src/components/SideBar/SideBarNavigation.tsx index 501849b..0774983 100644 --- a/src/components/SideBar/SideBarNavigation.tsx +++ b/src/components/SideBar/SideBarNavigation.tsx @@ -8,30 +8,30 @@ import SideBarLink from './SideBarLink'; import { LinkToPage } from '../../utils/type'; const useStyles = makeStyles((theme: Theme) => ({ - root: {}, - item: { - display: 'flex', - paddingTop: 0, - paddingBottom: 0, - }, - button: { - // color: theme.palette.button, - padding: '10px 8px', - justifyContent: 'flex-start', - textTransform: 'none', - letterSpacing: 0, - width: '100%', - // fontWeight: theme.typography.fontWeightMedium, - flexGrow: 1, - }, - iconOrMargin: { - // color: theme.palette.icon, - width: 24, - height: 24, - display: 'flex', - alignItems: 'center', - marginRight: theme.spacing(1), - }, + root: {}, + item: { + display: 'flex', + paddingTop: 0, + paddingBottom: 0, + }, + button: { + // color: theme.palette.button, + padding: '10px 8px', + justifyContent: 'flex-start', + textTransform: 'none', + letterSpacing: 0, + width: '100%', + // fontWeight: theme.typography.fontWeightMedium, + flexGrow: 1, + }, + iconOrMargin: { + // color: theme.palette.icon, + width: 24, + height: 24, + display: 'flex', + alignItems: 'center', + marginRight: theme.spacing(1), + }, })); /** @@ -41,39 +41,39 @@ const useStyles = makeStyles((theme: Theme) => ({ * @param {func} [props.afterLinkClink] - optional callback called when some link was clicked */ interface Props { - className?: string; - items: Array; - showIcons?: boolean; - afterLinkClick?: React.MouseEventHandler; + className?: string; + items: Array; + showIcons?: boolean; + afterLinkClick?: React.MouseEventHandler; } const SideBarNavigation: React.FC = ({ - className, - items, - showIcons = false, - afterLinkClick, - ...restOfProps + className, + items, + showIcons = false, + afterLinkClick, + ...restOfProps }) => { - const classes = useStyles(); - const classRoot = clsx(classes.root, className); - return ( - - ); + const classes = useStyles(); + const classRoot = clsx(classes.root, className); + return ( + + ); }; -export default SideBarNavigation; \ No newline at end of file +export default SideBarNavigation; diff --git a/src/components/SideBar/index.tsx b/src/components/SideBar/index.tsx index 4d3778b..eac880b 100644 --- a/src/components/SideBar/index.tsx +++ b/src/components/SideBar/index.tsx @@ -1,3 +1,3 @@ -import SideBar from './SideBar' +import SideBar from './SideBar'; -export { SideBar as default, SideBar } \ No newline at end of file +export { SideBar as default, SideBar }; diff --git a/src/components/TopBar/TopBar.tsx b/src/components/TopBar/TopBar.tsx index b929ba8..889f40e 100644 --- a/src/components/TopBar/TopBar.tsx +++ b/src/components/TopBar/TopBar.tsx @@ -6,70 +6,70 @@ import Typography from '@material-ui/core/Typography'; import AppIconButton from '../AppIconButton'; const useStyles = makeStyles((theme: Theme) => ({ - root: { - //boxShadow: 'none', - minWidth: '20rem', - }, - toolbar: { - paddingLeft: theme.spacing(1), - paddingRight: theme.spacing(1), - }, - logo: { - height: theme.spacing(4), - }, - title: { - marginLeft: theme.spacing(1), - marginRight: theme.spacing(1), - flexGrow: 1, - textAlign: 'center', - whiteSpace: 'nowrap', - }, - buttons: {}, + root: { + //boxShadow: 'none', + minWidth: '20rem', + }, + toolbar: { + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + }, + logo: { + height: theme.spacing(4), + }, + title: { + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + flexGrow: 1, + textAlign: 'center', + whiteSpace: 'nowrap', + }, + buttons: {}, })); /** * Renders TopBar composition */ interface Props { - className?: string; - title?: string; - isAuthenticated?: boolean; - onMenu?: () => void; - onNotifications?: () => void; + className?: string; + title?: string; + isAuthenticated?: boolean; + onMenu?: () => void; + onNotifications?: () => void; } const TopBar: React.FC = ({ - className, - title = '', - isAuthenticated, - onMenu, - onNotifications, - ...restOfProps + className, + title = '', + isAuthenticated, + onMenu, + onNotifications, + ...restOfProps }) => { - const classes = useStyles(); - // const iconMenu = isAuthenticated ? 'account' : 'menu'; + const classes = useStyles(); + // const iconMenu = isAuthenticated ? 'account' : 'menu'; - return ( - - - + return ( + + + - - {title} - + + {title} + -
- {isAuthenticated && ( - - )} - {/* */} -
-
-
- ); +
+ {isAuthenticated && ( + + )} + {/* */} +
+
+
+ ); }; -export default TopBar; \ No newline at end of file +export default TopBar; diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx index cbaf5e8..e73d0d0 100644 --- a/src/components/TopBar/index.tsx +++ b/src/components/TopBar/index.tsx @@ -1,3 +1,3 @@ -import TopBar from './TopBar' +import TopBar from './TopBar'; -export { TopBar as default, TopBar } \ No newline at end of file +export { TopBar as default, TopBar }; diff --git a/src/components/UserInfo/UserInfo.tsx b/src/components/UserInfo/UserInfo.tsx index 92986ed..2b51bd1 100644 --- a/src/components/UserInfo/UserInfo.tsx +++ b/src/components/UserInfo/UserInfo.tsx @@ -5,26 +5,26 @@ import { Theme, makeStyles } from '@material-ui/core/styles'; import AppLink from '../AppLink'; const useStyles = makeStyles((theme: Theme) => ({ - root: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - minHeight: 'fit-content', - }, - avatar: { - width: 64, - height: 64, - fontSize: '3rem', - }, - name: { - marginTop: theme.spacing(1), - }, + root: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + minHeight: 'fit-content', + }, + avatar: { + width: 64, + height: 64, + fontSize: '3rem', + }, + name: { + marginTop: theme.spacing(1), + }, })); interface UserInfoProps { - className?: string; - showAvatar?: boolean; - user?: any; + className?: string; + showAvatar?: boolean; + user?: any; } /** @@ -34,25 +34,25 @@ interface UserInfoProps { * @param {object} [user] - logged user data {name, email, avatar...} */ const UserInfo = ({ className, showAvatar = false, user, ...restOfProps }: UserInfoProps) => { - const classes = useStyles(); + const classes = useStyles(); - const fullName = [user?.first_name || '', user?.last_name || ''].join(' ').trim(); - const srcAvatar = user?.avatar ? user?.avatar : undefined; - const userPhoneOrEmail = user?.phone || (user?.email as string); + const fullName = [user?.first_name || '', user?.last_name || ''].join(' ').trim(); + const srcAvatar = user?.avatar ? user?.avatar : undefined; + const userPhoneOrEmail = user?.phone || (user?.email as string); - return ( -
- {showAvatar ? ( - - - - ) : null} - - {fullName || 'Current User'} - - {userPhoneOrEmail || 'Loading...'} -
- ); + return ( +
+ {showAvatar ? ( + + + + ) : null} + + {fullName || 'Current User'} + + {userPhoneOrEmail || 'Loading...'} +
+ ); }; -export default UserInfo; \ No newline at end of file +export default UserInfo; diff --git a/src/components/UserInfo/index.tsx b/src/components/UserInfo/index.tsx index 128ca1e..46ad597 100644 --- a/src/components/UserInfo/index.tsx +++ b/src/components/UserInfo/index.tsx @@ -1,3 +1,3 @@ -import UserInfo from './UserInfo' +import UserInfo from './UserInfo'; -export { UserInfo as default, UserInfo } \ No newline at end of file +export { UserInfo as default, UserInfo }; diff --git a/src/components/index.tsx b/src/components/index.tsx index 76a6447..ed1a808 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -5,5 +5,6 @@ import AppButton from './AppButton'; import AppIcon from './AppIcon'; import AppIconButton from './AppIconButton'; import AppAlert from './AppAlert'; +import AppForm from './AppForm'; -export { NotImplemented, ErrorBoundary, AppLink, AppButton, AppIcon, AppIconButton, AppAlert }; +export { NotImplemented, ErrorBoundary, AppLink, AppButton, AppIcon, AppIconButton, AppAlert, AppForm }; diff --git a/src/index.css b/src/index.css index ec2585e..7323ae8 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,11 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', + 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/src/routes/AppRouter.tsx b/src/routes/AppRouter.tsx index 39f4b00..650e3bc 100644 --- a/src/routes/AppRouter.tsx +++ b/src/routes/AppRouter.tsx @@ -4,7 +4,7 @@ import { BrowserRouter } from 'react-router-dom'; * Router of the Application */ const AppRouter: React.FC = ({ children }) => { - return {children}; + return {children}; }; export default AppRouter; diff --git a/src/routes/Layout/PrivateLayout.tsx b/src/routes/Layout/PrivateLayout.tsx index b5f25dd..ecff4c5 100644 --- a/src/routes/Layout/PrivateLayout.tsx +++ b/src/routes/Layout/PrivateLayout.tsx @@ -15,25 +15,25 @@ const DESKTOP_SIDEBAR_ANCHOR = 'left'; // 'right'; export const SIDEBAR_WIDTH = 240; // 240px const useStyles = makeStyles((theme: Theme) => ({ - root: { - minHeight: '100vh', // Full screen height - paddingTop: 56, - [theme.breakpoints.up('sm')]: { - paddingTop: 64, - }, - }, - header: {}, - shiftContent: { - paddingLeft: DESKTOP_SIDEBAR_ANCHOR.includes('left') ? SIDEBAR_WIDTH : 0, - paddingRight: DESKTOP_SIDEBAR_ANCHOR.includes('right') ? SIDEBAR_WIDTH : 0, - }, - content: { - flexGrow: 1, // Takes all possible space - paddingLeft: theme.spacing(1), - paddingRight: theme.spacing(1), - paddingTop: theme.spacing(1), - }, - footer: {}, + root: { + minHeight: '100vh', // Full screen height + paddingTop: 56, + [theme.breakpoints.up('sm')]: { + paddingTop: 64, + }, + }, + header: {}, + shiftContent: { + paddingLeft: DESKTOP_SIDEBAR_ANCHOR.includes('left') ? SIDEBAR_WIDTH : 0, + paddingRight: DESKTOP_SIDEBAR_ANCHOR.includes('right') ? SIDEBAR_WIDTH : 0, + }, + content: { + flexGrow: 1, // Takes all possible space + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + paddingTop: theme.spacing(1), + }, + footer: {}, })); /** @@ -41,94 +41,94 @@ const useStyles = makeStyles((theme: Theme) => ({ */ function updateDocumentTitle(title = '') { - if (title) { - document.title = `${title} - ${TITLE_PRIVATE}`; - } else { - document.title = TITLE_PRIVATE; - } - return document.title; + if (title) { + document.title = `${title} - ${TITLE_PRIVATE}`; + } else { + document.title = TITLE_PRIVATE; + } + return document.title; } /** * "Link to Page" items in Sidebar */ const SIDE_BAR_PRIVATE_ITEMS: Array = [ - { - title: 'Home', - href: '/', - icon: 'home', - }, - { - title: 'Profile', - href: '/user', - icon: 'account', - }, - { - title: 'About', - href: '/about', - icon: 'info', - }, - { - title: 'Dev Tools', - href: '/dev', - icon: 'settings', - }, + { + title: 'Home', + href: '/', + icon: 'home', + }, + { + title: 'Profile', + href: '/user', + icon: 'account', + }, + { + title: 'About', + href: '/about', + icon: 'info', + }, + { + title: 'Dev Tools', + href: '/dev', + icon: 'settings', + }, ]; /** * Renders "Private Layout" composition */ const Layout: React.FC = ({ children }) => { - const [state] = useAppStore(); - const [openSideBar, setOpenSideBar] = useState(false); - const theme = useTheme(); - const classes = useStyles(); - const isDesktop = useMediaQuery(theme.breakpoints.up('md'), { defaultMatches: true }); - const history = useHistory(); + const [state] = useAppStore(); + const [openSideBar, setOpenSideBar] = useState(false); + const theme = useTheme(); + const classes = useStyles(); + const isDesktop = useMediaQuery(theme.breakpoints.up('md'), { defaultMatches: true }); + const history = useHistory(); - const handleLogoClick = useCallback(() => { - // Navigate to '/' when clicking on Logo/Menu icon when the SideBar is already visible - history.push('/'); - }, [history]); + const handleLogoClick = useCallback(() => { + // Navigate to '/' when clicking on Logo/Menu icon when the SideBar is already visible + history.push('/'); + }, [history]); - const handleSideBarOpen = useCallback(() => { - if (!openSideBar) setOpenSideBar(true); - }, [openSideBar]); + const handleSideBarOpen = useCallback(() => { + if (!openSideBar) setOpenSideBar(true); + }, [openSideBar]); - const handleSideBarClose = useCallback(() => { - if (openSideBar) setOpenSideBar(false); - }, [openSideBar]); + const handleSideBarClose = useCallback(() => { + if (openSideBar) setOpenSideBar(false); + }, [openSideBar]); - const classRoot = clsx({ - [classes.root]: true, - [classes.shiftContent]: isDesktop, - }); - const title = updateDocumentTitle(); - const shouldOpenSideBar = isDesktop ? true : openSideBar; + const classRoot = clsx({ + [classes.root]: true, + [classes.shiftContent]: isDesktop, + }); + const title = updateDocumentTitle(); + const shouldOpenSideBar = isDesktop ? true : openSideBar; - return ( - - - + return ( + + + - - + + - - {children} - - - ); + + {children} + + + ); }; -export default Layout; \ No newline at end of file +export default Layout; diff --git a/src/routes/Layout/PublicLayout.tsx b/src/routes/Layout/PublicLayout.tsx index 68b5839..87929e4 100644 --- a/src/routes/Layout/PublicLayout.tsx +++ b/src/routes/Layout/PublicLayout.tsx @@ -7,133 +7,130 @@ import { ErrorBoundary, AppIconButton, AppIcon } from '../../components'; import SideBar from '../../components/SideBar/SideBar'; import { LinkToPage } from '../../utils/type'; -const TITLE_PUBLIC = 'Some Private App'; +const TITLE_PUBLIC = 'Some App'; const useStyles = makeStyles((theme: Theme) => ({ - root: { - minHeight: '100vh', // Full screen height - paddingTop: 56, // on Small screen - [theme.breakpoints.up('sm')]: { - paddingTop: 64, // on Large screen - }, - }, - header: {}, - toolbar: { - paddingLeft: theme.spacing(1), - paddingRight: theme.spacing(1), - }, - title: { - marginLeft: theme.spacing(1), - marginRight: theme.spacing(1), - flexGrow: 1, - textAlign: 'center', - whiteSpace: 'nowrap', - }, - content: { - flexGrow: 1, // Takes all possible space - padding: theme.spacing(1), - }, - footer: {}, + root: { + minHeight: '100vh', // Full screen height + paddingTop: 56, // on Small screen + [theme.breakpoints.up('sm')]: { + paddingTop: 64, // on Large screen + }, + }, + header: {}, + toolbar: { + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + }, + title: { + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + flexGrow: 1, + textAlign: 'center', + whiteSpace: 'nowrap', + }, + content: { + flexGrow: 1, // Takes all possible space + padding: theme.spacing(1), + }, + footer: {}, })); /** * "Link to Page" items in Sidebar */ const SIDE_BAR_PUBLIC_ITEMS: Array = [ - { - title: 'Log In', - href: '/auth/login', - icon: 'login', - }, - { - title: 'Sign Up', - href: '/auth/signup', - icon: 'signup', - }, - { - title: 'About', - href: '/about', - icon: 'info', - }, + { + title: 'Log In', + href: '/auth/login', + icon: 'login', + }, + { + title: 'Sign Up', + href: '/auth/signup', + icon: 'signup', + }, + { + title: 'About', + href: '/about', + icon: 'info', + }, ]; /** * Renders "Public Layout" composition */ const PublicLayout: React.FC = ({ children }) => { - const classes = useStyles(); - const [openSideBar, setOpenSideBar] = useState(false); - const [state, dispatch] = useAppStore(); - const history = useHistory(); - - const title = TITLE_PUBLIC; - document.title = title; // Also Update Tab Title - - const handleSwitchDarkMode = useCallback(() => { - dispatch({ - type: 'SET_DARK_MODE', - darkMode: !state.darkMode, - payload: !state.darkMode, - }); - }, [state, dispatch]); - - const handleSideBarOpen = useCallback(() => { - if (!openSideBar) setOpenSideBar(true); - }, [openSideBar]); - - const handleSideBarClose = useCallback(() => { - if (openSideBar) setOpenSideBar(false); - }, [openSideBar]); - - const handleBottomNavigationChange = (event: React.ChangeEvent<{}>, value: any) => { - history.push(value); - }; - - return ( - - - - - - - - {title} - - - - - - - - - - - {children} - - - - - } /> - } /> - } /> - - - - ); + const classes = useStyles(); + const [openSideBar, setOpenSideBar] = useState(false); + const [state, dispatch] = useAppStore(); + const history = useHistory(); + + const title = TITLE_PUBLIC; + document.title = title; // Also Update Tab Title + + const handleSwitchDarkMode = useCallback(() => { + dispatch({ + type: 'SET_DARK_MODE', + darkMode: !state.darkMode, + payload: !state.darkMode, + }); + }, [state, dispatch]); + + const handleSideBarOpen = useCallback(() => { + if (!openSideBar) setOpenSideBar(true); + }, [openSideBar]); + + const handleSideBarClose = useCallback(() => { + if (openSideBar) setOpenSideBar(false); + }, [openSideBar]); + + const handleBottomNavigationChange = (event: React.ChangeEvent<{}>, value: any) => { + history.push(value); + }; + + return ( + + + + + + + + {title} + + + + + + + + + + + {children} + + + + + } /> + } /> + } /> + + + + ); }; -export default PublicLayout; \ No newline at end of file +export default PublicLayout; diff --git a/src/routes/Layout/index.tsx b/src/routes/Layout/index.tsx index da9a967..246ab91 100644 --- a/src/routes/Layout/index.tsx +++ b/src/routes/Layout/index.tsx @@ -1,4 +1,4 @@ import PublicLayout from './PublicLayout'; import PrivateLayout from './PrivateLayout'; -export { PublicLayout, PrivateLayout }; \ No newline at end of file +export { PublicLayout, PrivateLayout }; diff --git a/src/routes/PrivateRoutes.tsx b/src/routes/PrivateRoutes.tsx index e114645..70b6b73 100644 --- a/src/routes/PrivateRoutes.tsx +++ b/src/routes/PrivateRoutes.tsx @@ -7,16 +7,16 @@ import { PrivateLayout } from './Layout'; * Also renders the "Private Layout" composition */ const PrivateRoutes = () => { - return ( - - - - - , - - - - ); + return ( + + + + + , + + + + ); }; -export default PrivateRoutes; \ No newline at end of file +export default PrivateRoutes; diff --git a/src/routes/PublicRoutes.tsx b/src/routes/PublicRoutes.tsx index 574ad14..23f63d0 100644 --- a/src/routes/PublicRoutes.tsx +++ b/src/routes/PublicRoutes.tsx @@ -1,23 +1,24 @@ import { Route, Switch } from 'react-router-dom'; -import { Welcome, About, NotFound } from '../views'; +import AuthRoutes from '../views/Auth'; +import { About, NotFound } from '../views'; import { PublicLayout } from './Layout'; +import LoginEmailView from '../views/Auth/Login/Email'; /** * List of routes available only for anonymous users * Also renders the "Public Layout" composition */ const PublicRoutes = () => { - return ( - - - {/* - */} - - , - - - - ); + return ( + + + + + , + + + + ); }; -export default PublicRoutes; \ No newline at end of file +export default PublicRoutes; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index f42bda7..8e34e83 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -7,26 +7,26 @@ import PrivateRoutes from './PrivateRoutes'; * Renders routes depending on Authenticated or Anonymous users */ const Routes = () => { - const [state /*, dispatch*/] = useAppStore(); + const [state /*, dispatch*/] = useAppStore(); - // useEffect(() => { - // // Check isn't token expired? - // const isLogged = isUserStillLoggedIn(); - // if (state.isAuthenticated && !isLogged) { - // // Token was expired, logout immediately! - // console.warn('Token was expired, logout immediately!'); - // api.auth.logout(); - // dispatch({ type: 'LOG_OUT' }); - // return; // Thats all for now, the App will be completely re-rendered soon - // } - // if (isLogged && !state.isAuthenticated) { - // // Valid token is present but we are not logged in somehow, lets fix it - // dispatch({ type: 'LOG_IN' }); - // } - // }, [state.isAuthenticated, dispatch]); // Effect for every state.isAuthenticated change + // useEffect(() => { + // // Check isn't token expired? + // const isLogged = isUserStillLoggedIn(); + // if (state.isAuthenticated && !isLogged) { + // // Token was expired, logout immediately! + // console.warn('Token was expired, logout immediately!'); + // api.auth.logout(); + // dispatch({ type: 'LOG_OUT' }); + // return; // Thats all for now, the App will be completely re-rendered soon + // } + // if (isLogged && !state.isAuthenticated) { + // // Valid token is present but we are not logged in somehow, lets fix it + // dispatch({ type: 'LOG_IN' }); + // } + // }, [state.isAuthenticated, dispatch]); // Effect for every state.isAuthenticated change - console.log('Routes() - isAuthenticated:', state.isAuthenticated); - return state.isAuthenticated ? : ; + console.log('Routes() - isAuthenticated:', state.isAuthenticated); + return state.isAuthenticated ? : ; }; -export default Routes; \ No newline at end of file +export default Routes; diff --git a/src/store/AppReducer.ts b/src/store/AppReducer.ts index d2e4209..6f4b8b9 100644 --- a/src/store/AppReducer.ts +++ b/src/store/AppReducer.ts @@ -8,36 +8,36 @@ import { IAppState } from './AppStore'; * @param {*} [action.payload] - optional data object or the function to get data object */ const AppReducer: React.Reducer = (state, action) => { - // console.log('AppReducer() - action:', action); - switch (action.type || action.action) { - case 'SET_CURRENT_USER': - return { - ...state, - currentUser: action?.currentUser || action?.payload, - }; - case 'SIGN_UP': - case 'LOG_IN': - return { - ...state, - isAuthenticated: true, - }; - case 'LOG_OUT': - return { - ...state, - isAuthenticated: false, - currentUser: undefined, // Also reset previous user data - }; - case 'SET_DARK_MODE': { - const darkMode = action?.darkMode ?? action?.payload; - localStorageSet('darkMode', darkMode); - return { - ...state, - darkMode, - }; - } - default: - return state; - } + // console.log('AppReducer() - action:', action); + switch (action.type || action.action) { + case 'SET_CURRENT_USER': + return { + ...state, + currentUser: action?.currentUser || action?.payload, + }; + case 'SIGN_UP': + case 'LOG_IN': + return { + ...state, + isAuthenticated: true, + }; + case 'LOG_OUT': + return { + ...state, + isAuthenticated: false, + currentUser: undefined, // Also reset previous user data + }; + case 'SET_DARK_MODE': { + const darkMode = action?.darkMode ?? action?.payload; + localStorageSet('darkMode', darkMode); + return { + ...state, + darkMode, + }; + } + default: + return state; + } }; -export default AppReducer; \ No newline at end of file +export default AppReducer; diff --git a/src/store/AppStore.tsx b/src/store/AppStore.tsx index fa12026..1512b82 100644 --- a/src/store/AppStore.tsx +++ b/src/store/AppStore.tsx @@ -7,13 +7,13 @@ import { localStorageGet } from '../utils/localStorage'; * AppState structure and initial values */ export interface IAppState { - darkMode: boolean; - isAuthenticated: boolean; - currentUser?: object | undefined; + darkMode: boolean; + isAuthenticated: boolean; + currentUser?: object | undefined; } const initialAppState: IAppState = { - darkMode: false, // Overridden by useMediaQuery('(prefers-color-scheme: dark)') in AppStore - isAuthenticated: false, // Overridden in AppStore by checking auth token + darkMode: false, // Overridden by useMediaQuery('(prefers-color-scheme: dark)') in AppStore + isAuthenticated: false, // Overridden in AppStore by checking auth token }; /** @@ -32,33 +32,32 @@ const AppContext = createContext([initialAppState, () => null]); * */ const AppStore: React.FC = ({ children }) => { - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - const previousDarkMode = Boolean(localStorageGet('darkMode')); - // const tokenExists = Boolean(loadToken()); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + const previousDarkMode = Boolean(localStorageGet('darkMode')); + // const tokenExists = Boolean(loadToken()); - const initialState: IAppState = { - ...initialAppState, - darkMode: previousDarkMode || prefersDarkMode, - // isAuthenticated: tokenExists, - }; - const value: IAppContext = useReducer(AppReducer, initialState); + const initialState: IAppState = { + ...initialAppState, + darkMode: previousDarkMode || prefersDarkMode, + // isAuthenticated: tokenExists, + }; + const value: IAppContext = useReducer(AppReducer, initialState); - return {children}; + return {children}; }; /** * Hook to use the AppStore in functional components - * + * * import {useAppStore} from './store' * ... * const [state, dispatch] = useAppStore(); */ const useAppStore = (): IAppContext => useContext(AppContext); - /** * HOC to inject the ApStore to class component, also works for functional components - * + * * import {withAppStore} from './store' * ... * class MyComponent @@ -66,10 +65,10 @@ const useAppStore = (): IAppContext => useContext(AppContext); * export default withAppStore(MyComponent) */ interface WithAppStoreProps { - store: object; + store: object; } const withAppStore = (Component: React.ComponentType): React.FC => (props) => { - return ; + return ; }; -export { AppStore, AppContext, useAppStore, withAppStore }; \ No newline at end of file +export { AppStore, AppContext, useAppStore, withAppStore }; diff --git a/src/theme.tsx b/src/theme.tsx index 4562b4d..d1d513b 100644 --- a/src/theme.tsx +++ b/src/theme.tsx @@ -12,58 +12,58 @@ import { useAppStore } from './store/AppStore'; * Material Color Tool: https://material.io/resources/color/#!/?view.left=0&view.right=0&secondary.color=EF9A9A&primary.color=64B5F6 */ const FRONT_COLORS = { - primary: { - main: '#64B5F6', - contrastText: '#000000', - }, - secondary: { - main: '#EF9A9A', - contrastText: '#000000', - }, + primary: { + main: '#64B5F6', + contrastText: '#000000', + }, + secondary: { + main: '#EF9A9A', + contrastText: '#000000', + }, }; /** * Material UI theme config for "Light Mode" */ const LIGHT_THEME: ThemeOptions = { - palette: { - type: 'light', - // background: { - // paper: '#f5f5f5', // Gray 100 - Background of "Paper" based component - // default: '#FFFFFF', - // }, - ...FRONT_COLORS, - }, + palette: { + type: 'light', + // background: { + // paper: '#f5f5f5', // Gray 100 - Background of "Paper" based component + // default: '#FFFFFF', + // }, + ...FRONT_COLORS, + }, }; /** * Material UI theme config for "Dark Mode" */ const DARK_THEME: ThemeOptions = { - palette: { - type: 'dark', - // background: { - // paper: '#424242', // Gray 800 - Background of "Paper" based component - // default: '#303030', - // }, - ...FRONT_COLORS, - }, + palette: { + type: 'dark', + // background: { + // paper: '#424242', // Gray 800 - Background of "Paper" based component + // default: '#303030', + // }, + ...FRONT_COLORS, + }, }; /** * Material UI Provider with Light and Dark themes depending on global "state.darkMode" */ const AppThemeProvider: React.FunctionComponent = ({ children }) => { - const [state] = useAppStore(); - // const theme = useMemo(() => (state.darkMode ? createMuiTheme(DARK_THEME) : createMuiTheme(LIGHT_THEME))); - const theme = state.darkMode ? createMuiTheme(DARK_THEME) : createMuiTheme(LIGHT_THEME); + const [state] = useAppStore(); + // const theme = useMemo(() => (state.darkMode ? createMuiTheme(DARK_THEME) : createMuiTheme(LIGHT_THEME))); + const theme = state.darkMode ? createMuiTheme(DARK_THEME) : createMuiTheme(LIGHT_THEME); - return ( - - - {children} - - ); + return ( + + + {children} + + ); }; -export { AppThemeProvider }; \ No newline at end of file +export { AppThemeProvider }; diff --git a/src/utils/date.ts b/src/utils/date.ts index e2cb980..8652ab4 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -19,4 +19,4 @@ export function dateToString(dateOrString: string | Date, dateFormat = FORMAT_DA result = fallbackValue; } return result; -} \ No newline at end of file +} diff --git a/src/utils/form.ts b/src/utils/form.ts index 901431b..160a335 100644 --- a/src/utils/form.ts +++ b/src/utils/form.ts @@ -1,4 +1,3 @@ - import { useState, useEffect, useCallback, SyntheticEvent } from 'react'; import validate from 'validate.js'; import { ObjectPropByName } from './type'; @@ -10,7 +9,6 @@ export const SHARED_CONTROL_PROPS = { fullWidth: true, } as const; - // "Schema" for formState interface FormState { isValid: boolean; @@ -126,4 +124,4 @@ export function useAppForm({ validationSchema, initialValues = {} }: UseAppFormP // Return state and methods return [formState, setFormState, onFieldChange, fieldGetError, fieldHasError] as const; -} \ No newline at end of file +} diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts index 70a8d0f..af152d6 100644 --- a/src/utils/localStorage.ts +++ b/src/utils/localStorage.ts @@ -1,7 +1,7 @@ /** * Smartly reads value from localStorage */ - export function localStorageGet(name: string, defaultValue = ''): string { +export function localStorageGet(name: string, defaultValue = ''): string { const valueFromStore = localStorage.getItem(name); if (valueFromStore === null) return defaultValue; // No value in store, return default one @@ -41,4 +41,4 @@ export function localStorageDelete(name: string) { } else { localStorage.clear(); } -} \ No newline at end of file +} diff --git a/src/utils/style.ts b/src/utils/style.ts index de0837f..566670a 100644 --- a/src/utils/style.ts +++ b/src/utils/style.ts @@ -27,7 +27,6 @@ export const paperStyle = (theme: Theme) => ({ export const formStyle = (theme: Theme) => ({ width: '100%', maxWidth: '40rem', // 640px - // maxWidth: '32rem', // 512px }); /** @@ -228,4 +227,4 @@ export const buttonStylesByNames = (theme: Theme) => ({ backgroundColor: theme.palette.success.light, }, }, -}); \ No newline at end of file +}); diff --git a/src/utils/type.ts b/src/utils/type.ts index b60c9f6..81968ec 100644 --- a/src/utils/type.ts +++ b/src/utils/type.ts @@ -1,7 +1,6 @@ // Helper to read object properties as object['name'] export type ObjectPropByName = Record; - /** * Data for "Page Link" in SideBar adn other UI elements */ diff --git a/src/views/About/About.tsx b/src/views/About/About.tsx index a0bf60a..4913b81 100644 --- a/src/views/About/About.tsx +++ b/src/views/About/About.tsx @@ -6,133 +6,128 @@ import { AppButton, AppLink, AppIconButton } from '../../components'; * url: /about/* */ const AboutView = () => { - return ( - + return ( + + + + + Detailed description of the application here... + + + OK + + + + - - - - - Detailed description of the application here... - - - - OK - - - - - - - - - - MUI initial MUI inherit{' '} - MUI primary MUI secondary{' '} - MUI textPrimary MUI textSecondary{' '} - MUI error
- Internal Link   + + + + + MUI initial MUI inherit{' '} + MUI primary MUI secondary{' '} + MUI textPrimary MUI textSecondary{' '} + MUI error
+ Internal Link   - Internal Link in New Tab + Internal Link in New Tab {' '}   External Link   - External Link in Same Tab + External Link in Same Tab {' '}  
- - - - - - - - - -
-
-
+ + + + + + + + + +
+
+
- - - - - Default - Primary - Secondary - Error - Warning - Info - Success - False - True - - Inherit + + + + + Default + Primary + Secondary + Error + Warning + Info + Success + False + True + + Inherit - - - + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - MUI Typo h1 - MUI Typography h2 - MUI Typography h3 - MUI Typography h4 - MUI Typography h5 - MUI Typography h6 - - MUI Typography subtitle1 - MUI Typography subtitle2 - MUI Typography caption - - - MUI Typography body1 - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit - esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa - qui officia deserunt mollit anim id est laborum. + + + + MUI Typo h1 + MUI Typography h2 + MUI Typography h3 + MUI Typography h4 + MUI Typography h5 + MUI Typography h6 + + MUI Typography subtitle1 + MUI Typography subtitle2 + MUI Typography caption + + + MUI Typography body1 - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit + esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa + qui officia deserunt mollit anim id est laborum. - - - MUI Typography body2 - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit - esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa - qui officia deserunt mollit anim id est laborum. + + + MUI Typography body2 - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit + esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa + qui officia deserunt mollit anim id est laborum. - - MUI Typography overline - - MUI Typography button - - - - - - - ); + + MUI Typography overline + + MUI Typography button + + +
+
+ ); }; -export default AboutView; \ No newline at end of file +export default AboutView; diff --git a/src/views/About/index.tsx b/src/views/About/index.tsx index 42612b6..00fedce 100644 --- a/src/views/About/index.tsx +++ b/src/views/About/index.tsx @@ -6,11 +6,11 @@ import AboutView from './About'; * url: /about/* */ const AboutRoutes = () => { - return ( - - - - ); + return ( + + + + ); }; -export default AboutRoutes; \ No newline at end of file +export default AboutRoutes; diff --git a/src/views/Auth/Auth.tsx b/src/views/Auth/Auth.tsx new file mode 100644 index 0000000..d870630 --- /dev/null +++ b/src/views/Auth/Auth.tsx @@ -0,0 +1,27 @@ +import { Grid, Typography } from '@material-ui/core'; +import { AppLink } from '../../components'; + +/** + * Renders default view for Auth flow + * url: /auth/ + */ +const AuthView = () => { + return ( + + Auth View +
    +
  • + Signup +
  • +
  • + Login +
  • +
  • + Recovery +
  • +
+
+ ); +}; + +export default AuthView; diff --git a/src/views/Auth/Login/Email.tsx b/src/views/Auth/Login/Email.tsx new file mode 100644 index 0000000..b88638c --- /dev/null +++ b/src/views/Auth/Login/Email.tsx @@ -0,0 +1,123 @@ +import { SyntheticEvent, useCallback, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Grid, TextField, Card, CardHeader, CardContent, InputAdornment } from '@material-ui/core'; +import { useAppStore } from '../../../store'; +import { AppButton, AppLink, AppIconButton, AppAlert, AppForm } from '../../../components'; +import { useAppForm, SHARED_CONTROL_PROPS, eventPreventDefault } from '../../../utils/form'; + +const VALIDATE_FORM_LOGIN_EMAIL = { + email: { + presence: true, + email: true, + }, + password: { + presence: true, + length: { + minimum: 8, + maximum: 32, + message: 'must be between 8 and 32 characters', + }, + }, +}; + +interface FormStateValues { + email: string; + password: string; +} + +/** + * Renders "Login with Email" view for Login flow + * url: /auth/login/email/* + */ +const LoginEmailView = () => { + const history = useHistory(); + const [, dispatch] = useAppStore(); + const [formState, , /* setFormState */ onFieldChange, fieldGetError, fieldHasError] = useAppForm({ + validationSchema: VALIDATE_FORM_LOGIN_EMAIL, + initialValues: { email: '', password: '' } as FormStateValues, + }); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(); + const values = formState.values as FormStateValues; // Typed alias to formState.values as the "Source of Truth" + + const handleShowPasswordClick = useCallback(() => { + setShowPassword((oldValue) => !oldValue); + }, []); + + const handleFormSubmit = useCallback( + async (event: SyntheticEvent) => { + event.preventDefault(); + + const result = true; // await api.auth.loginWithEmail(values); + if (!result) { + setError('Please check email and password'); + return; + } + + dispatch({ type: 'LOG_IN' }); + history.replace('/'); + }, + [dispatch, /*values,*/ history] + ); + + const handleCloseError = useCallback(() => setError(undefined), []); + + return ( + + + + + + + + + ), + }} + /> + {error ? ( + + {error} + + ) : null} + + + Login with Email + + + Forgot Password + + + + + + ); +}; + +export default LoginEmailView; diff --git a/src/views/Auth/Login/index.tsx b/src/views/Auth/Login/index.tsx new file mode 100644 index 0000000..24ebaca --- /dev/null +++ b/src/views/Auth/Login/index.tsx @@ -0,0 +1,17 @@ +import { Route, Switch } from 'react-router-dom'; +import LoginEmailView from './Email'; + +/** + * Routes for "Login" flow + * url: /auth/login/* + */ +const LoginRoutes = () => { + return ( + + + + + ); +}; + +export default LoginRoutes; diff --git a/src/views/Auth/Recovery/Password.tsx b/src/views/Auth/Recovery/Password.tsx new file mode 100644 index 0000000..edc6162 --- /dev/null +++ b/src/views/Auth/Recovery/Password.tsx @@ -0,0 +1,78 @@ +import { SyntheticEvent, useCallback, useState } from 'react'; +import { Grid, TextField, Card, CardHeader, CardContent } from '@material-ui/core'; +import { AppButton, AppAlert, AppForm } from '../../../components'; +import { useAppForm, SHARED_CONTROL_PROPS } from '../../../utils/form'; + +const VALIDATE_FORM_RECOVERY_PASSWORD = { + email: { + presence: true, + email: true, + }, +}; + +interface FormStateValues { + email: string; +} + +interface Props { + email?: string; +} + +/** + * Renders "Recover Password" view for Login flow + * url: /uth/recovery/password + * @param {string} [props.email] - pre-populated email in case the user already enters it + */ +const RecoveryPasswordView = ({ email = '' }: Props) => { + const [formState, , /* setFormState */ onFieldChange, fieldGetError, fieldHasError] = useAppForm({ + validationSchema: VALIDATE_FORM_RECOVERY_PASSWORD, + initialValues: { email } as FormStateValues, + }); + const [message, setMessage] = useState(); + const values = formState.values as FormStateValues; // Typed alias to formState.values as the "Source of Truth" + + const handleFormSubmit = async (event: SyntheticEvent) => { + event.preventDefault(); + + // await api.auth.recoverPassword(values); + + //Show message with instructions for the user + setMessage('Email with instructions has been sent to your address'); + }; + + const handleCloseError = useCallback(() => setMessage(undefined), []); + + return ( + + + + + + + {message ? ( + + {message} + + ) : null} + + + + Send Password Recovery Email + + + + + + ); +}; + +export default RecoveryPasswordView; diff --git a/src/views/Auth/Recovery/index.tsx b/src/views/Auth/Recovery/index.tsx new file mode 100644 index 0000000..1bb2647 --- /dev/null +++ b/src/views/Auth/Recovery/index.tsx @@ -0,0 +1,17 @@ +import { Route, Switch } from 'react-router-dom'; +import RecoveryPasswordView from './Password'; + +/** + * Routes for "Recovery" flow + * url: /auth/recovery/* + */ +const RecoveryRoutes = () => { + return ( + + + + + ); +}; + +export default RecoveryRoutes; diff --git a/src/views/Auth/Signup/ConfirmEmail.tsx b/src/views/Auth/Signup/ConfirmEmail.tsx new file mode 100644 index 0000000..ba3ec11 --- /dev/null +++ b/src/views/Auth/Signup/ConfirmEmail.tsx @@ -0,0 +1,61 @@ +import { useCallback, useState, useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import { Card, CardHeader, CardContent, TextField } from '@material-ui/core'; +import { SHARED_CONTROL_PROPS } from '../../../utils/form'; +import { AppAlert, AppForm } from '../../../components'; + +const TOKEN_QUERY_PARAM = 'token'; + +/** + * Renders "Confirm Email" view for Signup flow + * url: /auth/signup/confirm-email + */ +const ConfirmEmailView = () => { + const [email, setEmail] = useState(''); + const [error, setError] = useState(); + + function useQuery() { + return new URLSearchParams(useLocation().search); + } + const token = useQuery().get(TOKEN_QUERY_PARAM) || ''; + console.log('token:', token); + + useEffect(() => { + // Component Mount + let componentMounted = true; + + async function fetchData() { + //TODO: Call any Async API here + if (!componentMounted) return; // Component was unmounted during the API call + //TODO: Verify API call here + + setEmail('example@domain.com'); + } + fetchData(); // Call API asynchronously + + return () => { + // Component Un-mount + componentMounted = false; + }; + }, []); + + const handleCloseError = useCallback(() => setError(undefined), []); + + return ( + + + + + + {error ? ( + + {error} + + ) : null} + + + + ); +}; + +export default ConfirmEmailView; diff --git a/src/views/Auth/Signup/Signup.tsx b/src/views/Auth/Signup/Signup.tsx new file mode 100644 index 0000000..2eac104 --- /dev/null +++ b/src/views/Auth/Signup/Signup.tsx @@ -0,0 +1,266 @@ +import { SyntheticEvent, useCallback, useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + Grid, + TextField, + Card, + CardHeader, + CardContent, + Checkbox, + FormControlLabel, + InputAdornment, + LinearProgress, +} from '@material-ui/core'; +import { useAppStore } from '../../../store'; +import { AppButton, AppIconButton, AppAlert, AppForm } from '../../../components'; +import { useAppForm, SHARED_CONTROL_PROPS, eventPreventDefault } from '../../../utils/form'; + +const VALIDATE_FORM_SIGNUP = { + email: { + email: true, + presence: true, + }, + phone: { + type: 'string', + format: { + pattern: '^$|[- .+()0-9]+', // Note: We have to allow empty in the pattern + message: 'should contain numbers', + }, + // length: { + // is: 10, + // message: 'must be exactly 10 digits', + // }, + }, + firstName: { + type: 'string', + presence: { allowEmpty: false }, + format: { + pattern: '^[A-Za-z ]+$', // Note: Allow only alphabets and space + message: 'should contain only alphabets', + }, + }, + lastName: { + type: 'string', + presence: { allowEmpty: false }, + format: { + pattern: '^[A-Za-z ]+$', // Note: Allow only alphabets and space + message: 'should contain only alphabets', + }, + }, + password: { + presence: true, + length: { + minimum: 8, + maximum: 32, + message: 'must be between 8 and 32 characters', + }, + }, +}; + +const VALIDATE_EXTENSION = { + confirmPassword: { + equality: 'password', + }, +}; + +interface FormStateValues { + firstName: string; + lastName: string; + email: string; + phone: string; + password: string; + confirmPassword?: string; +} + +/** + * Renders "Signup" view + * url: /auth/signup + */ +const SignupView = () => { + const history = useHistory(); + const [, dispatch] = useAppStore(); + const [validationSchema, setValidationSchema] = useState({ + ...VALIDATE_FORM_SIGNUP, + ...VALIDATE_EXTENSION, + }); + const [formState, , /* setFormState */ onFieldChange, fieldGetError, fieldHasError] = useAppForm({ + validationSchema: validationSchema, // the state value, so could be changed in time + initialValues: { + firstName: '', + lastName: '', + email: '', + phone: '', + password: '', + confirmPassword: '', + } as FormStateValues, + }); + const [showPassword, setShowPassword] = useState(false); + const [agree, setAgree] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(); + const values = formState.values as FormStateValues; // Typed alias to formState.values as the "Source of Truth" + + useEffect(() => { + // Component Mount + let componentMounted = true; + + async function fetchData() { + //TODO: Call any Async API here + if (!componentMounted) return; // Component was unmounted during the API call + //TODO: Verify API call here + + setLoading(false); // Reset "Loading..." indicator + } + fetchData(); // Call API asynchronously + + return () => { + // Component Un-mount + componentMounted = false; + }; + }, []); + + useEffect(() => { + // Update Validation Schema when Show/Hide password changed + let newSchema; + if (showPassword) { + newSchema = VALIDATE_FORM_SIGNUP; // Validation without .confirmPassword + } else { + newSchema = { ...VALIDATE_FORM_SIGNUP, ...VALIDATE_EXTENSION }; // Full validation + } + setValidationSchema(newSchema); + }, [showPassword]); + + const handleShowPasswordClick = useCallback(() => { + setShowPassword((oldValue) => !oldValue); + }, []); + + const handleAgreeClick = useCallback(() => { + setAgree((oldValue) => !oldValue); + }, []); + + const handleFormSubmit = useCallback( + async (event: SyntheticEvent) => { + event.preventDefault(); + + const apiResult = true; // await api.auth.signup(values); + + if (!apiResult) { + setError('Can not create user for given email, if you already have account please sign in'); + return; // Unsuccessful signup + } + + dispatch({ type: 'SIGN_UP' }); + return history.replace('/'); + }, + [dispatch, /*values,*/ history] + ); + + const handleCloseError = useCallback(() => setError(undefined), []); + + if (loading) return ; + + return ( + + + + + + + + + + + + ), + }} + /> + {!showPassword && ( + + )} + } + label="You must agree with Terms of Use and Privacy Policy to use our service *" + /> + + {error ? ( + + {error} + + ) : null} + + + + Confirm and Sign Up + + + + + + ); +}; + +export default SignupView; diff --git a/src/views/Auth/Signup/index.tsx b/src/views/Auth/Signup/index.tsx new file mode 100644 index 0000000..396fb5a --- /dev/null +++ b/src/views/Auth/Signup/index.tsx @@ -0,0 +1,19 @@ +import { Route, Switch } from 'react-router-dom'; +import SignupView from './Signup'; +import ConfirmEmailView from './ConfirmEmail'; + +/** + * Routes for "Signup" flow + * url: /auth/signup/* + */ +const SignupRoutes = () => { + return ( + + + + + + ); +}; + +export default SignupRoutes; diff --git a/src/views/Auth/index.tsx b/src/views/Auth/index.tsx new file mode 100644 index 0000000..da6a8db --- /dev/null +++ b/src/views/Auth/index.tsx @@ -0,0 +1,22 @@ +import { Route, Switch } from 'react-router-dom'; +import AuthView from './Auth'; +import SignupRoutes from './Signup'; +import LoginRoutes from './Login'; +import RecoveryRoutes from './Recovery'; + +/** + * Routes for "Auth" flow + * url: /auth/* + */ +const AuthRoutes = () => { + return ( + + + + + + + ); +}; + +export default AuthRoutes; diff --git a/src/views/NotFound.tsx b/src/views/NotFound.tsx index 4a7c382..d46e641 100644 --- a/src/views/NotFound.tsx +++ b/src/views/NotFound.tsx @@ -2,7 +2,7 @@ * "Not Found" aka "Error 404" view */ const PageNotFoundView = () => { - return
Page not found!
; + return
Page not found!
; }; export default PageNotFoundView; diff --git a/src/views/NotImplemented.tsx b/src/views/NotImplemented.tsx index ab01079..fefc6cb 100644 --- a/src/views/NotImplemented.tsx +++ b/src/views/NotImplemented.tsx @@ -3,41 +3,41 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import { AppLink } from '../components'; interface MatchParams { - id?: string; + id?: string; } interface Props extends RouteComponentProps { - name?: string; + name?: string; } /** * Boilerplate for non-implemented Views */ class NotImplementedView extends Component { - render() { - const { name, location, match } = this.props; - const componentName = - name || (this as any)?.displayName || (this as any)?._reactInternalFiber?.elementType?.name || 'View'; - const paramId = match.params?.id; + render() { + const { name, location, match } = this.props; + const componentName = + name || (this as any)?.displayName || (this as any)?._reactInternalFiber?.elementType?.name || 'View'; + const paramId = match.params?.id; - return ( -
-

{componentName} is under construction

-

- This view is not implemented yet. Go to home page -

-

- You've called the {location?.pathname} url + return ( +

+

{componentName} is under construction

+

+ This view is not implemented yet. Go to home page +

+

+ You've called the {location?.pathname} url {paramId && ( - - {' '} + + {' '} where {paramId} is a parameter - - )} -

-
- ); - } + + )} +

+
+ ); + } } -export default withRouter(NotImplementedView); \ No newline at end of file +export default withRouter(NotImplementedView); diff --git a/src/views/index.tsx b/src/views/index.tsx index dbee2ae..68aacd2 100644 --- a/src/views/index.tsx +++ b/src/views/index.tsx @@ -2,6 +2,5 @@ import NotImplemented from './NotImplemented'; import NotFound from './NotFound'; import About from './About'; // import Welcome from './Welcome'; -// import Auth from './Auth/Auth'; -export { NotFound, About, NotImplemented as Welcome, NotImplemented as User }; \ No newline at end of file +export { NotFound, About, NotImplemented as Welcome, NotImplemented as User }; diff --git a/yarn.lock b/yarn.lock index fa9d9ae..7d48041 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8995,6 +8995,11 @@ prepend-http@^1.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= +prettier@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== + pretty-bytes@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"