Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions app/(dashboard)/skills/_components/skills-library.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
'use client'

import { useState, useTransition } from 'react'
import { MdBolt, MdCheckCircle, MdOpenInNew } from 'react-icons/md'
import { useRouter } from 'next/navigation'
import { toast } from 'sonner'

import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { enableGlobalSkill } from '@/lib/actions/skill'
import { getSkillCatalog } from '@/lib/skills/catalog'

type SkillsLibraryProps = {
enabledSkillIds: string[]
}

export function SkillsLibrary({ enabledSkillIds }: SkillsLibraryProps) {
const router = useRouter()
const [pendingSkillId, setPendingSkillId] = useState<string | null>(null)
const [isPending, startTransition] = useTransition()
const catalog = getSkillCatalog()
const enabledSkillSet = new Set(enabledSkillIds)

const handleEnable = (skillId: string) => {
startTransition(async () => {
setPendingSkillId(skillId)

const result = await enableGlobalSkill(skillId)
if (!result.success) {
toast.error(result.error)
setPendingSkillId(null)
return
}

toast.success('Global skill enabled. Install tasks will fan out across your projects.')
router.refresh()
setPendingSkillId(null)
})
}

return (
<div className="space-y-8">
<div className="space-y-3">
<Badge variant="outline" className="border-primary/30 bg-primary/10 text-primary">
Global Skills
</Badge>
<div className="space-y-2">
<h1 className="text-3xl font-display font-bold text-white">Skills</h1>
<p className="max-w-3xl text-sm leading-6 text-muted-foreground">
Enabling a skill here creates global desired state for the user. Existing projects get
`INSTALL_SKILL` tasks immediately, and new projects inherit the same skill when they
are created.
</p>
</div>
</div>

<div className="grid gap-4 md:grid-cols-2">
{catalog.map((skill) => {
const isEnabled = enabledSkillSet.has(skill.skillId)
const isLoading = isPending && pendingSkillId === skill.skillId

return (
<Card key={skill.skillId} className="border-border/80 bg-card/70">
<CardHeader className="space-y-3">
<div className="flex items-start justify-between gap-3">
<div className="flex size-11 items-center justify-center rounded-xl border border-primary/20 bg-primary/10">
<MdBolt className="size-5 text-primary" />
</div>
<Badge
variant={isEnabled ? 'default' : 'outline'}
className={isEnabled ? '' : 'border-border/70 text-muted-foreground'}
>
{isEnabled ? 'Enabled' : 'Available'}
</Badge>
</div>
<div className="space-y-1">
<CardTitle className="font-display text-xl text-white">{skill.name}</CardTitle>
<CardDescription className="leading-6">{skill.description}</CardDescription>
</div>
</CardHeader>

<CardContent className="space-y-4">
<div className="space-y-2 rounded-xl border border-border/70 bg-background/60 p-3">
<div className="flex items-center gap-2 text-xs uppercase tracking-[0.18em] text-muted-foreground">
<MdCheckCircle className="size-3.5" />
Install Command
</div>
<code className="block whitespace-pre-wrap break-all text-xs leading-5 text-slate-200">
{skill.installCommand}
</code>
</div>
</CardContent>

<CardFooter className="justify-between gap-3 border-t border-border/70">
<Button variant="ghost" size="sm" asChild>
<a href={skill.sourceUrl} target="_blank" rel="noreferrer">
<MdOpenInNew className="size-4" />
Source
</a>
</Button>

<Button
onClick={() => handleEnable(skill.skillId)}
disabled={isEnabled || isLoading}
>
{isEnabled ? 'Enabled' : isLoading ? 'Enabling...' : 'Enable Skill'}
</Button>
</CardFooter>
</Card>
)
})}
</div>
</div>
)
}
14 changes: 14 additions & 0 deletions app/(dashboard)/skills/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Sidebar } from '@/components/sidebar'

export default function SkillsLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex min-h-screen bg-background">
<Sidebar />
<main className="flex-1 overflow-y-auto">{children}</main>
</div>
)
}
29 changes: 29 additions & 0 deletions app/(dashboard)/skills/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { redirect } from 'next/navigation'

import { auth } from '@/lib/auth'
import { getUserSkills } from '@/lib/data/user-skill'

import { SkillsLibrary } from './_components/skills-library'

export const metadata = {
title: 'Skills | Fulling',
description: 'Enable global skills that will be installed across your projects.',
}

export default async function SkillsPage() {
const session = await auth()

if (!session) {
redirect('/login')
}

const userSkills = await getUserSkills(session.user.id)

return (
<div className="mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-10">
<SkillsLibrary
enabledSkillIds={userSkills.map((skill) => skill.skillId)}
/>
</div>
)
}
7 changes: 6 additions & 1 deletion components/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { FaGithub } from 'react-icons/fa6'
import {
MdDashboardCustomize,
Expand All @@ -10,6 +12,7 @@ import {
} from 'react-icons/md'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
Expand Down Expand Up @@ -61,14 +64,16 @@ function LogoSection() {
}

function NavMenu() {
const pathname = usePathname()

return (
<nav className="flex flex-col gap-1 flex-1">
{menuItems.map((item, index) => {
if ('divider' in item) {
return <Separator key={`divider-${index}`} className="my-2" />
}

const isActive = item.active
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`)

return (
<Link
Expand Down
2 changes: 2 additions & 0 deletions docs/prds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,5 @@ Each PRD should usually include:
## Current PRDs

- [Import Project Control Flow](./import-project-control-flow.md)
- [Global Skill Enablement Control Flow](./global-skill-enablement-control-flow.md)
- [Uninstall Skill Control Flow](./uninstall-skill-control-flow.md)
Loading
Loading