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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user