All files / src/components/website BuildStatusMonitor.tsx

100% Statements 6/6
95.12% Branches 39/41
100% Functions 1/1
100% Lines 6/6

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130                                                  15x 15x 15x                   15x               15x               15x                                                                                                                                                        
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { BUILD_STATUS_DISPLAY } from "@/lib/website-config";
import type { WebsiteBuildJob } from "@/lib/api";
 
interface BuildStatusMonitorProps {
	websiteStatus: string | undefined;
	lastPublishedAt: string | Date | null | undefined;
	hasUnpublishedChanges: boolean | undefined;
	latestBuild: WebsiteBuildJob | null;
	isBuilding: boolean;
	cancelling: boolean;
	onCancelBuild: () => void;
}
 
export function BuildStatusMonitor({
	websiteStatus,
	lastPublishedAt,
	hasUnpublishedChanges,
	latestBuild,
	isBuilding,
	cancelling,
	onCancelBuild,
}: BuildStatusMonitorProps) {
	// Determine what to show in the single status row
	const isFailed = latestBuild?.status === "failed";
	const buildStatus = latestBuild?.status ?? "";
	const statusLabel = isBuilding
		? BUILD_STATUS_DISPLAY[buildStatus]?.label || buildStatus
		: isFailed
			? "Build Failed"
			: websiteStatus === "published"
				? "Published"
				: websiteStatus === "preview"
					? "Preview Only"
					: "Not Published";
 
	const dotColor = isBuilding
		? BUILD_STATUS_DISPLAY[buildStatus]?.textColor || "text-foreground-muted"
		: isFailed
			? "bg-error"
			: websiteStatus === "published"
				? "bg-success"
				: "bg-foreground-subtle";
 
	const statusDate = isBuilding && latestBuild?.dateCreated
		? `Started: ${new Date(latestBuild.dateCreated).toLocaleString(undefined, {
				month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
			})}`
		: lastPublishedAt
			? `Last published: ${new Date(lastPublishedAt).toLocaleDateString()}`
			: null;
 
	return (
		<Card>
			<CardHeader>
				<div className="flex items-center justify-between">
					<CardTitle>Publish Status</CardTitle>
					{hasUnpublishedChanges && (
						<span className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium bg-amber-100 text-amber-700 rounded-full">
							<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
							Unpublished changes
						</span>
					)}
				</div>
			</CardHeader>
			<CardContent className="space-y-3">
				{/* Unified status row */}
				<div className="flex items-center justify-between p-4 bg-background-subtle rounded-lg">
					<div className="flex items-center gap-3">
						{isBuilding ? (
							<svg
								className={`w-4 h-4 animate-spin flex-shrink-0 ${dotColor}`}
								viewBox="0 0 24 24"
								aria-hidden="true"
							>
								<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
								<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
							</svg>
						) : (
							<div className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${dotColor}`} />
						)}
						<div>
							<p className="font-semibold text-foreground-default">{statusLabel}</p>
							{statusDate && (
								<p className="text-xs text-foreground-muted mt-0.5">{statusDate}</p>
							)}
						</div>
					</div>
					{isBuilding && (
						<Button
							variant="outline"
							size="sm"
							onClick={onCancelBuild}
							disabled={cancelling}
							className="text-error border-error/30 hover:bg-error-light flex-shrink-0"
						>
							{cancelling ? "Cancelling..." : "Cancel Build"}
						</Button>
					)}
				</div>
 
				{/* Error message — build failed */}
				{latestBuild?.errorMessage && (
					<div className="flex items-start gap-2 p-3 bg-notification-error-bg border border-notification-error-border rounded-lg">
						<svg className="w-4 h-4 text-notification-error-text flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
							<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
						</svg>
						<p className="text-sm text-notification-error-text">
							{latestBuild.errorMessage}
						</p>
					</div>
				)}
 
				{/* DNS info — first publish only */}
				{latestBuild?.status === "deployed" && !lastPublishedAt && (
					<div className="flex items-start gap-2 p-3 bg-info-light border border-info rounded-lg">
						<svg className="w-4 h-4 text-info flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
							<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
						</svg>
						<p className="text-sm text-info">
							Your site has been published. DNS changes may take up to 15 minutes to reflect.
						</p>
					</div>
				)}
			</CardContent>
		</Card>
	);
}