11"use client" ;
22import React from "react" ;
33import Link from "@docusaurus/Link" ;
4- import { getAuthorProfiles , getAuthorTooltip } from "../../utils/authors" ;
4+ import { getAuthorProfiles } from "../../utils/authors" ;
55
66interface BlogCardProps {
77 type : string ;
@@ -11,140 +11,172 @@ interface BlogCardProps {
1111 imageUrl : string ;
1212 id : string ;
1313 authors ?: string [ ] ;
14+ tags ?: string [ ] ;
15+ category ?: string ;
16+ }
17+
18+ const TAG_COLORS = [
19+ { dot : "#f59e0b" , border : "#fde68a" , bg : "#fffbeb" , text : "#92400e" } ,
20+ { dot : "#6366f1" , border : "#c7d2fe" , bg : "#eef2ff" , text : "#3730a3" } ,
21+ { dot : "#ec4899" , border : "#fbcfe8" , bg : "#fdf2f8" , text : "#9d174d" } ,
22+ { dot : "#10b981" , border : "#a7f3d0" , bg : "#ecfdf5" , text : "#065f46" } ,
23+ { dot : "#3b82f6" , border : "#bfdbfe" , bg : "#eff6ff" , text : "#1e40af" } ,
24+ { dot : "#8b5cf6" , border : "#ddd6fe" , bg : "#f5f3ff" , text : "#5b21b6" } ,
25+ { dot : "#f97316" , border : "#fed7aa" , bg : "#fff7ed" , text : "#9a3412" } ,
26+ { dot : "#14b8a6" , border : "#99f6e4" , bg : "#f0fdfa" , text : "#134e4a" } ,
27+ ] ;
28+
29+ function tagColor ( label : string ) {
30+ let hash = 0 ;
31+ for ( let i = 0 ; i < label . length ; i ++ )
32+ hash = label . charCodeAt ( i ) + ( ( hash << 5 ) - hash ) ;
33+ return TAG_COLORS [ Math . abs ( hash ) % TAG_COLORS . length ] ;
34+ }
35+
36+ function formatDate ( dateStr ?: string ) {
37+ if ( ! dateStr ) return "" ;
38+ const d = new Date ( dateStr ) ;
39+ if ( isNaN ( d . getTime ( ) ) ) return dateStr ;
40+ return d . toLocaleDateString ( "en-US" , {
41+ month : "short" ,
42+ day : "2-digit" ,
43+ year : "numeric" ,
44+ } ) ;
1445}
1546
1647const BlogCard = ( {
1748 type,
49+ date,
1850 title,
19- content,
2051 imageUrl,
2152 id,
2253 authors,
54+ tags,
55+ category,
2356} : BlogCardProps ) => {
2457 const authorProfiles = getAuthorProfiles ( authors || [ ] ) ;
2558
2659 if ( ! id || ! type ) {
2760 return < div > data not fetched properly, imageId and entryId not found</ div > ;
2861 }
2962
30- // Get category from title for demo purposes
31- const getCategory = ( title ) => {
32- if (
33- title . toLowerCase ( ) . includes ( "design" ) ||
34- title . toLowerCase ( ) . includes ( "ux" )
35- )
36- return "Design" ;
37- if (
38- title . toLowerCase ( ) . includes ( "ai" ) ||
39- title . toLowerCase ( ) . includes ( "deepmind" )
40- )
41- return "AI & Tech" ;
42- if (
43- title . toLowerCase ( ) . includes ( "github" ) ||
44- title . toLowerCase ( ) . includes ( "git" )
45- )
46- return "Development" ;
47- return "Resources" ;
48- } ;
63+ // Tags: use tags array first, then category, skip generic "blog"
64+ const rawTags = Array . isArray ( tags ) && tags . length > 0
65+ ? tags
66+ : category
67+ ? [ category ]
68+ : [ ] ;
4969
50- const category = getCategory ( title ) ;
70+ const tagList = rawTags . filter (
71+ ( t ) => t && t . toLowerCase ( ) !== "blog" && t . toLowerCase ( ) !== "post"
72+ ) ;
5173
5274 return (
53- < div className = "relative h-full overflow-hidden transition-all duration-300" >
54- < div className = "article-card h-full" >
55- { /* Category Badge */ }
56- < div className = "card-category" > { category } </ div >
57-
58- { /* Card Image */ }
59- < div className = "card-image" >
60- < img src = { imageUrl } alt = { title } />
61- </ div >
75+ < div className = "article-card" >
76+ { /* Image */ }
77+ < div className = "card-image" >
78+ < img src = { imageUrl } alt = { title } loading = "lazy" />
79+ </ div >
6280
63- { /* Card Content */ }
64- < div className = "card-content" >
65- < h3 className = "card-title" >
66- < Link to = { `/blog/ ${ id } ` } className = "card-title-link " >
67- { title }
68- </ Link >
69- </ h3 >
70- < p className = "card-description" > { content } </ p >
81+ { /* Content */ }
82+ < div className = "card-content" >
83+ { /* Title */ }
84+ < h3 className = "card-title" >
85+ < Link to = { `/blog/ ${ id } ` } className = "card-title-link" >
86+ { title }
87+ </ Link >
88+ </ h3 >
7189
72- { /* Card Meta */ }
73- < div className = "card-meta" >
74- < div className = "card-author" >
75- { /* Stacked Author Avatars */ }
76- { authorProfiles . length > 0 &&
77- ( ( ) => {
78- const max = 3 ;
79- const visible = authorProfiles . slice ( 0 , max ) ;
80- const extra = Math . max ( 0 , authorProfiles . length - max ) ;
81- return (
82- < div className = "author-stack" aria-hidden >
83- { visible . map ( ( a , i ) => (
84- < div
85- key = { a . id }
86- className = "author-stack-item"
87- style = { { zIndex : max - i } }
88- >
89- { a . imageUrl ? (
90- < img
91- src = { a . imageUrl }
92- alt = { a . name }
93- className = "author-stack-avatar"
94- onError = { ( e ) => {
95- const target = e . currentTarget ;
96- target . style . display = "none" ;
97- const fallback =
98- target . nextElementSibling as HTMLElement | null ;
99- if ( fallback ) fallback . style . display = "flex" ;
100- } }
101- />
102- ) : (
103- < span className = "author-stack-fallback" >
104- { a . name . charAt ( 0 ) . toUpperCase ( ) }
105- </ span >
106- ) }
107- </ div >
108- ) ) }
109- { extra > 0 && (
110- < div className = "author-stack-more" > +{ extra } </ div >
111- ) }
112- </ div >
113- ) ;
114- } ) ( ) }
90+ { /* Tag pills */ }
91+ { tagList . length > 0 && (
92+ < div className = "card-tags" >
93+ { tagList . slice ( 0 , 5 ) . map ( ( tag ) => {
94+ const c = tagColor ( tag ) ;
95+ return (
96+ < span
97+ key = { tag }
98+ className = "card-tag"
99+ style = { {
100+ "--tag-dot" : c . dot ,
101+ "--tag-border" : c . border ,
102+ "--tag-bg" : c . bg ,
103+ "--tag-text" : c . text ,
104+ } as React . CSSProperties }
105+ >
106+ < span className = "card-tag-dot" />
107+ { tag }
108+ </ span >
109+ ) ;
110+ } ) }
111+ </ div >
112+ ) }
115113
116- { /* Author Names */ }
117- < div className = "author-name-group" >
118- { authorProfiles . map ( ( author , authorIndex ) => (
119- < span key = { author . id } className = "author-item" >
120- { authorIndex > 0 && (
121- < span className = "author-separator" > ,</ span >
122- ) }
123- < Link
124- href = { author . githubUrl }
125- className = "author-name author-link"
126- target = "_blank"
127- rel = "noopener noreferrer"
128- data-author-tooltip = { getAuthorTooltip ( author . id ) }
129- aria-label = { `Open ${ author . name } on GitHub` }
114+ { /* Footer: avatars + author names/date + Read → */ }
115+ < div className = "card-footer" >
116+ < div className = "card-author-row" >
117+ { /* Avatar stack — shows all authors overlapped */ }
118+ { authorProfiles . length > 0 && (
119+ < div className = "card-avatar-stack" >
120+ { authorProfiles . map ( ( author , i ) => (
121+ < div
122+ key = { author . id || i }
123+ className = "card-avatar"
124+ style = { { zIndex : authorProfiles . length - i } }
125+ >
126+ { author . imageUrl ? (
127+ < img
128+ src = { author . imageUrl }
129+ alt = { author . name }
130+ className = "card-avatar-img"
131+ onError = { ( e ) => {
132+ const t = e . currentTarget ;
133+ t . style . display = "none" ;
134+ const fb = t . nextElementSibling as HTMLElement | null ;
135+ if ( fb ) fb . style . display = "flex" ;
136+ } }
137+ />
138+ ) : null }
139+ < span
140+ className = "card-avatar-fallback"
141+ style = { { display : author . imageUrl ? "none" : "flex" } }
130142 >
131- { author . name }
132- </ Link >
133- </ span >
143+ { author . name . charAt ( 0 ) . toUpperCase ( ) }
144+ </ span >
145+ </ div >
134146 ) ) }
135147 </ div >
148+ ) }
149+
150+ < div className = "card-author-info" >
151+ { /* All author names inline, separated by commas */ }
152+ { authorProfiles . length > 0 && (
153+ < div className = "card-author-names" >
154+ { authorProfiles . map ( ( author , i ) => (
155+ < React . Fragment key = { author . id || i } >
156+ { i > 0 && < span className = "card-author-sep" > , </ span > }
157+ < Link
158+ href = { author . githubUrl }
159+ className = "card-author-handle"
160+ target = "_blank"
161+ rel = "noopener noreferrer"
162+ >
163+ @{ author . id || author . name . toLowerCase ( ) . replace ( / \s + / g, "" ) }
164+ </ Link >
165+ </ React . Fragment >
166+ ) ) }
167+ </ div >
168+ ) }
169+ { date && < span className = "card-date" > { formatDate ( date ) } </ span > }
136170 </ div >
137- < span className = "card-read-time" > 5 min read</ span >
138171 </ div >
139172
140- { /* Read More Button */ }
141- < Link to = { `/blog/${ id } ` } className = "card-read-more" >
142- Read Article →
173+ < Link to = { `/blog/${ id } ` } className = "card-read-link" >
174+ Read →
143175 </ Link >
144176 </ div >
145177 </ div >
146178 </ div >
147179 ) ;
148180} ;
149181
150- export default BlogCard ;
182+ export default BlogCard ;
0 commit comments