@bloom-housing/ui-components 2.0.0-pre-tailwind

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.jest/setup-tests.js +24 -0
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +195 -0
  4. package/index.ts +148 -0
  5. package/jest.config.js +41 -0
  6. package/package.json +98 -0
  7. package/public/images/alameda-logo-white.svg +1 -0
  8. package/public/images/arrow-down.png +0 -0
  9. package/public/images/arrow-down.svg +1 -0
  10. package/public/images/check.png +0 -0
  11. package/public/images/check.svg +11 -0
  12. package/public/images/eho-logo-white.svg +1 -0
  13. package/public/images/eho-logo.svg +1 -0
  14. package/public/images/logo_glyph.svg +11 -0
  15. package/src/actions/Button.scss +157 -0
  16. package/src/actions/Button.tsx +80 -0
  17. package/src/actions/ExpandableContent.tsx +29 -0
  18. package/src/actions/ExpandableText.scss +18 -0
  19. package/src/actions/ExpandableText.tsx +52 -0
  20. package/src/actions/LinkButton.tsx +30 -0
  21. package/src/actions/LocalizedLink.tsx +11 -0
  22. package/src/authentication/AuthContext.ts +327 -0
  23. package/src/authentication/RequireLogin.tsx +62 -0
  24. package/src/authentication/index.ts +5 -0
  25. package/src/authentication/timeout.tsx +127 -0
  26. package/src/authentication/token.ts +17 -0
  27. package/src/authentication/useRequireLoggedInUser.ts +19 -0
  28. package/src/blocks/ActionBlock.scss +108 -0
  29. package/src/blocks/ActionBlock.tsx +51 -0
  30. package/src/blocks/AppStatusItem.scss +140 -0
  31. package/src/blocks/AppStatusItem.tsx +75 -0
  32. package/src/blocks/DashBlock.tsx +42 -0
  33. package/src/blocks/DashBlocks.scss +56 -0
  34. package/src/blocks/DashBlocks.tsx +7 -0
  35. package/src/blocks/FormCard.scss +201 -0
  36. package/src/blocks/FormCard.tsx +29 -0
  37. package/src/blocks/HousingCounselor.tsx +51 -0
  38. package/src/blocks/ImageCard.scss +91 -0
  39. package/src/blocks/ImageCard.tsx +77 -0
  40. package/src/blocks/InfoCard.scss +42 -0
  41. package/src/blocks/InfoCard.tsx +44 -0
  42. package/src/blocks/StatusBar.scss +30 -0
  43. package/src/blocks/StatusBar.tsx +31 -0
  44. package/src/blocks/ViewItem.scss +59 -0
  45. package/src/blocks/ViewItem.tsx +32 -0
  46. package/src/config/ConfigContext.tsx +36 -0
  47. package/src/config/NavigationContext.tsx +54 -0
  48. package/src/config/index.ts +2 -0
  49. package/src/footers/ExygyFooter.tsx +12 -0
  50. package/src/footers/SiteFooter.scss +28 -0
  51. package/src/footers/SiteFooter.tsx +10 -0
  52. package/src/forms/CloudinaryUpload.ts +50 -0
  53. package/src/forms/DOBField.tsx +132 -0
  54. package/src/forms/DateField.tsx +120 -0
  55. package/src/forms/Dropzone.scss +17 -0
  56. package/src/forms/Dropzone.tsx +67 -0
  57. package/src/forms/Field.tsx +115 -0
  58. package/src/forms/FieldGroup.tsx +82 -0
  59. package/src/forms/Form.tsx +22 -0
  60. package/src/forms/HouseholdMemberForm.tsx +41 -0
  61. package/src/forms/HouseholdSizeField.tsx +74 -0
  62. package/src/forms/PhoneField.tsx +69 -0
  63. package/src/forms/PhoneMask.tsx +24 -0
  64. package/src/forms/Select.tsx +80 -0
  65. package/src/forms/Textarea.scss +40 -0
  66. package/src/forms/Textarea.tsx +64 -0
  67. package/src/forms/TimeField.tsx +176 -0
  68. package/src/global/AppearanceTypes.ts +46 -0
  69. package/src/global/ApplicationStatusType.ts +6 -0
  70. package/src/global/accordion.scss +4 -0
  71. package/src/global/blocks.scss +137 -0
  72. package/src/global/custom_counter.scss +50 -0
  73. package/src/global/forms.scss +362 -0
  74. package/src/global/headers.scss +89 -0
  75. package/src/global/homepage.scss +8 -0
  76. package/src/global/index.scss +72 -0
  77. package/src/global/lists.scss +21 -0
  78. package/src/global/markdown.scss +33 -0
  79. package/src/global/mixins.scss +175 -0
  80. package/src/global/navbar.scss +280 -0
  81. package/src/global/print.scss +59 -0
  82. package/src/global/tables.scss +197 -0
  83. package/src/global/text.scss +141 -0
  84. package/src/global/vendor/AgPagination.tsx +133 -0
  85. package/src/global/vendor/_setup_bulma.scss +31 -0
  86. package/src/global/vendor/ag_grid.scss +140 -0
  87. package/src/headers/Hero.scss +56 -0
  88. package/src/headers/Hero.tsx +76 -0
  89. package/src/headers/PageHeader.scss +31 -0
  90. package/src/headers/PageHeader.tsx +39 -0
  91. package/src/headers/SiteHeader.tsx +136 -0
  92. package/src/helpers/address.tsx +46 -0
  93. package/src/helpers/blankApplication.ts +108 -0
  94. package/src/helpers/capitalize.tsx +7 -0
  95. package/src/helpers/dateToString.ts +11 -0
  96. package/src/helpers/debounce.ts +12 -0
  97. package/src/helpers/formOptions.tsx +229 -0
  98. package/src/helpers/formatYesNoLabel.ts +9 -0
  99. package/src/helpers/getTranslationWithArguments.ts +14 -0
  100. package/src/helpers/links.ts +7 -0
  101. package/src/helpers/localeRoute.tsx +13 -0
  102. package/src/helpers/mergeDeep.ts +12 -0
  103. package/src/helpers/nextjs.ts +7 -0
  104. package/src/helpers/numberOrdinal.ts +17 -0
  105. package/src/helpers/occupancyFormatting.tsx +46 -0
  106. package/src/helpers/pdfs.ts +19 -0
  107. package/src/helpers/photos.ts +19 -0
  108. package/src/helpers/preferences.tsx +426 -0
  109. package/src/helpers/resolveObject.ts +5 -0
  110. package/src/helpers/state.tsx +7 -0
  111. package/src/helpers/tableSummaries.tsx +80 -0
  112. package/src/helpers/translator.tsx +37 -0
  113. package/src/helpers/useKeyPress.ts +17 -0
  114. package/src/helpers/useMutate.ts +40 -0
  115. package/src/helpers/useOutsideClick.ts +25 -0
  116. package/src/helpers/validators.ts +3 -0
  117. package/src/icons/HeaderBadge.scss +29 -0
  118. package/src/icons/HeaderBadge.tsx +38 -0
  119. package/src/icons/Icon.scss +76 -0
  120. package/src/icons/Icon.tsx +145 -0
  121. package/src/icons/Icons.tsx +556 -0
  122. package/src/lists/PreferencesList.scss +72 -0
  123. package/src/lists/PreferencesList.tsx +60 -0
  124. package/src/locales/es.json +745 -0
  125. package/src/locales/general.json +1307 -0
  126. package/src/locales/general_OLD.json +868 -0
  127. package/src/locales/vi.json +745 -0
  128. package/src/locales/zh.json +745 -0
  129. package/src/navigation/Breadcrumbs.scss +25 -0
  130. package/src/navigation/Breadcrumbs.tsx +27 -0
  131. package/src/navigation/FooterNav.scss +47 -0
  132. package/src/navigation/FooterNav.tsx +19 -0
  133. package/src/navigation/LanguageNav.scss +32 -0
  134. package/src/navigation/LanguageNav.tsx +53 -0
  135. package/src/navigation/ProgressNav.scss +102 -0
  136. package/src/navigation/ProgressNav.tsx +50 -0
  137. package/src/navigation/TabNav.scss +38 -0
  138. package/src/navigation/TabNav.tsx +69 -0
  139. package/src/navigation/Tabs.scss +65 -0
  140. package/src/navigation/Tabs.tsx +93 -0
  141. package/src/navigation/UserNav.tsx +37 -0
  142. package/src/notifications/AlertBox.scss +78 -0
  143. package/src/notifications/AlertBox.tsx +79 -0
  144. package/src/notifications/AlertNotice.scss +58 -0
  145. package/src/notifications/AlertNotice.tsx +37 -0
  146. package/src/notifications/ApplicationStatus.scss +10 -0
  147. package/src/notifications/ApplicationStatus.tsx +64 -0
  148. package/src/notifications/ErrorMessage.tsx +15 -0
  149. package/src/notifications/SiteAlert.tsx +54 -0
  150. package/src/notifications/StatusAside.scss +11 -0
  151. package/src/notifications/StatusAside.tsx +25 -0
  152. package/src/notifications/StatusMessage.scss +25 -0
  153. package/src/notifications/StatusMessage.tsx +59 -0
  154. package/src/notifications/alertTypes.ts +7 -0
  155. package/src/notifications/index.ts +4 -0
  156. package/src/overlays/Drawer.scss +105 -0
  157. package/src/overlays/Drawer.tsx +51 -0
  158. package/src/overlays/LoadingOverlay.scss +25 -0
  159. package/src/overlays/LoadingOverlay.tsx +29 -0
  160. package/src/overlays/Modal.scss +55 -0
  161. package/src/overlays/Modal.tsx +61 -0
  162. package/src/overlays/Overlay.scss +50 -0
  163. package/src/overlays/Overlay.tsx +100 -0
  164. package/src/page_components/listing/AdditionalFees.tsx +56 -0
  165. package/src/page_components/listing/ListingCard.scss +47 -0
  166. package/src/page_components/listing/ListingCard.tsx +34 -0
  167. package/src/page_components/listing/ListingDetailHeader.tsx +25 -0
  168. package/src/page_components/listing/ListingDetails.tsx +29 -0
  169. package/src/page_components/listing/ListingMap.scss +36 -0
  170. package/src/page_components/listing/ListingMap.tsx +138 -0
  171. package/src/page_components/listing/ListingsGroup.scss +65 -0
  172. package/src/page_components/listing/ListingsGroup.tsx +49 -0
  173. package/src/page_components/listing/UnitTables.tsx +111 -0
  174. package/src/page_components/listing/listing_sidebar/ApplicationSection.tsx +49 -0
  175. package/src/page_components/listing/listing_sidebar/Apply.tsx +225 -0
  176. package/src/page_components/listing/listing_sidebar/LeasingAgent.tsx +77 -0
  177. package/src/page_components/listing/listing_sidebar/ListingUpdated.tsx +20 -0
  178. package/src/page_components/listing/listing_sidebar/ReferralApplication.tsx +28 -0
  179. package/src/page_components/listing/listing_sidebar/SidebarAddress.tsx +56 -0
  180. package/src/page_components/listing/listing_sidebar/Waitlist.tsx +94 -0
  181. package/src/page_components/listing/listing_sidebar/WhatToExpect.tsx +22 -0
  182. package/src/page_components/listing/listing_sidebar/events/DownloadLotteryResults.tsx +34 -0
  183. package/src/page_components/listing/listing_sidebar/events/EventDateSection.tsx +24 -0
  184. package/src/page_components/listing/listing_sidebar/events/LotteryResultsEvent.tsx +26 -0
  185. package/src/page_components/listing/listing_sidebar/events/OpenHouseEvent.tsx +27 -0
  186. package/src/page_components/listing/listing_sidebar/events/PublicLotteryEvent.tsx +22 -0
  187. package/src/prototypes/AppCard.scss +64 -0
  188. package/src/prototypes/Back.scss +19 -0
  189. package/src/prototypes/ButtonGroup.scss +6 -0
  190. package/src/prototypes/ButtonPager.scss +22 -0
  191. package/src/prototypes/FieldSection.scss +35 -0
  192. package/src/prototypes/FieldSection.tsx +31 -0
  193. package/src/prototypes/GridItem.tsx +15 -0
  194. package/src/prototypes/SideNav.scss +32 -0
  195. package/src/prototypes/SideNav.tsx +14 -0
  196. package/src/prototypes/SummaryCard.scss +34 -0
  197. package/src/sections/ContentSection.scss +15 -0
  198. package/src/sections/ContentSection.tsx +25 -0
  199. package/src/sections/FooterSection.scss +6 -0
  200. package/src/sections/FooterSection.tsx +16 -0
  201. package/src/sections/GridSection.scss +72 -0
  202. package/src/sections/GridSection.tsx +82 -0
  203. package/src/sections/InfoCardGrid.scss +45 -0
  204. package/src/sections/InfoCardGrid.tsx +20 -0
  205. package/src/sections/ListSection.scss +7 -0
  206. package/src/sections/ListSection.tsx +23 -0
  207. package/src/sections/MarkdownSection.scss +13 -0
  208. package/src/sections/MarkdownSection.tsx +21 -0
  209. package/src/sections/ResponsiveContentList.tsx +67 -0
  210. package/src/sections/ResponsiveWrappers.tsx +23 -0
  211. package/src/tables/GroupedTable.tsx +86 -0
  212. package/src/tables/MinimalTable.tsx +32 -0
  213. package/src/tables/ResponsiveTable.tsx +24 -0
  214. package/src/tables/StandardTable.tsx +229 -0
  215. package/src/text/Description.scss +52 -0
  216. package/src/text/Description.tsx +24 -0
  217. package/src/text/Message.scss +16 -0
  218. package/src/text/Message.tsx +16 -0
  219. package/src/text/Tag.scss +94 -0
  220. package/src/text/Tag.tsx +22 -0
  221. package/tailwind.config.js +128 -0
  222. package/tailwind.tosass.js +29 -0
  223. package/tsconfig.json +31 -0
@@ -0,0 +1,7 @@
1
+ export const isExternalLink = (href: string) => {
2
+ return href.startsWith("http://") || href.startsWith("https://")
3
+ }
4
+
5
+ export const isInternalLink = (href: string) => {
6
+ return href.startsWith("/") && !href.startsWith("//")
7
+ }
@@ -0,0 +1,13 @@
1
+ import { t } from "./translator"
2
+
3
+ export const lRoute = (routeString: string) => {
4
+ if (routeString.startsWith("http")) return routeString
5
+
6
+ let routePrefix = t("config.routePrefix")
7
+ if (routePrefix == "config.routePrefix" || routePrefix == "") {
8
+ routePrefix = "" // no prefix needed for default routes
9
+ } else {
10
+ routePrefix = "/" + routePrefix
11
+ }
12
+ return `${routePrefix}${routeString}`
13
+ }
@@ -0,0 +1,12 @@
1
+ export const mergeDeep = (target: any, source: any) => {
2
+ // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
3
+ for (const key of Object.keys(source)) {
4
+ if (source[key] instanceof Object) {
5
+ Object.assign(source[key], mergeDeep(target[key], source[key]))
6
+ }
7
+ }
8
+
9
+ // Join `target` and modified `source`
10
+ Object.assign(target || {}, source)
11
+ return target
12
+ }
@@ -0,0 +1,7 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ export const OnClientSide = () => {
4
+ const [mounted, setMounted] = useState(false)
5
+ useEffect(() => setMounted(true), [])
6
+ return mounted
7
+ }
@@ -0,0 +1,17 @@
1
+ export const numberOrdinal = (num: number): string => {
2
+ const standardSuffix = "th"
3
+ const oneToThreeSuffixes = ["st", "nd", "rd"]
4
+
5
+ const numStr = num.toString()
6
+ const lastTwoDigits = parseInt(numStr.slice(-2), 10)
7
+ const lastDigit = parseInt(numStr.slice(-1), 10)
8
+
9
+ let suffix = ""
10
+ if (lastDigit >= 1 && lastDigit <= 3 && !(lastTwoDigits >= 11 && lastTwoDigits <= 13)) {
11
+ suffix = oneToThreeSuffixes[lastDigit - 1]
12
+ } else {
13
+ suffix = standardSuffix
14
+ }
15
+
16
+ return `${num}${suffix}`
17
+ }
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+ import { t } from "./translator"
3
+ import { Listing } from "@bloom-housing/backend-core/types"
4
+
5
+ export const occupancyTable = (listing: Listing) => {
6
+ let occupancyData = [] as any
7
+ if (listing.unitsSummarized && listing.unitsSummarized.byUnitType) {
8
+ occupancyData = listing.unitsSummarized.byUnitType.map((unitSummary) => {
9
+ let occupancy = ""
10
+
11
+ if (unitSummary.occupancyRange.max == null) {
12
+ occupancy = `at least ${unitSummary.occupancyRange.min} ${
13
+ unitSummary.occupancyRange.min == 1 ? t("t.person") : t("t.people")
14
+ }`
15
+ } else if (unitSummary.occupancyRange.max > 1) {
16
+ occupancy = `${unitSummary.occupancyRange.min}-${unitSummary.occupancyRange.max} ${
17
+ unitSummary.occupancyRange.max == 1 ? t("t.person") : t("t.people")
18
+ }`
19
+ } else {
20
+ occupancy = `1 ${t("t.person")}`
21
+ }
22
+
23
+ return {
24
+ unitType: <strong>{t("listings.unitTypes." + unitSummary.unitType.name)}</strong>,
25
+ occupancy: occupancy,
26
+ }
27
+ })
28
+ }
29
+
30
+ return occupancyData
31
+ }
32
+
33
+ export const getOccupancyDescription = (listing: Listing) => {
34
+ const unitsSummarized = listing.unitsSummarized
35
+ if (
36
+ unitsSummarized &&
37
+ unitsSummarized.unitTypes &&
38
+ unitsSummarized.unitTypes.map((unitType) => unitType.name).includes("SRO")
39
+ ) {
40
+ return unitsSummarized.unitTypes.length == 1
41
+ ? t("listings.occupancyDescriptionAllSro")
42
+ : t("listings.occupancyDescriptionSomeSro")
43
+ } else {
44
+ return t("listings.occupancyDescriptionNoSro")
45
+ }
46
+ }
@@ -0,0 +1,19 @@
1
+ import { ListingEvent, ListingEventType } from "@bloom-housing/backend-core/types"
2
+
3
+ export const cloudinaryPdfFromId = (publicId: string, cloudName: string) => {
4
+ return `https://res.cloudinary.com/${cloudName}/image/upload/${publicId}.pdf`
5
+ }
6
+
7
+ export const pdfUrlFromListingEvents = (
8
+ events: ListingEvent[],
9
+ listingType: ListingEventType,
10
+ cloudName: string
11
+ ) => {
12
+ const event = events.find((event) => event.type === listingType)
13
+ if (event) {
14
+ return event.file?.label == "cloudinaryPDF"
15
+ ? cloudinaryPdfFromId(event.file.fileId, cloudName)
16
+ : event.url
17
+ }
18
+ return null
19
+ }
@@ -0,0 +1,19 @@
1
+ import { Asset, Listing } from "@bloom-housing/backend-core/types"
2
+
3
+ export const cloudinaryUrlFromId = (publicId: string, size = 400) => {
4
+ const cloudName = process.env.cloudinaryCloudName || process.env.CLOUDINARY_CLOUD_NAME
5
+ return `https://res.cloudinary.com/${cloudName}/image/upload/w_${size},c_limit,q_65/${publicId}.jpg`
6
+ }
7
+
8
+ export const imageUrlFromListing = (listing: Listing, size = 400) => {
9
+ // Use the new `image` field
10
+ const imageAssets = listing?.image ? [listing.image] : listing?.assets
11
+
12
+ // Fallback to `assets`
13
+ const cloudinaryBuilding = imageAssets?.find(
14
+ (asset: Asset) => asset.label == "cloudinaryBuilding"
15
+ )?.fileId
16
+ if (cloudinaryBuilding) return cloudinaryUrlFromId(cloudinaryBuilding, size)
17
+
18
+ return imageAssets?.find((asset: Asset) => asset.label == "building")?.fileId
19
+ }
@@ -0,0 +1,426 @@
1
+ import React from "react"
2
+ import {
3
+ InputType,
4
+ ApplicationPreference,
5
+ FormMetadataOptions,
6
+ Preference,
7
+ } from "@bloom-housing/backend-core/types"
8
+ import { UseFormMethods } from "react-hook-form"
9
+ import {
10
+ t,
11
+ GridSection,
12
+ ViewItem,
13
+ GridCell,
14
+ Field,
15
+ Select,
16
+ SelectOption,
17
+ resolveObject,
18
+ } from "@bloom-housing/ui-components"
19
+ import { stateKeys } from "./formOptions"
20
+
21
+ type ExtraFieldProps = {
22
+ metaKey: string
23
+ optionKey: string
24
+ extraKey: string
25
+ type: InputType
26
+ register: UseFormMethods["register"]
27
+ errors?: UseFormMethods["errors"]
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ hhMembersOptions?: SelectOption[]
30
+ }
31
+
32
+ type FormAddressProps = {
33
+ subtitle: string
34
+ dataKey: string
35
+ type: AddressType
36
+ register: UseFormMethods["register"]
37
+ errors?: UseFormMethods["errors"]
38
+ required?: boolean
39
+ }
40
+
41
+ type AddressType =
42
+ | "residence"
43
+ | "residence-member"
44
+ | "work"
45
+ | "mailing"
46
+ | "alternate"
47
+ | "preference"
48
+
49
+ /*
50
+ Path to the preferences from listing object
51
+ */
52
+ export const PREFERENCES_FORM_PATH = "application.preferences.options"
53
+ export const PREFERENCES_NONE_FORM_PATH = "application.preferences.none"
54
+ /*
55
+ It generates inner fields for preferences form
56
+ */
57
+ export const ExtraField = ({
58
+ metaKey,
59
+ optionKey,
60
+ extraKey,
61
+ type,
62
+ register,
63
+ errors,
64
+ hhMembersOptions,
65
+ }: ExtraFieldProps) => {
66
+ const FIELD_NAME = `${PREFERENCES_FORM_PATH}.${metaKey}.${optionKey}.${extraKey}`
67
+
68
+ return (
69
+ <div className="my-4" key={FIELD_NAME}>
70
+ {(() => {
71
+ if (type === InputType.text) {
72
+ return (
73
+ <Field
74
+ id={FIELD_NAME}
75
+ name={FIELD_NAME}
76
+ type="text"
77
+ label={t(`application.preferences.options.${extraKey}`)}
78
+ register={register}
79
+ validation={{ required: true }}
80
+ error={!!resolveObject(FIELD_NAME, errors)}
81
+ errorMessage={t("errors.requiredFieldError")}
82
+ />
83
+ )
84
+ } else if (type === InputType.address) {
85
+ return (
86
+ <div className="pb-4">
87
+ <FormAddress
88
+ subtitle={t("application.preferences.options.address")}
89
+ dataKey={FIELD_NAME}
90
+ type="preference"
91
+ register={register}
92
+ errors={errors}
93
+ required={true}
94
+ />
95
+ </div>
96
+ )
97
+ } else if (type === InputType.hhMemberSelect) {
98
+ if (!hhMembersOptions)
99
+ return (
100
+ <Field
101
+ id={FIELD_NAME}
102
+ name={FIELD_NAME}
103
+ type="text"
104
+ label={t(`application.preferences.options.${extraKey}`)}
105
+ register={register}
106
+ validation={{ required: true }}
107
+ error={!!resolveObject(FIELD_NAME, errors)}
108
+ errorMessage={t("errors.requiredFieldError")}
109
+ />
110
+ )
111
+
112
+ return (
113
+ <>
114
+ <Select
115
+ id={FIELD_NAME}
116
+ name={FIELD_NAME}
117
+ label={t(`application.preferences.options.${extraKey}`)}
118
+ register={register}
119
+ controlClassName="control"
120
+ placeholder={t("t.selectOne")}
121
+ options={hhMembersOptions}
122
+ validation={{ required: true }}
123
+ error={!!resolveObject(FIELD_NAME, errors)}
124
+ errorMessage={t("errors.requiredFieldError")}
125
+ />
126
+ </>
127
+ )
128
+ }
129
+
130
+ return <></>
131
+ })()}
132
+ </div>
133
+ )
134
+ }
135
+
136
+ export const FormAddress = ({
137
+ subtitle,
138
+ dataKey,
139
+ type,
140
+ register,
141
+ errors,
142
+ required,
143
+ }: FormAddressProps) => {
144
+ return (
145
+ <>
146
+ <GridSection subtitle={subtitle}>
147
+ <GridCell span={2}>
148
+ <ViewItem label={t("application.contact.streetAddress")}>
149
+ <Field
150
+ id={`${dataKey}.street`}
151
+ name={`${dataKey}.street`}
152
+ label={t("application.contact.streetAddress")}
153
+ placeholder={t("application.contact.streetAddress")}
154
+ register={register}
155
+ validation={{ required }}
156
+ error={!!resolveObject(`${dataKey}.street`, errors)}
157
+ errorMessage={t("errors.streetError")}
158
+ readerOnly
159
+ />
160
+ </ViewItem>
161
+ </GridCell>
162
+ <GridCell>
163
+ <ViewItem label={t("application.contact.apt")}>
164
+ <Field
165
+ id={`${dataKey}.street2`}
166
+ name={`${dataKey}.street2`}
167
+ label={t("application.contact.apt")}
168
+ placeholder={t("application.contact.apt")}
169
+ register={register}
170
+ readerOnly
171
+ />
172
+ </ViewItem>
173
+ </GridCell>
174
+
175
+ <GridCell>
176
+ <ViewItem label={t("application.contact.city")}>
177
+ <Field
178
+ id={`${dataKey}.city`}
179
+ name={`${dataKey}.city`}
180
+ label={t("application.contact.cityName")}
181
+ placeholder={t("application.contact.cityName")}
182
+ register={register}
183
+ validation={{ required }}
184
+ error={!!resolveObject(`${dataKey}.city`, errors)}
185
+ errorMessage={t("errors.cityError")}
186
+ readerOnly
187
+ />
188
+ </ViewItem>
189
+ </GridCell>
190
+
191
+ <GridCell className="md:grid md:grid-cols-2 md:gap-8" span={2}>
192
+ <ViewItem label={t("application.contact.state")} className="mb-0">
193
+ <Select
194
+ id={`${dataKey}.state`}
195
+ name={`${dataKey}.state`}
196
+ label={t("application.contact.state")}
197
+ labelClassName="sr-only"
198
+ register={register}
199
+ controlClassName="control"
200
+ options={stateKeys}
201
+ keyPrefix="states"
202
+ validation={{ required }}
203
+ error={!!resolveObject(`${dataKey}.state`, errors)}
204
+ errorMessage={t("errors.stateError")}
205
+ />
206
+ </ViewItem>
207
+
208
+ <ViewItem label={t("application.contact.zip")}>
209
+ <Field
210
+ id={`${dataKey}.zipCode`}
211
+ name={`${dataKey}.zipCode`}
212
+ label={t("application.contact.zip")}
213
+ placeholder={t("application.contact.zipCode")}
214
+ register={register}
215
+ validation={{ required }}
216
+ error={!!resolveObject(`${dataKey}.zipCode`, errors)}
217
+ errorMessage={t("errors.zipCodeError")}
218
+ readerOnly
219
+ />
220
+ </ViewItem>
221
+ </GridCell>
222
+
223
+ {type === "residence" && (
224
+ <GridCell span={2}>
225
+ <Field
226
+ id="application.sendMailToMailingAddress"
227
+ name="application.sendMailToMailingAddress"
228
+ type="checkbox"
229
+ label={t("application.contact.sendMailToMailingAddress")}
230
+ register={register}
231
+ />
232
+ </GridCell>
233
+ )}
234
+ </GridSection>
235
+ </>
236
+ )
237
+ }
238
+
239
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
+ export const mapPreferencesToApi = (data: Record<string, any>) => {
241
+ if (!data.application?.preferences) return []
242
+
243
+ const CLAIMED_KEY = "claimed"
244
+
245
+ const preferencesFormData = data.application.preferences.options
246
+
247
+ const keys = Object.keys(preferencesFormData)
248
+
249
+ return keys.map((key) => {
250
+ const currentPreference = preferencesFormData[key]
251
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
+ const currentPreferenceValues = Object.values(currentPreference) as Record<string, any>
253
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
254
+ const claimed = currentPreferenceValues.map((c: { claimed: any }) => c.claimed).includes(true)
255
+
256
+ const options = Object.keys(currentPreference).map((option) => {
257
+ const currentOption = currentPreference[option]
258
+
259
+ // count keys except claimed
260
+ const extraKeys = Object.keys(currentOption).filter((item) => item !== CLAIMED_KEY)
261
+
262
+ const response = {
263
+ key: option,
264
+ checked: currentOption[CLAIMED_KEY],
265
+ }
266
+
267
+ // assign extra data and detect data type
268
+ if (extraKeys.length) {
269
+ const extraData = extraKeys.map((extraKey) => {
270
+ const type = (() => {
271
+ if (typeof currentOption[extraKey] === "boolean") return InputType.boolean
272
+ // if object includes "city" property, it should be an address
273
+ if (Object.keys(currentOption[extraKey]).includes("city")) return InputType.address
274
+
275
+ return InputType.text
276
+ })()
277
+
278
+ return {
279
+ key: extraKey,
280
+ type,
281
+ value: currentOption[extraKey],
282
+ }
283
+ })
284
+
285
+ Object.assign(response, { extraData })
286
+ } else {
287
+ Object.assign(response, { extraData: [] })
288
+ }
289
+
290
+ return response
291
+ })
292
+
293
+ return {
294
+ key,
295
+ claimed,
296
+ options,
297
+ }
298
+ })
299
+ }
300
+
301
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
+ export const mapApiToPreferencesForm = (preferences: ApplicationPreference[]) => {
303
+ const preferencesFormData = {}
304
+
305
+ preferences.forEach((item) => {
306
+ const options = item.options.reduce((acc, curr) => {
307
+ // extraData which comes from the API is an array, in the form we expect an object
308
+ const extraData =
309
+ curr?.extraData?.reduce((extraAcc, extraCurr) => {
310
+ // value - it can be string or nested address object
311
+ const value = extraCurr.value
312
+ Object.assign(extraAcc, {
313
+ [extraCurr.key]: value,
314
+ })
315
+
316
+ return extraAcc
317
+ }, {}) || {}
318
+
319
+ // each form option has "claimed" property - it's "checked" property in the API
320
+ const claimed = curr.checked
321
+
322
+ Object.assign(acc, {
323
+ [curr.key]: {
324
+ claimed,
325
+ ...extraData,
326
+ },
327
+ })
328
+ return acc
329
+ }, {})
330
+
331
+ Object.assign(preferencesFormData, {
332
+ [item.key]: options,
333
+ })
334
+ })
335
+
336
+ const noneValues = preferences.reduce((acc, item) => {
337
+ const isClaimed = item.claimed
338
+
339
+ Object.assign(acc, {
340
+ [`${item.key}-none`]: !isClaimed,
341
+ })
342
+
343
+ return acc
344
+ }, {})
345
+
346
+ return { options: preferencesFormData, none: noneValues }
347
+ }
348
+
349
+ /*
350
+ It generates checkbox name in proper prefrences structure
351
+ */
352
+ export const getPreferenceOptionName = (key: string, metaKey: string, noneOption?: boolean) => {
353
+ if (noneOption) return getExclusivePreferenceOptionName(key)
354
+ else return getNormalPreferenceOptionName(metaKey, key)
355
+ }
356
+
357
+ export const getNormalPreferenceOptionName = (metaKey: string, key: string) => {
358
+ return `${PREFERENCES_FORM_PATH}.${metaKey}.${key}.claimed`
359
+ }
360
+
361
+ export const getExclusivePreferenceOptionName = (key: string | undefined) => {
362
+ return `${PREFERENCES_NONE_FORM_PATH}.${key}-none`
363
+ }
364
+
365
+ export type ExclusiveKey = {
366
+ optionKey: string
367
+ preferenceKey: string | undefined
368
+ }
369
+ /*
370
+ Create an array of all exclusive keys from a preference set
371
+ */
372
+ export const getExclusiveKeys = (preferences: Preference[]) => {
373
+ const exclusive: ExclusiveKey[] = []
374
+ preferences?.forEach((preference) => {
375
+ preference?.formMetadata?.options.forEach((option: FormMetadataOptions) => {
376
+ if (option.exclusive)
377
+ exclusive.push({
378
+ optionKey: getPreferenceOptionName(option.key, preference?.formMetadata?.key ?? ""),
379
+ preferenceKey: preference?.formMetadata?.key,
380
+ })
381
+ })
382
+ if (!preference?.formMetadata?.hideGenericDecline)
383
+ exclusive.push({
384
+ optionKey: getExclusivePreferenceOptionName(preference?.formMetadata?.key),
385
+ preferenceKey: preference?.formMetadata?.key,
386
+ })
387
+ })
388
+ return exclusive
389
+ }
390
+
391
+ const uncheckPreference = (
392
+ metaKey: string,
393
+ options: FormMetadataOptions[] | undefined,
394
+ setValue: (key: string, value: boolean) => void
395
+ ) => {
396
+ options?.forEach((option) => {
397
+ setValue(getPreferenceOptionName(option.key, metaKey), false)
398
+ })
399
+ }
400
+
401
+ /*
402
+ Set the value of an exclusive checkbox, unchecking all the appropriate boxes in response to the value
403
+ */
404
+ export const setExclusive = (
405
+ value: boolean,
406
+ setValue: (key: string, value: boolean) => void,
407
+ exclusiveKeys: ExclusiveKey[],
408
+ key: string,
409
+ preference: Preference
410
+ ) => {
411
+ if (value) {
412
+ // Uncheck all other keys if setting an exclusive key to true
413
+ uncheckPreference(
414
+ preference?.formMetadata?.key ?? "",
415
+ preference?.formMetadata?.options,
416
+ setValue
417
+ )
418
+ setValue(key ?? "", true)
419
+ } else {
420
+ // Uncheck all exclusive keys if setting a normal key to true
421
+ exclusiveKeys.forEach((thisKey) => {
422
+ if (thisKey.preferenceKey === preference?.formMetadata?.key)
423
+ setValue(thisKey.optionKey, false)
424
+ })
425
+ }
426
+ }