11"use client" ;
2- import React from "react" ;
2+ import * as React from "react" ;
3+ import { motion } from "framer-motion" ;
34import Link from "@docusaurus/Link" ;
4- import { getAuthorProfiles } from "../../utils/authors" ;
5+ import { getAuthorProfiles , getAuthorTooltip } from "../../utils/authors" ;
56
67interface BlogCardProps {
78 type : string ;
@@ -11,176 +12,139 @@ interface BlogCardProps {
1112 imageUrl : string ;
1213 id : string ;
1314 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- } ) ;
4515}
4616
4717const BlogCard = ( {
4818 type,
49- date,
5019 title,
20+ content,
5121 imageUrl,
5222 id,
5323 authors,
54- tags,
55- category,
5624} : BlogCardProps ) => {
5725 const authorProfiles = getAuthorProfiles ( authors || [ ] ) ;
5826
5927 if ( ! id || ! type ) {
6028 return < div > data not fetched properly, imageId and entryId not found</ div > ;
6129 }
6230
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- : [ ] ;
69-
70- const tagList = rawTags . filter (
71- ( t ) => t && t . toLowerCase ( ) !== "blog" && t . toLowerCase ( ) !== "post"
72- ) ;
31+ const getCategory = ( title : string ) => {
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+ } ;
49+
50+ const category = getCategory ( title ) ;
7351
7452 return (
75- < div className = "relative h-full overflow-hidden transition-all duration-300" >
76- < div className = "article-card h-full" >
77- { /* Category Badge */ }
78- < div className = "card-category" > { category } </ div >
79-
80- { /* Card Image */ }
81- < Link to = { `/blog/${ id } ` } className = "card-image" style = { { display : "block" } } >
82- < img src = { imageUrl } alt = { title } />
83- </ Link >
84-
85- { /* Content */ }
86- < div className = "card-content" >
87- { /* Title */ }
88- < h3 className = "card-title" >
89- < Link to = { `/blog/${ id } ` } className = "card-title-link" >
90- { title }
91- </ Link >
92- </ h3 >
93-
94- { /* Tag pills */ }
95- { tagList . length > 0 && (
96- < div className = "card-tags" >
97- { tagList . slice ( 0 , 5 ) . map ( ( tag ) => {
98- const c = tagColor ( tag ) ;
99- return (
100- < span
101- key = { tag }
102- className = "card-tag"
103- style = { {
104- "--tag-dot" : c . dot ,
105- "--tag-border" : c . border ,
106- "--tag-bg" : c . bg ,
107- "--tag-text" : c . text ,
108- } as React . CSSProperties }
109- >
110- < span className = "card-tag-dot" />
111- { tag }
112- </ span >
113- ) ;
114- } ) }
53+ < motion . div
54+ initial = { { opacity : 0 , y : 40 , scale : 0.95 } }
55+ animate = { { opacity : 1 , y : 0 , scale : 1 } }
56+ transition = { { duration : 0.6 , ease : "easeOut" } }
57+ whileHover = { {
58+ y : - 8 ,
59+ scale : 1.02 ,
60+ transition : { duration : 0.4 , ease : "easeOut" } ,
61+ } }
62+ className = "relative h-full overflow-hidden transition-all duration-300"
63+ >
64+ < Link
65+ to = { `/blog/${ id } ` }
66+ className = "block h-full"
67+ style = { { textDecoration : "none" } }
68+ aria-label = { `Read article: ${ title } ` }
69+ >
70+ < div className = "article-card h-full" style = { { cursor : "pointer" } } >
71+ < div className = "card-category" > { category } </ div >
72+ < div className = "card-image" >
73+ < img src = { imageUrl } alt = { title } />
11574 </ div >
116- ) }
117-
118- { /* Footer: avatars + author names/date + Read → */ }
119- < div className = "card-footer" >
120- < div className = "card-author-row" >
121- { /* Avatar stack — shows all authors overlapped */ }
122- { authorProfiles . length > 0 && (
123- < div className = "card-avatar-stack" >
124- { authorProfiles . map ( ( author , i ) => (
125- < div
126- key = { author . id || i }
127- className = "card-avatar"
128- style = { { zIndex : authorProfiles . length - i } }
129- >
130- { author . imageUrl ? (
131- < img
132- src = { author . imageUrl }
133- alt = { author . name }
134- className = "card-avatar-img"
135- onError = { ( e ) => {
136- const t = e . currentTarget ;
137- t . style . display = "none" ;
138- const fb = t . nextElementSibling as HTMLElement | null ;
139- if ( fb ) fb . style . display = "flex" ;
140- } }
141- />
142- ) : null }
143- < span
144- className = "card-avatar-fallback"
145- style = { { display : author . imageUrl ? "none" : "flex" } }
146- >
147- { author . name . charAt ( 0 ) . toUpperCase ( ) }
148- </ span >
149- </ div >
150- ) ) }
151- </ div >
152- ) }
153-
154- < div className = "card-author-info" >
155- { /* All author names inline, separated by commas */ }
156- { authorProfiles . length > 0 && (
157- < div className = "card-author-names" >
158- { authorProfiles . map ( ( author , i ) => (
159- < React . Fragment key = { author . id || i } >
160- { i > 0 && < span className = "card-author-sep" > , </ span > }
161- < Link
162- href = { author . githubUrl }
163- className = "card-author-handle"
164- target = "_blank"
165- rel = "noopener noreferrer"
75+ < div className = "card-content" >
76+ < h3 className = "card-title" >
77+ < span className = "card-title-link" > { title } </ span >
78+ </ h3 >
79+ < p className = "card-description" > { content } </ p >
80+ < div className = "card-meta" >
81+ < div className = "card-author" >
82+ { authorProfiles . length > 0 &&
83+ ( ( ) => {
84+ const max = 3 ;
85+ const visible = authorProfiles . slice ( 0 , max ) ;
86+ const extra = Math . max ( 0 , authorProfiles . length - max ) ;
87+ return (
88+ < div className = "author-stack" aria-hidden >
89+ { visible . map ( ( a , i ) => (
90+ < div
91+ key = { a . id }
92+ className = "author-stack-item"
93+ style = { { zIndex : max - i } }
94+ >
95+ { a . imageUrl ? (
96+ < img
97+ src = { a . imageUrl }
98+ alt = { a . name }
99+ className = "author-stack-avatar"
100+ onError = { ( e ) => {
101+ const target = e . currentTarget ;
102+ target . style . display = "none" ;
103+ const fallback =
104+ target . nextElementSibling as HTMLElement | null ;
105+ if ( fallback ) fallback . style . display = "flex" ;
106+ } }
107+ />
108+ ) : (
109+ < span className = "author-stack-fallback" >
110+ { a . name . charAt ( 0 ) . toUpperCase ( ) }
111+ </ span >
112+ ) }
113+ </ div >
114+ ) ) }
115+ { extra > 0 && (
116+ < div className = "author-stack-more" > +{ extra } </ div >
117+ ) }
118+ </ div >
119+ ) ;
120+ } ) ( ) }
121+ < div className = "author-name-group" >
122+ { authorProfiles . map ( ( author , authorIndex ) => (
123+ < span key = { author . id } className = "author-item" >
124+ { authorIndex > 0 && (
125+ < span className = "author-separator" > & </ span >
126+ ) }
127+ < span
128+ className = "author-name author-link"
129+ data-author-tooltip = { getAuthorTooltip ( author . id ) }
130+ aria-label = { `Open ${ author . name } on GitHub` }
166131 >
167- @ { author . id || author . name . toLowerCase ( ) . replace ( / \s + / g , "" ) }
168- </ Link >
169- </ React . Fragment >
132+ { author . name }
133+ </ span >
134+ </ span >
170135 ) ) }
171136 </ div >
172- ) }
173- { date && < span className = "card-date" > { formatDate ( date ) } </ span > }
137+ </ div >
138+ < span className = "card-read-more" >
139+ < span > Read Article →</ span >
140+ </ span >
141+ < span className = "card-read-time" > 5 min read</ span >
174142 </ div >
175143 </ div >
176-
177- < Link to = { `/blog/${ id } ` } className = "card-read-link" >
178- Read →
179- </ Link >
180144 </ div >
181- </ div >
182- </ div >
145+ </ Link >
146+ </ motion . div >
183147 ) ;
184148} ;
185149
186- export default BlogCard ;
150+ export default BlogCard ;
0 commit comments