SkillSlap Component Builder
Generate Next.js 16 App Router components following SkillSlap patterns: Tailwind + Shadcn, server/client split, typed Supabase queries, no SELECT *. Describe the UI you need and get a production-ready component.
@system/skillslap-component-builder
SkillSlap Component Builder
Purpose: Generate a production-ready Next.js 16 App Router component following SkillSlap's exact codebase conventions. The output should be paste-ready into
skillslap/components/with no modification.
Invocation
/component <description>
Examples:
/component A skill report modal — user selects a reason from a dropdown and submits/component A collections sidebar showing the user's saved skill collections with add/remove/component A skill version history timeline showing diffs between versions
SkillSlap Component Conventions
Server vs Client
Server component (default — no 'use client'):
- Fetches data directly from Supabase via
createClient()from@/lib/supabase/server - Cannot use hooks, event listeners, or browser APIs
- Use for: pages, data-fetching wrappers, layout sections
Client component (add 'use client' at top):
- Required for:
useState,useEffect, event handlers,useRouter,useSearchParams - Keep client components small — push data fetching up to a server parent
// Server parent passes data down
async function SkillCollectionsList({ userId }: { userId: string }) {
const supabase = await createClient()
const { data } = await (supabase.from('skill_collections') as any)
.select('id, title, created_at')
.eq('user_id', userId)
return <SkillCollectionsClient collections={data ?? []} />
}
Supabase queries — NEVER SELECT *
Always use explicit column lists. For skills table use SKILLS_COLUMNS from @/lib/supabase/skills-columns:
import { SKILLS_COLUMNS } from '@/lib/supabase/skills-columns'
const { data } = await supabase
.from('skills')
.select(`${SKILLS_COLUMNS}, author:users!skills_author_id_fkey(id, username, avatar_url)`)
.eq('status', 'active')
For joins or partial selects, add as any to satisfy the typed client:
const { data } = await (supabase.from('skill_collections') as any)
.select('id, title, skill_count')
Styling — Tailwind + cn() utility
import { cn } from '@/lib/utils/cn'
// Always use cn() for conditional classes
<div className={cn(
'base-classes here',
isActive && 'active-classes',
className // always accept className prop
)} />
Color palette:
- Background:
bg-gray-950,bg-gray-900,bg-gray-800 - Borders:
border-gray-800,border-gray-700 - Text:
text-white,text-gray-300,text-gray-400,text-gray-500 - Accent:
text-purple-400,border-purple-600/50,bg-purple-600 - Success:
text-green-400 - Warning:
text-yellow-400 - Danger:
text-red-400
Shadcn UI components (already installed)
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import * as Dialog from '@radix-ui/react-dialog'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
Icons — lucide-react only
import { Plus, X, Search, ChevronDown, Loader2 } from 'lucide-react'
Error + loading states
// Loading skeleton
<div className="h-8 bg-gray-800 rounded animate-pulse" />
// Error display
<p className="text-sm text-red-400">{error}</p>
// Empty state
<div className="text-center py-12 text-gray-500">
<p className="text-sm">No items yet</p>
</div>
Forms + mutations (client component)
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client' // note: client, not server
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
setError(null)
try {
const supabase = createClient()
const { error } = await supabase.from('table').insert({ ... })
if (error) throw error
onSuccess?.()
} catch (err: any) {
setError(err.message ?? 'Something went wrong')
} finally {
setLoading(false)
}
}
Props interface pattern
interface MyComponentProps {
// required props first
skillId: string
// optional props with defaults
className?: string
onSuccess?: () => void
}
export function MyComponent({ skillId, className, onSuccess }: MyComponentProps) {
TypeScript types
import type { SkillWithAuthor } from '@/lib/supabase/types'
Output Format
- Main component file — full TypeScript, no placeholders, no TODOs
- File path — where it belongs in
skillslap/components/ - Imports — only what's used
- If server + client split needed — output both files
Checklist Before Outputting
- No
SELECT *— explicit columns everywhere -
as anyon Supabase joins -
cn()for all conditional classNames -
className?: stringprop on every component - Loading and error states present
-
'use client'only where actually needed - No hardcoded colors — use SkillSlap palette
$20 more to next tier
Created by
Info
Embed
Add this skill card to any webpage.
<iframe src="https://skillslap.com/skill/a4d142cd-d11b-47ac-98e4-6d9432bc3700/embed"
width="400" height="200"
style="border:none;border-radius:12px;"
title="SkillSlap Skill: SkillSlap Component Builder">
</iframe>