All files / src/components/blogs MyGalleryTab.tsx

96.42% Statements 27/28
85.71% Branches 18/21
100% Functions 11/11
100% Lines 25/25

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 131 132 133 134 135 136 137 138 139 140 141 142 143                                                17x 17x 17x   17x 17x   17x 17x   17x 2x 2x     17x 2x 2x 1x 1x 1x 2x   1x       17x         2x   2x                                                   8x           2x                                                         1x               1x                              
import { useState } from "react";
import { notify } from "../../lib/notify";
import { Image as ImageIcon, Search } from "lucide-react";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { getImageUrl } from "../../lib/api/base";
import { proApi } from "../../lib/api";
import { useProBlogMediaSearch } from "../../hooks/queries/useBlogImageQueries";
import { useAddMediaImage } from "../../hooks/mutations/useBlogImageMutations";
import type { BlogMediaItem } from "../../lib/api/blog-images";
 
interface MyGalleryTabProps {
	proId: string;
	blogId: string;
	onInsertImage: (image: { url: string; alt: string }) => void;
	onClose: () => void;
}
 
export function MyGalleryTab({
	proId,
	blogId,
	onInsertImage,
	onClose,
}: MyGalleryTabProps) {
	const [search, setSearch] = useState("");
	const [debouncedSearch, setDebouncedSearch] = useState("");
	const [page, setPage] = useState(1);
 
	const { data, isLoading } = useProBlogMediaSearch(proId, blogId, debouncedSearch, page);
	const addMediaMutation = useAddMediaImage(proId, blogId);
 
	const items = data?.data ?? [];
	const meta = data?.meta;
 
	const handleSearch = () => {
		setDebouncedSearch(search);
		setPage(1);
	};
 
	const handlePickItem = async (item: BlogMediaItem) => {
		try {
			const added = await addMediaMutation.mutateAsync({ mediaId: item.id });
			Iif (!added) return;
			const url = proApi.getBlogImageUrl(added);
			onInsertImage({ url, alt: added.altText ?? "" });
			onClose();
		} catch {
			notify.error("Failed to insert image. Please try again.");
		}
	};
 
	return (
		<div className="space-y-4">
			<div className="flex gap-2">
				<Input
					value={search}
					onChange={(e) => setSearch(e.target.value)}
					placeholder="Search your media library..."
					onKeyDown={(e) => e.key === "Enter" && handleSearch()}
				/>
				<Button onClick={handleSearch} disabled={isLoading}>
					<Search className="h-4 w-4 mr-2" />
					Search
				</Button>
			</div>
 
			{isLoading ? (
				<div className="text-center py-12">
					<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500 mx-auto" />
					<p className="mt-2 text-sm text-foreground-muted">Loading media...</p>
				</div>
			) : items.length === 0 ? (
				<div className="text-center py-12 text-foreground-muted">
					<ImageIcon className="h-12 w-12 mx-auto mb-3 text-foreground-subtle" />
					<p>{debouncedSearch ? `No results for "${debouncedSearch}"` : "No media in your library yet."}</p>
					<p className="text-sm mt-1">Upload project photos to see them here.</p>
				</div>
			) : (
				<>
					<div
						data-testid="media-grid"
						className="grid grid-cols-3 gap-4"
					>
						{items.map((item) => (
							<button
								type="button"
								key={item.id}
								data-testid="media-item"
								disabled={addMediaMutation.isPending}
								className="group relative aspect-video bg-background-subtle rounded-lg overflow-hidden border border-default hover:border-primary-500 cursor-pointer transition-all text-left disabled:opacity-50"
								onClick={() => handlePickItem(item)}
							>
								<img
									src={getImageUrl(item.storageKey)}
									alt={item.altText ?? ""}
									className="w-full h-full object-cover"
									loading="lazy"
								/>
								<div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-center justify-center">
									<span className="opacity-0 group-hover:opacity-100 transition-opacity bg-white text-black text-xs font-medium px-2 py-1 rounded">
										Insert
									</span>
								</div>
								{item.altText && (
									<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2">
										<p className="text-foreground-inverse text-xs truncate">{item.altText}</p>
									</div>
								)}
							</button>
						))}
					</div>
 
					{meta && meta.totalPages > 1 && (
						<div className="flex items-center justify-between mt-4">
							<p className="text-sm text-foreground-muted">
								Page {meta.page} of {meta.totalPages}
							</p>
							<div className="flex gap-2">
								<Button
									onClick={() => setPage((p) => Math.max(1, p - 1))}
									disabled={!meta.hasPrev}
									variant="outline"
									size="sm"
								>
									Previous
								</Button>
								<Button
									onClick={() => setPage((p) => p + 1)}
									disabled={!meta.hasNext}
									variant="outline"
									size="sm"
								>
									Next
								</Button>
							</div>
						</div>
					)}
				</>
			)}
		</div>
	);
}