@@ -4,21 +4,52 @@ import Layout from "@theme/Layout";
44import Link from "@docusaurus/Link" ;
55import blogs from "../../database/blogs/index" ;
66import Head from "@docusaurus/Head" ;
7- import { getAuthorProfiles , getAuthorTooltip } from "../../utils/authors" ;
7+ import { getAuthorProfiles } from "../../utils/authors" ;
88import { filterBlogsBySearchTerm } from "../../utils/blogFilters" ;
99
1010import "./blogs-new.css" ;
1111
1212const POSTS_PER_PAGE = 12 ;
1313
14+ // Stable color per tag label (cycles through palette)
15+ const TAG_COLORS = [
16+ { dot : "#f59e0b" , border : "#fde68a" , bg : "#fffbeb" , text : "#92400e" } ,
17+ { dot : "#6366f1" , border : "#c7d2fe" , bg : "#eef2ff" , text : "#3730a3" } ,
18+ { dot : "#ec4899" , border : "#fbcfe8" , bg : "#fdf2f8" , text : "#9d174d" } ,
19+ { dot : "#10b981" , border : "#a7f3d0" , bg : "#ecfdf5" , text : "#065f46" } ,
20+ { dot : "#3b82f6" , border : "#bfdbfe" , bg : "#eff6ff" , text : "#1e40af" } ,
21+ { dot : "#8b5cf6" , border : "#ddd6fe" , bg : "#f5f3ff" , text : "#5b21b6" } ,
22+ { dot : "#f97316" , border : "#fed7aa" , bg : "#fff7ed" , text : "#9a3412" } ,
23+ { dot : "#14b8a6" , border : "#99f6e4" , bg : "#f0fdfa" , text : "#134e4a" } ,
24+ ] ;
25+
26+ function tagColor ( label : string ) {
27+ let hash = 0 ;
28+ for ( let i = 0 ; i < label . length ; i ++ )
29+ hash = label . charCodeAt ( i ) + ( ( hash << 5 ) - hash ) ;
30+ return TAG_COLORS [ Math . abs ( hash ) % TAG_COLORS . length ] ;
31+ }
32+
33+ function formatDate ( dateStr ?: string ) {
34+ if ( ! dateStr ) return "" ;
35+ const d = new Date ( dateStr ) ;
36+ if ( isNaN ( d . getTime ( ) ) ) return dateStr ;
37+ return d . toLocaleDateString ( "en-US" , {
38+ month : "short" ,
39+ day : "2-digit" ,
40+ year : "numeric" ,
41+ } ) ;
42+ }
43+
1444export default function Blogs ( ) {
1545 const { siteConfig } = useDocusaurusContext ( ) ;
1646 const [ searchInput , setSearchInput ] = React . useState ( "" ) ;
1747 const [ searchTerm , setSearchTerm ] = React . useState ( "" ) ;
1848 const [ currentPage , setCurrentPage ] = React . useState ( 1 ) ;
49+
1950 const filteredBlogs = React . useMemo (
2051 ( ) => filterBlogsBySearchTerm ( blogs , searchTerm ) ,
21- [ searchTerm ] ,
52+ [ searchTerm ]
2253 ) ;
2354
2455 React . useEffect ( ( ) => {
@@ -30,8 +61,9 @@ export default function Blogs() {
3061 const showingEnd = Math . min ( currentPage * POSTS_PER_PAGE , filteredBlogs . length ) ;
3162 const paginatedBlogs = filteredBlogs . slice (
3263 ( currentPage - 1 ) * POSTS_PER_PAGE ,
33- currentPage * POSTS_PER_PAGE ,
64+ currentPage * POSTS_PER_PAGE
3465 ) ;
66+
3567 const visiblePages = ( ( ) => {
3668 const pages : number [ ] = [ ] ;
3769 const maxVisible = 5 ;
@@ -44,6 +76,7 @@ export default function Blogs() {
4476 for ( let i = start ; i <= end ; i ++ ) pages . push ( i ) ;
4577 return pages ;
4678 } ) ( ) ;
79+
4780 const showLastPage = visiblePages [ visiblePages . length - 1 ] < totalPages ;
4881
4982 const handleSearchChange = ( e : { target : { value : string } } ) => {
@@ -67,11 +100,7 @@ export default function Blogs() {
67100 >
68101 < Head >
69102 < link rel = "preconnect" href = "https://fonts.googleapis.com" />
70- < link
71- rel = "preconnect"
72- href = "https://fonts.gstatic.com"
73- crossOrigin = "anonymous"
74- />
103+ < link rel = "preconnect" href = "https://fonts.gstatic.com" crossOrigin = "anonymous" />
75104 < link
76105 href = "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800& display = swap "
77106 rel = "stylesheet"
@@ -115,14 +144,9 @@ export default function Blogs() {
115144 < div className = "blog-search-panel" >
116145 < p className = "blog-search-eyebrow" > Explore articles</ p >
117146 < h2 className = "blog-search-title" > Find the right guide</ h2 >
118- < form
119- className = "blog-search-form"
120- onSubmit = { handleSearchSubmit }
121- >
147+ < form className = "blog-search-form" onSubmit = { handleSearchSubmit } >
122148 < label className = "blog-search-field" >
123- < span className = "blog-search-visually-hidden" >
124- Search blog articles
125- </ span >
149+ < span className = "blog-search-visually-hidden" > Search blog articles</ span >
126150 < svg
127151 className = "blog-search-submit-icon"
128152 viewBox = "0 0 24 24"
@@ -140,9 +164,7 @@ export default function Blogs() {
140164 onChange = { handleSearchChange }
141165 />
142166 </ label >
143- < button className = "blog-search-button" type = "submit" >
144- Search
145- </ button >
167+ < button className = "blog-search-button" type = "submit" > Search</ button >
146168 { searchTerm && (
147169 < button
148170 className = "blog-search-clear-button"
@@ -155,7 +177,6 @@ export default function Blogs() {
155177 </ form >
156178 </ div >
157179
158- { /* Search Results Counter */ }
159180 { searchTerm && (
160181 < div className = "search-results-info" >
161182 < p >
@@ -179,13 +200,8 @@ export default function Blogs() {
179200 < div className = "no-results-content" >
180201 < div className = "no-results-icon" > 🔍</ div >
181202 < h3 > No articles found</ h3 >
182- < p >
183- Try adjusting your search terms or browse all articles.
184- </ p >
185- < button
186- className = "clear-search-btn"
187- onClick = { handleClearFilters }
188- >
203+ < p > Try adjusting your search terms or browse all articles.</ p >
204+ < button className = "clear-search-btn" onClick = { handleClearFilters } >
189205 Clear Filters
190206 </ button >
191207 </ div >
@@ -209,21 +225,18 @@ export default function Blogs() {
209225 { visiblePages . map ( ( page ) => (
210226 < button
211227 key = { page }
212- className = { `pagination-number ${ currentPage === page ? "active-page" : ""
213- } `}
228+ className = { `pagination-number ${ currentPage === page ? "active-page" : "" } ` }
214229 aria-label = { `Go to page ${ page } ` }
215230 onClick = { ( ) => setCurrentPage ( page ) }
216231 >
217232 { page }
218233 </ button >
219234 ) ) }
220-
221235 { showLastPage && (
222236 < >
223237 < span className = "pagination-ellipsis" > ...</ span >
224238 < button
225- className = { `pagination-number ${ currentPage === totalPages ? "active-page" : ""
226- } `}
239+ className = { `pagination-number ${ currentPage === totalPages ? "active-page" : "" } ` }
227240 aria-label = { `Go to page ${ totalPages } ` }
228241 onClick = { ( ) => setCurrentPage ( totalPages ) }
229242 >
@@ -243,8 +256,7 @@ export default function Blogs() {
243256 </ button >
244257 </ div >
245258 < p className = "pagination-summary" >
246- Showing { showingStart } - { showingEnd } of{ " " }
247- { filteredBlogs . length } posts
259+ Showing { showingStart } - { showingEnd } of { filteredBlogs . length } posts
248260 </ p >
249261 </ div >
250262 ) }
@@ -256,92 +268,118 @@ export default function Blogs() {
256268 ) ;
257269}
258270
271+ // ─── BlogCard ────────────────────────────────────────────────────────────────
272+
259273const BlogCard = ( { blog } : { blog : ( typeof blogs ) [ number ] } ) => {
260274 const authors = getAuthorProfiles ( blog . authors || [ ] ) ;
275+ // Use only the first author for the bottom row (matches reference design)
276+ const primaryAuthor = authors [ 0 ] ;
277+
278+ // Tags — use blog.tags if present, fallback to blog.category as single tag
279+ const tags : string [ ] =
280+ Array . isArray ( ( blog as any ) . tags ) && ( blog as any ) . tags . length > 0
281+ ? ( blog as any ) . tags
282+ : blog . category
283+ ? [ blog . category ]
284+ : [ ] ;
261285
262286 return (
263287 < div className = "article-card" >
264- < div className = "card-category" > { blog . category } </ div >
288+ { /* ── Image ── */ }
265289 < div className = "card-image" >
266- < img src = { blog . image } alt = { blog . title } />
290+ < img src = { blog . image } alt = { blog . title } loading = "lazy" />
267291 </ div >
292+
293+ { /* ── Content ── */ }
268294 < div className = "card-content" >
295+ { /* Title */ }
269296 < h3 className = "card-title" >
270297 < Link to = { `/blog/${ blog . slug } ` } className = "card-title-link" >
271298 { blog . title }
272299 </ Link >
273300 </ h3 >
274- < p className = "card-description" > { blog . description } </ p >
275- < div className = "card-meta" >
276- < div className = "card-author" >
277- { /* Stacked Author Avatars */ }
278- { authors . length > 0 &&
279- ( ( ) => {
280- const max = 3 ;
281- const visible = authors . slice ( 0 , max ) ;
282- const extra = Math . max ( 0 , authors . length - max ) ;
283- return (
284- < div className = "author-stack" aria-hidden >
285- { visible . map ( ( a , i ) => (
286- < div
287- key = { a . id }
288- className = "author-stack-item"
289- style = { { zIndex : max - i } }
290- >
291- { a . imageUrl ? (
292- < img
293- src = { a . imageUrl }
294- alt = { a . name }
295- className = "author-stack-avatar"
296- onError = { ( e ) => {
297- const target = e . currentTarget ;
298- target . style . display = "none" ;
299- const fallback =
300- target . nextElementSibling as HTMLElement | null ;
301- if ( fallback ) fallback . style . display = "flex" ;
302- } }
303- />
304- ) : (
305- < span className = "author-stack-fallback" >
306- { a . name . charAt ( 0 ) . toUpperCase ( ) }
307- </ span >
308- ) }
309- </ div >
310- ) ) }
311- { extra > 0 && (
312- < div className = "author-stack-more" > +{ extra } </ div >
313- ) }
314- </ div >
315- ) ;
316- } ) ( ) }
317-
318- { /* Author Names */ }
319- < div className = "author-name-group" >
320- { authors . map ( ( author , authorIndex ) => (
321- < span key = { author . id } className = "author-item" >
322- { authorIndex > 0 && (
323- < span className = "author-separator" > ,</ span >
324- ) }
325- < Link
326- href = { author . githubUrl }
327- className = "author-name author-link"
328- target = "_blank"
329- rel = "noopener noreferrer"
330- data-author-tooltip = { getAuthorTooltip ( author . id ) }
331- aria-label = { `Open ${ author . name } on GitHub` }
332- >
333- { author . name }
334- </ Link >
301+
302+ { /* Description */ }
303+ { blog . description && (
304+ < p className = "card-description" > { blog . description } </ p >
305+ ) }
306+
307+ { /* Tag pills */ }
308+ { tags . length > 0 && (
309+ < div className = "card-tags" >
310+ { tags . slice ( 0 , 6 ) . map ( ( tag ) => {
311+ const c = tagColor ( tag ) ;
312+ return (
313+ < span
314+ key = { tag }
315+ className = "card-tag"
316+ style = { {
317+ "--tag-dot" : c . dot ,
318+ "--tag-border" : c . border ,
319+ "--tag-bg" : c . bg ,
320+ "--tag-text" : c . text ,
321+ } as React . CSSProperties }
322+ >
323+ < span className = "card-tag-dot" />
324+ { tag }
325+ </ span >
326+ ) ;
327+ } ) }
328+ </ div >
329+ ) }
330+
331+ { /* Bottom row: avatar + author + date ··· Read → */ }
332+ < div className = "card-footer" >
333+ < div className = "card-author-row" >
334+ { /* Avatar */ }
335+ { primaryAuthor && (
336+ < div className = "card-avatar" >
337+ { primaryAuthor . imageUrl ? (
338+ < img
339+ src = { primaryAuthor . imageUrl }
340+ alt = { primaryAuthor . name }
341+ className = "card-avatar-img"
342+ onError = { ( e ) => {
343+ const t = e . currentTarget ;
344+ t . style . display = "none" ;
345+ const fb = t . nextElementSibling as HTMLElement | null ;
346+ if ( fb ) fb . style . display = "flex" ;
347+ } }
348+ />
349+ ) : null }
350+ < span
351+ className = "card-avatar-fallback"
352+ style = { { display : primaryAuthor . imageUrl ? "none" : "flex" } }
353+ >
354+ { primaryAuthor . name . charAt ( 0 ) . toUpperCase ( ) }
335355 </ span >
336- ) ) }
356+ </ div >
357+ ) }
358+
359+ { /* Name + date stacked */ }
360+ < div className = "card-author-info" >
361+ { primaryAuthor && (
362+ < Link
363+ href = { primaryAuthor . githubUrl }
364+ className = "card-author-handle"
365+ target = "_blank"
366+ rel = "noopener noreferrer"
367+ >
368+ By @{ primaryAuthor . id || primaryAuthor . name . toLowerCase ( ) . replace ( / \s + / g, "" ) }
369+ </ Link >
370+ ) }
371+ { ( blog as any ) . date && (
372+ < span className = "card-date" > { formatDate ( ( blog as any ) . date ) } </ span >
373+ ) }
337374 </ div >
338375 </ div >
339- < span className = "card-read-time" > 5 min read</ span >
376+
377+ { /* Read link */ }
378+ < Link to = { `/blog/${ blog . slug } ` } className = "card-read-link" >
379+ Read →
380+ </ Link >
340381 </ div >
341- < Link to = { `/blog/${ blog . slug } ` } className = "card-read-more" >
342- Read Article →
343- </ Link >
344382 </ div >
345383 </ div >
346384 ) ;
347- } ;
385+ } ;
0 commit comments