Skip to content

Commit

Permalink
Add filters to blog page (nrwl#26997)
Browse files Browse the repository at this point in the history
Co-authored-by: Juri <[email protected]>
  • Loading branch information
ndcunningham and juristr authored Jul 23, 2024
1 parent 157aca4 commit 3b1deb1
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 18 deletions.
2 changes: 1 addition & 1 deletion docs/blog/2024-03-20-why-speed-matters.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Monorepos - Why Speed Matters
slug: 'monorepos-why-speed-matters'
authors: ['Katerina Skroumpelou', 'Jeff Cross']
tags: nx, nxdevtools, speed, ci
tags: [nx, nxdevtools, speed, ci]
cover_image: '/blog/images/2024-03-20/featured_img.png'
---

Expand Down
9 changes: 9 additions & 0 deletions nx-dev/data-access-documents/src/lib/blog.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export class BlogApi {
}
}

async getBlogTags(): Promise<string[]> {
const blogs = await this.getBlogs();
const tags = new Set<string>();
blogs.forEach((blog) => {
blog.tags.forEach((tag) => tags.add(tag));
});
return Array.from(tags);
}

async getBlogs(): Promise<BlogPostDataEntry[]> {
const files: string[] = await readdir(this.options.blogRoot);
const authors = JSON.parse(
Expand Down
5 changes: 4 additions & 1 deletion nx-dev/nx-dev/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata, ResolvingMetadata } from 'next';
import { blogApi } from '../../../lib/blog.api';
import { BlogDetails } from '@nx/nx-dev/ui-blog';
import { DefaultLayout } from '@nx/nx-dev/ui-common';
interface BlogPostDetailProps {
params: { slug: string };
}
Expand Down Expand Up @@ -48,7 +49,9 @@ export default async function BlogPostDetail({
<>
{/* This empty div is necessary as app router does not automatically scroll on route changes */}
<div></div>
<BlogDetails post={blog} />
<DefaultLayout>
<BlogDetails post={blog} />
</DefaultLayout>
</>
) : null;
}
14 changes: 11 additions & 3 deletions nx-dev/nx-dev/app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from 'next';
import { blogApi } from '../../lib/blog.api';
import { BlogContainer } from '@nx/nx-dev/ui-blog';
import { DefaultLayout } from '@nx/nx-dev/ui-common';
import { Suspense } from 'react';

export const metadata: Metadata = {
title: 'Nx Blog - Updates from the Nx & Nx Cloud team',
Expand All @@ -28,11 +29,18 @@ async function getBlogs() {
return await blogApi.getBlogPosts();
}

async function getBlogTags() {
return await blogApi.getBlogTags();
}

export default async function BlogIndex() {
const blogs = await getBlogs();
const tags = await getBlogTags();
return (
<DefaultLayout>
<BlogContainer blogPosts={blogs} />
</DefaultLayout>
<Suspense>
<DefaultLayout>
<BlogContainer blogPosts={blogs} tags={tags} />
</DefaultLayout>
</Suspense>
);
}
158 changes: 146 additions & 12 deletions nx-dev/ui-blog/src/lib/blog-container.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,161 @@
'use client';
import { BlogPostDataEntry } from '@nx/nx-dev/data-access-documents/node-only';
import { MoreBlogs } from './more-blogs';
import { FeaturedBlogs } from './featured-blogs';
import { useEffect, useMemo, useState } from 'react';
import { Filters } from './filters';
import { useSearchParams } from 'next/navigation';
import {
ComputerDesktopIcon,
BookOpenIcon,
MicrophoneIcon,
CubeIcon,
AcademicCapIcon,
ChatBubbleOvalLeftEllipsisIcon,
ListBulletIcon,
} from '@heroicons/react/24/outline';

export interface BlogContainerProps {
blogPosts: BlogPostDataEntry[];
tags: string[];
}
let ALL_TOPICS = [
{
label: 'All',
icon: ListBulletIcon,
value: 'All',
heading: 'All Blogs',
},
{
label: 'Stories',
icon: BookOpenIcon,
value: 'customer story',
heading: 'Customer Stories',
},
{
label: 'Webinars',
icon: ComputerDesktopIcon,
value: 'webinar',
heading: 'Webinars',
},
{
label: 'Podcasts',
icon: MicrophoneIcon,
value: 'podcast',
heading: 'Podcasts',
},
{
label: 'Releases',
icon: CubeIcon,
value: 'release',
heading: 'Release Blogs',
},
{
label: 'Talks',
icon: ChatBubbleOvalLeftEllipsisIcon,
value: 'talk',
heading: 'Talks',
},
{
label: 'Tutorials',
icon: AcademicCapIcon,
value: 'tutorial',
heading: 'Tutorials',
},
];
export function BlogContainer({ blogPosts, tags }: BlogContainerProps) {
const searchParams = useSearchParams();
const [filteredList, setFilteredList] = useState(blogPosts);

// Only show filters that have blog posts
const filters = useMemo(() => {
return [
ALL_TOPICS[0],
...ALL_TOPICS.filter((filter) => tags.includes(filter.value)),
];
}, [tags]);

const {
initialFirstFive,
initialRest,
initialSelectedFilterHeading,
initialSelectedFilter,
} = useMemo(
() => initializeFilters(blogPosts, searchParams),
[blogPosts, searchParams]
);

const [firstFiveBlogs, setFirstFiveBlogs] =
useState<BlogPostDataEntry[]>(initialFirstFive);
const [remainingBlogs, setRemainingBlogs] =
useState<BlogPostDataEntry[]>(initialRest);
const [selectedFilterHeading, setSelectedFilterHeading] = useState(
initialSelectedFilterHeading
);

function updateBlogPosts() {
setFirstFiveBlogs(
filteredList.slice(0, filteredList.length > 5 ? 5 : filteredList.length)
);
setRemainingBlogs(filteredList.length > 5 ? filteredList.slice(5) : []);
}

useEffect(() => updateBlogPosts(), [filteredList]);

export function BlogContainer({ blogPosts }: BlogContainerProps) {
const [blog1, blog2, blog3, blog4, blog5, ...restOfPosts] = blogPosts;
return (
<main id="main" role="main" className="w-full py-8">
<div className="mx-auto mb-8 w-full max-w-[1088px] px-8">
<header className="mx-auto mb-16">
<h1
id="blog-title"
className="text-xl font-semibold tracking-tight text-slate-900 md:text-2xl dark:text-slate-100"
>
Blog
</h1>
</header>
<FeaturedBlogs blogs={[blog1, blog2, blog3, blog4, blog5]} />
{restOfPosts.length > 0 ? <MoreBlogs blogs={restOfPosts} /> : null}
<div className="mb-12 mt-20 flex items-center justify-between">
<header>
<h1
id="blog-title"
className="text-xl font-semibold tracking-tight text-slate-900 md:text-2xl dark:text-slate-100"
>
{selectedFilterHeading}
</h1>
</header>
<div className="flex items-center justify-end md:justify-start">
<Filters
blogs={blogPosts}
filters={filters}
initialSelectedFilter={initialSelectedFilter}
setFilteredList={setFilteredList}
setSelectedFilterHeading={setSelectedFilterHeading}
/>
</div>
</div>
<FeaturedBlogs blogs={firstFiveBlogs} />
{!!remainingBlogs.length && <MoreBlogs blogs={remainingBlogs} />}
</div>
</main>
);
}

function initializeFilters(
blogPosts: BlogPostDataEntry[],
searchParams: URLSearchParams
) {
const filterBy = searchParams.get('filterBy');

const defaultState = {
initialFirstFive: blogPosts.slice(0, 5),
initialRest: blogPosts.slice(5),
initialSelectedFilterHeading: 'All Blogs',
initialSelectedFilter: 'All',
};

if (!filterBy) {
return defaultState;
}

const result = blogPosts.filter((post) => post.tags.includes(filterBy));

const initialFilter = ALL_TOPICS.find((filter) => filter.value === filterBy);

return {
initialFirstFive: result.slice(0, 5),
initialRest: result.length > 5 ? result.slice(5) : [],
initialSelectedFilterHeading: initialFilter?.heading || 'All Blogs',
initialSelectedFilter: initialFilter?.value || 'All',
};
}
2 changes: 1 addition & 1 deletion nx-dev/ui-blog/src/lib/featured-blogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function FeaturedBlogs({ blogs }: FeaturedBlogsProps) {
<div className="grid grid-cols-6 gap-6">
{blogs.map((blog, index) => (
<div
key={index}
key={blog.title}
className={`col-span-6 ${
index <= 1 ? 'md:col-span-3' : 'sm:col-span-3'
} ${index > 0 ? 'md:col-span-2' : ''}`}
Expand Down
Loading

0 comments on commit 3b1deb1

Please sign in to comment.