Skip to content

Commit 96641c4

Browse files
committed
Update index.tsx
1 parent 4c023ff commit 96641c4

1 file changed

Lines changed: 140 additions & 102 deletions

File tree

src/pages/blogs/index.tsx

Lines changed: 140 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,52 @@ import Layout from "@theme/Layout";
44
import Link from "@docusaurus/Link";
55
import blogs from "../../database/blogs/index";
66
import Head from "@docusaurus/Head";
7-
import { getAuthorProfiles, getAuthorTooltip } from "../../utils/authors";
7+
import { getAuthorProfiles } from "../../utils/authors";
88
import { filterBlogsBySearchTerm } from "../../utils/blogFilters";
99

1010
import "./blogs-new.css";
1111

1212
const POSTS_PER_PAGE = 12;
1313

14+
// Stable color per tag label (cycles through palette)
15+
const TAG_COLORS = [
16+
{ dot: "#f59e0b", border: "#fde68a", bg: "#fffbeb", text: "#92400e" },
17+
{ dot: "#6366f1", border: "#c7d2fe", bg: "#eef2ff", text: "#3730a3" },
18+
{ dot: "#ec4899", border: "#fbcfe8", bg: "#fdf2f8", text: "#9d174d" },
19+
{ dot: "#10b981", border: "#a7f3d0", bg: "#ecfdf5", text: "#065f46" },
20+
{ dot: "#3b82f6", border: "#bfdbfe", bg: "#eff6ff", text: "#1e40af" },
21+
{ dot: "#8b5cf6", border: "#ddd6fe", bg: "#f5f3ff", text: "#5b21b6" },
22+
{ dot: "#f97316", border: "#fed7aa", bg: "#fff7ed", text: "#9a3412" },
23+
{ dot: "#14b8a6", border: "#99f6e4", bg: "#f0fdfa", text: "#134e4a" },
24+
];
25+
26+
function tagColor(label: string) {
27+
let hash = 0;
28+
for (let i = 0; i < label.length; i++)
29+
hash = label.charCodeAt(i) + ((hash << 5) - hash);
30+
return TAG_COLORS[Math.abs(hash) % TAG_COLORS.length];
31+
}
32+
33+
function formatDate(dateStr?: string) {
34+
if (!dateStr) return "";
35+
const d = new Date(dateStr);
36+
if (isNaN(d.getTime())) return dateStr;
37+
return d.toLocaleDateString("en-US", {
38+
month: "short",
39+
day: "2-digit",
40+
year: "numeric",
41+
});
42+
}
43+
1444
export default function Blogs() {
1545
const { siteConfig } = useDocusaurusContext();
1646
const [searchInput, setSearchInput] = React.useState("");
1747
const [searchTerm, setSearchTerm] = React.useState("");
1848
const [currentPage, setCurrentPage] = React.useState(1);
49+
1950
const filteredBlogs = React.useMemo(
2051
() => filterBlogsBySearchTerm(blogs, searchTerm),
21-
[searchTerm],
52+
[searchTerm]
2253
);
2354

2455
React.useEffect(() => {
@@ -30,8 +61,9 @@ export default function Blogs() {
3061
const showingEnd = Math.min(currentPage * POSTS_PER_PAGE, filteredBlogs.length);
3162
const paginatedBlogs = filteredBlogs.slice(
3263
(currentPage - 1) * POSTS_PER_PAGE,
33-
currentPage * POSTS_PER_PAGE,
64+
currentPage * POSTS_PER_PAGE
3465
);
66+
3567
const visiblePages = (() => {
3668
const pages: number[] = [];
3769
const maxVisible = 5;
@@ -44,6 +76,7 @@ export default function Blogs() {
4476
for (let i = start; i <= end; i++) pages.push(i);
4577
return pages;
4678
})();
79+
4780
const showLastPage = visiblePages[visiblePages.length - 1] < totalPages;
4881

4982
const handleSearchChange = (e: { target: { value: string } }) => {
@@ -67,11 +100,7 @@ export default function Blogs() {
67100
>
68101
<Head>
69102
<link rel="preconnect" href="https://fonts.googleapis.com" />
70-
<link
71-
rel="preconnect"
72-
href="https://fonts.gstatic.com"
73-
crossOrigin="anonymous"
74-
/>
103+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
75104
<link
76105
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
77106
rel="stylesheet"
@@ -115,14 +144,9 @@ export default function Blogs() {
115144
<div className="blog-search-panel">
116145
<p className="blog-search-eyebrow">Explore articles</p>
117146
<h2 className="blog-search-title">Find the right guide</h2>
118-
<form
119-
className="blog-search-form"
120-
onSubmit={handleSearchSubmit}
121-
>
147+
<form className="blog-search-form" onSubmit={handleSearchSubmit}>
122148
<label className="blog-search-field">
123-
<span className="blog-search-visually-hidden">
124-
Search blog articles
125-
</span>
149+
<span className="blog-search-visually-hidden">Search blog articles</span>
126150
<svg
127151
className="blog-search-submit-icon"
128152
viewBox="0 0 24 24"
@@ -140,9 +164,7 @@ export default function Blogs() {
140164
onChange={handleSearchChange}
141165
/>
142166
</label>
143-
<button className="blog-search-button" type="submit">
144-
Search
145-
</button>
167+
<button className="blog-search-button" type="submit">Search</button>
146168
{searchTerm && (
147169
<button
148170
className="blog-search-clear-button"
@@ -155,7 +177,6 @@ export default function Blogs() {
155177
</form>
156178
</div>
157179

158-
{/* Search Results Counter */}
159180
{searchTerm && (
160181
<div className="search-results-info">
161182
<p>
@@ -179,13 +200,8 @@ export default function Blogs() {
179200
<div className="no-results-content">
180201
<div className="no-results-icon">🔍</div>
181202
<h3>No articles found</h3>
182-
<p>
183-
Try adjusting your search terms or browse all articles.
184-
</p>
185-
<button
186-
className="clear-search-btn"
187-
onClick={handleClearFilters}
188-
>
203+
<p>Try adjusting your search terms or browse all articles.</p>
204+
<button className="clear-search-btn" onClick={handleClearFilters}>
189205
Clear Filters
190206
</button>
191207
</div>
@@ -209,21 +225,18 @@ export default function Blogs() {
209225
{visiblePages.map((page) => (
210226
<button
211227
key={page}
212-
className={`pagination-number ${currentPage === page ? "active-page" : ""
213-
}`}
228+
className={`pagination-number ${currentPage === page ? "active-page" : ""}`}
214229
aria-label={`Go to page ${page}`}
215230
onClick={() => setCurrentPage(page)}
216231
>
217232
{page}
218233
</button>
219234
))}
220-
221235
{showLastPage && (
222236
<>
223237
<span className="pagination-ellipsis">...</span>
224238
<button
225-
className={`pagination-number ${currentPage === totalPages ? "active-page" : ""
226-
}`}
239+
className={`pagination-number ${currentPage === totalPages ? "active-page" : ""}`}
227240
aria-label={`Go to page ${totalPages}`}
228241
onClick={() => setCurrentPage(totalPages)}
229242
>
@@ -243,8 +256,7 @@ export default function Blogs() {
243256
</button>
244257
</div>
245258
<p className="pagination-summary">
246-
Showing {showingStart} - {showingEnd} of{" "}
247-
{filteredBlogs.length} posts
259+
Showing {showingStart} - {showingEnd} of {filteredBlogs.length} posts
248260
</p>
249261
</div>
250262
)}
@@ -256,92 +268,118 @@ export default function Blogs() {
256268
);
257269
}
258270

271+
// ─── BlogCard ────────────────────────────────────────────────────────────────
272+
259273
const BlogCard = ({ blog }: { blog: (typeof blogs)[number] }) => {
260274
const authors = getAuthorProfiles(blog.authors || []);
275+
// Use only the first author for the bottom row (matches reference design)
276+
const primaryAuthor = authors[0];
277+
278+
// Tags — use blog.tags if present, fallback to blog.category as single tag
279+
const tags: string[] =
280+
Array.isArray((blog as any).tags) && (blog as any).tags.length > 0
281+
? (blog as any).tags
282+
: blog.category
283+
? [blog.category]
284+
: [];
261285

262286
return (
263287
<div className="article-card">
264-
<div className="card-category">{blog.category}</div>
288+
{/* ── Image ── */}
265289
<div className="card-image">
266-
<img src={blog.image} alt={blog.title} />
290+
<img src={blog.image} alt={blog.title} loading="lazy" />
267291
</div>
292+
293+
{/* ── Content ── */}
268294
<div className="card-content">
295+
{/* Title */}
269296
<h3 className="card-title">
270297
<Link to={`/blog/${blog.slug}`} className="card-title-link">
271298
{blog.title}
272299
</Link>
273300
</h3>
274-
<p className="card-description">{blog.description}</p>
275-
<div className="card-meta">
276-
<div className="card-author">
277-
{/* Stacked Author Avatars */}
278-
{authors.length > 0 &&
279-
(() => {
280-
const max = 3;
281-
const visible = authors.slice(0, max);
282-
const extra = Math.max(0, authors.length - max);
283-
return (
284-
<div className="author-stack" aria-hidden>
285-
{visible.map((a, i) => (
286-
<div
287-
key={a.id}
288-
className="author-stack-item"
289-
style={{ zIndex: max - i }}
290-
>
291-
{a.imageUrl ? (
292-
<img
293-
src={a.imageUrl}
294-
alt={a.name}
295-
className="author-stack-avatar"
296-
onError={(e) => {
297-
const target = e.currentTarget;
298-
target.style.display = "none";
299-
const fallback =
300-
target.nextElementSibling as HTMLElement | null;
301-
if (fallback) fallback.style.display = "flex";
302-
}}
303-
/>
304-
) : (
305-
<span className="author-stack-fallback">
306-
{a.name.charAt(0).toUpperCase()}
307-
</span>
308-
)}
309-
</div>
310-
))}
311-
{extra > 0 && (
312-
<div className="author-stack-more">+{extra}</div>
313-
)}
314-
</div>
315-
);
316-
})()}
317-
318-
{/* Author Names */}
319-
<div className="author-name-group">
320-
{authors.map((author, authorIndex) => (
321-
<span key={author.id} className="author-item">
322-
{authorIndex > 0 && (
323-
<span className="author-separator">,</span>
324-
)}
325-
<Link
326-
href={author.githubUrl}
327-
className="author-name author-link"
328-
target="_blank"
329-
rel="noopener noreferrer"
330-
data-author-tooltip={getAuthorTooltip(author.id)}
331-
aria-label={`Open ${author.name} on GitHub`}
332-
>
333-
{author.name}
334-
</Link>
301+
302+
{/* Description */}
303+
{blog.description && (
304+
<p className="card-description">{blog.description}</p>
305+
)}
306+
307+
{/* Tag pills */}
308+
{tags.length > 0 && (
309+
<div className="card-tags">
310+
{tags.slice(0, 6).map((tag) => {
311+
const c = tagColor(tag);
312+
return (
313+
<span
314+
key={tag}
315+
className="card-tag"
316+
style={{
317+
"--tag-dot": c.dot,
318+
"--tag-border": c.border,
319+
"--tag-bg": c.bg,
320+
"--tag-text": c.text,
321+
} as React.CSSProperties}
322+
>
323+
<span className="card-tag-dot" />
324+
{tag}
325+
</span>
326+
);
327+
})}
328+
</div>
329+
)}
330+
331+
{/* Bottom row: avatar + author + date ··· Read → */}
332+
<div className="card-footer">
333+
<div className="card-author-row">
334+
{/* Avatar */}
335+
{primaryAuthor && (
336+
<div className="card-avatar">
337+
{primaryAuthor.imageUrl ? (
338+
<img
339+
src={primaryAuthor.imageUrl}
340+
alt={primaryAuthor.name}
341+
className="card-avatar-img"
342+
onError={(e) => {
343+
const t = e.currentTarget;
344+
t.style.display = "none";
345+
const fb = t.nextElementSibling as HTMLElement | null;
346+
if (fb) fb.style.display = "flex";
347+
}}
348+
/>
349+
) : null}
350+
<span
351+
className="card-avatar-fallback"
352+
style={{ display: primaryAuthor.imageUrl ? "none" : "flex" }}
353+
>
354+
{primaryAuthor.name.charAt(0).toUpperCase()}
335355
</span>
336-
))}
356+
</div>
357+
)}
358+
359+
{/* Name + date stacked */}
360+
<div className="card-author-info">
361+
{primaryAuthor && (
362+
<Link
363+
href={primaryAuthor.githubUrl}
364+
className="card-author-handle"
365+
target="_blank"
366+
rel="noopener noreferrer"
367+
>
368+
By @{primaryAuthor.id || primaryAuthor.name.toLowerCase().replace(/\s+/g, "")}
369+
</Link>
370+
)}
371+
{(blog as any).date && (
372+
<span className="card-date">{formatDate((blog as any).date)}</span>
373+
)}
337374
</div>
338375
</div>
339-
<span className="card-read-time">5 min read</span>
376+
377+
{/* Read link */}
378+
<Link to={`/blog/${blog.slug}`} className="card-read-link">
379+
Read →
380+
</Link>
340381
</div>
341-
<Link to={`/blog/${blog.slug}`} className="card-read-more">
342-
Read Article →
343-
</Link>
344382
</div>
345383
</div>
346384
);
347-
};
385+
};

0 commit comments

Comments
 (0)