146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
||
import { getFileUrl } from "@/lib/minio";
|
||
import FeedMasonry from "@/app/components/FeedMasonry";
|
||
import BackButton from "@/app/components/BackButton";
|
||
import { FeedItem } from "@/app/types/feed";
|
||
import { notFound } from "next/navigation";
|
||
import Image from "next/image";
|
||
|
||
export default async function AuthorPage({
|
||
params,
|
||
}: {
|
||
params: Promise<{ secUid: string }>;
|
||
}) {
|
||
const secUid = (await params).secUid;
|
||
|
||
const author = await prisma.author.findUnique({
|
||
where: { sec_uid: secUid },
|
||
});
|
||
|
||
if (!author) {
|
||
notFound();
|
||
}
|
||
|
||
// Initial feed fetch
|
||
const limit = 24;
|
||
const [videos, posts] = await Promise.all([
|
||
prisma.video.findMany({
|
||
where: { authorId: secUid },
|
||
orderBy: { created_at: "desc" },
|
||
take: limit,
|
||
include: { author: true },
|
||
}),
|
||
prisma.imagePost.findMany({
|
||
where: { authorId: secUid },
|
||
orderBy: { created_at: "desc" },
|
||
take: limit,
|
||
include: { author: true, images: { orderBy: { order: "asc" }, take: 1 } },
|
||
}),
|
||
]);
|
||
|
||
const initialItems: FeedItem[] = [
|
||
...videos.map((v) => ({
|
||
type: "video" as const,
|
||
aweme_id: v.aweme_id,
|
||
created_at: v.created_at,
|
||
desc: v.desc,
|
||
video_url: getFileUrl(v.video_url),
|
||
cover_url: getFileUrl(v.cover_url ?? "default_cover.png"),
|
||
width: v.width ?? null,
|
||
height: v.height ?? null,
|
||
author: {
|
||
nickname: v.author.nickname,
|
||
avatar_url: getFileUrl(v.author.avatar_url ?? ""),
|
||
sec_uid: v.author.sec_uid,
|
||
},
|
||
likes: Number(v.digg_count),
|
||
})),
|
||
...posts.map((p) => ({
|
||
type: "image" as const,
|
||
aweme_id: p.aweme_id,
|
||
created_at: p.created_at,
|
||
desc: p.desc,
|
||
cover_url: getFileUrl(p.images?.[0]?.url ?? null),
|
||
width: p.images?.[0]?.width ?? null,
|
||
height: p.images?.[0]?.height ?? null,
|
||
author: {
|
||
nickname: p.author.nickname,
|
||
avatar_url: getFileUrl(p.author.avatar_url ?? ""),
|
||
sec_uid: p.author.sec_uid,
|
||
},
|
||
likes: Number(p.digg_count),
|
||
})),
|
||
]
|
||
.sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at))
|
||
.slice(0, limit);
|
||
|
||
const initialCursor =
|
||
initialItems.length > 0
|
||
? new Date(
|
||
initialItems[initialItems.length - 1].created_at as any,
|
||
).toISOString()
|
||
: null;
|
||
|
||
return (
|
||
<main className="min-h-screen bg-white dark:bg-black text-black dark:text-white">
|
||
<div className="sticky top-0 z-10 p-4 bg-white/80 dark:bg-black/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-800 flex items-center gap-4">
|
||
<BackButton />
|
||
<h1 className="text-lg font-bold truncate">{author.nickname}</h1>
|
||
</div>
|
||
|
||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||
{/* Author Profile Header */}
|
||
<div className="flex flex-col md:flex-row items-center md:items-start gap-6 mb-12">
|
||
<div className="relative w-24 h-24 md:w-32 md:h-32 shrink-0">
|
||
<Image
|
||
src={getFileUrl(author.avatar_url || "default-avatar.png")}
|
||
alt={author.nickname}
|
||
fill
|
||
className="rounded-full object-cover border-2 border-gray-200 dark:border-gray-800"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex-1 text-center md:text-left space-y-4">
|
||
<div>
|
||
<h2 className="text-2xl font-bold">{author.nickname}</h2>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||
抖音号:{author.unique_id || author.short_id || "未知"}
|
||
</p>
|
||
</div>
|
||
|
||
{author.signature && (
|
||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap max-w-2xl">
|
||
{author.signature}
|
||
</p>
|
||
)}
|
||
|
||
<div className="flex items-center justify-center md:justify-start gap-6 text-sm">
|
||
<div className="flex items-center gap-1">
|
||
<span className="font-bold text-lg">
|
||
{Number(author.total_favorited).toLocaleString()}
|
||
</span>
|
||
<span className="text-gray-500">获赞</span>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<span className="font-bold text-lg">
|
||
{Number(author.follower_count).toLocaleString()}
|
||
</span>
|
||
<span className="text-gray-500">粉丝</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="border-t border-gray-200 dark:border-gray-800 pt-8">
|
||
<h3 className="text-lg font-semibold mb-6">作品</h3>
|
||
<FeedMasonry
|
||
initialItems={initialItems}
|
||
initialCursor={initialCursor}
|
||
fetchUrl={`/api/author/${secUid}/feed`}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|