All files / src/components UpdateBanner.tsx

100% Statements 57/57
95.65% Branches 22/23
100% Functions 1/1
100% Lines 57/57

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 801x 1x               1x 18x 18x   18x 18x 2x 2x 2x 16x 16x 18x   18x   16x 16x 16x 18x 18x   18x 18x 18x 18x 18x 5x 5x 5x 5x   11x 11x 11x   18x 4x 4x 4x 4x 4x 4x   18x   18x   18x 5x   11x 11x 11x 11x   11x   11x 11x 11x 11x 11x   11x 11x 11x   18x   18x  
import { useEffect, useState } from "react";
import { Download, X, RefreshCw } from "lucide-react";
import type { UpdateState } from "@/hooks/useUpdateChecker";
 
interface UpdateBannerProps {
	state: UpdateState;
	hidden?: boolean;
}
 
export const UpdateBanner = ({ state, hidden }: UpdateBannerProps) => {
	const { update, installing, progress, error, packageInstall, dismiss, install } = state;
	const [visible, setVisible] = useState(false);
 
	useEffect(() => {
		if (!update || hidden) {
			setVisible(false);
			return;
		}
		const id = requestAnimationFrame(() => setVisible(true));
		return () => cancelAnimationFrame(id);
	}, [update, hidden]);
 
	if (!update || hidden) return null;
 
	return (
		<div
			className={`fixed bottom-4 right-4 z-50 flex items-center gap-3 bg-card border border-border rounded-xl px-4 py-3 shadow-2xl max-w-sm transition-all duration-300 ease-out ${
				visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
			}`}
		>
			<div className="flex-1 min-w-0">
				<p className="text-sm font-semibold leading-tight">
					Update available — v{update.version}
				</p>
				{installing ? (
					<p className="text-xs text-muted-foreground mt-0.5">
						{progress !== null ? `Downloading… ${progress}%` : "Installing…"}
						{packageInstall && progress === 100 && " (password required)"}
					</p>
				) : (
					<p className="text-xs text-muted-foreground mt-0.5 truncate">
						{update.body ?? "A new version is ready to install."}
					</p>
				)}
				{installing && progress !== null && (
					<div className="mt-1.5 h-1 w-full rounded-full bg-secondary overflow-hidden">
						<div
							className="h-full bg-primary transition-all duration-200"
							style={{ width: `${progress}%` }}
						/>
					</div>
				)}
			</div>
 
			{error && <p className="text-xs text-destructive mt-1">{error}</p>}
 
			{installing ? (
				<RefreshCw className="h-4 w-4 text-muted-foreground animate-spin shrink-0" />
			) : (
				<>
					<button
						onClick={install}
						className="flex items-center gap-1.5 text-xs font-semibold bg-primary text-primary-foreground px-3 py-1.5 rounded-lg hover:bg-primary/90 transition-colors shrink-0"
					>
						<Download className="h-3 w-3" />
						Install
					</button>
					<button
						onClick={dismiss}
						aria-label="Dismiss update"
						className="text-muted-foreground hover:text-foreground transition-colors shrink-0"
					>
						<X className="h-4 w-4" />
					</button>
				</>
			)}
		</div>
	);
};