feature/4-pilots---add

This commit is contained in:
2024-06-22 07:14:30 -05:00
parent 7bcddf9d13
commit 9d5a6f7d09
35 changed files with 1430 additions and 3201 deletions

View File

@@ -10,13 +10,13 @@
"preview": "vite preview"
},
"dependencies": {
"@azure/msal-react": "^2.0.15",
"@azure/msal-browser": "^3.17.0",
"@azure/msal-react": "^2.0.19",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@nextui-org/react": "^2.3.6",
"@noahspan/noahspan-components": "^0.2.5",
"@noahspan/noahspan-components": "^0.3.0",
"axios": "^1.7.2",
"framer-motion": "^11.1.7",
"react": "^18.2.0",
@@ -25,6 +25,7 @@
"react-router-dom": "^6.23.0"
},
"devDependencies": {
"@microsoft/microsoft-graph-types": "^2.40.0",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",

View File

@@ -5,10 +5,25 @@ import { useAppContext } from './hooks/appContext/UseAppContext';
import { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
import { useFeatureFlag } from './hooks/featureFlag/UseFeatureFlag';
import { useMsal } from '@azure/msal-react';
import SiteNav from './components/siteNav/SiteNav';
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
import { User } from '@microsoft/microsoft-graph-types';
type EventPayloadExtended = EventPayload & { accessToken: string };
const App: React.FC<unknown> = () => {
const httpClient: AxiosInstance = useHttpClient();
const appContext = useAppContext();
const { instance } = useMsal();
const handleSignIn = async () => {
await instance.loginRedirect({
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
});
};
const handleSignOut = () => {
instance.logoutRedirect();
};
useEffect(() => {
const getFeatureFlags = async () => {
@@ -33,12 +48,51 @@ const App: React.FC<unknown> = () => {
getFeatureFlags();
}, []);
useEffect(() => {
const callback = instance.addEventCallback(
async (message: EventMessage) => {
if (message.eventType === EventType.LOGIN_SUCCESS) {
try {
const eventPayload: EventPayloadExtended =
message.payload as EventPayloadExtended;
const response: AxiosResponse = await httpClient.get(
`api/userProfile`,
{
headers: {
Authorization: `Bearer ${eventPayload.accessToken}`
}
}
);
const userProfile: User = response.data;
appContext.dispatch({
type: 'SET_USER_PROFILE',
payload: userProfile
});
} catch (error) {
console.log(error);
}
}
}
);
return () => {
if (callback) {
instance.removeEventCallback(callback);
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
}
};
}, []);
return (
<Routes>
{useFeatureFlag('flying-pilots')?.enabled && (
<Route path="/" element={<Pilots />} />
)}
</Routes>
<>
<SiteNav handleSignIn={handleSignIn} handleSignOut={handleSignOut} />
<Routes>
{useFeatureFlag('flying-pilots')?.enabled && (
<Route path="/" element={<Pilots />} />
)}
</Routes>
</>
);
};

View File

@@ -1,9 +1,6 @@
import SiteNav from '../siteNav/SiteNav';
const Logbook: React.FC<unknown> = () => {
return (
<div>
<SiteNav />
<div>Logbook goes here</div>
</div>
);

View File

@@ -7,78 +7,150 @@
// Select,
// SelectItem
// } from '@nextui-org/react';
// import { useForm, Controller, SubmitHandler } from 'react-hook-form';
// import { useForm, Controller, FormProvider, SubmitHandler, Form } from 'react-hook-form';
// import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
// const PilotForm: React.FC<unknown> = () => {
// const { control, handleSubmit } = useForm();
// const methods = useForm({
// defaultValues: {
// certificates: [
// {
// type: '',
// number: '',
// dateOfIssue: null
// }
// ]
// }
// });
// const { control, handleSubmit } = methods;
// const onSubmit = (data: unknown) => {
// console.log(data);
// };
// return (
// <form className="m-10" onSubmit={handleSubmit(onSubmit)}>
// <div className="grid grid-cols-2 gap-4">
// <div className="self-center">
// <label>First Name</label>
// </div>
// <div>
// <Controller
// name="firstName"
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className="self-center">
// <label>Last Name</label>
// </div>
// <div>
// <Controller
// name="lastName"
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// </div>
// <Accordion>
// <AccordionItem title="Medical Certificate">
// <div className="grid grid-cols-2 gap-4">
// <div className="self-center">
// <label>Class</label>
// <FormProvider {...methods}>
// <form className="m-10" onSubmit={handleSubmit(onSubmit)}>
// <Accordion>
// <AccordionItem title='Profile'>
// <div className="grid grid-cols-3 gap-4">
// <div className="self-center">
// <label>First Name</label>
// </div>
// <div className='col-span-2'>
// <Controller
// name="firstName"
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className="self-center">
// <label>Last Name</label>
// </div>
// <div className='col-span-2'>
// <Controller
// name="lastName"
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className='self-center'>
// <label>Address</label>
// </div>
// <div className='col-span-2'>
// <Controller
// name='address'
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className='self-center'>
// <label>City</label>
// </div>
// <div className='col-span-2'>
// <Controller
// name='city'
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className='self-center'>
// <label>State</label>
// </div>
// <div className='col-span-2'>
// <Controller
// name='state'
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className='self-center'>
// <label>Postal Code</label>
// </div>
// <div className='col-span-2'>
// <Controller
// name='postalCode'
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// </div>
// </AccordionItem>
// </Accordion>
// <Accordion>
// <AccordionItem title='Certificates'>
// <PilotFormCertificates certificates={[]}/>
// </AccordionItem>
// </Accordion>
// <Accordion>
// <AccordionItem title='Endorsements'>
// </AccordionItem>
// </Accordion>
// <Accordion>
// <AccordionItem title="Medical">
// <div className="grid grid-cols-2 gap-4">
// <div className="self-center">
// <label>Class</label>
// </div>
// <div>
// <Controller
// name="medicalClass"
// control={control}
// render={({ field }) => (
// <Select>
// <SelectItem key="first" value="First">
// First
// </SelectItem>
// <SelectItem key="second" value="Second">
// Second
// </SelectItem>
// <SelectItem key="third" value="Third">
// Third
// </SelectItem>
// <SelectItem key="basicMed" value="Basic Med">
// Basic Med
// </SelectItem>
// </Select>
// )}
// />
// </div>
// <div className="self-center">
// <label>Expires</label>
// </div>
// <div>
// <Controller
// name="medicalExpiration"
// control={control}
// render={({ field }) => <DatePicker />}
// />
// </div>
// </div>
// <div>
// <Controller
// name="medicalClass"
// control={control}
// render={({ field }) => (
// <Select>
// <SelectItem key="first" value="First">
// First
// </SelectItem>
// <SelectItem key="second" value="Second">
// Second
// </SelectItem>
// <SelectItem key="Third" value="Third">
// Third
// </SelectItem>
// </Select>
// )}
// />
// </div>
// <div className="self-center">
// <label>Expires</label>
// </div>
// <div>
// <Controller
// name="medicalExpiration"
// control={control}
// render={({ field }) => <DatePicker />}
// />
// </div>
// </div>
// </AccordionItem>
// </Accordion>
// </form>
// </AccordionItem>
// </Accordion>
// </form>
// </FormProvider>
// );
// };

View File

@@ -0,0 +1,5 @@
import { Certificate } from './certificate.type';
export interface IPilotFormCertificates {
certificates: Certificate[];
}

View File

@@ -0,0 +1,104 @@
// import { IPilotFormCertificates } from './IPilotFormCertificates';
// import { Button, DatePicker, Input, Select, SelectItem } from '@nextui-org/react';
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form'
// const PilotFormCertificates: React.FC<IPilotFormCertificates> = ({ certificates }: IPilotFormCertificates) => {
// const { control, formState: { errors } } = useFormContext();
// const { fields, append, remove } = useFieldArray({
// name: 'certificates',
// control
// })
// return (
// <div className='grid grid-cols-4 gap-4'>
// <div className='self-center'>
// <label>Type</label>
// </div>
// <div className='self-center'>
// <label>Number</label>
// </div>
// <div className='self-center '>
// <label>Date of Issue</label>
// </div>
// <div className='self-center'>
// </div>
// {fields.map((field, index) => {
// return (
// <>
// {/* // <div key={field.id}> */}
// <div>
// <Controller
// name={`certificates.${index}.type`}
// control={control}
// render={({ field }) => {
// console.log(field)
// return (
// <Select {...field}>
// <SelectItem key="student" value="Student">
// Student
// </SelectItem>
// <SelectItem key="private" value="Private">
// Private
// </SelectItem>
// <SelectItem key="instrument" value="Instrument">
// Instrument
// </SelectItem>
// <SelectItem key="recreational" value="Recreational">
// Recreational
// </SelectItem>
// <SelectItem key="sport" value="Sport">
// Sport
// </SelectItem>
// </Select>
// )
// }}
// />
// </div>
// <div>
// <Controller
// name={`certificates.${index}.number`}
// control={control}
// render={({ field }) => {
// return (
// <Input {...field} />
// )
// }}
// />
// </div>
// <div>
// <Controller
// name={`certificates.${index}.dateOfIssue`}
// control={control}
// render={({ field }) => {
// return (
// <DatePicker {...field} />
// )
// }}
// />
// </div>
// <div>
// <Button onClick={() => remove(index)}>Remove</Button>
// </div>
// {/* </div> */}
// </>
// )
// })}
// <Button
// onClick={() => {
// append({
// type: '',
// number: '',
// dateOfIssue: null
// });
// }}
// >
// Add
// </Button>
// </div>
// )
// }
// export default PilotFormCertificates;

View File

@@ -0,0 +1,5 @@
export type Certificate = {
type: string;
number: string;
dateOfIssue: Date;
};

View File

@@ -1,16 +1,6 @@
import { useState } from 'react';
import SiteNav from '../siteNav/SiteNav';
// import PilotForm from '../pilotForm/PilotForm';
import { Button } from '@nextui-org/react';
import {
Drawer,
DrawerBody,
DrawerContent,
DrawerFooter,
DrawerHeader
} from '@noahspan/noahspan-components';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Drawer } from '@noahspan/noahspan-components';
const Pilots: React.FC<unknown> = () => {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
@@ -20,24 +10,26 @@ const Pilots: React.FC<unknown> = () => {
return (
<div className="container mx-auto">
<SiteNav />
blah
<div className="px-6">
<h1 role="heading">Pilots</h1>
<Button
color="default"
variant="light"
variant="filled"
onClick={onOpenCloseDrawer}
startContent={<FontAwesomeIcon icon={faPlus} />}
// startContent={<FontAwesomeIcon icon={faPlus} />}
data-testid="new-pilot-button"
>
New
</Button>
<Drawer isOpen={isDrawerOpen} data-testid="pilot-drawer">
<DrawerContent>
<Drawer open={isDrawerOpen} data-testid="pilot-drawer">
Blah
{/* <DrawerContent>
<DrawerHeader>
<h2>Add Pilot</h2>
</DrawerHeader>
<DrawerBody>{/* <PilotForm /> */}</DrawerBody>
<DrawerBody>
<PilotForm />
</DrawerBody>
<DrawerFooter>
<div className="flex gap-4 justify-end justify-self-center">
<div>
@@ -56,7 +48,7 @@ const Pilots: React.FC<unknown> = () => {
</div>
</div>
</DrawerFooter>
</DrawerContent>
</DrawerContent> */}
</Drawer>
</div>
</div>

View File

@@ -1,65 +1,116 @@
// import { Link as ReactRouterLink } from 'react-router-dom';
import { useAppContext } from '../../hooks/appContext/UseAppContext';
import {
Avatar,
Link,
Button,
Menu,
MenuHandler,
MenuItem,
MenuList,
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Button
} from '@nextui-org/react';
import { Link as ReactRouterLink } from 'react-router-dom';
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
import { useAppContext } from '../../hooks/appContext/UseAppContext';
import { Logo, Plane } from '@noahspan/noahspan-components';
NavbarLinks,
NavbarMenu,
NavbarItemProps,
Plane,
Typography
} from '@noahspan/noahspan-components';
import { useIsAuthenticated } from '@azure/msal-react';
const SiteNav: React.FC<unknown> = () => {
const isAuthenticated = useIsAuthenticated();
const { accounts, instance } = useMsal();
const initializeLogin = () => {
instance.loginRedirect();
};
interface SiteNavProps {
handleSignIn: () => void;
handleSignOut: () => void;
}
const SiteNav: React.FC<SiteNavProps> = ({
handleSignIn,
handleSignOut
}: SiteNavProps) => {
const appContext = useAppContext();
const isAuthenticated = useIsAuthenticated();
const navItems: NavbarItemProps[] = [
{
name: 'Pilots',
url: '#'
}
];
return (
<Navbar
classNames={{
base: 'bg-transparent z-0'
}}
isBlurred={false}
maxWidth="full"
data-testid="flying-navbar"
>
<Navbar>
<NavbarBrand>
<Logo className="pr-3" height={50} width={50} data-testid="logo" />
<Plane size="2xl" />
<img height={40} width={40} src="noahspan-logo.png" />{' '}
<Plane size="2x" />
</NavbarBrand>
<NavbarContent justify="center">
<NavbarItem>
{appContext.state.featureFlags.find(
(featureFlag) => featureFlag.key === 'flying-pilots'
)?.enabled && (
<Link>
<ReactRouterLink to="/">Pilots</ReactRouterLink>
</Link>
<NavbarLinks items={navItems} />
<NavbarMenu>
<div className="flex items-center gap-2 hidden lg:inline-block">
{isAuthenticated && (
<Menu placement="bottom-end">
<MenuHandler>
<div>
<Typography>
{appContext.state.userProfile.displayName}
</Typography>
</div>
</MenuHandler>
<MenuList>
<MenuItem>
<Button variant="text" size="sm" onClick={handleSignOut}>
Sign Out
</Button>
</MenuItem>
</MenuList>
</Menu>
)}
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
{!isAuthenticated && (
<Button
as={Link}
color="primary"
href="#"
onClick={initializeLogin}
data-testid="login-link"
>
Login
</Button>
)}
{isAuthenticated && <Avatar name={accounts[0]?.username} />}
</NavbarContent>
{!isAuthenticated && (
<Button variant="text" size="sm" onClick={handleSignIn}>
Sign In
</Button>
)}
</div>
{/* <IconButton
variant='text'
className='ml-auto h-6 w-6 text-inherit hover:bg-transparent focus:bg-transparent active:bg-transparent lg:hidden'
ripple={false}
onClick={() => setOpenNav(!openNav)}
>
<FontAwesomeIcon icon={faBars} size='2x' />
</IconButton> */}
</NavbarMenu>
</Navbar>
);
// return (
// <Navbar
// classNames={{
// base: 'bg-transparent z-0'
// }}
// isBlurred={false}
// maxWidth="full"
// data-testid="flying-navbar"
// >
// <NavbarBrand>
// <Logo className="pr-3" height={50} width={50} data-testid="logo" />
// <Plane size="2xl" />
// </NavbarBrand>
// <NavbarContent justify="center">
// <NavbarItem>
// {appContext.state.featureFlags.find(
// (featureFlag) => featureFlag.key === 'flying-pilots'
// )?.enabled && (
// <Link>
// <ReactRouterLink to="/">Pilots</ReactRouterLink>
// </Link>
// )}
// </NavbarItem>
// </NavbarContent>
// <NavbarContent justify='end'>
// <Login
// loginCompleted={loginCompleted}
// loginView='compact'
// />
// </NavbarContent>
// </Navbar>
// );
};
export default SiteNav;

View File

@@ -9,7 +9,8 @@ const AppContextProvider: React.FC<IAppContextProviderProps> = (
props: IAppContextProviderProps
) => {
const intialState: IAppContextState = {
featureFlags: []
featureFlags: [],
userProfile: {}
};
const [state, dispatch] = useReducer(reducer, intialState);
const contextValue: IAppContextProps = useMemo(() => {

View File

@@ -1,3 +1,6 @@
import { User } from '@microsoft/microsoft-graph-types';
export interface IAppContextState {
featureFlags: { key: string; enabled: boolean }[];
userProfile: User;
}

View File

@@ -1,9 +1,9 @@
import { User } from '@microsoft/microsoft-graph-types';
import { IAppContextState } from './IAppContextState';
export type Action = {
type: 'SET_FEATURE_FLAGS';
payload: { key: string; enabled: boolean }[];
};
export type Action =
| { type: 'SET_FEATURE_FLAGS'; payload: { key: string; enabled: boolean }[] }
| { type: 'SET_USER_PROFILE'; payload: User };
export const reducer = (
state: IAppContextState,
@@ -16,6 +16,12 @@ export const reducer = (
featureFlags: action.payload
};
}
case 'SET_USER_PROFILE': {
return {
...state,
userProfile: action.payload
};
}
default: {
return state;
}

View File

@@ -1,34 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { MsalProvider } from '@azure/msal-react';
import { Configuration, PublicClientApplication } from '@azure/msal-browser';
import { NextUIProvider } from '@nextui-org/react';
import App from './App.tsx';
import { BrowserRouter } from 'react-router-dom';
import AppContextProvider from './context/appContext/AppContextProvider.tsx';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import '@noahspan/noahspan-components/noahspan-components.css';
import { PublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';
const configuration: Configuration = {
const pca: PublicClientApplication = new PublicClientApplication({
auth: {
clientId: 'd3562a45-050d-4f9a-baed-0497c7156924',
authority:
'https://login.microsoftonline.com/0f23652e-4b15-420f-991e-3d6fc769a31d',
redirectUri: 'http://localhost:5173'
clientId: import.meta.env.VITE_CLIENT_ID,
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`,
redirectUri: import.meta.env.VITE_REDIRECT_URL
}
};
const pca = new PublicClientApplication(configuration);
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MsalProvider instance={pca}>
<AppContextProvider>
<NextUIProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</NextUIProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</AppContextProvider>
</MsalProvider>
</React.StrictMode>

View File

@@ -2,6 +2,9 @@
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_CLIENT_ID: string;
readonly VITE_TENANT_ID: string;
readonly VITE_REDIRECT_URL: string;
}
interface ImportMeta {

View File

@@ -1,15 +1,9 @@
const { nextui } = require('@nextui-org/react');
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
'../node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {}
},
darkMode: 'class',
plugins: [nextui()]
plugins: []
};