Allow a category landing page for Copilot Cookbook (#53002)
This commit is contained in:
62
src/frame/components/context/CategoryLandingContext.tsx
Normal file
62
src/frame/components/context/CategoryLandingContext.tsx
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ const layoutNames = [
|
|||||||
'product-guides',
|
'product-guides',
|
||||||
'release-notes',
|
'release-notes',
|
||||||
'inline',
|
'inline',
|
||||||
|
'category-landing',
|
||||||
false,
|
false,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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 } : {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
src/landings/components/CategoryLanding.tsx
Normal file
67
src/landings/components/CategoryLanding.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user