Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 136 additions & 104 deletions src/components/blogCarousel/blogCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import Link from "@docusaurus/Link";
import { getAuthorProfiles, getAuthorTooltip } from "../../utils/authors";
import { getAuthorProfiles } from "../../utils/authors";

interface BlogCardProps {
type: string;
Expand All @@ -11,140 +11,172 @@ interface BlogCardProps {
imageUrl: string;
id: string;
authors?: string[];
tags?: string[];
category?: string;
}

const TAG_COLORS = [
{ dot: "#f59e0b", border: "#fde68a", bg: "#fffbeb", text: "#92400e" },
{ dot: "#6366f1", border: "#c7d2fe", bg: "#eef2ff", text: "#3730a3" },
{ dot: "#ec4899", border: "#fbcfe8", bg: "#fdf2f8", text: "#9d174d" },
{ dot: "#10b981", border: "#a7f3d0", bg: "#ecfdf5", text: "#065f46" },
{ dot: "#3b82f6", border: "#bfdbfe", bg: "#eff6ff", text: "#1e40af" },
{ dot: "#8b5cf6", border: "#ddd6fe", bg: "#f5f3ff", text: "#5b21b6" },
{ dot: "#f97316", border: "#fed7aa", bg: "#fff7ed", text: "#9a3412" },
{ dot: "#14b8a6", border: "#99f6e4", bg: "#f0fdfa", text: "#134e4a" },
];

function tagColor(label: string) {
let hash = 0;
for (let i = 0; i < label.length; i++)
hash = label.charCodeAt(i) + ((hash << 5) - hash);
return TAG_COLORS[Math.abs(hash) % TAG_COLORS.length];
}

function formatDate(dateStr?: string) {
if (!dateStr) return "";
const d = new Date(dateStr);
if (isNaN(d.getTime())) return dateStr;
return d.toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
});
}

const BlogCard = ({
type,
date,
title,
content,
imageUrl,
id,
authors,
tags,
category,
}: BlogCardProps) => {
const authorProfiles = getAuthorProfiles(authors || []);

if (!id || !type) {
return <div>data not fetched properly, imageId and entryId not found</div>;
}

// Get category from title for demo purposes
const getCategory = (title) => {
if (
title.toLowerCase().includes("design") ||
title.toLowerCase().includes("ux")
)
return "Design";
if (
title.toLowerCase().includes("ai") ||
title.toLowerCase().includes("deepmind")
)
return "AI & Tech";
if (
title.toLowerCase().includes("github") ||
title.toLowerCase().includes("git")
)
return "Development";
return "Resources";
};
// Tags: use tags array first, then category, skip generic "blog"
const rawTags = Array.isArray(tags) && tags.length > 0
? tags
: category
? [category]
: [];

const category = getCategory(title);
const tagList = rawTags.filter(
(t) => t && t.toLowerCase() !== "blog" && t.toLowerCase() !== "post"
);

return (
<div className="relative h-full overflow-hidden transition-all duration-300">
<div className="article-card h-full">
{/* Category Badge */}
<div className="card-category">{category}</div>

{/* Card Image */}
<div className="card-image">
<img src={imageUrl} alt={title} />
</div>
<div className="article-card">
{/* Image */}
<div className="card-image">
<img src={imageUrl} alt={title} loading="lazy" />
</div>

{/* Card Content */}
<div className="card-content">
<h3 className="card-title">
<Link to={`/blog/${id}`} className="card-title-link">
{title}
</Link>
</h3>
<p className="card-description">{content}</p>
{/* Content */}
<div className="card-content">
{/* Title */}
<h3 className="card-title">
<Link to={`/blog/${id}`} className="card-title-link">
{title}
</Link>
</h3>

{/* Card Meta */}
<div className="card-meta">
<div className="card-author">
{/* Stacked Author Avatars */}
{authorProfiles.length > 0 &&
(() => {
const max = 3;
const visible = authorProfiles.slice(0, max);
const extra = Math.max(0, authorProfiles.length - max);
return (
<div className="author-stack" aria-hidden>
{visible.map((a, i) => (
<div
key={a.id}
className="author-stack-item"
style={{ zIndex: max - i }}
>
{a.imageUrl ? (
<img
src={a.imageUrl}
alt={a.name}
className="author-stack-avatar"
onError={(e) => {
const target = e.currentTarget;
target.style.display = "none";
const fallback =
target.nextElementSibling as HTMLElement | null;
if (fallback) fallback.style.display = "flex";
}}
/>
) : (
<span className="author-stack-fallback">
{a.name.charAt(0).toUpperCase()}
</span>
)}
</div>
))}
{extra > 0 && (
<div className="author-stack-more">+{extra}</div>
)}
</div>
);
})()}
{/* Tag pills */}
{tagList.length > 0 && (
<div className="card-tags">
{tagList.slice(0, 5).map((tag) => {
const c = tagColor(tag);
return (
<span
key={tag}
className="card-tag"
style={{
"--tag-dot": c.dot,
"--tag-border": c.border,
"--tag-bg": c.bg,
"--tag-text": c.text,
} as React.CSSProperties}
>
<span className="card-tag-dot" />
{tag}
</span>
);
})}
</div>
)}

{/* Author Names */}
<div className="author-name-group">
{authorProfiles.map((author, authorIndex) => (
<span key={author.id} className="author-item">
{authorIndex > 0 && (
<span className="author-separator">,</span>
)}
<Link
href={author.githubUrl}
className="author-name author-link"
target="_blank"
rel="noopener noreferrer"
data-author-tooltip={getAuthorTooltip(author.id)}
aria-label={`Open ${author.name} on GitHub`}
{/* Footer: avatars + author names/date + Read → */}
<div className="card-footer">
<div className="card-author-row">
{/* Avatar stack — shows all authors overlapped */}
{authorProfiles.length > 0 && (
<div className="card-avatar-stack">
{authorProfiles.map((author, i) => (
<div
key={author.id || i}
className="card-avatar"
style={{ zIndex: authorProfiles.length - i }}
>
{author.imageUrl ? (
<img
src={author.imageUrl}
alt={author.name}
className="card-avatar-img"
onError={(e) => {
const t = e.currentTarget;
t.style.display = "none";
const fb = t.nextElementSibling as HTMLElement | null;
if (fb) fb.style.display = "flex";
}}
/>
) : null}
<span
className="card-avatar-fallback"
style={{ display: author.imageUrl ? "none" : "flex" }}
>
{author.name}
</Link>
</span>
{author.name.charAt(0).toUpperCase()}
</span>
</div>
))}
</div>
)}

<div className="card-author-info">
{/* All author names inline, separated by commas */}
{authorProfiles.length > 0 && (
<div className="card-author-names">
{authorProfiles.map((author, i) => (
<React.Fragment key={author.id || i}>
{i > 0 && <span className="card-author-sep">, </span>}
<Link
href={author.githubUrl}
className="card-author-handle"
target="_blank"
rel="noopener noreferrer"
>
@{author.id || author.name.toLowerCase().replace(/\s+/g, "")}
</Link>
</React.Fragment>
))}
</div>
)}
{date && <span className="card-date">{formatDate(date)}</span>}
</div>
<span className="card-read-time">5 min read</span>
</div>

{/* Read More Button */}
<Link to={`/blog/${id}`} className="card-read-more">
Read Article →
<Link to={`/blog/${id}`} className="card-read-link">
Read →
</Link>
</div>
</div>
</div>
);
};

export default BlogCard;
export default BlogCard;
12 changes: 7 additions & 5 deletions src/components/blogCarousel/blogCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-100">
From the Blog
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
<p className="text-sm text-black-500 dark:text-gray-400 mt-1">
Latest articles from our contributors
</p>
</div>
Expand Down Expand Up @@ -74,13 +74,15 @@
className="basis-full sm:basis-1/2 lg:basis-1/3 xl:basis-1/4"
>
<BlogCard
type="blog"
date="2024-01-01"
type={blog.category}
date={(blog as any).date}

Check warning on line 78 in src/components/blogCarousel/blogCarousel.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
title={blog.title}
content={blog.description}
imageUrl={blog.image}
id={blog.slug}
authors={blog.authors}
tags={blog.tags}
category={blog.category}
/>
</CarouselItem>
))}
Expand All @@ -98,8 +100,8 @@
onClick={() => api?.scrollTo(index)}
aria-label={`Go to slide ${index + 1}`}
className={`h-2 rounded-full transition-all duration-300 ${current === index + 1
? "w-5 bg-indigo-500"
: "w-2 bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500"
? "w-5 bg-indigo-500"
: "w-2 bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500"
}`}
/>
))}
Expand Down
Loading