- Create ToolHealthBadge component in @tpmjs/ui for broken tool indicator - Create ToolHealthBanner component in @tpmjs/ui for detailed health warnings - Integrate both components into playground ToolsSidebar: - Badge shows in tool cards in left sidebar - Banner shows in tool detail modal - Update search-registry to include health fields in API responses - Add package.json exports for new health components These components provide consistent UI for displaying broken tool status across the application (tool search, tool detail pages, playground). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
245 lines
9 KiB
TypeScript
245 lines
9 KiB
TypeScript
import { prisma } from '@tpmjs/db';
|
|
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
|
import { Button } from '@tpmjs/ui/Button/Button';
|
|
import { Container } from '@tpmjs/ui/Container/Container';
|
|
import Link from 'next/link';
|
|
import { AppHeader } from '../components/AppHeader';
|
|
import { HeroSection } from '../components/home/HeroSection';
|
|
|
|
async function getHomePageData() {
|
|
try {
|
|
// Fetch stats in parallel
|
|
const [packageCount, toolCount, featuredTools, categoryStats] = await Promise.all([
|
|
// Total package count
|
|
prisma.package.count(),
|
|
|
|
// Total tool count
|
|
prisma.tool.count(),
|
|
|
|
// Top 6 featured tools by quality score
|
|
prisma.tool.findMany({
|
|
orderBy: [{ qualityScore: 'desc' }, { package: { npmDownloadsLastMonth: 'desc' } }],
|
|
take: 6,
|
|
select: {
|
|
id: true,
|
|
exportName: true,
|
|
description: true,
|
|
qualityScore: true,
|
|
package: {
|
|
select: {
|
|
npmPackageName: true,
|
|
category: true,
|
|
npmDownloadsLastMonth: true,
|
|
isOfficial: true,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
|
|
// Category distribution for stats (group by package category)
|
|
prisma.package.groupBy({
|
|
by: ['category'],
|
|
_count: {
|
|
_all: true,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
return {
|
|
stats: {
|
|
packageCount,
|
|
toolCount,
|
|
categoryCount: categoryStats.length,
|
|
},
|
|
featuredTools,
|
|
categories: categoryStats.slice(0, 5).map((c) => ({
|
|
name: c.category,
|
|
count: c._count._all,
|
|
})),
|
|
};
|
|
} catch (error) {
|
|
console.error('Failed to fetch homepage data:', error);
|
|
return {
|
|
stats: {
|
|
packageCount: 0,
|
|
toolCount: 0,
|
|
categoryCount: 0,
|
|
},
|
|
featuredTools: [],
|
|
categories: [],
|
|
};
|
|
}
|
|
}
|
|
|
|
export default async function HomePage(): Promise<React.ReactElement> {
|
|
const data = await getHomePageData();
|
|
return (
|
|
<div className="min-h-screen flex flex-col">
|
|
<AppHeader />
|
|
|
|
<main className="flex-1">
|
|
{/* Hero Section - Dithered Design */}
|
|
<HeroSection stats={data.stats} />
|
|
|
|
{/* Featured Tools Section */}
|
|
<section className="py-16 bg-background">
|
|
<Container size="xl" padding="lg">
|
|
<div className="text-center mb-12">
|
|
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-foreground">
|
|
Featured Tools
|
|
</h2>
|
|
<p className="text-lg text-foreground-secondary max-w-2xl mx-auto mb-8">
|
|
Top-rated tools from our registry. Sorted by quality score and community adoption.
|
|
</p>
|
|
</div>
|
|
|
|
{data.featuredTools.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
|
{data.featuredTools.map((tool) => (
|
|
<Link
|
|
key={tool.id}
|
|
href={`/tool/${tool.package.npmPackageName}/${tool.exportName}`}
|
|
className="group"
|
|
>
|
|
<div className="p-6 border border-border rounded-lg bg-surface hover:border-foreground transition-colors h-full flex flex-col">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<h3 className="text-lg font-semibold text-foreground group-hover:text-brutalist-accent transition-colors">
|
|
{tool.package.npmPackageName}
|
|
<span className="text-xs text-foreground-tertiary ml-2">
|
|
({tool.exportName})
|
|
</span>
|
|
</h3>
|
|
{tool.package.isOfficial && (
|
|
<Badge variant="default" size="sm">
|
|
Official
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<p className="text-sm text-foreground-secondary mb-4 flex-1 line-clamp-3">
|
|
{tool.description}
|
|
</p>
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<Badge variant="outline" size="sm">
|
|
{tool.package.category}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="mt-4 pt-4 border-t border-border flex items-center justify-between text-xs text-foreground-tertiary">
|
|
<span>
|
|
Quality:{' '}
|
|
{tool.qualityScore ? Number(tool.qualityScore).toFixed(2) : 'N/A'}
|
|
</span>
|
|
<span>
|
|
{tool.package.npmDownloadsLastMonth?.toLocaleString() || '0'} downloads/mo
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<p className="text-foreground-secondary">
|
|
No tools available yet. Check back soon!
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
|
<Link href="/tool/tool-search">
|
|
<Button size="lg" variant="default">
|
|
Browse All {data.stats.toolCount} Tools
|
|
</Button>
|
|
</Link>
|
|
<Link href="/tool/tool-search">
|
|
<Button size="lg" variant="outline">
|
|
Search by Category
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
|
|
{/* Publish Your Tool Section */}
|
|
<section className="py-16 bg-surface">
|
|
<Container size="xl" padding="lg">
|
|
<div className="text-center max-w-3xl mx-auto">
|
|
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-foreground">
|
|
Publish Your AI Tool
|
|
</h2>
|
|
<p className="text-lg text-foreground-secondary mb-8">
|
|
Share your tool with the AI community. Automatic discovery, quality scoring, and
|
|
seamless integration with popular AI frameworks.
|
|
</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
<div className="p-4">
|
|
<div className="text-3xl mb-2">🚀</div>
|
|
<h3 className="font-semibold mb-1 text-foreground">Quick Setup</h3>
|
|
<p className="text-sm text-foreground-secondary">
|
|
Add one keyword to package.json and publish to NPM
|
|
</p>
|
|
</div>
|
|
<div className="p-4">
|
|
<div className="text-3xl mb-2">⚡</div>
|
|
<h3 className="font-semibold mb-1 text-foreground">Auto Discovery</h3>
|
|
<p className="text-sm text-foreground-secondary">
|
|
Your tool appears on tpmjs.com within 15 minutes
|
|
</p>
|
|
</div>
|
|
<div className="p-4">
|
|
<div className="text-3xl mb-2">📊</div>
|
|
<h3 className="font-semibold mb-1 text-foreground">Quality Metrics</h3>
|
|
<p className="text-sm text-foreground-secondary">
|
|
Automatic scoring based on docs, downloads, and stars
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Link href="/publish">
|
|
<Button size="lg" variant="default">
|
|
Learn How to Publish
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className="py-8 border-t border-border bg-surface">
|
|
<Container size="xl" padding="lg">
|
|
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
|
<p className="text-sm text-foreground-secondary">© 2025 TPMJS. All rights reserved.</p>
|
|
<div className="flex items-center gap-4 text-sm">
|
|
<a
|
|
href="mailto:hello@tpmjs.com"
|
|
className="text-foreground-secondary hover:text-foreground transition-colors"
|
|
>
|
|
Contact
|
|
</a>
|
|
<span className="text-border">·</span>
|
|
<a
|
|
href="https://github.com/your-org/tpmjs"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-foreground-secondary hover:text-foreground transition-colors"
|
|
>
|
|
GitHub
|
|
</a>
|
|
<span className="text-border">·</span>
|
|
<a
|
|
href="https://twitter.com/tpmjs"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-foreground-secondary hover:text-foreground transition-colors"
|
|
>
|
|
Twitter
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|