Skip to content

Commit

Permalink
[PR] Add ability for users to add custom pages
Browse files Browse the repository at this point in the history
  • Loading branch information
danielfdsilva authored Jun 19, 2023
2 parents ce036b3 + 8e7f0c4 commit 3e7ac89
Show file tree
Hide file tree
Showing 18 changed files with 305 additions and 42 deletions.
5 changes: 3 additions & 2 deletions app/scripts/components/about/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ function About() {
<ContentOverride with='aboutContent'>
<FoldProse>
<p>
This is the app about page, where you can find information about the whole app. To customize this content use Content Overrides.
This is the app about page, where you can find information about the
whole app. To customize this content use Content Overrides.
</p>
</FoldProse>
</ContentOverride>
</PageMainContent>
);
}

export default About;
export default About;
31 changes: 20 additions & 11 deletions app/scripts/components/common/blocks/images/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,26 @@ Caption.propTypes = {
export default function Image(props) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { align, caption, attrAuthor, ...propsWithoutAttrs } = props;
const imageAlign = align ? align : 'center';
return caption || attrAuthor ? (
// if it is an inline image with a caption
<Figure className={`align-${imageAlign}`}>
<img loading='lazy' {...propsWithoutAttrs} />
<Caption attrAuthor={attrAuthor} attrUrl={props.attrUrl}>
{caption}
</Caption>
</Figure>
) : (
<img loading='lazy' {...propsWithoutAttrs} />
if (caption || attrAuthor) {
const imageAlign = align ? align : 'center';
return (
// if it is an inline image with a caption
<Figure className={`align-${imageAlign}`}>
<img loading='lazy' {...propsWithoutAttrs} />
<Caption attrAuthor={attrAuthor} attrUrl={props.attrUrl}>
{caption}
</Caption>
</Figure>
);
}

const imageAlign = align ? align : 'left';
return (
<img
className={`img-align-${imageAlign}`}
loading='lazy'
{...propsWithoutAttrs}
/>
);
}

Expand Down
5 changes: 3 additions & 2 deletions app/scripts/components/common/mdx-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function MdxContent(props) {
Link: SmartLink
}}
>
<pageMdx.MdxContent />
<pageMdx.MdxContent {...(props.throughProps || {})} />
</MDXProvider>
);
}
Expand All @@ -53,7 +53,8 @@ function MdxContent(props) {
}

MdxContent.propTypes = {
loader: T.func
loader: T.func,
throughProps: T.object
};

export default MdxContent;
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled, { css } from 'styled-components';
import { Link, NavLink } from 'react-router-dom';
import { userPages, getOverride } from 'veda';
import {
glsp,
listReset,
media,
rgba,
themeVal,
visuallyHidden
} from '@devseed-ui/theme-provider';
import { reveal } from '@devseed-ui/animation';
import { Heading, Overline } from '@devseed-ui/typography';
import { ShadowScrollbar } from '@devseed-ui/shadow-scrollbar';
import { Button } from '@devseed-ui/button';
import { CollecticonHamburgerMenu } from '@devseed-ui/collecticons';
import {
CollecticonEllipsisVertical,
CollecticonHamburgerMenu
} from '@devseed-ui/collecticons';
import { DropMenu, DropMenuItem } from '@devseed-ui/dropdown';

import DropdownScrollable from './dropdown-scrollable';

import NasaLogo from './nasa-logo';
import GoogleForm from './google-form';
Expand All @@ -31,6 +39,8 @@ import { useMediaQuery } from '$utils/use-media-query';
import { HEADER_ID } from '$utils/use-sliding-sticky-header';
import { ComponentOverride } from '$components/common/page-overrides';

const rgbaFixed = rgba as any;

const appTitle = process.env.APP_TITLE;
const appVersion = process.env.APP_VERSION;

Expand Down Expand Up @@ -139,7 +149,7 @@ const PageTitleSecLink = styled(Link)`
`}
`;

const GlobalNav = styled.nav`
const GlobalNav = styled.nav<{ revealed: boolean }>`
position: fixed;
inset: 0 0 0 auto;
z-index: 900;
Expand Down Expand Up @@ -307,12 +317,18 @@ const GlobalMenuLink = styled(NavLink)`
${GlobalMenuLinkCSS}
`;

const DropMenuNavItem = styled(DropMenuItem)`
&.active {
background-color: ${rgbaFixed(themeVal('color.link'), 0.08)};
}
`;

function PageHeader() {
const { isMediumDown } = useMediaQuery();

const [globalNavRevealed, setGlobalNavRevealed] = useState(false);

const globalNavBodyRef = useRef(null);
const globalNavBodyRef = useRef<HTMLDivElement>(null);
// Click listener for the whole global nav body so we can close it when clicking
// the overlay on medium down media query.
const onGlobalNavClick = useCallback((e) => {
Expand Down Expand Up @@ -350,6 +366,7 @@ function PageHeader() {
? 'Close Global Navigation'
: 'Open Global Navigation'
}
// @ts-expect-error UI lib error. achromic-text does exit
variation='achromic-text'
fitting='skinny'
onClick={() => setGlobalNavRevealed((v) => !v)}
Expand Down Expand Up @@ -422,6 +439,11 @@ function PageHeader() {
<GoogleForm />
</li>
)}

<UserPagesMenu
onItemClick={closeNavOnClick}
isMediumDown={isMediumDown}
/>
</GlobalMenu>
</SectionsNavBlock>
</GlobalNavBodyInner>
Expand All @@ -433,3 +455,58 @@ function PageHeader() {
}

export default PageHeader;

function UserPagesMenu(props: {
isMediumDown: boolean;
onItemClick: () => void;
}) {
const { isMediumDown, onItemClick } = props;

if (!userPages.length) return <>{false}</>;

if (isMediumDown) {
return (
<>
{userPages.map((id) => {
const page = getOverride(id as any);
if (!page?.data.menu) return false;

return (
<li key={id}>
<GlobalMenuLink to={id} onClick={onItemClick}>
{page.data.menu}
</GlobalMenuLink>
</li>
);
})}
</>
);
}

return (
<DropdownScrollable
alignment='right'
triggerElement={(props) => (
// @ts-expect-error UI lib error. achromic-text does exit
<Button {...props} variation='achromic-text' fitting='skinny'>
<CollecticonEllipsisVertical meaningful title='View pages menu' />
</Button>
)}
>
<DropMenu>
{userPages.map((id) => {
const page = getOverride(id as any);
if (!page?.data.menu) return false;

return (
<li key={id}>
<DropMenuNavItem as={NavLink} to={id} data-dropdown='click.close'>
{page.data.menu}
</DropMenuNavItem>
</li>
);
})}
</DropMenu>
</DropdownScrollable>
);
}
13 changes: 9 additions & 4 deletions app/scripts/components/common/page-overrides.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function ComponentOverride(props: ComponentOverrideProps) {
const pageMdx = useMdxPageLoader(loader);

if (!loader) {
return children;
return <>{children}</>;
}

if (pageMdx.status === S_SUCCEEDED) {
Expand All @@ -30,11 +30,16 @@ export function ComponentOverride(props: ComponentOverrideProps) {
);
}

return null;
return <>{false}</>;
}

export function ContentOverride(props: ComponentOverrideProps) {
const loader = getOverride(props.with)?.content;
const { with: _with, children, ...rest } = props;
const loader = getOverride(_with)?.content;

return loader ? <MdxContent loader={loader} /> : <>{props.children}</>;
return loader ? (
<MdxContent loader={loader} throughProps={rest} />
) : (
<>{children}</>
);
}
35 changes: 35 additions & 0 deletions app/scripts/components/user-pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { useParams } from 'react-router';
import { getOverride } from 'veda';

import { LayoutProps } from '$components/common/layout-root';
import { PageMainContent } from '$styles/page';
import PageHero from '$components/common/page-hero';
import { FoldProse } from '$components/common/fold';
import { ContentOverride } from '$components/common/page-overrides';
import { resourceNotFound } from '$components/uhoh';

function UserPages(props: { id: any }) {
const page = getOverride(props.id);

const params = useParams();

if (!page) throw resourceNotFound();

return (
<PageMainContent>
<LayoutProps title='UserPages' />
<PageHero
title={page.data.title || 'Page title is missing'}
description={page.data.description}
/>
<ContentOverride with={props.id} {...params}>
<FoldProse>
<p>Content for this page comes from the relevant mdx file.</p>
</FoldProse>
</ContentOverride>
</PageMainContent>
);
}

export default UserPages;
11 changes: 11 additions & 0 deletions app/scripts/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render } from 'react-dom';
import T from 'prop-types';
import { BrowserRouter, Route, Routes, useLocation } from 'react-router-dom';
import { DevseedUiThemeProvider as DsTp } from '@devseed-ui/theme-provider';
import { userPages } from 'veda';

import { thematicRoutes } from './thematic-redirect';

Expand Down Expand Up @@ -35,6 +36,8 @@ const AnalysisResults = lazy(() => import('$components/analysis/results'));

const Sandbox = lazy(() => import('$components/sandbox'));

const UserPagesComponent = lazy(() => import('$components/user-pages'));

// Handle wrong types from devseed-ui.
const DevseedUiThemeProvider = DsTp as any;

Expand Down Expand Up @@ -113,6 +116,14 @@ function Root() {
{/* Legacy: Routes related to thematic areas redirect. */}
{thematicRoutes}

{userPages.map((p) => (
<Route
key={p}
path={p}
element={<UserPagesComponent id={p} />}
/>
))}

<Route path='*' element={<UhOh />} />
</Route>
</Routes>
Expand Down
9 changes: 9 additions & 0 deletions app/scripts/styles/content-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ const ContentBlockProse = styled(VarProse)`
text-align: center;
}
}
.img-align-right {
margin-left: auto;
}
.img-align-center {
margin-left: auto;
margin-right: auto;
}
`;

// assign displayName that a block can tell
Expand Down
35 changes: 35 additions & 0 deletions docs/content/CUSTOM_PAGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Custom pages

To adapt the Veda dashboard to the individual needs of you instance you can create additional content pages.

These pages are defined under the `pageOverrides` property of `veda.config.js`. The `key` should be the desired url of the page (staring with a forward slash `/`), and the value should be a path to the MDX file to load.

```js
pageOverrides: {
'/custom-page': './pages/custom.mdx'
}
```

The example above will make a page available at `/custom-page` with the content from the `custom.mdx` file.

![](./media/custom-page.png)

## Frontmatter and content

Each custom page has the following properties:

**menu**
`string`
The menu label for this page.

**title**
`string`
Title for this page shown on the header.

**description**
`string`
Brief optional description of this page, shown on the header.

The content of the custom pages can be written using the different blocks available. Check the [MDX Blocks documentation](./MDX_BLOCKS.md) to see all the possibilities.
If you are looking for something more custom, check the [creating complex overrides](./PAGE_OVERRIDES.md#creating-complex-overrides) section of PAGE_OVERRIDES.

11 changes: 8 additions & 3 deletions docs/content/PAGE_OVERRIDES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@ There are essentially 2 types of possible overrides:
- `Content Overrides` - allow you to change the default content of a page. Like with the different content types (discoveries, datasets), you'll have access to all [MDX_BLOCK.md](./MDX_BLOCKS.md). Depending on the content override you'll also be able to provide some frontmatter variables. The name of the override config variable will follow the `<name>Content` scheme.
- `Component Overrides` - allow you to alter a specific component of the app, by providing new javascript code for it (advanced usage). No Mdx Blocks are available.

> 🍀 Although it is not an override, custom pages are also defined under the `pageOverrides` key. See [CUSTOM_PAGES.md](./CUSTOM_PAGES.md) form more information.
The overrides are defined in the `veda.config.js` under `pageOverrides` by specifying the path to the mdx file to load.
These are the current available overrides:

```js
pageOverrides: {
// Type: Content override
aboutContent: '<file path>.mdx'

// There are currently no component overrides defined.
aboutContent: '<file path>.mdx',
homeContent: '<file path>.mdx',

// Type: Component override
headerBrand: '<file path>.mdx',
pageFooter: '<file path>.mdx',
}
```

Expand Down
Binary file added docs/content/media/custom-page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 3e7ac89

Please sign in to comment.