Skip to content

Commit 76b91ac

Browse files
committed
blog
1 parent 6f25eac commit 76b91ac

1 file changed

Lines changed: 112 additions & 148 deletions

File tree

Lines changed: 112 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use client";
2-
import React from "react";
2+
import * as React from "react";
3+
import { motion } from "framer-motion";
34
import Link from "@docusaurus/Link";
4-
import { getAuthorProfiles } from "../../utils/authors";
5+
import { getAuthorProfiles, getAuthorTooltip } from "../../utils/authors";
56

67
interface 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

4717
const 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

Comments
 (0)