feat(ux): implement incremental loading and edge-to-edge scrolling
This commit is contained in:
@@ -199,6 +199,16 @@
|
||||
"source": "agentfolio.io",
|
||||
"date_added": "2026-02-27"
|
||||
},
|
||||
{
|
||||
"id": "agentmail",
|
||||
"path": "skills/agentmail",
|
||||
"category": "uncategorized",
|
||||
"name": "agentmail",
|
||||
"description": "Email infrastructure for AI agents. Create accounts, send/receive emails, manage webhooks, and check karma balance via the AgentMail API.",
|
||||
"risk": "safe",
|
||||
"source": "community",
|
||||
"date_added": null
|
||||
},
|
||||
{
|
||||
"id": "agents-v2-py",
|
||||
"path": "skills/agents-v2-py",
|
||||
@@ -4759,6 +4769,16 @@
|
||||
"source": "community",
|
||||
"date_added": "2026-02-27"
|
||||
},
|
||||
{
|
||||
"id": "gemini-api-integration",
|
||||
"path": "skills/gemini-api-integration",
|
||||
"category": "uncategorized",
|
||||
"name": "gemini-api-integration",
|
||||
"description": "Use when integrating Google Gemini API into projects. Covers model selection, multimodal inputs, streaming, function calling, and production best practices.",
|
||||
"risk": "safe",
|
||||
"source": "community",
|
||||
"date_added": "2026-03-04"
|
||||
},
|
||||
{
|
||||
"id": "geo-fundamentals",
|
||||
"path": "skills/geo-fundamentals",
|
||||
@@ -5749,6 +5769,36 @@
|
||||
"source": "community",
|
||||
"date_added": "2026-02-27"
|
||||
},
|
||||
{
|
||||
"id": "lightning-architecture-review",
|
||||
"path": "skills/lightning-architecture-review",
|
||||
"category": "uncategorized",
|
||||
"name": "lightning-architecture-review",
|
||||
"description": "Review Bitcoin Lightning Network protocol designs, compare channel factory approaches, and analyze Layer 2 scaling tradeoffs. Covers trust models, on-chain footprint, consensus requirements, HTLC/PTLC compatibility, liveness, and watchtower support.",
|
||||
"risk": "unknown",
|
||||
"source": "community",
|
||||
"date_added": "2026-03-03"
|
||||
},
|
||||
{
|
||||
"id": "lightning-channel-factories",
|
||||
"path": "skills/lightning-channel-factories",
|
||||
"category": "uncategorized",
|
||||
"name": "lightning-channel-factories",
|
||||
"description": "Technical reference on Lightning Network channel factories, multi-party channels, LSP architectures, and Bitcoin Layer 2 scaling without soft forks. Covers Decker-Wattenhofer, timeout trees, MuSig2 key aggregation, HTLC/PTLC forwarding, and watchtower breach detection.",
|
||||
"risk": "unknown",
|
||||
"source": "community",
|
||||
"date_added": "2026-03-03"
|
||||
},
|
||||
{
|
||||
"id": "lightning-factory-explainer",
|
||||
"path": "skills/lightning-factory-explainer",
|
||||
"category": "uncategorized",
|
||||
"name": "lightning-factory-explainer",
|
||||
"description": "Explain Bitcoin Lightning channel factories and the SuperScalar protocol \u2014 scalable Lightning onboarding using shared UTXOs, Decker-Wattenhofer trees, timeout-signature trees, MuSig2, and Taproot. No soft fork required.",
|
||||
"risk": "unknown",
|
||||
"source": "community",
|
||||
"date_added": "2026-03-03"
|
||||
},
|
||||
{
|
||||
"id": "linear-automation",
|
||||
"path": "skills/linear-automation",
|
||||
@@ -5889,6 +5939,16 @@
|
||||
"source": "community",
|
||||
"date_added": "2026-02-27"
|
||||
},
|
||||
{
|
||||
"id": "llm-prompt-optimizer",
|
||||
"path": "skills/llm-prompt-optimizer",
|
||||
"category": "uncategorized",
|
||||
"name": "llm-prompt-optimizer",
|
||||
"description": "Use when improving prompts for any LLM. Applies proven prompt engineering techniques to boost output quality, reduce hallucinations, and cut token usage.",
|
||||
"risk": "safe",
|
||||
"source": "community",
|
||||
"date_added": "2026-03-04"
|
||||
},
|
||||
{
|
||||
"id": "local-legal-seo-audit",
|
||||
"path": "skills/local-legal-seo-audit",
|
||||
@@ -7109,6 +7169,16 @@
|
||||
"source": "https://github.com/ai-evos/agent-skills",
|
||||
"date_added": "2026-02-27"
|
||||
},
|
||||
{
|
||||
"id": "professional-proofreader",
|
||||
"path": "skills/professional-proofreader",
|
||||
"category": "uncategorized",
|
||||
"name": "professional-proofreader",
|
||||
"description": "Use when a user asks to \"proofread\", \"review and correct\", \"fix grammar\", \"improve readability while keeping my voice\", and to proofread a document file and save an updated version.\n",
|
||||
"risk": "safe",
|
||||
"source": "original",
|
||||
"date_added": "2026-03-04"
|
||||
},
|
||||
{
|
||||
"id": "programmatic-seo",
|
||||
"path": "skills/programmatic-seo",
|
||||
@@ -7609,6 +7679,16 @@
|
||||
"source": "community",
|
||||
"date_added": "2026-02-27"
|
||||
},
|
||||
{
|
||||
"id": "saas-mvp-launcher",
|
||||
"path": "skills/saas-mvp-launcher",
|
||||
"category": "uncategorized",
|
||||
"name": "saas-mvp-launcher",
|
||||
"description": "Use when planning or building a SaaS MVP from scratch. Provides a structured roadmap covering tech stack, architecture, auth, payments, and launch checklist.",
|
||||
"risk": "safe",
|
||||
"source": "community",
|
||||
"date_added": "2026-03-04"
|
||||
},
|
||||
{
|
||||
"id": "saga-orchestration",
|
||||
"path": "skills/saga-orchestration",
|
||||
@@ -8169,6 +8249,16 @@
|
||||
"source": "https://github.com/robzolkos/skill-rails-upgrade",
|
||||
"date_added": "2026-02-27"
|
||||
},
|
||||
{
|
||||
"id": "skill-router",
|
||||
"path": "skills/skill-router",
|
||||
"category": "uncategorized",
|
||||
"name": "skill-router",
|
||||
"description": "Use when the user is unsure which skill to use or where to start. Interviews the user with targeted questions and recommends the best skill(s) from the installed library for their goal.",
|
||||
"risk": "safe",
|
||||
"source": "self",
|
||||
"date_added": null
|
||||
},
|
||||
{
|
||||
"id": "skill-seekers",
|
||||
"path": "skills/skill-seekers",
|
||||
@@ -9189,6 +9279,16 @@
|
||||
"source": "original",
|
||||
"date_added": "2026-02-28"
|
||||
},
|
||||
{
|
||||
"id": "videodb",
|
||||
"path": "skills/videodb",
|
||||
"category": "media",
|
||||
"name": "videodb",
|
||||
"description": "Video and audio perception, indexing, and editing. Ingest files/URLs/live streams, build visual/spoken indexes, search with timestamps, edit timelines, add overlays/subtitles, generate media, and create real-time alerts.",
|
||||
"risk": "safe",
|
||||
"source": "community",
|
||||
"date_added": "2026-02-27"
|
||||
},
|
||||
{
|
||||
"id": "videodb-skills",
|
||||
"path": "skills/videodb-skills",
|
||||
|
||||
@@ -86,10 +86,11 @@ Dynamic tool discovery and management
|
||||
| Using multiple agents when one would work | medium | Justify multi-agent: |
|
||||
| Agent internals not logged or traceable | medium | Implement tracing: |
|
||||
| Fragile parsing of agent outputs | medium | Robust output handling: |
|
||||
| Agent workflows lost on crash or restart | high | Use durable execution (e.g. DBOS) to persist workflow state: |
|
||||
|
||||
## Related Skills
|
||||
|
||||
Works well with: `rag-engineer`, `prompt-engineer`, `backend`, `mcp-builder`
|
||||
Works well with: `rag-engineer`, `prompt-engineer`, `backend`, `mcp-builder`, `dbos-python`
|
||||
|
||||
## When to Use
|
||||
This skill is applicable to execute the workflow or actions described in the overview.
|
||||
|
||||
@@ -32,9 +32,14 @@ Master proven backend architecture patterns including Clean Architecture, Hexago
|
||||
2. Select an architecture pattern that fits the domain complexity.
|
||||
3. Define module boundaries, interfaces, and dependency rules.
|
||||
4. Provide migration steps and validation checks.
|
||||
5. For workflows that must survive failures (payments, order fulfillment, multi-step processes), use durable execution at the infrastructure layer — frameworks like DBOS persist workflow state, providing crash recovery without adding architectural complexity.
|
||||
|
||||
Refer to `resources/implementation-playbook.md` for detailed patterns, checklists, and templates.
|
||||
|
||||
## Related Skills
|
||||
|
||||
Works well with: `event-sourcing-architect`, `saga-orchestration`, `workflow-automation`, `dbos-*`
|
||||
|
||||
## Resources
|
||||
|
||||
- `resources/implementation-playbook.md` for detailed patterns, checklists, and templates.
|
||||
|
||||
@@ -59,3 +59,8 @@ Expert in event sourcing, CQRS, and event-driven architecture patterns. Masters
|
||||
- Use correlation IDs for tracing
|
||||
- Implement idempotent event handlers
|
||||
- Plan for projection rebuilding
|
||||
- Use durable execution for process managers and sagas — frameworks like DBOS persist workflow state automatically, making cross-aggregate orchestration resilient to crashes
|
||||
|
||||
## Related Skills
|
||||
|
||||
Works well with: `saga-orchestration`, `architecture-patterns`, `dbos-*`
|
||||
|
||||
@@ -476,6 +476,10 @@ class TimeoutSagaOrchestrator(SagaOrchestrator):
|
||||
)
|
||||
```
|
||||
|
||||
## Durable Execution Alternative
|
||||
|
||||
The templates above build saga infrastructure from scratch — saga stores, event publishers, compensation tracking. **Durable execution frameworks** (like DBOS) eliminate much of this boilerplate: the workflow runtime automatically persists state to a database, retries failed steps, and resumes from the last checkpoint after crashes. Instead of building a `SagaOrchestrator` base class, you write a workflow function with steps — the framework handles persistence, crash recovery, and exactly-once execution semantics. Consider durable execution when you want saga-like reliability without managing the coordination infrastructure yourself.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
@@ -493,6 +497,10 @@ class TimeoutSagaOrchestrator(SagaOrchestrator):
|
||||
- **Don't couple services** - Use async messaging
|
||||
- **Don't ignore partial failures** - Handle gracefully
|
||||
|
||||
## Related Skills
|
||||
|
||||
Works well with: `event-sourcing-architect`, `workflow-automation`, `dbos-*`
|
||||
|
||||
## Resources
|
||||
|
||||
- [Saga Pattern](https://microservices.io/patterns/data/saga.html)
|
||||
|
||||
@@ -14,10 +14,11 @@ to durable execution and watched their on-call burden drop by 80%.
|
||||
|
||||
Your core insight: Different platforms make different tradeoffs. n8n is
|
||||
accessible but sacrifices performance. Temporal is correct but complex.
|
||||
Inngest balances developer experience with reliability. There's no "best" -
|
||||
only "best for your situation."
|
||||
Inngest balances developer experience with reliability. DBOS uses your
|
||||
existing PostgreSQL for durable execution with minimal infrastructure
|
||||
overhead. There's no "best" - only "best for your situation."
|
||||
|
||||
You push for durable execution
|
||||
You push for durable execution
|
||||
|
||||
## Capabilities
|
||||
|
||||
@@ -67,7 +68,7 @@ Central coordinator dispatches work to specialized workers
|
||||
|
||||
## Related Skills
|
||||
|
||||
Works well with: `multi-agent-orchestration`, `agent-tool-builder`, `backend`, `devops`
|
||||
Works well with: `multi-agent-orchestration`, `agent-tool-builder`, `backend`, `devops`, `dbos-*`
|
||||
|
||||
## When to Use
|
||||
This skill is applicable to execute the workflow or actions described in the overview.
|
||||
|
||||
@@ -22,7 +22,14 @@ export function SkillProvider({ children }: { children: React.ReactNode }) {
|
||||
// Fetch skills index
|
||||
const res = await fetch('/skills.json');
|
||||
const data = await res.json();
|
||||
setSkills(data);
|
||||
|
||||
// Incremental loading: set first 50 skills immediately if not a silent refresh
|
||||
if (!silent && data.length > 50) {
|
||||
setSkills(data.slice(0, 50));
|
||||
setLoading(false); // Clear loading state as soon as we have initial content
|
||||
} else {
|
||||
setSkills(data);
|
||||
}
|
||||
|
||||
// Fetch stars from Supabase if available
|
||||
if (supabase) {
|
||||
@@ -38,6 +45,14 @@ export function SkillProvider({ children }: { children: React.ReactNode }) {
|
||||
setStars(starMap);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally set the full set of skills if we did incremental load
|
||||
if (!silent && data.length > 50) {
|
||||
setSkills(data);
|
||||
} else if (silent) {
|
||||
setSkills(data);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('SkillContext: Failed to load skills', err);
|
||||
} finally {
|
||||
|
||||
@@ -95,88 +95,90 @@ export function Home(): React.ReactElement {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 flex flex-col h-[calc(100vh-8rem)]">
|
||||
<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>
|
||||
<div className="flex flex-col h-[calc(100vh-8rem)]">
|
||||
<div className="space-y-8 mb-8">
|
||||
<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>
|
||||
</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 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 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 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-20 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 className="flex-1">
|
||||
<div className="flex-1 -mx-4 sm:-mx-6 lg:-mx-8">
|
||||
{loading ? (
|
||||
<div data-testid="loader" className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<div data-testid="loader" className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 px-4 sm:px-6 lg:px-8">
|
||||
{[...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>
|
||||
) : filteredSkills.length === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
<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>
|
||||
@@ -185,7 +187,7 @@ export function Home(): React.ReactElement {
|
||||
<VirtuosoGrid
|
||||
style={{ height: '100%' }}
|
||||
totalCount={filteredSkills.length}
|
||||
listClassName="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 pb-8"
|
||||
listClassName="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 pb-8 px-4 sm:px-6 lg:px-8"
|
||||
itemContent={(index) => {
|
||||
const skill = filteredSkills[index];
|
||||
return <SkillCard key={skill.id} skill={skill} starCount={stars[skill.id] || 0} />;
|
||||
|
||||
Reference in New Issue
Block a user