Files
skillhub/apps/web/app/[locale]/page.tsx
airano c8216dfcd2 feat(curation): auto-curation after crawl + quality messaging
Automate curation:
- Add runPostCrawlCuration() to DB queries — runs after every
  full and incremental crawl (classify, dedup, category counts)
- No more need for manual curate.mjs runs after each crawl

Quality messaging:
- Homepage stats label: "Skills" → "Curated Skills" (en/fa)
- Add curation note: "deduplicated and quality-filtered from X+ repos"
- Update fallback counts from 172K to 16K across all touchpoints
  (BetaBanner, getting-started prompts, email templates)
- getting-started skill count query now uses browse-ready filter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:06:18 +03:30

291 lines
11 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 { getTranslations, setRequestLocale } from 'next-intl/server';
import Link from 'next/link';
import { Search, ArrowLeft, ArrowRight, Download, Users, Layers, Sparkles, Terminal, Zap } from 'lucide-react';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { HeroSearch } from '@/components/HeroSearch';
import { SkillCard } from '@/components/SkillCard';
import { createDb, categoryQueries, skillQueries, skills, sql } from '@skillhub/db';
import { formatCompactNumber } from '@/lib/format-number';
// Force dynamic rendering to fetch fresh data from database
export const dynamic = 'force-dynamic';
// Get stats directly from database
async function getStats() {
try {
const db = createDb();
// Browse-ready filter: exclude duplicates and aggregators
const browseReady = sql`${skills.isDuplicate} = false AND (${skills.skillType} IS NULL OR ${skills.skillType} != 'aggregator')`;
// Get total skills count (browse-ready, SKILL.md only)
const skillsResult = await db
.select({ count: sql<number>`count(*)::int` })
.from(skills)
.where(sql`${skills.sourceFormat} = 'skill.md' AND ${skills.isBlocked} = false AND ${browseReady}`);
const totalSkills = skillsResult[0]?.count ?? 0;
// Get total downloads (browse-ready only)
const downloadsResult = await db
.select({ sum: sql<number>`coalesce(sum(${skills.downloadCount}), 0)::int` })
.from(skills)
.where(browseReady);
const totalDownloads = downloadsResult[0]?.sum ?? 0;
// Get total categories
const categories = await categoryQueries.getAll(db);
const totalCategories = categories.length;
// Get unique contributors (browse-ready skills only)
const contributorsResult = await db
.select({ count: sql<number>`count(distinct ${skills.githubOwner})::int` })
.from(skills)
.where(browseReady);
const totalContributors = contributorsResult[0]?.count ?? 0;
// Get total indexed skills (all, before curation) for curation note
const totalIndexedResult = await db
.select({ count: sql<number>`count(*)::int` })
.from(skills)
.where(sql`${skills.isBlocked} = false`);
const totalIndexed = totalIndexedResult[0]?.count ?? 0;
return {
totalSkills,
totalDownloads,
totalCategories,
totalContributors,
totalIndexed,
platforms: 5,
};
} catch (error) {
console.error('Error fetching stats:', error);
return null;
}
}
// Get featured skills directly from database
async function getFeaturedSkills() {
try {
const db = createDb();
// Get featured skills, or top skills by popularity if none are featured
let featuredSkills = await skillQueries.getFeatured(db, 6);
if (featuredSkills.length === 0) {
// Fallback to adaptive popularity with owner/repo diversity
featuredSkills = await skillQueries.getFeaturedWithDiversity(db, 6, 2, 3);
}
return featuredSkills;
} catch (error) {
console.error('Error fetching featured skills:', error);
return [];
}
}
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations('home');
const tCommon = await getTranslations('common');
const isRTL = locale === 'fa';
const ArrowIcon = isRTL ? ArrowLeft : ArrowRight;
// Fetch real data
const [statsData, featuredSkills] = await Promise.all([
getStats(),
getFeaturedSkills(),
]);
const stats = [
{ value: statsData ? formatCompactNumber(statsData.totalSkills, locale) : '۰', label: t('stats.skills'), icon: Layers },
{ value: statsData ? formatCompactNumber(statsData.totalDownloads, locale) : '۰', label: t('stats.downloads'), icon: Download },
{ value: statsData ? formatCompactNumber(statsData.totalContributors, locale) : '۰', label: t('stats.contributors'), icon: Users },
{ value: statsData ? formatCompactNumber(statsData.totalCategories || 8, locale) : '۸', label: t('stats.categories'), icon: Sparkles },
];
const steps = [
{
icon: Search,
title: t('howItWorks.step1.title'),
description: t('howItWorks.step1.description'),
},
{
icon: Terminal,
title: t('howItWorks.step2.title'),
description: t('howItWorks.step2.description'),
},
{
icon: Zap,
title: t('howItWorks.step3.title'),
description: t('howItWorks.step3.description'),
},
];
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1">
{/* Hero Section */}
<section className="relative overflow-hidden bg-gradient-subtle">
<div className="container-main py-20 lg:py-32">
<div className="max-w-3xl mx-auto text-center">
{/* Tagline */}
<p className="hero-tagline mb-4 animate-fade-up">
{t('hero.tagline')}
</p>
{/* Title */}
<h1 className="hero-title mb-6 animate-fade-up animation-delay-100 whitespace-pre-line">
{t('hero.title')}
</h1>
{/* Subtitle */}
<p className="hero-subtitle mb-8 animate-fade-up animation-delay-200">
{t('hero.subtitle')}
</p>
{/* Search - Client Component */}
<HeroSearch
placeholder={t('hero.searchPlaceholder')}
locale={locale}
/>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center animate-fade-up animation-delay-400">
<Link href={`/${locale}/browse`} className="btn-primary gap-2">
{t('hero.cta')}
<ArrowIcon className="w-4 h-4" />
</Link>
<Link href={`/${locale}/docs/getting-started`} className="btn-secondary">
{t('hero.ctaSecondary')}
</Link>
</div>
</div>
</div>
{/* Decorative elements */}
<div className="absolute inset-0 -z-10 overflow-hidden">
<div className="absolute top-1/4 start-1/4 w-96 h-96 bg-primary-200/30 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 end-1/4 w-96 h-96 bg-gold/20 rounded-full blur-3xl" />
</div>
</section>
{/* Stats Section */}
<section className="py-12 bg-surface border-y border-border">
<div className="container-main">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{stats.map((stat, index) => (
<div key={index} className="text-center">
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary-50 text-primary-600 mb-3">
<stat.icon className="w-6 h-6" />
</div>
<div className="text-3xl font-bold text-text-primary ltr-nums mb-1">
{stat.value}
</div>
<div className="text-text-secondary">
{stat.label}
</div>
</div>
))}
</div>
<p className="text-center text-text-muted text-sm mt-6">
{t('stats.curationNote', { totalIndexed: formatCompactNumber(statsData?.totalIndexed ?? 0, locale) })}
</p>
</div>
</section>
{/* Featured Skills */}
<section className="section bg-surface">
<div className="container-main">
<div className="text-center mb-12">
<h2 className="section-title">{t('featured.title')}</h2>
<p className="section-subtitle">{t('featured.subtitle')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{featuredSkills.length > 0 ? (
featuredSkills.map((skill) => (
<SkillCard
key={skill.id}
skill={skill}
locale={locale}
/>
))
) : (
// Fallback placeholder cards if no skills
[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="card p-6 animate-pulse">
<div className="h-6 bg-surface-subtle rounded mb-2 w-1/3"></div>
<div className="h-4 bg-surface-subtle rounded mb-4 w-2/3"></div>
<div className="flex gap-4">
<div className="h-4 bg-surface-subtle rounded w-16"></div>
<div className="h-4 bg-surface-subtle rounded w-16"></div>
</div>
</div>
))
)}
</div>
<div className="text-center mt-8">
<Link href={`/${locale}/featured`} className="btn-secondary gap-2">
{tCommon('viewAll')}
<ArrowIcon className="w-4 h-4" />
</Link>
</div>
</div>
</section>
{/* How It Works */}
<section className="section bg-surface-muted">
<div className="container-main">
<div className="text-center mb-12">
<h2 className="section-title">{t('howItWorks.title')}</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{steps.map((step, index) => (
<div key={index} className="text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-primary text-white mb-6 shadow-primary">
<step.icon className="w-8 h-8" />
</div>
<h3 className="text-xl font-semibold text-text-primary mb-3">
{step.title}
</h3>
<p className="text-text-secondary">
{step.description}
</p>
</div>
))}
</div>
{/* CLI Example */}
<div className="max-w-2xl mx-auto mt-12" dir="ltr">
<div className="glass-card p-6">
<div className="flex items-center gap-2 mb-4">
<div className="w-3 h-3 rounded-full bg-error" />
<div className="w-3 h-3 rounded-full bg-warning" />
<div className="w-3 h-3 rounded-full bg-success" />
</div>
<code className="block text-sm font-mono text-text-primary text-start">
<span className="text-text-muted">$</span> npx skillhub install anthropics/skills/pdf
</code>
<code className="block text-sm font-mono text-success mt-2 text-start">
Skill installed to ~/.claude/skills/pdf/
</code>
</div>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}