diff --git a/src/landings/components/CategoryLanding.tsx b/src/landings/components/CategoryLanding.tsx index 7e62560830..e2d12bafeb 100644 --- a/src/landings/components/CategoryLanding.tsx +++ b/src/landings/components/CategoryLanding.tsx @@ -35,7 +35,10 @@ export const CategoryLanding = () => { if (typeof value === 'string') { return value.toLowerCase().includes(searchQuery.toLowerCase()) } else if (Array.isArray(value)) { - return value.some((item) => item.toLowerCase().includes(searchQuery.toLowerCase())) + return value.some( + (item) => + typeof item === 'string' && item.toLowerCase().includes(searchQuery.toLowerCase()), + ) } return false }) diff --git a/src/landings/components/bespoke/BespokeLanding.tsx b/src/landings/components/bespoke/BespokeLanding.tsx index cd97570b5b..5dc38b3589 100644 --- a/src/landings/components/bespoke/BespokeLanding.tsx +++ b/src/landings/components/bespoke/BespokeLanding.tsx @@ -1,5 +1,3 @@ -import { useMemo } from 'react' - import { DefaultLayout } from '@/frame/components/DefaultLayout' import { useLandingContext } from '@/landings/context/LandingContext' import { LandingHero } from '@/landings/components/shared/LandingHero' @@ -7,16 +5,9 @@ import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWith import { UtmPreserver } from '@/frame/components/UtmPreserver' import { LandingCarousel } from '@/landings/components/shared/LandingCarousel' -import type { ArticleCardItems } from '@/landings/types' - export const BespokeLanding = () => { const { title, intro, heroImage, introLinks, tocItems, recommended } = useLandingContext() - const flatArticles: ArticleCardItems = useMemo( - () => tocItems.flatMap((item) => item.childTocItems || []), - [tocItems], - ) - return ( @@ -25,7 +16,7 @@ export const BespokeLanding = () => {
- +
diff --git a/src/landings/components/discovery/DiscoveryLanding.tsx b/src/landings/components/discovery/DiscoveryLanding.tsx index 105b671b81..c8fe5a2d59 100644 --- a/src/landings/components/discovery/DiscoveryLanding.tsx +++ b/src/landings/components/discovery/DiscoveryLanding.tsx @@ -1,5 +1,3 @@ -import { useMemo } from 'react' - import { DefaultLayout } from '@/frame/components/DefaultLayout' import { useLandingContext } from '@/landings/context/LandingContext' import { LandingHero } from '@/landings/components/shared/LandingHero' @@ -7,16 +5,9 @@ import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWith import { LandingCarousel } from '@/landings/components/shared/LandingCarousel' import { UtmPreserver } from '@/frame/components/UtmPreserver' -import type { ArticleCardItems } from '@/landings/types' - export const DiscoveryLanding = () => { const { title, intro, heroImage, introLinks, tocItems, recommended } = useLandingContext() - const flatArticles: ArticleCardItems = useMemo( - () => tocItems.flatMap((item) => item.childTocItems || []), - [tocItems], - ) - return ( @@ -24,7 +15,7 @@ export const DiscoveryLanding = () => {
- +
diff --git a/src/landings/components/shared/LandingArticleGridWithFilter.tsx b/src/landings/components/shared/LandingArticleGridWithFilter.tsx index abc3d20205..841f86bd81 100644 --- a/src/landings/components/shared/LandingArticleGridWithFilter.tsx +++ b/src/landings/components/shared/LandingArticleGridWithFilter.tsx @@ -1,20 +1,44 @@ -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, useMemo } from 'react' import { TextInput, ActionMenu, ActionList, Token, Pagination } from '@primer/react' import { SearchIcon } from '@primer/octicons-react' import cx from 'classnames' import { Link } from '@/frame/components/Link' import { useTranslation } from '@/languages/components/useTranslation' -import { ArticleCardItems, ChildTocItem } from '@/landings/types' +import { ArticleCardItems, ChildTocItem, TocItem } from '@/landings/types' import styles from './LandingArticleGridWithFilter.module.scss' type ArticleGridProps = { - flatArticles: ArticleCardItems + tocItems: TocItem[] } const ALL_CATEGORIES = 'all_categories' +// Helper function to recursively flatten nested articles +// Excludes index pages (pages with childTocItems) +const flattenArticlesRecursive = (articles: ArticleCardItems): ArticleCardItems => { + const flattened: ArticleCardItems = [] + + for (const article of articles) { + // If the article has children, recursively process them but don't include the parent (index page) + if (article.childTocItems && article.childTocItems.length > 0) { + flattened.push(...flattenArticlesRecursive(article.childTocItems)) + } else { + // Only add articles that don't have children (actual article pages, not index pages) + flattened.push(article) + } + } + + return flattened +} + +// Wrapper function that flattens and sorts alphabetically by title (only once) +const flattenArticles = (articles: ArticleCardItems): ArticleCardItems => { + const flattened = flattenArticlesRecursive(articles) + return flattened.sort((a, b) => a.title.localeCompare(b.title)) +} + // Hook to get current articles per page based on screen size const useResponsiveArticlesPerPage = () => { const [articlesPerPage, setArticlesPerPage] = useState(9) // Default to desktop @@ -42,7 +66,7 @@ const useResponsiveArticlesPerPage = () => { return articlesPerPage } -export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => { +export const ArticleGrid = ({ tocItems }: ArticleGridProps) => { const { t } = useTranslation('product_landing') const [searchQuery, setSearchQuery] = useState('') const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORIES) @@ -53,6 +77,12 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => { const inputRef = useRef(null) const headingRef = useRef(null) + // Extract child items from tocItems and recursively flatten nested articles to ensure we get all articles with categories + const allArticles = useMemo( + () => flattenArticles(tocItems.flatMap((item) => item.childTocItems || [])), + [tocItems], + ) + // Reset to first page when articlesPerPage changes (screen size changes) useEffect(() => { setCurrentPage(1) @@ -61,13 +91,13 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => { // Extract unique categories from the articles const categories: string[] = [ ALL_CATEGORIES, - ...Array.from(new Set(flatArticles.flatMap((item) => item.category || []))).sort((a, b) => + ...Array.from(new Set(allArticles.flatMap((item) => item.category || []))).sort((a, b) => a.localeCompare(b), ), ] const applyFilters = () => { - let results = flatArticles + let results = allArticles if (searchQuery) { results = results.filter((token) => { diff --git a/src/landings/types.ts b/src/landings/types.ts index e468cc8911..f6e0ddcfb7 100644 --- a/src/landings/types.ts +++ b/src/landings/types.ts @@ -12,11 +12,13 @@ export type BaseTocItem = { } // Extended type for child TOC items with additional metadata +// This is recursive - children can also have their own children export type ChildTocItem = BaseTocItem & { octicon?: ValidOcticon | null category?: string[] | null complexity?: string[] | null industry?: string[] | null + childTocItems?: ChildTocItem[] } // Main TOC item type that can contain children