canvas-backend/src/components/EachComponent/Customization/AddImageIntoShape.jsx
2025-02-11 17:55:34 +06:00

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;