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>
);
}
|