diff --git a/Dockerfile b/Dockerfile
index 3b3f43cc..7dc8da41 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -45,7 +45,8 @@ ENV VITE_PORTAL_SERVER_URL=$VITE_PORTAL_SERVER_URL \
VITE_HOME_IMAGE_URL=$VITE_HOME_IMAGE_URL \
VITE_APIS_IMAGE_URL=$VITE_APIS_IMAGE_URL \
VITE_LOGO_IMAGE_URL=$VITE_LOGO_IMAGE_URL \
- VITE_COMPANY_NAME=$VITE_COMPANY_NAME
+ VITE_COMPANY_NAME=$VITE_COMPANY_NAME \
+ VITE_CUSTOM_PAGES=$VITE_CUSTOM_PAGES
# Copy the server files, (this includes the UI build).
WORKDIR /app
@@ -70,4 +71,5 @@ ENTRYPOINT VITE_PORTAL_SERVER_URL=$VITE_PORTAL_SERVER_URL \
VITE_APIS_IMAGE_URL=$VITE_APIS_IMAGE_URL \
VITE_LOGO_IMAGE_URL=$VITE_LOGO_IMAGE_URL \
VITE_COMPANY_NAME=$VITE_COMPANY_NAME \
+ VITE_CUSTOM_PAGES=$VITE_CUSTOM_PAGES \
node ./bin/www
diff --git a/Makefile b/Makefile
index ccfdb8fc..eae3def5 100644
--- a/Makefile
+++ b/Makefile
@@ -104,6 +104,13 @@ ifneq ($(VITE_COMPANY_NAME),)
else ifneq ($(COMPANY_NAME),)
UI_ARGS += VITE_COMPANY_NAME=$(COMPANY_NAME)
endif
+#
+# CUSTOM PAGES
+ifneq ($(VITE_CUSTOM PAGES),)
+ UI_ARGS += VITE_CUSTOM PAGES=$(VITE_CUSTOM_PAGES)
+else ifneq ($(CUSTOM_PAGES),)
+ UI_ARGS += VITE_CUSTOM_PAGES=$(CUSTOM_PAGES)
+endif
diff --git a/README.md b/README.md
index fa8203bb..cb201135 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ This is an example Solo.io Gloo Platform Dev Portal frontend app, built with [Vi
2. Build your image.
```sh
- docker build -t "your-image-name"
+ docker build -t "your-image-name" .
```
3. Push your image:
@@ -137,6 +137,13 @@ You can add these environment variables to a `.env.local` file in the `projects/
- `VITE_HOME_IMAGE_URL` - This is an optional parameter to set the image URL on the home page.
- `VITE_APIS_IMAGE_URL` - This is an optional parameter to set the image URL on the apis page.
- `VITE_LOGO_IMAGE_URL` - This is an optional parameter to set the image URL for the logo in the upper left.
+- `VITE_CUSTOM_PAGES` - This is an optional value that describes Markdown or HTML custom pages that have been added to the `projects/ui/src/public` folder. In order to test this feature out out with the provided examples, set your `VITE_CUSTOM_PAGES` value to:
+ ```
+ '[{"title": "Markdown Example", "path": "/pages/markdown-example.md"}, {"title": "HTML Example", "path": "/pages/html-example.html"}]'
+ ```
+ When the website is opened, there should be two new pages in the top navigation bar.
+ 
+ The custom page's `path` property must be publicly accessible and end with `.md` or `.html`.
#### Environment Variables for PKCE Authorization Flow
diff --git a/changelog/v0.0.36/custom-pages.yaml b/changelog/v0.0.36/custom-pages.yaml
new file mode 100644
index 00000000..504cc325
--- /dev/null
+++ b/changelog/v0.0.36/custom-pages.yaml
@@ -0,0 +1,5 @@
+changelog:
+ - type: FIX
+ issueLink: https://github.com/solo-io/solo-projects/issues/6860
+ description: >-
+ Adds the ability for users to create custom pages that show up in the UI.
diff --git a/projects/ui/package.json b/projects/ui/package.json
index 17e5eeb8..dbed14df 100644
--- a/projects/ui/package.json
+++ b/projects/ui/package.json
@@ -25,6 +25,7 @@
"@mantine/hooks": "^6.0.6",
"@types/color": "^3.0.6",
"color": "^4.2.3",
+ "highlight.js": "^11.10.0",
"mobx": "^6.8.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/projects/ui/public/pages/gg-logo.png b/projects/ui/public/pages/gg-logo.png
new file mode 100644
index 00000000..053935de
Binary files /dev/null and b/projects/ui/public/pages/gg-logo.png differ
diff --git a/projects/ui/public/pages/html-example.html b/projects/ui/public/pages/html-example.html
new file mode 100644
index 00000000..dfc978a0
--- /dev/null
+++ b/projects/ui/public/pages/html-example.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+ Example HTML Page
+
+ Section 1
+
+
+ This is an example custom page.
+ Feel free to update this to your needs.
+
+
+ Section 2
+
+ Any HTML content can go here.
+
+ Here is an image:
+
+
+
+
+
+ Click me!
+
+
+
diff --git a/projects/ui/public/pages/markdown-example.md b/projects/ui/public/pages/markdown-example.md
new file mode 100644
index 00000000..02388cc7
--- /dev/null
+++ b/projects/ui/public/pages/markdown-example.md
@@ -0,0 +1,37 @@
+# Example Markdown Page (#)
+
+This is a custom Markdown page test.
+
+## Section 1 (##)
+
+- Supports bullet points
+- Supports bullet points
+
+### 1.1 (###)
+
+Testing that **Bold works** here.
+
+#### 1.1.1 (####)
+
+Testing that _Italics works_ here.
+
+##### 1.1.1 (#####)
+
+Links work: [www.solo.io](www.solo.io)
+
+1. Numbered lists work
+2. test
+3. test
+
+Images work:
+
+
+
+And code does too:
+
+```ts
+const x = 123;
+function y() {
+ return x + 5;
+}
+```
diff --git a/projects/ui/src/Components/ApiDetails/gloo-gateway-components/DocsTab/DocsTabContent.tsx b/projects/ui/src/Components/ApiDetails/gloo-gateway-components/DocsTab/DocsTabContent.tsx
index dd968a04..a1d83b3d 100644
--- a/projects/ui/src/Components/ApiDetails/gloo-gateway-components/DocsTab/DocsTabContent.tsx
+++ b/projects/ui/src/Components/ApiDetails/gloo-gateway-components/DocsTab/DocsTabContent.tsx
@@ -1,25 +1,7 @@
-import { css } from "@emotion/react";
-import styled from "@emotion/styled";
import { Box } from "@mantine/core";
-import Markdown from "react-markdown";
-import remarkGfm from "remark-gfm";
import { ApiVersion } from "../../../../Apis/api-types";
import { CardStyles } from "../../../../Styles/shared/Card.style";
-
-const MarkdownOuterContainer = styled.div(
- ({ theme }) => css`
- padding: 30px;
- * {
- margin: revert;
- padding: revert;
- font-family: revert;
- font-weight: revert;
- }
- blockquote p {
- color: ${theme.augustGrey};
- }
- `
-);
+import MarkdownRenderer from "../../../Common/MarkdownRenderer";
const DocsTabContent = ({
selectedApiVersion,
@@ -29,11 +11,7 @@ const DocsTabContent = ({
return (
-
-
- {selectedApiVersion.documentation}
-
-
+
);
diff --git a/projects/ui/src/Components/App.tsx b/projects/ui/src/Components/App.tsx
index 9dc89cb4..c0315548 100644
--- a/projects/ui/src/Components/App.tsx
+++ b/projects/ui/src/Components/App.tsx
@@ -1,6 +1,7 @@
import { Global, ThemeProvider } from "@emotion/react";
import { MantineProvider } from "@mantine/core";
import { AppContextProvider } from "../Context/AppContext";
+import { AppUtilsContextProvider } from "../Context/AppUtilsContext";
import { defaultTheme, globalStyles } from "../Styles";
import { mantineThemeOverride } from "../Styles/global-styles/mantine-theme";
import PortalServerTypeChecker from "../Utility/PortalServerTypeChecker";
@@ -15,17 +16,19 @@ export function App() {
return (
-
-
+
+
+
-
-
-
-
+
+
+
+
+
);
}
diff --git a/projects/ui/src/Components/AppContentRoutes.tsx b/projects/ui/src/Components/AppContentRoutes.tsx
index aaec904c..e7d89cfb 100644
--- a/projects/ui/src/Components/AppContentRoutes.tsx
+++ b/projects/ui/src/Components/AppContentRoutes.tsx
@@ -4,9 +4,11 @@ import { Navigate, Route, Routes } from "react-router-dom";
import { AppContext } from "../Context/AppContext";
import { AuthContext } from "../Context/AuthContext";
import {
+ customPages,
oidcAuthCodeConfigCallbackPath,
oidcAuthCodeConfigLogoutPath,
} from "../user_variables.tmplr";
+import { getCustomPagePath } from "../Utility/utility";
import AdminSubscriptionsPage from "./AdminSubscriptions/AdminSubscriptionsPage";
import AdminTeamsPage from "./AdminTeams/AdminTeamsPage";
import { ApiDetailsPage } from "./ApiDetails/ApiDetailsPage";
@@ -15,6 +17,7 @@ import { AppsPage } from "./Apps/AppsPage";
import AppDetailsPage from "./Apps/Details/AppDetailsPage";
import { ErrorBoundary } from "./Common/ErrorBoundary";
import LoggedOut from "./Common/LoggedOut";
+import CustomPageLanding from "./CustomPage/CustomPageLanding";
import { HomePage } from "./Home/HomePage";
import { Footer } from "./Structure/Footer";
import TeamDetailsPage from "./Teams/Details/TeamDetailsPage";
@@ -173,6 +176,22 @@ function AppContentRoutes() {
)}
>
)}
+ {customPages.map((page) => (
+ <>
+ {getCustomPagePath(page)}
+
+
+
+ }
+ />
+ >
+ ))}
diff --git a/projects/ui/src/Components/Common/MarkdownRenderer.tsx b/projects/ui/src/Components/Common/MarkdownRenderer.tsx
new file mode 100644
index 00000000..b632d46a
--- /dev/null
+++ b/projects/ui/src/Components/Common/MarkdownRenderer.tsx
@@ -0,0 +1,81 @@
+import { css } from "@emotion/react";
+import styled from "@emotion/styled";
+import hljs from "highlight.js";
+import { useEffect, useRef } from "react";
+import Markdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import { borderRadiusConstants } from "../../Styles/constants";
+
+export const MarkdownOuterContainer = styled.div(
+ ({ theme }) => css`
+ padding: 30px;
+ * {
+ margin: revert;
+ padding: revert;
+ font-family: revert;
+ font-weight: revert;
+ }
+ blockquote p {
+ color: ${theme.augustGrey};
+ }
+ pre:has(code) {
+ padding: 1rem 2rem;
+ border-radius: ${borderRadiusConstants.small};
+ width: 100%;
+ background-color: #1c1b1b;
+ }
+ em {
+ font-style: italic;
+ }
+ a {
+ text-decoration: underline;
+ }
+ h1 {
+ font-size: 2rem;
+ }
+ h2 {
+ font-size: 1.7rem;
+ }
+ h3 {
+ font-size: 1.5rem;
+ }
+ h4 {
+ font-size: 1.2rem;
+ }
+ h5 {
+ font-size: 1rem;
+ }
+ `
+);
+
+const MarkdownRenderer = ({ markdown }: { markdown: string }) => {
+ const mdContainerRef = useRef(null);
+
+ // Highlight the content when it's rendered.
+ useEffect(() => {
+ if (!markdown || !mdContainerRef.current) {
+ return;
+ }
+ // Highlight each code element.
+ // This is faster than doing `hljs.highlightAll()`.
+ const codeElements = mdContainerRef.current.querySelectorAll("code");
+ for (let i = 0; i < codeElements.length; i++) {
+ hljs.highlightElement(codeElements[i]);
+ }
+ return () => {
+ // If this "data-highlighted" attribute isn't reset, it may not
+ // highlight the code correctly when the page is revisited.
+ for (let i = 0; i < codeElements.length; i++) {
+ codeElements[i]?.removeAttribute("data-highlighted");
+ }
+ };
+ }, [markdown, mdContainerRef.current]);
+
+ return (
+
+ {markdown}
+
+ );
+};
+
+export default MarkdownRenderer;
diff --git a/projects/ui/src/Components/CustomPage/Content/CustomHtmlPage.tsx b/projects/ui/src/Components/CustomPage/Content/CustomHtmlPage.tsx
new file mode 100644
index 00000000..09ce177d
--- /dev/null
+++ b/projects/ui/src/Components/CustomPage/Content/CustomHtmlPage.tsx
@@ -0,0 +1,29 @@
+import { useContext } from "react";
+import { AppUtilsContext } from "../../../Context/AppUtilsContext";
+import { CustomPage } from "../../../user_variables.tmplr";
+import { footerHeightPx } from "../../Structure/Footer";
+import { headerHeightPx } from "../../Structure/Header.style";
+
+const CustomHtmlPage = ({
+ customPage,
+ customPageUrl,
+}: {
+ customPage: CustomPage;
+ customPageUrl: string;
+}) => {
+ const { windowInnerWidth, windowInnerHeight } = useContext(AppUtilsContext);
+
+ return (
+
+ );
+};
+
+export default CustomHtmlPage;
diff --git a/projects/ui/src/Components/CustomPage/Content/CustomMarkdownPage.tsx b/projects/ui/src/Components/CustomPage/Content/CustomMarkdownPage.tsx
new file mode 100644
index 00000000..ba548597
--- /dev/null
+++ b/projects/ui/src/Components/CustomPage/Content/CustomMarkdownPage.tsx
@@ -0,0 +1,31 @@
+import { Box } from "@mantine/core";
+import { GridCardStyles } from "../../../Styles/shared/GridCard.style";
+import { CustomPage } from "../../../user_variables.tmplr";
+import Breadcrumbs from "../../Common/Breadcrumbs";
+import MarkdownRenderer from "../../Common/MarkdownRenderer";
+import { PageContainer } from "../../Common/PageContainer";
+
+const CustomMarkdownPage = ({
+ customPage,
+ customPageContent,
+}: {
+ customPage: CustomPage;
+ customPageContent: string;
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CustomMarkdownPage;
diff --git a/projects/ui/src/Components/CustomPage/CustomPageLanding.tsx b/projects/ui/src/Components/CustomPage/CustomPageLanding.tsx
new file mode 100644
index 00000000..d02d694a
--- /dev/null
+++ b/projects/ui/src/Components/CustomPage/CustomPageLanding.tsx
@@ -0,0 +1,111 @@
+import { Code } from "@mantine/core";
+import { useEffect, useMemo, useState } from "react";
+import { useLocation } from "react-router-dom";
+import { customPages } from "../../user_variables.tmplr";
+import { getCustomPagePath } from "../../Utility/utility";
+import { EmptyData } from "../Common/EmptyData";
+import { Loading } from "../Common/Loading";
+
+import styled from "@emotion/styled";
+import CustomHtmlPage from "./Content/CustomHtmlPage";
+import CustomMarkdownPage from "./Content/CustomMarkdownPage";
+
+type CustomPageType = "md" | "html" | "loading" | "unsupported";
+
+const StyledCustomPageContainer = styled.div`
+ margin: 60px;
+`;
+
+//
+// region Component
+//
+const CustomPageLanding = () => {
+ const location = useLocation();
+
+ // Find our custom page object.
+ const customPage = useMemo(() => {
+ return customPages.find(
+ (page) => getCustomPagePath(page) === location.pathname
+ );
+ }, [location.pathname]);
+
+ // Get the page type.
+ const customPageType = useMemo(() => {
+ if (!customPage) {
+ return "loading";
+ }
+ const lowercasePath = customPage.path.toLowerCase();
+ if (lowercasePath.endsWith(".md")) {
+ return "md";
+ }
+ if (lowercasePath.endsWith(".html")) {
+ return "html";
+ }
+ return "unsupported";
+ }, [customPage?.path]);
+
+ // Fetch custom page content
+ const [customPageContent, setCustomPageContent] = useState();
+ useEffect(() => {
+ if (!customPage) {
+ return;
+ }
+ (async () => {
+ const newPageContent = await (await fetch(customPage.path)).text();
+ setCustomPageContent(newPageContent);
+ })();
+ }, [customPage, setCustomPageContent]);
+
+ //
+ // region Render
+ //
+ if (!customPage) {
+ return (
+
+ ;
+
+ );
+ }
+ if (customPageType === "loading") {
+ return (
+
+
+
+ );
+ }
+ if (customPageType === "unsupported") {
+ return (
+
+
+ Markdown and HTML custom pages are supported.
+
+ To view this page, update the path
for this custom page
+ so that it ends in .md
or .html
.
+
+
+ );
+ }
+ if (!customPageContent) {
+ return (
+
+
+ To view this page, make sure that your custom page is publicly
+ accessible at {customPage.path}
.
+
+
+ );
+ }
+ if (customPageType === "md") {
+ return (
+
+ );
+ }
+ return (
+
+ );
+};
+
+export default CustomPageLanding;
diff --git a/projects/ui/src/Components/Structure/BasicAuthVariant/Header.tsx b/projects/ui/src/Components/Structure/BasicAuthVariant/Header.tsx
index 0842edc4..34d817f7 100644
--- a/projects/ui/src/Components/Structure/BasicAuthVariant/Header.tsx
+++ b/projects/ui/src/Components/Structure/BasicAuthVariant/Header.tsx
@@ -1,12 +1,22 @@
-import { useContext, useMemo } from "react";
+import { Box, Popover } from "@mantine/core";
+import { useContext, useMemo, useState } from "react";
import { Link, NavLink, useLocation } from "react-router-dom";
+import { Icon } from "../../../Assets/Icons";
import { ReactComponent as Logo } from "../../../Assets/logo.svg";
import { AppContext } from "../../../Context/AppContext";
import { AuthContext } from "../../../Context/AuthContext";
-import { logoImageURL } from "../../../user_variables.tmplr";
+import { colors } from "../../../Styles";
+import {
+ CustomPage,
+ customPages,
+ logoImageURL,
+} from "../../../user_variables.tmplr";
+import { getCustomPagePath } from "../../../Utility/utility";
import { ErrorBoundary } from "../../Common/ErrorBoundary";
import { HeaderStyles } from "../Header.style";
-import HeaderSectionLoggedIn from "./HeaderSectionLoggedIn";
+import HeaderSectionLoggedIn, {
+ StyledUserDropdown,
+} from "./HeaderSectionLoggedIn";
import HeaderSectionLoggedOut from "./HeaderSectionLoggedOut";
if (!window.isSecureContext) {
@@ -17,42 +27,30 @@ if (!window.isSecureContext) {
);
}
+const useInArea = (paths: string[]) => {
+ const routerLocation = useLocation();
+ return useMemo(() => {
+ return paths.some((s) => {
+ return (
+ routerLocation.pathname.includes(s) ||
+ routerLocation.pathname.includes(getCustomPagePath(s))
+ );
+ });
+ }, [routerLocation.pathname, paths]);
+};
+
/**
* MAIN COMPONENT
**/
export function Header() {
const { isAdmin } = useContext(AuthContext);
- const routerLocation = useLocation();
const { isLoggedIn } = useContext(AuthContext);
- const inArea = (paths: string[]) => {
- return paths.some((s) => routerLocation.pathname.includes(s));
- };
-
- const inAdminTeamsArea = useMemo(
- () => inArea(["/admin/teams"]),
- [routerLocation.pathname]
- );
-
- const inAdminSubscriptionsArea = useMemo(
- () => inArea(["/admin/subscriptions"]),
- [routerLocation.pathname]
- );
-
- const inAPIsArea = useMemo(
- () => inArea(["/apis", "/api-details/"]),
- [routerLocation.pathname]
- );
-
- const inAppsArea = useMemo(
- () => inArea(["/apps", "/app-details/"]),
- [routerLocation.pathname]
- );
-
- const inTeamsArea = useMemo(
- () => inArea(["/teams", "/team-details/"]),
- [routerLocation.pathname]
- );
+ const inAdminTeamsArea = useInArea(["/admin/teams"]);
+ const inAdminSubscriptionsArea = useInArea(["/admin/subscriptions"]);
+ const inAPIsArea = useInArea(["/apis", "/api-details/"]);
+ const inAppsArea = useInArea(["/apps", "/app-details/"]);
+ const inTeamsArea = useInArea(["/teams", "/team-details/"]);
const { pageContentIsWide } = useContext(AppContext);
@@ -139,6 +137,8 @@ export function Header() {
>
)}
+
+
{isLoggedIn ? (
@@ -152,3 +152,76 @@ export function Header() {
);
}
+
+const CustomPagesNavSection = () => {
+ const [opened, setOpened] = useState(false);
+ const inAnyCustomPageArea = useInArea(
+ customPages?.map((page) => page.path) ?? []
+ );
+
+ if (!customPages.length) {
+ return null;
+ }
+ return (
+
+
+
+ setOpened(!opened)}
+ >
+
+ Navigation
+
+
+
+
+
+ {customPages.map((page) => (
+ setOpened(false)}
+ />
+ ))}
+
+
+
+ );
+};
+
+const CustomPageNavLink = ({
+ page,
+ onClick,
+}: {
+ page: CustomPage;
+ onClick: () => void;
+}) => {
+ const onThisPage = useInArea([page.path]);
+ return (
+
+ {page.title}
+
+ );
+};
diff --git a/projects/ui/src/Components/Structure/Footer.tsx b/projects/ui/src/Components/Structure/Footer.tsx
index 5870c414..d17d966a 100644
--- a/projects/ui/src/Components/Structure/Footer.tsx
+++ b/projects/ui/src/Components/Structure/Footer.tsx
@@ -4,12 +4,14 @@ import { useContext } from "react";
import { AppContext } from "../../Context/AppContext";
import { ContentWidthDiv } from "../../Styles/ContentWidthHelpers";
+export const footerHeightPx = 40;
+
const FooterContainer = styled.footer(
({ theme }) => css`
margin-bottom: 40px;
grid-area: footer;
width: 100%;
- height: 40px;
+ height: ${footerHeightPx}px;
background: ${theme.marchGrey};
color: ${theme.augustGrey};
display: block;
diff --git a/projects/ui/src/Components/Structure/Header.style.tsx b/projects/ui/src/Components/Structure/Header.style.tsx
index 450566bb..543388ab 100644
--- a/projects/ui/src/Components/Structure/Header.style.tsx
+++ b/projects/ui/src/Components/Structure/Header.style.tsx
@@ -2,9 +2,11 @@ import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { ContentWidthNav } from "../../Styles/ContentWidthHelpers";
+export const headerHeightPx = 90;
+
export namespace HeaderStyles {
export const StyledLogoImg = styled.img`
- height: 90px;
+ height: ${headerHeightPx}px;
padding: 5px 0px;
`;
@@ -12,8 +14,9 @@ export namespace HeaderStyles {
({ theme }) => css`
grid-area: header;
width: 100%;
- height: 90px;
+ height: ${headerHeightPx}px;
background: white;
+ border-bottom: 1px solid ${theme.marchGrey};
box-shadow: #253e580b 0px 2px 8px;
// These are the hover/active styles for the links in the main top bar,
@@ -118,6 +121,7 @@ export namespace HeaderStyles {
.userHolder {
display: flex;
align-items: center;
+ margin-top: -4px;
svg.userCircle {
width: 40px;
diff --git a/projects/ui/src/Context/AppUtilsContext.tsx b/projects/ui/src/Context/AppUtilsContext.tsx
new file mode 100644
index 00000000..5bf5786b
--- /dev/null
+++ b/projects/ui/src/Context/AppUtilsContext.tsx
@@ -0,0 +1,38 @@
+import { ReactNode, createContext, useState } from "react";
+import { useEventListener } from "../Utility/utility";
+
+//
+// Types
+//
+interface AppUtilsProviderProps {
+ children?: ReactNode;
+}
+interface IAppUtilsContext extends AppUtilsProviderProps {
+ windowInnerWidth: number;
+ windowInnerHeight: number;
+}
+
+//
+// Context
+//
+export const AppUtilsContext = createContext({} as IAppUtilsContext);
+
+//
+// Provider
+//
+export const AppUtilsContextProvider = (props: AppUtilsProviderProps) => {
+ const [windowInnerWidth, setWindowInnerWidth] = useState(window.innerWidth);
+ const [windowInnerHeight, setWindowInnerHeight] = useState(
+ window.innerHeight
+ );
+ useEventListener(window, "resize", () => {
+ setWindowInnerWidth(window.innerWidth);
+ setWindowInnerHeight(window.innerHeight);
+ });
+
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/projects/ui/src/Styles/global-styles/highlight.js.min.css b/projects/ui/src/Styles/global-styles/highlight.js.min.css
new file mode 100644
index 00000000..0eabaf28
--- /dev/null
+++ b/projects/ui/src/Styles/global-styles/highlight.js.min.css
@@ -0,0 +1,126 @@
+/*!
+ Theme: StackOverflow Dark
+ Description: Dark theme as used on stackoverflow.com
+ Author: stackoverflow.com
+ Maintainer: @Hirse
+ Website: https://github.com/StackExchange/Stacks
+ License: MIT
+ Updated: 2021-05-15
+
+ Updated for @stackoverflow/stacks v0.64.0
+ Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less
+ Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less
+*/
+
+.hljs {
+ /* var(--highlight-color) */
+ color: #ffffff;
+ /* var(--highlight-bg) */
+ background: #1c1b1b;
+}
+
+.hljs-subst {
+ /* var(--highlight-color) */
+ color: #ffffff;
+}
+
+.hljs-comment {
+ /* var(--highlight-comment) */
+ color: #999999;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-meta .hljs-keyword,
+.hljs-doctag,
+.hljs-section {
+ /* var(--highlight-keyword) */
+ color: #88aece;
+}
+
+.hljs-attr {
+ /* var(--highlight-attribute); */
+ color: #88aece;
+}
+
+.hljs-attribute {
+ /* var(--highlight-symbol) */
+ color: #c59bc1;
+}
+
+.hljs-name,
+.hljs-type,
+.hljs-number,
+.hljs-selector-id,
+.hljs-quote,
+.hljs-template-tag {
+ /* var(--highlight-namespace) */
+ color: #f08d49;
+}
+
+.hljs-selector-class {
+ /* var(--highlight-keyword) */
+ color: #88aece;
+}
+
+.hljs-string,
+.hljs-regexp,
+.hljs-symbol,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-link,
+.hljs-selector-attr {
+ /* var(--highlight-variable) */
+ color: #b5bd68;
+}
+
+.hljs-meta,
+.hljs-selector-pseudo {
+ /* var(--highlight-keyword) */
+ color: #88aece;
+}
+
+.hljs-built_in,
+.hljs-title,
+.hljs-literal {
+ /* var(--highlight-literal) */
+ color: #f08d49;
+}
+
+.hljs-bullet,
+.hljs-code {
+ /* var(--highlight-punctuation) */
+ color: #cccccc;
+}
+
+.hljs-meta .hljs-string {
+ /* var(--highlight-variable) */
+ color: #b5bd68;
+}
+
+.hljs-deletion {
+ /* var(--highlight-deletion) */
+ color: #de7176;
+}
+
+.hljs-addition {
+ /* var(--highlight-addition) */
+ color: #76c490;
+}
+
+.hljs-emphasis {
+ font-style: italic;
+}
+
+.hljs-strong {
+ font-weight: bold;
+}
+
+.hljs-formula,
+.hljs-operator,
+.hljs-params,
+.hljs-property,
+.hljs-punctuation,
+.hljs-tag {
+ /* purposely ignored */
+}
diff --git a/projects/ui/src/Styles/global-styles/index.ts b/projects/ui/src/Styles/global-styles/index.ts
index f1aab047..90a952a6 100644
--- a/projects/ui/src/Styles/global-styles/index.ts
+++ b/projects/ui/src/Styles/global-styles/index.ts
@@ -8,6 +8,8 @@ import "./style-reset.css";
import "./fontFace.css";
// prettier-ignore
import "./graphiql.min.css";
+// prettier-ignore
+import "./highlight.js.min.css";
export const globalStyles = css`
${mantineGlobalStyles}
diff --git a/projects/ui/src/Styles/global-styles/site.style.ts b/projects/ui/src/Styles/global-styles/site.style.ts
index 4f5c8cd7..cba6fbfd 100644
--- a/projects/ui/src/Styles/global-styles/site.style.ts
+++ b/projects/ui/src/Styles/global-styles/site.style.ts
@@ -2,7 +2,7 @@ import { css } from "@emotion/react";
import { colors } from "../colors";
export const siteGlobalStyles = css`
- * {
+ *:not(code) {
color: ${colors.neptuneBlue};
}
//
diff --git a/projects/ui/src/Styles/global-styles/style-reset.css b/projects/ui/src/Styles/global-styles/style-reset.css
index 2f7a0049..28dd33e3 100644
--- a/projects/ui/src/Styles/global-styles/style-reset.css
+++ b/projects/ui/src/Styles/global-styles/style-reset.css
@@ -5,7 +5,7 @@
*
*******************************
******************************/
-* {
+*:not(code) {
font-family: "Proxima Nova", "Open Sans", "Helvetica", "Arial", "sans-serif" !important;
box-sizing: border-box;
diff --git a/projects/ui/src/Utility/utility.ts b/projects/ui/src/Utility/utility.ts
index 64bf0440..edf520a5 100644
--- a/projects/ui/src/Utility/utility.ts
+++ b/projects/ui/src/Utility/utility.ts
@@ -2,10 +2,12 @@
// From https://stackoverflow.com/a/65996386
// navigator.clipboard.writeText doesn't always work.
+import { DependencyList, useEffect } from "react";
import {
ErrorMessageResponse,
isErrorMessageResponse,
} from "../Apis/api-types";
+import { CustomPage } from "../user_variables.tmplr";
//
export async function copyToClipboard(textToCopy: string) {
@@ -121,5 +123,34 @@ export const customLog = (...args: Parameters) => {
export const filterMetadataToDisplay = ([pairKey]: [
key: string,
- value: string
+ value: string,
]) => pairKey !== "imageURL";
+
+const customPagePrefix = "/pages/";
+
+export const getCustomPagePath = (page: CustomPage | string) => {
+ const pagePath = typeof page === "string" ? page : page.path;
+ return (
+ customPagePrefix +
+ encodeURIComponent(pagePath.replace(/^\//g, "").replaceAll(/\./g, "_"))
+ );
+};
+
+// Actual hook for above function definitions
+export function useEventListener(
+ element: Elem,
+ eventName: string,
+ listener: EventListenerOrEventListenerObject,
+ dependencies: DependencyList = [],
+ skip = false
+) {
+ useEffect(() => {
+ // If element doesn't currently exist or hook isn't active then don't add a listener
+ if (!element || !element.addEventListener || skip) return;
+
+ element.addEventListener(eventName, listener);
+ return () => {
+ element.removeEventListener(eventName, listener);
+ };
+ }, [element, skip, ...dependencies]);
+}
diff --git a/projects/ui/src/user_variables.tmplr.ts b/projects/ui/src/user_variables.tmplr.ts
index f06da005..f188ec16 100644
--- a/projects/ui/src/user_variables.tmplr.ts
+++ b/projects/ui/src/user_variables.tmplr.ts
@@ -168,3 +168,25 @@ export const logoImageURL = templateString(
import.meta.env.VITE_LOGO_IMAGE_URL,
""
);
+
+export type CustomPage = {
+ title: string;
+ path: string;
+};
+/**
+ * This is an optional, JSON serialized array of objects.
+ * Each object has a "title" and "path" that corresponds to a ".html" or ".md" file in the `projects/ui/src/public` folder.
+ * The name is the text that is displayed in the navbar header link.
+ * For example:
+ * '[{"title":"Custom Page","path":"/custom-page.md"}, {"title":"Another Page","path":"/some-path/another-page.html"}]'
+ */
+export const customPages = JSON.parse(
+ templateString(
+ "{{ tmplr.customPages }}",
+ insertedEnvironmentVariables?.VITE_CUSTOM_PAGES,
+ import.meta.env.VITE_CUSTOM_PAGES,
+ "[]"
+ )
+) as Array;
+// TODO: Check the paths and if any overlap with the dev-portal-starter.
+// console.log("Loaded custom pages", customPages);
diff --git a/projects/ui/yarn.lock b/projects/ui/yarn.lock
index 0b101183..d4a32f79 100644
--- a/projects/ui/yarn.lock
+++ b/projects/ui/yarn.lock
@@ -5951,6 +5951,11 @@ highlight.js@^10.4.1, highlight.js@~10.7.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
+highlight.js@^11.10.0:
+ version "11.10.0"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92"
+ integrity sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==
+
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
diff --git a/readme_assets/custom-pages-navbar.png b/readme_assets/custom-pages-navbar.png
new file mode 100644
index 00000000..ac2108f6
Binary files /dev/null and b/readme_assets/custom-pages-navbar.png differ