← back to ClawCoder
src/components/onboarding/result-screen.tsx
259 lines
'use client'
import { useState, useCallback } from 'react'
import { motion } from 'framer-motion'
import Link from 'next/link'
interface GeneratedBundle {
claudeMd: string
rulesFiles: { path: string; content: string }[]
settings: object
settingsJson: string
profile: {
slug: string
displayName: string
ageGroup: string
skillLevel: string
persona: string
}
}
type TabId = 'claude-md' | 'rules' | 'hooks' | 'skills' | 'mcp' | 'learning'
interface TabDef {
id: TabId
label: string
}
const TABS: TabDef[] = [
{ id: 'claude-md', label: 'CLAUDE.md' },
{ id: 'rules', label: 'Rules' },
{ id: 'hooks', label: 'Hooks' },
{ id: 'skills', label: 'Skills' },
{ id: 'mcp', label: 'MCP' },
{ id: 'learning', label: 'Learning Path' },
]
interface ResultScreenProps {
bundle: GeneratedBundle
fullProfile: {
skillBundle: { name: string; description: string }[]
mcpServers: {
name: string
package: string
installCmd: string
requiresKey: boolean
trustLevel: string
}[]
learningPath: {
title: string
url: string
type: string
level: string
}[]
} | null
}
export function ResultScreen({ bundle, fullProfile }: ResultScreenProps) {
const [activeTab, setActiveTab] = useState<TabId>('claude-md')
const [copiedTab, setCopiedTab] = useState<string | null>(null)
const getTabContent = useCallback(
(tab: TabId): string => {
switch (tab) {
case 'claude-md':
return bundle.claudeMd
case 'rules':
return bundle.rulesFiles
.map((f) => `# ${f.path}\n\n${f.content}`)
.join('\n\n---\n\n')
case 'hooks':
return bundle.settingsJson
case 'skills':
if (!fullProfile) return 'No skills data available.'
return fullProfile.skillBundle
.map((s) => `## ${s.name}\n${s.description}`)
.join('\n\n')
case 'mcp':
if (!fullProfile) return 'No MCP server data available.'
if (fullProfile.mcpServers.length === 0) return 'No MCP servers recommended for this profile.'
return fullProfile.mcpServers
.map(
(m) =>
`## ${m.name}\nPackage: ${m.package}\nInstall: ${m.installCmd}\nRequires API Key: ${m.requiresKey ? 'Yes' : 'No'}\nTrust: ${m.trustLevel}`
)
.join('\n\n')
case 'learning':
if (!fullProfile) return 'No learning path data available.'
if (fullProfile.learningPath.length === 0) return 'No learning resources listed for this profile.'
return fullProfile.learningPath
.map(
(l) =>
`- [${l.title}](${l.url}) (${l.type}, ${l.level})`
)
.join('\n')
default:
return ''
}
},
[bundle, fullProfile]
)
const handleCopy = useCallback(
async (tab: TabId) => {
const content = getTabContent(tab)
await navigator.clipboard.writeText(content)
setCopiedTab(tab)
setTimeout(() => setCopiedTab(null), 2000)
},
[getTabContent]
)
const handleDownloadTab = useCallback(
(tab: TabId) => {
const content = getTabContent(tab)
const filename =
tab === 'claude-md'
? 'CLAUDE.md'
: tab === 'hooks'
? 'settings.json'
: `${tab}.md`
const blob = new Blob([content], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
},
[getTabContent]
)
const handleDownloadAll = useCallback(() => {
// Create a simple text bundle (no zip library dependency)
const parts: string[] = [
`=== CLAUDE.md ===\n\n${bundle.claudeMd}`,
`\n\n=== settings.json ===\n\n${bundle.settingsJson}`,
]
for (const rule of bundle.rulesFiles) {
parts.push(`\n\n=== ${rule.path} ===\n\n${rule.content}`)
}
const blob = new Blob([parts.join('')], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `clawcoder-bundle-${bundle.profile.slug}.txt`
a.click()
URL.revokeObjectURL(url)
}, [bundle])
return (
<div className="flex min-h-screen flex-col items-center px-4 py-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="w-full max-w-4xl"
>
{/* Header */}
<div className="mb-8 text-center">
<h1 className="mb-2 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
Your setup is ready!
</h1>
<p className="text-slate-500">
Copy the files below into your project to get started.
</p>
</div>
{/* Profile badge */}
<div className="mb-8 flex flex-col items-center gap-2 rounded-2xl border border-green-200 bg-green-50 p-6">
<p className="text-sm font-medium uppercase tracking-wider text-green-600">
Generated for
</p>
<h2 className="text-xl font-bold text-green-900">
{bundle.profile.displayName}
</h2>
<p className="max-w-md text-center text-sm text-green-700">
{bundle.profile.persona}
</p>
<div className="mt-2 flex gap-3 text-xs text-green-700">
<span className="rounded-full bg-green-100 px-3 py-1">
{bundle.profile.ageGroup.replace('_', ' ')}
</span>
<span className="rounded-full bg-green-100 px-3 py-1">
{bundle.profile.skillLevel}
</span>
</div>
</div>
{/* Tabs */}
<div className="mb-4 flex flex-wrap gap-2 border-b border-slate-200 pb-2">
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={`rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 ${
activeTab === tab.id
? 'bg-blue-600 text-white'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Tab content */}
<div className="relative rounded-2xl border border-slate-200 bg-slate-900">
{/* Action buttons */}
<div className="absolute right-3 top-3 z-10 flex gap-2">
<button
type="button"
onClick={() => handleCopy(activeTab)}
className="rounded-lg bg-slate-700 px-3 py-1.5 text-xs font-medium text-slate-200 transition-colors hover:bg-slate-600"
>
{copiedTab === activeTab ? 'Copied!' : 'Copy'}
</button>
<button
type="button"
onClick={() => handleDownloadTab(activeTab)}
className="rounded-lg bg-slate-700 px-3 py-1.5 text-xs font-medium text-slate-200 transition-colors hover:bg-slate-600"
>
Download
</button>
</div>
{/* Content */}
<pre className="max-h-[500px] overflow-auto p-6 pt-12 text-sm leading-relaxed text-slate-200">
<code>{getTabContent(activeTab)}</code>
</pre>
</div>
{/* Bottom actions */}
<div className="mt-8 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
<button
type="button"
onClick={handleDownloadAll}
className="min-h-[48px] rounded-xl bg-blue-600 px-8 py-3 text-base font-medium text-white transition-colors hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Download All
</button>
<Link
href={`/builder?profile=${bundle.profile.slug}`}
className="inline-flex min-h-[48px] items-center justify-center rounded-xl border border-slate-300 px-8 py-3 text-base font-medium text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Go to Builder
</Link>
<Link
href="/analyzer"
className="inline-flex min-h-[48px] items-center justify-center rounded-xl border border-slate-300 px-8 py-3 text-base font-medium text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Analyze This
</Link>
</div>
</motion.div>
</div>
)
}