424 lines
13 KiB
JavaScript
424 lines
13 KiB
JavaScript
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
|
import { useContext, useRef, useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { fabric } from "fabric";
|
|
import Resizer from "react-image-file-resizer";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import {
|
|
HardDriveUpload,
|
|
ImagePlus,
|
|
Upload,
|
|
Crop,
|
|
RotateCw,
|
|
FileType,
|
|
ChevronUp,
|
|
ChevronDown,
|
|
} from "lucide-react";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import CollapsibleComponent from "./CollapsibleComponent";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/collapsible";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import useProject from "@/hooks/useProject";
|
|
import { uploadImage } from "@/api/uploadApi";
|
|
|
|
const features = [
|
|
{
|
|
icon: Upload,
|
|
title: "Auto-Resize",
|
|
description: "Images larger than 1080px are automatically resized",
|
|
},
|
|
{
|
|
icon: Crop,
|
|
title: "Custom Dimensions",
|
|
description: "Set your preferred resize dimensions",
|
|
},
|
|
{
|
|
icon: RotateCw,
|
|
title: "Quality & Rotation",
|
|
description: "Adjust image quality and rotation as needed",
|
|
},
|
|
{
|
|
icon: FileType,
|
|
title: "Format Conversion",
|
|
description: "Convert between various image formats",
|
|
},
|
|
];
|
|
|
|
const AddImageIntoShape = () => {
|
|
const { canvas } = useContext(CanvasContext);
|
|
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
|
const [errorMessage, setErrorMessage] = useState("");
|
|
const [width, setWidth] = useState(1080);
|
|
const [height, setHeight] = useState(1080);
|
|
const [quality, setQuality] = useState(100);
|
|
const [rotation, setRotation] = useState("0");
|
|
const [format, setFormat] = useState("JPEG");
|
|
const fileInputRef = useRef(null);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const { toast } = useToast();
|
|
|
|
const { id, projectData, projectUpdate } = useProject();
|
|
|
|
// Upload image handler
|
|
const { mutate: uploadMutate } = useMutation({
|
|
mutationFn: async ({ file, id }) => await uploadImage({ file, id }),
|
|
onSuccess: (data) => {
|
|
if (data?.status === 200) {
|
|
toast({ title: data?.status, description: data?.message });
|
|
handleImageInsert(data?.data[0]?.url);
|
|
} else {
|
|
toast({ variant: "destructive", title: data?.status, description: data?.message });
|
|
}
|
|
},
|
|
});
|
|
|
|
|
|
const handleResize = (file, callback) => {
|
|
Resizer.imageFileResizer(
|
|
file,
|
|
width,
|
|
height,
|
|
format,
|
|
quality,
|
|
parseInt(rotation),
|
|
(resizedFile) => {
|
|
callback(resizedFile); // Pass the resized file to the callback
|
|
},
|
|
"file" // Output type
|
|
);
|
|
};
|
|
|
|
const fileHandler = (e) => {
|
|
if (!activeObject) {
|
|
toast({ variant: "destructive", title: "No active object selected!" });
|
|
return;
|
|
}
|
|
const file = e.target.files[0];
|
|
if (!file && activeObject) {
|
|
setErrorMessage("No file selected.");
|
|
return;
|
|
}
|
|
|
|
// Check if the file is an SVG (skip compression)
|
|
if (file.type === "image/svg+xml") {
|
|
toast({ variant: "destructive", title: "SVG files are not supported!" });
|
|
clearFileInput();
|
|
return;
|
|
}
|
|
|
|
// Handle raster images (JPEG, PNG, etc.)
|
|
const imgElement = new Image();
|
|
const blobUrl = URL.createObjectURL(file);
|
|
imgElement.src = blobUrl;
|
|
|
|
imgElement.onload = () => {
|
|
if (imgElement.width > 1080) {
|
|
handleResize(file, (compressedFile) => {
|
|
uploadMutate({ file: compressedFile, id }); // Fixed key name
|
|
clearFileInput();
|
|
});
|
|
} else {
|
|
uploadMutate({ file, id }); // Direct upload if width is small
|
|
clearFileInput();
|
|
}
|
|
URL.revokeObjectURL(blobUrl); // Clean up
|
|
};
|
|
|
|
imgElement.onerror = () => {
|
|
console.error("Failed to load image.");
|
|
setErrorMessage("Failed to load image.");
|
|
URL.revokeObjectURL(blobUrl);
|
|
clearFileInput();
|
|
};
|
|
};
|
|
|
|
const clearFileInput = () => {
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = "";
|
|
}
|
|
};
|
|
|
|
// const handleImageInsert = (img) => {
|
|
// if (!activeObject) {
|
|
// setErrorMessage("No active object selected!");
|
|
// return;
|
|
// }
|
|
// // Ensure absolute positioning for the clipPath
|
|
// activeObject.set({
|
|
// isClipPath: true, // Custom property
|
|
// absolutePositioned: true,
|
|
// });
|
|
|
|
// // Calculate scale factors based on clip object size
|
|
// let scaleX = activeObject.width / img.width;
|
|
// let scaleY = activeObject.height / img.height;
|
|
// if (activeObject?.width < 100) {
|
|
// scaleX = 0.2;
|
|
// }
|
|
|
|
// if (activeObject.height < 100) {
|
|
// scaleY = 0.2;
|
|
// }
|
|
|
|
// // Create a fabric image object with scaling and clipPath
|
|
// const fabricImage = new fabric.Image.fromURL(img, {
|
|
// scaleX: scaleX,
|
|
// scaleY: scaleY,
|
|
// left: activeObject.left,
|
|
// top: activeObject.top,
|
|
// clipPath: activeObject, // Apply clipPath to the image
|
|
// originX: activeObject.originX, // Match origin point
|
|
// originY: activeObject.originY, // Match origin point
|
|
// }, { crossOrigin: "anonymous" });
|
|
|
|
// // Adjust position based on the clipPath's transformations
|
|
// fabricImage.set({
|
|
// left: activeObject.left,
|
|
// top: activeObject.top,
|
|
// angle: activeObject.angle, // Match rotation if any
|
|
// });
|
|
|
|
// canvas.add(fabricImage);
|
|
// canvas.setActiveObject(fabricImage);
|
|
// setActiveObject(fabricImage);
|
|
// canvas.renderAll();
|
|
// // Update the active object state
|
|
// projectUpdate({ id, updateData: { ...projectData?.data, object: canvas.toJSON(['id', 'selectable']), preview_url: "" } });
|
|
// };
|
|
|
|
|
|
const handleImageInsert = (imgUrl) => {
|
|
|
|
console.log(imgUrl);
|
|
|
|
// Ensure absolute positioning for the clipPath
|
|
activeObject.set({
|
|
isClipPath: true, // Custom property
|
|
absolutePositioned: true,
|
|
});
|
|
|
|
// Load the image asynchronously
|
|
fabric.Image.fromURL(
|
|
imgUrl,
|
|
(img) => {
|
|
// Ensure the image is fully loaded before applying transformations
|
|
let scaleX = activeObject.width / img.width;
|
|
let scaleY = activeObject.height / img.height;
|
|
|
|
// Prevent the image from being too small
|
|
if (activeObject.width < 100) scaleX = 0.2;
|
|
if (activeObject.height < 100) scaleY = 0.2;
|
|
|
|
// Set image properties
|
|
img.set({
|
|
scaleX: scaleX,
|
|
scaleY: scaleY,
|
|
left: activeObject.left,
|
|
top: activeObject.top,
|
|
clipPath: activeObject, // Apply clipPath to the image
|
|
originX: activeObject.originX, // Match origin point
|
|
originY: activeObject.originY, // Match origin point
|
|
crossOrigin: "anonymous", // Ensure CORS handling
|
|
});
|
|
|
|
// Add image to canvas
|
|
canvas.add(img);
|
|
canvas.setActiveObject(img);
|
|
setActiveObject(img);
|
|
canvas.renderAll();
|
|
|
|
// Update project data
|
|
projectUpdate({
|
|
id,
|
|
updateData: {
|
|
...projectData?.data,
|
|
object: canvas.toJSON(["id", "selectable"]),
|
|
preview_url: "",
|
|
},
|
|
});
|
|
},
|
|
{ crossOrigin: "anonymous" }
|
|
);
|
|
};
|
|
|
|
const content = () => {
|
|
return (
|
|
<div>
|
|
<Card className="my-2">
|
|
<CardContent className="p-0">
|
|
<Alert>
|
|
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">
|
|
Image Insertion <ImagePlus className="h-5 w-5" />
|
|
</AlertTitle>
|
|
<AlertDescription className="mt-1">
|
|
<p className="mb-1">
|
|
Insert and customize images within shapes. Adjust image
|
|
position and clipping after insertion.
|
|
</p>
|
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-between mt-2"
|
|
>
|
|
<span>Key Features</span>
|
|
{isOpen ? (
|
|
<ChevronUp className="h-4 w-4" />
|
|
) : (
|
|
<ChevronDown className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="grid grid-cols-1 gap-2 mt-2">
|
|
{features.map((feature, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-start p-4 bg-white rounded-lg shadow-sm"
|
|
>
|
|
<feature.icon className="h-6 w-6 mr-3 flex-shrink-0 text-primary" />
|
|
<div>
|
|
<h3 className="font-semibold mb-1">
|
|
{feature.title}
|
|
</h3>
|
|
<p className="text-sm text-gray-600">
|
|
{feature.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
</AlertDescription>
|
|
</Alert>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{errorMessage && (
|
|
<p className="text-red-600 text-sm mt-2">{errorMessage}</p>
|
|
)}
|
|
<div className="flex flex-col w-[100%]">
|
|
<div className="space-y-1">
|
|
{/* Width Slider */}
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Width: {width}px</Label>
|
|
<Slider
|
|
min={300}
|
|
value={[width]}
|
|
max={2000}
|
|
step={10}
|
|
onValueChange={(value) => setWidth(value[0])}
|
|
/>
|
|
</div>
|
|
|
|
{/* Height Slider */}
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Height: {height}px</Label>
|
|
<Slider
|
|
min={300}
|
|
value={[height]}
|
|
max={2000}
|
|
step={10}
|
|
onValueChange={(value) => setHeight(value[0])}
|
|
/>
|
|
</div>
|
|
|
|
{/* Quality Slider */}
|
|
{format === "JPEG" && (
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Quality: {quality}%</Label>
|
|
<Slider
|
|
value={[quality]}
|
|
max={100}
|
|
step={1}
|
|
onValueChange={(value) => setQuality(value[0])}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-1 items-center">
|
|
{/* Rotation */}
|
|
<div>
|
|
<Label className="text-xs">Rotation: {rotation}°</Label>
|
|
|
|
<Select
|
|
value={rotation}
|
|
onValueChange={(value) => setRotation(value)}
|
|
>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Select rotation in degree" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="0">0</SelectItem>
|
|
<SelectItem value="45">45</SelectItem>
|
|
<SelectItem value="90">90</SelectItem>
|
|
<SelectItem value="180">180</SelectItem>
|
|
<SelectItem value="270">270</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Format Dropdown */}
|
|
<div>
|
|
<Label className="text-xs">Format</Label>
|
|
<Select
|
|
value={format}
|
|
onValueChange={(value) => setFormat(value)}
|
|
>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Select format" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="JPEG">JPEG</SelectItem>
|
|
<SelectItem value="PNG">PNG</SelectItem>
|
|
<SelectItem value="WEBP">WEBP</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button className="w-fit h-fit flex flex-wrap gap-1 relative py-4 my-4 mx-auto">
|
|
<HardDriveUpload className="cursor-pointer" />
|
|
Upload Image
|
|
<Input
|
|
className="cursor-pointer bg-white text-black absolute top-0 opacity-0"
|
|
type="file"
|
|
accept="image/*"
|
|
ref={fileInputRef}
|
|
onChange={fileHandler}
|
|
/>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Card className="flex flex-col shadow-none border-0">
|
|
<CollapsibleComponent text={"Insert Image"}>
|
|
{content()}
|
|
</CollapsibleComponent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default AddImageIntoShape;
|