A Vercel-style deployment card showing build status, commit info, and animated building states. Essential for any developer tool or CI/CD dashboard.
Error: Cannot find module '@/components/DataTable' — Did you mean './DataTable'?
Waiting for available builder...
// Dependencies: react ^18, lucide-react
import React, { useEffect, useState } from 'react';
import { GitBranch, Clock, ExternalLink, RotateCcw, User } from 'lucide-react';
export type Deployment = {
id: string;
status: 'building' | 'ready' | 'error' | 'cancelled' | 'queued';
url?: string;
branch: string;
commit: { hash: string; message: string; author: string };
duration?: number;
createdAt: Date;
errorMessage?: string;
meta?: { framework?: string; region?: string };
};
type Props = {
deployment: Deployment;
onVisit?: () => void;
onRedeploy?: () => void;
};
const LOG_MESSAGES = [
'Installing dependencies...',
'Building application...',
'Optimising assets...',
'Generating static pages...',
];
const STATUS_CONFIG = {
building: { dot: 'bg-yellow-400', pulse: true, label: 'Building' },
ready: { dot: 'bg-emerald-400', pulse: false, label: 'Ready' },
error: { dot: 'bg-red-400', pulse: false, label: 'Failed' },
cancelled: { dot: 'bg-zinc-500', pulse: false, label: 'Cancelled' },
queued: { dot: 'bg-zinc-400', pulse: true, label: 'Queued' },
};
function formatTime(date: Date): string {
const diff = Date.now() - date.getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return mins + 'm ago';
const hrs = Math.floor(mins / 60);
if (hrs < 24) return hrs + 'h ago';
return Math.floor(hrs / 24) + 'd ago';
}
export function DeploymentStatusCard({ deployment, onVisit, onRedeploy }: Props) {
const [logIndex, setLogIndex] = useState(0);
const [progress, setProgress] = useState(18);
const cfg = STATUS_CONFIG[deployment.status];
useEffect(() => {
if (deployment.status !== 'building') return;
const interval = setInterval(() => {
setLogIndex((i) => (i + 1) % LOG_MESSAGES.length);
setProgress((p) => (p >= 88 ? 18 : p + 18));
}, 2000);
return () => clearInterval(interval);
}, [deployment.status]);
return (
<div className="rounded-xl border border-white/[0.08] bg-[#111] overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.06]">
<div className="flex items-center gap-2">
<span className={"w-2 h-2 rounded-full " + cfg.dot + (cfg.pulse ? " animate-pulse" : "")} />
<span className="text-sm font-semibold text-white">{cfg.label}</span>
</div>
<span className="text-[11px] text-zinc-600 font-mono">{formatTime(deployment.createdAt)}</span>
</div>
<div className="flex items-center gap-2.5 px-4 py-2.5 border-b border-white/[0.04]">
<GitBranch className="w-3.5 h-3.5 text-zinc-600 shrink-0" />
<span className="text-[12px] text-zinc-400 font-medium shrink-0">{deployment.branch}</span>
<span className="text-[11px] text-zinc-600 font-mono shrink-0">{deployment.commit.hash.slice(0, 7)}</span>
<span className="text-[12px] text-zinc-500 truncate flex-1">{deployment.commit.message}</span>
</div>
<div className="flex items-center gap-3 px-4 py-2 border-b border-white/[0.04]">
<User className="w-3 h-3 text-zinc-700 shrink-0" />
<span className="text-[11px] text-zinc-600">{deployment.commit.author}</span>
{deployment.duration != null && (
<>
<Clock className="w-3 h-3 text-zinc-700 shrink-0" />
<span className="text-[11px] text-zinc-600">{deployment.duration}s</span>
</>
)}
<div className="flex items-center gap-1.5 ml-auto">
{deployment.meta?.framework && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-zinc-800 border border-zinc-700/50 text-zinc-500 font-mono">
{deployment.meta.framework}
</span>
)}
{deployment.meta?.region && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-zinc-800 border border-zinc-700/50 text-zinc-500 font-mono">
{deployment.meta.region}
</span>
)}
</div>
</div>
<div className="px-4 py-3 min-h-[52px] flex items-center">
{deployment.status === 'building' && (
<div className="flex items-center gap-2 w-full">
<span className="w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse shrink-0" />
<span className="text-[12px] text-zinc-400 font-mono">{LOG_MESSAGES[logIndex]}</span>
</div>
)}
{deployment.status === 'ready' && deployment.url && (
<div className="flex items-center gap-1.5">
<span className="text-[12px] text-indigo-400 font-mono truncate">{deployment.url}</span>
<ExternalLink className="w-3 h-3 text-zinc-600 shrink-0" />
</div>
)}
{deployment.status === 'error' && deployment.errorMessage && (
<div className="rounded-md bg-red-500/[0.08] border border-red-500/20 px-3 py-2 w-full">
<p className="text-[11px] text-red-400 font-mono leading-relaxed line-clamp-2">
{deployment.errorMessage}
</p>
</div>
)}
{deployment.status === 'cancelled' && (
<p className="text-[12px] text-zinc-600">Deployment was cancelled.</p>
)}
{deployment.status === 'queued' && (
<p className="text-[12px] text-zinc-600">Waiting for available builder...</p>
)}
</div>
{(deployment.status === 'ready' || onRedeploy) && (
<div className="flex items-center gap-2 px-4 py-3 border-t border-white/[0.06]">
{deployment.status === 'ready' && onVisit && (
<button
onClick={onVisit}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-white text-zinc-950 text-[12px] font-semibold hover:bg-zinc-100 transition-colors"
>
<ExternalLink className="w-3 h-3" />
Visit
</button>
)}
{onRedeploy && (
<button
onClick={onRedeploy}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-white/[0.1] text-zinc-400 text-[12px] hover:border-white/[0.2] hover:text-white transition-colors"
>
<RotateCcw className="w-3 h-3" />
Redeploy
</button>
)}
</div>
)}
{deployment.status === 'building' && (
<div className="h-0.5 bg-zinc-800/80">
<div
className="h-full bg-yellow-400 transition-all duration-1000 ease-in-out"
style={{ width: progress + '%' }}
/>
</div>
)}
</div>
);
}Unlock to copy
Free access to all patterns