diff --git a/src/App.tsx b/src/App.tsx index 8f9b332..7ea23d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,14 +3,30 @@ import React from "react"; import {Route, Routes} from "react-router-dom"; import {Notification, Search, User} from "@carbon/icons-react"; +import {useAtomValue, useSetAtom, useAtom} from "jotai"; +import {Loading} from "@carbon/react"; import './App.css' +import {activeItemAtom, currentUserAtom, currentUserAtomLoadable} from "./atoms"; import {NotFound, UIShell} from "./components"; -import {CustomerRisk, Dashboard, KYC, KYCCaseDetail, KYCCaseList, KycSummarize, RTCQC, Utilities} from "./views"; import {MenuLinksModel, NavigationModel} from "./models"; -import {DataExtraction} from "./views/DataExtraction"; +import { + CustomerRisk, + Dashboard, + DataExtraction, + KYC, + KYCCaseDetail, + KYCCaseList, + KycSummarize, + Login, + RTCQC, + Utilities +} from "./views"; function App() { + const setCurrentUser = useSetAtom(currentUserAtom); + const loadable = useAtomValue(currentUserAtomLoadable) + const [activeItem, setActiveItem] = useAtom(activeItemAtom) const menuLinks: MenuLinksModel[] = [ {title: 'Dashboard', href: '/', element: }, @@ -43,7 +59,9 @@ function App() { {title: 'Notifications', action: ()}, {title: 'User Profile', action: ()} ], - sideNav: menuLinks.map(link => ({title: link.title, href: link.href})) + sideNav: menuLinks + .filter(link => !link.excludeFromMenu) + .map(link => ({title: link.title, href: link.href})) } const renderMenuLinks = (menuLinks: MenuLinksModel[]) => { @@ -68,16 +86,26 @@ function App() { }) } - return ( -
- - - {renderMenuLinks(menuLinks)} - } /> - - -
- ) + if (loadable.state === 'loading' || loadable.state === 'hasError') { + return () + } + + const currentUser = loadable.data; + + if (!currentUser) { + return () + } + + return ( +
+ + + {renderMenuLinks(menuLinks)} + } /> + + +
+ ) } export default App diff --git a/src/atoms/current-user.atom.ts b/src/atoms/current-user.atom.ts new file mode 100644 index 0000000..5955543 --- /dev/null +++ b/src/atoms/current-user.atom.ts @@ -0,0 +1,7 @@ +import {atom} from "jotai"; +import {UserProfileModel} from "../models"; +import {loadable} from "jotai/utils"; + +export const currentUserAtom = atom>(Promise.resolve(undefined)); + +export const currentUserAtomLoadable = loadable(currentUserAtom); diff --git a/src/atoms/index.ts b/src/atoms/index.ts index 428a0b9..435acb1 100644 --- a/src/atoms/index.ts +++ b/src/atoms/index.ts @@ -3,4 +3,6 @@ export * from './menu-config.atom'; export * from './kyc-case-management.atom'; export * from './menu-options.atom'; export * from './data-extraction.atom'; -export * from './dashboard.atom.ts'; +export * from './dashboard.atom'; +export * from './current-user.atom'; +export * from './navigation.atom'; diff --git a/src/atoms/navigation.atom.ts b/src/atoms/navigation.atom.ts new file mode 100644 index 0000000..a26c87b --- /dev/null +++ b/src/atoms/navigation.atom.ts @@ -0,0 +1,3 @@ +import {atom} from "jotai"; + +export const activeItemAtom = atom('') diff --git a/src/components/UIShell/UIShell.tsx b/src/components/UIShell/UIShell.tsx index 8a46e49..d8a8dc0 100644 --- a/src/components/UIShell/UIShell.tsx +++ b/src/components/UIShell/UIShell.tsx @@ -25,142 +25,133 @@ import {ErrorBoundary} from "../ErrorBoundary"; import {isMenuItemModel, MenuModel, NavigationModel} from "../../models"; export interface UIShellProps { - prefix: string - title?: string - navigation?: NavigationModel - children: unknown + prefix: string; + title?: string; + navigation?: NavigationModel; + activeItem: string; + setActiveItem: (item: string) => void; + children: unknown; } -export interface UIShellState { - activeItem: string -} - -export class UIShell extends React.Component { - constructor(props: UIShellProps) { - super(props); - this.state = { - activeItem: `/${window.location.pathname.split('/')[1] ?? ''}` - }; +const renderHeaderMenuItem = (item: MenuModel) => { + if (isMenuItemModel(item)) { + return ({item.title}) } - renderHeaderMenuItem(item: MenuModel) { - if (isMenuItemModel(item)) { - return ({item.title}) - } + return ( + + {item.items.map(childItem => renderHeaderMenuItem(childItem))} + + ) +} - return ( - - {item.items.map(childItem => this.renderHeaderMenuItem(childItem))} - - ) +const renderHeaderNavigation = (props: UIShellProps, label: string = 'React App') => { + const headerNav = props.navigation?.headerNav + if (headerNav === undefined || headerNav.length === 0) { + return (<>) } - renderHeaderNavigation(label: string = 'React App') { - const headerNav = this.props.navigation?.headerNav - if (headerNav === undefined || headerNav.length === 0) { - return (<>) - } + return ( + + {headerNav.map(item => renderHeaderMenuItem(item))} + + ) +} - return ( - - {headerNav.map(item => this.renderHeaderMenuItem(item))} - - ) +const renderGlobalBar = (props: UIShellProps) => { + const globalBar = props.navigation?.globalBar + if (globalBar === undefined || globalBar.length === 0) { + return (<>) } - renderGlobalBar() { - const globalBar = this.props.navigation?.globalBar - if (globalBar === undefined || globalBar.length === 0) { - return (<>) - } + return ( + + {globalBar.map(item => { + return ( + + {item.action} + + ) + })} + + + ) +} +const renderSideNavItem = (activeItem: string, setActiveItem, item: MenuModel) => { + if (isMenuItemModel(item)) { return ( - - {globalBar.map(item => { - return ( - - {item.action} - - ) - })} - - + setActiveItem(item.href)} + key={item.title} + > + {item.title} + ) } - renderSideNavItem(item: MenuModel) { - if (isMenuItemModel(item)) { - return ( - { this.setState({ activeItem: item.href }) }} - key={item.title} - > - {item.title} - - ) - } + return ( + + {item.items.map(childItem => renderSideNavItem(activeItem, setActiveItem, childItem))} + + ) +} - return ( - - {item.items.map(childItem => this.renderSideNavItem(childItem))} - - ) +const renderSideNav = (props: UIShellProps, activeItem: string, setActiveItem: (item: string) => void, isSideNavExpanded: boolean) => { + const sideNav = props.navigation?.sideNav + if (sideNav === undefined || sideNav.length === 0) { + return (<>) } - renderSideNav(isSideNavExpanded: boolean) { - const sideNav = this.props.navigation?.sideNav - if (sideNav === undefined || sideNav.length === 0) { - return (<>) - } + return ( + + + {sideNav.map(item => renderSideNavItem(activeItem, setActiveItem, item))} + + + ) +} - return ( - - - {sideNav.map(item => this.renderSideNavItem(item))} - - - ) - } - render() { - return ( - - - void}) => ( -
-
- - - -  {this.props.title} - - {this.renderHeaderNavigation()} - {this.renderGlobalBar()} - - {this.renderSideNav(isSideNavExpanded)} - -
-
- )} - /> -
- - {this.props.children} - -
- ); - } +export const UIShell: React.FunctionComponent = (props: UIShellProps) => { + + return ( + + + void}) => ( +
+
+ + + +  {props.title} + + {renderHeaderNavigation(props)} + {renderGlobalBar(props)} + + {renderSideNav(props, props.activeItem, props.setActiveItem, isSideNavExpanded)} + +
+
+ )} + /> +
+ + {props.children} + +
+ ); } diff --git a/src/models/index.ts b/src/models/index.ts index 3aec69c..f373d29 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -4,3 +4,4 @@ export * from './menu-links.model'; export * from './kyc-case.model'; export * from './form-option.model'; export * from './data-extraction.model'; +export * from './user-profile.model'; diff --git a/src/models/menu-links.model.ts b/src/models/menu-links.model.ts index 24b36bd..f717291 100644 --- a/src/models/menu-links.model.ts +++ b/src/models/menu-links.model.ts @@ -3,5 +3,6 @@ export interface MenuLinksModel { title: string; href: string; element: any; + excludeFromMenu?: boolean; subMenus?: MenuLinksModel[]; } diff --git a/src/models/user-profile.model.ts b/src/models/user-profile.model.ts new file mode 100644 index 0000000..22b9f2e --- /dev/null +++ b/src/models/user-profile.model.ts @@ -0,0 +1,11 @@ + +export interface UserProfileModel { + firstName: string; + lastName: string; + roles?: string[]; +} + +export interface LoginModel { + username: string; + password: string; +} diff --git a/src/services/index.ts b/src/services/index.ts index ddfd3d2..c34cbf6 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -2,3 +2,4 @@ export * from './kyc-case-management'; export * from './menu-options'; export * from './data-extraction'; export * from './file-upload'; +export * from './login'; diff --git a/src/services/login/index.ts b/src/services/login/index.ts new file mode 100644 index 0000000..9374e76 --- /dev/null +++ b/src/services/login/index.ts @@ -0,0 +1,13 @@ +import {LoginApi} from "./login.api"; +import {LoginMock} from "./login.mock"; + +export * from './login.api'; + +let _instance: LoginApi; +export const loginApi = (): LoginApi => { + if (_instance) { + return _instance + } + + return _instance = new LoginMock(); +} diff --git a/src/services/login/login.api.ts b/src/services/login/login.api.ts new file mode 100644 index 0000000..50cd774 --- /dev/null +++ b/src/services/login/login.api.ts @@ -0,0 +1,5 @@ +import {LoginModel, UserProfileModel} from "../../models"; + +export abstract class LoginApi { + abstract login(loginData: LoginModel): Promise; +} diff --git a/src/services/login/login.mock.ts b/src/services/login/login.mock.ts new file mode 100644 index 0000000..4ef0b90 --- /dev/null +++ b/src/services/login/login.mock.ts @@ -0,0 +1,13 @@ + +import {LoginApi} from "./login.api"; +import {UserProfileModel} from "../../models"; + +export class LoginMock implements LoginApi { + async login(): Promise { + + return { + firstName: 'John', + lastName: 'Doe', + } + } +} \ No newline at end of file diff --git a/src/views/Login/Login.tsx b/src/views/Login/Login.tsx new file mode 100644 index 0000000..66ad7b4 --- /dev/null +++ b/src/views/Login/Login.tsx @@ -0,0 +1,67 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import React, {useState} from 'react'; +import {Button, Form, TextInput} from "@carbon/react"; +import PasswordInput from "@carbon/react/es/components/TextInput/PasswordInput"; + +import {Stack} from "../../components"; +import {LoginModel} from "../../models"; +import {loginApi} from "../../services"; + +export interface LoginProps { + setCurrentUser; +} + +export const Login: React.FunctionComponent = (props: LoginProps) => { + const [login, setLogin] = useState({username: '', password: ''}); + + const service = loginApi(); + + const handleSubmit = () => { + service.login(login) + .then(profile => props.setCurrentUser(Promise.resolve(profile))) + } + + const handleChange = (value: keyof LoginModel) => { + return (event) => { + const newLogin = Object.assign({}, login); + + newLogin[value] = event.target.value; + + setLogin(newLogin); + } + } + + return ( +
+
+
+ +

Login

+ + +
+
+
+
+
+ ) +} diff --git a/src/views/Login/index.ts b/src/views/Login/index.ts new file mode 100644 index 0000000..705e9e6 --- /dev/null +++ b/src/views/Login/index.ts @@ -0,0 +1,2 @@ + +export * from './Login' diff --git a/src/views/Main/Main.tsx b/src/views/Main/Main.tsx new file mode 100644 index 0000000..6f364dc --- /dev/null +++ b/src/views/Main/Main.tsx @@ -0,0 +1,21 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import React from 'react'; +import {Outlet} from "react-router-dom"; + +import {UIShell} from "../../components"; +import {NavigationModel} from "../../models"; + +export interface MainProps { + navigation: NavigationModel +} + +export const Main: React.FunctionComponent = (props: MainProps) => { + return ( +
+ + + +
+ ) +} diff --git a/src/views/index.ts b/src/views/index.ts index bc466f2..640469a 100644 --- a/src/views/index.ts +++ b/src/views/index.ts @@ -5,3 +5,5 @@ export * from './RTCQC' export * from './CustomerRisk' export * from './KYC' export * from './KycSummarize' +export * from './Login' +export * from './DataExtraction'