@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,25 @@
1
+ import * as React from "react"
2
+ import { Icon } from "../../icons/Icon"
3
+
4
+ export interface ListingDetailHeaderProps {
5
+ imageAlt: string
6
+ imageSrc: string
7
+ subtitle: string
8
+ title: string
9
+ children?: React.ReactNode
10
+ hideHeader?: boolean
11
+ desktopClass?: string
12
+ }
13
+
14
+ const ListingDetailHeader = (props: ListingDetailHeaderProps) => (
15
+ <header className={props.hideHeader ? "detail-header md:hidden" : "detail-header"}>
16
+ <img alt={props.imageAlt} className="detail-header__image " src={props.imageSrc} />
17
+ <hgroup className="detail-header__hgroup">
18
+ <h2 className="detail-header__title">{props.title}</h2>
19
+ <span className="detail-header__subtitle">{props.subtitle}</span>
20
+ <Icon symbol="arrowDown" size="medium" />
21
+ </hgroup>
22
+ </header>
23
+ )
24
+
25
+ export { ListingDetailHeader as default, ListingDetailHeader }
@@ -0,0 +1,29 @@
1
+ import * as React from "react"
2
+ import {
3
+ ResponsiveContentList,
4
+ ResponsiveContentItem,
5
+ ResponsiveContentItemHeader,
6
+ ResponsiveContentItemBody,
7
+ } from "../../sections/ResponsiveContentList"
8
+ import { ListingDetailHeader, ListingDetailHeaderProps } from "./ListingDetailHeader"
9
+
10
+ export const ListingDetails = (props: any) => (
11
+ <div className="w-full md:w-2/3 md:pr-8 md:pt-8">
12
+ <ResponsiveContentList>{props.children}</ResponsiveContentList>
13
+ </div>
14
+ )
15
+
16
+ export const ListingDetailItem = (props: ListingDetailHeaderProps) => (
17
+ <ResponsiveContentItem desktopClass={props.desktopClass}>
18
+ <ResponsiveContentItemHeader>
19
+ <ListingDetailHeader
20
+ title={props.title}
21
+ subtitle={props.subtitle}
22
+ imageSrc={props.imageSrc}
23
+ imageAlt={props.imageAlt}
24
+ hideHeader={props.hideHeader}
25
+ />
26
+ </ResponsiveContentItemHeader>
27
+ <ResponsiveContentItemBody>{props.children}</ResponsiveContentItemBody>
28
+ </ResponsiveContentItem>
29
+ )
@@ -0,0 +1,36 @@
1
+ .pin {
2
+ @apply absolute;
3
+ width: 30px;
4
+ height: 30px;
5
+ border-radius: 50% 50% 50% 0;
6
+ background: #89849b;
7
+ transform: rotate(-45deg);
8
+ left: 50%;
9
+ top: 50%;
10
+ margin: -20px 0 0 -20px;
11
+ animation-name: bounce;
12
+ animation-fill-mode: both;
13
+ animation-duration: 1s;
14
+
15
+ &:after {
16
+ @apply absolute;
17
+ @apply rounded-full;
18
+ content: "";
19
+ width: 14px;
20
+ height: 14px;
21
+ margin: 8px 0 0 8px;
22
+ background: #2f2f2f;
23
+ }
24
+ }
25
+
26
+ .addressPopup {
27
+ @apply bg-white;
28
+ @apply p-4;
29
+ @apply shadow-md;
30
+ @apply inline-block;
31
+ @apply float-left;
32
+ @apply relative;
33
+ left: 15px;
34
+ top: 15px;
35
+ z-index: 1;
36
+ }
@@ -0,0 +1,138 @@
1
+ import React, { useState, useCallback, useEffect } from "react"
2
+ import "mapbox-gl/dist/mapbox-gl.css"
3
+ import MapGL, { Marker } from "react-map-gl"
4
+
5
+ import "./ListingMap.scss"
6
+ import { MultiLineAddress, Address } from "../../helpers/address"
7
+
8
+ export interface ListingMapProps {
9
+ address?: Address
10
+ listingName?: string
11
+ enableCustomPinPositioning?: boolean
12
+ setCustomMapPositionChosen?: (customMapPosition: boolean) => void
13
+ setLatLong?: (latLong: LatitudeLongitude) => void
14
+ }
15
+
16
+ export interface LatitudeLongitude {
17
+ latitude: number
18
+ longitude: number
19
+ }
20
+
21
+ export interface Viewport {
22
+ width: string | number
23
+ height: string | number
24
+ latitude?: number
25
+ longitude?: number
26
+ zoom: number
27
+ }
28
+
29
+ const isValidLatitude = (latitude: number) => {
30
+ return latitude >= -90 && latitude <= 90
31
+ }
32
+
33
+ const isValidLongitude = (longitude: number) => {
34
+ return longitude >= -180 && longitude <= 180
35
+ }
36
+
37
+ const ListingMap = (props: ListingMapProps) => {
38
+ const [marker, setMarker] = useState({
39
+ latitude: props.address?.latitude,
40
+ longitude: props.address?.longitude,
41
+ })
42
+
43
+ const [viewport, setViewport] = useState({
44
+ latitude: marker.latitude,
45
+ longitude: marker.longitude,
46
+ width: "100%",
47
+ height: 400,
48
+ zoom: 13,
49
+ } as Viewport)
50
+
51
+ const onViewportChange = (viewport: Viewport) => {
52
+ // width and height need to be set here to work properly with
53
+ // the responsive wrappers
54
+ const newViewport = { ...viewport }
55
+ newViewport.width = "100%"
56
+ newViewport.height = 400
57
+ setViewport(newViewport)
58
+ }
59
+
60
+ useEffect(() => {
61
+ onViewportChange({
62
+ ...viewport,
63
+ latitude: props.address?.latitude,
64
+ longitude: props.address?.longitude,
65
+ })
66
+ setMarker({
67
+ latitude: props.address?.latitude,
68
+ longitude: props.address?.longitude,
69
+ })
70
+ }, [props.address?.latitude, props.address?.longitude, props.enableCustomPinPositioning])
71
+
72
+ const onMarkerDragEnd = useCallback((event) => {
73
+ if (props.setLatLong) {
74
+ props.setLatLong({
75
+ latitude: event.lngLat[1],
76
+ longitude: event.lngLat[0],
77
+ })
78
+ }
79
+ if (props.setCustomMapPositionChosen) {
80
+ props.setCustomMapPositionChosen(true)
81
+ }
82
+ setMarker({
83
+ latitude: event.lngLat[1],
84
+ longitude: event.lngLat[0],
85
+ })
86
+ }, [])
87
+
88
+ if (
89
+ !props.address ||
90
+ !props.address.latitude ||
91
+ !props.address.longitude ||
92
+ !viewport.latitude ||
93
+ !viewport.longitude
94
+ )
95
+ return null
96
+
97
+ return (
98
+ <div className="listing-map">
99
+ <div className="addressPopup">
100
+ {props.listingName && <h3 className="text-caps-tiny">{props.listingName}</h3>}
101
+ <MultiLineAddress address={props.address} />
102
+ </div>
103
+ {(process.env.mapBoxToken || process.env.MAPBOX_TOKEN) && (
104
+ <MapGL
105
+ mapboxApiAccessToken={process.env.mapBoxToken || process.env.MAPBOX_TOKEN}
106
+ mapStyle="mapbox://styles/mapbox/streets-v11"
107
+ scrollZoom={false}
108
+ onViewportChange={onViewportChange}
109
+ {...viewport}
110
+ >
111
+ {marker.latitude &&
112
+ marker.longitude &&
113
+ isValidLatitude(marker.latitude) &&
114
+ isValidLongitude(marker.longitude) && (
115
+ <>
116
+ {props.enableCustomPinPositioning ? (
117
+ <Marker
118
+ latitude={marker.latitude}
119
+ longitude={marker.longitude}
120
+ offsetTop={-20}
121
+ draggable={true}
122
+ onDragEnd={onMarkerDragEnd}
123
+ >
124
+ <div className="pin"></div>
125
+ </Marker>
126
+ ) : (
127
+ <Marker latitude={marker.latitude} longitude={marker.longitude} offsetTop={-20}>
128
+ <div className="pin"></div>
129
+ </Marker>
130
+ )}
131
+ </>
132
+ )}
133
+ </MapGL>
134
+ )}
135
+ </div>
136
+ )
137
+ }
138
+ export { ListingMap as default, ListingMap }
@@ -0,0 +1,65 @@
1
+ .listings-group {
2
+ @apply border-t;
3
+ @apply border-gray-450;
4
+ @apply mb-5;
5
+ }
6
+
7
+ .listings-group__header {
8
+ @apply flex;
9
+ @apply flex-row;
10
+ @apply flex-wrap;
11
+ @apply max-w-5xl;
12
+ @apply m-auto;
13
+ @apply mt-6;
14
+ @apply mb-8;
15
+ @apply p-3;
16
+ }
17
+
18
+ .listings-group__icon {
19
+ @apply hidden;
20
+ @apply pt-2;
21
+ @apply pr-5;
22
+
23
+ @screen md {
24
+ @apply inline-block;
25
+ }
26
+ }
27
+
28
+ .listings-group__header-group {
29
+ @apply w-full;
30
+ @apply flex;
31
+ @apply items-center;
32
+ @apply mb-4;
33
+
34
+ @screen md {
35
+ @apply w-7/12;
36
+ @apply mb-0;
37
+ }
38
+ }
39
+
40
+ .listings-group__button {
41
+ @apply w-full;
42
+ @apply flex;
43
+ @apply items-center;
44
+
45
+ @screen md {
46
+ @apply w-4/12;
47
+ }
48
+ }
49
+
50
+ .listings-group__title {
51
+ @apply uppercase;
52
+ @apply font-alt-sans;
53
+ @apply font-black;
54
+ @apply my-1;
55
+ @apply tracking-widest;
56
+ @apply border-b-4;
57
+ @apply border-gray-600;
58
+ @apply pb-1;
59
+
60
+ @screen md {
61
+ @apply px-4;
62
+ @apply border-b-0;
63
+ @apply border-l-4;
64
+ }
65
+ }
@@ -0,0 +1,49 @@
1
+ import React, { useState } from "react"
2
+ import { Button } from "../../actions/Button"
3
+ import { Icon } from "../../icons/Icon"
4
+ import "./ListingsGroup.scss"
5
+
6
+ export interface ListingsGroupProps {
7
+ children?: React.ReactNode
8
+ listingsCount: number
9
+ header: string
10
+ info?: string
11
+ showButtonText: string
12
+ hideButtonText: string
13
+ }
14
+
15
+ const ListingsGroup = (props: ListingsGroupProps) => {
16
+ const [showListings, setShowListings] = useState(false)
17
+ const toggleListings = () => setShowListings(!showListings)
18
+
19
+ let buttonText
20
+
21
+ const listingsCount = ` (${props.listingsCount})`
22
+ if (showListings) {
23
+ buttonText = props.hideButtonText + listingsCount
24
+ } else {
25
+ buttonText = props.showButtonText + listingsCount
26
+ }
27
+
28
+ return (
29
+ <div className="listings-group">
30
+ <div className="listings-group__header">
31
+ <div className="listings-group__icon">
32
+ <Icon size="xlarge" symbol="clock" />
33
+ </div>
34
+ <div className="listings-group__header-group">
35
+ <h2 className="listings-group__title">{props.header}</h2>
36
+ {props.info && <div className="px-4 my-2">{props.info}</div>}
37
+ </div>
38
+ <div className="listings-group__button">
39
+ <Button className="w-full" onClick={() => toggleListings()}>
40
+ {buttonText}
41
+ </Button>
42
+ </div>
43
+ </div>
44
+ {showListings && props.children}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export { ListingsGroup as default, ListingsGroup }
@@ -0,0 +1,111 @@
1
+ import * as React from "react"
2
+ import { nanoid } from "nanoid"
3
+ import { MinMax, UnitSummary, Unit } from "@bloom-housing/backend-core/types"
4
+
5
+ import { StandardTable } from "../../tables/StandardTable"
6
+ import { t } from "../../helpers/translator"
7
+ import { numberOrdinal } from "../../helpers/numberOrdinal"
8
+
9
+ const formatRange = (range: MinMax, ordinalize?: boolean) => {
10
+ let min: string | number = range.min
11
+ let max: string | number = range.max
12
+
13
+ if (ordinalize) {
14
+ min = numberOrdinal(min)
15
+ max = numberOrdinal(max)
16
+ }
17
+
18
+ if (min == max) {
19
+ return min
20
+ } else {
21
+ return `${min} - ${max}`
22
+ }
23
+ }
24
+
25
+ const unitsLabel = (units: Unit[]): string => {
26
+ const label = units.length > 1 ? t("t.units") : t("t.unit")
27
+ return `${units.length} ${label}`
28
+ }
29
+
30
+ interface UnitTablesProps {
31
+ units: Unit[]
32
+ unitSummaries: UnitSummary[]
33
+ disableAccordion?: boolean
34
+ }
35
+
36
+ const UnitTables = (props: UnitTablesProps) => {
37
+ const unitSummaries = props.unitSummaries || []
38
+
39
+ const unitsHeaders = {
40
+ number: "t.unit",
41
+ sqFeet: "t.area",
42
+ numBathrooms: "listings.bath",
43
+ floor: "t.floor",
44
+ }
45
+
46
+ const toggleTable = (event: React.MouseEvent) => {
47
+ if (!props.disableAccordion) {
48
+ event.currentTarget.parentElement?.querySelector(".unit-table")?.classList?.toggle("hidden")
49
+ }
50
+ }
51
+
52
+ const buttonClasses = ["w-full", "text-left"]
53
+ if (props.disableAccordion) buttonClasses.push("cursor-default")
54
+
55
+ return (
56
+ <>
57
+ {unitSummaries.map((unitSummary: UnitSummary) => {
58
+ const uniqKey = process.env.NODE_ENV === "test" ? "" : nanoid()
59
+ const units = props.units.filter(
60
+ (unit: Unit) => unit.unitType?.name == unitSummary.unitType.name
61
+ )
62
+ const unitsFormatted = [] as Array<Record<string, React.ReactNode>>
63
+ let floorSection
64
+ units.forEach((unit: Unit) => {
65
+ unitsFormatted.push({
66
+ number: unit.number,
67
+ sqFeet: (
68
+ <>
69
+ <strong>{unit.sqFeet}</strong> {t("t.sqFeet")}
70
+ </>
71
+ ),
72
+ numBathrooms: <strong>{unit.numBathrooms}</strong>,
73
+ floor: <strong>{unit.floor}</strong>,
74
+ })
75
+ })
76
+
77
+ let areaRangeSection
78
+ if (unitSummary.areaRange?.min || unitSummary.areaRange?.max) {
79
+ areaRangeSection = `, ${formatRange(unitSummary.areaRange)} ${t("t.squareFeet")}`
80
+ }
81
+
82
+ if (unitSummary.floorRange && unitSummary.floorRange.min) {
83
+ floorSection = `, ${formatRange(unitSummary.floorRange, true)}
84
+ ${
85
+ unitSummary.floorRange.max > unitSummary.floorRange.min
86
+ ? t("t.floors")
87
+ : t("t.floor")
88
+ }`
89
+ }
90
+
91
+ return (
92
+ <div key={uniqKey} className="mb-4">
93
+ <button onClick={toggleTable} className={buttonClasses.join(" ")}>
94
+ <h3 className="toggle-header">
95
+ <strong>{t("listings.unitTypes." + unitSummary.unitType.name)}</strong>:&nbsp;
96
+ {unitsLabel(units)}
97
+ {areaRangeSection}
98
+ {floorSection}
99
+ </h3>
100
+ </button>
101
+ <div className="unit-table hidden">
102
+ <StandardTable headers={unitsHeaders} data={unitsFormatted} />
103
+ </div>
104
+ </div>
105
+ )
106
+ })}
107
+ </>
108
+ )
109
+ }
110
+
111
+ export { UnitTables as default, UnitTables }