added pagination controls, test

This commit is contained in:
FerrenF
2026-11-01 05:15:25 +00:00
parent bbb5efeb72
commit 806125f5e2
6 changed files with 485 additions and 12 deletions

View File

@@ -50,6 +50,7 @@ export type BlogPost = {
title: string;
description: string; // 1-2 sentence excerpt shown on cards
date: string; // display date, e.g. "2026-09-14"
category: string; // slug, e.g. "meta" — one category per post
tags: string[];
images?: GalleryImage[]; // optional, rendered via SimpleGallery
details: string; // markdown body
@@ -227,6 +228,7 @@ export const BLOG_POSTS: BlogPost[] = [
title: "Adding a blog tab to a portfolio site",
description: "A portfolio is a static-ish site with a lot of moving parts. Here's how I added a fourth tab to this very site without adding a single dependency.",
date: "2026-10-26",
category: "development",
tags: ["next.js", "react", "typescript"],
details: `# Adding a blog tab to a portfolio site
@@ -266,6 +268,7 @@ Editing a post from here on out is editing the array, which is the same as editi
title: "Hello, world. This is the new blog.",
description: "A short note about why this site grew a fourth tab, how the posts are written, and what the rest of this page demonstrates.",
date: "2026-09-14",
category: "meta",
tags: ["meta", "blog"],
details: `# Hello, world. This is the new blog.
@@ -294,6 +297,67 @@ This page is a dynamic route. The slug comes from the URL, the layout resolves i
}
];
/* ---- blog directory helpers ----
* Pagination and category filtering for the /blog list. Everything derives
* from BLOG_POSTS above, which remains the single source of truth. */
/** Small enough that pagination is visible as soon as a few posts exist. */
export const POSTS_PER_PAGE = 3;
/** Deduplicated, alphabetically sorted categories currently in use. */
export function blogCategories(): string[] {
return [...new Set(BLOG_POSTS.map((p) => p.category))].sort();
}
/** Slug -> display string: "development" -> "Development", "game-mods" -> "Game Mods". */
export function categoryLabel(slug: string): string {
return slug
.split(/[-_]+/)
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
/** All posts when omitted, otherwise the posts in the given category. */
export function postsForCategory(slug?: string): BlogPost[] {
if (!slug) return BLOG_POSTS;
return BLOG_POSTS.filter((p) => p.category === slug);
}
/**
* Pure pagination over an already-sorted array. Clamps `page` into
* [1, totalPages], so out-of-range values (?page=99) land on the last page
* instead of producing an empty or error state.
*/
export function paginate<T>(
items: T[],
page: number,
perPage: number
): {
items: T[];
page: number;
totalPages: number;
total: number;
startIndex: number;
endIndex: number;
} {
const total = items.length;
const safePerPage = Math.max(1, Math.floor(perPage));
const totalPages = Math.max(1, Math.ceil(total / safePerPage));
const requested = Number.isFinite(page) ? Math.floor(page) : NaN;
const clamped = Math.min(Math.max(1, requested || 1), totalPages);
const startIndex = total === 0 ? 0 : (clamped - 1) * safePerPage + 1;
const endIndex = Math.min(total, clamped * safePerPage);
return {
items: items.slice((clamped - 1) * safePerPage, clamped * safePerPage),
page: clamped,
totalPages,
total,
startIndex,
endIndex,
};
}
export const MOD_GAMES: ModdedGame[] = [{
id: 1,