diff --git a/components/ContentstackRichText/ContentstackEntry.tsx b/components/ContentstackRichText/ContentstackEntry.tsx index 78a5267d..e06e22f4 100644 --- a/components/ContentstackRichText/ContentstackEntry.tsx +++ b/components/ContentstackRichText/ContentstackEntry.tsx @@ -10,6 +10,7 @@ import ExpandableCard from '@leafygreen-ui/expandable-card'; import ArrowRight from '@leafygreen-ui/icon/dist/ArrowRight'; import { spacing } from '@leafygreen-ui/tokens'; +import LiveExampleBlock from './LiveExampleBlock/LiveExampleBlock'; import AnnotatedImageBlock from './AnnotatedImageBlock'; import BasicUsageBlock from './BasicUsageBlock'; import ExampleCardBlock from './ExampleCardBlock'; @@ -82,6 +83,9 @@ const blockToElementMap: { ), horizontal_layout: props => , + live_example_block: props => ( + + ), } as const; const ContentstackEntry = ({ diff --git a/components/ContentstackRichText/ContentstackText.tsx b/components/ContentstackRichText/ContentstackText.tsx index 250caeb5..36631c2b 100644 --- a/components/ContentstackRichText/ContentstackText.tsx +++ b/components/ContentstackRichText/ContentstackText.tsx @@ -2,6 +2,7 @@ import { Fragment } from 'react'; import { HTMLElementProps } from '@leafygreen-ui/lib'; import { Polymorph } from '@leafygreen-ui/polymorphic'; +import { InlineCode } from '@leafygreen-ui/typography'; import { CSTextNode } from './types'; @@ -10,9 +11,20 @@ interface CSRichTextProps extends HTMLElementProps<'span'> { } const ContentstackText = ({ node, ...rest }: CSRichTextProps) => { - const renderAs = node.bold ? 'b' : rest.className ? 'span' : Fragment; + let renderAs; + + if (node.bold) { + renderAs = 'b'; + } else if (node.inlineCode) { + renderAs = InlineCode; + } else if (rest.className) { + renderAs = 'span'; + } else { + renderAs = Fragment; + } return ( + // @ts-ignore href not needed as no links will be rendered as a result of this component's logic {node.text} diff --git a/components/ContentstackRichText/ExampleCardBlock/ExampleCardBlock.tsx b/components/ContentstackRichText/ExampleCardBlock/ExampleCardBlock.tsx index b9ee0f81..306d205e 100644 --- a/components/ContentstackRichText/ExampleCardBlock/ExampleCardBlock.tsx +++ b/components/ContentstackRichText/ExampleCardBlock/ExampleCardBlock.tsx @@ -19,6 +19,7 @@ import ImageContainer from './ImageContainer'; const ExampleCardBlockWrapper = styled.div` margin-block: ${spacing[5] + spacing[2]}px; + width: 100%; `; const TextContainer = styled.div` diff --git a/components/ContentstackRichText/HeaderContent.tsx b/components/ContentstackRichText/HeaderContent.tsx index 8d8a9cd0..e813af2c 100644 --- a/components/ContentstackRichText/HeaderContent.tsx +++ b/components/ContentstackRichText/HeaderContent.tsx @@ -1,11 +1,15 @@ +import { useEffect } from 'react'; import styled from '@emotion/styled'; +import { + BreadcrumbHeader, + useGuidelinesContext, +} from 'contexts/GuidelinesContext'; import { kebabCase } from 'lodash'; import Link from 'next/link'; import Icon from '@leafygreen-ui/icon'; import { palette } from '@leafygreen-ui/palette'; -import ContentstackChildren from './ContentstackChildren'; import { CSNode } from './types'; import { getCSNodeTextContent } from './utils'; @@ -53,14 +57,20 @@ const StyledIcon = styled(Icon)` * Content of headers in rich text markup need to be wrapped in links and anchors for hashed links. */ const HeaderContent = ({ node }: { node: CSNode }) => { + const nodeText = getCSNodeTextContent(node); const headerId = kebabCase(getCSNodeTextContent(node)); + const { pushHeader } = useGuidelinesContext(); + + useEffect(() => { + if (node.type === 'h2' || node.type === 'h4') { + pushHeader(node.type as BreadcrumbHeader, nodeText); + } + }, []); return ( - - - + {nodeText} diff --git a/components/ContentstackRichText/LiveExampleBlock/LiveExampleBlock.tsx b/components/ContentstackRichText/LiveExampleBlock/LiveExampleBlock.tsx new file mode 100644 index 00000000..e14afbca --- /dev/null +++ b/components/ContentstackRichText/LiveExampleBlock/LiveExampleBlock.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react'; +import styled from '@emotion/styled'; +import { ComponentStoryFn } from '@storybook/react'; +import { useGuidelinesContext } from 'contexts/GuidelinesContext'; +import { getComponentStories } from 'utils/getComponentStories'; + +import { useAsyncEffect } from 'components/pages/example/useAsyncEffect'; + +import Button from '@leafygreen-ui/button'; +import Card from '@leafygreen-ui/card'; +import Code from '@leafygreen-ui/code'; +import Icon from '@leafygreen-ui/icon'; +import { spacing } from '@leafygreen-ui/tokens'; + +const CodeWrapper = styled('div')` + > div { + border-left: 0; + border-right: 0; + border-bottom: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + overflow: hidden; + + & > div:before, + & > div:after { + z-index: 0; + } + } +`; + +const LiveExampleBlock = ({ storyName }: { storyName: string }) => { + const [StoryFn, setStoryFn] = useState< + ComponentStoryFn & React.FunctionComponent + >(); + const [args, setArgs] = useState({}); + const [sourceCode, setSourceCode] = useState(); + const { componentName } = useGuidelinesContext(); + const [showCode, setShowCode] = useState(true); + const toggleShowCode = () => setShowCode(sc => !sc); + + useAsyncEffect( + // @ts-ignore conditional checked in useAsyncEffect + () => getComponentStories(componentName), + { + then: module => { + if (module) { + const { default: meta, ...stories } = module; + setStoryFn(() => stories[storyName]); + setArgs({ ...meta.args, ...stories[storyName].args }); + setSourceCode(stories[storyName]?.parameters?.sourceCode); + } else { + console.error('Error parsing module', module); + } + }, + catch: err => { + console.error('Error loading LiveExample'); + console.error(err); + }, + }, + !!componentName, + [componentName], + ); + + return ( + <> + {StoryFn ? ( +
+ +
+
+ +
+
+ +
+
+ {showCode && sourceCode && ( + + {sourceCode} + + )} +
+
+ ) : ( + <>Loading... + )} + + ); +}; + +export default LiveExampleBlock; diff --git a/components/ContentstackRichText/componentMap.tsx b/components/ContentstackRichText/componentMap.tsx index 8e42248e..ad46a2e0 100644 --- a/components/ContentstackRichText/componentMap.tsx +++ b/components/ContentstackRichText/componentMap.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/react'; +import { useGuidelinesContext } from 'contexts/GuidelinesContext'; import Card from '@leafygreen-ui/card'; import { palette } from '@leafygreen-ui/palette'; @@ -17,6 +18,7 @@ import ContentstackChildren from './ContentstackChildren'; import ContentstackReference from './ContentstackReference'; import HeaderContent from './HeaderContent'; import { CSNode, CSNodeType, CSTextNode } from './types'; +import { getCSNodeTextContent } from './utils'; type CSNodeTypeMapFunction = ( node: CSNode | CSTextNode, @@ -48,23 +50,29 @@ export const nodeTypeToElementMap: { ), - [CSNodeType.HEADING_2]: (node, props) => ( -

{ + const { componentName, getHeaderRef } = useGuidelinesContext(); + const nodeText = getCSNodeTextContent(node); + const headerRef = getHeaderRef(`${componentName}-${nodeText}`); + return ( +

- -

- ), + &:not(:first-child) { + margin-top: ${spacing[6]}px; + } + ` + } + ref={headerRef} + {...props} + > + + + ); + }, [CSNodeType.HEADING_3]: (node, props) => (

), - [CSNodeType.HEADING_4]: (node, props) => ( - - - - ), + [CSNodeType.HEADING_4]: (node, props) => { + const { getHeaderRef, componentName } = useGuidelinesContext(); + const nodeText = getCSNodeTextContent(node); + const headerRef = getHeaderRef(`${componentName}-${nodeText}`); + // useEffect(() => { console.log(headerRef?.current)}, [headerRef]) + return ( + + + + ); + }, [CSNodeType.HEADING_5]: (node, props) => ( @@ -108,6 +123,7 @@ export const nodeTypeToElementMap: { css` & { margin-top: ${spacing[2]}px; + margin-bottom: ${spacing[4]}px; } ` } @@ -273,6 +289,7 @@ export const nodeTypeToElementMap: { [CSNodeType.REFERENCE]: (node, props) => ( ), + [CSNodeType.CODE]: (node, props) => , [CSNodeType.FRAGMENT]: (node, props) => ( ), diff --git a/components/ContentstackRichText/types.ts b/components/ContentstackRichText/types.ts index 361f478c..d16b2da6 100644 --- a/components/ContentstackRichText/types.ts +++ b/components/ContentstackRichText/types.ts @@ -4,7 +4,7 @@ import { BadgeProps } from '@leafygreen-ui/badge/dist/Badge/types'; import { ButtonProps } from '@leafygreen-ui/button'; import { CalloutProps } from '@leafygreen-ui/callout/dist/Callout/types'; -type AnyNode = CSNode | CSTextNode; +export type AnyNode = CSNode | CSTextNode; /** Contentstack is missing props in their type definitions */ export interface CSNode extends Node { @@ -26,6 +26,7 @@ export enum CSNodeType { DOCUMENT = 'doc', FRAGMENT = 'fragment', PARAGRAPH = 'p', + CODE = 'code', ANCHOR = 'a', REFERENCE = 'reference', HEADING_1 = 'h1', @@ -147,6 +148,10 @@ export interface TwoColumnExampleCardBlockProps { column_2_image: CSImage; } +export interface LiveExampleBlockProps { + story_name: string; +} + export interface BlockPropsMap { annotated_image_block: AnnotatedImageBlockProps; badge_block: BadgeBlockProps; @@ -158,6 +163,7 @@ export interface BlockPropsMap { expandable_card_block: ExpandableCardBlockProps; horizontal_layout: HorizontalLayoutBlockProps; example_card_block_2_column_: TwoColumnExampleCardBlockProps; + live_example_block: LiveExampleBlockProps; } export type ContentTypeUID = keyof BlockPropsMap; diff --git a/components/GuidelineBreadcrumbs.tsx b/components/GuidelineBreadcrumbs.tsx new file mode 100644 index 00000000..3cfdd846 --- /dev/null +++ b/components/GuidelineBreadcrumbs.tsx @@ -0,0 +1,64 @@ +import styled from '@emotion/styled'; +import { useGuidelinesContext } from 'contexts/GuidelinesContext'; +import kebabCase from 'lodash/kebabCase'; + +import { palette } from '@leafygreen-ui/palette'; +import { spacing } from '@leafygreen-ui/tokens'; +import { Overline } from '@leafygreen-ui/typography'; + +const BreadCrumb = styled('a')<{ $indented?: boolean; active: boolean }>` + display: block; + width: fit-content; + padding: 6px 12px; + margin-left: ${props => (props.$indented ? spacing[3] : 0)}px; + color: ${palette.gray.dark1}; + text-decoration: none; + border-radius: 24px; + + ${props => + props.active && + ` + background-color: ${palette.green.light3}; + color: ${palette.green.dark2}; + font-weight: bold; + `} +`; + +const GuidelineBreadcrumb = ({ header, active }) => { + return ( + + {header.text} + + ); +}; + +const GuidelineBreadcrumbs = () => { + const { headers, activeHeaderIndex } = useGuidelinesContext(); + return ( + <> + + Contents + + {headers.map((header, i) => ( + + ))} + {/* Scroll to top */} + + ); +}; + +export default GuidelineBreadcrumbs; diff --git a/components/SignIn/DesktopSignIn.tsx b/components/SignIn/DesktopSignIn.tsx index aaa4d8bb..eeff5a74 100644 --- a/components/SignIn/DesktopSignIn.tsx +++ b/components/SignIn/DesktopSignIn.tsx @@ -12,6 +12,8 @@ import { UserInfo } from '../UserInfo'; const Container = styled('div')` width: 100%; + position: absolute; + padding-right: 16px; align-items: center; justify-content: flex-end; padding-top: ${spacing[4]}px; @@ -74,7 +76,7 @@ const DesktopSignIn = () => { ) : ( )} - {knobsArray.map(knob => ( - - ))} + {showKnobs && ( + +
+ {knobsArray.map(knob => ( + + ))} +
+
+ )} ); }; diff --git a/components/pages/example/LiveExample.tsx b/components/pages/example/LiveExample.tsx index 939d9bec..59072c05 100644 --- a/components/pages/example/LiveExample.tsx +++ b/components/pages/example/LiveExample.tsx @@ -92,13 +92,12 @@ export const LiveExample = ({ // Currently disabling code examples for all components // TODO: Fix code examples: https://jira.mongodb.org/browse/LG-3310 - const codeExampleEnabled = false; //!disableCodeExampleFor.includes(componentName); + const codeExampleEnabled = true; //!disableCodeExampleFor.includes(componentName); return ( diff --git a/contexts/GuidelinesContext.tsx b/contexts/GuidelinesContext.tsx new file mode 100644 index 00000000..411650b0 --- /dev/null +++ b/contexts/GuidelinesContext.tsx @@ -0,0 +1,157 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; + +import { useDynamicRefs } from '@leafygreen-ui/hooks'; + +export type BreadcrumbHeader = 'h2' | 'h4'; + +interface GuidelineHeader { + text: string; + type: BreadcrumbHeader; +} + +interface GuidelinesContextValue { + headers: Array; + pushHeader: (headerType: BreadcrumbHeader, headerText: string) => void; + activeHeaderIndex: number; + componentName?: string; + getHeaderRef: ( + key?: string | undefined, + ) => React.RefObject | undefined; +} +const GuidelinesContext = createContext({ + headers: [], + pushHeader: () => {}, + activeHeaderIndex: 0, + componentName: '', + getHeaderRef: () => undefined, +}); + +/** + * https://codesandbox.io/s/highlight-menu-item-on-scroll-x0gzn?file=/src/ScrollHighlightNabbar/ScrollHighlightNabbar.js:127-1523 + * @param {number} currentPosition Current Scroll position + * @param {Array} sectionPositionArray Array of positions of all sections + * @param {number} startIndex Start index of array + * @param {number} endIndex End index of array + * @return {number} Current Active index + */ +const nearestScrolledRefIndex = ( + currentPosition, + headers, + startIndex, + endIndex, + getHeaderRef, + componentName, +) => { + if (startIndex === endIndex) return startIndex; + else if (startIndex === endIndex - 1) { + if ( + Math.abs( + getHeaderRef(`${componentName}-${headers[startIndex].text}`)?.current + .offsetTop - currentPosition, + ) < + Math.abs( + getHeaderRef(`${componentName}-${headers[endIndex].text}`)?.current + .offsetTop - currentPosition, + ) + ) + return startIndex; + else return endIndex; + } else { + const nextNearest = ~~((startIndex + endIndex) / 2); + const a = Math.abs( + getHeaderRef(`${componentName}-${headers[nextNearest].text}`)?.current + .offsetTop - currentPosition, + ); + const b = Math.abs( + getHeaderRef(`${componentName}-${headers[nextNearest + 1].text}`)?.current + .offsetTop - currentPosition, + ); + + if (a < b) { + return nearestScrolledRefIndex( + currentPosition, + headers, + startIndex, + nextNearest, + getHeaderRef, + componentName, + ); + } else { + return nearestScrolledRefIndex( + currentPosition, + headers, + nextNearest, + endIndex, + getHeaderRef, + componentName, + ); + } + } +}; + +export function GuidelinesContextProvider({ children, componentName }) { + const router = useRouter(); + const [headers, setHeaders] = useState>([]); + const pushHeader = (headerType: BreadcrumbHeader, headerText: string) => + setHeaders(oldHeaders => [ + ...oldHeaders, + { text: headerText, type: headerType }, + ]); + const getHeaderRef = useDynamicRefs({ + prefix: `${componentName}-guideline-header`, + }); + const [activeHeaderIndex, setActiveHeaderIndex] = useState(0); + + useEffect(() => { + if (headers.length > 0) { + const scrollContainer = document.querySelector('#scroll-container'); + + if (scrollContainer) { + const handleScroll = e => { + const index = nearestScrolledRefIndex( + scrollContainer.scrollTop, + headers, + 0, + headers.length - 1, + getHeaderRef, + componentName, + ); + setActiveHeaderIndex(index - 1); + }; + scrollContainer.addEventListener('scroll', handleScroll); + return () => { + scrollContainer.removeEventListener('scroll', handleScroll); + }; + } + } + }, [headers]); + + useEffect(() => { + const startHandler = () => { + setHeaders([]); + }; + router.events.on('routeChangeStart', startHandler); + return () => { + router.events.off('routeChangeStart', startHandler); + }; + }, []); + + return ( + + {children} + + ); +} + +export function useGuidelinesContext() { + return useContext(GuidelinesContext); +} diff --git a/layouts/BaseLayout.tsx b/layouts/BaseLayout.tsx index 3f387ae5..fe31a312 100644 --- a/layouts/BaseLayout.tsx +++ b/layouts/BaseLayout.tsx @@ -37,17 +37,15 @@ const layout = css` })} `; -const padding = `${spacing[5]}px`; - export const childrenWrapper = css` ${mq({ - paddingLeft: [0, 0, padding, padding], - paddingRight: [0, 0, padding, padding], - width: [ - '100%', - '100%', - `calc(100% - (${padding} * 2))', 'calc(1440px - 270px - (${padding} * 2))`, - ], + // paddingLeft: [0, 0, padding, padding], + // paddingRight: [0, 0, padding, padding], + // width: [ + // '100%', + // '100%', + // `calc(100% - (${padding} * 2))', 'calc(1440px - 270px - (${padding} * 2))`, + // ], minHeight: ['unset', 'unset', '100vh'], })} `; @@ -85,7 +83,11 @@ function BaseLayout({ children }: { children: React.ReactNode }) {
-
+
{children} diff --git a/layouts/NewComponentLayout.tsx b/layouts/NewComponentLayout.tsx new file mode 100644 index 00000000..f0ef1f6f --- /dev/null +++ b/layouts/NewComponentLayout.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { GuidelinesContextProvider } from 'contexts/GuidelinesContext'; +import startCase from 'lodash/startCase'; +import Head from 'next/head'; +import { useSession } from 'next-auth/react'; +import { ComponentFields } from 'utils/ContentStack/types'; +import getFullPageTitle from 'utils/getFullPageTitle'; +import { mq } from 'utils/mediaQuery'; +import { CustomComponentDoc } from 'utils/tsdoc.utils'; + +import GuidelineBreadcrumbs from 'components/GuidelineBreadcrumbs'; +import FigmaIcon from 'components/icons/FigmaIcon'; +import GithubIcon from 'components/icons/GithubIcon'; +import { LiveExample } from 'components/pages/example/LiveExample'; + +import Button from '@leafygreen-ui/button'; +import LinkIcon from '@leafygreen-ui/icon/dist/Link'; +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; +import { palette } from '@leafygreen-ui/palette'; +import { spacing } from '@leafygreen-ui/tokens'; +import { Body, H1, Overline } from '@leafygreen-ui/typography'; + +const Container = styled('div')` + ${mq({ + // 51px is a magic number for baseline alignment with the first SideNavGroup header + marginTop: [`${spacing[4]}px`, `${spacing[4]}px`, '51px'], + // width: ['100%', '100%', '90%', '75%'], + paddingLeft: [`${spacing[2]}`, `${spacing[4]}px`, '120px'], + paddingRight: [`${spacing[2]}`, `${spacing[4]}px`, '120px'], + })} +`; + +const FlexContainer = styled('div')` + display: flex; + gap: ${spacing[7]}px; + position: relative; + ${mq({ + flexDirection: ['column-reverse', 'row'], + })} +`; + +const ContentContainer = styled('div')` + flex: 2; +`; + +const BreadcrumbsContainer = styled('div')` + position: sticky; + height: fit-content; + top: ${spacing[6]}px; + flex: 1; +`; + +const HeaderContainer = styled('div')` + background-color: ${palette.green.dark3}; + ${mq({ + padding: [`${spacing[2]}`, `${spacing[4]}px`, `${spacing[7]}px`], + paddingLeft: [`${spacing[2]}`, `${spacing[4]}px`, '120px'], + paddingRight: [`${spacing[2]}`, `${spacing[4]}px`, '120px'], + })} + background-image: url('/Blob.svg'); + background-repeat: no-repeat; + background-position: 100% 0%; +`; + +const Header = styled(H1)` + margin-bottom: ${spacing[3]}px; + text-transform: capitalize; +`; + +const getLinkIcon = linkType => { + switch (linkType) { + case 'Figma': + return ; + case 'Github': + return ; + default: + return ; + } +}; + +const LinksContainer = styled('div')` + display: flex; + gap: ${spacing[2]}px; +`; + +const NewComponentLayout = ({ + component, + componentName, + children, + tsDoc, +}: { + component?: ComponentFields; + children: React.ReactNode; + componentName: string; + tsDoc: Array | null; +}) => { + const pageTitle = getFullPageTitle(startCase(componentName)); + const { data: session } = useSession(); + + return ( + +
+
+ + {pageTitle} + + {/* If the description field doesn't exist, it will default to a description of the site, defined in _document. */} + {component?.description && ( + + )} + + + + {component && + (!component.private || (component.private && session)) ? ( + <> + + + {/* Add id for hard-coded breadcrumb */} +
{startCase(componentName)}
+ + {component.description} + + + {component.links_data && + component.links_data.map(linkObject => ( + + ))} + +
+
+ + + +
+ + Playground + + +
+ {children} +
+ + + +
+
+ + ) : ( + children + )} +
+
+
+ ); +}; + +NewComponentLayout.displayName = 'NewComponentLayout'; + +export default NewComponentLayout; diff --git a/next.config.js b/next.config.js index 1940b296..0a38eebc 100644 --- a/next.config.js +++ b/next.config.js @@ -42,7 +42,7 @@ const nextConfig = { config.module.rules.push({ test: /\.svg$/, use: ['@svgr/webpack'], - include: isPathInLeafygreen, + // include: [isPathInLeafygreen, '/public'], }); return config; diff --git a/pages/[contentPageGroup]/private/[contentPageTitle].tsx b/pages/[contentPageGroup]/private/[contentPageTitle].tsx new file mode 100644 index 00000000..d46ee66f --- /dev/null +++ b/pages/[contentPageGroup]/private/[contentPageTitle].tsx @@ -0,0 +1,41 @@ +import { ReactElement } from 'react'; +import ContentPageLayout from 'layouts/ContentPageLayout'; +import startCase from 'lodash/startCase'; +import { unstable_getServerSession } from 'next-auth'; +import { authOptions } from 'pages/api/auth/[...nextauth]'; +import { getContentPage } from 'utils/ContentStack/getContentstackResources'; + +import ContentstackRichText from 'components/ContentstackRichText'; +import Unauthorized from 'components/Unauthorized'; + +const ContentPage = ({ contentPage }) => { + if (contentPage !== null) + return ; + + return ; +}; + +ContentPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export async function getServerSideProps({ params, req, res }) { + const session = await unstable_getServerSession(req, res, authOptions); + const contentPage = session + ? await getContentPage(startCase(params.contentPageTitle)) + : null; + return { + props: { + contentPage, + contentPageTitle: contentPage ? contentPage.title : null, + }, + }; +} + +export default ContentPage; diff --git a/pages/component/[componentName]/guidelines.tsx b/pages/component/[componentName]/guidelines.tsx deleted file mode 100644 index 5ef4f693..00000000 --- a/pages/component/[componentName]/guidelines.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ReactElement } from 'react'; -import ComponentLayout from 'layouts/ComponentLayout'; -import { containerPadding } from 'styles/globals'; -import { - getStaticComponentPaths, - getStaticComponentProps, -} from 'utils/ContentStack/getStaticComponent'; -import isEmptyRichText from 'utils/isEmptyRichText'; - -import ComingSoon from 'components/ComingSoon'; -import ContentstackRichText from 'components/ContentstackRichText'; - -const ComponentGuidelines = ({ component }) => { - const guidelines = component.designguidelines; - return !guidelines || isEmptyRichText(guidelines) ? ( - - ) : ( -
- {} -
- ); -}; - -ComponentGuidelines.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export const getStaticPaths = getStaticComponentPaths; -export const getStaticProps = getStaticComponentProps; - -export default ComponentGuidelines; diff --git a/pages/component/[componentName]/index.tsx b/pages/component/[componentName]/index.tsx new file mode 100644 index 00000000..2512d426 --- /dev/null +++ b/pages/component/[componentName]/index.tsx @@ -0,0 +1,59 @@ +import { ReactElement } from 'react'; +import NewComponentLayout from 'layouts/NewComponentLayout'; +import { containerPadding } from 'styles/globals'; +import { getTSDoc } from 'utils/_getComponentResources'; +import { getComponent } from 'utils/ContentStack/getContentstackResources'; +import { getStaticComponentPaths } from 'utils/ContentStack/getStaticComponent'; +import { ComponentPageMeta } from 'utils/ContentStack/types'; +import isEmptyRichText from 'utils/isEmptyRichText'; + +import ComingSoon from 'components/ComingSoon'; +import ContentstackRichText from 'components/ContentstackRichText'; + +import { ExamplePageProps } from './example'; + +const ComponentGuidelines = ({ component }) => { + const guidelines = component.designguidelines; + return !guidelines || isEmptyRichText(guidelines) ? ( + + ) : ( +
+ {} +
+ ); +}; + +ComponentGuidelines.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export const getStaticPaths = getStaticComponentPaths; +export const getStaticProps = async ({ + params, +}): Promise<{ props: ExamplePageProps }> => { + const { componentName } = params; + const component: ComponentPageMeta | null = + (await getComponent(componentName, { + includeContent: true, + })) ?? null; + + const tsDoc = await getTSDoc(componentName); + + return { + props: { + componentName, + component, + tsDoc, + }, + }; +}; + +export default ComponentGuidelines; diff --git a/public/Blob.svg b/public/Blob.svg new file mode 100644 index 00000000..3a6ed2f0 --- /dev/null +++ b/public/Blob.svg @@ -0,0 +1,3 @@ + + + diff --git a/utils/ContentStack/getContentstackResources.ts b/utils/ContentStack/getContentstackResources.ts index 72ec6948..67c869ec 100644 --- a/utils/ContentStack/getContentstackResources.ts +++ b/utils/ContentStack/getContentstackResources.ts @@ -14,6 +14,7 @@ const ENV_MAP = { production: 'main', staging: 'staging', dev: 'staging', + experimental: 'experimental', } as const; const environment = ((): string => { @@ -32,6 +33,10 @@ const Stack = Contentstack.Stack({ api_key: process.env.NEXT_PUBLIC_CONTENTSTACK_API_KEY as string, delivery_token: process.env.NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN as string, environment, + branch: + process.env.NEXT_PUBLIC_ENVIRONMENT === 'experimental' + ? 'experimental' + : 'main', }); interface QueryOptions { @@ -45,6 +50,7 @@ const componentProperties = [ 'url', 'figmaurl', 'private', + 'links_data', 'codesandbox_url', ]; const optionalComponentProperties = ['designguidelines']; @@ -108,7 +114,16 @@ export async function getContentPageGroups(): Promise> { const pageGroups: Array = ( await query .includeReference('content_pages') - .only(['content_pages', 'uid', 'title', 'url', 'iconname']) + .only([ + 'content_pages', + 'uid', + 'title', + 'url', + 'iconname', + 'private', + 'rendering_order', + ]) + .ascending('rendering_order') .toJSON() .find() )[0].map(({ content_pages, ...meta }: ContentPageGroup) => { @@ -117,11 +132,15 @@ export async function getContentPageGroups(): Promise> { content_pages: content_pages // TODO: strip fields in initial query // Strip any additional fields - .map(({ uid, title, url }) => ({ uid, title, url })) + .map(({ uid, title, url, is_private }: ContentPage) => ({ + uid, + title, + url, + is_private, + })) .sort((a, b) => a.title.localeCompare(b.title)), }; }); - return pageGroups; } catch (error) { console.error('No Content Page Groups found', error); diff --git a/utils/ContentStack/types.ts b/utils/ContentStack/types.ts index f9b0ee1c..4068b12c 100644 --- a/utils/ContentStack/types.ts +++ b/utils/ContentStack/types.ts @@ -1,9 +1,16 @@ +export interface ContentstackMetadata { + uid: string; +} +export interface ContentstackObject { + _metadata: ContentstackMetadata; +} + export interface ContentPageGroup extends Object { uid: string; title: string; url: string; iconname: string; - content_pages: Array; + content_pages: Array; } /** @@ -20,6 +27,7 @@ export interface ContentPageMeta { */ export interface ContentPage extends ContentPageMeta { content: unknown; + is_private?: boolean; } /** @@ -35,12 +43,18 @@ export interface ComponentPageMeta { private?: boolean; } +export interface LinkData extends ContentstackObject { + link: { title: string; href: string }; + type?: string; +} + /** * A {@link ComponentPageMeta} with additional data fields * including `figmaUrl` & `designguidelines` content */ export interface ComponentFields extends ComponentPageMeta { designguidelines?: unknown; + links_data?: Array<{ link_data: LinkData }>; } export interface BaseLayoutProps {