Add visible FAQ and concepts content, strengthen tool-specific integration guides, and publish a dedicated skills-vs-MCP explainer. Extend homepage SEO metadata and JSON-LD so the GitHub Pages catalog better reflects the repository's real positioning and common user questions.
417 lines
20 KiB
TypeScript
417 lines
20 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
||
import { Search, Filter, AlertCircle, RefreshCw, ArrowUpDown } from 'lucide-react';
|
||
import { VirtuosoGrid } from 'react-virtuoso';
|
||
import { useSkills } from '../context/SkillContext';
|
||
import { SkillCard } from '../components/SkillCard';
|
||
import type { SyncMessage, CategoryStats } from '../types';
|
||
import { usePageMeta } from '../hooks/usePageMeta';
|
||
import { APP_HOME_CATALOG_COUNT, buildHomeMeta, getHomeFaqItems } from '../utils/seo';
|
||
|
||
const conceptCards = [
|
||
{
|
||
title: 'Skills',
|
||
body: 'Reusable SKILL.md playbooks that teach an AI assistant how to execute a workflow with better structure and context.',
|
||
},
|
||
{
|
||
title: 'MCP tools',
|
||
body: 'External capabilities and system integrations the assistant can call. Tools provide actions; skills tell the assistant how to use them well.',
|
||
},
|
||
{
|
||
title: 'Bundles',
|
||
body: 'Curated starting sets of recommended skills for a role, domain, or team that wants a smaller shortlist first.',
|
||
},
|
||
{
|
||
title: 'Workflows',
|
||
body: 'Ordered execution playbooks that show how to combine multiple skills step by step for a concrete outcome.',
|
||
},
|
||
] as const;
|
||
|
||
const integrationGuides = [
|
||
{
|
||
name: 'Claude Code',
|
||
href: 'https://github.com/sickn33/antigravity-awesome-skills/blob/main/docs/users/claude-code-skills.md',
|
||
body: 'Install paths, starter prompts, plugin marketplace flow, and first skills to try.',
|
||
},
|
||
{
|
||
name: 'Cursor',
|
||
href: 'https://github.com/sickn33/antigravity-awesome-skills/blob/main/docs/users/cursor-skills.md',
|
||
body: 'A practical guide for chat-first UI, frontend, and full-stack workflows in Cursor.',
|
||
},
|
||
{
|
||
name: 'Codex CLI',
|
||
href: 'https://github.com/sickn33/antigravity-awesome-skills/blob/main/docs/users/codex-cli-skills.md',
|
||
body: 'How to use Antigravity Awesome Skills with Codex CLI for planning, implementation, testing, and review.',
|
||
},
|
||
{
|
||
name: 'Gemini CLI',
|
||
href: 'https://github.com/sickn33/antigravity-awesome-skills/blob/main/docs/users/gemini-cli-skills.md',
|
||
body: 'A broad starting point for engineering, agent systems, integrations, and applied AI workflows.',
|
||
},
|
||
] as const;
|
||
|
||
export function Home(): React.ReactElement {
|
||
const { skills, stars, loading, error, refreshSkills } = useSkills();
|
||
const [search, setSearch] = useState('');
|
||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||
const [categoryFilter, setCategoryFilter] = useState('all');
|
||
const [sortBy, setSortBy] = useState('default');
|
||
const [syncing, setSyncing] = useState(false);
|
||
const [syncMsg, setSyncMsg] = useState<SyncMessage | null>(null);
|
||
const [commandCopied, setCommandCopied] = useState(false);
|
||
const installCommand = 'npx antigravity-awesome-skills';
|
||
const docsLink = 'https://github.com/sickn33/antigravity-awesome-skills/blob/main/docs/users/usage.md';
|
||
const installLink = 'https://www.npmjs.com/package/antigravity-awesome-skills';
|
||
const faqItems = getHomeFaqItems();
|
||
|
||
usePageMeta(buildHomeMeta(skills.length));
|
||
|
||
const copyInstallCommand = async () => {
|
||
await navigator.clipboard.writeText(installCommand);
|
||
setCommandCopied(true);
|
||
window.setTimeout(() => setCommandCopied(false), 2000);
|
||
};
|
||
|
||
useEffect(() => {
|
||
const timeoutId = window.setTimeout(() => {
|
||
setDebouncedSearch(search);
|
||
}, 300);
|
||
|
||
return () => {
|
||
window.clearTimeout(timeoutId);
|
||
};
|
||
}, [search]);
|
||
|
||
const filteredSkills = useMemo(() => {
|
||
let result = [...skills];
|
||
|
||
if (debouncedSearch) {
|
||
const lowerSearch = debouncedSearch.toLowerCase();
|
||
result = result.filter(skill =>
|
||
skill.name.toLowerCase().includes(lowerSearch) ||
|
||
skill.description.toLowerCase().includes(lowerSearch)
|
||
);
|
||
}
|
||
|
||
if (categoryFilter !== 'all') {
|
||
result = result.filter(skill => skill.category === categoryFilter);
|
||
}
|
||
|
||
// Apply sorting
|
||
if (sortBy === 'stars') {
|
||
result = [...result].sort((a, b) => (stars[b.id] || 0) - (stars[a.id] || 0));
|
||
} else if (sortBy === 'newest') {
|
||
result = [...result].sort((a, b) => (b.date_added || '').localeCompare(a.date_added || ''));
|
||
} else if (sortBy === 'az') {
|
||
result = [...result].sort((a, b) => a.name.localeCompare(b.name));
|
||
}
|
||
|
||
return result;
|
||
}, [debouncedSearch, categoryFilter, sortBy, skills, stars]);
|
||
|
||
// Sort categories by count (most skills first), with 'uncategorized' at the end
|
||
const { categories, categoryStats } = useMemo(() => {
|
||
const stats: CategoryStats = {};
|
||
skills.forEach(skill => {
|
||
stats[skill.category] = (stats[skill.category] || 0) + 1;
|
||
});
|
||
|
||
const cats = ['all', ...Object.keys(stats)
|
||
.filter(cat => cat !== 'uncategorized')
|
||
.sort((a, b) => stats[b] - stats[a]),
|
||
...(stats['uncategorized'] ? ['uncategorized'] : [])
|
||
];
|
||
|
||
return { categories: cats, categoryStats: stats };
|
||
}, [skills]);
|
||
|
||
const handleSync = async () => {
|
||
setSyncing(true);
|
||
setSyncMsg(null);
|
||
try {
|
||
const res = await fetch('/api/refresh-skills', { method: 'POST' });
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
if (data.upToDate) {
|
||
setSyncMsg({ type: 'info', text: 'ℹ️ Skills are already up to date!' });
|
||
} else {
|
||
setSyncMsg({ type: 'success', text: `✅ Synced ${data.count} skills!` });
|
||
await refreshSkills();
|
||
}
|
||
} else {
|
||
setSyncMsg({ type: 'error', text: `❌ ${data.error}` });
|
||
}
|
||
} catch {
|
||
setSyncMsg({ type: 'error', text: '❌ Network error' });
|
||
} finally {
|
||
setSyncing(false);
|
||
setTimeout(() => setSyncMsg(null), 5000);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col min-h-[calc(100vh-8rem)]">
|
||
<div className="space-y-8 mb-8">
|
||
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 sm:p-7 shadow-sm">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-3">
|
||
Take action
|
||
</p>
|
||
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-100">
|
||
Discover, install, and use trusted AI skills in minutes
|
||
</h2>
|
||
<p className="mt-3 text-sm sm:text-base leading-relaxed text-slate-600 dark:text-slate-300 max-w-4xl">
|
||
Antigravity Awesome Skills is a discoverable catalog of installable capabilities for AI assistants.
|
||
Install once, then test the highest-value skill directly from your terminal without waiting for documentation hops.
|
||
Search, filter, then copy a ready-to-run prompt in one pass.
|
||
</p>
|
||
<div className="mt-5 flex flex-col gap-3 sm:flex-row sm:items-stretch">
|
||
<button
|
||
onClick={copyInstallCommand}
|
||
className="inline-flex items-center justify-center rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-indigo-700"
|
||
>
|
||
{commandCopied ? 'Copied install command' : 'Copy install command'}
|
||
</button>
|
||
<a
|
||
href={installLink}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="inline-flex items-center justify-center rounded-lg border border-indigo-600 text-sm font-semibold text-indigo-700 dark:text-indigo-200 px-4 py-2.5 hover:bg-indigo-50 dark:hover:bg-slate-800"
|
||
>
|
||
Install with npm
|
||
</a>
|
||
<a
|
||
href={docsLink}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="inline-flex items-center justify-center rounded-lg border border-slate-200 dark:border-slate-700 text-sm font-semibold text-slate-700 dark:text-slate-200 px-4 py-2.5 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||
>
|
||
Read getting started docs
|
||
</a>
|
||
</div>
|
||
<p className="mt-3 text-xs text-slate-500 dark:text-slate-400">
|
||
Recommended command:
|
||
<span className="ml-2 rounded-md bg-slate-100 dark:bg-slate-800 px-2 py-1 font-mono">{installCommand}</span>
|
||
</p>
|
||
</section>
|
||
|
||
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:justify-between md:space-y-0">
|
||
<div>
|
||
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-100 mb-2">Explore Skills</h1>
|
||
<p className="text-slate-500 dark:text-slate-400">
|
||
Discover {Math.max(skills.length, APP_HOME_CATALOG_COUNT)}+ agentic capabilities for your AI assistant.
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
{syncMsg && (
|
||
<span className={`text-sm font-medium px-3 py-1.5 rounded-full ${syncMsg.type === 'success'
|
||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||
: syncMsg.type === 'info'
|
||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||
}`}>
|
||
{syncMsg.text}
|
||
</span>
|
||
)}
|
||
<button
|
||
onClick={handleSync}
|
||
disabled={syncing}
|
||
className="flex items-center space-x-2 px-4 py-2.5 rounded-lg font-medium text-sm bg-indigo-600 hover:bg-indigo-700 text-white disabled:opacity-50 disabled:cursor-wait transition-colors shadow-sm"
|
||
>
|
||
<RefreshCw className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
|
||
<span>{syncing ? 'Syncing...' : 'Sync Skills'}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:space-x-4 md:space-y-0 bg-white dark:bg-slate-900 p-4 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm sticky top-0 z-40">
|
||
<div className="relative flex-1">
|
||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" />
|
||
<input
|
||
type="text"
|
||
placeholder="Search skills (e.g., 'react', 'security', 'python')..."
|
||
aria-label="Search skills"
|
||
className="w-full rounded-md border border-slate-200 bg-slate-50 px-9 py-2 text-sm outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50"
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center space-x-2 overflow-x-auto pb-2 md:pb-0 scrollbar-hide">
|
||
<Filter className="h-4 w-4 text-slate-500 shrink-0" />
|
||
<select
|
||
aria-label="Filter by category"
|
||
className="h-9 rounded-md border border-slate-200 bg-slate-50 px-3 text-sm outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50 min-w-[150px]"
|
||
value={categoryFilter}
|
||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||
>
|
||
{categories.map(cat => (
|
||
<option key={cat} value={cat}>
|
||
{cat === 'all'
|
||
? 'All Categories'
|
||
: `${cat.charAt(0).toUpperCase() + cat.slice(1)} (${categoryStats[cat] || 0})`
|
||
}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<ArrowUpDown className="h-4 w-4 text-slate-500 shrink-0 ml-2" />
|
||
<select
|
||
aria-label="Sort skills"
|
||
className="h-9 rounded-md border border-slate-200 bg-slate-50 px-3 text-sm outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50 min-w-[130px]"
|
||
value={sortBy}
|
||
onChange={(e) => setSortBy(e.target.value)}
|
||
>
|
||
<option value="default">Default</option>
|
||
<option value="stars">⭐ Most Stars</option>
|
||
<option value="newest">🆕 Newest</option>
|
||
<option value="az">🔤 A → Z</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex-1 min-h-[60vh] sm:min-h-[68vh] lg:min-h-[72vh] -mx-4">
|
||
{loading ? (
|
||
<div data-testid="loader" className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 px-4">
|
||
{[...Array(8)].map((_, i) => (
|
||
<div key={i} className="animate-pulse rounded-lg border border-slate-200 p-6 h-48 bg-slate-100 dark:border-slate-800 dark:bg-slate-900">
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : error && skills.length === 0 ? (
|
||
<div className="py-12 text-center px-4 sm:px-6 lg:px-8">
|
||
<AlertCircle className="mx-auto h-12 w-12 text-red-400" />
|
||
<h3 className="mt-4 text-lg font-semibold text-slate-900 dark:text-slate-100">Unable to load skills</h3>
|
||
<p className="mt-2 text-slate-500 dark:text-slate-400">{error}</p>
|
||
<button
|
||
onClick={() => void refreshSkills()}
|
||
className="mt-5 inline-flex items-center justify-center rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-indigo-700"
|
||
>
|
||
Retry loading catalog
|
||
</button>
|
||
</div>
|
||
) : filteredSkills.length === 0 ? (
|
||
<div className="py-12 text-center px-4 sm:px-6 lg:px-8">
|
||
<AlertCircle className="mx-auto h-12 w-12 text-slate-400" />
|
||
<h3 className="mt-4 text-lg font-semibold text-slate-900 dark:text-slate-100">No skills found</h3>
|
||
<p className="mt-2 text-slate-500 dark:text-slate-400">Try adjusting your search or filter.</p>
|
||
</div>
|
||
) : (
|
||
<VirtuosoGrid
|
||
useWindowScroll
|
||
totalCount={filteredSkills.length}
|
||
listClassName="grid gap-6 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 pb-8 px-4"
|
||
itemContent={(index) => {
|
||
const skill = filteredSkills[index];
|
||
return <SkillCard key={skill.id} skill={skill} starCount={stars[skill.id] || 0} />;
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-12 space-y-10">
|
||
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 sm:p-7 shadow-sm">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-3">
|
||
Concepts
|
||
</p>
|
||
<h2 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-100">
|
||
Understand the moving pieces before you install everything
|
||
</h2>
|
||
<p className="mt-3 max-w-4xl text-sm sm:text-base leading-relaxed text-slate-600 dark:text-slate-300">
|
||
The catalog is easier to navigate once you separate reusable playbooks from external tool integrations.
|
||
Skills explain how to execute a workflow well, MCP tools expose external systems, bundles narrow the
|
||
starting set, and workflows show the order of operations.
|
||
</p>
|
||
<div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||
{conceptCards.map((card) => (
|
||
<article
|
||
key={card.title}
|
||
className="rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950 p-4"
|
||
>
|
||
<h3 className="text-base font-semibold text-slate-900 dark:text-slate-100">{card.title}</h3>
|
||
<p className="mt-2 text-sm leading-relaxed text-slate-600 dark:text-slate-300">{card.body}</p>
|
||
</article>
|
||
))}
|
||
</div>
|
||
<div className="mt-5 flex flex-wrap gap-3">
|
||
<a
|
||
href="https://github.com/sickn33/antigravity-awesome-skills/blob/main/docs/users/skills-vs-mcp-tools.md"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="inline-flex items-center justify-center rounded-lg border border-slate-200 dark:border-slate-700 px-4 py-2.5 text-sm font-semibold text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||
>
|
||
Read skills vs MCP/tools
|
||
</a>
|
||
<a
|
||
href="https://github.com/sickn33/antigravity-awesome-skills/blob/main/docs/users/bundles.md"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="inline-flex items-center justify-center rounded-lg border border-slate-200 dark:border-slate-700 px-4 py-2.5 text-sm font-semibold text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||
>
|
||
Browse bundles
|
||
</a>
|
||
<a
|
||
href="https://github.com/sickn33/antigravity-awesome-skills/blob/main/docs/users/workflows.md"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="inline-flex items-center justify-center rounded-lg border border-slate-200 dark:border-slate-700 px-4 py-2.5 text-sm font-semibold text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||
>
|
||
Explore workflows
|
||
</a>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 sm:p-7 shadow-sm">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-3">
|
||
Integration Guides
|
||
</p>
|
||
<h2 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-100">
|
||
Start from the guide that matches your AI assistant
|
||
</h2>
|
||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||
{integrationGuides.map((guide) => (
|
||
<a
|
||
key={guide.name}
|
||
href={guide.href}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950 p-4 transition-colors hover:border-indigo-300 dark:hover:border-indigo-700"
|
||
>
|
||
<h3 className="text-base font-semibold text-slate-900 dark:text-slate-100">{guide.name}</h3>
|
||
<p className="mt-2 text-sm leading-relaxed text-slate-600 dark:text-slate-300">{guide.body}</p>
|
||
</a>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 sm:p-7 shadow-sm">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-3">
|
||
Quick FAQ
|
||
</p>
|
||
<h2 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-100">
|
||
Answers to the first questions most users ask
|
||
</h2>
|
||
<div className="mt-6 grid gap-4 lg:grid-cols-2">
|
||
{faqItems.map((item) => (
|
||
<article
|
||
key={item.question}
|
||
className="rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950 p-4"
|
||
>
|
||
<h3 className="text-base font-semibold text-slate-900 dark:text-slate-100">{item.question}</h3>
|
||
<p className="mt-2 text-sm leading-relaxed text-slate-600 dark:text-slate-300">{item.answer}</p>
|
||
</article>
|
||
))}
|
||
</div>
|
||
<a
|
||
href="https://github.com/sickn33/antigravity-awesome-skills/blob/main/docs/users/faq.md"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="mt-5 inline-flex items-center justify-center rounded-lg border border-slate-200 dark:border-slate-700 px-4 py-2.5 text-sm font-semibold text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||
>
|
||
Read the full FAQ
|
||
</a>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default Home;
|