1
0
mirror of synced 2025-12-23 11:54:18 -05:00

Allow a category landing page for Copilot Cookbook (#53002)

This commit is contained in:
Hector Alfaro
2024-11-07 15:35:35 -05:00
committed by GitHub
parent 90e0168b07
commit 2d14549744
5 changed files with 162 additions and 6 deletions

View File

@@ -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<TocItem>
variant?: 'compact' | 'expanded'
featuredLinks: Record<string, Array<FeaturedLink>>
renderedPage: string
currentLearningTrack?: LearningTrack
currentLayout: string
}
export const CategoryLandingContext = createContext<CategoryLandingContextT | null>(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,
}
}

View File

@@ -10,6 +10,7 @@ const layoutNames = [
'product-guides', 'product-guides',
'release-notes', 'release-notes',
'inline', 'inline',
'category-landing',
false, false,
] ]

View File

@@ -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) { export default async function genericToc(req: ExtendedRequest, res: Response, next: NextFunction) {
if (!req.context) throw new Error('request not contextualized') if (!req.context) throw new Error('request not contextualized')
if (!req.context.page) return next() 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. // This middleware can only run on product, category, and map topics.
if ( if (
req.context.page.documentType === 'homepage' || req.context.page.documentType === 'homepage' ||
@@ -92,7 +96,7 @@ export default async function genericToc(req: ExtendedRequest, res: Response, ne
renderIntros = false renderIntros = false
req.context.genericTocNested = await getTocItems(treePage, req.context, { req.context.genericTocNested = await getTocItems(treePage, req.context, {
recurse: isRecursive, recurse: isRecursive,
renderIntros, renderIntros: req.context.currentLayoutName === 'category-landing' ? true : false,
includeHidden, 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 // Deliberately don't use `textOnly:true` here because we intend
// to display the intro, in a table of contents component, // to display the intro, in a table of contents component,
// with the HTML (dangerouslySetInnerHTML). // with the HTML (dangerouslySetInnerHTML).
intro = await page.renderProp('rawIntro', context) intro = await page.renderProp(
'rawIntro',
context,
context.currentLayoutName === 'category-landing' ? { textOnly: true } : {},
)
} }
} }

View File

@@ -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 (
<DefaultLayout>
{router.route === '/[versionId]/rest/[category]' && <RestRedirect />}
{/* Doesn't matter *where* this is included because it will
never render anything. It always just return null. */}
<ClientSideRedirects />
<div className="container-xl px-3 px-md-6 my-4">
<div className={cx('d-none d-xl-block mt-3 mr-auto width-full')}>
<Breadcrumbs />
</div>
<ArticleTitle>{title}</ArticleTitle>
{intro && <Lead data-search="lead">{intro}</Lead>}
<h2 className="py-5">Spotlight</h2>
<div className="container-lg clearfix">
<div className="col-4 float-left border p-4">Spotlight 1</div>
<div className="col-4 float-left border p-4">Spotlight 2</div>
<div className="col-4 float-left border p-4">Spotlight 3</div>
</div>
<div className="pt-8">
<div className="py-5 clearfix border-bottom">
<div className="col-5 float-left p-3">
<h2>Explore {onlyFlatItems.length} prompt articles</h2>
</div>
<div className="col-3 float-left p-4">Searchbar</div>
<div className="col-1 float-left p-4">Category</div>
<div className="col-1 float-left p-4">Complexity</div>
<div className="col-1 float-left p-4">Industry</div>
<div className="col-1 float-left p-4">Reset</div>
</div>
<div className="clearfix gutter-md-spacious">
{/* TODO: replace with card components */}
{onlyFlatItems.map((item, index) => (
<div key={index} className="col-4 float-left p-4">
<div className="px-3 pb-3 border-bottom">
<div>{item.title}</div>
<div>{item.intro}</div>
</div>
</div>
))}
</div>
</div>
</div>
</DefaultLayout>
)
}

View File

@@ -35,11 +35,17 @@ import { ArticlePage } from 'src/frame/components/article/ArticlePage'
import { ProductLanding } from 'src/landings/components/ProductLanding' import { ProductLanding } from 'src/landings/components/ProductLanding'
import { ProductGuides } from 'src/landings/components/ProductGuides' import { ProductGuides } from 'src/landings/components/ProductGuides'
import { TocLanding } from 'src/landings/components/TocLanding' import { TocLanding } from 'src/landings/components/TocLanding'
import { CategoryLanding } from 'src/landings/components/CategoryLanding'
import { import {
getTocLandingContextFromRequest, getTocLandingContextFromRequest,
TocLandingContext, TocLandingContext,
TocLandingContextT, TocLandingContextT,
} from 'src/frame/components/context/TocLandingContext' } from 'src/frame/components/context/TocLandingContext'
import {
getCategoryLandingContextFromRequest,
CategoryLandingContext,
CategoryLandingContextT,
} from 'src/frame/components/context/CategoryLandingContext'
import { useEffect } from 'react' import { useEffect } from 'react'
function initiateArticleScripts() { function initiateArticleScripts() {
@@ -54,6 +60,7 @@ type Props = {
productGuidesContext?: ProductGuidesContextT productGuidesContext?: ProductGuidesContextT
tocLandingContext?: TocLandingContextT tocLandingContext?: TocLandingContextT
articleContext?: ArticleContextT articleContext?: ArticleContextT
categoryLandingContext?: CategoryLandingContextT
} }
const GlobalPage = ({ const GlobalPage = ({
mainContext, mainContext,
@@ -61,6 +68,7 @@ const GlobalPage = ({
productGuidesContext, productGuidesContext,
tocLandingContext, tocLandingContext,
articleContext, articleContext,
categoryLandingContext,
}: Props) => { }: Props) => {
const router = useRouter() const router = useRouter()
@@ -86,6 +94,12 @@ const GlobalPage = ({
<ProductGuides /> <ProductGuides />
</ProductGuidesContext.Provider> </ProductGuidesContext.Provider>
) )
} else if (categoryLandingContext) {
content = (
<CategoryLandingContext.Provider value={categoryLandingContext}>
<CategoryLanding />
</CategoryLandingContext.Provider>
)
} else if (tocLandingContext) { } else if (tocLandingContext) {
content = ( content = (
<TocLandingContext.Provider value={tocLandingContext}> <TocLandingContext.Provider value={tocLandingContext}>
@@ -133,9 +147,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
props.productGuidesContext = getProductGuidesContextFromRequest(req) props.productGuidesContext = getProductGuidesContextFromRequest(req)
additionalUINamespaces.push('product_guides') additionalUINamespaces.push('product_guides')
} else if (relativePath?.endsWith('index.md')) { } else if (relativePath?.endsWith('index.md')) {
props.tocLandingContext = getTocLandingContextFromRequest(req) if (currentLayoutName === 'category-landing') {
if (props.tocLandingContext.currentLearningTrack?.trackName) { props.categoryLandingContext = getCategoryLandingContextFromRequest(req)
additionalUINamespaces.push('learning_track_nav') } else {
props.tocLandingContext = getTocLandingContextFromRequest(req)
if (props.tocLandingContext.currentLearningTrack?.trackName) {
additionalUINamespaces.push('learning_track_nav')
}
} }
} else if (props.mainContext.page) { } else if (props.mainContext.page) {
// All articles that might have hover cards needs this // All articles that might have hover cards needs this