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事件
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user