Files
ai-directory-platform/src/themes/editorial/index.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

234 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { BookOpen, PenTool, ArrowRight, Star } from "lucide-react";
import { LocaleLink } from "@/components/locale-link";
import { SectionHeader } from "../shared/section";
import { getHomeDataSafe } from "../shared/home-data";
import type { ThemeModule } from "../types";
/** Editorial:AI 媒体 + 工具目录(Serif / 内容优先) */
const brand = {
name: "The AI Ledger",
tagline: "Independent journalism on AI tools, with a community-ranked directory.",
headline: "AI tools, reviewed and ranked",
subheadline:
"Guides, comparisons and a community-powered directory of the best AI tools.",
};
const navItems = [
{ label: "Stories", href: "/blog" },
{ label: "Guides", href: "/ai-tools-for" },
{ label: "Best Picks", href: "/best" },
{ label: "Directory", href: "/tools" },
{ label: "Rankings", href: "/rankings" },
{ label: "Submit", href: "/submit" },
];
const footerGroups = [
{
title: "Read",
links: [
{ label: "Latest Stories", href: "/blog" },
{ label: "Buying Guides", href: "/best" },
{ label: "Use Case Guides", href: "/ai-tools-for" },
],
},
{
title: "Directory",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Community Rankings", href: "/rankings" },
{ label: "Submit a Tool", href: "/submit" },
],
},
{
title: "Company",
links: [
{ label: "Contact", href: "/contact" },
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
],
},
];
async function EditorialHome({ locale }: { locale: string }) {
const data = await getHomeDataSafe(6);
const featured = data.articles[0];
const editorPicks = data.editorsChoice.slice(0, 4);
return (
<div className="min-h-screen bg-background">
{/* Masthead */}
<section className="border-b border-border">
<div className="mx-auto max-w-4xl px-4 py-16 text-center sm:px-6 md:py-20">
<p className="text-xs font-medium uppercase tracking-[0.3em] text-muted-foreground">
Independent AI Coverage
</p>
<h1 className="mt-4 text-4xl font-bold tracking-tight text-foreground md:text-5xl">
{brand.headline}
</h1>
<p className="mx-auto mt-4 max-w-xl text-base leading-relaxed text-muted-foreground">
{brand.subheadline}
</p>
</div>
</section>
<div className="mx-auto max-w-4xl space-y-14 px-4 py-12 sm:px-6">
{/* Featured Story */}
{featured && (
<section>
<div className="mb-4 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-primary">
<BookOpen className="h-3.5 w-3.5" />
Featured story
</div>
<LocaleLink href={`/blog/${featured.slug}`} className="group block">
<h2 className="text-2xl font-bold leading-snug text-foreground group-hover:underline md:text-3xl">
{featured.title}
</h2>
{featured.description && (
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
{featured.description}
</p>
)}
<span className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary">
Read the story
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
</span>
</LocaleLink>
</section>
)}
{/* Editor's Picks */}
{editorPicks.length > 0 && (
<section>
<div className="mb-4 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-primary">
<PenTool className="h-3.5 w-3.5" />
Editor's picks
</div>
<div className="divide-y divide-border border-y border-border">
{editorPicks.map((tool, i) => (
<LocaleLink
key={tool.id}
href={`/tools/${tool.slug}`}
className="group flex items-baseline gap-4 py-4"
>
<span className="text-sm font-bold text-muted-foreground tabular-nums">
{String(i + 1).padStart(2, "0")}
</span>
<div className="min-w-0 flex-1">
<h3 className="text-base font-semibold text-foreground group-hover:underline">
{locale === "zh" && tool.nameZh ? tool.nameZh : tool.nameEn}
</h3>
<p className="mt-0.5 line-clamp-1 text-sm text-muted-foreground">
{tool.taglineEn || tool.descriptionEn}
</p>
</div>
{(tool.ratingAvg ?? 0) > 0 && (
<span className="inline-flex shrink-0 items-center gap-1 text-sm font-semibold text-foreground">
<Star className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
{(tool.ratingAvg ?? 0).toFixed(1)}
</span>
)}
</LocaleLink>
))}
</div>
</section>
)}
{/* Best Lists(编辑内容型榜单) */}
{data.collections.length > 0 && (
<section>
<SectionHeader
title="Buying guides"
subtitle="Deep dives that help you choose"
viewAllHref="/best"
viewAllLabel="All guides"
/>
<div className="grid gap-6 sm:grid-cols-2">
{data.collections.slice(0, 4).map((col) => (
<LocaleLink
key={col.id}
href={`/best/${col.slug}`}
className="group border-t-2 border-foreground/80 pt-4"
>
<h3 className="text-lg font-bold leading-snug text-foreground group-hover:underline">
{locale === "zh" && col.nameZh ? col.nameZh : col.nameEn}
</h3>
<p className="mt-2 line-clamp-2 text-sm leading-relaxed text-muted-foreground">
{locale === "zh" && col.introZh ? col.introZh : col.introEn}
</p>
<p className="mt-2 text-xs uppercase tracking-wide text-muted-foreground">
{col.toolCount} tools compared
</p>
</LocaleLink>
))}
</div>
</section>
)}
{/* Latest articles */}
{data.articles.length > 1 && (
<section>
<SectionHeader title="Latest stories" viewAllHref="/blog" />
<div className="grid gap-6 sm:grid-cols-2">
{data.articles.slice(1).map((article) => (
<LocaleLink key={article.slug} href={`/blog/${article.slug}`} className="group">
<h3 className="text-base font-semibold leading-snug text-foreground group-hover:underline">
{article.title}
</h3>
{article.description && (
<p className="mt-1.5 line-clamp-2 text-sm text-muted-foreground">
{article.description}
</p>
)}
</LocaleLink>
))}
</div>
</section>
)}
{/* Community signal */}
{data.latestReviews.length > 0 && (
<section>
<SectionHeader
title="From the community"
viewAllHref="/tools"
viewAllLabel="Browse directory"
/>
<div className="grid gap-6 sm:grid-cols-2">
{data.latestReviews.slice(0, 4).map((review, i) => (
<blockquote
key={i}
className="border-l-2 border-primary/40 pl-4"
>
<p className="text-sm italic leading-relaxed text-foreground">
"{review.content.slice(0, 140)}
{review.content.length > 140 ? "..." : ""}"
</p>
<footer className="mt-2 text-xs text-muted-foreground">
{review.authorName} on{" "}
<LocaleLink
href={`/tools/${review.toolSlug}`}
className="font-medium text-foreground hover:underline"
>
{review.toolName}
</LocaleLink>
{" · "}
{review.rating}/5
</footer>
</blockquote>
))}
</div>
</section>
)}
</div>
</div>
);
}
export const editorialTheme: ThemeModule = {
id: "editorial",
brand,
HomePage: EditorialHome,
};
export { navItems as editorialNavItems, footerGroups as editorialFooterGroups };