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 (
+
+ )
+}
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'