diff --git a/src/app/components/body-units/body-units.tsx b/src/app/components/body-units/body-units.tsx index 18c44ac9a..fd8afc2aa 100644 --- a/src/app/components/body-units/body-units.tsx +++ b/src/app/components/body-units/body-units.tsx @@ -1,6 +1,6 @@ import React from 'react'; import RawHTML from '~/components/jsx-helpers/raw-html'; -import Quote from '~/components/quote/quote.js'; +import Quote from './quote'; import JITLoad from '~/helpers/jit-load'; function Unknown({data, type}: {data: unknown; type: string}) { @@ -19,7 +19,7 @@ type CTAData = { description: string; button_href: string; button_text: string; -} +}; function CTA({data}: {data: CTAData}) { const alignment = convertAlignment(data.alignment); @@ -45,7 +45,7 @@ type AImageData = { original: { src: string; alt: string; - } + }; }; alignment: string; alt_text: string; @@ -71,11 +71,10 @@ function AlignedImage({data}: {data: AImageData}) { type PQData = { quote: string; attribution: string; -} +}; function PullQuote({data}: {data: PQData}) { const model = { - image: {}, content: data.quote, attribution: data.attribution }; @@ -83,13 +82,14 @@ function PullQuote({data}: {data: PQData}) { return ; } - export type DocumentData = { download_url: string; -} +}; function Document({data}: {data: DocumentData}) { - return import('./import-pdf-unit.js')} data={data} />; + return ( + import('./import-pdf-unit.js')} data={data} /> + ); } // Using CMS tags, which are not camel-case @@ -105,25 +105,39 @@ const bodyUnits = { export type UnitType = { id: string; -} & ({ - type: 'paragraph' | 'aligned_html'; - value: string; -} | { - type: 'aligned_image'; - value: AImageData; -} | { - type: 'pullquote'; - value: PQData; -} | { - type: 'document'; - value: DocumentData; -} | { - type: 'blog_cta'; - value: CTAData; -}) +} & ( + | { + type: 'paragraph' | 'aligned_html'; + value: string; + } + | { + type: 'aligned_image'; + value: AImageData; + } + | { + type: 'pullquote'; + value: PQData; + } + | { + type: 'document'; + value: DocumentData; + } + | { + type: 'blog_cta'; + value: CTAData; + } +); export default function BodyUnit({unit}: {unit: UnitType}) { - const Unit = bodyUnits[unit.type] as ({data}: {data: typeof unit.value}) => React.JSX.Element; - - return Unit ? : ; + const Unit = bodyUnits[unit.type] as ({ + data + }: { + data: typeof unit.value; + }) => React.JSX.Element; + + return Unit ? ( + + ) : ( + + ); } diff --git a/src/app/components/body-units/pdf-unit.tsx b/src/app/components/body-units/pdf-unit.tsx index 67b13e9c7..3a5cd8a9f 100644 --- a/src/app/components/body-units/pdf-unit.tsx +++ b/src/app/components/body-units/pdf-unit.tsx @@ -66,7 +66,9 @@ function toggleFullscreen( if (!fsFn) { return; } - elem.requestFullscreen = elem[fsFn as keyof typeof elem] as () => Promise; + elem.requestFullscreen = elem[ + fsFn as keyof typeof elem + ] as () => Promise; if (!document.fullscreenElement) { elem.requestFullscreen().catch((err) => { @@ -123,7 +125,9 @@ export default function Document({data}: {data: DocumentData}) { > setNumPages(n)} + onLoadSuccess={({numPages: n}: {numPages: number}) => + setNumPages(n) + } inputRef={ref} >
diff --git a/src/app/components/body-units/quote.tsx b/src/app/components/body-units/quote.tsx new file mode 100644 index 000000000..5d0de2c07 --- /dev/null +++ b/src/app/components/body-units/quote.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import RawHTML from '~/components/jsx-helpers/raw-html'; + +export default function Quote({ + model +}: { + model: { + content: string; + attribution: string; + }; +}) { + return ( +
+
+ + {Boolean(model.attribution) && ( +
— {model.attribution}
+ )} +
+
+ ); +} diff --git a/src/app/components/form-select/form-select.tsx b/src/app/components/form-select/form-select.tsx index 057f31066..04f2fc977 100644 --- a/src/app/components/form-select/form-select.tsx +++ b/src/app/components/form-select/form-select.tsx @@ -1,21 +1,28 @@ import React from 'react'; import DropdownSelect from '~/components/select/drop-down/drop-down'; +import {SelectItem} from '../select/select-context'; export default function FormSelect({ - label, name, selectAttributes, options=[], onValueUpdate + label, + name, + selectAttributes, + options = [], + onValueUpdate }: { label: string; name: string; selectAttributes: object; - options: unknown[]; - onValueUpdate?: unknown; + options: SelectItem[]; + onValueUpdate?: (v: string) => void; }) { return (
{label && }
); diff --git a/src/app/components/paginator/paginator-context.js b/src/app/components/paginator/paginator-context.js deleted file mode 100644 index 71371f93e..000000000 --- a/src/app/components/paginator/paginator-context.js +++ /dev/null @@ -1,26 +0,0 @@ -import {useState, useCallback} from 'react'; -import buildContext from '~/components/jsx-helpers/build-context'; - -function useContextValue(params={}) { - const {resultsPerPage=1, initialPage=1} = params; - const [currentPage, setCurrentPage] = useState(initialPage); - const firstOnPage = (currentPage - 1) * resultsPerPage; - const lastOnPage = firstOnPage + resultsPerPage - 1; - const isVisible = useCallback( - (childIndex) => childIndex >= firstOnPage && childIndex <= lastOnPage, - [firstOnPage, lastOnPage] - ); - const visibleChildren = useCallback( - (children) => children.slice(firstOnPage, lastOnPage + 1), - [firstOnPage, lastOnPage] - ); - - return {currentPage, setCurrentPage, resultsPerPage, firstOnPage, lastOnPage, isVisible, visibleChildren}; -} - -const {useContext, ContextProvider} = buildContext({useContextValue}); - -export { - useContext as default, - ContextProvider as PaginatorContextProvider -}; diff --git a/src/app/components/paginator/paginator-context.ts b/src/app/components/paginator/paginator-context.ts new file mode 100644 index 000000000..1c24bd796 --- /dev/null +++ b/src/app/components/paginator/paginator-context.ts @@ -0,0 +1,38 @@ +import {useState, useCallback} from 'react'; +import buildContext from '~/components/jsx-helpers/build-context'; + +type ContextParams = { + resultsPerPage?: number; + initialPage?: number; +}; + +function useContextValue(params: ContextParams = {}) { + const {resultsPerPage = 1, initialPage = 1} = params; + const [currentPage, setCurrentPage] = useState(initialPage); + const firstOnPage = (currentPage - 1) * resultsPerPage; + const lastOnPage = firstOnPage + resultsPerPage - 1; + const isVisible = useCallback( + (childIndex: number) => + childIndex >= firstOnPage && childIndex <= lastOnPage, + [firstOnPage, lastOnPage] + ); + const visibleChildren = useCallback( + (children: React.ReactNode[]) => + children.slice(firstOnPage, lastOnPage + 1), + [firstOnPage, lastOnPage] + ); + + return { + currentPage, + setCurrentPage, + resultsPerPage, + firstOnPage, + lastOnPage, + isVisible, + visibleChildren + }; +} + +const {useContext, ContextProvider} = buildContext({useContextValue}); + +export {useContext as default, ContextProvider as PaginatorContextProvider}; diff --git a/src/app/components/paginator/search-results/paginator.js b/src/app/components/paginator/search-results/paginator.tsx similarity index 62% rename from src/app/components/paginator/search-results/paginator.js rename to src/app/components/paginator/search-results/paginator.tsx index 5f7810a65..3cad8131d 100644 --- a/src/app/components/paginator/search-results/paginator.js +++ b/src/app/components/paginator/search-results/paginator.tsx @@ -2,28 +2,28 @@ import React from 'react'; import usePaginatorContext from '../paginator-context'; import './paginator.scss'; -function getPageIndicators(pages, currentPage) { +function getPageIndicators(pages: number, currentPage: number) { const indicatorCount = Math.min(pages, 5); - const propsFor = (label) => ({ + const propsFor = (label: number) => ({ label, page: `page ${label}`, disabled: label === currentPage, selected: label === currentPage }); - const result = Array(indicatorCount).fill(); + const result = Array(indicatorCount).fill(0); if (pages - currentPage < 3) { result[0] = pages - indicatorCount + 1; } else { - result[0] = (currentPage > 3) ? currentPage - 2 : 1; + result[0] = currentPage > 3 ? currentPage - 2 : 1; } for (let i = 1; i < indicatorCount; ++i) { - result[i] = result[i-1] + 1; + result[i] = result[i - 1] + 1; } return result.map(propsFor); } -function PageButtonBar({pages}) { +function PageButtonBar({pages}: {pages: number}) { const {currentPage, setCurrentPage} = usePaginatorContext(); const disablePrevious = currentPage === 1; const disableNext = currentPage === pages; @@ -37,32 +37,44 @@ function PageButtonBar({pages}) { } return ( - ); } -export function PaginatorControls({items}) { +export function PaginatorControls({items}: {items: number}) { const {currentPage, resultsPerPage} = usePaginatorContext(); const pages = Math.ceil(items / resultsPerPage); const firstIndex = (currentPage - 1) * resultsPerPage; const endBefore = Math.min(firstIndex + resultsPerPage, items); const resultRange = `${firstIndex + 1}-${endBefore}`; - const searchTerm = new window.URLSearchParams(window.location.search).get('q'); + const searchTerm = new window.URLSearchParams(window.location.search).get( + 'q' + ); return (
diff --git a/src/app/components/paginator/simple-paginator.js b/src/app/components/paginator/simple-paginator.tsx similarity index 68% rename from src/app/components/paginator/simple-paginator.js rename to src/app/components/paginator/simple-paginator.tsx index 742418a29..3c072d6f5 100644 --- a/src/app/components/paginator/simple-paginator.js +++ b/src/app/components/paginator/simple-paginator.tsx @@ -5,17 +5,28 @@ import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'; import range from 'lodash/range'; import './simple-paginator.scss'; -function ControlButton({active, icon, onClick}) { +type Direction = 'next' | 'previous'; + +function ControlButton({active, direction, onClick}: { + active?: boolean; + direction: Direction; + onClick: React.MouseEventHandler +}) { + const icon = direction === 'next' ? faChevronRight : faChevronLeft; + return ( - ); } -function PageLink({page, setPage}) { +function PageLink({page, setPage}: { + page: number; + setPage: (p: number) => void; +}) { const onClick = React.useCallback( - (e) => { + (e: React.MouseEvent) => { e.preventDefault(); setPage(page); }, @@ -23,11 +34,17 @@ function PageLink({page, setPage}) { ); return ( - {page} + {page} ); } -function PagesBeforeCurrent({currentPage, totalPages, setPage}) { +type PaginatorArgs = { + currentPage: number; + setPage: (p: number) => void; + totalPages: number; +} + +function PagesBeforeCurrent({currentPage, totalPages, setPage}: PaginatorArgs) { if (currentPage === 1) { return null; } @@ -53,7 +70,7 @@ function PagesBeforeCurrent({currentPage, totalPages, setPage}) { ); } -function PagesAfterCurrent({currentPage, totalPages, setPage}) { +function PagesAfterCurrent({currentPage, totalPages, setPage}: PaginatorArgs) { if (currentPage === totalPages) { return null; } @@ -82,29 +99,34 @@ function PagesAfterCurrent({currentPage, totalPages, setPage}) { ); } -export default function SimplePaginator({currentPage, setPage, totalPages}) { +export default function SimplePaginator({currentPage, setPage, totalPages}: PaginatorArgs) { const nextPage = () => setPage(currentPage + 1); const prevPage = () => setPage(currentPage - 1); return ( -
- 1} icon={faChevronLeft} onClick={prevPage} /> +
+ + ); } -export function itemRangeOnPage(page, perPage, totalCount) { +export function itemRangeOnPage(page: number, perPage: number, totalCount: number) { const end = Math.min(totalCount, page * perPage); const start = (page - 1) * perPage + 1; return [start, end]; } -export function Showing({page, perPage=9, totalCount, ofWhat}) { +export function Showing({page, perPage, totalCount, ofWhat}: { + page: number; + perPage: number; + totalCount: number; + ofWhat: string; +}) { const [start, end] = itemRangeOnPage(page, perPage, totalCount); const countMessage = totalCount <= perPage ? 'all' : `${start}-${end} of`; diff --git a/src/app/components/progress-ring/progress-ring.js b/src/app/components/progress-ring/progress-ring.js deleted file mode 100644 index aca753828..000000000 --- a/src/app/components/progress-ring/progress-ring.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import './progress-ring.scss'; - -function Circle({basicProps, className, strokeDashoffset=0}) { - return ( - - ); -} - -export default function ProgressRing({message, radius, progress, stroke}) { - const normalizedRadius = radius - stroke * 2; - const circumference = normalizedRadius * 2 * Math.PI; - const basicCircleProps = React.useMemo( - () => { - return ({ - cx: radius, - cy: radius, - fill: 'transparent', - r: normalizedRadius, - strokeDasharray: circumference, - strokeWidth: stroke - }); - }, - [radius, normalizedRadius, circumference, stroke] - ); - const strokeDashoffset = circumference - progress / 100 * circumference; - - return ( -
-
- {message} min read -
- - - - -
- ); -} diff --git a/src/app/components/progress-ring/progress-ring.tsx b/src/app/components/progress-ring/progress-ring.tsx new file mode 100644 index 000000000..52ba307ea --- /dev/null +++ b/src/app/components/progress-ring/progress-ring.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import './progress-ring.scss'; + +export default function ProgressRing({ + message, + radius, + progress, + stroke +}: { + message?: number; + radius: number; + progress: number; + stroke: number; +}) { + const normalizedRadius = radius - stroke * 2; + const circumference = normalizedRadius * 2 * Math.PI; + const basicCircleProps = React.useMemo(() => { + return { + cx: radius, + cy: radius, + fill: 'transparent', + r: normalizedRadius, + strokeDasharray: circumference, + strokeWidth: stroke + }; + }, [radius, normalizedRadius, circumference, stroke]); + const strokeDashoffset = circumference - (progress / 100) * circumference; + + return ( +
+
{message} min read
+ + + + +
+ ); +} diff --git a/src/app/components/quote/quote.js b/src/app/components/quote/quote.js deleted file mode 100644 index 439fb872f..000000000 --- a/src/app/components/quote/quote.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import RawHTML from '~/components/jsx-helpers/raw-html'; - -// eslint-disable-next-line complexity -export default function Quote({model}) { - const classList = ['quote-bucket', model.image.image && model.image.alignment || 'full'] - .join(' '); - const styleSpecs = [`background-image: url(${model.image.image})`]; - - if (model.height) { - styleSpecs.push(`height: ${model.height}px`); - } - - return ( -
- { - model.image.image && -
- } -
-
- - { - Boolean(model.attribution) && -
— {model.attribution}
- } -
- { - Boolean(model.overlay) && - {model.alt_text} - } - { - Boolean(model.cta) && - {model.cta} - } -
-
- ); -} diff --git a/src/app/components/radio-panel/radio-panel.js b/src/app/components/radio-panel/radio-panel.js deleted file mode 100644 index 32d9f8607..000000000 --- a/src/app/components/radio-panel/radio-panel.js +++ /dev/null @@ -1,73 +0,0 @@ -import React, {useState} from 'react'; -import RawHTML from '~/components/jsx-helpers/raw-html'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'; -import cn from 'classnames'; - -function RadioButton({item, isSelected, onChange}) { - const onClick = () => { - onChange(item.value); - }; - const onKeyDown = (event) => { - if ([' ', 'Enter'].includes(event.key)) { - onClick(); - } - }; - - return ( -
- { - isSelected(item.value) && - - Filter by: - - } - - { - isSelected(item.value) && - - - - } -
- ); -} - -function RadioPanelJsx({items, selectedValue, onChange}) { - const isSelected = (val) => { - return val === selectedValue; - }; - - return ( - - { - items && items.map((item) => ( - - )) - } - - ); -} - -export function RadioPanel({selectedItem, items, onChange}) { - const [active, setActive] = useState(false); - - function toggleActive() { - setActive(!active); - } - - return ( -
- -
- ); -} diff --git a/src/app/components/radio-panel/radio-panel.tsx b/src/app/components/radio-panel/radio-panel.tsx new file mode 100644 index 000000000..10c110b6a --- /dev/null +++ b/src/app/components/radio-panel/radio-panel.tsx @@ -0,0 +1,108 @@ +import React, {useState} from 'react'; +import RawHTML from '~/components/jsx-helpers/raw-html'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'; +import cn from 'classnames'; + +type Item = { + value: string; + html: string; +}; + +type OnChange = (v: string) => void; + +function RadioButton({ + item, + isSelected, + onChange +}: { + item: Item; + isSelected: (v: string) => boolean; + onChange: OnChange; +}) { + const onClick = () => { + onChange(item.value); + }; + const onKeyDown = (event: React.KeyboardEvent) => { + if ([' ', 'Enter'].includes(event.key)) { + onClick(); + } + }; + + return ( +
+ {isSelected(item.value) && ( + Filter by: + )} + + {isSelected(item.value) && ( + + + + )} +
+ ); +} + +function RadioPanelJsx({ + items, + selectedValue, + onChange +}: { + items: Item[]; + selectedValue: string; + onChange: OnChange; +}) { + const isSelected = (val: string) => { + return val === selectedValue; + }; + + return ( + + {items && + items.map((item) => ( + + ))} + + ); +} + +export function RadioPanel({ + selectedItem, + items, + onChange +}: { + selectedItem: string; + items: Item[]; + onChange: OnChange; +}) { + const [active, setActive] = useState(false); + + function toggleActive() { + setActive(!active); + } + + return ( +
+ +
+ ); +} diff --git a/src/app/components/role-selector/role-selector.js b/src/app/components/role-selector/role-selector.tsx similarity index 54% rename from src/app/components/role-selector/role-selector.js rename to src/app/components/role-selector/role-selector.tsx index 5ee75f0b0..e26ce225b 100644 --- a/src/app/components/role-selector/role-selector.js +++ b/src/app/components/role-selector/role-selector.tsx @@ -5,8 +5,21 @@ import {FormattedMessage, useIntl} from 'react-intl'; import useLanguageContext from '~/contexts/language'; import './role-selector.scss'; -export function RoleDropdown({ options, setValue, name = 'subject' }) { - const optionsAsOptions = options.map((opt) => ({ +export type Option = { + displayName: string; + salesforceName: string; +}; + +export function RoleDropdown({ + options, + setValue, + name = 'subject' +}: { + options: Option[]; + setValue: (v: string) => void; + name?: string; +}) { + const optionsAsOptions = options.map((opt: Option) => ({ label: opt.displayName, value: opt.salesforceName })); @@ -23,34 +36,49 @@ export function RoleDropdown({ options, setValue, name = 'subject' }) { ); } -/* eslint-disable */ +type Props = { + value: string; + setValue: (v: string) => void; + hidden?: boolean; + children: React.ReactNode[]; +}; + function RoleSelector({ data: options, value, setValue, children, - hidden = false -}) { + hidden +}: { + data: Option[]; +} & Props) { const [studentContent, facultyContent] = children; return (
- {value === "Student" && studentContent} - {!["", undefined, "Student"].includes(value) && facultyContent} + {value === 'Student' && studentContent} + {!['', undefined, 'Student'].includes(value) && facultyContent}
); } -export default function RoleSelectorLoader(props) { +export default function RoleSelectorLoader(props: Props) { const {language} = useLanguageContext(); return ( - + ); } diff --git a/src/app/components/salesforce-form/salesforce-form.js b/src/app/components/salesforce-form/salesforce-form.js deleted file mode 100644 index 228ec12b6..000000000 --- a/src/app/components/salesforce-form/salesforce-form.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import useSalesforceContext from '~/contexts/salesforce'; - -export function HiddenFields({leadSource}) { - const {oid, debug} = useSalesforceContext(); - - if (!oid) { - return (
Loading...
); - } - return ( - - - - - { - debug && - } - - - ); -} - -function SfForm({children, postTo, afterSubmit}) { - const [listening, setListening] = React.useState(false); - const {webtocaseUrl, debug, oid} = useSalesforceContext(); - const onSubmit = React.useCallback( - () => setListening(true), - [] - ); - const onLoad = React.useCallback( - () => { - if (listening && afterSubmit) { - setListening(false); - afterSubmit(); - } - }, - [listening, afterSubmit] - ); - - return ( - -