All files / src/components/blogs UploadImageTab.tsx

100% Statements 21/21
100% Branches 6/6
100% Functions 5/5
100% Lines 20/20

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                                20x 20x 20x 20x   20x 5x 5x   4x 4x 4x 2x 2x               2x 2x           4x       20x 1x     20x                         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 { uploadImage } from "../../lib/upload";
 
interface UploadImageTabProps {
	proId: string;
	blogId: string;
	onInsertImage: (image: { url: string; alt: string }) => void;
	onClose: () => void;
}
 
export function UploadImageTab({ proId, blogId, onInsertImage, onClose }: UploadImageTabProps) {
	const [altText, setAltText] = useState("");
	const [caption, setCaption] = useState("");
	const [isUploading, setIsUploading] = useState(false);
	const fileInputRef = useRef<HTMLInputElement>(null);
 
	const handleFileSelect = async (e: ChangeEvent<HTMLInputElement>) => {
		const file = e.target.files?.[0];
		if (!file) return;
 
		setIsUploading(true);
		try {
			const result = await uploadImage(proId, file, { type: "blog-image", id: blogId });
			onInsertImage({ url: getImageUrl(result.storageKey), alt: altText });
			onClose();
		} catch (err) {
			// Surface the real server error (e.g. "Blog not found", "File too
			// large", "Invalid file type") so bug reports get an actionable
			// cause instead of the generic placeholder. Only ApiError messages
			// are trusted for display — they're authored strings from the API
			// route, not arbitrary runtime error text like "TypeError: Failed
			// to fetch".
			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();
	};
 
	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={isUploading}
						className="w-full"
					>
						{isUploading ? (
							<>
								<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>
	);
}