Deployment Status Card

A Vercel-style deployment card showing build status, commit info, and animated building states. Essential for any developer tool or CI/CD dashboard.

Dashboard StatsInspired by Vercel
Live PreviewInteractive
Building
2m ago
maina4f2c8eAdd command palette pattern
you
Installing dependencies...
Ready
15m ago
main3b9d1f2Fix sidebar collapse animation
you43s
Next.jssfo1
modrim-git-main.vercel.app
Failed
1h ago
feat/data-table7c3e8a1Add inline data table component
you

Error: Cannot find module '@/components/DataTable' — Did you mean './DataTable'?

Queued
1m ago
feat/properties-panel2d5f9b3WIP: properties panel
you

Waiting for available builder...

DeploymentStatusCard.tsx
// 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