Files
ai-directory-platform/src/features/reviews/components/reviews-admin-table.tsx
T
AI Directory Platform e46b4b52a3 feat: AI Directory Platform 底座升级 + 五套主题站点
- 数据模型: vote/review/submission/claim/use_case/audience/collection/event (+tool 冗余统计列)
- 社区: 投票(限流+原子toggle+API) / 评论(评分+优缺点+Helpful+聚合回写) / 提交(重复检测+审批发布) / 认领(Owner白名单编辑)
- Ranking Engine: trending/popular/most-voted/most-saved/highest-rated/new/editors-choice (纯函数可测)
- 发现体系: Use Case + Audience + Collection 三维发现 / /ai-tools-for / /best SEO 页
- Programmatic SEO: 页面生成器 / sitemap 270条 / AggregateRating/FAQPage/Breadcrumb/ItemList schema / alternatives + vs 对比页
- Analytics: event 事件流 + 外链跳转埋点 + /admin/analytics CTR 与搜索词分析
- Admin: submissions/claims/reviews/discovery/vote-audit/analytics 六个新后台 + 仪表盘扩展
- 主题系统: themes registry + 五套站点 (classic/product/editorial/niche/minimal) 构建期注入
- 认证: GitHub OAuth (env门控) + 登录回跳 redirect
- i18n: 新增 7 个命名空间中英文案 (546 键一致)
- 测试: bun test 32 用例 (ranking/slug/去重/SEO生成器) + Playwright e2e 脚本
- 种子: 14分类/16用例/9受众/40工具/6用户/投票/评论/6合集/7718事件
2026-09-08 17:38:06 +00:00

106 lines
3.5 KiB
TypeScript

"use client";
import { useTransition } from "react";
import { useRouter } from "next/navigation";
import { EyeOff, Eye, Star } from "lucide-react";
import { setReviewStatus } from "@/features/reviews/actions";
import { toast } from "sonner";
interface AdminReviewRow {
id: string;
rating: number;
title: string | null;
content: string;
status: string;
createdAt: Date;
toolName: string;
toolSlug: string;
authorName: string;
}
/**
* # 组件 ReviewsAdminTable
* 说明:评论审核表 —— 隐藏/恢复(隐藏后重算工具聚合评分)。
*/
export function ReviewsAdminTable({ reviews }: { reviews: AdminReviewRow[] }) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const toggle = (id: string, currentStatus: string) => {
startTransition(async () => {
try {
await setReviewStatus(id, currentStatus === "published" ? "hidden" : "published");
toast.success(currentStatus === "published" ? "Review hidden" : "Review restored");
router.refresh();
} catch {
toast.error("Failed");
}
});
};
if (reviews.length === 0) {
return (
<p className="rounded-xl border border-dashed border-border p-12 text-center text-sm text-muted-foreground">
No reviews yet.
</p>
);
}
return (
<div className="space-y-3">
{reviews.map((review) => (
<div
key={review.id}
className="flex flex-wrap items-start justify-between gap-3 rounded-xl border border-border p-4"
>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2 text-sm">
<span className="font-semibold text-foreground">{review.authorName}</span>
<span className="text-xs text-muted-foreground">on</span>
<span className="font-medium text-foreground">{review.toolName}</span>
<span className="inline-flex items-center gap-0.5 text-xs font-semibold text-amber-500">
<Star className="h-3 w-3 fill-current" />
{review.rating}
</span>
<span
className={
review.status === "published"
? "rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-600 dark:text-emerald-400"
: "rounded-full bg-red-500/10 px-2 py-0.5 text-[10px] font-medium text-red-600 dark:text-red-400"
}
>
{review.status}
</span>
</div>
{review.title && (
<p className="mt-1 text-sm font-medium text-foreground">{review.title}</p>
)}
<p className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
{review.content}
</p>
</div>
<button
type="button"
disabled={isPending}
onClick={() => toggle(review.id, review.status)}
className="inline-flex shrink-0 items-center gap-1 rounded-lg border border-border px-3 py-2 text-xs font-medium text-muted-foreground hover:text-foreground disabled:opacity-50"
>
{review.status === "published" ? (
<>
<EyeOff className="h-3.5 w-3.5" />
Hide
</>
) : (
<>
<Eye className="h-3.5 w-3.5" />
Restore
</>
)}
</button>
</div>
))}
</div>
);
}