@bloom-housing/ui-components 4.0.3 → 4.1.0
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.
- package/.jest/setup-tests.js +8 -0
- package/CHANGELOG.md +534 -37
- package/README.md +1 -1
- package/index.ts +8 -4
- package/package.json +4 -4
- package/src/authentication/AuthContext.ts +32 -3
- package/src/blocks/FormCard.scss +12 -0
- package/src/blocks/ImageCard.scss +10 -8
- package/src/blocks/ViewItem.tsx +5 -1
- package/src/config/NavigationContext.tsx +4 -0
- package/src/forms/DOBField.tsx +1 -1
- package/src/forms/Field.tsx +4 -2
- package/src/forms/FieldGroup.tsx +27 -14
- package/src/global/headers.scss +7 -3
- package/src/global/lists.scss +4 -5
- package/src/global/tables.scss +3 -1
- package/src/headers/PageHeader.tsx +5 -1
- package/src/helpers/tableSummaries.tsx +1 -1
- package/src/helpers/useIntersect.ts +48 -0
- package/src/icons/Icon.tsx +6 -1
- package/src/icons/Icons.tsx +1 -1
- package/src/locales/es.json +1 -1
- package/src/locales/general.json +37 -5
- package/src/locales/vi.json +1 -1
- package/src/locales/zh.json +1 -1
- package/src/notifications/AlertBox.scss +3 -3
- package/src/notifications/AlertBox.tsx +3 -1
- package/src/notifications/AlertNotice.tsx +6 -1
- package/src/notifications/ApplicationStatus.scss +2 -7
- package/src/notifications/ApplicationStatus.tsx +10 -13
- package/src/overlays/Modal.tsx +2 -0
- package/src/overlays/Overlay.scss +8 -0
- package/src/overlays/Overlay.tsx +2 -1
- package/src/page_components/forgot-password/FormForgotPassword.tsx +114 -0
- package/src/page_components/listing/ContentAccordion.scss +34 -0
- package/src/page_components/listing/ContentAccordion.tsx +77 -0
- package/src/page_components/listing/ListingMap.scss +4 -0
- package/src/page_components/listing/ListingMap.tsx +13 -3
- package/src/page_components/listing/UnitTables.tsx +37 -27
- package/src/page_components/listing/listing_sidebar/events/DownloadLotteryResults.tsx +21 -22
- package/src/page_components/listing/listing_sidebar/events/EventSection.tsx +54 -0
- package/src/page_components/sign-in/FormSignIn.tsx +9 -33
- package/src/page_components/sign-in/FormSignInAddPhone.tsx +87 -0
- package/src/page_components/sign-in/FormSignInErrorBox.tsx +43 -0
- package/src/page_components/sign-in/FormSignInMFACode.tsx +98 -0
- package/src/page_components/sign-in/FormSignInMFAType.tsx +95 -0
- package/src/tables/StackedTable.tsx +1 -1
- package/src/page_components/listing/listing_sidebar/events/EventDateSection.tsx +0 -25
- package/src/page_components/listing/listing_sidebar/events/LotteryResultsEvent.tsx +0 -26
- package/src/page_components/listing/listing_sidebar/events/OpenHouseEvent.tsx +0 -27
- package/src/page_components/listing/listing_sidebar/events/PublicLotteryEvent.tsx +0 -22
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
.alert-box {
|
|
2
2
|
@apply relative;
|
|
3
|
-
@apply
|
|
3
|
+
@apply py-3;
|
|
4
|
+
@apply px-4;
|
|
4
5
|
@apply leading-snug;
|
|
5
6
|
@apply flex;
|
|
6
7
|
@apply items-center;
|
|
7
|
-
max-height: 50px;
|
|
8
8
|
|
|
9
9
|
.alert-box_inner {
|
|
10
10
|
@apply m-auto;
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
|
|
80
80
|
.alert-box__close {
|
|
81
81
|
@apply text-3xl;
|
|
82
|
-
line-height: 1rem;
|
|
83
82
|
right: 1rem;
|
|
84
83
|
@apply ml-3;
|
|
85
84
|
@apply p-0;
|
|
85
|
+
line-height: 1rem;
|
|
86
86
|
}
|
|
@@ -52,6 +52,7 @@ const AlertBox = (props: AlertBoxProps) => {
|
|
|
52
52
|
size="medium"
|
|
53
53
|
symbol={icons[props.type || "alert"]}
|
|
54
54
|
fill={props.inverted ? IconFillColors.white : undefined}
|
|
55
|
+
ariaHidden={true}
|
|
55
56
|
/>
|
|
56
57
|
</span>
|
|
57
58
|
<span className="alert-box__body">
|
|
@@ -63,8 +64,9 @@ const AlertBox = (props: AlertBoxProps) => {
|
|
|
63
64
|
<button
|
|
64
65
|
className={`alert-box__close ${props.inverted ? "text-white" : ""}`}
|
|
65
66
|
onClick={onClose}
|
|
67
|
+
aria-label="close alert"
|
|
66
68
|
>
|
|
67
|
-
|
|
69
|
+
<span aria-hidden={true}>×</span>
|
|
68
70
|
</button>
|
|
69
71
|
)}
|
|
70
72
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
+
import Markdown from "markdown-to-jsx"
|
|
2
3
|
import type { ReactNode } from "react"
|
|
3
4
|
import type { AlertTypes } from "./alertTypes"
|
|
4
5
|
import { colorClasses } from "./alertTypes"
|
|
@@ -30,7 +31,11 @@ const AlertNotice = (props: AlertNoticeProps) => {
|
|
|
30
31
|
)}
|
|
31
32
|
|
|
32
33
|
<div className="alert-notice__body">
|
|
33
|
-
{typeof props.children === "string" ?
|
|
34
|
+
{typeof props.children === "string" ? (
|
|
35
|
+
<Markdown>{props.children}</Markdown>
|
|
36
|
+
) : (
|
|
37
|
+
props.children
|
|
38
|
+
)}
|
|
34
39
|
</div>
|
|
35
40
|
</div>
|
|
36
41
|
)
|
|
@@ -27,12 +27,7 @@ const ApplicationStatus = (props: ApplicationStatusProps) => {
|
|
|
27
27
|
let icon
|
|
28
28
|
|
|
29
29
|
if (withIcon) {
|
|
30
|
-
icon =
|
|
31
|
-
<span>
|
|
32
|
-
<Icon size="medium" symbol={iconType} fill={vivid ? IconFillColors.white : undefined} />{" "}
|
|
33
|
-
|
|
34
|
-
</span>
|
|
35
|
-
)
|
|
30
|
+
icon = <Icon size="medium" symbol={iconType} fill={vivid ? IconFillColors.white : undefined} />
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
switch (status) {
|
|
@@ -57,13 +52,15 @@ const ApplicationStatus = (props: ApplicationStatusProps) => {
|
|
|
57
52
|
return (
|
|
58
53
|
<div className={`application-status ${textSize} ${textColor} ${bgColor}`}>
|
|
59
54
|
{icon}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
<span>
|
|
56
|
+
{content}
|
|
57
|
+
{props.subContent && (
|
|
58
|
+
<>
|
|
59
|
+
<br />
|
|
60
|
+
{props.subContent}
|
|
61
|
+
</>
|
|
62
|
+
)}
|
|
63
|
+
</span>
|
|
67
64
|
</div>
|
|
68
65
|
)
|
|
69
66
|
}
|
package/src/overlays/Modal.tsx
CHANGED
|
@@ -8,6 +8,7 @@ export interface ModalProps extends Omit<OverlayProps, "children"> {
|
|
|
8
8
|
actions?: React.ReactNode[]
|
|
9
9
|
hideCloseIcon?: boolean
|
|
10
10
|
children?: React.ReactNode
|
|
11
|
+
slim?: boolean
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const ModalHeader = (props: { title: string }) => (
|
|
@@ -34,6 +35,7 @@ export const Modal = (props: ModalProps) => {
|
|
|
34
35
|
open={props.open}
|
|
35
36
|
onClose={props.onClose}
|
|
36
37
|
backdrop={props.backdrop}
|
|
38
|
+
slim={props.slim}
|
|
37
39
|
>
|
|
38
40
|
<div className="modal">
|
|
39
41
|
<ModalHeader title={props.title} />
|
package/src/overlays/Overlay.tsx
CHANGED
|
@@ -14,6 +14,7 @@ export type OverlayProps = {
|
|
|
14
14
|
backdrop?: boolean
|
|
15
15
|
onClose?: () => void
|
|
16
16
|
children: React.ReactNode
|
|
17
|
+
slim?: boolean
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const OverlayInner = (props: OverlayProps) => {
|
|
@@ -37,7 +38,7 @@ const OverlayInner = (props: OverlayProps) => {
|
|
|
37
38
|
if (e.target === e.currentTarget) closeHandler()
|
|
38
39
|
}}
|
|
39
40
|
>
|
|
40
|
-
<div className="fixed-overlay__inner">
|
|
41
|
+
<div className={`fixed-overlay__inner ${props.slim ? "fixed-overlay__inner-slim" : ""}`}>
|
|
41
42
|
<FocusLock>{props.children}</FocusLock>
|
|
42
43
|
</div>
|
|
43
44
|
</div>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React, { useContext } from "react"
|
|
2
|
+
import {
|
|
3
|
+
AppearanceStyleType,
|
|
4
|
+
Button,
|
|
5
|
+
Field,
|
|
6
|
+
Form,
|
|
7
|
+
FormCard,
|
|
8
|
+
Icon,
|
|
9
|
+
t,
|
|
10
|
+
AlertBox,
|
|
11
|
+
SiteAlert,
|
|
12
|
+
AlertNotice,
|
|
13
|
+
ErrorMessage,
|
|
14
|
+
emailRegex,
|
|
15
|
+
} from "@bloom-housing/ui-components"
|
|
16
|
+
import { NavigationContext } from "../../config/NavigationContext"
|
|
17
|
+
import type { UseFormMethods } from "react-hook-form"
|
|
18
|
+
|
|
19
|
+
export type FormForgotPasswordProps = {
|
|
20
|
+
control: FormForgotPasswordControl
|
|
21
|
+
onSubmit: (data: FormForgotPasswordValues) => void
|
|
22
|
+
networkError: FormForgotPasswordNetworkError
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type NetworkErrorReset = () => void
|
|
26
|
+
|
|
27
|
+
export type NetworkErrorValue = {
|
|
28
|
+
title: string
|
|
29
|
+
content: string
|
|
30
|
+
} | null
|
|
31
|
+
|
|
32
|
+
export type FormForgotPasswordNetworkError = {
|
|
33
|
+
error: NetworkErrorValue
|
|
34
|
+
reset: NetworkErrorReset
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type FormForgotPasswordControl = {
|
|
38
|
+
errors: UseFormMethods["errors"]
|
|
39
|
+
handleSubmit: UseFormMethods["handleSubmit"]
|
|
40
|
+
register: UseFormMethods["register"]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type FormForgotPasswordValues = {
|
|
44
|
+
email: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const FormForgotPassword = ({
|
|
48
|
+
onSubmit,
|
|
49
|
+
networkError,
|
|
50
|
+
control: { errors, register, handleSubmit },
|
|
51
|
+
}: FormForgotPasswordProps) => {
|
|
52
|
+
const onError = () => {
|
|
53
|
+
window.scrollTo(0, 0)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { router } = useContext(NavigationContext)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<FormCard>
|
|
60
|
+
<div className="form-card__lead text-center border-b mx-0">
|
|
61
|
+
<Icon size="2xl" symbol="profile" />
|
|
62
|
+
<h2 className="form-card__title">{t("authentication.forgotPassword.sendEmail")}</h2>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{Object.entries(errors).length > 0 && !networkError.error && (
|
|
66
|
+
<AlertBox type="alert" inverted closeable>
|
|
67
|
+
{errors.authentication ? errors.authentication.message : t("errors.errorsToResolve")}
|
|
68
|
+
</AlertBox>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{!!networkError.error && Object.entries(errors).length === 0 && (
|
|
72
|
+
<ErrorMessage id={"householdsize-error"} error={!!networkError.error}>
|
|
73
|
+
<AlertBox type="alert" inverted onClose={() => networkError.reset()}>
|
|
74
|
+
{networkError.error.title}
|
|
75
|
+
</AlertBox>
|
|
76
|
+
|
|
77
|
+
<AlertNotice title="" type="alert" inverted>
|
|
78
|
+
{networkError.error.content}
|
|
79
|
+
</AlertNotice>
|
|
80
|
+
</ErrorMessage>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
<SiteAlert type="notice" dismissable />
|
|
84
|
+
|
|
85
|
+
<div className="form-card__group pt-0 border-b">
|
|
86
|
+
<Form id="sign-in" className="mt-10" onSubmit={handleSubmit(onSubmit, onError)}>
|
|
87
|
+
<Field
|
|
88
|
+
caps={true}
|
|
89
|
+
name="email"
|
|
90
|
+
label={t("t.email")}
|
|
91
|
+
validation={{ required: true, pattern: emailRegex }}
|
|
92
|
+
error={errors.email}
|
|
93
|
+
errorMessage={errors.email ? t("authentication.signIn.loginError") : undefined}
|
|
94
|
+
register={register}
|
|
95
|
+
/>
|
|
96
|
+
<section className="bg-gray-300">
|
|
97
|
+
<div className="text-center mt-6">
|
|
98
|
+
<Button styleType={AppearanceStyleType.primary}>
|
|
99
|
+
{t("authentication.forgotPassword.sendEmail")}
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="text-center mt-6">
|
|
103
|
+
<a href="#" onClick={() => router.back()}>
|
|
104
|
+
{t("t.cancel")}
|
|
105
|
+
</a>
|
|
106
|
+
</div>
|
|
107
|
+
</section>
|
|
108
|
+
</Form>
|
|
109
|
+
</div>
|
|
110
|
+
</FormCard>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { FormForgotPassword as default, FormForgotPassword }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.accordion-blue-theme__bar {
|
|
2
|
+
@apply bg-primary-light;
|
|
3
|
+
@apply p-4;
|
|
4
|
+
@apply border-b;
|
|
5
|
+
@apply border-primary;
|
|
6
|
+
@apply flex;
|
|
7
|
+
@apply justify-between;
|
|
8
|
+
@apply font-sans;
|
|
9
|
+
@apply text-tiny;
|
|
10
|
+
@apply text-gray-800;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.accordion-gray-theme__bar {
|
|
14
|
+
@apply bg-gray-400;
|
|
15
|
+
@apply px-4;
|
|
16
|
+
@apply py-4;
|
|
17
|
+
@apply border-0;
|
|
18
|
+
@apply rounded-md;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
|
|
21
|
+
@apply flex;
|
|
22
|
+
@apply justify-between;
|
|
23
|
+
@apply font-sans;
|
|
24
|
+
@apply text-base;
|
|
25
|
+
@apply text-gray-950;
|
|
26
|
+
|
|
27
|
+
svg {
|
|
28
|
+
fill: #1f2937; // gray-800
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.accordion-gray-theme__bar.accordion-open {
|
|
33
|
+
border-radius: 8px 8px 0px 0px;
|
|
34
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, { useState, useRef } from "react"
|
|
2
|
+
import { Icon, IconFillColors } from "../../icons/Icon"
|
|
3
|
+
import "./ContentAccordion.scss"
|
|
4
|
+
|
|
5
|
+
interface ContentAccordionProps {
|
|
6
|
+
customBarContent?: React.ReactNode
|
|
7
|
+
customExpandedContent?: React.ReactNode
|
|
8
|
+
disableAccordion?: boolean
|
|
9
|
+
accordionTheme?: AccordionTheme
|
|
10
|
+
barClass?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type AccordionTheme = "blue" | "gray"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* An accordion that consists of header bar content and expandable content
|
|
17
|
+
* Two existing themes under our design system are available
|
|
18
|
+
*/
|
|
19
|
+
const ContentAccordion = (props: ContentAccordionProps) => {
|
|
20
|
+
const [accordionOpen, setAccordionOpen] = useState(false)
|
|
21
|
+
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
22
|
+
|
|
23
|
+
const toggleTable = () => {
|
|
24
|
+
if (!props.disableAccordion) {
|
|
25
|
+
setAccordionOpen(!accordionOpen)
|
|
26
|
+
buttonRef?.current?.focus()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={`mb-4`}>
|
|
32
|
+
<button
|
|
33
|
+
onClick={toggleTable}
|
|
34
|
+
className={`w-full text-left ${props.disableAccordion && "cursor-default"}`}
|
|
35
|
+
ref={buttonRef}
|
|
36
|
+
aria-expanded={accordionOpen}
|
|
37
|
+
data-test-id={"content-accordion-button"}
|
|
38
|
+
>
|
|
39
|
+
<div
|
|
40
|
+
className={`flex justify-between ${props.barClass} ${
|
|
41
|
+
props.accordionTheme === "blue" && "accordion-blue-theme__bar"
|
|
42
|
+
} ${props.accordionTheme === "gray" && "accordion-gray-theme__bar"} ${
|
|
43
|
+
accordionOpen && "accordion-open"
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
{props.customBarContent}
|
|
47
|
+
{!props.disableAccordion && (
|
|
48
|
+
<>
|
|
49
|
+
{accordionOpen ? (
|
|
50
|
+
<Icon
|
|
51
|
+
symbol={"closeSmall"}
|
|
52
|
+
size={"base"}
|
|
53
|
+
fill={IconFillColors.primary}
|
|
54
|
+
className={"pt-1 flex items-center"}
|
|
55
|
+
dataTestId={"accordion-close"}
|
|
56
|
+
key={"accordion-close"}
|
|
57
|
+
/>
|
|
58
|
+
) : (
|
|
59
|
+
<Icon
|
|
60
|
+
symbol={"arrowDown"}
|
|
61
|
+
size={"base"}
|
|
62
|
+
fill={IconFillColors.primary}
|
|
63
|
+
className={"flex items-center"}
|
|
64
|
+
dataTestId={"accordion-open"}
|
|
65
|
+
key={"accordion-open"}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</button>
|
|
72
|
+
{accordionOpen && <div>{props.customExpandedContent}</div>}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { ContentAccordion as default, ContentAccordion }
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import React, { useState, useCallback, useEffect } from "react"
|
|
1
|
+
import React, { useState, useCallback, useEffect, useMemo } from "react"
|
|
2
2
|
import "mapbox-gl/dist/mapbox-gl.css"
|
|
3
3
|
import MapGL, { Marker } from "react-map-gl"
|
|
4
4
|
|
|
5
5
|
import "./ListingMap.scss"
|
|
6
6
|
import { MultiLineAddress, Address } from "../../helpers/address"
|
|
7
|
+
import { useIntersect } from "../../.."
|
|
7
8
|
|
|
8
9
|
export interface ListingMapProps {
|
|
9
10
|
address?: Address
|
|
@@ -35,6 +36,15 @@ const isValidLongitude = (longitude: number) => {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const ListingMap = (props: ListingMapProps) => {
|
|
39
|
+
// Lazy load the map component only when it will become visible on screen
|
|
40
|
+
const { setIntersectingElement, intersecting } = useIntersect({
|
|
41
|
+
// `window` isn't set for SSR, so we'll use `global` instead—doesn't really
|
|
42
|
+
// matter because the map won't ever get rendered in SSR anyway
|
|
43
|
+
rootMargin: `${global.innerHeight || 0}px`,
|
|
44
|
+
})
|
|
45
|
+
const [hasIntersected, setHasIntersected] = useState(false)
|
|
46
|
+
if (intersecting && !hasIntersected) setHasIntersected(true)
|
|
47
|
+
|
|
38
48
|
const [marker, setMarker] = useState({
|
|
39
49
|
latitude: props.address?.latitude,
|
|
40
50
|
longitude: props.address?.longitude,
|
|
@@ -95,12 +105,12 @@ const ListingMap = (props: ListingMapProps) => {
|
|
|
95
105
|
return null
|
|
96
106
|
|
|
97
107
|
return (
|
|
98
|
-
<div className="listing-map">
|
|
108
|
+
<div className="listing-map" ref={setIntersectingElement}>
|
|
99
109
|
<div className="addressPopup">
|
|
100
110
|
{props.listingName && <h3 className="text-caps-tiny">{props.listingName}</h3>}
|
|
101
111
|
<MultiLineAddress address={props.address} />
|
|
102
112
|
</div>
|
|
103
|
-
{(process.env.mapBoxToken || process.env.MAPBOX_TOKEN) && (
|
|
113
|
+
{(process.env.mapBoxToken || process.env.MAPBOX_TOKEN) && hasIntersected && (
|
|
104
114
|
<MapGL
|
|
105
115
|
mapboxApiAccessToken={process.env.mapBoxToken || process.env.MAPBOX_TOKEN}
|
|
106
116
|
mapStyle="mapbox://styles/mapbox/streets-v11"
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React from "react"
|
|
2
2
|
import { nanoid } from "nanoid"
|
|
3
3
|
import { MinMax, UnitSummary, Unit } from "@bloom-housing/backend-core/types"
|
|
4
4
|
|
|
5
5
|
import { StandardTable } from "../../tables/StandardTable"
|
|
6
6
|
import { t } from "../../helpers/translator"
|
|
7
7
|
import { numberOrdinal } from "../../helpers/numberOrdinal"
|
|
8
|
+
import ContentAccordion from "./ContentAccordion"
|
|
8
9
|
|
|
9
10
|
const formatRange = (range: MinMax, ordinalize?: boolean) => {
|
|
10
11
|
let min: string | number = range.min
|
|
@@ -43,30 +44,26 @@ const UnitTables = (props: UnitTablesProps) => {
|
|
|
43
44
|
floor: "t.floor",
|
|
44
45
|
}
|
|
45
46
|
|
|
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
47
|
return (
|
|
56
48
|
<>
|
|
57
|
-
{unitSummaries.map((unitSummary: UnitSummary) => {
|
|
58
|
-
const uniqKey = process.env.NODE_ENV === "test" ? "" : nanoid()
|
|
49
|
+
{unitSummaries.map((unitSummary: UnitSummary, index) => {
|
|
59
50
|
const units = props.units.filter(
|
|
60
51
|
(unit: Unit) => unit.unitType?.name == unitSummary.unitType.name
|
|
61
52
|
)
|
|
62
53
|
const unitsFormatted = [] as Array<Record<string, React.ReactNode>>
|
|
63
|
-
let floorSection
|
|
54
|
+
let floorSection: React.ReactNode
|
|
64
55
|
units.forEach((unit: Unit) => {
|
|
65
56
|
unitsFormatted.push({
|
|
66
57
|
number: unit.number,
|
|
67
58
|
sqFeet: (
|
|
68
59
|
<>
|
|
69
|
-
|
|
60
|
+
{unit.sqFeet ? (
|
|
61
|
+
<>
|
|
62
|
+
<strong>{parseInt(unit.sqFeet)}</strong> {t("t.sqFeet")}
|
|
63
|
+
</>
|
|
64
|
+
) : (
|
|
65
|
+
<></>
|
|
66
|
+
)}
|
|
70
67
|
</>
|
|
71
68
|
),
|
|
72
69
|
numBathrooms: <strong>{unit.numBathrooms}</strong>,
|
|
@@ -74,7 +71,7 @@ const UnitTables = (props: UnitTablesProps) => {
|
|
|
74
71
|
})
|
|
75
72
|
})
|
|
76
73
|
|
|
77
|
-
let areaRangeSection
|
|
74
|
+
let areaRangeSection: React.ReactNode
|
|
78
75
|
if (unitSummary.areaRange?.min || unitSummary.areaRange?.max) {
|
|
79
76
|
areaRangeSection = `, ${formatRange(unitSummary.areaRange)} ${t("t.squareFeet")}`
|
|
80
77
|
}
|
|
@@ -88,20 +85,33 @@ const UnitTables = (props: UnitTablesProps) => {
|
|
|
88
85
|
}`
|
|
89
86
|
}
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
88
|
+
const getBarContent = () => {
|
|
89
|
+
return (
|
|
90
|
+
<h3 className={"toggle-header-content"}>
|
|
91
|
+
<strong>{t("listings.unitTypes." + unitSummary.unitType.name)}</strong>:
|
|
92
|
+
{unitsLabel(units)}
|
|
93
|
+
{areaRangeSection}
|
|
94
|
+
{floorSection}
|
|
95
|
+
</h3>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const getExpandableContent = () => {
|
|
100
|
+
return (
|
|
101
|
+
<div className="unit-table">
|
|
102
102
|
<StandardTable headers={unitsHeaders} data={unitsFormatted} />
|
|
103
103
|
</div>
|
|
104
|
-
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<ContentAccordion
|
|
109
|
+
customBarContent={getBarContent()}
|
|
110
|
+
customExpandedContent={getExpandableContent()}
|
|
111
|
+
disableAccordion={props.disableAccordion}
|
|
112
|
+
accordionTheme={"blue"}
|
|
113
|
+
key={index}
|
|
114
|
+
/>
|
|
105
115
|
)
|
|
106
116
|
})}
|
|
107
117
|
</>
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { ListingEvent } from "@bloom-housing/backend-core/types"
|
|
3
2
|
import { t } from "../../../../helpers/translator"
|
|
4
|
-
import dayjs from "dayjs"
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
type DownloadLotteryResultsProps = {
|
|
5
|
+
resultsDate?: string
|
|
6
|
+
buttonText?: string
|
|
7
|
+
pdfURL?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DownloadLotteryResults = (props: DownloadLotteryResultsProps) => {
|
|
11
|
+
if (!props.pdfURL) return null
|
|
9
12
|
return (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<p className="uppercase text-gray-800 text-tiny font-semibold pb-4">
|
|
15
|
-
{dayjs(event.startTime).format("MMMM D, YYYY")}
|
|
16
|
-
</p>
|
|
17
|
-
<a
|
|
18
|
-
className="button is-primary w-full mb-2"
|
|
19
|
-
href={eventUrl}
|
|
20
|
-
title={t("listings.lotteryResults.downloadResults")}
|
|
21
|
-
target="_blank"
|
|
22
|
-
>
|
|
23
|
-
{t("listings.lotteryResults.downloadResults")}
|
|
24
|
-
</a>
|
|
25
|
-
</section>
|
|
13
|
+
<section className="aside-block text-center">
|
|
14
|
+
<h2 className="text-caps pb-4">{t("listings.lotteryResults.header")}</h2>
|
|
15
|
+
{props.resultsDate && (
|
|
16
|
+
<p className="uppercase text-gray-800 text-tiny font-semibold pb-4">{props.resultsDate}</p>
|
|
26
17
|
)}
|
|
27
|
-
|
|
18
|
+
<a
|
|
19
|
+
className="button is-primary w-full mb-2"
|
|
20
|
+
href={props.pdfURL}
|
|
21
|
+
title={props.buttonText}
|
|
22
|
+
target="_blank"
|
|
23
|
+
>
|
|
24
|
+
{props.buttonText ?? t("listings.lotteryResults.downloadResults")}
|
|
25
|
+
</a>
|
|
26
|
+
</section>
|
|
28
27
|
)
|
|
29
28
|
}
|
|
30
29
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
export type EventType = {
|
|
4
|
+
timeString?: string
|
|
5
|
+
dateString?: string
|
|
6
|
+
linkURL?: string
|
|
7
|
+
linkText?: string
|
|
8
|
+
note?: string | React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type EventSectionProps = {
|
|
12
|
+
events: EventType[]
|
|
13
|
+
headerText?: string
|
|
14
|
+
sectionHeader?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const EventSection = (props: EventSectionProps) => {
|
|
18
|
+
if (!props.events.length) return null
|
|
19
|
+
return (
|
|
20
|
+
<section className="aside-block">
|
|
21
|
+
{props.headerText && (
|
|
22
|
+
<h4 className={props.sectionHeader ? "text-caps-underline" : "text-caps-tiny"}>
|
|
23
|
+
{props.headerText}
|
|
24
|
+
</h4>
|
|
25
|
+
)}
|
|
26
|
+
{props.events.map((event, index) => (
|
|
27
|
+
<div key={`events-${index}`} className={`${index !== props.events.length - 1 && "pb-3"}`}>
|
|
28
|
+
{event.dateString && (
|
|
29
|
+
<p className="text text-gray-800 pb-2 flex justify-between items-center">
|
|
30
|
+
<span className="inline-block text-tiny uppercase">{event.dateString}</span>
|
|
31
|
+
{event.timeString && (
|
|
32
|
+
<span className="inline-block text-sm font-bold ml-5 font-alt-sans">
|
|
33
|
+
{event.timeString}
|
|
34
|
+
</span>
|
|
35
|
+
)}
|
|
36
|
+
</p>
|
|
37
|
+
)}
|
|
38
|
+
{event.linkURL && event.linkText && (
|
|
39
|
+
<p className="pb-2 text-tiny">
|
|
40
|
+
<a href={event.linkURL}>{event.linkText}</a>
|
|
41
|
+
</p>
|
|
42
|
+
)}
|
|
43
|
+
{event.note && (
|
|
44
|
+
<p className={`text-tiny text-gray-700 ${index !== props.events.length - 1 && "pb-3"}`}>
|
|
45
|
+
{event.note}
|
|
46
|
+
</p>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</section>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { EventSection as default, EventSection }
|