Files
zhuiguang-ai/src/components/common/CommentCard.tsx
T

170 lines
6.9 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { ThumbsUp, MessageSquare, ChevronDown, ChevronUp, Star } from "lucide-react";
import MemberBadge from "@/components/common/MemberBadge";
import { UserLevel } from "@prisma/client";
interface User {
id: number;
name: string;
avatarUrl?: string;
level: UserLevel;
points: number;
}
interface Reply {
id: number;
content: string;
likeCount: number;
createdAt: string;
liked: boolean;
user: User;
}
interface CommentData {
id: number;
content: string;
rating?: number;
likeCount: number;
isPinned: boolean;
createdAt: string;
updatedAt: string;
replyCount: number;
liked: boolean;
user: User;
replies?: Reply[];
}
interface CommentCardProps {
comment: CommentData;
targetType: "tool" | "skill";
targetSlug: string;
onReply: (parentId: number, replyToName: string) => void;
onLike: (commentId: number) => void;
}
function getTimeAgo(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
if (seconds < 60) return "刚刚";
if (minutes < 60) return `${minutes}分钟前`;
if (hours < 24) return `${hours}小时前`;
if (days < 30) return `${days}天前`;
if (months < 12) return `${months}个月前`;
return date.toLocaleDateString("zh-CN");
}
export default function CommentCard({ comment, targetType, targetSlug, onReply, onLike }: CommentCardProps) {
const { data: session } = useSession();
const [showReplies, setShowReplies] = useState(false);
const [likeAnimating, setLikeAnimating] = useState(false);
const handleLike = () => {
setLikeAnimating(true);
onLike(comment.id);
setTimeout(() => setLikeAnimating(false), 300);
};
const initial = comment.user.name.charAt(0).toUpperCase();
return (
<div className={`rounded-xl border transition-all ${comment.isPinned ? "border-primary/30 bg-primary/[0.03]" : "border-border/40 bg-white hover:border-border/60"} shadow-[0_1px_2px_rgba(0,0,0,0.04)]`}>
{comment.isPinned && (
<div className="px-4 pt-2">
<span className="inline-flex items-center gap-1 text-[10px] font-medium text-primary bg-primary/10 px-2 py-0.5 rounded-full">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a1 1 0 011 1v1.323l3.954 1.582 1.699-3.189a1 1 0 011.827 1.035L17.1 5.5H19a1 1 0 010 2h-2.095l-1.6 3.2a1 1 0 01-1.21.447L10 9.5V17a1 1 0 11-2 0v-7.5L3.805 7.5a1 1 0 01-1.21-.447L.995 3.85a1 1 0 011.827-1.035L4.427 5.5 8.364 3.924A1 1 0 019 3.09V3a1 1 0 011-1z" />
</svg>
置顶
</span>
</div>
)}
<div className="p-4">
<div className="flex items-start gap-3">
{comment.user.avatarUrl ? (
<img src={comment.user.avatarUrl} alt="" className="h-8 w-8 rounded-full border border-border/40 object-cover" />
) : (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">
{initial}
</div>
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium">{comment.user.name}</span>
<MemberBadge level={comment.user.level} points={comment.user.points} size="sm" showPoints={false} />
{comment.rating && (
<div className="flex items-center gap-0.5">
{[1, 2, 3, 4, 5].map((s) => (
<Star key={s} className={`w-3 h-3 ${s <= comment.rating! ? "fill-amber-400 text-amber-400" : "text-muted-foreground/20"}`} />
))}
</div>
)}
<span className="text-[10px] text-muted-foreground/60">{getTimeAgo(new Date(comment.createdAt))}</span>
</div>
<p className="mt-2 text-sm leading-relaxed text-foreground/85 whitespace-pre-wrap">{comment.content}</p>
<div className="mt-3 flex items-center gap-4">
<button
onClick={handleLike}
className={`flex items-center gap-1 text-xs transition-colors ${comment.liked ? "text-primary" : "text-muted-foreground/70 hover:text-foreground"}`}
>
<ThumbsUp className={`w-3.5 h-3.5 transition-transform ${likeAnimating ? "scale-125" : ""}`} />
<span>{comment.likeCount || "点赞"}</span>
</button>
<button
onClick={() => onReply(comment.id, comment.user.name)}
className="flex items-center gap-1 text-xs text-muted-foreground/70 hover:text-foreground transition-colors"
>
<MessageSquare className="w-3.5 h-3.5" />
<span>回复</span>
</button>
</div>
{comment.replies && comment.replies.length > 0 && (
<div className="mt-3 space-y-2">
<button
onClick={() => setShowReplies(!showReplies)}
className="flex items-center gap-1 text-xs text-muted-foreground/70 hover:text-foreground transition-colors"
>
{showReplies ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
<span>{comment.replyCount > (comment.replies?.length || 0) ? `共${comment.replyCount}条回复,已加载${comment.replies?.length}条` : `共${comment.replies.length}条回复`}</span>
</button>
{showReplies && comment.replies.map((reply) => (
<div key={reply.id} className="flex items-start gap-2 pl-3 border-l-2 border-border/30 py-2">
{reply.user.avatarUrl ? (
<img src={reply.user.avatarUrl} alt="" className="h-6 w-6 rounded-full border border-border/30 object-cover" />
) : (
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-muted/50 text-[10px] font-medium text-muted-foreground">
{reply.user.name.charAt(0).toUpperCase()}
</div>
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span className="text-xs font-medium">{reply.user.name}</span>
<span className="text-[10px] text-muted-foreground/60">{getTimeAgo(new Date(reply.createdAt))}</span>
</div>
<p className="mt-0.5 text-xs text-foreground/80 whitespace-pre-wrap">{reply.content}</p>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}