diff --git a/app/public/mockServiceWorker.js b/app/public/mockServiceWorker.js
new file mode 100644
index 00000000..cb86abcd
--- /dev/null
+++ b/app/public/mockServiceWorker.js
@@ -0,0 +1,287 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker (2.2.3).
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+
+const INTEGRITY_CHECKSUM = '223d191a56023cd36aa88c802961b911'
+const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
+const activeClientIds = new Set()
+
+self.addEventListener('install', function () {
+ self.skipWaiting()
+})
+
+self.addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim())
+})
+
+self.addEventListener('message', async function (event) {
+ const clientId = event.source.id
+
+ if (!clientId || !self.clients) {
+ return
+ }
+
+ const client = await self.clients.get(clientId)
+
+ if (!client) {
+ return
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE',
+ })
+ break
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: INTEGRITY_CHECKSUM,
+ })
+ break
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId)
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: true,
+ })
+ break
+ }
+
+ case 'MOCK_DEACTIVATE': {
+ activeClientIds.delete(clientId)
+ break
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId)
+
+ const remainingClients = allClients.filter((client) => {
+ return client.id !== clientId
+ })
+
+ // Unregister itself when there are no more clients
+ if (remainingClients.length === 0) {
+ self.registration.unregister()
+ }
+
+ break
+ }
+ }
+})
+
+self.addEventListener('fetch', function (event) {
+ const { request } = event
+
+ // Bypass navigation requests.
+ if (request.mode === 'navigate') {
+ return
+ }
+
+ // Opening the DevTools triggers the "only-if-cached" request
+ // that cannot be handled by the worker. Bypass such requests.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ return
+ }
+
+ // Bypass all requests when there are no active clients.
+ // Prevents the self-unregistered worked from handling requests
+ // after it's been deleted (still remains active until the next reload).
+ if (activeClientIds.size === 0) {
+ return
+ }
+
+ // Generate unique request ID.
+ const requestId = crypto.randomUUID()
+ event.respondWith(handleRequest(event, requestId))
+})
+
+async function handleRequest(event, requestId) {
+ const client = await resolveMainClient(event)
+ const response = await getResponse(event, client, requestId)
+
+ // Send back the response clone for the "response:*" life-cycle events.
+ // Ensure MSW is active and ready to handle the message, otherwise
+ // this message will pend indefinitely.
+ if (client && activeClientIds.has(client.id)) {
+ ;(async function () {
+ const responseClone = response.clone()
+
+ sendToClient(
+ client,
+ {
+ type: 'RESPONSE',
+ payload: {
+ requestId,
+ isMockedResponse: IS_MOCKED_RESPONSE in response,
+ type: responseClone.type,
+ status: responseClone.status,
+ statusText: responseClone.statusText,
+ body: responseClone.body,
+ headers: Object.fromEntries(responseClone.headers.entries()),
+ },
+ },
+ [responseClone.body],
+ )
+ })()
+ }
+
+ return response
+}
+
+// Resolve the main client for the given event.
+// Client that issues a request doesn't necessarily equal the client
+// that registered the worker. It's with the latter the worker should
+// communicate with during the response resolving phase.
+async function resolveMainClient(event) {
+ const client = await self.clients.get(event.clientId)
+
+ if (client?.frameType === 'top-level') {
+ return client
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ return allClients
+ .filter((client) => {
+ // Get only those clients that are currently visible.
+ return client.visibilityState === 'visible'
+ })
+ .find((client) => {
+ // Find the client ID that's recorded in the
+ // set of clients that have registered the worker.
+ return activeClientIds.has(client.id)
+ })
+}
+
+async function getResponse(event, client, requestId) {
+ const { request } = event
+
+ // Clone the request because it might've been already used
+ // (i.e. its body has been read and sent to the client).
+ const requestClone = request.clone()
+
+ function passthrough() {
+ const headers = Object.fromEntries(requestClone.headers.entries())
+
+ // Remove internal MSW request header so the passthrough request
+ // complies with any potential CORS preflight checks on the server.
+ // Some servers forbid unknown request headers.
+ delete headers['x-msw-intention']
+
+ return fetch(requestClone, { headers })
+ }
+
+ // Bypass mocking when the client is not active.
+ if (!client) {
+ return passthrough()
+ }
+
+ // Bypass initial page load requests (i.e. static assets).
+ // The absence of the immediate/parent client in the map of the active clients
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+ // and is not ready to handle requests.
+ if (!activeClientIds.has(client.id)) {
+ return passthrough()
+ }
+
+ // Bypass requests with the explicit bypass header.
+ // Such requests can be issued by "ctx.fetch()".
+ const mswIntention = request.headers.get('x-msw-intention')
+ if (['bypass', 'passthrough'].includes(mswIntention)) {
+ return passthrough()
+ }
+
+ // Notify the client that a request has been intercepted.
+ const requestBuffer = await request.arrayBuffer()
+ const clientMessage = await sendToClient(
+ client,
+ {
+ type: 'REQUEST',
+ payload: {
+ id: requestId,
+ url: request.url,
+ mode: request.mode,
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ cache: request.cache,
+ credentials: request.credentials,
+ destination: request.destination,
+ integrity: request.integrity,
+ redirect: request.redirect,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ body: requestBuffer,
+ keepalive: request.keepalive,
+ },
+ },
+ [requestBuffer],
+ )
+
+ switch (clientMessage.type) {
+ case 'MOCK_RESPONSE': {
+ return respondWithMock(clientMessage.data)
+ }
+
+ case 'MOCK_NOT_FOUND': {
+ return passthrough()
+ }
+ }
+
+ return passthrough()
+}
+
+function sendToClient(client, message, transferrables = []) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel()
+
+ channel.port1.onmessage = (event) => {
+ if (event.data && event.data.error) {
+ return reject(event.data.error)
+ }
+
+ resolve(event.data)
+ }
+
+ client.postMessage(
+ message,
+ [channel.port2].concat(transferrables.filter(Boolean)),
+ )
+ })
+}
+
+async function respondWithMock(response) {
+ // Setting response status code to 0 is a no-op.
+ // However, when responding with a "Response.error()", the produced Response
+ // instance will have status code set to 0. Since it's not possible to create
+ // a Response instance with status code 0, handle that use-case separately.
+ if (response.status === 0) {
+ return Response.error()
+ }
+
+ const mockedResponse = new Response(response.body, response)
+
+ Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
+ value: true,
+ enumerable: true,
+ })
+
+ return mockedResponse
+}
diff --git a/app/src/components/common/__tests__/GuestInviteButton.test.tsx b/app/src/components/common/__tests__/GuestInviteButton.test.tsx
index cbb06980..63cba846 100644
--- a/app/src/components/common/__tests__/GuestInviteButton.test.tsx
+++ b/app/src/components/common/__tests__/GuestInviteButton.test.tsx
@@ -113,7 +113,7 @@ describe('', () => {
const message = 'There was an error sending the invite.';
server.use(
- http.post(`/api/auth/invite`, () => {
+ http.post('/api/auth/invite', () => {
return HttpResponse.json(
{
message,
diff --git a/app/src/components/dashboard/DashboardTask.tsx b/app/src/components/dashboard/DashboardTask.tsx
index 0e0a9c75..6d6295bc 100644
--- a/app/src/components/dashboard/DashboardTask.tsx
+++ b/app/src/components/dashboard/DashboardTask.tsx
@@ -2,34 +2,29 @@ import {Stack, Box, Typography, Button} from '@mui/material';
import CheckCircleOutlined from '@mui/icons-material/CheckCircleOutlined';
import LockIcon from '@mui/icons-material/Lock';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
-import {useNavigate} from 'react-router-dom';
+import {Link} from 'react-router-dom';
import {SubTask} from '../../views/GuestApplicationTracker';
export type DashboardTaskProps = Pick<
SubTask,
- 'title' | 'description' | 'status' | 'buttonText' | 'url'
+ 'title' | 'description' | 'status' | 'linkText' | 'url'
>;
export const DashboardTask = ({
title,
description,
status,
- buttonText,
+ linkText,
url,
}: DashboardTaskProps) => {
- const navigate = useNavigate();
-
+ console.log(url);
const statusIcons = {
inProgress: ,
complete: ,
locked: ,
};
- const handleClick = () => {
- navigate(url);
- };
-
return (
{statusIcons[status]}
@@ -53,9 +48,10 @@ export const DashboardTask = ({
size="medium"
variant="contained"
disabled={status === 'complete'}
- onClick={handleClick}
+ to={url}
+ component={Link}
>
- {buttonText}
+ {linkText}
) : (
Upcoming
diff --git a/app/src/components/dashboard/DashboardTaskAccordion.tsx b/app/src/components/dashboard/DashboardTaskAccordion.tsx
index 79bc657e..84df9541 100644
--- a/app/src/components/dashboard/DashboardTaskAccordion.tsx
+++ b/app/src/components/dashboard/DashboardTaskAccordion.tsx
@@ -69,14 +69,14 @@ export const DashboardTaskAccordion = ({
- {subTasks.map(({id, title, description, status, buttonText, url}) => {
+ {subTasks.map(({id, title, description, status, linkText, url}) => {
return (
);
diff --git a/app/src/components/dashboard/__tests__/DashboardTask.test.tsx b/app/src/components/dashboard/__tests__/DashboardTask.test.tsx
index d9fb4d0e..56189891 100644
--- a/app/src/components/dashboard/__tests__/DashboardTask.test.tsx
+++ b/app/src/components/dashboard/__tests__/DashboardTask.test.tsx
@@ -21,7 +21,7 @@ function setup(props?: Partial) {
title: 'Application',
status: 'inProgress',
description: 'Start your guest application to move on to the next step.',
- buttonText: 'Start Application',
+ linkText: 'Start Application',
url: '/guest-application',
...props,
};
@@ -48,17 +48,14 @@ describe('DashboardTask', () => {
});
describe('when the task is in progress', () => {
- it('should render a button to start the task', async () => {
- const user = userEvent.setup();
+ it('should render a link to start the task', async () => {
+ userEvent.setup();
const {props} = setup();
- const button = screen.getByRole('button', {name: props.buttonText});
+ const link = screen.getByRole('link', {name: props.linkText});
- expect(button).toBeInTheDocument();
- expect(button).not.toBeDisabled();
-
- await user.click(button);
- expect(navigate).toHaveBeenCalledWith(props.url);
+ expect(link).toBeInTheDocument();
+ expect(link).not.toBeDisabled();
});
it('should render clock icon', () => {
@@ -69,12 +66,12 @@ describe('DashboardTask', () => {
});
describe('when the task is complete', () => {
- it('should render a disabled button', () => {
+ it('should render a disabled link', () => {
const {props} = setup({status: 'complete'});
- const button = screen.getByRole('button', {name: props.buttonText});
+ const link = screen.getByRole('link', {name: props.linkText});
- expect(button).toBeInTheDocument();
- expect(button).toBeDisabled();
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute('aria-disabled', 'true');
});
it('should render check icon', () => {
@@ -89,7 +86,7 @@ describe('DashboardTask', () => {
const {props} = setup({status: 'locked'});
expect(
- screen.queryByRole('button', {name: props.buttonText}),
+ screen.queryByRole('link', {name: props.linkText}),
).not.toBeInTheDocument();
expect(screen.getByText('Upcoming')).toBeInTheDocument();
});
diff --git a/app/src/components/dashboard/__tests__/DashboardTaskAccordion.test.tsx b/app/src/components/dashboard/__tests__/DashboardTaskAccordion.test.tsx
index 7f76e0c4..835b69a9 100644
--- a/app/src/components/dashboard/__tests__/DashboardTaskAccordion.test.tsx
+++ b/app/src/components/dashboard/__tests__/DashboardTaskAccordion.test.tsx
@@ -15,7 +15,7 @@ const task: TaskAccordionProps = {
title: 'Application',
status: 'complete',
description: 'Start your guest application to move on to the next step.',
- buttonText: 'Start Application',
+ linkText: 'Start Application',
url: '/guest-application',
},
{
@@ -23,7 +23,7 @@ const task: TaskAccordionProps = {
title: 'Coordinator Interview',
status: 'inProgress',
description: 'Meet with your Coordinator to share more about yourself.',
- buttonText: 'Schedule interview',
+ linkText: 'Schedule interview',
url: '/schedule',
},
{
@@ -32,7 +32,7 @@ const task: TaskAccordionProps = {
status: 'locked',
description:
'Complete a training session to prepare you for the host home experience.',
- buttonText: 'Schedule training',
+ linkText: 'Schedule training',
url: '/schedule',
},
],
diff --git a/app/src/components/intake-profile/AdditionaGuestsField.tsx b/app/src/components/intake-profile/AdditionaGuestsField.tsx
new file mode 100644
index 00000000..9251c801
--- /dev/null
+++ b/app/src/components/intake-profile/AdditionaGuestsField.tsx
@@ -0,0 +1,99 @@
+import {faker} from '@faker-js/faker';
+import {Stack, Typography, Button, TextField} from '@mui/material';
+import {FormikErrors, FormikHandlers, FieldArray} from 'formik';
+import {InitialValues} from 'src/views/IntakeProfile';
+import {Guest} from '../../services/profile';
+
+interface AdditionalGuestsFieldProps {
+ errors: FormikErrors;
+ guests: Guest[];
+ fieldId: string;
+ groupId: string;
+ onChange: FormikHandlers['handleChange'];
+}
+
+export const AdditionalGuestsField = ({
+ errors,
+ guests,
+ fieldId,
+ groupId,
+ onChange,
+}: AdditionalGuestsFieldProps) => {
+ return (
+
+
+ {({push, remove}) => (
+ <>
+ {guests.map((guest, index) => {
+ return (
+
+
+ Guest {index + 1}
+
+
+
+
+
+
+ );
+ })}
+
+ >
+ )}
+
+
+ );
+};
diff --git a/app/src/components/intake-profile/IntakeProfileGroups.tsx b/app/src/components/intake-profile/IntakeProfileGroups.tsx
new file mode 100644
index 00000000..da739acb
--- /dev/null
+++ b/app/src/components/intake-profile/IntakeProfileGroups.tsx
@@ -0,0 +1,193 @@
+import {
+ Container,
+ FormControl,
+ FormControlLabel,
+ FormHelperText,
+ MenuItem,
+ Radio,
+ RadioGroup,
+ Select,
+ Stack,
+ TextField,
+ Typography,
+} from '@mui/material';
+import {FormikErrors, useFormikContext, FormikHandlers} from 'formik';
+import {useOutletContext} from 'react-router-dom';
+
+import {Values, InitialValues} from 'src/views/IntakeProfile';
+import {AdditionalGuestsField} from './AdditionaGuestsField';
+import {FieldGroup, Fields, Guest} from 'src/services/profile';
+
+interface OutletContext {
+ groupId: string;
+ fieldGroups: FieldGroup[];
+}
+
+export const FieldGroupList = () => {
+ const {groupId, fieldGroups} = useOutletContext();
+ const {values, handleChange, errors} = useFormikContext();
+
+ if (fieldGroups === undefined || groupId === undefined) return null;
+ const fieldGroup = fieldGroups.find(group => group.id === groupId);
+ const fields = fieldGroup?.fields || [];
+
+ return (
+
+
+ {fieldGroup?.title}
+ {fields.map(field => {
+ return (
+
+ {field.title}
+
+
+ );
+ })}
+
+
+ );
+};
+
+interface RenderFieldProps {
+ groupId: string;
+ field: Fields;
+ values: Values;
+ handleChange: FormikHandlers['handleChange'];
+ errors: FormikErrors;
+}
+
+export const RenderFields = ({
+ groupId,
+ field,
+ values,
+ handleChange,
+ errors,
+}: RenderFieldProps) => {
+ const props = {
+ name: `${groupId}.${field.id}`,
+ value: values[field.id],
+ onChange: handleChange,
+ };
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ const error = Boolean(errors[groupId]?.[field.id]);
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ const helperText = errors[groupId]?.[field.id];
+
+ switch (field.type) {
+ case 'short_text':
+ return (
+
+ );
+ case 'long_text':
+ return (
+
+ );
+ case 'number':
+ return (
+
+ );
+ case 'email':
+ return (
+
+ );
+ case 'yes_no':
+ return (
+
+
+ } label="Yes" />
+ } label="No" />
+
+ {helperText}
+
+ );
+ case 'dropdown':
+ if (field.properties.choices === undefined)
+ throw new Error('Invalid field type');
+
+ return (
+
+
+ {helperText}
+
+ );
+ case 'multiple_choice':
+ return (
+
+ );
+ case 'additional_guests':
+ return (
+
+ );
+ default:
+ throw new Error('Invalid field type');
+ }
+};
diff --git a/app/src/main.tsx b/app/src/main.tsx
index 70f19de8..ae888de2 100644
--- a/app/src/main.tsx
+++ b/app/src/main.tsx
@@ -43,6 +43,7 @@ import {
Language,
About,
Review,
+ IntakeProfile,
} from './views';
import {AccountVerification} from './views/AccountVerification';
@@ -53,6 +54,8 @@ import {
GuestDashboardLayout,
} from './components/layout';
import {GuestApplicationContext} from './components/common/GuestApplicationContext';
+import {FieldGroupList} from './components/intake-profile/IntakeProfileGroups';
+import {enableMocking} from './utils/test/browser';
function HuuApp() {
const [session] = useSessionMutation();
@@ -123,6 +126,16 @@ function HuuApp() {
} />
} />
+
+
+
+ }
+ >
+ } />
+
-
-
-
-
-
-
-
-
-
-
- ,
-);
+enableMocking().then(() => {
+ ReactDOM.createRoot(appRoot).render(
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+});
diff --git a/app/src/services/profile.ts b/app/src/services/profile.ts
new file mode 100644
index 00000000..5394baec
--- /dev/null
+++ b/app/src/services/profile.ts
@@ -0,0 +1,104 @@
+import {api} from './api';
+
+export const fieldTypes = [
+ 'short_text',
+ 'long_text',
+ 'number',
+ 'email',
+ 'dropdown',
+ 'multiple_choice',
+ 'yes_no',
+ 'additional_guests',
+] as const;
+
+type FieldTypeTuple = typeof fieldTypes;
+
+export type FieldTypes = FieldTypeTuple[number];
+
+export interface Choice {
+ id: string;
+ label: string;
+}
+
+export interface ReduiredIf {
+ field_id: string;
+ value: string;
+}
+
+export interface Fields {
+ id: string;
+ title: string;
+ type: FieldTypes;
+ properties: {
+ description?: string;
+ randomize?: boolean;
+ alphabetical_order?: boolean;
+ allow_multiple_selection?: boolean;
+ allow_other_choice?: boolean;
+ choices?: Choice[];
+ };
+ validations: {
+ required?: boolean;
+ max_characters?: number;
+ required_if?: ReduiredIf;
+ };
+}
+
+export interface FieldGroup {
+ id: string;
+ title: string;
+ fields: Fields[];
+}
+
+export interface Guest {
+ id: string;
+ name: string;
+ dob: string;
+ relationship: string;
+}
+
+export interface Response {
+ id: string;
+ fieldId: string;
+ value: string | Guest[] | undefined;
+}
+
+const injectedRtkApi = api.injectEndpoints({
+ endpoints: build => ({
+ getProfile: build.query({
+ query: queryArg => ({
+ url: `/profile/${queryArg.profileId}`,
+ }),
+ }),
+ getResponses: build.query<
+ GetProfileResponsesApiResponse,
+ GetProfileResponsesApiArg
+ >({
+ query: queryArg => ({
+ url: `/profile/responses/${queryArg.userId}`,
+ }),
+ }),
+ }),
+ overrideExisting: false,
+});
+
+export {injectedRtkApi as hostAPI};
+
+export interface GetProfileApiResponse {
+ id: string;
+ fieldGroups: FieldGroup[];
+}
+
+export interface GetProfileApiArg {
+ profileId: string | undefined;
+}
+
+export interface GetProfileResponsesApiResponse {
+ responses: Response[];
+}
+
+export interface GetProfileResponsesApiArg {
+ userId: string | undefined;
+}
+
+export const {useGetProfileQuery, useGetResponsesQuery} = injectedRtkApi;
diff --git a/app/src/utils/test/browser.ts b/app/src/utils/test/browser.ts
new file mode 100644
index 00000000..9999dc0e
--- /dev/null
+++ b/app/src/utils/test/browser.ts
@@ -0,0 +1,32 @@
+import {setupWorker} from 'msw/browser';
+import {handlers as profileHandlers} from './handlers/profile';
+
+export const worker = setupWorker(...profileHandlers);
+
+export const enableMocking = async () => {
+ if (process.env.NODE_ENV !== 'development') {
+ return;
+ }
+
+ // `worker.start()` returns a Promise that resolves
+ // once the Service Worker is up and ready to intercept requests.
+ return worker.start({
+ onUnhandledRequest(req, print) {
+ // Ignore any requests from these URLs.
+ const excludedRoutes = [
+ '/api/auth/user',
+ '/api/auth/session',
+ '/api/auth/refresh',
+ ];
+
+ const isExcluded = excludedRoutes.some(route => req.url.includes(route));
+
+ if (isExcluded || !req.url.includes('/api/')) {
+ return;
+ }
+
+ // Otherwise, print an unhandled request warning.
+ print.warning();
+ },
+ });
+};
diff --git a/app/src/utils/test/db/profile.ts b/app/src/utils/test/db/profile.ts
new file mode 100644
index 00000000..eba76a22
--- /dev/null
+++ b/app/src/utils/test/db/profile.ts
@@ -0,0 +1,469 @@
+import {faker} from '@faker-js/faker';
+import {GetProfileApiResponse} from 'src/services/profile';
+
+export const intakeProfiles: GetProfileApiResponse[] = [
+ {
+ id: '1',
+ fieldGroups: [
+ {
+ id: faker.string.numeric(4),
+ title: 'Basic Information',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title: 'First Name',
+ type: 'short_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Last Name',
+ type: 'short_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Date of Birth',
+ type: 'short_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Gender Identity',
+ type: 'dropdown',
+ properties: {
+ choices: [
+ {id: faker.string.numeric(4), label: 'Woman'},
+ {id: faker.string.numeric(4), label: 'Man'},
+ {id: faker.string.numeric(4), label: 'Questioning'},
+ {id: faker.string.numeric(4), label: 'Transgender'},
+ {id: faker.string.numeric(4), label: 'Non-binary'},
+ {
+ id: faker.string.numeric(4),
+ label: 'Doesn’t know or prefers not to answer ',
+ },
+ ],
+ },
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Contact Information',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title: 'Email',
+ type: 'email',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Phone Number',
+ type: 'number',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Other Guests/Pets',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title: 'Additional Guests',
+ type: 'additional_guests',
+ properties: {},
+ validations: {},
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Pets',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Employment Information',
+ fields: [
+ {
+ id: '5478',
+ title: 'Are you currently employed?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'If yes, please describe your employment.',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required_if: {
+ field_id: '5478',
+ value: 'yes',
+ },
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title:
+ 'If no, are you currently looking for work? If so, what type?',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required_if: {
+ field_id: '5478',
+ value: 'no',
+ },
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Education',
+ fields: [
+ {
+ id: '6478',
+ title: 'Are you enrolled in an Educational Program?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'If yes, please describe the program.',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required_if: {
+ field_id: '6478',
+ value: 'yes',
+ },
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title:
+ 'If no, are you hoping to enroll in an Educational Program? If so, what type?',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required_if: {
+ field_id: '6478',
+ value: 'no',
+ },
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Language Proficiency',
+ fields: [
+ {
+ id: '5479',
+ title: 'Are you bilingual or multilingual?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'If yes, what languages do you speak?',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required_if: {
+ field_id: '5479',
+ value: 'yes',
+ },
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Substance Use',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title: 'Do you smoke cigarettes?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Do you drink alcohol?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Do you use any other substances?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Mental Health',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title: 'Do you suffer mental illness?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Interest in Being a Guest',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title:
+ 'Please share how you think participating in the Host Homes Program will help you obtain long-term housing and meet your educational and/or employment goals:',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title:
+ 'What kind of relationship do you hope to have with your host home?',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title:
+ 'Please describe any challenges you foresee encountering in participating in the Host Homes Program.',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'About You',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title:
+ 'Please take some time to write an introduction of yourself that you would feel comfortable with the Host Homes Coordinator sharing with a potential host. Feel free to talk about your interests, your story or anything else that you think would be important to share:',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: '2',
+ fieldGroups: [
+ {
+ id: faker.string.numeric(4),
+ title: 'Personal Information',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title: 'First Name',
+ type: 'short_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Last Name',
+ type: 'short_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Date of Birth',
+ type: 'short_text',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Contact Information',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title: 'Email',
+ type: 'email',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Phone Number',
+ type: 'number',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Home Information',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title:
+ 'Do you have an extra bedroom or private space in their home?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title:
+ 'Are you able to provide Guest with access to a kitchen in which to prepare meals, store food and access to shared or private bathroom?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title:
+ 'Can you commit to hosting youth Guest for at least 3-6 months?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ ],
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Restrictions',
+ fields: [
+ {
+ id: faker.string.numeric(4),
+ title: 'Do you or anyone in your houshold smoke?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Is smoking allowed inside your home?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'Do you or anyone in your household drink alcohol?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: '9846',
+ title: 'Do you have concerns about your drinking?',
+ type: 'yes_no',
+ properties: {},
+ validations: {
+ required: true,
+ },
+ },
+ {
+ id: faker.string.numeric(4),
+ title: 'If yes, please explain why you are concerned',
+ type: 'long_text',
+ properties: {},
+ validations: {
+ required_if: {
+ field_id: '9846',
+ value: 'yes',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+];
diff --git a/app/src/utils/test/handlers.ts b/app/src/utils/test/handlers/auth.ts
similarity index 84%
rename from app/src/utils/test/handlers.ts
rename to app/src/utils/test/handlers/auth.ts
index 3d705e89..b346adc7 100644
--- a/app/src/utils/test/handlers.ts
+++ b/app/src/utils/test/handlers/auth.ts
@@ -1,6 +1,6 @@
import {http, HttpResponse} from 'msw';
-const handlers = [
+export const handlers = [
http.post('/api/auth/forgot_password', () => {
return new HttpResponse();
}),
@@ -8,5 +8,3 @@ const handlers = [
return new HttpResponse();
}),
];
-
-export {handlers};
diff --git a/app/src/utils/test/handlers/profile.ts b/app/src/utils/test/handlers/profile.ts
new file mode 100644
index 00000000..4c1f05d0
--- /dev/null
+++ b/app/src/utils/test/handlers/profile.ts
@@ -0,0 +1,19 @@
+import {http, HttpResponse} from 'msw';
+import {intakeProfiles} from '../db/profile';
+
+export const handlers = [
+ http.get('/api/profile/:profileId', req => {
+ const id = req.params.profileId;
+ const profile = intakeProfiles.find(p => p.id === id);
+
+ if (profile) {
+ return HttpResponse.json(profile);
+ }
+
+ return new HttpResponse(null, {status: 404});
+ }),
+
+ http.get('/api/profile/responses/:userId', () => {
+ return HttpResponse.json({responses: []});
+ }),
+];
diff --git a/app/src/utils/test/server.ts b/app/src/utils/test/server.ts
index 867a7f68..90d5af70 100644
--- a/app/src/utils/test/server.ts
+++ b/app/src/utils/test/server.ts
@@ -1,7 +1,8 @@
import {setupServer} from 'msw/node';
-import {handlers} from './handlers';
+import {handlers as authHandlers} from './handlers/auth';
+import {handlers as profileHandlers} from './handlers/profile';
-const server = setupServer(...handlers);
+const server = setupServer(...authHandlers, ...profileHandlers);
server.events.on('request:start', ({request}) => {
console.log('Outgoing:', request.method, request.url);
diff --git a/app/src/views/GuestApplicationTracker.tsx b/app/src/views/GuestApplicationTracker.tsx
index f18b6c81..5a8db5ae 100644
--- a/app/src/views/GuestApplicationTracker.tsx
+++ b/app/src/views/GuestApplicationTracker.tsx
@@ -19,7 +19,7 @@ export interface SubTask {
title: string;
status: TaskStatus;
description: string;
- buttonText: string;
+ linkText: string;
url: string;
}
@@ -35,15 +35,15 @@ const tasks: Task[] = [
status: 'inProgress',
description:
'Start your guest application to move on to the next step.',
- buttonText: 'Start Application',
- url: '/guest/application/welcome',
+ linkText: 'Start Application',
+ url: 'profile/1/group/1',
},
{
id: 2,
title: 'Coordinator Interview',
status: 'locked',
description: 'Meet with your Coordinator to share more about yourself.',
- buttonText: 'Schedule interview',
+ linkText: 'Schedule interview',
url: '/schedule',
},
{
@@ -52,7 +52,7 @@ const tasks: Task[] = [
status: 'locked',
description:
'Complete a training session to prepare you for the host home experience.',
- buttonText: 'Schedule training',
+ linkText: 'Schedule training',
url: '/schedule',
},
],
@@ -67,7 +67,7 @@ const tasks: Task[] = [
title: 'Match with a Host',
status: 'locked',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- buttonText: 'Find hosts',
+ linkText: 'Find hosts',
url: '/match',
},
{
@@ -75,7 +75,7 @@ const tasks: Task[] = [
title: 'Meeting with Host',
status: 'locked',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- buttonText: 'Schedule meeting',
+ linkText: 'Schedule meeting',
url: '/schedule',
},
],
@@ -90,7 +90,7 @@ const tasks: Task[] = [
title: 'Sign Agreement',
status: 'locked',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- buttonText: 'Sign agreement',
+ linkText: 'Sign agreement',
url: '/schedule',
},
],
diff --git a/app/src/views/IntakeProfile/constants/index.ts b/app/src/views/IntakeProfile/constants/index.ts
new file mode 100644
index 00000000..2613dcfe
--- /dev/null
+++ b/app/src/views/IntakeProfile/constants/index.ts
@@ -0,0 +1,179 @@
+import {faker} from '@faker-js/faker';
+import {array, object, string} from 'yup';
+import {InitialValues} from '..';
+import {
+ FieldGroup,
+ Fields,
+ fieldTypes,
+ FieldTypes,
+ Response,
+} from '../../../services/profile';
+
+export const fieldGroupBuilder = (
+ options: Partial = {},
+): FieldGroup => ({
+ id: faker.string.numeric(4),
+ title: faker.lorem.sentence({min: 2, max: 4}),
+ fields: [],
+ ...options,
+});
+
+export const fieldBuilder = (options: Partial = {}): Fields => ({
+ id: faker.string.numeric(4),
+ title: faker.lorem.sentence({min: 5, max: 10}),
+ type: faker.helpers.arrayElement(fieldTypes),
+ ...options,
+ properties: {
+ ...options.properties,
+ },
+ validations: {
+ ...options.validations,
+ },
+});
+
+/**
+ * Creates an object used for the initial Formik valiues
+ * It takes the form of:
+ * {
+ * fieldGroupId: {
+ * fieldId: responseValue
+ * }
+ * }
+ */
+const fieldDefaultValue = (fieldType: FieldTypes) => {
+ switch (fieldType) {
+ case 'short_text':
+ return '';
+ case 'long_text':
+ return '';
+ case 'dropdown':
+ return '';
+ case 'number':
+ return '';
+ case 'additional_guests':
+ return [];
+ case 'email':
+ return '';
+ case 'multiple_choice':
+ return '';
+ case 'yes_no':
+ return '';
+ default:
+ return '';
+ }
+};
+
+export const createInitialValues = (
+ fieldGroups: FieldGroup[],
+ responses: Response[],
+): InitialValues => {
+ return fieldGroups.reduce((acc: InitialValues, fieldGroup) => {
+ const fields = fieldGroup.fields.reduce((acc, field) => {
+ return {
+ ...acc,
+ [field.id]:
+ responses.find(response => response.fieldId === field.id)?.value ||
+ fieldDefaultValue(field.type),
+ };
+ }, {});
+
+ return {
+ ...acc,
+ [fieldGroup.id]: {...fields},
+ };
+ }, {});
+};
+
+/**
+ * Creates a validation schema for Formik based on field type
+ * It takes the form of:
+ * {
+ * fieldGroupId: {
+ * fieldId: validationSchema
+ * }
+ * }
+ */
+
+const phoneRegExp =
+ /^((\\+[1-9]{1,4}[ \\-]*)|(\\([0-9]{2,3}\\)[ \\-]*)|([0-9]{2,4})[ \\-]*)*?[0-9]{3,4}?[ \\-]*[0-9]{3,4}?$/;
+
+export const typeValidations = {
+ short_text: string(),
+ long_text: string(),
+ number: string().matches(phoneRegExp, 'phone number is not valid'),
+ email: string().email('Must be a valid email address'),
+ yes_no: string(),
+ dropdown: string(),
+ multiple_choice: string(),
+ additional_guests: array().of(
+ object().shape({
+ name: string().required('Name is required'),
+ dob: string().required('DOB is required'),
+ relationship: string().required('Relationship is required'),
+ }),
+ ),
+};
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+const merge = (...schemas) => {
+ const [first, ...rest] = schemas;
+
+ const merged = rest.reduce(
+ (mergedSchemas, schema) => mergedSchemas.concat(schema),
+ first,
+ );
+
+ return merged;
+};
+
+const createFieldValidationSchema = ({type, validations}: Fields) => {
+ if (!typeValidations[type]) {
+ console.error(`Invalid type: ${type}`);
+ }
+
+ let schema = typeValidations[type];
+
+ if (validations.required) {
+ // only works for string types at the moment
+ schema = merge(schema, string().required('This field is required'));
+ }
+
+ if (validations.required_if) {
+ const {field_id, value} = validations.required_if;
+ // only works for string types at the moment
+ const requiedIfSchema = string().when(field_id, {
+ is: value,
+ then: schema => schema.required('This field is required'),
+ otherwise: schema => schema,
+ });
+ schema = merge(schema, requiedIfSchema);
+ }
+
+ return schema;
+};
+
+export const buildValidationSchema = (
+ fieldGroup: FieldGroup[],
+ groupId: string | undefined,
+) => {
+ if (groupId === undefined) {
+ console.error('Invalid groupId');
+ return object().shape({});
+ }
+
+ const fields = fieldGroup.find(group => group.id === groupId)?.fields || [];
+
+ const schema = object().shape(
+ fields.reduce((acc, field) => {
+ return {
+ ...acc,
+ [field.id]: createFieldValidationSchema(field),
+ };
+ }, {}),
+ );
+
+ return object().shape({
+ [groupId]: object().shape({...schema.fields}),
+ });
+};
diff --git a/app/src/views/IntakeProfile/hooks/useFieldGroups.ts b/app/src/views/IntakeProfile/hooks/useFieldGroups.ts
new file mode 100644
index 00000000..542761c2
--- /dev/null
+++ b/app/src/views/IntakeProfile/hooks/useFieldGroups.ts
@@ -0,0 +1,117 @@
+import {faker} from '@faker-js/faker';
+import {useEffect, useState} from 'react';
+import {fieldBuilder, fieldGroupBuilder} from '../constants';
+import {Response, FieldGroup} from 'src/services/profile';
+
+interface UseFieldGroups {
+ profileId: string;
+}
+
+/**
+ * Generates field groups and responses for a given profile id
+ */
+
+export const useFieldGroups = ({profileId}: UseFieldGroups) => {
+ const [responses, setResponses] = useState([]);
+ const [fieldGroups, setFieldGroups] = useState([]);
+
+ useEffect(() => {
+ if (profileId) {
+ const fieldGroups = Array.from(Array(3), () => {
+ const fields = Array.from(Array(3), () => {
+ const field = fieldBuilder();
+
+ if (field.type === 'multiple_choice' || field.type === 'dropdown') {
+ field.properties.choices = [
+ {
+ id: faker.string.uuid(),
+ label: 'choice 1',
+ },
+ {
+ id: faker.string.uuid(),
+ label: 'choice 2',
+ },
+ {
+ id: faker.string.uuid(),
+ label: 'choice 3',
+ },
+ ];
+
+ field.validations.required = true;
+ }
+
+ return field;
+ });
+ return fieldGroupBuilder({fields});
+ });
+
+ setFieldGroups(fieldGroups);
+
+ const responsesArr = fieldGroups
+ .map(fieldGroup => fieldGroup.fields)
+ .flat()
+ .map(field => {
+ let value;
+ if (field.type === 'yes_no') {
+ //yes or no
+ value = faker.helpers.arrayElement(['unanswered', 'yes', 'no']);
+ }
+
+ if (field.type === 'short_text') {
+ //short text
+ value = faker.lorem.sentence();
+ }
+
+ if (field.type === 'long_text') {
+ //long text
+ value = faker.lorem.paragraph();
+ }
+
+ if (field.type === 'number') {
+ //number
+ value = faker.phone.number();
+ }
+
+ if (field.type === 'email') {
+ //email
+ value = faker.internet.email();
+ }
+
+ if (field.type === 'dropdown' || field.type === 'multiple_choice') {
+ //dropdown or multiple choice
+ value = faker.helpers.arrayElement(
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ field.properties.choices.map(choice => choice.label),
+ );
+ }
+
+ if (field.type === 'additional_guests') {
+ value = [
+ {
+ id: faker.string.uuid(),
+ name: faker.person.fullName(),
+ dob: faker.date.birthdate().toString(),
+ relationship: faker.helpers.arrayElement([
+ 'mom',
+ 'dad',
+ 'sibling',
+ 'friend',
+ 'partner',
+ ]),
+ },
+ ];
+ }
+
+ return {
+ id: faker.string.uuid(),
+ fieldId: field.id,
+ value,
+ };
+ });
+ setResponses(responsesArr);
+ }
+ }, [profileId]);
+
+ return {responses, fieldGroups};
+};
diff --git a/app/src/views/IntakeProfile/index.tsx b/app/src/views/IntakeProfile/index.tsx
new file mode 100644
index 00000000..c5c1f6ab
--- /dev/null
+++ b/app/src/views/IntakeProfile/index.tsx
@@ -0,0 +1,121 @@
+import {Button, Stack, useTheme} from '@mui/material';
+import {Link, Outlet, useParams} from 'react-router-dom';
+import {Formik} from 'formik';
+
+import {buildValidationSchema, createInitialValues} from './constants';
+import {
+ useGetProfileQuery,
+ useGetResponsesQuery,
+ Response,
+} from '../../services/profile';
+
+export type Values = {
+ [key: string]: Response['value'];
+};
+
+export type InitialValues = Record;
+
+export const IntakeProfile = () => {
+ const theme = useTheme();
+ const toolbarHeight = Number(theme.mixins.toolbar.minHeight);
+ const {profileId, groupId} = useParams();
+
+ const {data: profileData} = useGetProfileQuery(
+ {profileId: profileId},
+ {skip: !profileId},
+ );
+ const {data: responsesData} = useGetResponsesQuery({userId: '1'});
+
+ if (
+ profileId === undefined ||
+ groupId === undefined ||
+ profileData === undefined ||
+ responsesData === undefined
+ )
+ return null;
+
+ const {fieldGroups} = profileData;
+ const {responses} = responsesData;
+
+ const validationSchema = buildValidationSchema(fieldGroups, groupId);
+ const initalValues = createInitialValues(fieldGroups, responses);
+
+ return (
+ {
+ const updateResponses = Object.entries(values[groupId]).map(
+ ([fieldId, value]) => {
+ const response = responses.find(
+ response => response.fieldId === fieldId,
+ );
+ if (response) {
+ response.value = value;
+ return response;
+ } else {
+ return {
+ fieldId,
+ value,
+ };
+ }
+ },
+ );
+
+ window.alert(JSON.stringify(updateResponses, null, 2));
+ }}
+ >
+ {({errors, handleSubmit}) => (
+
+
+ {fieldGroups.map(({id, title}) => {
+ const fieldTitle = title || '...';
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/app/src/views/__tests__/ForgotPasswordCode.test.tsx b/app/src/views/__tests__/ForgotPasswordCode.test.tsx
index 20cacdff..c1174d46 100644
--- a/app/src/views/__tests__/ForgotPasswordCode.test.tsx
+++ b/app/src/views/__tests__/ForgotPasswordCode.test.tsx
@@ -103,7 +103,7 @@ describe('ForgotPasswordCode page', () => {
test('display an error message', async () => {
server.use(
- http.post(`/api/auth/forgot_password`, async () => {
+ http.post('/api/auth/forgot_password', async () => {
await delay();
return HttpResponse.json(
{
diff --git a/app/src/views/index.ts b/app/src/views/index.ts
index d545652e..40a9df5c 100644
--- a/app/src/views/index.ts
+++ b/app/src/views/index.ts
@@ -33,3 +33,4 @@ export {Sections} from './guestApplicationForm/Sections';
export {SubstanceUse} from './guestApplicationForm/SubstanceUse';
export {Welcome} from './guestApplicationForm/Welcome';
export {About} from './guestApplicationForm/About';
+export {IntakeProfile} from './IntakeProfile';