From e35b42c4d9a152a1a1b38c95632a11bb09ffa047 Mon Sep 17 00:00:00 2001
From: Maximilian Kaske <56969857+mxkaske@users.noreply.github.com>
Date: Thu, 6 Nov 2025 13:42:46 +0100
Subject: [PATCH] chore: blog category selector (#1540)
* chore: blog category selector
* fix: all categories
* fix: height
---
.../blog/_components/category-select.tsx | 44 +++++++++++++++++++
.../src/app/(pages)/(content)/blog/page.tsx | 40 ++++++++++++-----
.../(pages)/(content)/blog/search-params.ts | 18 ++++++--
apps/web/src/components/content/timeline.tsx | 2 +-
4 files changed, 89 insertions(+), 15 deletions(-)
create mode 100644 apps/web/src/app/(pages)/(content)/blog/_components/category-select.tsx
diff --git a/apps/web/src/app/(pages)/(content)/blog/_components/category-select.tsx b/apps/web/src/app/(pages)/(content)/blog/_components/category-select.tsx
new file mode 100644
index 00000000..36a0edd2
--- /dev/null
+++ b/apps/web/src/app/(pages)/(content)/blog/_components/category-select.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@openstatus/ui/src/components/select";
+import { allPosts } from "content-collections";
+import { useQueryStates } from "nuqs";
+import { searchParamsParsers } from "../search-params";
+
+const categories = new Set(allPosts.map((post) => post.tag));
+
+export function CategorySelect() {
+ const [{ category }, setSearchParams] = useQueryStates(searchParamsParsers, {
+ shallow: false,
+ });
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(pages)/(content)/blog/page.tsx b/apps/web/src/app/(pages)/(content)/blog/page.tsx
index f3b9b0c8..e9332173 100644
--- a/apps/web/src/app/(pages)/(content)/blog/page.tsx
+++ b/apps/web/src/app/(pages)/(content)/blog/page.tsx
@@ -15,9 +15,10 @@ import { allPosts } from "content-collections";
import { Rss } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
+import { CategorySelect } from "./_components/category-select";
import {
ITEMS_PER_PAGE,
- MAX_PAGE_INDEX,
+ getMaxPageIndex,
searchParamsCache,
} from "./search-params";
@@ -38,28 +39,45 @@ export default async function Post(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
- const { pageIndex } = searchParamsCache.parse(searchParams);
+ const { pageIndex, category } = searchParamsCache.parse(searchParams);
+
+ const maxPageIndex = getMaxPageIndex(category);
+ const currentPageIndex = Math.min(pageIndex, maxPageIndex);
const posts = allPosts
.sort(
(a, b) =>
new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime(),
)
- .slice(pageIndex * ITEMS_PER_PAGE, (pageIndex + 1) * ITEMS_PER_PAGE);
+ .filter((post) => {
+ if (!category) return true;
+ return post.tag === category;
+ })
+ .slice(
+ currentPageIndex * ITEMS_PER_PAGE,
+ (currentPageIndex + 1) * ITEMS_PER_PAGE,
+ );
return (
+ actions={[
+ ,
+
- }
+ ,
+ ]}
>
{posts.map((post) => (
- {Array.from({ length: MAX_PAGE_INDEX + 1 }).map((_, index) => {
+ {Array.from({ length: maxPageIndex + 1 }).map((_, index) => {
return (
{index + 1}
diff --git a/apps/web/src/app/(pages)/(content)/blog/search-params.ts b/apps/web/src/app/(pages)/(content)/blog/search-params.ts
index d47bfc31..02abc3b3 100644
--- a/apps/web/src/app/(pages)/(content)/blog/search-params.ts
+++ b/apps/web/src/app/(pages)/(content)/blog/search-params.ts
@@ -3,24 +3,34 @@ import {
createParser,
createSearchParamsCache,
parseAsInteger,
+ parseAsStringEnum,
} from "nuqs/server";
+export const ITEMS_PER_PAGE = 10;
+
+// Helper function to calculate max page index based on filtered posts
+export function getMaxPageIndex(category?: string | null) {
+ let filteredPosts = allPosts;
+ if (category) {
+ filteredPosts = allPosts.filter((post) => post.tag === category);
+ }
+ return Math.max(0, Math.ceil(filteredPosts.length / ITEMS_PER_PAGE) - 1);
+}
+
const parseAsPageIndex = createParser({
parse(queryValue) {
const parsed = parseAsInteger.parse(queryValue);
if (!parsed || parsed < 0) return 0;
- return Math.min(parsed, MAX_PAGE_INDEX);
+ return parsed; // We'll validate against max page index in the component
},
serialize(value) {
return value.toString();
},
});
-export const ITEMS_PER_PAGE = 10;
-export const MAX_PAGE_INDEX = Math.ceil(allPosts.length / ITEMS_PER_PAGE) - 1;
-
export const searchParamsParsers = {
pageIndex: parseAsPageIndex.withDefault(0),
+ category: parseAsStringEnum(allPosts.map((post) => post.tag)),
};
export const searchParamsCache = createSearchParamsCache(searchParamsParsers);
diff --git a/apps/web/src/components/content/timeline.tsx b/apps/web/src/components/content/timeline.tsx
index 89174c4d..329cddf6 100644
--- a/apps/web/src/components/content/timeline.tsx
+++ b/apps/web/src/components/content/timeline.tsx
@@ -25,7 +25,7 @@ export function Timeline({
{title}
{description}
- {actions}
+ {actions}
{children}
--
2.51.2