195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
"use client"
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
import { gql, useQuery } from "@apollo/client";
|
|
import Link from "next/link";
|
|
import { formatDistanceToNow } from "date-fns";
|
|
|
|
const BLOG = gql`
|
|
query Blog {
|
|
blogs(filter: {
|
|
isActive: true
|
|
status: "published"
|
|
},
|
|
sort: {
|
|
field: "createdAt"
|
|
direction: "DESC"
|
|
}
|
|
) {
|
|
items {
|
|
id
|
|
title
|
|
excerpt
|
|
slug
|
|
createdAt
|
|
createdBy
|
|
categoryId
|
|
featuredImage
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const INFO = gql`
|
|
query Info($tagId: String!, $categoryId: String!, $authorId: String!) {
|
|
blogTag(id: $tagId) {
|
|
name
|
|
slug
|
|
color
|
|
}
|
|
|
|
blogCategory(id: $categoryId) {
|
|
name
|
|
slug
|
|
}
|
|
|
|
userWithGroups(id: $authorId) {
|
|
user {
|
|
username
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
export default function Blog() {
|
|
const { data, loading, error } = useQuery(BLOG);
|
|
const items = data?.blogs?.items || [];
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="w-full py-20 lg:py-40">
|
|
<div className="container mx-auto">
|
|
<div className="text-center">Loading...</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="w-full py-20 lg:py-40">
|
|
<div className="container mx-auto">
|
|
<div className="text-center text-red-500">Error: {error.message}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full py-20 lg:py-40">
|
|
<div className="container mx-auto flex flex-col gap-14">
|
|
<div className="flex w-full flex-col sm:flex-row sm:justify-between sm:items-center gap-8">
|
|
<h4 className="text-3xl md:text-5xl tracking-tighter max-w-xl font-regular">
|
|
Latest articles
|
|
</h4>
|
|
</div>
|
|
|
|
{items.length === 0 ? (
|
|
<div className="text-center text-muted-foreground">No articles found</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
{items.map((blog: any, index: number) => (
|
|
<BlogItem
|
|
key={blog.id}
|
|
blog={blog}
|
|
featured={index === 0}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface BlogItemProps {
|
|
blog: {
|
|
id: string;
|
|
title: string;
|
|
excerpt: string;
|
|
slug: string;
|
|
createdAt: string;
|
|
createdBy: string;
|
|
categoryId: string;
|
|
featuredImage?: string;
|
|
};
|
|
featured?: boolean;
|
|
}
|
|
|
|
function BlogItem({ blog, featured = false }: BlogItemProps) {
|
|
const timeAgo = formatDistanceToNow(new Date(blog.createdAt), { addSuffix: true });
|
|
|
|
// 使用 INFO 查询获取详细信息
|
|
const { data: infoData, loading: infoLoading } = useQuery(INFO, {
|
|
variables: {
|
|
tagId: blog.categoryId || "",
|
|
categoryId: blog.categoryId,
|
|
authorId: blog.createdBy
|
|
},
|
|
skip: !blog.categoryId || !blog.createdBy,
|
|
});
|
|
|
|
const category = infoData?.blogCategory;
|
|
const author = infoData?.userWithGroups?.user;
|
|
const tag = infoData?.blogTag;
|
|
|
|
return (
|
|
<Link href={`/blog/${blog.slug}`}>
|
|
<div className={`flex flex-col gap-4 hover:opacity-75 cursor-pointer transition-opacity ${featured ? 'md:col-span-2' : ''
|
|
}`}>
|
|
{/* Featured Image */}
|
|
<div className="bg-muted rounded-md aspect-video overflow-hidden">
|
|
{blog.featuredImage ? (
|
|
<img
|
|
src={blog.featuredImage}
|
|
alt={blog.title}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full bg-muted flex items-center justify-center text-muted-foreground">
|
|
No image
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Meta Information */}
|
|
<div className="flex flex-row gap-4 items-center">
|
|
<Badge
|
|
variant="secondary"
|
|
style={{
|
|
backgroundColor: tag?.color || undefined,
|
|
color: tag?.color ? '#ffffff' : undefined
|
|
}}
|
|
>
|
|
{category?.name || tag?.name || 'Article'}
|
|
</Badge>
|
|
<div className="flex flex-row gap-2 text-sm items-center">
|
|
<span className="text-muted-foreground">By</span>
|
|
<Avatar className="h-6 w-6">
|
|
<AvatarFallback>
|
|
{author?.username?.charAt(0)?.toUpperCase() || 'A'}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span>{infoLoading ? 'Loading...' : (author?.username || 'Anonymous')}</span>
|
|
<span className="text-muted-foreground">•</span>
|
|
<span className="text-muted-foreground">{timeAgo}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className={`flex flex-col ${featured ? 'gap-2' : 'gap-1'}`}>
|
|
<h3 className={`max-w-3xl tracking-tight ${featured ? 'text-4xl' : 'text-2xl'
|
|
}`}>
|
|
{blog.title}
|
|
</h3>
|
|
<p className="max-w-3xl text-muted-foreground text-base">
|
|
{blog.excerpt || 'No excerpt available...'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
}
|
|
|