2026-02-06 23:01:30 -05:00

237 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useEffect, useState } from 'react';
import {
Check,
Copy,
ExternalLink,
ChevronDown,
ChevronUp,
LayoutDashboard,
Globe,
Rocket,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import type { DeployResult } from '@mcpengine/ai-pipeline/types';
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
export interface DeploySuccessProps {
result: DeployResult;
serverName?: string;
onDashboard?: () => void;
onViewServer?: () => void;
onDeployAnother?: () => void;
className?: string;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function DeploySuccess({
result,
serverName = 'MCP Server',
onDashboard,
onViewServer,
onDeployAnother,
className,
}: DeploySuccessProps) {
const [copied, setCopied] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [confettiFired, setConfettiFired] = useState(false);
// Fire confetti on mount
useEffect(() => {
if (confettiFired) return;
setConfettiFired(true);
// Dynamic import canvas-confetti (client-side only)
import('canvas-confetti')
.then((mod) => {
const confetti = mod.default;
// Burst 1 — center
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
colors: ['#818cf8', '#34d399', '#f59e0b', '#f472b6'],
});
// Burst 2 — left
setTimeout(() => {
confetti({
particleCount: 50,
angle: 60,
spread: 55,
origin: { x: 0, y: 0.65 },
colors: ['#818cf8', '#34d399'],
});
}, 200);
// Burst 3 — right
setTimeout(() => {
confetti({
particleCount: 50,
angle: 120,
spread: 55,
origin: { x: 1, y: 0.65 },
colors: ['#f59e0b', '#f472b6'],
});
}, 400);
})
.catch(() => {
// canvas-confetti not installed — no big deal
console.log('canvas-confetti not available');
});
}, [confettiFired]);
const copyUrl = async () => {
if (result.url) {
await navigator.clipboard.writeText(result.url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const slug = result.url
?.replace(/^https?:\/\//, '')
.replace(/\.mcpengine\.run.*/, '') ?? 'my-server';
const claudeConfig = JSON.stringify(
{
mcpServers: {
[slug]: {
url: result.endpoint ?? result.url,
},
},
},
null,
2,
);
return (
<div className={cn('flex flex-col items-center text-center', className)}>
{/* Checkmark with glow */}
<div className="relative mb-6">
<div className="absolute inset-0 animate-ping rounded-full bg-emerald-500/20" />
<div className="absolute -inset-4 rounded-full bg-emerald-500/10 blur-xl" />
<div className="relative flex h-20 w-20 items-center justify-center rounded-full border-2 border-emerald-500 bg-emerald-500/20 shadow-2xl shadow-emerald-500/30">
<Check className="h-10 w-10 text-emerald-400" strokeWidth={3} />
</div>
</div>
{/* Heading */}
<h2 className="mb-2 text-2xl font-bold text-white">
Deployment Successful! 🚀
</h2>
<p className="mb-6 text-gray-400">
<span className="font-semibold text-white">{serverName}</span> is now
live and ready to connect
</p>
{/* URL display with copy */}
{result.url && (
<div className="mb-8 w-full max-w-lg">
<div className="group relative flex items-center gap-2 rounded-xl border border-emerald-500/30 bg-emerald-500/5 px-4 py-3 transition-all hover:border-emerald-500/50 hover:bg-emerald-500/10">
<Globe className="h-4 w-4 shrink-0 text-emerald-400" />
<span className="truncate font-mono text-sm text-emerald-300 animate-pulse">
{result.url}
</span>
<button
onClick={copyUrl}
className={cn(
'ml-auto flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-all',
copied
? 'bg-emerald-500 text-white'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700',
)}
>
{copied ? (
<>
<Check className="h-3.5 w-3.5" />
Copied!
</>
) : (
<>
<Copy className="h-3.5 w-3.5" />
Copy
</>
)}
</button>
</div>
</div>
)}
{/* Claude Desktop config */}
<div className="mb-8 w-full max-w-lg">
<button
onClick={() => setShowConfig(!showConfig)}
className="flex w-full items-center justify-between rounded-xl border border-gray-700 bg-gray-800/50 px-4 py-3 text-sm font-medium text-gray-300 transition-all hover:border-gray-600 hover:bg-gray-800"
>
<span className="flex items-center gap-2">
<span className="text-lg">🖥</span>
Add to Claude Desktop
</span>
{showConfig ? (
<ChevronUp className="h-4 w-4 text-gray-500" />
) : (
<ChevronDown className="h-4 w-4 text-gray-500" />
)}
</button>
{showConfig && (
<div className="mt-2 overflow-hidden rounded-xl border border-gray-700 bg-gray-900">
<div className="flex items-center justify-between border-b border-gray-800 px-4 py-2">
<span className="text-xs text-gray-500">
claude_desktop_config.json
</span>
<button
onClick={async () => {
await navigator.clipboard.writeText(claudeConfig);
}}
className="flex items-center gap-1 text-xs text-gray-400 hover:text-white"
>
<Copy className="h-3 w-3" />
Copy
</button>
</div>
<pre className="overflow-x-auto p-4 text-left font-mono text-xs leading-relaxed text-indigo-300">
{claudeConfig}
</pre>
</div>
)}
</div>
{/* Action buttons */}
<div className="flex flex-wrap items-center justify-center gap-3">
<button
onClick={onDashboard}
className="flex items-center gap-2 rounded-xl border border-gray-700 bg-gray-800 px-5 py-2.5 text-sm font-medium text-gray-300 transition-all hover:border-gray-600 hover:bg-gray-700"
>
<LayoutDashboard className="h-4 w-4" />
Open Dashboard
</button>
{result.url && (
<button
onClick={onViewServer}
className="flex items-center gap-2 rounded-xl border border-indigo-500/30 bg-indigo-500/10 px-5 py-2.5 text-sm font-medium text-indigo-300 transition-all hover:border-indigo-500/50 hover:bg-indigo-500/20"
>
<ExternalLink className="h-4 w-4" />
View Server
</button>
)}
<button
onClick={onDeployAnother}
className="flex items-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 px-5 py-2.5 text-sm font-medium text-white shadow-lg shadow-indigo-500/25 transition-all hover:shadow-indigo-500/40"
>
<Rocket className="h-4 w-4" />
Deploy Another
</button>
</div>
</div>
);
}