diff --git a/src/frame/components/context/CategoryLandingContext.tsx b/src/frame/components/context/CategoryLandingContext.tsx new file mode 100644 index 000000000000..fa33b2fff1f5 --- /dev/null +++ b/src/frame/components/context/CategoryLandingContext.tsx @@ -0,0 +1,62 @@ +import pick from 'lodash/pick' +import { createContext, useContext } from 'react' +import { LearningTrack } from './ArticleContext' +import { + FeaturedLink, + getFeaturedLinksFromReq, +} from 'src/landings/components/ProductLandingContext' + +export type TocItem = { + fullPath: string + title: string + intro?: string + childTocItems?: Array<{ + fullPath: string + title: string + intro: string + }> +} + +export type CategoryLandingContextT = { + title: string + intro: string + productCallout: string + permissions: string + tocItems: Array + variant?: 'compact' | 'expanded' + featuredLinks: Record> + renderedPage: string + currentLearningTrack?: LearningTrack + currentLayout: string +} + +export const CategoryLandingContext = createContext(null) + +export const useCategoryLandingContext = (): CategoryLandingContextT => { + const context = useContext(CategoryLandingContext) + + if (!context) { + throw new Error( + '"useCategoryLandingContext" may only be used inside "CategoryLandingContext.Provider"', + ) + } + + return context +} + +export const getCategoryLandingContextFromRequest = (req: any): CategoryLandingContextT => { + return { + title: req.context.page.title, + productCallout: req.context.page.product || '', + permissions: req.context.page.permissions || '', + intro: req.context.page.intro, + tocItems: (req.context.genericTocFlat || req.context.genericTocNested || []).map((obj: any) => + pick(obj, ['fullPath', 'title', 'intro', 'childTocItems']), + ), + variant: req.context.genericTocFlat ? 'expanded' : 'compact', + featuredLinks: getFeaturedLinksFromReq(req), + renderedPage: req.context.renderedPage, + currentLearningTrack: req.context.currentLearningTrack, + currentLayout: req.context.currentLayoutName, + } +} diff --git a/src/frame/lib/frontmatter.js b/src/frame/lib/frontmatter.js index 49d409b857df..7fb0dfb2d35d 100644 --- a/src/frame/lib/frontmatter.js +++ b/src/frame/lib/frontmatter.js @@ -10,6 +10,7 @@ const layoutNames = [ 'product-guides', 'release-notes', 'inline', + 'category-landing', false, ] diff --git a/src/frame/middleware/context/generic-toc.ts b/src/frame/middleware/context/generic-toc.ts index 34053247c264..ed036cc53816 100644 --- a/src/frame/middleware/context/generic-toc.ts +++ b/src/frame/middleware/context/generic-toc.ts @@ -9,7 +9,11 @@ import findPageInSiteTree from '@/frame/lib/find-page-in-site-tree.js' export default async function genericToc(req: ExtendedRequest, res: Response, next: NextFunction) { if (!req.context) throw new Error('request not contextualized') if (!req.context.page) return next() - if (req.context.currentLayoutName !== 'default') return next() + if ( + req.context.currentLayoutName !== 'default' && + req.context.currentLayoutName !== 'category-landing' + ) + return next() // This middleware can only run on product, category, and map topics. if ( req.context.page.documentType === 'homepage' || @@ -92,7 +96,7 @@ export default async function genericToc(req: ExtendedRequest, res: Response, ne renderIntros = false req.context.genericTocNested = await getTocItems(treePage, req.context, { recurse: isRecursive, - renderIntros, + renderIntros: req.context.currentLayoutName === 'category-landing' ? true : false, includeHidden, }) } @@ -127,7 +131,11 @@ async function getTocItems(node: Tree, context: Context, opts: Options): Promise // Deliberately don't use `textOnly:true` here because we intend // to display the intro, in a table of contents component, // with the HTML (dangerouslySetInnerHTML). - intro = await page.renderProp('rawIntro', context) + intro = await page.renderProp( + 'rawIntro', + context, + context.currentLayoutName === 'category-landing' ? { textOnly: true } : {}, + ) } } diff --git a/src/landings/components/CategoryLanding.tsx b/src/landings/components/CategoryLanding.tsx new file mode 100644 index 000000000000..206aba4933b3 --- /dev/null +++ b/src/landings/components/CategoryLanding.tsx @@ -0,0 +1,67 @@ +import { useRouter } from 'next/router' +import cx from 'classnames' + +import { useCategoryLandingContext } from 'src/frame/components/context/CategoryLandingContext' +import { DefaultLayout } from 'src/frame/components/DefaultLayout' +import { ArticleTitle } from 'src/frame/components/article/ArticleTitle' +import { Lead } from 'src/frame/components/ui/Lead' +import { ClientSideRedirects } from 'src/rest/components/ClientSideRedirects' +import { RestRedirect } from 'src/rest/components/RestRedirect' +import { Breadcrumbs } from 'src/frame/components/page-header/Breadcrumbs' + +export const CategoryLanding = () => { + const router = useRouter() + const { title, intro, tocItems } = useCategoryLandingContext() + + // tocItems contains directories and its children, we only want the child articles + const onlyFlatItems = tocItems.flatMap((item) => item.childTocItems || []) + + return ( + + {router.route === '/[versionId]/rest/[category]' && } + {/* Doesn't matter *where* this is included because it will + never render anything. It always just return null. */} + + +
+
+ +
+ {title} + + {intro && {intro}} + +

Spotlight

+
+
Spotlight 1
+
Spotlight 2
+
Spotlight 3
+
+ +
+
+
+

Explore {onlyFlatItems.length} prompt articles

+
+
Searchbar
+
Category
+
Complexity
+
Industry
+
Reset
+
+
+ {/* TODO: replace with card components */} + {onlyFlatItems.map((item, index) => ( +
+
+
{item.title}
+
{item.intro}
+
+
+ ))} +
+
+
+
+ ) +} diff --git a/src/landings/pages/product.tsx b/src/landings/pages/product.tsx index 5f19a7e69760..d40871964167 100644 --- a/src/landings/pages/product.tsx +++ b/src/landings/pages/product.tsx @@ -35,11 +35,17 @@ import { ArticlePage } from 'src/frame/components/article/ArticlePage' import { ProductLanding } from 'src/landings/components/ProductLanding' import { ProductGuides } from 'src/landings/components/ProductGuides' import { TocLanding } from 'src/landings/components/TocLanding' +import { CategoryLanding } from 'src/landings/components/CategoryLanding' import { getTocLandingContextFromRequest, TocLandingContext, TocLandingContextT, } from 'src/frame/components/context/TocLandingContext' +import { + getCategoryLandingContextFromRequest, + CategoryLandingContext, + CategoryLandingContextT, +} from 'src/frame/components/context/CategoryLandingContext' import { useEffect } from 'react' function initiateArticleScripts() { @@ -54,6 +60,7 @@ type Props = { productGuidesContext?: ProductGuidesContextT tocLandingContext?: TocLandingContextT articleContext?: ArticleContextT + categoryLandingContext?: CategoryLandingContextT } const GlobalPage = ({ mainContext, @@ -61,6 +68,7 @@ const GlobalPage = ({ productGuidesContext, tocLandingContext, articleContext, + categoryLandingContext, }: Props) => { const router = useRouter() @@ -86,6 +94,12 @@ const GlobalPage = ({ ) + } else if (categoryLandingContext) { + content = ( + + + + ) } else if (tocLandingContext) { content = ( @@ -133,9 +147,13 @@ export const getServerSideProps: GetServerSideProps = async (context) => props.productGuidesContext = getProductGuidesContextFromRequest(req) additionalUINamespaces.push('product_guides') } else if (relativePath?.endsWith('index.md')) { - props.tocLandingContext = getTocLandingContextFromRequest(req) - if (props.tocLandingContext.currentLearningTrack?.trackName) { - additionalUINamespaces.push('learning_track_nav') + if (currentLayoutName === 'category-landing') { + props.categoryLandingContext = getCategoryLandingContextFromRequest(req) + } else { + props.tocLandingContext = getTocLandingContextFromRequest(req) + if (props.tocLandingContext.currentLearningTrack?.trackName) { + additionalUINamespaces.push('learning_track_nav') + } } } else if (props.mainContext.page) { // All articles that might have hover cards needs this