@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,54 @@
1
+ import React, { AriaAttributes, createContext, FunctionComponent } from "react"
2
+ import { UrlObject } from "url"
3
+
4
+ type Url = UrlObject | string
5
+
6
+ export interface LinkProps {
7
+ href: string
8
+ "aria-label"?: string
9
+ "aria-current"?: AriaAttributes["aria-current"]
10
+ className?: string
11
+ tabIndex?: number
12
+ }
13
+
14
+ export interface GenericRouterOptions {
15
+ locale?: string
16
+ }
17
+
18
+ export interface GenericRouter {
19
+ push: (url: Url, as?: Url, options?: GenericRouterOptions) => void
20
+ pathname: string
21
+ asPath: string
22
+ locale?: string
23
+ }
24
+
25
+ export interface NavigationContextProps {
26
+ LinkComponent: FunctionComponent<LinkProps>
27
+ router: GenericRouter
28
+ }
29
+
30
+ export const NavigationContext = createContext<NavigationContextProps>({
31
+ // Replace with an official app solution
32
+ LinkComponent: (props) => (
33
+ <a
34
+ className={props.className}
35
+ href={props.href}
36
+ onClick={(e) => {
37
+ e.preventDefault()
38
+ alert(`You clicked: ${props.href}`)
39
+ }}
40
+ {...(props["aria-label"] ? { "aria-label": props["aria-label"] } : {})}
41
+ {...(props["aria-current"] ? { "aria-current": props["aria-current"] } : {})}
42
+ >
43
+ {props.children}
44
+ </a>
45
+ ),
46
+ router: {
47
+ push: () => {
48
+ // no-op
49
+ },
50
+ pathname: "",
51
+ asPath: "",
52
+ locale: "",
53
+ },
54
+ })
@@ -0,0 +1,2 @@
1
+ export { ConfigProvider, ConfigContext } from "./ConfigContext"
2
+ export * from "./NavigationContext"
@@ -0,0 +1,12 @@
1
+ import * as React from "react"
2
+
3
+ const ExygyFooter = () => (
4
+ <span className="text-sm tracking-wider">
5
+ made with ❤️ by{" "}
6
+ <a href="http://exygy.com" target="_blank" aria-label="Opens in new window">
7
+ exygy
8
+ </a>
9
+ </span>
10
+ )
11
+
12
+ export { ExygyFooter as default, ExygyFooter }
@@ -0,0 +1,28 @@
1
+ footer.site-footer {
2
+ @apply text-gray-500;
3
+ @apply w-full;
4
+ @apply bg-gray-800;
5
+ @apply text-center;
6
+
7
+ img {
8
+ @apply inline;
9
+ width: 130px;
10
+ opacity: 0.8;
11
+ }
12
+
13
+ a {
14
+ @apply text-white;
15
+ }
16
+
17
+ p {
18
+ @apply mb-5;
19
+ }
20
+
21
+ & > .footer-row:first-of-type {
22
+ @apply pt-12;
23
+ }
24
+
25
+ & > .footer-row:last-of-type {
26
+ @apply pt-0;
27
+ }
28
+ }
@@ -0,0 +1,10 @@
1
+ import * as React from "react"
2
+ import "./SiteFooter.scss"
3
+
4
+ export interface FooterProps {
5
+ children: React.ReactNode
6
+ }
7
+
8
+ const SiteFooter = (props: FooterProps) => <footer className="site-footer">{props.children}</footer>
9
+
10
+ export { SiteFooter as default, SiteFooter }
@@ -0,0 +1,50 @@
1
+ import axios from "axios"
2
+
3
+ interface CloudinaryUploadProps {
4
+ file: File
5
+ onUploadProgress: (progress: number) => void
6
+ cloudName: string
7
+ uploadPreset: string
8
+ tag?: string
9
+ signature?: string
10
+ apiKey?: string
11
+ timestamp?: number
12
+ }
13
+
14
+ export const CloudinaryUpload = async ({
15
+ file,
16
+ onUploadProgress,
17
+ cloudName,
18
+ uploadPreset,
19
+ signature,
20
+ apiKey,
21
+ timestamp,
22
+ tag = "browser_upload",
23
+ }: CloudinaryUploadProps) => {
24
+ const url = `https://api.cloudinary.com/v1_1/${cloudName}/upload`
25
+ const data = new FormData()
26
+ data.append("upload_preset", uploadPreset)
27
+ data.append("tags", tag)
28
+ data.append("file", file)
29
+ if (signature && timestamp && apiKey) {
30
+ data.append("signature", signature)
31
+ data.append("timestamp", `${timestamp}`)
32
+ data.append("api_key", apiKey)
33
+ }
34
+
35
+ if (!cloudName || cloudName == "" || !uploadPreset || uploadPreset == "") {
36
+ const err = "Please supply a cloud name and upload preset for Cloudinary"
37
+ alert(err)
38
+ throw err
39
+ }
40
+
41
+ const response = await axios.request({
42
+ method: "post",
43
+ url: url,
44
+ data: data,
45
+ onUploadProgress: (p) => {
46
+ onUploadProgress(parseInt(((p.loaded / p.total) * 100).toFixed(0), 10))
47
+ },
48
+ })
49
+ return response
50
+ }
@@ -0,0 +1,132 @@
1
+ import React from "react"
2
+ import { t } from "../helpers/translator"
3
+ import { Field } from "./Field"
4
+ import moment from "moment"
5
+ import { UseFormMethods, FieldError, DeepMap } from "react-hook-form"
6
+
7
+ export type DOBFieldValues = {
8
+ birthDay: string
9
+ birthMonth: string
10
+ birthYear: string
11
+ }
12
+
13
+ export interface DOBFieldProps {
14
+ error?: DeepMap<DOBFieldValues, FieldError>
15
+ errorMessage?: string
16
+ label: React.ReactNode
17
+ register: UseFormMethods["register"]
18
+ watch: UseFormMethods["watch"]
19
+ defaultDOB?: DOBFieldValues
20
+ validateAge18?: boolean
21
+ name?: string
22
+ id?: string
23
+ required?: boolean
24
+ disabled?: boolean
25
+ readerOnly?: boolean
26
+ }
27
+
28
+ const DOBField = (props: DOBFieldProps) => {
29
+ const { defaultDOB, error, register, watch, validateAge18, name, id, errorMessage } = props
30
+
31
+ const getFieldName = (baseName: string) => {
32
+ // Append overall date field name to individual date field name
33
+ return [name, baseName].filter((item) => item).join(".")
34
+ }
35
+
36
+ const birthDay = watch(getFieldName("birthDay"))
37
+ const birthMonth = watch(getFieldName("birthMonth"))
38
+
39
+ const validateAge = (value: string) => {
40
+ return (
41
+ parseInt(value) > 1900 &&
42
+ moment(`${birthMonth}/${birthDay}/${value}`, "MM/DD/YYYY") < moment().subtract(18, "years")
43
+ )
44
+ }
45
+
46
+ const labelClasses = ["field-label--caps"]
47
+ if (props.readerOnly) labelClasses.push("sr-only")
48
+
49
+ return (
50
+ <fieldset id={id}>
51
+ <legend className={labelClasses.join(" ")}>{props.label}</legend>
52
+
53
+ <div className="field-group--date">
54
+ <Field
55
+ name={getFieldName("birthMonth")}
56
+ label={t("t.month")}
57
+ disabled={props.disabled}
58
+ readerOnly={true}
59
+ placeholder="MM"
60
+ defaultValue={defaultDOB?.birthMonth ? defaultDOB.birthMonth : ""}
61
+ error={error?.birthMonth !== undefined}
62
+ validation={{
63
+ required: props.required,
64
+ validate: {
65
+ monthRange: (value: string) => {
66
+ if (!props.required && !value?.length) return true
67
+
68
+ return parseInt(value) > 0 && parseInt(value) <= 12
69
+ },
70
+ },
71
+ }}
72
+ inputProps={{ maxLength: 2 }}
73
+ register={register}
74
+ />
75
+ <Field
76
+ name={getFieldName("birthDay")}
77
+ label={t("t.day")}
78
+ disabled={props.disabled}
79
+ readerOnly={true}
80
+ placeholder="DD"
81
+ defaultValue={defaultDOB?.birthDay ? defaultDOB.birthDay : ""}
82
+ error={error?.birthDay !== undefined}
83
+ validation={{
84
+ required: props.required,
85
+ validate: {
86
+ dayRange: (value: string) => {
87
+ if (!props.required && !value?.length) return true
88
+
89
+ return parseInt(value) > 0 && parseInt(value) <= 31
90
+ },
91
+ },
92
+ }}
93
+ inputProps={{ maxLength: 2 }}
94
+ register={register}
95
+ />
96
+ <Field
97
+ name={getFieldName("birthYear")}
98
+ label={t("t.year")}
99
+ disabled={props.disabled}
100
+ readerOnly={true}
101
+ placeholder="YYYY"
102
+ defaultValue={defaultDOB?.birthYear ? defaultDOB.birthYear : ""}
103
+ error={error?.birthYear !== undefined}
104
+ validation={{
105
+ required: props.required,
106
+ validate: {
107
+ yearRange: (value: string) => {
108
+ if (props.required && value && parseInt(value) < 1900) return false
109
+ if (props.required && value && parseInt(value) > moment().year() + 10) return false
110
+ if (!props.required && !value?.length) return true
111
+ if (value?.length && validateAge18) return validateAge(value)
112
+ return true
113
+ },
114
+ },
115
+ }}
116
+ inputProps={{ maxLength: 4 }}
117
+ register={register}
118
+ />
119
+ </div>
120
+
121
+ {(error?.birthMonth || error?.birthDay || error?.birthYear) && (
122
+ <div className="field error">
123
+ <span id={`${id}-error`} className="error-message">
124
+ {errorMessage ? errorMessage : t("errors.dateOfBirthError")}
125
+ </span>
126
+ </div>
127
+ )}
128
+ </fieldset>
129
+ )
130
+ }
131
+
132
+ export { DOBField as default, DOBField }
@@ -0,0 +1,120 @@
1
+ import React from "react"
2
+ import { t } from "../helpers/translator"
3
+ import { Field } from "./Field"
4
+ import moment from "moment"
5
+ import { UseFormMethods, FieldError, DeepMap } from "react-hook-form"
6
+
7
+ export type DateFieldValues = {
8
+ day: string
9
+ month: string
10
+ year: string
11
+ }
12
+
13
+ export interface DateFieldProps {
14
+ defaultDate?: DateFieldValues
15
+ disabled?: boolean
16
+ error?: DeepMap<DateFieldValues, FieldError>
17
+ errorMessage?: string
18
+ id?: string
19
+ label: React.ReactNode
20
+ labelClass?: string
21
+ name?: string
22
+ note?: string
23
+ readerOnly?: boolean
24
+ register: UseFormMethods["register"]
25
+ required?: boolean
26
+ watch: UseFormMethods["watch"]
27
+ }
28
+
29
+ const DateField = (props: DateFieldProps) => {
30
+ const { defaultDate, error, register, name, id, errorMessage } = props
31
+
32
+ const getFieldName = (baseName: string) => {
33
+ // Append overall date field name to individual date field name
34
+ return [name, baseName].filter((item) => item).join(".")
35
+ }
36
+
37
+ const labelClasses = ["field-label", props.labelClass]
38
+ if (props.readerOnly) labelClasses.push("sr-only")
39
+
40
+ return (
41
+ <fieldset id={id}>
42
+ <legend className={labelClasses.join(" ")}>{props.label}</legend>
43
+ <div className="field-group--date">
44
+ <Field
45
+ name={getFieldName("month")}
46
+ label={t("t.month")}
47
+ disabled={props.disabled}
48
+ readerOnly={true}
49
+ placeholder="MM"
50
+ defaultValue={defaultDate?.month ?? ""}
51
+ error={error?.month !== undefined}
52
+ validation={{
53
+ required: props.required,
54
+ validate: {
55
+ monthRange: (value: string) => {
56
+ if (!props.required && !value?.length) return true
57
+ return parseInt(value) > 0 && parseInt(value) <= 12
58
+ },
59
+ },
60
+ }}
61
+ inputProps={{ maxLength: 2 }}
62
+ register={register}
63
+ />
64
+ <Field
65
+ name={getFieldName("day")}
66
+ label={t("t.day")}
67
+ disabled={props.disabled}
68
+ readerOnly={true}
69
+ placeholder="DD"
70
+ defaultValue={defaultDate?.day ?? ""}
71
+ error={error?.day !== undefined}
72
+ validation={{
73
+ required: props.required,
74
+ validate: {
75
+ dayRange: (value: string) => {
76
+ if (!props.required && !value?.length) return true
77
+ return parseInt(value) > 0 && parseInt(value) <= 31
78
+ },
79
+ },
80
+ }}
81
+ inputProps={{ maxLength: 2 }}
82
+ register={register}
83
+ />
84
+ <Field
85
+ name={getFieldName("year")}
86
+ label={t("t.year")}
87
+ disabled={props.disabled}
88
+ readerOnly={true}
89
+ placeholder="YYYY"
90
+ defaultValue={defaultDate?.year ?? ""}
91
+ error={error?.year !== undefined}
92
+ validation={{
93
+ required: props.required,
94
+ validate: {
95
+ yearRange: (value: string) => {
96
+ if (props.required && value && parseInt(value) < 1900) return false
97
+ if (props.required && value && parseInt(value) > moment().year() + 10) return false
98
+ if (!props.required && !value?.length) return true
99
+ return true
100
+ },
101
+ },
102
+ }}
103
+ inputProps={{ maxLength: 4 }}
104
+ register={register}
105
+ />
106
+ </div>
107
+ {props.note && <p className="field-note mb-2 mt-4">{props.note}</p>}
108
+
109
+ {(error?.month || error?.day || error?.year) && (
110
+ <div className="field error">
111
+ <span id={`${id}-error`} className="error-message">
112
+ {errorMessage ? errorMessage : t("errors.dateError")}
113
+ </span>
114
+ </div>
115
+ )}
116
+ </fieldset>
117
+ )
118
+ }
119
+
120
+ export { DateField as default, DateField }
@@ -0,0 +1,17 @@
1
+ .dropzone {
2
+ @apply border-2;
3
+ @apply border-gray-600;
4
+ @apply border-dashed;
5
+ @apply text-center;
6
+ padding: 2.5rem;
7
+ max-width: 32rem;
8
+ cursor: pointer;
9
+
10
+ &.is-active {
11
+ @apply bg-accent-cool-light;
12
+ }
13
+ }
14
+
15
+ .dropzone__progress {
16
+ max-width: 250px;
17
+ }
@@ -0,0 +1,67 @@
1
+ import React, { useCallback } from "react"
2
+ import { useDropzone } from "react-dropzone"
3
+ import { t } from "../helpers/translator"
4
+ import "./Dropzone.scss"
5
+
6
+ interface DropzoneProps {
7
+ uploader: (file: File) => void
8
+ id: string
9
+ label: string
10
+ helptext?: string
11
+ accept?: string | string[]
12
+ progress?: number
13
+ className?: string
14
+ }
15
+
16
+ const Dropzone = (props: DropzoneProps) => {
17
+ const { uploader } = props
18
+ const classNames = ["field"]
19
+ if (props.className) classNames.push(props.className)
20
+
21
+ const onDrop = useCallback(
22
+ (acceptedFiles) => {
23
+ acceptedFiles.forEach((file: File) => uploader(file))
24
+ },
25
+ [uploader]
26
+ )
27
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
28
+ onDrop,
29
+ accept: props.accept,
30
+ maxFiles: 1,
31
+ })
32
+
33
+ const dropzoneClasses = ["dropzone", "control"]
34
+ if (isDragActive) dropzoneClasses.push("is-active")
35
+
36
+ // Three states:
37
+ // * File dropzone by default
38
+ // * Progress > 0 and < 100 shows a progress bar
39
+ // * Progress 100 doesn't show progress bar or dropzone
40
+ return (
41
+ <div className={classNames.join(" ")}>
42
+ <label htmlFor={props.id} className="label">
43
+ {props.label}
44
+ </label>
45
+ {props.helptext && <p className="view-item__label mt-2 mb-4">{props.helptext}</p>}
46
+ {props.progress && props.progress === 100 ? (
47
+ <></>
48
+ ) : props.progress && props.progress > 0 ? (
49
+ <progress className="dropzone__progress" max="100" value={props.progress}></progress>
50
+ ) : (
51
+ <div className={dropzoneClasses.join(" ")} {...getRootProps()}>
52
+ <input id={props.id} {...getInputProps()} />
53
+ {isDragActive ? (
54
+ <p>{t("t.dropFilesHere")}</p>
55
+ ) : (
56
+ <p>
57
+ {t("t.dragFilesHere")} {t("t.or")}{" "}
58
+ <u className="text-primary">{t("t.chooseFromFolder").toLowerCase()}</u>
59
+ </p>
60
+ )}
61
+ </div>
62
+ )}
63
+ </div>
64
+ )
65
+ }
66
+
67
+ export { Dropzone as default, Dropzone }