Skip to content

Commit a0ed8e3

Browse files
authored
Merge pull request #1514 from steam-bell-92/main
Enhancing Blog Card with author details
2 parents cfc00b3 + 2356c0d commit a0ed8e3

5 files changed

Lines changed: 622 additions & 78 deletions

File tree

src/components/blogCarousel/blogCard.tsx

Lines changed: 93 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"use client";
2-
import * as React from "react";
3-
import { useState } from "react";
4-
import { motion } from "framer-motion";
52
import Link from "@docusaurus/Link";
63
import { Card, CardContent } from "../ui/card";
7-
import { getAuthorNames } from "../../utils/authors";
4+
import { getAuthorProfiles, getAuthorTooltip } from "../../utils/authors";
5+
6+
declare const require: any;
7+
const React = require("react");
88

99
const BlogCard = ({ type, date, title, content, imageUrl, id, authors }) => {
10-
const [isHovered, setIsHovered] = useState(false);
10+
const [isHovered, setIsHovered] = React.useState(false);
11+
const authorProfiles = getAuthorProfiles(authors || []);
1112

1213
if (!id || !type) {
1314
return <div>data not fetched properly, imageId and entryId not found</div>;
@@ -36,58 +37,104 @@ const BlogCard = ({ type, date, title, content, imageUrl, id, authors }) => {
3637
const category = getCategory(title);
3738

3839
return (
39-
<motion.div
40-
initial={{ opacity: 0, y: 40, scale: 0.95 }}
41-
animate={{ opacity: 1, y: 0, scale: 1 }}
42-
transition={{ duration: 0.6, ease: "easeOut" }}
43-
whileHover={{
44-
y: -8,
45-
scale: 1.02,
46-
transition: { duration: 0.4, ease: "easeOut" },
47-
}}
40+
<div
4841
onMouseEnter={() => setIsHovered(true)}
4942
onMouseLeave={() => setIsHovered(false)}
5043
className="relative h-full overflow-hidden transition-all duration-300"
5144
>
52-
<Link
53-
to={`/blog/${id}`}
54-
className="text-decoration-none block h-full"
55-
style={{ textDecoration: "none" }}
56-
>
57-
<div className="article-card h-full">
58-
{/* Category Badge */}
59-
<div className="card-category">{category}</div>
45+
<div className="article-card h-full">
46+
{/* Category Badge */}
47+
<div className="card-category">{category}</div>
6048

61-
{/* Card Image */}
62-
<div className="card-image">
63-
<img src={imageUrl} alt={title} />
64-
</div>
49+
{/* Card Image */}
50+
<div className="card-image">
51+
<img src={imageUrl} alt={title} />
52+
</div>
6553

66-
{/* Card Content */}
67-
<div className="card-content">
68-
<h3 className="card-title">{title}</h3>
69-
<p className="card-description">{content}</p>
54+
{/* Card Content */}
55+
<div className="card-content">
56+
<h3 className="card-title">
57+
<Link to={`/blog/${id}`} className="card-title-link">
58+
{title}
59+
</Link>
60+
</h3>
61+
<p className="card-description">{content}</p>
7062

71-
{/* Card Meta */}
72-
<div className="card-meta">
73-
<div className="card-author">
74-
<span className="author-avatar">👤</span>
75-
<span
76-
className="author-name"
77-
data-full-name={getAuthorNames(authors || [])}
78-
>
79-
{getAuthorNames(authors || [])}
80-
</span>
63+
{/* Card Meta */}
64+
<div className="card-meta">
65+
<div className="card-author">
66+
{/* Stacked Author Avatars */}
67+
{authorProfiles.length > 0 && (
68+
(() => {
69+
const max = 3;
70+
const visible = authorProfiles.slice(0, max);
71+
const extra = Math.max(0, authorProfiles.length - max);
72+
return (
73+
<div className="author-stack" aria-hidden>
74+
{visible.map((a, i) => (
75+
<div
76+
key={a.id}
77+
className="author-stack-item"
78+
style={{ zIndex: max - i }}
79+
>
80+
{a.imageUrl ? (
81+
<img
82+
src={a.imageUrl}
83+
alt={a.name}
84+
className="author-stack-avatar"
85+
onError={(e) => {
86+
const target = e.currentTarget;
87+
target.style.display = "none";
88+
const fallback = target.nextElementSibling;
89+
if (fallback) fallback.style.display = "flex";
90+
}}
91+
/>
92+
) : (
93+
<span className="author-stack-fallback">
94+
{a.name.charAt(0).toUpperCase()}
95+
</span>
96+
)}
97+
</div>
98+
))}
99+
{extra > 0 && (
100+
<div className="author-stack-more">+{extra}</div>
101+
)}
102+
</div>
103+
);
104+
})()
105+
)}
106+
107+
{/* Author Names */}
108+
<div className="author-name-group">
109+
{authorProfiles.map((author, authorIndex) => (
110+
<span key={author.id} className="author-item">
111+
{authorIndex > 0 && (
112+
<span className="author-separator">&</span>
113+
)}
114+
<Link
115+
href={author.githubUrl}
116+
className="author-name author-link"
117+
target="_blank"
118+
rel="noopener noreferrer"
119+
data-author-tooltip={getAuthorTooltip(author.id)}
120+
aria-label={`Open ${author.name} on GitHub`}
121+
>
122+
{author.name}
123+
</Link>
124+
</span>
125+
))}
81126
</div>
82-
<span className="card-read-time">5 min read</span>
83127
</div>
84-
85-
{/* Read More Button */}
86-
<div className="card-read-more">Read Article →</div>
128+
<span className="card-read-time">5 min read</span>
87129
</div>
130+
131+
{/* Read More Button */}
132+
<Link to={`/blog/${id}`} className="card-read-more">
133+
Read Article →
134+
</Link>
88135
</div>
89-
</Link>
90-
</motion.div>
136+
</div>
137+
</div>
91138
);
92139
};
93140

src/components/blogCarousel/blogCarousel.css

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,20 @@
179179
color: #a78bfa;
180180
}
181181

182+
.card-title-link {
183+
color: inherit;
184+
text-decoration: none;
185+
transition: color 0.2s ease;
186+
}
187+
188+
.card-title-link:hover {
189+
color: #6366f1;
190+
}
191+
192+
[data-theme="dark"] .card-title-link:hover {
193+
color: #a78bfa;
194+
}
195+
182196
.card-description {
183197
font-size: 14px;
184198
color: #64748b;
@@ -237,6 +251,125 @@
237251
min-width: 0;
238252
}
239253

254+
.author-avatar-wrapper {
255+
position: relative;
256+
width: 28px;
257+
height: 28px;
258+
flex-shrink: 0;
259+
}
260+
/*
261+
.author-avatar-image {
262+
width: 100%;
263+
height: 100%;
264+
border-radius: 50%;
265+
object-fit: cover;
266+
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
267+
transition: all 0.3s ease;
268+
border: 1px solid rgba(99, 102, 241, 0.2);
269+
}
270+
271+
.author-avatar-image:hover {
272+
transform: scale(1.1);
273+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
274+
}
275+
276+
.author-avatar-fallback {
277+
position: absolute;
278+
top: 0;
279+
left: 0;
280+
width: 100%;
281+
height: 100%;
282+
border-radius: 50%;
283+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
284+
display: flex;
285+
align-items: center;
286+
justify-content: center;
287+
font-size: 12px;
288+
color: white;
289+
font-weight: 600;
290+
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
291+
transition: all 0.3s ease;
292+
}
293+
294+
.author-avatar-fallback:hover {
295+
transform: scale(1.1);
296+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
297+
} */
298+
299+
.author-name-group {
300+
display: flex;
301+
align-items: center;
302+
flex-wrap: wrap;
303+
gap: 6px;
304+
min-width: 0;
305+
}
306+
307+
/* Stacked avatars */
308+
.author-stack {
309+
display: flex;
310+
align-items: center;
311+
margin-right: 8px;
312+
}
313+
314+
.author-stack-item {
315+
width: 28px;
316+
height: 28px;
317+
border-radius: 50%;
318+
overflow: hidden;
319+
margin-left: -10px;
320+
display: inline-flex;
321+
align-items: center;
322+
justify-content: center;
323+
background: #fff;
324+
border: 2px solid #fff;
325+
}
326+
327+
.author-stack-avatar {
328+
width: 100%;
329+
height: 100%;
330+
object-fit: cover;
331+
display: block;
332+
}
333+
334+
.author-stack-fallback {
335+
width: 100%;
336+
height: 100%;
337+
display: flex;
338+
align-items: center;
339+
justify-content: center;
340+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
341+
color: #fff;
342+
font-weight: 700;
343+
}
344+
345+
.author-stack-more {
346+
width: 28px;
347+
height: 28px;
348+
border-radius: 50%;
349+
background: #6366f1;
350+
color: #fff;
351+
display: inline-flex;
352+
align-items: center;
353+
justify-content: center;
354+
margin-left: -10px;
355+
font-size: 12px;
356+
font-weight: 700;
357+
border: 2px solid #fff;
358+
}
359+
360+
.author-separator {
361+
color: #94a3b8;
362+
font-size: 12px;
363+
font-weight: 700;
364+
}
365+
366+
.author-link {
367+
display: inline-flex;
368+
align-items: center;
369+
position: relative;
370+
text-decoration: none;
371+
}
372+
240373
.author-avatar {
241374
width: 28px;
242375
height: 28px;
@@ -272,6 +405,58 @@
272405
color: #6366f1;
273406
}
274407

408+
.author-name::after {
409+
content: attr(data-author-tooltip);
410+
position: absolute;
411+
bottom: 100%;
412+
left: 50%;
413+
transform: translateX(-50%);
414+
background: rgba(0, 0, 0, 0.9);
415+
color: white;
416+
padding: 8px 12px;
417+
border-radius: 6px;
418+
font-size: 12px;
419+
white-space: pre-line;
420+
max-width: 240px;
421+
text-align: center;
422+
opacity: 0;
423+
visibility: hidden;
424+
transition: all 0.3s ease;
425+
z-index: 1000;
426+
pointer-events: none;
427+
margin-bottom: 5px;
428+
}
429+
430+
.author-name::before {
431+
content: "";
432+
position: absolute;
433+
bottom: 100%;
434+
left: 50%;
435+
transform: translateX(-50%);
436+
border: 5px solid transparent;
437+
border-top-color: rgba(0, 0, 0, 0.9);
438+
opacity: 0;
439+
visibility: hidden;
440+
transition: all 0.3s ease;
441+
z-index: 1000;
442+
pointer-events: none;
443+
}
444+
445+
[data-theme="dark"] .author-name::after {
446+
background: rgba(18, 18, 18, 0.95);
447+
color: #f7fafc;
448+
}
449+
450+
[data-theme="dark"] .author-name::before {
451+
border-top-color: rgba(18, 18, 18, 0.95);
452+
}
453+
454+
.author-name:hover::after,
455+
.author-name:hover::before {
456+
opacity: 1;
457+
visibility: visible;
458+
}
459+
275460
[data-theme="dark"] .author-name {
276461
color: #cbd5e1;
277462
}

0 commit comments

Comments
 (0)