diff --git a/src/app/components/loading-placeholder/loading-placeholder.js b/src/app/components/loading-placeholder/loading-placeholder.js deleted file mode 100644 index ab0be78d7..000000000 --- a/src/app/components/loading-placeholder/loading-placeholder.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import './loader.scss'; - -export default function LoadingPlaceholder() { - /* eslint-disable */ - return ( -
- - - - - - - - -
- ); -} diff --git a/src/app/components/loading-placeholder/loading-placeholder.tsx b/src/app/components/loading-placeholder/loading-placeholder.tsx new file mode 100644 index 000000000..29e5f817b --- /dev/null +++ b/src/app/components/loading-placeholder/loading-placeholder.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import './loader.scss'; + +export default function LoadingPlaceholder() { + /* eslint-disable */ + return ( +
+ + + + + + + + +
+ ); +} diff --git a/src/app/components/shell/router-context.js b/src/app/components/shell/router-context.js deleted file mode 100644 index 59652659d..000000000 --- a/src/app/components/shell/router-context.js +++ /dev/null @@ -1,40 +0,0 @@ -import {useEffect, useState, useCallback} from 'react'; -import {useErrorBoundary} from 'preact/hooks'; -import buildContext from '~/components/jsx-helpers/build-context'; -import {useLocation} from 'react-router-dom'; - -function useContextValue() { - const [failCount, setFailCount] = useState(0); - const [error, resetError] = useErrorBoundary( - (err) => { - console.warn('Error boundary error:', err); - setFailCount(failCount + 1); - if (failCount > 10) { - throw new Error(`Too many fails: ${err.toString()}`); - } - } - ); - const [goto404, setGoto404] = useState(false); - const loc = useLocation(); - const fail = useCallback((info = 'Router force fail') => { - setGoto404(info); - }, []); - - useEffect(() => { - resetError(); - setGoto404(); - }, [loc, resetError, setGoto404]); - - return { - isValid: !error, - goto404, - fail - }; -} - -const {useContext, ContextProvider} = buildContext({useContextValue}); - -export { - useContext as default, - ContextProvider as RouterContextProvider -}; diff --git a/src/app/components/shell/router-context.tsx b/src/app/components/shell/router-context.tsx new file mode 100644 index 000000000..29688b2d3 --- /dev/null +++ b/src/app/components/shell/router-context.tsx @@ -0,0 +1,12 @@ +import {useErrorBoundary} from 'preact/hooks'; +import buildContext from '~/components/jsx-helpers/build-context'; + +function useContextValue() { + useErrorBoundary(); + + return {}; +} + +const {useContext, ContextProvider} = buildContext({useContextValue}); + +export {useContext as default, ContextProvider as RouterContextProvider}; diff --git a/src/app/components/sticky-footer/sticky-footer-wrapper.js b/src/app/components/sticky-footer/sticky-footer-wrapper.js deleted file mode 100644 index 2efa83659..000000000 --- a/src/app/components/sticky-footer/sticky-footer-wrapper.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import useWindowContext, {WindowContextProvider} from '~/contexts/window'; -import {useMainSticky} from '~/helpers/main-class-hooks'; -import cn from 'classnames'; -import './sticky-footer.scss'; - -// Note: menus show and hide based on scroll position, which causes the menu to -// show, then hide, then show again when near the top of the page. -function useCollapsedState() { - const {scrollY, innerHeight} = useWindowContext(); - const distanceFromBottom = document.body.offsetHeight - innerHeight - scrollY; - - return scrollY < 100 || distanceFromBottom < 100; -} - -function StickyFooterWrapper({children}) { - const collapsed = useCollapsedState(); - - useMainSticky(); - - return ( -
- {children} -
- ); -} - -export default function StickyFooter({children}) { - return ( - - - - ); -} diff --git a/src/app/components/sticky-footer/sticky-footer.js b/src/app/components/sticky-footer/sticky-footer.js deleted file mode 100644 index 19879982b..000000000 --- a/src/app/components/sticky-footer/sticky-footer.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import RawHTML from '~/components/jsx-helpers/raw-html'; -import useWindowContext, {WindowContextProvider} from '~/contexts/window'; -import useSharedDataContext from '~/contexts/shared-data'; -import {useMainSticky} from '~/helpers/main-class-hooks'; -import cn from 'classnames'; -import './sticky-footer.scss'; - -// Note: menus show and hide based on scroll position, which causes the menu to -// show, then hide, then show again when near the top of the page. -function useCollapsedState() { - const {scrollY, innerHeight} = useWindowContext(); - const distanceFromBottom = document.body.offsetHeight - innerHeight - scrollY; - - return scrollY < 100 || distanceFromBottom < 100; -} - -export function StickyFooterBody({leftButton, rightButton}) { - const collapsed = useCollapsedState(); - const {stickyFooterState: [_, setSFS]} = useSharedDataContext(); - - useMainSticky(); - - React.useEffect( - () => { - setSFS(collapsed); - return () => setSFS(null); - }, - [collapsed, setSFS] - ); - - return ( -
-
- {leftButton.text} - { - leftButton.description && -
{leftButton.description}
- } - { - leftButton.descriptionHtml && - - } -
- { - rightButton && -
-
{rightButton.description}
- {rightButton.text} -
- } -
- ); -} - -export default function StickyFooter(model) { - return ( - - - - ); -} diff --git a/src/app/components/sticky-footer/sticky-footer.scss b/src/app/components/sticky-footer/sticky-footer.scss deleted file mode 100644 index 2de3d3801..000000000 --- a/src/app/components/sticky-footer/sticky-footer.scss +++ /dev/null @@ -1,98 +0,0 @@ -@import 'pattern-library/core/pattern-library/headers'; - -// Adapted from https://raw.githubusercontent.com/daneden/animate.css/master/animate.css -@keyframes slideInUp { - from { - transform: translate3d(0, 100%, 0); - visibility: visible; - } - - to { - transform: translate3d(0, 0, 0); - } -} - -@keyframes slideOutDown { - from { - transform: translate3d(0, 0, 0); - } - - to { - transform: translate3d(0, 100%, 0); - visibility: hidden; - } -} - -%animated { - animation-duration: 0.2s; - animation-fill-mode: both; -} - -.sticky-footer { - @extend %animated; - - background-color: ui-color(white); - bottom: 0; - box-shadow: 0 -0.2rem 0.6rem 0 rgba(ui-color(black), 0.25); - display: grid; - font-weight: inherit; - grid-template-columns: 1fr 1fr; - justify-content: stretch; - max-height: 9rem; - padding: 0.5rem 1rem; - position: fixed; - width: 100%; - z-index: 4; - - @include wider-than($tablet-max) { - padding: $normal-margin 3rem; - } - - &:not(.collapsed) { - animation-name: slideInUp; - } - - &.collapsed { - animation-name: slideOutDown; - } - - .button-group { - align-items: center; - display: grid; - grid-auto-flow: column; - grid-gap: $normal-margin; - justify-content: right; - transition: opacity 0.2s 0.1s; - - &:first-child { - justify-content: left; - } - } - - .btn { - @include set-font(helper-label); - - border: 0; - flex-basis: 20rem; - margin: 0; - padding: 1rem 3rem; - - @include width-up-to($phone-max) { - flex-basis: calc(100% - 1.5rem); - margin: 0; - } - - @media (max-width: 565px) { - flex-basis: 17rem; - padding: 1rem; - } - } - - .description { - flex: 1 1 50%; - - @include width-up-to($phone-max) { - display: none; - } - } -} diff --git a/src/app/components/student-form/student-form.js b/src/app/components/student-form/student-form.tsx similarity index 70% rename from src/app/components/student-form/student-form.js rename to src/app/components/student-form/student-form.tsx index 0a63eaccc..6c0c3cc8d 100644 --- a/src/app/components/student-form/student-form.js +++ b/src/app/components/student-form/student-form.tsx @@ -1,20 +1,17 @@ import React from 'react'; -import { useNavigate } from 'react-router-dom'; +import {useNavigate} from 'react-router-dom'; import {FormattedMessage} from 'react-intl'; import './student-form.scss'; export default function StudentForm() { const navigate = useNavigate(); - const goBack = React.useCallback( - () => navigate(-1), - [navigate] - ); + const goBack = React.useCallback(() => navigate(-1), [navigate]); return (
-
diff --git a/src/app/contexts/shared-data.ts b/src/app/contexts/shared-data.ts index 92440b495..29f228034 100644 --- a/src/app/contexts/shared-data.ts +++ b/src/app/contexts/shared-data.ts @@ -1,4 +1,3 @@ -import {useState} from 'react'; import buildContext from '~/components/jsx-helpers/build-context'; import cmsFetch from '~/helpers/cms-fetch'; import {usePromise} from '~/helpers/use-data'; @@ -24,11 +23,9 @@ function useFlags() { function useContextValue() { const flags = useFlags(); - const stickyFooterState = useState(null); return { - flags, - stickyFooterState + flags } as const; } diff --git a/src/app/layouts/default/microsurvey-popup/microsurvey-popup.tsx b/src/app/layouts/default/microsurvey-popup/microsurvey-popup.tsx index 742ce5635..2d3f4ef4f 100644 --- a/src/app/layouts/default/microsurvey-popup/microsurvey-popup.tsx +++ b/src/app/layouts/default/microsurvey-popup/microsurvey-popup.tsx @@ -1,20 +1,17 @@ import React from 'react'; import {PutAway} from '../shared'; import useMSQueue from './queue'; -import useSharedDataContext from '~/contexts/shared-data'; import './microsurvey-popup.scss'; export default function MicroSurvey() { const [QueuedItem, nextItem] = useMSQueue(); - const bottom = useBottom(); - const style = React.useMemo(() => (bottom ? {bottom} : {}), [bottom]); if (!QueuedItem) { return null; } return ( -
+
@@ -23,27 +20,3 @@ export default function MicroSurvey() { } const SF_DURATION = 200; // sticky-footer animation duration - -function useBottom() { - const [value, setValue] = React.useState(0); - const { - stickyFooterState: [sfs] - } = useSharedDataContext(); - - React.useEffect(() => { - if (sfs === null) { - setValue(null); - } - const sf = document.querySelector('.sticky-footer'); - - if (sf) { - window.setTimeout(() => { - const {top} = sf.getBoundingClientRect(); - - setValue(window.innerHeight - top + 15); - }, SF_DURATION); - } - }, [sfs]); - - return value; -} diff --git a/src/app/pages/errata-summary/hero/hero.tsx b/src/app/pages/errata-summary/hero/hero.tsx index 9b02a6880..3865880f6 100644 --- a/src/app/pages/errata-summary/hero/hero.tsx +++ b/src/app/pages/errata-summary/hero/hero.tsx @@ -5,7 +5,6 @@ import LoaderPage from '~/components/jsx-helpers/loader-page'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle'; import bookPromise from '~/models/book-titles'; -import useRouterContext from '~/components/shell/router-context'; import cn from 'classnames'; import './hero.scss'; @@ -28,9 +27,10 @@ type PopTipProps = { isOpen: boolean; } +const NOT_FOUND = '*error*'; + function useBookInfo(book: string): [string, string] { const [info, setInfo] = useState<[string, string]>(['', '']); - const {fail} = useRouterContext(); useEffect(() => { bookPromise.then((bookList) => { @@ -42,10 +42,10 @@ function useBookInfo(book: string): [string, string] { setInfo([slug, title]); } else { - fail(`Could not find book info for ${book}`); + setInfo([NOT_FOUND, `${book} not found`]); } }); - }, [book, fail]); + }, [book]); return info; } @@ -137,6 +137,10 @@ function HeroContent({data}: HeroContentProps) { export default function Hero({book}: HeroProps) { const [slug, title] = useBookInfo(book); + if (slug === NOT_FOUND) { + return
{title}
; + } + return (
{slug ? diff --git a/test/src/components/shell.test.tsx b/test/src/components/shell.test.tsx index c601c54cd..7139e58f6 100644 --- a/test/src/components/shell.test.tsx +++ b/test/src/components/shell.test.tsx @@ -95,7 +95,6 @@ describe('shell', () => { }); const setPortal = jest.fn(); const spyGP = jest.spyOn(GP, 'GeneralPageFromSlug'); - const saveWarn = console.warn; type WindowWithPiTracker = (typeof window) & { piTracker: (path: string) => void; @@ -176,7 +175,6 @@ describe('shell', () => { }); it('routes adoption (no CMS page data) page when in portal', async () => { - console.warn = jest.fn(); setPortalPrefix('/landing-page'); mockBrowserInitialEntries(['/landing-page/adoption']); @@ -184,8 +182,6 @@ describe('shell', () => { await screen.findByRole('combobox'); await screen.findByText('Let us know you\'re using OpenStax'); - await waitFor(() => expect(console.warn).toHaveBeenCalled()); - console.warn = saveWarn; }); it('routes "errata" paths', async () => { mockBrowserInitialEntries(['/errata']); @@ -247,14 +243,11 @@ describe('shell', () => { }); // -- Warnings are generated from failed reads it('renders as a portal route with nothing beyond the portal', async () => { - console.warn = jest.fn(); setPortalPrefix('/landing-page'); mockBrowserInitialEntries(['/landing-page/']); render(AppElement); await screen.findByText('Loaded page ""'); - await waitFor(() => expect(console.warn).toHaveBeenCalled()); - console.warn = saveWarn; }); it('renders page within a portal route', async () => { setPortalPrefix('/'); @@ -264,33 +257,24 @@ describe('shell', () => { await waitFor(() => expect(setPortal).toHaveBeenCalledWith('landing-page')); }); it('renders ordinary page through portal', async () => { - console.warn = jest.fn(); setPortalPrefix('/landing-page'); mockBrowserInitialEntries(['/landing-page/contact']); render(AppElement); expect(await screen.findByText('What is your question about?')).toBeInTheDocument(); - await waitFor(() => expect(console.warn).toHaveBeenCalled()); - console.warn = saveWarn; }); it('returns 404 for unknown portal path', async () => { - console.warn = jest.fn(); setPortalPrefix('/landing-page'); mockBrowserInitialEntries(['/landing-page/invalid']); render(AppElement); await screen.findByText('Uh-oh, no page here'); - await waitFor(() => expect(console.warn).toHaveBeenCalled()); - console.warn = saveWarn; }); it('loads flex page within a portal route', async () => { - console.warn = jest.fn(); setPortalPrefix('/landing-page'); mockBrowserInitialEntries(['/landing-page/flex-page']); render(AppElement); await screen.findByRole('heading', {level: 2, name: 'Apply today to be an OpenStax Partner'}); - await waitFor(() => expect(console.warn).toHaveBeenCalled()); - console.warn = saveWarn; }); it('reroutes flex pages with extra path components to the page', async () => { setPortalPrefix(''); @@ -299,13 +283,10 @@ describe('shell', () => { await screen.findByRole('heading', {level: 2, name: 'Apply today to be an OpenStax Partner'}); }); it('loads general page within a portal route', async () => { - console.warn = jest.fn(); setPortalPrefix('/landing-page'); mockBrowserInitialEntries(['/landing-page/general-page']); render(AppElement); await waitFor(() => expect(spyGP).toHaveBeenCalled()); spyGP.mockClear(); - await waitFor(() => expect(console.warn).toHaveBeenCalled()); - console.warn = saveWarn; }); }); diff --git a/test/src/components/shell/microsurvey-popup.test.tsx b/test/src/components/shell/microsurvey-popup.test.tsx index 38e8804f8..325b39112 100644 --- a/test/src/components/shell/microsurvey-popup.test.tsx +++ b/test/src/components/shell/microsurvey-popup.test.tsx @@ -13,11 +13,10 @@ jest.mock('~/contexts/shared-data', () => jest.fn()); jest.useFakeTimers(); describe('microsurvey-popup', () => { - it('renders without sticky footer', () => { + it('renders', () => { (useMSQueue as jest.Mock).mockReturnValue(['item', () => null]); (useSharedDataContext as jest.Mock).mockReturnValue({ - flags: {}, - stickyFooterState: [true] + flags: {} }); render( @@ -29,22 +28,4 @@ describe('microsurvey-popup', () => { jest.runAllTimers(); expect(document.getElementById('microsurvey')).toBeTruthy(); }); - it('renders with sticky footer', () => { - (useMSQueue as jest.Mock).mockReturnValue([null, () => null]); - (useSharedDataContext as jest.Mock).mockReturnValue({ - flags: {}, - stickyFooterState: [null] - }); - - render( - - - -
- - - ); - jest.runAllTimers(); - expect(document.getElementById('microsurvey')).toBeNull(); - }); }); diff --git a/test/src/contexts/user.test.tsx b/test/src/contexts/user.test.tsx index c00908b55..9cd5222a0 100644 --- a/test/src/contexts/user.test.tsx +++ b/test/src/contexts/user.test.tsx @@ -22,8 +22,7 @@ const uData = { describe('user context', () => { beforeAll(() => { jest.spyOn(SDC, 'default').mockReturnValue({ - flags: false, - stickyFooterState: [true, jest.fn()] + flags: false }); }); const saveDebug = console.debug; diff --git a/test/src/general/general.test.tsx b/test/src/general/general.test.tsx index 4820eb793..d393f1a40 100644 --- a/test/src/general/general.test.tsx +++ b/test/src/general/general.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {render, screen} from '@testing-library/preact'; import MemoryRouter from '~/../../test/helpers/future-memory-router'; -import {RouterContextProvider} from '~/components/shell/router-context'; import GeneralPageLoader from '~/pages/general/general'; import * as DH from '~/helpers/use-document-head'; import * as PDU from '~/helpers/page-data-utils'; @@ -15,9 +14,7 @@ describe('general page', () => { async () => { render( - - - + ); await screen.findByText(/Example activities/i); @@ -27,9 +24,7 @@ describe('general page', () => { document.title = 'OpenStax Test'; render( - - - + ); await screen.findByText(/Example activities/i); @@ -43,9 +38,7 @@ describe('general page', () => { render( - - - + ); diff --git a/test/src/layouts/default/default.test.tsx b/test/src/layouts/default/default.test.tsx index 9c4ff236d..2abc1bed0 100644 --- a/test/src/layouts/default/default.test.tsx +++ b/test/src/layouts/default/default.test.tsx @@ -44,7 +44,7 @@ jest.mock('react-router-dom', () => ({ const mockUseSharedDataContext = jest .fn() - .mockReturnValue({stickyFooterState: [false, () => undefined]}); + .mockReturnValue({}); jest.mock('~/contexts/shared-data', () => ({ __esModule: true, diff --git a/test/src/pages/errata/errata-summary.test.tsx b/test/src/pages/errata/errata-summary.test.tsx index 9729d2c12..1d9fdac6b 100644 --- a/test/src/pages/errata/errata-summary.test.tsx +++ b/test/src/pages/errata/errata-summary.test.tsx @@ -4,7 +4,6 @@ import {waitFor, within} from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import ErrataSummaryLoader from '~/pages/errata-summary/errata-summary'; import MemoryRouter from '~/../../test/helpers/future-memory-router'; -import * as RC from '~/components/shell/router-context'; const searchStr = '/errata/?book=Anatomy%20and%20Physiology'; @@ -96,19 +95,13 @@ describe('errata-summary', () => { }); it('fails for unknown book', async () => { window.history.pushState('', '', '/errata/?book=Unknown%20Book'); - const fail = jest.fn(); - const mockUseContext = jest.spyOn(RC, 'default'); - - mockUseContext.mockReturnValue({fail} as any); // eslint-disable-line render( ); - await new Promise((resolve) => setTimeout(resolve, 10)); - expect(fail).toHaveBeenCalled(); - mockUseContext.mockReset(); + await screen.findByText('Unknown Book not found'); }); it('aborts if not book or errata ID is selected', () => { window.history.pushState('', '', '/errata/'); diff --git a/test/src/pages/interest.test.tsx b/test/src/pages/interest.test.tsx index da1378112..424b5b9fc 100644 --- a/test/src/pages/interest.test.tsx +++ b/test/src/pages/interest.test.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import {useLocation} from 'react-router-dom'; import {render, screen} from '@testing-library/preact'; import userEvent from '@testing-library/user-event'; import InterestPage from '~/pages/interest/interest'; @@ -7,8 +8,16 @@ import {MainClassContextProvider} from '~/contexts/main-class'; import {SharedDataContextProvider} from '~/contexts/shared-data'; import {LanguageContextProvider} from '~/contexts/language'; import * as CI from '~/components/contact-info/contact-info'; +import * as DH from '~/helpers/use-document-head'; jest.spyOn(CI, 'default').mockReturnValue(

Contact info

); +jest.spyOn(DH, 'default').mockReturnValue(); + +function Path() { + const {pathname} = useLocation(); + + return
{pathname}
; +} function Component() { return ( @@ -22,6 +31,7 @@ function Component() { > + @@ -32,6 +42,8 @@ function Component() { const user = userEvent.setup(); describe('interest form', () => { + const saveError = console.error; + it('renders', async () => { render(); const roleSelector = await screen.findByRole('combobox'); @@ -60,6 +72,26 @@ describe('interest form', () => { await user.click(screen.getByRole('checkbox', {name: 'Web search'})); await user.click(screen.getByRole('checkbox', {name: 'Email'})); + console.error = jest.fn(); + await user.click(screen.getByRole('button', {name: 'Submit'})); + expect(console.error).toHaveBeenCalled(); + console.error = saveError; + }); + it('navigates to previous page', async () => { + render(); + const roleSelector = await screen.findByRole('combobox'); + + await user.click(roleSelector); + const options = screen.getAllByRole('option'); + + await user.click( + options.find((o) => o.textContent === 'Student') as Element + ); + screen.findByText("Students don't need to fill out any forms", { + exact: false + }); + await user.click(screen.getByRole('button', {name: 'Go back'})); + expect(document.querySelector('.path')?.textContent).toBe('/details/books/college-algebra'); }); }); diff --git a/test/src/pages/webinars/main-page.test.tsx b/test/src/pages/webinars/main-page.test.tsx index 85a9da345..4b9542269 100644 --- a/test/src/pages/webinars/main-page.test.tsx +++ b/test/src/pages/webinars/main-page.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import {describe, expect, it} from '@jest/globals'; import {render, screen} from '@testing-library/preact'; import MemoryRouter from '~/../../test/helpers/future-memory-router'; -import {RouterContextProvider} from '~/components/shell/router-context'; import MainPage from '~/pages/webinars/main-page/main-page'; import * as UWC from '~/pages/webinars/webinar-context'; import * as UDH from '~/helpers/use-document-head'; @@ -11,9 +10,7 @@ import {pageData} from '../../data/webinars'; function Component() { return ( - - - + ); } diff --git a/test/src/pages/webinars/search-page.test.tsx b/test/src/pages/webinars/search-page.test.tsx index 4ac882751..fe8f3168f 100644 --- a/test/src/pages/webinars/search-page.test.tsx +++ b/test/src/pages/webinars/search-page.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import {describe, expect, it} from '@jest/globals'; import {render, screen} from '@testing-library/preact'; import MemoryRouter from '~/../../test/helpers/future-memory-router'; -import {RouterContextProvider} from '~/components/shell/router-context'; import SearchPage from '~/pages/webinars/search-page/search-page'; import * as UFD from '~/helpers/use-data'; import {upcomingWebinar} from '../../data/webinars'; @@ -11,9 +10,7 @@ import * as UWC from '~/pages/webinars/webinar-context'; function Component({term = ''}: {term?: string}) { return ( - - - + ); } diff --git a/test/src/pages/webinars/view-webinars-page.test.tsx b/test/src/pages/webinars/view-webinars-page.test.tsx index f86b4f427..57c0a058e 100644 --- a/test/src/pages/webinars/view-webinars-page.test.tsx +++ b/test/src/pages/webinars/view-webinars-page.test.tsx @@ -2,16 +2,13 @@ import React from 'react'; import {describe, expect, it} from '@jest/globals'; import {render, screen} from '@testing-library/preact'; import MemoryRouter from '~/../../test/helpers/future-memory-router'; -import {RouterContextProvider} from '~/components/shell/router-context'; import {upcomingWebinar} from '../../data/webinars'; import ViewWebinarsPage from '~/pages/webinars/view-webinars-page/view-webinars-page'; function Component(props: Parameters[0]) { return ( - - - + ); }