Files
ai-directory-platform/src/themes/niche/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

204 lines
7.5 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 { Code2, Terminal, Workflow, Layers } from "lucide-react";
import { ToolCard } from "@/components/tool-card";
import { LocaleLink } from "@/components/locale-link";
import { SectionHeader } from "../shared/section";
import { getHomeDataSafe } from "../shared/home-data";
import type { ThemeModule } from "../types";
/** Niche:垂直行业目录(AI for Developers) */
const brand = {
name: "DevAI.tools",
tagline: "AI tools for developers — curated, tested, ranked by the community.",
headline: "AI tools for developers",
subheadline:
"Code assistants, DevOps automation, testing copilots and everything else you ship with.",
};
const navItems = [
{ label: "Tools", href: "/tools" },
{ label: "Workflows", href: "/best" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Rankings", href: "/rankings" },
{ label: "Submit", href: "/submit" },
];
const footerGroups = [
{
title: "Directory",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Rankings", href: "/rankings" },
{ label: "Use Cases", href: "/ai-tools-for" },
],
},
{
title: "Build",
links: [
{ label: "Workflows", href: "/best" },
{ label: "Submit a Tool", href: "/submit" },
{ label: "Blog", href: "/blog" },
],
},
{
title: "Legal",
links: [
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
],
},
];
async function NicheHome({ locale }: { locale: string }) {
const data = await getHomeDataSafe(6);
return (
<div className="min-h-screen bg-background">
{/* Terminal 风格 Hero */}
<section className="border-b border-border bg-[hsl(var(--secondary))]/40">
<div className="mx-auto max-w-5xl px-4 py-14 sm:px-6 md:py-20">
<div className="mx-auto max-w-2xl overflow-hidden rounded-xl border border-border bg-background shadow-sm">
<div className="flex items-center gap-1.5 border-b border-border bg-secondary/50 px-4 py-2.5">
<span className="h-2.5 w-2.5 rounded-full bg-red-400" />
<span className="h-2.5 w-2.5 rounded-full bg-amber-400" />
<span className="h-2.5 w-2.5 rounded-full bg-emerald-400" />
<span className="ml-2 text-xs text-muted-foreground">devai — zsh</span>
</div>
<div className="space-y-1.5 p-5 font-mono text-sm">
<p className="text-muted-foreground">
<span className="text-emerald-600 dark:text-emerald-400">$</span>{" "}
find-ai-tools --for developer
</p>
<p className="flex items-center gap-2 text-foreground">
<Terminal className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
{brand.headline}
</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{brand.subheadline}
</p>
<p className="pt-2">
<LocaleLink
href="/tools"
className="inline-block rounded-md bg-primary px-4 py-1.5 font-sans text-xs font-semibold text-primary-foreground hover:opacity-90"
>
Browse tools
</LocaleLink>
</p>
</div>
</div>
</div>
</section>
<div className="mx-auto max-w-5xl space-y-12 px-4 py-12 sm:px-6">
{/* Use Cases as workflows */}
{data.useCases.length > 0 && (
<section>
<SectionHeader
title="Ship faster with AI"
subtitle="Pick your workflow — we ranked the tools for it"
viewAllHref="/ai-tools-for"
viewAllLabel="All use cases"
/>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{data.useCases.slice(0, 6).map((uc) => (
<LocaleLink
key={uc.id}
href={`/ai-tools-for/${uc.slug}`}
className="group rounded-xl border border-border p-4 transition-all hover:border-primary/40 hover:bg-primary/5"
>
<div className="flex items-center justify-between">
<Workflow className="h-4 w-4 text-primary" />
<span className="text-xs text-muted-foreground tabular-nums">
{uc.toolCount} tools
</span>
</div>
<h3 className="mt-2 text-sm font-semibold text-foreground group-hover:text-primary">
{locale === "zh" && uc.nameZh ? uc.nameZh : uc.nameEn}
</h3>
</LocaleLink>
))}
</div>
</section>
)}
{/* Trending tools */}
{data.trending.length > 0 && (
<section>
<SectionHeader
title="Most upvoted this week"
viewAllHref="/rankings?board=trending"
/>
<div className="grid gap-3 md:grid-cols-2">
{data.trending.slice(0, 6).map((tool, i) => (
<ToolCard
key={tool.id}
tool={tool}
locale={locale}
rank={i + 1}
/>
))}
</div>
</section>
)}
{/* Workflows(collections) */}
{data.collections.length > 0 && (
<section>
<SectionHeader
title="Workflows & stacks"
subtitle="Batteries-included tool stacks for common dev tasks"
viewAllHref="/best"
viewAllLabel="All workflows"
/>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{data.collections.slice(0, 3).map((col) => (
<LocaleLink
key={col.id}
href={`/best/${col.slug}`}
className="group rounded-xl border border-border p-4 transition-all hover:border-primary/40"
>
<Layers className="h-4 w-4 text-primary" />
<h3 className="mt-2 text-sm font-semibold text-foreground group-hover:text-primary">
{locale === "zh" && col.nameZh ? col.nameZh : col.nameEn}
</h3>
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{locale === "zh" && col.introZh ? col.introZh : col.introEn}
</p>
</LocaleLink>
))}
</div>
</section>
)}
{/* Categories */}
{data.categories.length > 0 && (
<section>
<SectionHeader title="Browse by category" viewAllHref="/tools" />
<div className="flex flex-wrap gap-2">
{data.categories.slice(0, 14).map((cat) => (
<LocaleLink
key={cat.id}
href={`/category/${cat.slug}`}
className="rounded-lg border border-border px-3 py-1.5 font-mono text-xs text-foreground transition-colors hover:border-primary/40 hover:bg-primary/5"
>
{locale === "zh" && cat.nameZh ? cat.nameZh : cat.nameEn}
<span className="ml-1.5 text-muted-foreground">
{cat.toolCount}
</span>
</LocaleLink>
))}
</div>
</section>
)}
</div>
</div>
);
}
export const nicheTheme: ThemeModule = {
id: "niche",
brand,
HomePage: NicheHome,
};
export { navItems as nicheNavItems, footerGroups as nicheFooterGroups };