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
Command Palette
Linear
Global Search with Results Grouping
Linear
Recent + Suggested Search
Raycast
Filter Builder (AND / OR)
Linear
Sidebar Navigation
Linear
Collapsible Nested Tree Nav
Notion
Tab Bar with Overflow Menu
Vercel
Breadcrumb with Dropdowns
Linear
Workspace Switcher
Slack
Deployment Status Card
Vercel
Empty State
Vercel
Error State with Retry
Notion
Slash Command Menu
Notion
Stats & Metrics Row
Vercel
Single Big Metric Card
Stripe
Usage / Quota Meter
Vercel
Skeleton Loading Grid
Linear
Activity Heatmap
Notion
Sparkline Trend Cards
Linear
Confirmation Dialog
Linear
Slide-Over Panel
Stripe
Bottom Sheet
Stripe
Toast / Snackbar Stack
Vercel
Notification Center
Linear
Issue / Task Card
Linear
Subtask Checklist
Linear
Kanban Board Column
Linear
Activity Feed
Linear + Slack
Comment Thread
Linear
Audit Log Timeline
Vercel
Properties Panel
Notion + Linear
Multi-Step Form Wizard
Stripe
Tag Multi-Select
Linear
Toggle Switch Group
Stripe
File Upload Dropzone
Vercel
Inline Data Table
Notion + Linear
Pagination
Vercel
Sortable / Selectable Table
Linear
Row Detail Drawer
Stripe
Editable Cell
Notion
Status Badge System
Linear
Segmented Control
Notion