@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.
- package/.jest/setup-tests.js +24 -0
- package/CHANGELOG.md +20 -0
- package/README.md +195 -0
- package/index.ts +148 -0
- package/jest.config.js +41 -0
- package/package.json +98 -0
- package/public/images/alameda-logo-white.svg +1 -0
- package/public/images/arrow-down.png +0 -0
- package/public/images/arrow-down.svg +1 -0
- package/public/images/check.png +0 -0
- package/public/images/check.svg +11 -0
- package/public/images/eho-logo-white.svg +1 -0
- package/public/images/eho-logo.svg +1 -0
- package/public/images/logo_glyph.svg +11 -0
- package/src/actions/Button.scss +157 -0
- package/src/actions/Button.tsx +80 -0
- package/src/actions/ExpandableContent.tsx +29 -0
- package/src/actions/ExpandableText.scss +18 -0
- package/src/actions/ExpandableText.tsx +52 -0
- package/src/actions/LinkButton.tsx +30 -0
- package/src/actions/LocalizedLink.tsx +11 -0
- package/src/authentication/AuthContext.ts +327 -0
- package/src/authentication/RequireLogin.tsx +62 -0
- package/src/authentication/index.ts +5 -0
- package/src/authentication/timeout.tsx +127 -0
- package/src/authentication/token.ts +17 -0
- package/src/authentication/useRequireLoggedInUser.ts +19 -0
- package/src/blocks/ActionBlock.scss +108 -0
- package/src/blocks/ActionBlock.tsx +51 -0
- package/src/blocks/AppStatusItem.scss +140 -0
- package/src/blocks/AppStatusItem.tsx +75 -0
- package/src/blocks/DashBlock.tsx +42 -0
- package/src/blocks/DashBlocks.scss +56 -0
- package/src/blocks/DashBlocks.tsx +7 -0
- package/src/blocks/FormCard.scss +201 -0
- package/src/blocks/FormCard.tsx +29 -0
- package/src/blocks/HousingCounselor.tsx +51 -0
- package/src/blocks/ImageCard.scss +91 -0
- package/src/blocks/ImageCard.tsx +77 -0
- package/src/blocks/InfoCard.scss +42 -0
- package/src/blocks/InfoCard.tsx +44 -0
- package/src/blocks/StatusBar.scss +30 -0
- package/src/blocks/StatusBar.tsx +31 -0
- package/src/blocks/ViewItem.scss +59 -0
- package/src/blocks/ViewItem.tsx +32 -0
- package/src/config/ConfigContext.tsx +36 -0
- package/src/config/NavigationContext.tsx +54 -0
- package/src/config/index.ts +2 -0
- package/src/footers/ExygyFooter.tsx +12 -0
- package/src/footers/SiteFooter.scss +28 -0
- package/src/footers/SiteFooter.tsx +10 -0
- package/src/forms/CloudinaryUpload.ts +50 -0
- package/src/forms/DOBField.tsx +132 -0
- package/src/forms/DateField.tsx +120 -0
- package/src/forms/Dropzone.scss +17 -0
- package/src/forms/Dropzone.tsx +67 -0
- package/src/forms/Field.tsx +115 -0
- package/src/forms/FieldGroup.tsx +82 -0
- package/src/forms/Form.tsx +22 -0
- package/src/forms/HouseholdMemberForm.tsx +41 -0
- package/src/forms/HouseholdSizeField.tsx +74 -0
- package/src/forms/PhoneField.tsx +69 -0
- package/src/forms/PhoneMask.tsx +24 -0
- package/src/forms/Select.tsx +80 -0
- package/src/forms/Textarea.scss +40 -0
- package/src/forms/Textarea.tsx +64 -0
- package/src/forms/TimeField.tsx +176 -0
- package/src/global/AppearanceTypes.ts +46 -0
- package/src/global/ApplicationStatusType.ts +6 -0
- package/src/global/accordion.scss +4 -0
- package/src/global/blocks.scss +137 -0
- package/src/global/custom_counter.scss +50 -0
- package/src/global/forms.scss +362 -0
- package/src/global/headers.scss +89 -0
- package/src/global/homepage.scss +8 -0
- package/src/global/index.scss +72 -0
- package/src/global/lists.scss +21 -0
- package/src/global/markdown.scss +33 -0
- package/src/global/mixins.scss +175 -0
- package/src/global/navbar.scss +280 -0
- package/src/global/print.scss +59 -0
- package/src/global/tables.scss +197 -0
- package/src/global/text.scss +141 -0
- package/src/global/vendor/AgPagination.tsx +133 -0
- package/src/global/vendor/_setup_bulma.scss +31 -0
- package/src/global/vendor/ag_grid.scss +140 -0
- package/src/headers/Hero.scss +56 -0
- package/src/headers/Hero.tsx +76 -0
- package/src/headers/PageHeader.scss +31 -0
- package/src/headers/PageHeader.tsx +39 -0
- package/src/headers/SiteHeader.tsx +136 -0
- package/src/helpers/address.tsx +46 -0
- package/src/helpers/blankApplication.ts +108 -0
- package/src/helpers/capitalize.tsx +7 -0
- package/src/helpers/dateToString.ts +11 -0
- package/src/helpers/debounce.ts +12 -0
- package/src/helpers/formOptions.tsx +229 -0
- package/src/helpers/formatYesNoLabel.ts +9 -0
- package/src/helpers/getTranslationWithArguments.ts +14 -0
- package/src/helpers/links.ts +7 -0
- package/src/helpers/localeRoute.tsx +13 -0
- package/src/helpers/mergeDeep.ts +12 -0
- package/src/helpers/nextjs.ts +7 -0
- package/src/helpers/numberOrdinal.ts +17 -0
- package/src/helpers/occupancyFormatting.tsx +46 -0
- package/src/helpers/pdfs.ts +19 -0
- package/src/helpers/photos.ts +19 -0
- package/src/helpers/preferences.tsx +426 -0
- package/src/helpers/resolveObject.ts +5 -0
- package/src/helpers/state.tsx +7 -0
- package/src/helpers/tableSummaries.tsx +80 -0
- package/src/helpers/translator.tsx +37 -0
- package/src/helpers/useKeyPress.ts +17 -0
- package/src/helpers/useMutate.ts +40 -0
- package/src/helpers/useOutsideClick.ts +25 -0
- package/src/helpers/validators.ts +3 -0
- package/src/icons/HeaderBadge.scss +29 -0
- package/src/icons/HeaderBadge.tsx +38 -0
- package/src/icons/Icon.scss +76 -0
- package/src/icons/Icon.tsx +145 -0
- package/src/icons/Icons.tsx +556 -0
- package/src/lists/PreferencesList.scss +72 -0
- package/src/lists/PreferencesList.tsx +60 -0
- package/src/locales/es.json +745 -0
- package/src/locales/general.json +1307 -0
- package/src/locales/general_OLD.json +868 -0
- package/src/locales/vi.json +745 -0
- package/src/locales/zh.json +745 -0
- package/src/navigation/Breadcrumbs.scss +25 -0
- package/src/navigation/Breadcrumbs.tsx +27 -0
- package/src/navigation/FooterNav.scss +47 -0
- package/src/navigation/FooterNav.tsx +19 -0
- package/src/navigation/LanguageNav.scss +32 -0
- package/src/navigation/LanguageNav.tsx +53 -0
- package/src/navigation/ProgressNav.scss +102 -0
- package/src/navigation/ProgressNav.tsx +50 -0
- package/src/navigation/TabNav.scss +38 -0
- package/src/navigation/TabNav.tsx +69 -0
- package/src/navigation/Tabs.scss +65 -0
- package/src/navigation/Tabs.tsx +93 -0
- package/src/navigation/UserNav.tsx +37 -0
- package/src/notifications/AlertBox.scss +78 -0
- package/src/notifications/AlertBox.tsx +79 -0
- package/src/notifications/AlertNotice.scss +58 -0
- package/src/notifications/AlertNotice.tsx +37 -0
- package/src/notifications/ApplicationStatus.scss +10 -0
- package/src/notifications/ApplicationStatus.tsx +64 -0
- package/src/notifications/ErrorMessage.tsx +15 -0
- package/src/notifications/SiteAlert.tsx +54 -0
- package/src/notifications/StatusAside.scss +11 -0
- package/src/notifications/StatusAside.tsx +25 -0
- package/src/notifications/StatusMessage.scss +25 -0
- package/src/notifications/StatusMessage.tsx +59 -0
- package/src/notifications/alertTypes.ts +7 -0
- package/src/notifications/index.ts +4 -0
- package/src/overlays/Drawer.scss +105 -0
- package/src/overlays/Drawer.tsx +51 -0
- package/src/overlays/LoadingOverlay.scss +25 -0
- package/src/overlays/LoadingOverlay.tsx +29 -0
- package/src/overlays/Modal.scss +55 -0
- package/src/overlays/Modal.tsx +61 -0
- package/src/overlays/Overlay.scss +50 -0
- package/src/overlays/Overlay.tsx +100 -0
- package/src/page_components/listing/AdditionalFees.tsx +56 -0
- package/src/page_components/listing/ListingCard.scss +47 -0
- package/src/page_components/listing/ListingCard.tsx +34 -0
- package/src/page_components/listing/ListingDetailHeader.tsx +25 -0
- package/src/page_components/listing/ListingDetails.tsx +29 -0
- package/src/page_components/listing/ListingMap.scss +36 -0
- package/src/page_components/listing/ListingMap.tsx +138 -0
- package/src/page_components/listing/ListingsGroup.scss +65 -0
- package/src/page_components/listing/ListingsGroup.tsx +49 -0
- package/src/page_components/listing/UnitTables.tsx +111 -0
- package/src/page_components/listing/listing_sidebar/ApplicationSection.tsx +49 -0
- package/src/page_components/listing/listing_sidebar/Apply.tsx +225 -0
- package/src/page_components/listing/listing_sidebar/LeasingAgent.tsx +77 -0
- package/src/page_components/listing/listing_sidebar/ListingUpdated.tsx +20 -0
- package/src/page_components/listing/listing_sidebar/ReferralApplication.tsx +28 -0
- package/src/page_components/listing/listing_sidebar/SidebarAddress.tsx +56 -0
- package/src/page_components/listing/listing_sidebar/Waitlist.tsx +94 -0
- package/src/page_components/listing/listing_sidebar/WhatToExpect.tsx +22 -0
- package/src/page_components/listing/listing_sidebar/events/DownloadLotteryResults.tsx +34 -0
- package/src/page_components/listing/listing_sidebar/events/EventDateSection.tsx +24 -0
- package/src/page_components/listing/listing_sidebar/events/LotteryResultsEvent.tsx +26 -0
- package/src/page_components/listing/listing_sidebar/events/OpenHouseEvent.tsx +27 -0
- package/src/page_components/listing/listing_sidebar/events/PublicLotteryEvent.tsx +22 -0
- package/src/prototypes/AppCard.scss +64 -0
- package/src/prototypes/Back.scss +19 -0
- package/src/prototypes/ButtonGroup.scss +6 -0
- package/src/prototypes/ButtonPager.scss +22 -0
- package/src/prototypes/FieldSection.scss +35 -0
- package/src/prototypes/FieldSection.tsx +31 -0
- package/src/prototypes/GridItem.tsx +15 -0
- package/src/prototypes/SideNav.scss +32 -0
- package/src/prototypes/SideNav.tsx +14 -0
- package/src/prototypes/SummaryCard.scss +34 -0
- package/src/sections/ContentSection.scss +15 -0
- package/src/sections/ContentSection.tsx +25 -0
- package/src/sections/FooterSection.scss +6 -0
- package/src/sections/FooterSection.tsx +16 -0
- package/src/sections/GridSection.scss +72 -0
- package/src/sections/GridSection.tsx +82 -0
- package/src/sections/InfoCardGrid.scss +45 -0
- package/src/sections/InfoCardGrid.tsx +20 -0
- package/src/sections/ListSection.scss +7 -0
- package/src/sections/ListSection.tsx +23 -0
- package/src/sections/MarkdownSection.scss +13 -0
- package/src/sections/MarkdownSection.tsx +21 -0
- package/src/sections/ResponsiveContentList.tsx +67 -0
- package/src/sections/ResponsiveWrappers.tsx +23 -0
- package/src/tables/GroupedTable.tsx +86 -0
- package/src/tables/MinimalTable.tsx +32 -0
- package/src/tables/ResponsiveTable.tsx +24 -0
- package/src/tables/StandardTable.tsx +229 -0
- package/src/text/Description.scss +52 -0
- package/src/text/Description.tsx +24 -0
- package/src/text/Message.scss +16 -0
- package/src/text/Message.tsx +16 -0
- package/src/text/Tag.scss +94 -0
- package/src/text/Tag.tsx +22 -0
- package/tailwind.config.js +128 -0
- package/tailwind.tosass.js +29 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { FunctionComponent, useContext, useEffect } from "react"
|
|
2
|
+
import { setSiteAlertMessage } from "../notifications/SiteAlert"
|
|
3
|
+
import { NavigationContext } from "../config/NavigationContext"
|
|
4
|
+
import { AuthContext } from "./AuthContext"
|
|
5
|
+
|
|
6
|
+
// See https://github.com/Microsoft/TypeScript/issues/14094
|
|
7
|
+
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
|
|
8
|
+
type XOR<T, U> = T | U extends Record<string, unknown>
|
|
9
|
+
? (Without<T, U> & U) | (Without<U, T> & T)
|
|
10
|
+
: T | U
|
|
11
|
+
|
|
12
|
+
type RequireLoginProps = {
|
|
13
|
+
signInPath: string
|
|
14
|
+
signInMessage: string
|
|
15
|
+
} & XOR<{ requireForRoutes?: string[] }, { skipForRoutes: string[] }>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Require a login to render children. Will redirect to `signInPath` if not logged in.
|
|
19
|
+
*
|
|
20
|
+
* Props can be specified with either a "whitelist" (list of routes to skip check for) or a "blacklist" (list of
|
|
21
|
+
* routes to apply test on). If no list of routes is provided, then will always apply check.
|
|
22
|
+
*/
|
|
23
|
+
const RequireLogin: FunctionComponent<RequireLoginProps> = ({
|
|
24
|
+
children,
|
|
25
|
+
signInPath,
|
|
26
|
+
signInMessage,
|
|
27
|
+
...rest
|
|
28
|
+
}) => {
|
|
29
|
+
const { router } = useContext(NavigationContext)
|
|
30
|
+
const { profile, initialStateLoaded } = useContext(AuthContext)
|
|
31
|
+
|
|
32
|
+
// Parse just the pathname portion of the signInPath (in case we want to pass URL params)
|
|
33
|
+
const [signInPathname] = signInPath.split("?")
|
|
34
|
+
|
|
35
|
+
// Check if this route requires a login or not (can be specified as a whitelist or a blacklist).
|
|
36
|
+
const loginRequiredForPath =
|
|
37
|
+
// by definition, we shouldn't require login on the sign in page itself
|
|
38
|
+
router.pathname !== signInPathname &&
|
|
39
|
+
("requireForRoutes" in rest
|
|
40
|
+
? rest.requireForRoutes
|
|
41
|
+
? rest.requireForRoutes.some((path) => new RegExp(path).exec(router.pathname))
|
|
42
|
+
: true
|
|
43
|
+
: rest.skipForRoutes
|
|
44
|
+
? !rest.skipForRoutes.some((path) => new RegExp(path).exec(router.pathname))
|
|
45
|
+
: true)
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (loginRequiredForPath && initialStateLoaded && !profile) {
|
|
49
|
+
setSiteAlertMessage(signInMessage, "notice")
|
|
50
|
+
void router.push(signInPath)
|
|
51
|
+
}
|
|
52
|
+
}, [loginRequiredForPath, initialStateLoaded, profile, router, signInPath, signInMessage])
|
|
53
|
+
|
|
54
|
+
if (loginRequiredForPath && !profile) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Login either isn't required, or the user object is loaded successfully, continue rendering as normal.
|
|
59
|
+
return <>{children}</>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { RequireLogin as default, RequireLogin }
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AuthContext, AuthProvider } from "./AuthContext"
|
|
2
|
+
export { RequireLogin } from "./RequireLogin"
|
|
3
|
+
export { useRequireLoggedInUser } from "./useRequireLoggedInUser"
|
|
4
|
+
export { ACCESS_TOKEN_LOCAL_STORAGE_KEY } from "./token"
|
|
5
|
+
export { IdleTimeout, LoggedInUserIdleTimeout } from "./timeout"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { createElement, FunctionComponent, useContext, useEffect, useState } from "react"
|
|
2
|
+
import { AuthContext } from "./AuthContext"
|
|
3
|
+
import { ConfigContext, NavigationContext } from "../config"
|
|
4
|
+
import { Button } from "../actions/Button"
|
|
5
|
+
import { Modal } from "../overlays/Modal"
|
|
6
|
+
import { setSiteAlertMessage } from "../notifications/SiteAlert"
|
|
7
|
+
import { AlertTypes } from "../notifications/alertTypes"
|
|
8
|
+
import { t } from "../helpers/translator"
|
|
9
|
+
import { AppearanceStyleType } from "../global/AppearanceTypes"
|
|
10
|
+
|
|
11
|
+
const PROMPT_TIMEOUT = 60000
|
|
12
|
+
const events = ["mousemove", "keypress", "scroll"]
|
|
13
|
+
|
|
14
|
+
function useIdleTimeout(timeoutMs: number, onTimeout: () => void) {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let timer: number
|
|
17
|
+
const restartTimer = () => {
|
|
18
|
+
if (timer) {
|
|
19
|
+
clearTimeout(timer)
|
|
20
|
+
}
|
|
21
|
+
timer = (setTimeout(onTimeout, timeoutMs) as unknown) as number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Listen for any activity events & reset the timer when they are found
|
|
25
|
+
if (typeof document !== "undefined") {
|
|
26
|
+
events.forEach((event) => document.addEventListener(event, restartTimer, false))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Clean up our listeners & clear the timeout on unmounting/updating the effect
|
|
30
|
+
return () => {
|
|
31
|
+
if (timer) {
|
|
32
|
+
clearTimeout(timer)
|
|
33
|
+
}
|
|
34
|
+
events.forEach((event) => document.removeEventListener(event, restartTimer, false))
|
|
35
|
+
}
|
|
36
|
+
}, [timeoutMs, onTimeout])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type IdleTimeoutProps = {
|
|
40
|
+
promptTitle: string
|
|
41
|
+
promptText: string
|
|
42
|
+
promptAction: string
|
|
43
|
+
redirectPath: string
|
|
44
|
+
alertMessage: string
|
|
45
|
+
alertType?: AlertTypes
|
|
46
|
+
onTimeout: () => unknown
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const IdleTimeout: FunctionComponent<IdleTimeoutProps> = ({
|
|
50
|
+
promptTitle,
|
|
51
|
+
promptAction,
|
|
52
|
+
promptText,
|
|
53
|
+
redirectPath,
|
|
54
|
+
alertMessage,
|
|
55
|
+
alertType = "alert",
|
|
56
|
+
onTimeout,
|
|
57
|
+
}) => {
|
|
58
|
+
const { idleTimeout } = useContext(ConfigContext)
|
|
59
|
+
const [promptTimeout, setPromptTimeout] = useState<number | undefined>()
|
|
60
|
+
const { router } = useContext(NavigationContext)
|
|
61
|
+
|
|
62
|
+
useIdleTimeout(idleTimeout, () => {
|
|
63
|
+
// Clear any existing prompt timeouts
|
|
64
|
+
if (promptTimeout) {
|
|
65
|
+
clearTimeout(promptTimeout)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Give the user 1 minute to respond to the prompt before the onTimeout action
|
|
69
|
+
setPromptTimeout(
|
|
70
|
+
(setTimeout(() => {
|
|
71
|
+
const timeoutAction = async () => {
|
|
72
|
+
setPromptTimeout(undefined)
|
|
73
|
+
await onTimeout()
|
|
74
|
+
setSiteAlertMessage(alertMessage, alertType)
|
|
75
|
+
void router.push(redirectPath)
|
|
76
|
+
}
|
|
77
|
+
void timeoutAction()
|
|
78
|
+
}, PROMPT_TIMEOUT) as unknown) as number
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const modalActions = [
|
|
83
|
+
<Button
|
|
84
|
+
styleType={AppearanceStyleType.primary}
|
|
85
|
+
onClick={() => {
|
|
86
|
+
clearTimeout(promptTimeout)
|
|
87
|
+
setPromptTimeout(undefined)
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{promptAction}
|
|
91
|
+
</Button>,
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Modal
|
|
96
|
+
open={Boolean(promptTimeout)}
|
|
97
|
+
title={promptTitle}
|
|
98
|
+
ariaDescription={promptText}
|
|
99
|
+
actions={modalActions}
|
|
100
|
+
hideCloseIcon
|
|
101
|
+
>
|
|
102
|
+
{promptText}
|
|
103
|
+
</Modal>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const LoggedInUserIdleTimeout = ({ onTimeout }: { onTimeout?: () => unknown }) => {
|
|
108
|
+
const { profile, signOut } = useContext(AuthContext)
|
|
109
|
+
|
|
110
|
+
const timeoutFxn = async () => {
|
|
111
|
+
onTimeout && (await onTimeout())
|
|
112
|
+
signOut && signOut()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Only render the IdleTimeout component if the user is logged in
|
|
116
|
+
return profile && signOut
|
|
117
|
+
? createElement(IdleTimeout, {
|
|
118
|
+
promptTitle: t("t.areYouStillWorking"),
|
|
119
|
+
promptText: t("authentication.timeout.text"),
|
|
120
|
+
promptAction: t("authentication.timeout.action"),
|
|
121
|
+
redirectPath: `/sign-in`,
|
|
122
|
+
alertMessage: t("authentication.timeout.signOutMessage"),
|
|
123
|
+
alertType: "notice",
|
|
124
|
+
onTimeout: timeoutFxn,
|
|
125
|
+
})
|
|
126
|
+
: null
|
|
127
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import jwtDecode from "jwt-decode"
|
|
2
|
+
|
|
3
|
+
export const ACCESS_TOKEN_LOCAL_STORAGE_KEY = "@bht"
|
|
4
|
+
|
|
5
|
+
const getStorage = (type: string) => (type === "local" ? localStorage : sessionStorage)
|
|
6
|
+
|
|
7
|
+
export const setToken = (storageType: string, token: string) =>
|
|
8
|
+
getStorage(storageType).setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, token)
|
|
9
|
+
export const getToken = (storageType: string) =>
|
|
10
|
+
getStorage(storageType).getItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY)
|
|
11
|
+
export const clearToken = (storageType: string) =>
|
|
12
|
+
getStorage(storageType).removeItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY)
|
|
13
|
+
|
|
14
|
+
export const getTokenTtl = (token: string) => {
|
|
15
|
+
const { exp = 0 } = jwtDecode(token)
|
|
16
|
+
return new Date(exp * 1000).valueOf() - new Date().valueOf()
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useContext } from "react"
|
|
2
|
+
import { AuthContext } from "./AuthContext"
|
|
3
|
+
import { NavigationContext } from "../config/NavigationContext"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Require a logged in user. Waits on initial load, then initiates a redirect to `redirectPath` if user is not
|
|
7
|
+
* logged in.
|
|
8
|
+
*/
|
|
9
|
+
function useRequireLoggedInUser(redirectPath: string) {
|
|
10
|
+
const { profile, initialStateLoaded } = useContext(AuthContext)
|
|
11
|
+
const { router } = useContext(NavigationContext)
|
|
12
|
+
|
|
13
|
+
if (initialStateLoaded && !profile) {
|
|
14
|
+
void router.push(redirectPath)
|
|
15
|
+
}
|
|
16
|
+
return profile
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { useRequireLoggedInUser as default, useRequireLoggedInUser }
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
.action-block {
|
|
2
|
+
display: flex;
|
|
3
|
+
@apply p-4;
|
|
4
|
+
|
|
5
|
+
&.primary-lighter {
|
|
6
|
+
@apply bg-primary-lighter;
|
|
7
|
+
}
|
|
8
|
+
&.primary-darker {
|
|
9
|
+
@apply bg-primary-darker;
|
|
10
|
+
@apply text-white;
|
|
11
|
+
.action-block__icon {
|
|
12
|
+
@apply border-white;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
.action-block__actions {
|
|
16
|
+
@media (max-width: 640px) {
|
|
17
|
+
display: flex;
|
|
18
|
+
justify-items: center;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
.action-block__actions button {
|
|
22
|
+
@apply m-2;
|
|
23
|
+
}
|
|
24
|
+
.action-block__subheader {
|
|
25
|
+
@apply pt-2;
|
|
26
|
+
@apply font-sans;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.action-block__block {
|
|
31
|
+
align-items: center;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
@apply pb-8;
|
|
35
|
+
|
|
36
|
+
.action-block__head {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.action-block__icon {
|
|
44
|
+
@apply p-4;
|
|
45
|
+
@apply border-solid;
|
|
46
|
+
@apply border-b-4;
|
|
47
|
+
@apply border-primary;
|
|
48
|
+
@apply mb-6;
|
|
49
|
+
}
|
|
50
|
+
.action-block__head {
|
|
51
|
+
@apply mb-6;
|
|
52
|
+
}
|
|
53
|
+
.action-block__actions {
|
|
54
|
+
@media (max-width: 640px) {
|
|
55
|
+
display: block;
|
|
56
|
+
text-align: center;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.action-block__inline {
|
|
62
|
+
align-items: baseline;
|
|
63
|
+
flex-direction: row;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
@media (max-width: 640px) {
|
|
66
|
+
display: block;
|
|
67
|
+
text-align: center;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.action-block__actions {
|
|
71
|
+
display: flex;
|
|
72
|
+
justify-content: flex-end;
|
|
73
|
+
|
|
74
|
+
@media (max-width: 640px) {
|
|
75
|
+
display: block;
|
|
76
|
+
text-align: center;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.action-block__head {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
flex-direction: row;
|
|
84
|
+
@media (max-width: 640px) {
|
|
85
|
+
display: block;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
.action-block__icon {
|
|
89
|
+
@apply p-4;
|
|
90
|
+
@apply border-solid;
|
|
91
|
+
@apply border-r-4;
|
|
92
|
+
@apply border-primary;
|
|
93
|
+
|
|
94
|
+
@media (max-width: 640px) {
|
|
95
|
+
display: inline-block;
|
|
96
|
+
@apply border-solid;
|
|
97
|
+
@apply border-r-0;
|
|
98
|
+
@apply border-b-4;
|
|
99
|
+
@apply mb-4;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
.action-block__header {
|
|
103
|
+
@apply ml-6;
|
|
104
|
+
@media (max-width: 640px) {
|
|
105
|
+
@apply ml-0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import "./ActionBlock.scss"
|
|
3
|
+
|
|
4
|
+
export enum ActionBlockLayout {
|
|
5
|
+
block = "block",
|
|
6
|
+
inline = "inline",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export enum ActionBlockBackground {
|
|
10
|
+
none = "none",
|
|
11
|
+
primaryLighter = "primary-lighter",
|
|
12
|
+
primaryDarker = "primary-darker",
|
|
13
|
+
}
|
|
14
|
+
interface ActionBlockProps {
|
|
15
|
+
actions: React.ReactNode[]
|
|
16
|
+
background?: string
|
|
17
|
+
header: string
|
|
18
|
+
icon?: React.ReactNode
|
|
19
|
+
layout?: ActionBlockLayout
|
|
20
|
+
subheader?: string
|
|
21
|
+
}
|
|
22
|
+
const ActionBlock = ({
|
|
23
|
+
actions,
|
|
24
|
+
background = ActionBlockBackground.none,
|
|
25
|
+
header,
|
|
26
|
+
icon,
|
|
27
|
+
layout = ActionBlockLayout.block,
|
|
28
|
+
subheader,
|
|
29
|
+
}: ActionBlockProps) => {
|
|
30
|
+
const actionBlockClasses = ["action-block"]
|
|
31
|
+
if (background) actionBlockClasses.push(background)
|
|
32
|
+
if (layout === "block") {
|
|
33
|
+
actionBlockClasses.push("action-block__block")
|
|
34
|
+
} else {
|
|
35
|
+
actionBlockClasses.push("action-block__inline")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={actionBlockClasses.join(" ")}>
|
|
40
|
+
<div className="action-block__head">
|
|
41
|
+
{icon && <div className="action-block__icon">{icon}</div>}
|
|
42
|
+
<h3 className="action-block__header">{header}</h3>
|
|
43
|
+
{subheader && layout === ActionBlockLayout.block && (
|
|
44
|
+
<p className="action-block__subheader">{subheader}</p>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
<div className="action-block__actions">{actions}</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
export { ActionBlock as default, ActionBlock }
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
.status-item {
|
|
2
|
+
@apply relative;
|
|
3
|
+
@apply text-gray-800;
|
|
4
|
+
@apply border-b;
|
|
5
|
+
@apply border-solid;
|
|
6
|
+
@apply border-gray-450;
|
|
7
|
+
|
|
8
|
+
&:last-of-type {
|
|
9
|
+
@apply border-b-0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
&.is-editable {
|
|
13
|
+
@apply bg-primary-lighter;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.status-item__inner {
|
|
18
|
+
@apply p-4;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.status-item__header {
|
|
22
|
+
@apply pb-2;
|
|
23
|
+
|
|
24
|
+
@screen md {
|
|
25
|
+
@apply flex;
|
|
26
|
+
@apply mb-4;
|
|
27
|
+
@apply justify-between;
|
|
28
|
+
@apply border-b;
|
|
29
|
+
@apply border-solid;
|
|
30
|
+
@apply border-gray-450;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.status-item__title {
|
|
35
|
+
@apply text-xl;
|
|
36
|
+
@apply font-alt-sans;
|
|
37
|
+
@apply tracking-wider;
|
|
38
|
+
@apply mb-0;
|
|
39
|
+
@apply uppercase;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.status-item__due {
|
|
43
|
+
@screen md {
|
|
44
|
+
@apply text-right;
|
|
45
|
+
@apply self-center;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.status-item__content {
|
|
50
|
+
@screen md {
|
|
51
|
+
@apply flex;
|
|
52
|
+
@apply justify-between;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.status-confirmation__number {
|
|
57
|
+
@apply pt-4;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.status-item__details {
|
|
61
|
+
@apply text-center;
|
|
62
|
+
@screen md {
|
|
63
|
+
@apply text-left;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.status-item__action {
|
|
68
|
+
@apply text-center;
|
|
69
|
+
@apply pt-4;
|
|
70
|
+
@apply mb-4;
|
|
71
|
+
|
|
72
|
+
@screen md {
|
|
73
|
+
@apply pt-0;
|
|
74
|
+
@apply text-right;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.status-item__footer {
|
|
79
|
+
@apply text-center;
|
|
80
|
+
|
|
81
|
+
@screen md {
|
|
82
|
+
@apply flex;
|
|
83
|
+
@apply justify-between;
|
|
84
|
+
@apply text-left;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.status-item__links {
|
|
89
|
+
@apply pb-4;
|
|
90
|
+
|
|
91
|
+
@screen md {
|
|
92
|
+
@apply pb-0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.status-item__meta {
|
|
97
|
+
@apply mt-4;
|
|
98
|
+
|
|
99
|
+
@screen md {
|
|
100
|
+
@apply text-right;
|
|
101
|
+
@apply mt-0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.status-item__status {
|
|
106
|
+
@apply pb-4;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.status-item__label {
|
|
110
|
+
@apply relative;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.status-item__link {
|
|
114
|
+
@screen md {
|
|
115
|
+
@apply mr-4;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.status-item__date {
|
|
120
|
+
@apply text-sm;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.status-item__address {
|
|
124
|
+
@apply mb-4;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.status-item__confirm-text {
|
|
128
|
+
@apply text-tiny;
|
|
129
|
+
@apply mb-4;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.status-item__confirm-number {
|
|
133
|
+
@apply text-lg;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.status-item__address {
|
|
137
|
+
@screen md {
|
|
138
|
+
@apply text-left;
|
|
139
|
+
}
|
|
140
|
+
}
|