| // src/components/article/ArticleCard.tsx | |
| import { Link } from "react-router-dom"; | |
| import { useState } from "react"; | |
| import { ArticleMeta } from "./ArticleMeta"; | |
| import { ArticleAuthor } from "./ArticleAuthor"; | |
| import { ArticleTags } from "./ArticleTags"; | |
| import { ArticleActions } from "./ArticleActions"; | |
| import { ArticleStats } from "./ArticleStats"; | |
| import { motion } from "framer-motion"; | |
| interface ArticleCardProps { | |
| article: { | |
| id: number; | |
| title: string; | |
| abstract?: string; | |
| coverImage?: string; | |
| createdAt: string; | |
| updatedAt?: string; | |
| readTime?: number; | |
| views: number; | |
| likes?: number; | |
| likesCount?: number; | |
| comments?: number; | |
| commentsCount?: number; | |
| bookmarks?: number; | |
| shares?: number; | |
| citations?: number; | |
| impactScore?: number; | |
| tags: string[]; | |
| author?: { | |
| id: number; | |
| username: string; | |
| fullName?: string; | |
| avatarUrl?: string; | |
| affiliation?: string; | |
| isVerified?: boolean; | |
| } | string; | |
| authorUsername?: string; | |
| status?: string; | |
| featured?: boolean; | |
| trending?: boolean; | |
| }; | |
| variant?: "default" | "featured" | "compact" | "horizontal" | "minimal"; | |
| showAuthor?: boolean; | |
| showTags?: boolean; | |
| showActions?: boolean; | |
| showStats?: boolean; | |
| className?: string; | |
| onLike?: () => void; | |
| onBookmark?: () => void; | |
| onShare?: () => void; | |
| } | |
| export function ArticleCard({ | |
| article, | |
| variant = "default", | |
| showAuthor = true, | |
| showTags = true, | |
| showActions = true, | |
| showStats = false, | |
| className = "", | |
| onLike, | |
| onBookmark, | |
| onShare | |
| }: ArticleCardProps) { | |
| const [imageLoaded, setImageLoaded] = useState(false); | |
| const author = typeof article.author === "string" || !article.author | |
| ? { | |
| id: 0, | |
| username: article.authorUsername ?? (typeof article.author === "string" ? article.author : "unknown"), | |
| fullName: article.authorUsername ?? (typeof article.author === "string" ? article.author : "Unknown author"), | |
| } | |
| : article.author; | |
| const likes = article.likes ?? article.likesCount ?? 0; | |
| const comments = article.comments ?? article.commentsCount ?? 0; | |
| if (variant === "compact") { | |
| return ( | |
| <div className={`group ${className}`}> | |
| <Link to={`/articles/${article.id}`}> | |
| <div className="flex gap-3"> | |
| <div className="w-24 h-24 rounded-xl overflow-hidden flex-shrink-0"> | |
| <img | |
| src={article.coverImage || `https://picsum.photos/seed/${article.id}/100/100`} | |
| alt={article.title} | |
| className="w-full h-full object-cover group-hover:scale-110 transition duration-300" | |
| onLoad={() => setImageLoaded(true)} | |
| /> | |
| </div> | |
| <div className="flex-1"> | |
| <ArticleMeta | |
| createdAt={article.createdAt} | |
| readTime={article.readTime} | |
| views={article.views} | |
| likes={likes} | |
| comments={comments} | |
| variant="compact" | |
| /> | |
| <h4 className="font-semibold text-gray-900 dark:text-white mt-1 line-clamp-2"> | |
| {article.title} | |
| </h4> | |
| {showAuthor && ( | |
| <ArticleAuthor author={author} variant="compact" className="mt-2" /> | |
| )} | |
| </div> | |
| </div> | |
| </Link> | |
| </div> | |
| ); | |
| } | |
| if (variant === "horizontal") { | |
| return ( | |
| <div className={`flex gap-4 group ${className}`}> | |
| <Link to={`/articles/${article.id}`} className="flex-shrink-0"> | |
| <div className="w-48 h-32 rounded-xl overflow-hidden"> | |
| <img | |
| src={article.coverImage || `https://picsum.photos/seed/${article.id}/200/150`} | |
| alt={article.title} | |
| className="w-full h-full object-cover group-hover:scale-105 transition duration-300" | |
| onLoad={() => setImageLoaded(true)} | |
| /> | |
| </div> | |
| </Link> | |
| <div className="flex-1"> | |
| <ArticleMeta | |
| createdAt={article.createdAt} | |
| readTime={article.readTime} | |
| views={article.views} | |
| likes={likes} | |
| comments={comments} | |
| /> | |
| <Link to={`/articles/${article.id}`}> | |
| <h3 className="text-xl font-bold text-gray-900 dark:text-white mt-2 mb-2 line-clamp-2 group-hover:text-primary transition"> | |
| {article.title} | |
| </h3> | |
| </Link> | |
| <p className="text-gray-600 dark:text-gray-400 line-clamp-2 mb-3"> | |
| {article.abstract} | |
| </p> | |
| {showTags && article.tags && ( | |
| <ArticleTags tags={article.tags.slice(0, 3)} variant="chips" /> | |
| )} | |
| {showAuthor && ( | |
| <ArticleAuthor author={author} className="mt-3" /> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (variant === "minimal") { | |
| return ( | |
| <Link to={`/articles/${article.id}`} className={`block group ${className}`}> | |
| <div className="flex items-start gap-3 p-3 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800 transition"> | |
| <div className="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0"> | |
| <img | |
| src={article.coverImage || `https://picsum.photos/seed/${article.id}/50/50`} | |
| alt="" | |
| className="w-full h-full object-cover" | |
| /> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <h4 className="font-medium text-gray-900 dark:text-white line-clamp-1 group-hover:text-primary transition"> | |
| {article.title} | |
| </h4> | |
| <div className="flex items-center gap-3 text-xs text-gray-500 mt-1"> | |
| <span>{new Date(article.createdAt).toLocaleDateString()}</span> | |
| <span>{article.readTime || 5} min read</span> | |
| </div> | |
| </div> | |
| </div> | |
| </Link> | |
| ); | |
| } | |
| if (variant === "featured") { | |
| return ( | |
| <div className={`group relative overflow-hidden rounded-2xl ${className}`}> | |
| <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent z-10" /> | |
| <img | |
| src={article.coverImage || `https://picsum.photos/seed/${article.id}/800/500`} | |
| alt={article.title} | |
| className="w-full h-96 object-cover group-hover:scale-105 transition duration-500" | |
| onLoad={() => setImageLoaded(true)} | |
| /> | |
| <div className="absolute bottom-0 left-0 right-0 p-6 z-20"> | |
| {article.featured && ( | |
| <span className="inline-block px-2 py-1 text-xs font-semibold bg-yellow-500 text-black rounded mb-3"> | |
| Featured | |
| </span> | |
| )} | |
| <Link to={`/articles/${article.id}`}> | |
| <h2 className="text-2xl font-bold text-white mb-2 line-clamp-2 hover:text-primary transition"> | |
| {article.title} | |
| </h2> | |
| </Link> | |
| <p className="text-white/80 line-clamp-2 mb-3">{article.abstract}</p> | |
| <ArticleMeta | |
| createdAt={article.createdAt} | |
| readTime={article.readTime} | |
| views={article.views} | |
| likes={likes} | |
| comments={comments} | |
| className="text-white/60" | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Default variant | |
| return ( | |
| <motion.article | |
| whileHover={{ y: -4 }} | |
| className={`group premium-glass rounded-2xl shadow-sm hover-lift transition-all overflow-hidden ${className}`} | |
| > | |
| <Link to={`/articles/${article.id}`}> | |
| <div className="relative h-48 overflow-hidden"> | |
| <img | |
| src={article.coverImage || `https://picsum.photos/seed/${article.id}/400/300`} | |
| alt={article.title} | |
| className="w-full h-full object-cover group-hover:scale-105 transition duration-500" | |
| onLoad={() => setImageLoaded(true)} | |
| /> | |
| {article.trending && ( | |
| <div className="absolute top-3 left-3"> | |
| <span className="px-2 py-1 text-xs font-semibold bg-gradient-to-r from-orange-500 to-red-500 text-white rounded-full"> | |
| Trending | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| </Link> | |
| <div className="p-5"> | |
| <ArticleMeta | |
| createdAt={article.createdAt} | |
| readTime={article.readTime} | |
| views={article.views} | |
| likes={likes} | |
| comments={comments} | |
| variant="compact" | |
| /> | |
| <Link to={`/articles/${article.id}`}> | |
| <h3 className="text-xl font-bold text-gray-900 dark:text-white mt-2 mb-2 line-clamp-2 group-hover:text-primary transition"> | |
| {article.title} | |
| </h3> | |
| </Link> | |
| <p className="text-gray-600 dark:text-gray-400 text-sm line-clamp-2 mb-3"> | |
| {article.abstract} | |
| </p> | |
| {showTags && article.tags && ( | |
| <ArticleTags tags={article.tags.slice(0, 3)} variant="chips" className="mb-3" /> | |
| )} | |
| <div className="flex items-center justify-between"> | |
| {showAuthor && ( | |
| <ArticleAuthor author={author as any} variant="compact" /> | |
| )} | |
| {showActions && ( | |
| <ArticleActions | |
| articleId={article.id} | |
| likesCount={likes} | |
| onLike={onLike} | |
| onBookmark={onBookmark} | |
| onShare={onShare} | |
| variant="horizontal" | |
| showCounts={false} | |
| /> | |
| )} | |
| </div> | |
| {showStats && ( | |
| <ArticleStats | |
| views={article.views} | |
| likes={likes} | |
| comments={comments} | |
| bookmarks={article.bookmarks || 0} | |
| shares={article.shares || 0} | |
| citations={article.citations || 0} | |
| variant="minimal" | |
| className="mt-3 pt-3 border-t border-gray-100 dark:border-gray-700" | |
| /> | |
| )} | |
| </div> | |
| </motion.article> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 9.98 kB
- Xet hash:
- cedcfed1397bcc3eab598a9b945d8fcaa06371d7915aa6807ac086dd59fdf22a
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.