104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Author, BlogMdxFrontmatter, getAllBlogs } from "@/lib/markdown";
|
|
import { formatDate2, stringToDate } from "@/lib/utils";
|
|
import { getMetadata } from "@/app/layout";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { AuroraText } from "@/components/ui/aurora";
|
|
import { ShineBorder } from "@/components/ui/shine-border";
|
|
import docuConfig from "@/docu.json";
|
|
|
|
export const metadata = getMetadata({
|
|
title: "Blog",
|
|
description: "Discover the latest updates, tutorials, and insights on DocuBook.",
|
|
});
|
|
const { meta } = docuConfig;
|
|
export default async function BlogIndexPage() {
|
|
const blogs = (await getAllBlogs()).sort(
|
|
(a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime()
|
|
);
|
|
return (
|
|
<div className="flex flex-col items-center justify-center px-2 py-8 text-center sm:py-36">
|
|
<div className="w-full max-w-[800px] pb-8">
|
|
<AuroraText className="text-lg"># Stay Informed, Stay Ahead</AuroraText>
|
|
<h1 className="mb-4 text-2xl font-bold sm:text-5xl">
|
|
Blog Posts
|
|
</h1>
|
|
<p className="mb-8 sm:text-xl text-muted-foreground">
|
|
Explore updates, tips, and deep dives from the {meta.title}.
|
|
</p>
|
|
</div>
|
|
<div className="text-left grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 sm:gap-8 gap-4 my-6">
|
|
{blogs.map((blog) => (
|
|
<BlogCard {...blog} slug={blog.slug} key={blog.slug} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BlogCard({
|
|
date,
|
|
title,
|
|
description,
|
|
slug,
|
|
cover,
|
|
authors,
|
|
}: BlogMdxFrontmatter & { slug: string }) {
|
|
return (
|
|
<Link
|
|
href={`/blog/${slug}`}
|
|
className="flex flex-col gap-2 items-start border rounded-md max-h-[420px] min-h-[420px]"
|
|
>
|
|
<div className="w-full">
|
|
<Image
|
|
src={cover}
|
|
alt={title}
|
|
width={400}
|
|
height={150}
|
|
quality={80}
|
|
className="w-full rounded-md object-cover h-[200px]"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col items-start px-3 py-3 gap-2 mb-auto">
|
|
<h3 className="text-md font-semibold line-clamp-2">{title}</h3>
|
|
<p className="text-sm text-muted-foreground line-clamp-3">{description}</p>
|
|
</div>
|
|
<div className="flex items-center justify-between w-full px-3 mb-6">
|
|
<p className="text-[13px] text-muted-foreground">
|
|
Published on {formatDate2(date)}
|
|
</p>
|
|
<AvatarGroup users={authors} />
|
|
</div>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
function AvatarGroup({ users, max = 4 }: { users: Author[]; max?: number }) {
|
|
const displayUsers = users.slice(0, max);
|
|
const remainingUsers = Math.max(users.length - max, 0);
|
|
|
|
return (
|
|
<div className="flex items-center">
|
|
{displayUsers.map((user, index) => (
|
|
<Avatar
|
|
key={user.username}
|
|
className={`inline-block border-2 w-9 h-9 border-background ${
|
|
index !== 0 ? "-ml-3" : ""
|
|
} `}
|
|
>
|
|
<AvatarImage src={user.avatar} alt={user.username} />
|
|
<AvatarFallback>
|
|
{user.username.slice(0, 2).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
))}
|
|
{remainingUsers > 0 && (
|
|
<Avatar className="-ml-3 inline-block border-2 border-background hover:translate-y-1 transition-transform">
|
|
<AvatarFallback>+{remainingUsers}</AvatarFallback>
|
|
</Avatar>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|