@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.
Files changed (51) hide show
  1. package/.jest/setup-tests.js +8 -0
  2. package/CHANGELOG.md +534 -37
  3. package/README.md +1 -1
  4. package/index.ts +8 -4
  5. package/package.json +4 -4
  6. package/src/authentication/AuthContext.ts +32 -3
  7. package/src/blocks/FormCard.scss +12 -0
  8. package/src/blocks/ImageCard.scss +10 -8
  9. package/src/blocks/ViewItem.tsx +5 -1
  10. package/src/config/NavigationContext.tsx +4 -0
  11. package/src/forms/DOBField.tsx +1 -1
  12. package/src/forms/Field.tsx +4 -2
  13. package/src/forms/FieldGroup.tsx +27 -14
  14. package/src/global/headers.scss +7 -3
  15. package/src/global/lists.scss +4 -5
  16. package/src/global/tables.scss +3 -1
  17. package/src/headers/PageHeader.tsx +5 -1
  18. package/src/helpers/tableSummaries.tsx +1 -1
  19. package/src/helpers/useIntersect.ts +48 -0
  20. package/src/icons/Icon.tsx +6 -1
  21. package/src/icons/Icons.tsx +1 -1
  22. package/src/locales/es.json +1 -1
  23. package/src/locales/general.json +37 -5
  24. package/src/locales/vi.json +1 -1
  25. package/src/locales/zh.json +1 -1
  26. package/src/notifications/AlertBox.scss +3 -3
  27. package/src/notifications/AlertBox.tsx +3 -1
  28. package/src/notifications/AlertNotice.tsx +6 -1
  29. package/src/notifications/ApplicationStatus.scss +2 -7
  30. package/src/notifications/ApplicationStatus.tsx +10 -13
  31. package/src/overlays/Modal.tsx +2 -0
  32. package/src/overlays/Overlay.scss +8 -0
  33. package/src/overlays/Overlay.tsx +2 -1
  34. package/src/page_components/forgot-password/FormForgotPassword.tsx +114 -0
  35. package/src/page_components/listing/ContentAccordion.scss +34 -0
  36. package/src/page_components/listing/ContentAccordion.tsx +77 -0
  37. package/src/page_components/listing/ListingMap.scss +4 -0
  38. package/src/page_components/listing/ListingMap.tsx +13 -3
  39. package/src/page_components/listing/UnitTables.tsx +37 -27
  40. package/src/page_components/listing/listing_sidebar/events/DownloadLotteryResults.tsx +21 -22
  41. package/src/page_components/listing/listing_sidebar/events/EventSection.tsx +54 -0
  42. package/src/page_components/sign-in/FormSignIn.tsx +9 -33
  43. package/src/page_components/sign-in/FormSignInAddPhone.tsx +87 -0
  44. package/src/page_components/sign-in/FormSignInErrorBox.tsx +43 -0
  45. package/src/page_components/sign-in/FormSignInMFACode.tsx +98 -0
  46. package/src/page_components/sign-in/FormSignInMFAType.tsx +95 -0
  47. package/src/tables/StackedTable.tsx +1 -1
  48. package/src/page_components/listing/listing_sidebar/events/EventDateSection.tsx +0 -25
  49. package/src/page_components/listing/listing_sidebar/events/LotteryResultsEvent.tsx +0 -26
  50. package/src/page_components/listing/listing_sidebar/events/OpenHouseEvent.tsx +0 -27
  51. 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 p-4;
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
- &times;
69
+ <span aria-hidden={true}>&times;</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" ? <p>{props.children}</p> : props.children}
34
+ {typeof props.children === "string" ? (
35
+ <Markdown>{props.children}</Markdown>
36
+ ) : (
37
+ props.children
38
+ )}
34
39
  </div>
35
40
  </div>
36
41
  )
@@ -1,10 +1,5 @@
1
1
  .application-status {
2
2
  @apply p-4;
3
- @screen lg {
4
- @apply whitespace-nowrap;
5
- }
6
- }
7
-
8
- .application-status__sub-content {
9
- padding-left: 22px;
3
+ @apply flex;
4
+ gap: 0.45rem;
10
5
  }
@@ -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
- &nbsp;
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
- {content}
61
- {props.subContent && (
62
- <>
63
- <br />
64
- <span className={"application-status__sub-content"}>{props.subContent}</span>
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
  }
@@ -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} />
@@ -48,3 +48,11 @@
48
48
 
49
49
  @include transition-timing;
50
50
  }
51
+
52
+ .fixed-overlay__inner-slim {
53
+ width: 75vw;
54
+
55
+ @screen md {
56
+ width: 50vw;
57
+ }
58
+ }
@@ -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,3 +1,7 @@
1
+ .listing-map {
2
+ min-height: 12rem;
3
+ }
4
+
1
5
  .pin {
2
6
  @apply absolute;
3
7
  width: 30px;
@@ -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 * as React from "react"
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
- <strong>{unit.sqFeet}</strong> {t("t.sqFeet")}
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
- 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">
88
+ const getBarContent = () => {
89
+ return (
90
+ <h3 className={"toggle-header-content"}>
91
+ <strong>{t("listings.unitTypes." + unitSummary.unitType.name)}</strong>:&nbsp;
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
- </div>
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
- const DownloadLotteryResults = (props: { event: ListingEvent; pdfUrl: string }) => {
7
- const { event, pdfUrl } = props
8
- const eventUrl = event ? pdfUrl : null
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
- {eventUrl && (
12
- <section className="aside-block text-center">
13
- <h2 className="text-caps pb-4">{t("listings.lotteryResults.header")}</h2>
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 }