Feature/4 pilots add (#20)
* changing api url to env variable * updating env variables * updating env variables * updating env variables * updating env variables * updating env variables * updating env variables * feature/4-pilots---add * feature/4-pilots---add * feature/4-pilots---add * feature/4-pilots---add * ad pilot * add pilot * add pilot * add pilot * add pilot * add pilot * adding pilots * add pilot * add pilot * add pilots * add pilot * add pilot * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * Testing menu button * Testing menu button * Testing menu button * Testing menu button * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot --------- Co-authored-by: Noah Spannbauer <nspannbauer@sensonix.com>
This commit was merged in pull request #20.
This commit is contained in:
7905
app/package-lock.json
generated
7905
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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.6.8",
|
||||
"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",
|
||||
|
||||
@@ -2,21 +2,23 @@ import { useEffect } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import Pilots from './components/pilots/Pilots';
|
||||
import { useAppContext } from './hooks/appContext/UseAppContext';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
|
||||
import { useFeatureFlag } from './hooks/featureFlag/UseFeatureFlag';
|
||||
import SiteNav from './components/siteNav/SiteNav';
|
||||
|
||||
const App: React.FC<unknown> = () => {
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const appContext = useAppContext();
|
||||
|
||||
useEffect(() => {
|
||||
const getFeatureFlags = async () => {
|
||||
try {
|
||||
const featureFlagKeys: string = 'flying-pilots';
|
||||
const response: AxiosResponse = await axios.get(
|
||||
`http://localhost:7071/api/featureFlags?keys=${featureFlagKeys}&label=${process.env.NODE_ENV}`
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/featureFlags?keys=${featureFlagKeys}&label=${import.meta.env.MODE}`
|
||||
);
|
||||
const featureFlags: { key: string; enabled: boolean }[] = response.data;
|
||||
console.log(featureFlags);
|
||||
|
||||
if (featureFlags.length > 0) {
|
||||
appContext.dispatch({
|
||||
@@ -33,11 +35,14 @@ const App: React.FC<unknown> = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
{useFeatureFlag('flying-pilots')?.enabled && (
|
||||
<Route path="/" element={<Pilots />} />
|
||||
)}
|
||||
</Routes>
|
||||
<div className="container mx-auto">
|
||||
<SiteNav />
|
||||
<Routes>
|
||||
{useFeatureFlag('flying-pilots')?.enabled && (
|
||||
<Route path="/" element={<Pilots />} />
|
||||
)}
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import SiteNav from '../siteNav/SiteNav';
|
||||
|
||||
const Logbook: React.FC<unknown> = () => {
|
||||
return (
|
||||
<div>
|
||||
<SiteNav />
|
||||
<div>Logbook goes here</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
5
app/src/components/pilotForm/IPilotFormProps.ts
Normal file
5
app/src/components/pilotForm/IPilotFormProps.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface IPilotFormProps {
|
||||
pilotId?: string;
|
||||
isDrawerOpen: boolean;
|
||||
onOpenCloseDrawer: () => void;
|
||||
}
|
||||
@@ -1,85 +1,473 @@
|
||||
// import {
|
||||
// Accordion,
|
||||
// AccordionItem,
|
||||
// Button,
|
||||
// DatePicker,
|
||||
// Input,
|
||||
// Select,
|
||||
// SelectItem
|
||||
// } from '@nextui-org/react';
|
||||
// import { useForm, Controller, SubmitHandler } from 'react-hook-form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
useForm,
|
||||
Controller,
|
||||
FormProvider,
|
||||
FieldValues
|
||||
} from 'react-hook-form';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
Input,
|
||||
Option,
|
||||
PeoplePicker,
|
||||
SaveIcon,
|
||||
Select,
|
||||
StateSelect,
|
||||
Typography,
|
||||
XmarkIcon
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { IPilotFormProps } from './IPilotFormProps';
|
||||
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
||||
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
||||
import { Person } from '@microsoft/microsoft-graph-types';
|
||||
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { IPilotFormCertificates } from '../pilotFormCertificates/IPilotFormCertificates';
|
||||
import { IPilotFormEndorsements } from '../pilotFormEndorsements/IPilotFormEndorsements';
|
||||
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
|
||||
|
||||
// const PilotForm: React.FC<unknown> = () => {
|
||||
// const { control, handleSubmit } = useForm();
|
||||
const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
pilotId,
|
||||
isDrawerOpen,
|
||||
onOpenCloseDrawer
|
||||
}: IPilotFormProps) => {
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const [peoplePickerResults, setPeoplePickerResults] = useState<Person[]>([]);
|
||||
const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
|
||||
useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const methods = useForm();
|
||||
|
||||
// const onSubmit = (data: unknown) => {
|
||||
// console.log(data);
|
||||
// };
|
||||
const handlePeoplePickerOnClick = (
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
const divElement: HTMLDivElement = event.target as HTMLDivElement;
|
||||
|
||||
// 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>
|
||||
// </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>
|
||||
// );
|
||||
// };
|
||||
methods.setValue('id', divElement.id);
|
||||
methods.setValue('name', divElement.textContent);
|
||||
setPeoplePickerResults([]);
|
||||
};
|
||||
|
||||
// export default PilotForm;
|
||||
const handlePeoplePickerOnChange = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setIsPeoplePickerLoading(true);
|
||||
|
||||
try {
|
||||
methods.setValue('name', event.target.value);
|
||||
|
||||
const searchString: string = event.target.value;
|
||||
const accessToken: string = await getAccessToken();
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/personSearch?search=${searchString}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(response);
|
||||
setPeoplePickerResults(response.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsPeoplePickerLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: unknown) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const accessToken: string = await getAccessToken();
|
||||
const response: AxiosResponse = await httpClient.post(
|
||||
`api/pilots`,
|
||||
data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response) console.log(response);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errResp = error.response;
|
||||
|
||||
console.log(errResp?.data.message);
|
||||
} else {
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(methods.formState.errors);
|
||||
}, [methods.formState.errors]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={isDrawerOpen}
|
||||
placement="right"
|
||||
size={1000}
|
||||
data-testid="pilot-drawer"
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<DrawerHeader text="Add Pilot" onClose={onOpenCloseDrawer} />
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
<DrawerBody>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Name *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="name"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A name must be selected' }}
|
||||
render={({ field: { disabled, value } }) => (
|
||||
<PeoplePicker
|
||||
results={peoplePickerResults}
|
||||
inputProps={{
|
||||
disabled: disabled,
|
||||
labelProps: {
|
||||
className: 'before:content-none after:content-none'
|
||||
},
|
||||
onChange: (event) => handlePeoplePickerOnChange(event),
|
||||
error: methods.formState.errors.name ? true : false,
|
||||
helperText: methods.formState.errors.name
|
||||
? methods.formState.errors.name.message?.toString()
|
||||
: undefined,
|
||||
value: value
|
||||
}}
|
||||
listItemProps={{
|
||||
children: null,
|
||||
onClick: handlePeoplePickerOnClick
|
||||
}}
|
||||
loading={isPeoplePickerLoading}
|
||||
data-testid="pilot-form-people-picker"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Address *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="address"
|
||||
control={methods.control}
|
||||
rules={{ required: 'An address is required' }}
|
||||
render={({ field: { disabled, onChange, value } }) => (
|
||||
<Input
|
||||
className="!border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-address-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">City *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="city"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A city is required' }}
|
||||
render={({ field: { disabled, onChange, value } }) => (
|
||||
<Input
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
error={methods.formState.errors.city ? true : false}
|
||||
helperText={
|
||||
methods.formState.errors.city
|
||||
? methods.formState.errors.city.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-city-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">State *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="state"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A state must be selected' }}
|
||||
render={({ field: { disabled, onChange, value } }) => (
|
||||
<StateSelect
|
||||
disabled={disabled}
|
||||
error={methods.formState.errors.state ? true : false}
|
||||
helperText={
|
||||
methods.formState.errors.state
|
||||
? methods.formState.errors.state.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
variant="outlined"
|
||||
data-testid="pilot-form-state-dropdown"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Postal Code *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="postalCode"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A postal code is required' }}
|
||||
render={({ field: { disabled, onChange, value } }) => (
|
||||
<Input
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
error={methods.formState.errors.postalCode ? true : false}
|
||||
helperText={
|
||||
methods.formState.errors.postalCode
|
||||
? methods.formState.errors.postalCode.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-postal-code-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Email</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="email"
|
||||
control={methods.control}
|
||||
rules={{
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: 'Invalid email address'
|
||||
}
|
||||
}}
|
||||
render={({ field: { disabled, onChange, value } }) => (
|
||||
<Input
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
error={methods.formState.errors.email ? true : false}
|
||||
helperText={
|
||||
methods.formState.errors.email
|
||||
? methods.formState.errors.email.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-email-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Phone Number</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="phone"
|
||||
control={methods.control}
|
||||
rules={{
|
||||
pattern: {
|
||||
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
|
||||
message: 'Enter phone number as 123-456-7890'
|
||||
}
|
||||
}}
|
||||
render={({ field: { disabled, onChange, value } }) => (
|
||||
<Input
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
error={methods.formState.errors.phone ? true : false}
|
||||
helperText={
|
||||
methods.formState.errors.phone
|
||||
? methods.formState.errors.phone.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-phone-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{pilotId && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Last Review</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="lastReview"
|
||||
control={methods.control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
handleDateChanged={(date: string) => {
|
||||
methods.setValue('lastReview', date);
|
||||
}}
|
||||
inputProps={{
|
||||
value: field.value
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{pilotId && (
|
||||
<>
|
||||
<div className="col-span-4">
|
||||
<Typography variant="h5">Medical</Typography>
|
||||
<hr className="my-3" />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Class</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="medicalClass"
|
||||
control={methods.control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Select
|
||||
labelProps={{
|
||||
className:
|
||||
'before:content-none after:content-none'
|
||||
}}
|
||||
{...field}
|
||||
>
|
||||
<Option key="first" value="First">
|
||||
First
|
||||
</Option>
|
||||
<Option key="second" value="Second">
|
||||
Second
|
||||
</Option>
|
||||
<Option key="third" value="Third">
|
||||
Third
|
||||
</Option>
|
||||
<Option key="basicMed" value="Basic Med">
|
||||
Basic Med
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Expiration Date</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="medicalExpiration"
|
||||
control={methods.control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
handleDateChanged={(date: string) => {
|
||||
methods.setValue('medicalExpiration', date);
|
||||
}}
|
||||
inputProps={{
|
||||
value: field.value
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{pilotId && (
|
||||
<>
|
||||
<div className="col-span-4">
|
||||
<Typography variant="h5">Certificates</Typography>
|
||||
<hr className="my-3" />
|
||||
</div>
|
||||
<PilotFormCertificates certificates={[]} />
|
||||
</>
|
||||
)}
|
||||
{pilotId && (
|
||||
<>
|
||||
<div className="col-span-4">
|
||||
<Typography variant="h5">Endorsements</Typography>
|
||||
<hr className="my-3" />
|
||||
</div>
|
||||
<PilotFormEndorsements endorsements={[]} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
<div className="flex gap-2 justify-end justify-self-center pt-4">
|
||||
<div>
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
variant="outlined"
|
||||
onClick={onOpenCloseDrawer}
|
||||
data-testid="pilot-cancel-button"
|
||||
>
|
||||
<XmarkIcon size="lg" />
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
loading={isLoading}
|
||||
variant="filled"
|
||||
type="submit"
|
||||
data-testid="pilot-save-button"
|
||||
>
|
||||
<SaveIcon size="lg" />
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerFooter>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PilotForm;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Certificate } from './certificate.type';
|
||||
|
||||
export interface IPilotFormCertificates {
|
||||
certificates: Certificate[];
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { IPilotFormCertificates } from './IPilotFormCertificates';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Input,
|
||||
Option,
|
||||
PlusIcon,
|
||||
Select,
|
||||
TrashIcon,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
const PilotFormCertificates: React.FC<IPilotFormCertificates> = ({
|
||||
certificates
|
||||
}: IPilotFormCertificates) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useFormContext();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'certificates',
|
||||
control
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.length > 0 && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Type</Typography>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Number</Typography>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Date of Issue</Typography>
|
||||
</div>
|
||||
<div className="col-span-1"></div>
|
||||
</>
|
||||
)}
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`certificates.${index}.type`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Select label="Type" {...field}>
|
||||
<Option key="student" value="Student">
|
||||
Student
|
||||
</Option>
|
||||
<Option key="private" value="Private">
|
||||
Private
|
||||
</Option>
|
||||
<Option key="instrument" value="Instrument">
|
||||
Instrument
|
||||
</Option>
|
||||
<Option key="recreational" value="Recreational">
|
||||
Recreational
|
||||
</Option>
|
||||
<Option key="sport" value="Sport">
|
||||
Sport
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`certificates.${index}.number`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return <Input label="Number" {...field} />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`certificates.${index}.dateOfIssue`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
handleDateChanged={(date: string) => {
|
||||
setValue(`certificates.${index}.dateOfIssue`, date);
|
||||
}}
|
||||
inputProps={{
|
||||
value: field.value
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => remove(index)}
|
||||
variant="outlined"
|
||||
>
|
||||
<TrashIcon size="lg" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<div className="col-span-4">
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => {
|
||||
append({
|
||||
type: '',
|
||||
number: '',
|
||||
dateOfIssue: null
|
||||
});
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
<PlusIcon size="lg" />
|
||||
Add Certificate
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PilotFormCertificates;
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Certificate = {
|
||||
type: string;
|
||||
number: string;
|
||||
dateOfIssue: Date;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Endorsement } from './endorsement.type';
|
||||
|
||||
export interface IPilotFormEndorsements {
|
||||
endorsements: Endorsement[];
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { IPilotFormEndorsements } from './IPilotFormEndorsements';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Input,
|
||||
Option,
|
||||
PlusIcon,
|
||||
Select,
|
||||
TrashIcon,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
const PilotFormEndorsements: React.FC<IPilotFormEndorsements> = ({
|
||||
endorsements
|
||||
}: IPilotFormEndorsements) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useFormContext();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'endorsements',
|
||||
control
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.length > 0 && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Type</Typography>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Date of Issue</Typography>
|
||||
</div>
|
||||
<div className="col-span-1"></div>
|
||||
<div className="col-span-1"></div>
|
||||
</>
|
||||
)}
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`endorsements.${index}.type`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Select label="Type" {...field}>
|
||||
<Option key="complex" value="Complex">
|
||||
Complex
|
||||
</Option>
|
||||
<Option key="highPerformance" value="High Performance">
|
||||
High Performance
|
||||
</Option>
|
||||
<Option key="highAltitude" value="High Altitude">
|
||||
High Altitude
|
||||
</Option>
|
||||
<Option key="tailwheel" value="Tailwheel">
|
||||
Tailwheel
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`endorsements.${index}.dateOfIssue`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
handleDateChanged={(date: string) => {
|
||||
setValue(`endorsements.${index}.dateOfIssue`, date);
|
||||
}}
|
||||
inputProps={{
|
||||
value: field.value
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => remove(index)}
|
||||
variant="outlined"
|
||||
>
|
||||
<TrashIcon size="lg" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<div className="col-span-4">
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => {
|
||||
append({
|
||||
type: '',
|
||||
number: '',
|
||||
dateOfIssue: null
|
||||
});
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
<PlusIcon size="lg" />
|
||||
Add Endorsement
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PilotFormEndorsements;
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Endorsement = {
|
||||
type: string;
|
||||
number: string;
|
||||
dateOfIssue: Date;
|
||||
};
|
||||
@@ -1,65 +1,127 @@
|
||||
import { useState } from 'react';
|
||||
import SiteNav from '../siteNav/SiteNav';
|
||||
// import PilotForm from '../pilotForm/PilotForm';
|
||||
import { Button } from '@nextui-org/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import PilotForm from '../pilotForm/PilotForm';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerContent,
|
||||
DrawerFooter,
|
||||
DrawerHeader
|
||||
Button,
|
||||
EllipsisVerticalIcon,
|
||||
EyeIcon,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
PenIcon,
|
||||
PlusIcon,
|
||||
Table,
|
||||
TableColumnDef,
|
||||
TrashIcon,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
|
||||
const Pilots: React.FC<unknown> = () => {
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [pilots, setPilots] = useState<Pilot[]>([]);
|
||||
const onOpenCloseDrawer = () => {
|
||||
setIsDrawerOpen(!isDrawerOpen);
|
||||
};
|
||||
|
||||
type Pilot = {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const columns: TableColumnDef[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name'
|
||||
}
|
||||
// {
|
||||
// id: 'actions',
|
||||
// header: 'Actions',
|
||||
// cellProps: {
|
||||
// className: 'text-center'
|
||||
// },
|
||||
// cell: () => {
|
||||
// return (
|
||||
// <Menu placement="bottom-end">
|
||||
// <MenuHandler>
|
||||
// <div>
|
||||
// <IconButton variant="text">
|
||||
// <EllipsisVerticalIcon size="xl" />
|
||||
// </IconButton>
|
||||
// </div>
|
||||
// </MenuHandler>
|
||||
// <MenuList>
|
||||
// <MenuItem className="flex gap-3">
|
||||
// <PenIcon size="lg" />
|
||||
// Edit
|
||||
// </MenuItem>
|
||||
// <MenuItem className="flex gap-3">
|
||||
// <EyeIcon size="lg" />
|
||||
// View
|
||||
// </MenuItem>
|
||||
// <hr className="my-3" />
|
||||
// <MenuItem className="flex gap-3">
|
||||
// <TrashIcon size="lg" />
|
||||
// Delete
|
||||
// </MenuItem>
|
||||
// </MenuList>
|
||||
// </Menu>
|
||||
// );
|
||||
// },
|
||||
// enableSorting: false
|
||||
// }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const getPilots = async () => {
|
||||
try {
|
||||
const accessToken: string = await getAccessToken();
|
||||
const response: AxiosResponse = await httpClient.get(`api/pilots`, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
});
|
||||
console.log(response.data);
|
||||
setPilots(response.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
getPilots();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<SiteNav />
|
||||
<div className="px-6">
|
||||
<h1 role="heading">Pilots</h1>
|
||||
<Button
|
||||
color="default"
|
||||
variant="light"
|
||||
onClick={onOpenCloseDrawer}
|
||||
startContent={<FontAwesomeIcon icon={faPlus} />}
|
||||
data-testid="new-pilot-button"
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
<Drawer isOpen={isDrawerOpen} data-testid="pilot-drawer">
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<h2>Add Pilot</h2>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>{/* <PilotForm /> */}</DrawerBody>
|
||||
<DrawerFooter>
|
||||
<div className="flex gap-4 justify-end justify-self-center">
|
||||
<div>
|
||||
<Button
|
||||
color="default"
|
||||
onClick={onOpenCloseDrawer}
|
||||
data-testid="pilot-drawer-cancel-button"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button color="primary" onClick={onOpenCloseDrawer}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 w-full rounded-xl py-4 px-8 shadow-md backdrop-saturate-200 backdrop-blur-2xl bg-opacity-80 border border-white/80 bg-white mt-6">
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h2">Pilots</Typography>
|
||||
</div>
|
||||
<div className="col-span-1 justify-self-end">
|
||||
<Button
|
||||
className="flex justify-center gap-3"
|
||||
variant="filled"
|
||||
onClick={onOpenCloseDrawer}
|
||||
data-testid="pilot-add-button"
|
||||
>
|
||||
<PlusIcon size="lg" />
|
||||
Add Pilot
|
||||
</Button>
|
||||
</div>
|
||||
{pilots.length > 0 && <Table defaultData={pilots} columns={columns} />}
|
||||
</div>
|
||||
</div>
|
||||
<PilotForm
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onOpenCloseDrawer={onOpenCloseDrawer}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
7
app/src/components/siteNav/ISiteNavProps.ts
Normal file
7
app/src/components/siteNav/ISiteNavProps.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { InteractionStatus } from '@azure/msal-browser';
|
||||
|
||||
export interface ISiteNavProps {
|
||||
handleSignIn: () => void;
|
||||
handleSignOut: () => void;
|
||||
inProgress: InteractionStatus;
|
||||
}
|
||||
@@ -1,67 +1,258 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ISiteNavProps } from './ISiteNavProps';
|
||||
// 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';
|
||||
NavbarLinks,
|
||||
NavbarMenu,
|
||||
NavbarItemProps,
|
||||
PlaneIcon,
|
||||
SignOutIcon,
|
||||
Spinner,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
|
||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||
import { Logo, Plane } from '@noahspan/noahspan-components';
|
||||
import { InteractionStatus } from '@azure/msal-browser';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { User } from '@microsoft/microsoft-graph-types';
|
||||
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
|
||||
|
||||
type EventPayloadExtended = EventPayload & { accessToken: string };
|
||||
|
||||
const SiteNav: React.FC<unknown> = () => {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { accounts, instance } = useMsal();
|
||||
const initializeLogin = () => {
|
||||
instance.loginRedirect();
|
||||
};
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [userPhoto, setUserPhoto] = useState<string>();
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const appContext = useAppContext();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const { inProgress, instance } = useMsal();
|
||||
const navItems: NavbarItemProps[] = [
|
||||
{
|
||||
name: 'Pilots',
|
||||
url: '#'
|
||||
}
|
||||
];
|
||||
const handleSignIn = async () => {
|
||||
await instance.loginRedirect({
|
||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||
});
|
||||
};
|
||||
const handleSignOut = () => {
|
||||
instance.logoutRedirect();
|
||||
};
|
||||
const getUserProfile = async (accessToken: string): Promise<User> => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(`api/userProfile`, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
});
|
||||
const userProfile: User = response.data;
|
||||
|
||||
console.log(accounts);
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
const getUserPhoto = async (accessToken: string): Promise<string> => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(`api/userPhoto`, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
const arrayBufferView = new Uint8Array(response.data);
|
||||
const blob = new Blob([arrayBufferView], { type: 'image/png' });
|
||||
const imageUrl = window.URL.createObjectURL(blob);
|
||||
|
||||
return imageUrl;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const callback = instance.addEventCallback(
|
||||
async (message: EventMessage) => {
|
||||
if (message.eventType === EventType.LOGIN_SUCCESS) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const eventPayload: EventPayloadExtended =
|
||||
message.payload as EventPayloadExtended;
|
||||
const userProfile: User = await getUserProfile(
|
||||
eventPayload.accessToken
|
||||
);
|
||||
const userPhoto = await getUserPhoto(eventPayload.accessToken);
|
||||
|
||||
appContext.dispatch({
|
||||
type: 'SET_USER_PROFILE',
|
||||
payload: userProfile
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (callback) {
|
||||
instance.removeEventCallback(callback);
|
||||
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const setUserProfile = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const accessToken: string = await getAccessToken();
|
||||
const userProfile = await getUserProfile(accessToken);
|
||||
const userPhoto = await getUserPhoto(accessToken);
|
||||
|
||||
setUserPhoto(userPhoto);
|
||||
|
||||
appContext.dispatch({
|
||||
type: 'SET_USER_PROFILE',
|
||||
payload: userProfile
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
isAuthenticated &&
|
||||
Object.keys(appContext.state.userProfile).length === 0
|
||||
) {
|
||||
setUserProfile();
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
return (
|
||||
<Navbar
|
||||
classNames={{
|
||||
base: 'bg-transparent z-0'
|
||||
}}
|
||||
isBlurred={false}
|
||||
maxWidth="full"
|
||||
data-testid="flying-navbar"
|
||||
>
|
||||
<Navbar className="mt-6">
|
||||
<NavbarBrand>
|
||||
<Logo className="pr-3" height={50} width={50} data-testid="logo" />
|
||||
<Plane size="2xl" />
|
||||
<img height={40} width={40} src="noahspan-logo.png" />{' '}
|
||||
<PlaneIcon 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">
|
||||
{!loading && isAuthenticated && (
|
||||
<Menu placement="bottom-end">
|
||||
<MenuHandler>
|
||||
<>
|
||||
{/* {userPhoto && */}
|
||||
{/* <img className='rounded-full' src={userPhoto} /> */}
|
||||
{/* } */}
|
||||
{/* {!userPhoto && */}
|
||||
<div className="rounded-full text-white text-center pt-2 bg-black h-[40px] w-[40px]">
|
||||
NS
|
||||
</div>
|
||||
|
||||
{/* <div className="flex gap-2">
|
||||
<div className='flex-none'>
|
||||
|
||||
</div>
|
||||
<Button className='flex-1' variant="text" size="sm">
|
||||
{appContext.state.userProfile.displayName}
|
||||
</Button>
|
||||
</div> */}
|
||||
</>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleSignOut}>
|
||||
<Typography
|
||||
className="flex justify-center gap-3"
|
||||
variant="small"
|
||||
>
|
||||
<SignOutIcon size="lg" />
|
||||
Sign Out
|
||||
</Typography>
|
||||
</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>
|
||||
{loading && isAuthenticated && (
|
||||
<div className="flex justify-center gap-3">
|
||||
<Spinner size="xs" />
|
||||
<Typography variant="small">Loading...</Typography>
|
||||
</div>
|
||||
)}
|
||||
{!isAuthenticated && (
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
onClick={handleSignIn}
|
||||
loading={inProgress === InteractionStatus.Login ? true : false}
|
||||
>
|
||||
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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { User } from '@microsoft/microsoft-graph-types';
|
||||
|
||||
export interface IAppContextState {
|
||||
featureFlags: { key: string; enabled: boolean }[];
|
||||
userProfile: User;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
32
app/src/hooks/accessToken/UseAcessToken.tsx
Normal file
32
app/src/hooks/accessToken/UseAcessToken.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
AuthenticationResult,
|
||||
InteractionRequiredAuthError
|
||||
} from '@azure/msal-browser';
|
||||
import { useMsal } from '@azure/msal-react';
|
||||
|
||||
export const useAccessToken = () => {
|
||||
const { accounts, instance } = useMsal();
|
||||
const getAccessToken = async () => {
|
||||
const tokenRequest = {
|
||||
account: accounts[0],
|
||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||
};
|
||||
|
||||
try {
|
||||
const response: AuthenticationResult =
|
||||
await instance.acquireTokenSilent(tokenRequest);
|
||||
|
||||
return `Bearer ${response.accessToken}`;
|
||||
} catch (error) {
|
||||
if (error instanceof InteractionRequiredAuthError) {
|
||||
await instance.acquireTokenRedirect(tokenRequest);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getAccessToken
|
||||
};
|
||||
};
|
||||
24
app/src/hooks/httpClient/UseHttpClient.tsx
Normal file
24
app/src/hooks/httpClient/UseHttpClient.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import axios, { AxiosInstance, CreateAxiosDefaults } from 'axios';
|
||||
|
||||
export const useHttpClient = () => {
|
||||
let config: CreateAxiosDefaults<any>;
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
config = {
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const httpClient: AxiosInstance = axios.create(config);
|
||||
|
||||
return httpClient;
|
||||
};
|
||||
@@ -3,10 +3,8 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
h1 {
|
||||
@apply text-2xl font-bold leading-7 text-gray-900;
|
||||
}
|
||||
h2 {
|
||||
@apply text-2xl font-bold leading-7 text-gray-900;
|
||||
body {
|
||||
@apply bg-[#ECEFF1];
|
||||
@apply text-black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
11
app/src/vite-env.d.ts
vendored
11
app/src/vite-env.d.ts
vendored
@@ -1 +1,12 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
readonly VITE_CLIENT_ID: string;
|
||||
readonly VITE_TENANT_ID: string;
|
||||
readonly VITE_REDIRECT_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@@ -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: []
|
||||
};
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
|
||||
Reference in New Issue
Block a user