=== NEW === - studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan) - docs/FACTORY-V2.md — Factory v2 architecture doc - docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report === UPDATED SERVERS === - fieldedge: Added jobs-tools, UI build script, main entry update - lightspeed: Updated main + server entry points - squarespace: Added collection-browser + page-manager apps - toast: Added main + server entry points === INFRA === - infra/command-center/state.json — Updated pipeline state - infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
227 lines
8.3 KiB
TypeScript
227 lines
8.3 KiB
TypeScript
'use client';
|
|
|
|
import { ArrowLeft, Wrench, LayoutGrid, GitFork, ExternalLink, BadgeCheck } from 'lucide-react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { ForkButton } from './ForkButton';
|
|
import type { MarketplaceTemplate } from '@mcpengine/ai-pipeline/types';
|
|
|
|
const CATEGORY_COLORS: Record<string, string> = {
|
|
CRM: 'bg-blue-600',
|
|
eCommerce: 'bg-emerald-600',
|
|
HR: 'bg-violet-600',
|
|
Finance: 'bg-amber-600',
|
|
Marketing: 'bg-pink-600',
|
|
Support: 'bg-teal-600',
|
|
ProjectMgmt: 'bg-orange-600',
|
|
Scheduling: 'bg-cyan-600',
|
|
Analytics: 'bg-rose-600',
|
|
DevTools: 'bg-lime-600',
|
|
Social: 'bg-fuchsia-600',
|
|
Communication: 'bg-sky-600',
|
|
Storage: 'bg-indigo-600',
|
|
'AI/ML': 'bg-purple-600',
|
|
};
|
|
|
|
interface ToolInfo {
|
|
name: string;
|
|
description?: string | null;
|
|
groupName?: string | null;
|
|
inputSchema?: { properties?: Record<string, unknown>; required?: string[] } | null;
|
|
}
|
|
|
|
interface TemplateDetailData extends MarketplaceTemplate {
|
|
tools?: ToolInfo[];
|
|
readme?: string;
|
|
relatedTemplates?: MarketplaceTemplate[];
|
|
}
|
|
|
|
interface TemplateDetailProps {
|
|
template: TemplateDetailData;
|
|
}
|
|
|
|
function getMethodFromToolName(name: string): string {
|
|
const lower = name.toLowerCase();
|
|
if (lower.startsWith('create') || lower.startsWith('add') || lower.startsWith('post')) return 'POST';
|
|
if (lower.startsWith('update') || lower.startsWith('edit') || lower.startsWith('patch')) return 'PATCH';
|
|
if (lower.startsWith('delete') || lower.startsWith('remove')) return 'DELETE';
|
|
if (lower.startsWith('list') || lower.startsWith('get') || lower.startsWith('search') || lower.startsWith('fetch')) return 'GET';
|
|
return 'GET';
|
|
}
|
|
|
|
const METHOD_COLORS: Record<string, string> = {
|
|
GET: 'bg-emerald-500/20 text-emerald-400',
|
|
POST: 'bg-blue-500/20 text-blue-400',
|
|
PUT: 'bg-amber-500/20 text-amber-400',
|
|
PATCH: 'bg-orange-500/20 text-orange-400',
|
|
DELETE: 'bg-red-500/20 text-red-400',
|
|
};
|
|
|
|
export function TemplateDetail({ template }: TemplateDetailProps) {
|
|
const router = useRouter();
|
|
const catColor = CATEGORY_COLORS[template.category] || 'bg-gray-600';
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto">
|
|
{/* Back button */}
|
|
<button
|
|
onClick={() => router.push('/marketplace')}
|
|
className="flex items-center gap-2 text-sm text-gray-400 hover:text-gray-200
|
|
transition-colors mb-6"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Back to Marketplace
|
|
</button>
|
|
|
|
{/* Header */}
|
|
<div className="flex items-start gap-5 mb-8">
|
|
<div
|
|
className={`
|
|
flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center
|
|
text-white font-bold text-2xl ${catColor}
|
|
`}
|
|
>
|
|
{template.name.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<h1 className="text-2xl font-bold text-white truncate">
|
|
{template.name}
|
|
</h1>
|
|
{template.isOfficial && (
|
|
<span className="flex items-center gap-1 text-xs font-medium text-indigo-400
|
|
bg-indigo-500/10 px-2.5 py-0.5 rounded-full">
|
|
<BadgeCheck className="h-3.5 w-3.5" />
|
|
Official
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className={`text-xs px-2.5 py-0.5 rounded-full text-white ${catColor}`}>
|
|
{template.category}
|
|
</span>
|
|
<span className="text-sm text-gray-400">
|
|
by {template.author?.name || 'Anonymous'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<p className="text-gray-300 text-base leading-relaxed mb-6">
|
|
{template.description || 'No description provided.'}
|
|
</p>
|
|
|
|
{/* Stats row */}
|
|
<div className="flex items-center gap-6 mb-8 pb-6 border-b border-gray-800">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Wrench className="h-4 w-4 text-gray-500" />
|
|
<span className="text-gray-300 font-medium">{template.toolCount}</span>
|
|
<span className="text-gray-500">tools</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<LayoutGrid className="h-4 w-4 text-gray-500" />
|
|
<span className="text-gray-300 font-medium">{template.appCount}</span>
|
|
<span className="text-gray-500">apps</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<GitFork className="h-4 w-4 text-gray-500" />
|
|
<span className="text-gray-300 font-medium">{template.forkCount}</span>
|
|
<span className="text-gray-500">forks</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action buttons */}
|
|
<div className="flex items-center gap-3 mb-10">
|
|
<ForkButton templateId={template.id} />
|
|
<button
|
|
onClick={() => window.open(`/api/marketplace/${template.id}`, '_blank')}
|
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg
|
|
font-medium text-sm transition-all duration-200
|
|
bg-gray-800 text-gray-300 border border-gray-700
|
|
hover:bg-gray-700 hover:text-white"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
View Source
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tools section */}
|
|
{template.tools && template.tools.length > 0 && (
|
|
<div className="mb-10">
|
|
<h2 className="text-lg font-semibold text-white mb-4">
|
|
Tools ({template.tools.length})
|
|
</h2>
|
|
<div className="space-y-2">
|
|
{template.tools.map((tool, i) => {
|
|
const method = getMethodFromToolName(tool.name);
|
|
return (
|
|
<div
|
|
key={i}
|
|
className="flex items-center gap-3 p-3 rounded-lg bg-gray-800/50 border border-gray-800"
|
|
>
|
|
<span
|
|
className={`
|
|
flex-shrink-0 px-2 py-0.5 rounded text-xs font-mono font-bold
|
|
${METHOD_COLORS[method] || METHOD_COLORS.GET}
|
|
`}
|
|
>
|
|
{method}
|
|
</span>
|
|
<span className="font-medium text-gray-200 text-sm">{tool.name}</span>
|
|
<span className="text-sm text-gray-500 truncate flex-1">
|
|
{tool.description || ''}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* README section */}
|
|
{template.readme && (
|
|
<div className="mb-10">
|
|
<h2 className="text-lg font-semibold text-white mb-4">README</h2>
|
|
<div
|
|
className="prose prose-invert prose-sm max-w-none p-6 rounded-xl bg-gray-800/50 border border-gray-800"
|
|
dangerouslySetInnerHTML={{ __html: template.readme }}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Related templates */}
|
|
{template.relatedTemplates && template.relatedTemplates.length > 0 && (
|
|
<div className="mb-10">
|
|
<h2 className="text-lg font-semibold text-white mb-4">Related Templates</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{template.relatedTemplates.map((related) => (
|
|
<div
|
|
key={related.id}
|
|
onClick={() => router.push(`/marketplace/${related.id}`)}
|
|
className="p-4 rounded-lg bg-gray-800/50 border border-gray-800
|
|
cursor-pointer hover:border-gray-600 transition-all"
|
|
>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<div
|
|
className={`
|
|
w-8 h-8 rounded-lg flex items-center justify-center
|
|
text-white font-bold text-xs
|
|
${CATEGORY_COLORS[related.category] || 'bg-gray-600'}
|
|
`}
|
|
>
|
|
{related.name.charAt(0).toUpperCase()}
|
|
</div>
|
|
<span className="font-medium text-gray-200 text-sm truncate">
|
|
{related.name}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-gray-500 line-clamp-2">{related.description}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|