added pagination controls, test
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user