All files / src/components/blogs UploadImageTab.tsx

96.66% Statements 29/30
89.47% Branches 17/19
100% Functions 5/5
100% Lines 28/28

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                                                  24x 24x 24x 24x         24x   24x 6x 6x   5x 5x 5x 1x         1x 1x 1x 1x     4x       2x 2x       2x 2x           5x       24x 1x     24x   24x                         2x         1x                                                        
import { useState, useRef, type ChangeEvent } from "react";
import { notify } from "../../lib/notify";
import { Upload } from "lucide-react";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { ApiError, getImageUrl } from "../../lib/api";
import { proApi } from "../../lib/api";
import { uploadImage } from "../../lib/upload";
import { useAdminUploadBlogImage } from "../../hooks/mutations/useAdminBlogImageMutations";
 
interface UploadImageTabProps {
	proId?: string;
	blogId: string;
	mode?: "pro" | "admin";
	onInsertImage: (image: { url: string; alt: string }) => void;
	onClose: () => void;
}
 
export function UploadImageTab({
	proId,
	blogId,
	mode = "pro",
	onInsertImage,
	onClose,
}: UploadImageTabProps) {
	const [altText, setAltText] = useState("");
	const [caption, setCaption] = useState("");
	const [isUploading, setIsUploading] = useState(false);
	const fileInputRef = useRef<HTMLInputElement>(null);
 
	// Always call both hooks; gate via enabled (admin hook has no proId-based disable,
	// but pro upload goes through uploadImage() imperatively, so only adminUpload is
	// a hook that we activate conditionally via mode check at call time).
	const adminUploadMutation = useAdminUploadBlogImage(blogId);
 
	const handleFileSelect = async (e: ChangeEvent<HTMLInputElement>) => {
		const file = e.target.files?.[0];
		if (!file) return;
 
		setIsUploading(true);
		try {
			if (mode === "admin") {
				const added = await adminUploadMutation.mutateAsync({
					file,
					altText: altText || undefined,
					caption: caption || undefined,
				});
				Iif (!added) return;
				const url = proApi.getBlogImageUrl(added);
				onInsertImage({ url, alt: added.altText ?? altText });
				onClose();
			} else {
				// Pro path: goes through lib/upload.ts (presigned/worker)
				const result = await uploadImage(proId as string, file, {
					type: "blog-image",
					id: blogId,
				});
				onInsertImage({ url: getImageUrl(result.storageKey), alt: altText });
				onClose();
			}
		} catch (err) {
			// Surface the real server error so bug reports get an actionable cause.
			console.error("[UploadImageTab] upload failed:", err);
			notify.error(
				err instanceof ApiError
					? `Failed to upload image: ${err.message}`
					: "Failed to upload image. Please try again.",
			);
		} finally {
			setIsUploading(false);
		}
	};
 
	const triggerFileUpload = () => {
		fileInputRef.current?.click();
	};
 
	const uploading = isUploading || adminUploadMutation.isPending;
 
	return (
		<div className="space-y-4">
			<div className="p-6 border-2 border-dashed border-default rounded-lg">
				<input
					type="file"
					ref={fileInputRef}
					onChange={handleFileSelect}
					accept="image/jpeg,image/png,image/webp,image/gif,image/avif"
					className="hidden"
				/>
				<div className="space-y-3">
					<Input
						value={altText}
						onChange={(e) => setAltText(e.target.value)}
						placeholder="Alt text (optional, recommended for SEO)"
					/>
					<Input
						value={caption}
						onChange={(e) => setCaption(e.target.value)}
						placeholder="Caption (optional)"
					/>
					<Button
						onClick={triggerFileUpload}
						disabled={uploading}
						className="w-full"
					>
						{uploading ? (
							<>
								<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
								Uploading...
							</>
						) : (
							<>
								<Upload className="h-4 w-4 mr-2" />
								Choose Image
							</>
						)}
					</Button>
				</div>
				<p className="text-xs text-foreground-muted mt-3 text-center">
					Supported: JPEG, PNG, WebP, GIF, AVIF (max 10MB)
				</p>
			</div>
		</div>
	);
}