Skip to content

Core 901 add portal routing handler #2732

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 33 additions & 13 deletions src/app/components/jsx-helpers/raw-html.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,64 @@
import React from 'react';
import usePortalContext from '~/contexts/portal';

// Making scripts work, per https://stackoverflow.com/a/47614491/392102
function activateScripts(el: HTMLElement) {
const scripts: HTMLScriptElement[] = Array.from(el.querySelectorAll('script'));
const processOne = (() => {
const scripts: HTMLScriptElement[] = Array.from(
el.querySelectorAll('script')
);
const processOne = () => {
const s = scripts.shift();

if (!s) {
return;
}
const newScript = document.createElement('script');
const p = (s.src) ? new Promise((resolve) => {
newScript.onload = resolve;
}) : Promise.resolve();
const p = s.src
? new Promise((resolve) => {
newScript.onload = resolve;
})
: Promise.resolve();

Array.from(s.attributes)
.forEach((a) => newScript.setAttribute(a.name, a.value));
Array.from(s.attributes).forEach((a) =>
newScript.setAttribute(a.name, a.value)
);
if (s.textContent) {
newScript.appendChild(document.createTextNode(s.textContent));
}
newScript.async = false;
s.parentNode?.replaceChild(newScript, s);

p.then(processOne);
});
};

processOne();
}

type RawHTMLArgs = {Tag?: string, html?: TrustedHTML, embed?: boolean} & React.HTMLAttributes<HTMLDivElement>;
type RawHTMLArgs = {
Tag?: string;
html?: TrustedHTML;
embed?: boolean;
} & React.HTMLAttributes<HTMLDivElement>;

export default function RawHTML({Tag='div', html, embed=false, ...otherProps}: RawHTMLArgs) {
export default function RawHTML({
Tag = 'div',
html,
embed = false,
...otherProps
}: RawHTMLArgs) {
const ref = React.useRef<HTMLElement>();
const {rewriteLinks} = usePortalContext();

React.useEffect(() => {
if (embed && ref.current) {
activateScripts(ref.current);
}
});
return (
React.createElement(Tag, {ref, dangerouslySetInnerHTML: {__html: html}, ...otherProps})
);
React.useLayoutEffect(() => rewriteLinks?.(ref.current as HTMLElement), [rewriteLinks]);

return React.createElement(Tag, {
ref,
dangerouslySetInnerHTML: {__html: html},
...otherProps
});
}
8 changes: 8 additions & 0 deletions src/app/components/list-of-links/list-of-links.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.list-of-links {
list-style-type: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
10 changes: 10 additions & 0 deletions src/app/components/list-of-links/list-of-links.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import './list-of-links.scss';

export default function ListOfLinks({children}: React.PropsWithChildren<object>) {
return (
<ul className="list-of-links">
{React.Children.toArray(children).map((c, i) => (<li key={i}>{c}</li>))}
</ul>
);
}
21 changes: 21 additions & 0 deletions src/app/components/shell/portal-router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import {RoutesAlsoInPortal} from './router';
import usePortalContext from '~/contexts/portal';
import {useParams} from 'react-router-dom';

export default function PortalRouter() {
// If we want to validate the portal name, that can be done here
const {portal, '*': rest} = useParams();
const {setPortal} = usePortalContext();

React.useEffect(() => {
// It seems that the path "/press" in particular winds up matching
// the portal-router Route, so we need to ensure it's really a portal
// route by verifying that there's something after the :portal param
if (rest) {
setPortal(portal as string);
}
}, [rest, portal, setPortal]);

return <RoutesAlsoInPortal />;
}
11 changes: 10 additions & 1 deletion src/app/components/shell/router-helpers/fallback-to.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import usePageData from '~/helpers/use-page-data';
import loadable from 'react-loadable';
import LoadingPlaceholder from '~/components/loading-placeholder/loading-placeholder';
import useLayoutContext from '~/contexts/layout';
import usePortalContext from '~/contexts/portal';

const FallbackToGeneralPage = loadable({
loader: () => import('./fallback-to-general.js'),
Expand All @@ -22,7 +23,7 @@ export default function FallbackTo({name}) {
}

export const isFlexPage = (data) => (
typeof data.meta?.type === 'string' &&
typeof data?.meta?.type === 'string' &&
['pages.FlexPage', 'pages.RootPage'].includes(data.meta.type)
);

Expand All @@ -31,6 +32,14 @@ function LoadedPage({data, name}) {
const {setLayoutParameters, layoutParameters} = useLayoutContext();
const hasError = 'error' in data;
const isFlex = !hasError && isFlexPage(data);
const isPortal = isFlex && layoutParameters.name === 'landing';
const {setPortal} = usePortalContext();

React.useEffect(() => {
if (isPortal) {
setPortal(name);
}
}, [isPortal, name, setPortal]);

React.useEffect(() => {
if (isFlex) {
Expand Down
77 changes: 45 additions & 32 deletions src/app/components/shell/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useRouterContext, {RouterContextProvider} from './router-context';
import loadable from 'react-loadable';
import LoadingPlaceholder from '~/components/loading-placeholder/loading-placeholder';
import useLayoutContext, { LayoutContextProvider } from '~/contexts/layout';
import PortalRouter from './portal-router';
import './skip-to-content.scss';

function useAnalyticsPageView() {
Expand All @@ -28,6 +29,7 @@ const Fallback = loadable({
loader: () => import('./router-helpers/fallback-to.js'),
loading: () => <h1>...Loading</h1>
});

const Error404 = loadable({
loader: () => import('~/pages/404/404'),
loading: () => <h1>404</h1>
Expand Down Expand Up @@ -56,14 +58,14 @@ function useLoading(name) {
}

function DefaultLayout({children}) {
const {portal} = useParams();
const {setLayoutParameters, layoutParameters} = useLayoutContext();

React.useEffect(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was unable to see that this did anything.

() => setLayoutParameters(),
[setLayoutParameters]
);
if (portal) {
setLayoutParameters({name: 'landing', data: layoutParameters.data});
}

return layoutParameters.name === 'default' ? children : null;
return layoutParameters.name ? children : null;
}

function usePage(name) {
Expand Down Expand Up @@ -133,39 +135,50 @@ function MainRoutes() {
<Layout>
<Routes>
<Route path="/" element={<ImportedPage name="home" />} />
{
FOOTER_PAGES.map(
(path) => <Route path={path} key={path} element={<ImportedPage name="footer-page" />} />
)
}
<Route path="/errata/" element={<ImportedPage name="errata-summary" />} />
<Route path="/errata/form/" element={<ImportedPage name="errata-form" />} />
<Route path="/errata/*" element={<ImportedPage name="errata-detail" />} />
<Route path="/details/books/:title" element={<ImportedPage name="details" />} />
<Route path="/details/:title" element={<RedirectToCanonicalDetailsPage />} />
<Route path="/details/" element={<Navigate to="/subjects" replace />} />
<Route path="/books/:title" element={<RedirectToCanonicalDetailsPage />} />
<Route path="/textbooks/:title" element={<RedirectToCanonicalDetailsPage />} />
<Route path="/subjects/*" element={<ImportedPage name="subjects" />} />
<Route path="/k12/*" element={<ImportedPage name="k12" />} />
<Route path="/blog/*" element={<ImportedPage name="blog" />} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm wondering if there are any of these, like k12, blog, general, that we explicitly don't want to work in the portal. i guess probably they're not hurting anything by being in there

<Route path="/webinars/*" element={<ImportedPage name="webinars" />} />
<Route path="/general/*" element={<ImportedPage name="general" />} />
<Route path="/confirmation/*" element={<ImportedPage name="confirmation" />} />
<Route path="/campaign/*" element={<ImportedPage name="campaign" />} />
<Route path="/press/*" element={<ImportedPage name="press" />} />
<Route
path="/edtech-partner-program"
element={<ImportedPage name="/openstax-ally-technology-partner-program" />}
/>
<Route path="/:name/" element={<TopLevelPage />} />
<Route path="/:name/*" element={<Error404 />} />
</Routes>
<RoutesAlsoInPortal />
<Routes>
<Route path="/:portal/*" element={<PortalRouter />} />
<Route element={<h1>Fell through</h1>} />
</Routes>
</Layout>
);
}


export function RoutesAlsoInPortal() {
return (
<Routes>
{
FOOTER_PAGES.map(
(path) => <Route path={path} key={path} element={<ImportedPage name="footer-page" />} />
)
}
<Route path="/errata/" element={<ImportedPage name="errata-summary" />} />
<Route path="/errata/form/" element={<ImportedPage name="errata-form" />} />
<Route path="/errata/*" element={<ImportedPage name="errata-detail" />} />
<Route path="/details/books/:title" element={<ImportedPage name="details" />} />
<Route path="/details/:title" element={<RedirectToCanonicalDetailsPage />} />
<Route path="/details/" element={<Navigate to="/subjects" replace />} />
<Route path="/books/:title" element={<RedirectToCanonicalDetailsPage />} />
<Route path="/textbooks/:title" element={<RedirectToCanonicalDetailsPage />} />
<Route path="/subjects/*" element={<ImportedPage name="subjects" />} />
<Route path="/k12/*" element={<ImportedPage name="k12" />} />
<Route path="/blog/*" element={<ImportedPage name="blog" />} />
<Route path="/webinars/*" element={<ImportedPage name="webinars" />} />
<Route path="/general/*" element={<ImportedPage name="general" />} />
<Route path="/confirmation/*" element={<ImportedPage name="confirmation" />} />
<Route path="/campaign/*" element={<ImportedPage name="campaign" />} />
<Route path="/press/*" element={<ImportedPage name="press" />} />
<Route
path="/edtech-partner-program"
element={<ImportedPage name="/openstax-ally-technology-partner-program" />}
/>
<Route path="/:name/" element={<TopLevelPage />} />
</Routes>
);
}

function doSkipToContent(event) {
event.preventDefault();
const mainEl = document.getElementById('main');
Expand Down
9 changes: 6 additions & 3 deletions src/app/components/shell/shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {BrowserRouter, Routes, Route} from 'react-router-dom';
import {SharedDataContextProvider} from '../../contexts/shared-data';
import JITLoad from '~/helpers/jit-load';
import {SalesforceContextProvider} from '~/contexts/salesforce';
import {PortalContextProvider} from '~/contexts/portal';

import Error404 from '~/pages/404/404';

Expand All @@ -14,9 +15,11 @@ function AppContext({children}: React.PropsWithChildren<object>) {
<SharedDataContextProvider>
<UserContextProvider>
<LanguageContextProvider>
<SubjectCategoryContextProvider>
{children}
</SubjectCategoryContextProvider>
<PortalContextProvider>
<SubjectCategoryContextProvider>
{children}
</SubjectCategoryContextProvider>
</PortalContextProvider>
</LanguageContextProvider>
</UserContextProvider>
</SharedDataContextProvider>
Expand Down
24 changes: 24 additions & 0 deletions src/app/contexts/portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import buildContext from '~/components/jsx-helpers/build-context';

function useContextValue() {
const [portal, setPortal] = React.useState('');
const portalPrefix = portal ? `/${portal}` : '';
const rewriteLinks = React.useCallback((container: HTMLElement) => {
if (!portalPrefix) {return;}
const linkNodes = container.querySelectorAll('a[href^="/"]');

for (const node of linkNodes) {
const href = node.getAttribute('href');

node.setAttribute('href', `${portalPrefix}${href}`);
}
},
[portalPrefix]);

return {portalPrefix, setPortal, rewriteLinks};
}

const {useContext, ContextProvider} = buildContext({useContextValue});

export {useContext as default, ContextProvider as PortalContextProvider};
9 changes: 0 additions & 9 deletions src/app/layouts/default/footer/footer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,6 @@ $lower-footer: #3b3b3b;
margin: 0;
}

.list-of-links {
list-style-type: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.mission {
grid-area: mission;

Expand Down
Loading