1- import { Suspense } from "react" ;
21import { blog , getPageImage } from "@/lib/source" ;
3- import { BlogGrid } from "@/components/BlogGrid" ;
2+ import { BlogGrid , type BlogCardItem } from "@/components/BlogGrid" ;
3+ import { CategoryTagFilter } from "@/components/CategoryTagFilter" ;
44import { BLOG_HOME_DESCRIPTION , BLOG_HOME_TITLE } from "@/lib/blog-metadata" ;
55import type { Metadata } from "next" ;
66import { withBlogBasePath , withBlogBasePathForImageSrc } from "@/lib/url" ;
7+ import {
8+ Pagination ,
9+ PaginationContent ,
10+ PaginationEllipsis ,
11+ PaginationItem ,
12+ PaginationLink ,
13+ PaginationNext ,
14+ PaginationPrevious ,
15+ } from "@prisma/eclipse" ;
16+ import { LargeSearchToggle } from "@/components/search-toggle" ;
17+
18+ const SHOW_ALL = "show-all" ;
19+ const PAGE_SIZE = 12 ;
20+
21+ type BlogHomeSearchParams = {
22+ page ?: string | string [ ] ;
23+ tag ?: string | string [ ] ;
24+ } ;
25+
26+ function getFirstQueryValue ( value ?: string | string [ ] ) : string | undefined {
27+ if ( Array . isArray ( value ) ) {
28+ return value [ 0 ] ;
29+ }
30+
31+ return value ;
32+ }
33+
34+ function parsePage ( value : string | undefined ) : number {
35+ const parsed = Number . parseInt ( value ?? "1" , 10 ) ;
36+ return Number . isNaN ( parsed ) || parsed < 1 ? 1 : parsed ;
37+ }
38+
39+ function buildBlogHref ( tag : string , page : number ) : string {
40+ const params = new URLSearchParams ( ) ;
41+
42+ if ( tag !== SHOW_ALL ) {
43+ params . set ( "tag" , tag ) ;
44+ }
45+
46+ if ( page > 1 ) {
47+ params . set ( "page" , String ( page ) ) ;
48+ }
49+
50+ const query = params . toString ( ) ;
51+ const basePath = "/"
52+ return query ? `${ basePath } ?${ query } ` : basePath ;
53+ }
54+
55+ function getPaginationSequence ( totalPages : number , currentPage : number ) {
56+ if ( totalPages <= 7 ) {
57+ return Array . from ( { length : totalPages } , ( _ , index ) => index + 1 ) ;
58+ }
59+
60+ const pages : Array < number | "ellipsis" > = [ 1 ] ;
61+ const start = Math . max ( 2 , currentPage - 1 ) ;
62+ const end = Math . min ( totalPages - 1 , currentPage + 1 ) ;
63+
64+ if ( start > 2 ) {
65+ pages . push ( "ellipsis" ) ;
66+ }
67+
68+ for ( let page = start ; page <= end ; page += 1 ) {
69+ pages . push ( page ) ;
70+ }
71+
72+ if ( end < totalPages - 1 ) {
73+ pages . push ( "ellipsis" ) ;
74+ }
75+
76+ pages . push ( totalPages ) ;
77+ return pages ;
78+ }
779
880export async function generateMetadata ( ) : Promise < Metadata > {
981 return {
@@ -28,7 +100,12 @@ export async function generateMetadata(): Promise<Metadata> {
28100 } ;
29101}
30102
31- export default function BlogHome ( ) {
103+ export default async function BlogHome ( {
104+ searchParams,
105+ } : {
106+ searchParams ?: Promise < BlogHomeSearchParams > | BlogHomeSearchParams ;
107+ } ) {
108+ const resolvedSearchParams = ( await Promise . resolve ( searchParams ) ) ?? { } ;
32109 const posts = blog . getPages ( ) . sort ( ( a , b ) => {
33110 const aTime =
34111 a . data . date instanceof Date
@@ -48,7 +125,7 @@ export default function BlogHome() {
48125 } ;
49126
50127
51- const items = posts . map ( ( post ) => {
128+ const items : BlogCardItem [ ] = posts . map ( ( post ) => {
52129 const data = post . data as any ;
53130
54131 // Safely convert date to ISO string with validation
@@ -77,52 +154,93 @@ export default function BlogHome() {
77154 tags : data . tags ,
78155 } ;
79156 } ) ;
157+
80158 const uniqueTags = [
81- ...new Set ( items . filter ( ( item ) => item . tags ) . flatMap ( ( item ) => item . tags ) ) ,
159+ ...new Set (
160+ items
161+ . flatMap ( ( item ) => item . tags ?? [ ] )
162+ . filter ( ( tag ) : tag is string => Boolean ( tag ) ) ,
163+ ) ,
82164 ] ;
83-
165+ const validTags = new Set ( uniqueTags ) ;
166+ const tagFromQuery = getFirstQueryValue ( resolvedSearchParams . tag ) ;
167+ const currentCategory =
168+ tagFromQuery && validTags . has ( tagFromQuery ) ? tagFromQuery : SHOW_ALL ;
169+ const filteredItems =
170+ currentCategory === SHOW_ALL
171+ ? items
172+ : items . filter ( ( item ) => item . tags ?. includes ( currentCategory ) ) ;
173+
174+ const totalPages = Math . max ( 1 , Math . ceil ( filteredItems . length / PAGE_SIZE ) ) ;
175+ const pageFromQuery = parsePage ( getFirstQueryValue ( resolvedSearchParams . page ) ) ;
176+ const currentPage = Math . max ( 1 , Math . min ( pageFromQuery , totalPages ) ) ;
177+
178+ const shouldShowFeatured = currentCategory === SHOW_ALL && currentPage === 1 ;
179+ const featuredPost = shouldShowFeatured ? filteredItems [ 0 ] : undefined ;
180+ const postsToRender = shouldShowFeatured
181+ ? filteredItems . slice ( 1 , PAGE_SIZE )
182+ : filteredItems . slice ( ( currentPage - 1 ) * PAGE_SIZE , currentPage * PAGE_SIZE ) ;
183+
184+ const paginationSequence = getPaginationSequence ( totalPages , currentPage ) ;
185+
84186 return (
85187 < main className = "flex-1 w-full max-w-249 mx-auto px-4 py-8 z-1" >
86188 < h1 className = "stretch-display text-4xl font-bold mb-2 landing-h1 text-center mt-9 font-sans-display" >
87189 Blog
88190 </ h1 >
89- { /* Category pills (static "Show all" to match layout) */ }
90191 < div className = "pt-6 pb-12 mt-10" >
91- { /* Grid with pagination */ }
92- < Suspense
93- fallback = {
94- < div className = "animate-pulse" >
95- < div className = "flex justify-between items-center gap-4 mb-8" >
96- < div className = "flex flex-wrap gap-2" >
97- { [ ...Array ( 5 ) ] . map ( ( _ , index ) => (
98- < div
99- key = { `pill-${ index } ` }
100- className = "h-8 w-20 rounded-full bg-fd-secondary border border-fd-primary/20"
101- />
102- ) ) }
103- </ div >
104- < div className = "h-10 w-20 md:w-52 rounded-full bg-fd-secondary border border-fd-primary/20" />
105- </ div >
106-
107- < div className = "rounded-square border border-fd-primary/20 bg-fd-secondary h-64 md:h-80 mb-12" />
108-
109- < div className = "grid gap-6 mt-12 grid-cols-1" >
110- { items . slice ( 0 , 6 ) . map ( ( post ) => (
111- < div
112- key = { post . url }
113- className = "h-44 border-b border-fd-primary/20 bg-fd-secondary/60"
114- />
115- ) ) }
116- </ div >
117- </ div >
118- }
119- >
120- < BlogGrid
121- items = { items }
122- pageSize = { 12 }
192+ < div className = "flex justify-between items-center gap-4 mb-8" >
193+ < CategoryTagFilter
123194 uniqueTags = { uniqueTags }
195+ currentCategory = { currentCategory }
196+ className = "flex justify-center flex-wrap gap-1"
124197 />
125- </ Suspense >
198+ < LargeSearchToggle className = "w-20 h-full md:w-52" />
199+ </ div >
200+
201+ < BlogGrid
202+ items = { postsToRender }
203+ featuredPost = { featuredPost }
204+ currentCategory = { currentCategory }
205+ />
206+
207+ < div className = "mt-8" >
208+ { totalPages > 1 ? (
209+ < Pagination >
210+ < PaginationContent >
211+ < PaginationItem >
212+ < PaginationPrevious
213+ href = { buildBlogHref ( currentCategory , Math . max ( 1 , currentPage - 1 ) ) }
214+ aria-disabled = { currentPage === 1 }
215+ />
216+ </ PaginationItem >
217+ { paginationSequence . map ( ( entry , index ) => (
218+ < PaginationItem key = { `${ entry } -${ index } ` } >
219+ { entry === "ellipsis" ? (
220+ < PaginationEllipsis />
221+ ) : (
222+ < PaginationLink
223+ href = { buildBlogHref ( currentCategory , entry ) }
224+ isActive = { entry === currentPage }
225+ >
226+ { entry }
227+ </ PaginationLink >
228+ ) }
229+ </ PaginationItem >
230+ ) ) }
231+ < PaginationItem >
232+ < PaginationNext
233+ href = { buildBlogHref (
234+ currentCategory ,
235+ Math . min ( totalPages , currentPage + 1 ) ,
236+ ) }
237+ aria-disabled = { currentPage === totalPages }
238+ />
239+ </ PaginationItem >
240+ </ PaginationContent >
241+ </ Pagination >
242+ ) : null }
243+ </ div >
126244 </ div >
127245 </ main >
128246 ) ;
0 commit comments