feat: enhance web app with fuzzy search, syntax highlighting, and pagination

- Expand README with detailed Web App section (English)
- Improve SEO meta tags in index.html
- Add rehype-highlight for code syntax highlighting in skill details
- Implement fuzzy search with scoring (name > category > description)
- Add clear search button and result counter
- Implement Load More pagination (24 items initially) for 950+ skills
- Add rehype-highlight and highlight.js dependencies

Made-with: Cursor
This commit is contained in:
sck_0
2026-02-27 09:05:13 +01:00
parent 1e73502c3d
commit 6b4dae330c
6 changed files with 208 additions and 33 deletions

View File

@@ -10,6 +10,7 @@ export function Home() {
const [search, setSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState('all');
const [loading, setLoading] = useState(true);
const [displayCount, setDisplayCount] = useState(24); // Show 24 initially (6 rows of 4)
useEffect(() => {
fetch('/skills.json')
@@ -25,15 +26,39 @@ export function Home() {
});
}, []);
// Fuzzy search with scoring
const calculateScore = (skill, terms) => {
let score = 0;
const nameLower = skill.name.toLowerCase();
const descLower = (skill.description || '').toLowerCase();
const catLower = (skill.category || '').toLowerCase();
for (const term of terms) {
// Exact name match (highest priority)
if (nameLower === term) score += 100;
// Name starts with term
else if (nameLower.startsWith(term)) score += 50;
// Name contains term
else if (nameLower.includes(term)) score += 30;
// Category match
else if (catLower.includes(term)) score += 20;
// Description contains term
else if (descLower.includes(term)) score += 10;
}
return score;
};
useEffect(() => {
let result = skills;
if (search) {
const lowerSearch = search.toLowerCase();
result = result.filter(skill =>
skill.name.toLowerCase().includes(lowerSearch) ||
skill.description.toLowerCase().includes(lowerSearch)
);
const terms = search.toLowerCase().trim().split(/\s+/).filter(t => t.length > 0);
if (terms.length > 0) {
result = result
.map(skill => ({ ...skill, _score: calculateScore(skill, terms) }))
.filter(skill => skill._score > 0)
.sort((a, b) => b._score - a._score);
}
}
if (categoryFilter !== 'all') {
@@ -43,6 +68,11 @@ export function Home() {
setFilteredSkills(result);
}, [search, categoryFilter, skills]);
// Reset display count when search/filter changes
useEffect(() => {
setDisplayCount(24);
}, [search, categoryFilter]);
const categories = ['all', ...new Set(skills.map(s => s.category).filter(Boolean))];
return (
@@ -50,7 +80,11 @@ export function Home() {
<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 {skills.length} agentic capabilities for your AI assistant.</p>
<p className="text-slate-500 dark:text-slate-400">
{search || categoryFilter !== 'all'
? `Showing ${filteredSkills.length} of ${skills.length} skills`
: `Discover ${skills.length} agentic capabilities for your AI assistant.`}
</p>
</div>
</div>
@@ -59,11 +93,20 @@ export function Home() {
<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')..."
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"
placeholder="Search skills (e.g., 'react', 'security', 'python', 'testing')..."
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 pr-9"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{search && (
<button
onClick={() => setSearch('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
title="Clear search"
>
×
</button>
)}
</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" />
@@ -93,7 +136,7 @@ export function Home() {
<p className="mt-2 text-slate-500 dark:text-slate-400">Try adjusting your search or filter.</p>
</div>
) : (
filteredSkills.map((skill) => (
filteredSkills.slice(0, displayCount).map((skill) => (
<motion.div
key={skill.id}
layout
@@ -133,6 +176,21 @@ export function Home() {
))
)}
</AnimatePresence>
{/* Load More Button */}
{!loading && filteredSkills.length > displayCount && (
<div className="col-span-full flex justify-center py-8">
<button
onClick={() => setDisplayCount(prev => prev + 24)}
className="flex items-center space-x-2 px-6 py-3 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
<span>Load More</span>
<span className="text-sm text-slate-500 dark:text-slate-400">
({filteredSkills.length - displayCount} remaining)
</span>
</button>
</div>
)}
</div>
</div>
);

View File

@@ -2,7 +2,9 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import Markdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import { ArrowLeft, Copy, Check, FileCode, AlertTriangle } from 'lucide-react';
import 'highlight.js/styles/github-dark.css';
export function SkillDetail() {
const { id } = useParams();
@@ -157,8 +159,8 @@ export function SkillDetail() {
</div>
<div className="p-6 sm:p-8">
<div className="prose prose-slate dark:prose-invert max-w-none">
<Markdown>{content}</Markdown>
<div className="prose prose-slate dark:prose-invert max-w-none prose-code:before:content-none prose-code:after:content-none">
<Markdown rehypePlugins={[rehypeHighlight]}>{content}</Markdown>
</div>
</div>
</div>