|
1 | 1 | "use client"; |
2 | | -import * as React from "react"; |
3 | | -import { useState } from "react"; |
4 | | -import { motion } from "framer-motion"; |
5 | 2 | import Link from "@docusaurus/Link"; |
6 | 3 | 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"); |
8 | 8 |
|
9 | 9 | 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 || []); |
11 | 12 |
|
12 | 13 | if (!id || !type) { |
13 | 14 | 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 }) => { |
36 | 37 | const category = getCategory(title); |
37 | 38 |
|
38 | 39 | 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 |
48 | 41 | onMouseEnter={() => setIsHovered(true)} |
49 | 42 | onMouseLeave={() => setIsHovered(false)} |
50 | 43 | className="relative h-full overflow-hidden transition-all duration-300" |
51 | 44 | > |
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> |
60 | 48 |
|
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> |
65 | 53 |
|
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> |
70 | 62 |
|
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 | + ))} |
81 | 126 | </div> |
82 | | - <span className="card-read-time">5 min read</span> |
83 | 127 | </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> |
87 | 129 | </div> |
| 130 | + |
| 131 | + {/* Read More Button */} |
| 132 | + <Link to={`/blog/${id}`} className="card-read-more"> |
| 133 | + Read Article → |
| 134 | + </Link> |
88 | 135 | </div> |
89 | | - </Link> |
90 | | - </motion.div> |
| 136 | + </div> |
| 137 | + </div> |
91 | 138 | ); |
92 | 139 | }; |
93 | 140 |
|
|
0 commit comments