From 59da95ce85e6714e6bb6d0f34047ddd9676149d9 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Sun, 30 Nov 2025 15:33:10 +1000 Subject: [PATCH] feat: update homepage to use real database data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add server component to fetch live stats from database - Tool count from Tool table - Invocations from successful Simulation records - Average latency from recent executions - Category distribution stats - Add featured tools section - Display top 6 tools by quality score - Show tool cards with name, description, category, tags - Include quality score and download metrics - Official badge for verified tools - Click-through to tool detail pages - Update HeroSection component - Accept stats prop with real database metrics - Format large numbers (e.g., "1.2K", "5.3M") - Add functional search navigation - Enter key and button click navigate to tool-search - Empty search browses all tools - Optimize database queries - Use Promise.all() for parallel execution - Calculate avg latency from last 100 simulations - Graceful error handling with fallback values 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/src/app/page.tsx | 187 +++++++++++++++---- apps/web/src/components/home/HeroSection.tsx | 50 ++++- 2 files changed, 192 insertions(+), 45 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index e4b46b6..1952622 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,3 +1,5 @@ +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 { Header } from '@tpmjs/ui/Header/Header'; @@ -5,7 +7,90 @@ import Link from 'next/link'; import { ThemeToggle } from '../components/ThemeToggle'; import { HeroSection } from '../components/home/HeroSection'; -export default function HomePage(): React.ReactElement { +async function getHomePageData() { + try { + // Fetch stats in parallel + const [toolCount, simulationCount, featuredTools, categoryStats] = await Promise.all([ + // Total tool count + prisma.tool.count(), + + // Total successful simulations (as proxy for invocations) + prisma.simulation.count({ + where: { status: 'success' }, + }), + + // Top 6 featured tools by quality score + prisma.tool.findMany({ + orderBy: [{ qualityScore: 'desc' }, { npmDownloadsLastMonth: 'desc' }], + take: 6, + select: { + id: true, + npmPackageName: true, + description: true, + category: true, + tags: true, + qualityScore: true, + npmDownloadsLastMonth: true, + isOfficial: true, + }, + }), + + // Category distribution for stats + prisma.tool.groupBy({ + by: ['category'], + _count: true, + }), + ]); + + // Calculate average latency from recent successful simulations + const recentSimulations = await prisma.simulation.findMany({ + where: { + status: 'success', + executionTimeMs: { not: null }, + }, + select: { executionTimeMs: true }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + + const avgLatency = + recentSimulations.length > 0 + ? Math.round( + recentSimulations.reduce((sum, s) => sum + (s.executionTimeMs || 0), 0) / + recentSimulations.length + ) + : 0; + + return { + stats: { + toolCount, + invocations: simulationCount, + avgLatency, + categoryCount: categoryStats.length, + }, + featuredTools, + categories: categoryStats.slice(0, 5).map((c) => ({ + name: c.category, + count: c._count, + })), + }; + } catch (error) { + console.error('Failed to fetch homepage data:', error); + return { + stats: { + toolCount: 0, + invocations: 0, + avgLatency: 0, + categoryCount: 0, + }, + featuredTools: [], + categories: [], + }; + } +} + +export default async function HomePage(): Promise { + const data = await getHomePageData(); return (
{/* Header */} @@ -48,61 +133,83 @@ export default function HomePage(): React.ReactElement {
{/* Hero Section - Dithered Design */} - + {/* Featured Tools Section */}

- Discover AI Tools + Featured Tools

- Browse our collection of AI-ready tools. Search, filter, and integrate the perfect - tools for your AI agents. + Top-rated tools from our registry. Sorted by quality score and community adoption.

-
- - - - - - -
-
- {/* Feature cards */} -
-
🔍
-

Smart Search

-

- Find tools by name, category, tags, or functionality. Advanced filters help you - discover exactly what you need. -

-
+ {data.featuredTools.length > 0 ? ( +
+ {data.featuredTools.map((tool) => ( + +
+
+

+ {tool.npmPackageName} +

+ {tool.isOfficial && ( + + Official + + )} +
-
-
⚡
-

Quality Metrics

-

- Every tool includes quality scores, download stats, and community feedback to help - you choose wisely. -

-
+

+ {tool.description} +

-
-
🤖
-

AI Agent Ready

+
+ + {tool.category} + + {tool.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} +
+ +
+ + Quality:{' '} + {tool.qualityScore ? Number(tool.qualityScore).toFixed(2) : 'N/A'} + + + {tool.npmDownloadsLastMonth?.toLocaleString() || '0'} downloads/mo + +
+
+ + ))} +
+ ) : ( +

- All tools include AI agent integration guides, parameter specs, and usage - examples. + No tools available yet. Check back soon!

+ )} + +
+ + + + + +
diff --git a/apps/web/src/components/home/HeroSection.tsx b/apps/web/src/components/home/HeroSection.tsx index d0f6742..95b1625 100644 --- a/apps/web/src/components/home/HeroSection.tsx +++ b/apps/web/src/components/home/HeroSection.tsx @@ -2,10 +2,45 @@ import { Button } from '@tpmjs/ui/Button/Button'; import { Input } from '@tpmjs/ui/Input/Input'; +import { useRouter } from 'next/navigation'; import { useState } from 'react'; -export function HeroSection(): React.ReactElement { +interface HeroSectionProps { + stats: { + toolCount: number; + invocations: number; + avgLatency: number; + categoryCount: number; + }; +} + +function formatNumber(num: number): string { + if (num >= 1000000) { + return `${(num / 1000000).toFixed(1)}M`; + } + if (num >= 1000) { + return `${(num / 1000).toFixed(1)}K`; + } + return num.toString(); +} + +export function HeroSection({ stats }: HeroSectionProps): React.ReactElement { const [searchQuery, setSearchQuery] = useState(''); + const router = useRouter(); + + const handleSearch = () => { + if (searchQuery.trim()) { + router.push(`/tool/tool-search?q=${encodeURIComponent(searchQuery)}`); + } else { + router.push('/tool/tool-search'); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; return (
@@ -37,18 +72,21 @@ export function HeroSection(): React.ReactElement { {/* Live Metrics Strip */}
- 2,847 + {formatNumber(stats.toolCount)} TOOLS
/
- 12M+ + + {formatNumber(stats.invocations)} + {stats.invocations >= 1000 ? '+' : ''} + INVOCATIONS
/
- 47ms - LATENCY + {stats.avgLatency || '--'}ms + AVG LATENCY
@@ -71,6 +109,7 @@ export function HeroSection(): React.ReactElement { type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} + onKeyDown={handleKeyDown} placeholder="search tools..." className="brutalist-border h-16 md:h-20 pl-14 pr-36 md:pr-40 text-lg md:text-xl font-mono placeholder:text-foreground-tertiary placeholder:uppercase focus:ring-4 focus:ring-brutalist-accent focus:ring-offset-0 bg-background" style={{ borderRadius: 0 }} @@ -79,6 +118,7 @@ export function HeroSection(): React.ReactElement { {/* Search Button */}