added all panel and solved canvas issue when claer and also added zoom functionallity

This commit is contained in:
smfahim25 2025-02-02 16:37:06 +06:00
parent 8e6637f7fb
commit 1cf062f326
26 changed files with 1875 additions and 1074 deletions

View file

@ -16,3 +16,8 @@
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
.\[\&_svg\]\:size-4 svg {
width: 1.3rem !important;
height: 1.3rem !important;
}

View file

@ -15,6 +15,7 @@ import EditorPanel from "./components/Panel/EditorPanel";
import CanvasContext from "./components/Context/canvasContext/CanvasContext"; import CanvasContext from "./components/Context/canvasContext/CanvasContext";
import Canvas from "./components/Canvas/Canvas"; import Canvas from "./components/Canvas/Canvas";
import ActiveObjectContext from "./components/Context/activeObject/ObjectContext"; import ActiveObjectContext from "./components/Context/activeObject/ObjectContext";
import CanvasCapture from "./components/CanvasCapture";
function App() { function App() {
useEffect(() => { useEffect(() => {
@ -100,6 +101,7 @@ function App() {
{activeObject && <TopBar />} {activeObject && <TopBar />}
<ActionButtons /> <ActionButtons />
<Canvas /> <Canvas />
<CanvasCapture />
</div> </div>
</div> </div>
); );

View file

@ -1,4 +1,5 @@
import CanvasContext from "./Context/canvasContext/CanvasContext"; import CanvasContext from "./Context/canvasContext/CanvasContext";
import OpenContext from "./Context/openContext/OpenContext";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { import {
Select, Select,
@ -28,6 +29,7 @@ const aspectRatios = [
]; ];
export function ActionButtons() { export function ActionButtons() {
const { setCaptureOpen } = useContext(OpenContext);
const { setCanvasRatio, canvasRatio } = useContext(CanvasContext); const { setCanvasRatio, canvasRatio } = useContext(CanvasContext);
const handleRatioChange = (newRatio) => { const handleRatioChange = (newRatio) => {
setCanvasRatio(newRatio); setCanvasRatio(newRatio);
@ -54,7 +56,12 @@ export function ActionButtons() {
</Select> </Select>
</div> </div>
</div> </div>
<div className="mr-5"> <div
className="mr-5"
onClick={() => {
setCaptureOpen(true);
}}
>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"

View file

@ -1,9 +1,10 @@
import { useEffect, useContext } from "react"; import { useEffect, useContext, useState, useRef } from "react";
import { AspectRatio } from "@/components/ui/aspect-ratio"; import { AspectRatio } from "@/components/ui/aspect-ratio";
import OpenContext from "../Context/openContext/OpenContext"; import OpenContext from "../Context/openContext/OpenContext";
import CanvasContext from "../Context/canvasContext/CanvasContext"; import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Card, CardContent } from "../ui/card"; import { Card, CardContent } from "../ui/card";
import ActiveObjectContext from "../Context/activeObject/ObjectContext"; import ActiveObjectContext from "../Context/activeObject/ObjectContext";
import { Slider } from "@/components/ui/slider";
export default function Canvas() { export default function Canvas() {
const { const {
@ -26,14 +27,35 @@ export default function Canvas() {
} = useContext(CanvasContext); } = useContext(CanvasContext);
const { activeObject, setActiveObject } = useContext(ActiveObjectContext); const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const [zoomLevel, setZoomLevel] = useState(100);
const containerRef = useRef(null);
const handleZoom = (newZoom) => {
const zoom = Math.min(Math.max(newZoom, 0), 100);
setZoomLevel(zoom);
if (canvasRef.current && canvas) {
const scale = zoom / 100;
// Update canvas dimensions
const newWidth = canvasRef.current.offsetWidth * scale;
const newHeight = canvasRef.current.offsetHeight * scale;
canvas.setWidth(newWidth);
canvas.setHeight(newHeight);
setCanvasWidth(newWidth);
setCanvasHeight(newHeight);
canvas.renderAll();
}
};
useEffect(() => { useEffect(() => {
if (!canvas) return; // Ensure canvas is available if (!canvas) return;
// Event handler for mouse down
const handleMouseDown = (event) => { const handleMouseDown = (event) => {
const target = event.target; // Get the clicked target const target = event.target;
const activeObject = canvas.getActiveObject(); // Get the active object const activeObject = canvas.getActiveObject();
if (target) { if (target) {
if (target.type === "group") { if (target.type === "group") {
@ -46,20 +68,99 @@ export default function Canvas() {
} }
}; };
// Attach the event listener const handleWheel = (event) => {
canvas.on("mouse:down", handleMouseDown); if (event.ctrlKey || event.metaKey) {
event.preventDefault();
// Cleanup function to remove the event listener const delta = event.deltaY > 0 ? -1 : 1;
return () => { handleZoom(zoomLevel + delta);
canvas.off("mouse:down", handleMouseDown); // Remove the listener on unmount event.stopPropagation();
}
}; };
}, [canvas, setActiveObject]);
const handleKeyboard = (event) => {
if (
(event.ctrlKey || event.metaKey) &&
(event.key === "=" || event.key === "-")
) {
event.preventDefault();
const delta = event.key === "=" ? 1 : -1;
handleZoom(zoomLevel + delta);
}
};
let lastDistance = 0;
const handleTouchStart = (event) => {
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
lastDistance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
}
};
const handleTouchMove = (event) => {
if (event.touches.length === 2) {
event.preventDefault();
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const distance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
if (lastDistance > 0) {
const delta = distance - lastDistance;
const zoomDelta = delta > 0 ? 1 : -1;
handleZoom(zoomLevel + zoomDelta);
}
lastDistance = distance;
}
};
const handleTouchEnd = () => {
lastDistance = 0;
};
const handleResize = () => {
handleZoom(zoomLevel);
};
canvas.on("mouse:down", handleMouseDown);
const canvasContainer = document.getElementById("canvas-ref");
if (canvasContainer) {
canvasContainer.addEventListener("wheel", handleWheel, {
passive: false,
});
canvasContainer.addEventListener("touchstart", handleTouchStart);
canvasContainer.addEventListener("touchmove", handleTouchMove, {
passive: false,
});
canvasContainer.addEventListener("touchend", handleTouchEnd);
window.addEventListener("keydown", handleKeyboard);
window.addEventListener("resize", handleResize);
}
return () => {
canvas.off("mouse:down", handleMouseDown);
if (canvasContainer) {
canvasContainer.removeEventListener("wheel", handleWheel);
canvasContainer.removeEventListener("touchstart", handleTouchStart);
canvasContainer.removeEventListener("touchmove", handleTouchMove);
canvasContainer.removeEventListener("touchend", handleTouchEnd);
window.removeEventListener("keydown", handleKeyboard);
window.removeEventListener("resize", handleResize);
}
};
}, [canvas, setActiveObject, zoomLevel]);
useEffect(() => { useEffect(() => {
import("fabric").then((fabricModule) => { import("fabric").then((fabricModule) => {
window.fabric = fabricModule.fabric; window.fabric = fabricModule.fabric;
}); });
}, []); }, [canvasRef, fabricCanvasRef, setCanvas]);
const getRatioValue = (ratio) => { const getRatioValue = (ratio) => {
const [width, height] = ratio.split(":").map(Number); const [width, height] = ratio.split(":").map(Number);
@ -69,42 +170,33 @@ export default function Canvas() {
useEffect(() => { useEffect(() => {
const updateCanvasSize = () => { const updateCanvasSize = () => {
if (canvasRef.current && canvas) { if (canvasRef.current && canvas) {
// Update canvas dimensions const newWidth = canvasRef.current.offsetWidth;
const newWidth = canvasRef?.current?.offsetWidth; const newHeight = canvasRef.current.offsetHeight;
const newHeight = canvasRef?.current?.offsetHeight;
canvas.setWidth(newWidth); canvas.setWidth(newWidth);
canvas.setHeight(newHeight); canvas.setHeight(newHeight);
setCanvasWidth(newWidth); setCanvasWidth(newWidth);
setCanvasHeight(newHeight); setCanvasHeight(newHeight);
// Adjust the background image to fit the updated canvas size
const bgImage = canvas.backgroundImage; const bgImage = canvas.backgroundImage;
if (bgImage) { if (bgImage) {
// Calculate scaling factors for width and height
const scaleX = newWidth / bgImage.width; const scaleX = newWidth / bgImage.width;
const scaleY = newHeight / bgImage.height; const scaleY = newHeight / bgImage.height;
// Use the larger scale to cover the entire canvas
const scale = Math.max(scaleX, scaleY); const scale = Math.max(scaleX, scaleY);
// Apply scale and position the image
bgImage.scaleX = scale; bgImage.scaleX = scale;
bgImage.scaleY = scale; bgImage.scaleY = scale;
bgImage.left = 0; // Align left bgImage.left = 0;
bgImage.top = 0; // Align top bgImage.top = 0;
// Update the background image
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas)); canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
} else { } else {
// Render the canvas if no background image
canvas.renderAll(); canvas.renderAll();
} }
} }
setScreenWidth(document.getElementById("root").offsetWidth); setScreenWidth(document.getElementById("root").offsetWidth);
// Handle responsive behavior for panels
if (document.getElementById("root").offsetWidth <= 640) { if (document.getElementById("root").offsetWidth <= 640) {
setLeftPanelOpen(false); setLeftPanelOpen(false);
setRightPanelOpen(false); setRightPanelOpen(false);
@ -114,13 +206,9 @@ export default function Canvas() {
setOpenSetting(false); setOpenSetting(false);
} }
}; };
// Initial setup
updateCanvasSize(); updateCanvasSize();
// Listen for window resize
window.addEventListener("resize", updateCanvasSize); window.addEventListener("resize", updateCanvasSize);
// Cleanup listener on unmount
return () => window.removeEventListener("resize", updateCanvasSize); return () => window.removeEventListener("resize", updateCanvasSize);
}, [ }, [
setCanvasWidth, setCanvasWidth,
@ -139,44 +227,75 @@ export default function Canvas() {
useEffect(() => { useEffect(() => {
if (window.fabric) { if (window.fabric) {
if (fabricCanvasRef?.current) { if (fabricCanvasRef?.current) {
fabricCanvasRef?.current.dispose(); fabricCanvasRef.current.dispose();
} }
// Set styles directly on the canvas element
const canvasElement = document.getElementById("fabric-canvas"); const canvasElement = document.getElementById("fabric-canvas");
if (canvasElement) { if (canvasElement) {
canvasElement.classList.add("fabric-canvas-container"); // Add the CSS class canvasElement.classList.add("fabric-canvas-container");
} }
fabricCanvasRef.current = new window.fabric.Canvas("fabric-canvas", { fabricCanvasRef.current = new window.fabric.Canvas("fabric-canvas", {
width: canvasRef?.current?.offsetWidth, width: canvasRef?.current?.offsetWidth,
height: canvasRef?.current?.offsetWidth, height: canvasRef?.current?.offsetWidth,
backgroundColor: "#ffffff", backgroundColor: "#ffffff",
allowTouchScrolling: true,
selection: true,
preserveObjectStacking: true,
}); });
setCanvas(fabricCanvasRef?.current); setCanvas(fabricCanvasRef.current);
} }
}, []); }, [canvasRef, fabricCanvasRef, setCanvas]);
return ( return (
<Card <>
className={`w-full max-w-3xl p-2 my-4 overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-background rounded-none flex-1 flex flex-col ${ {/* Zoom Controls */}
activeObject ? "mt-20" : "mt-20" <div className="fixed bottom-4 right-4 flex items-center gap-4 bg-white p-3 rounded-lg shadow-lg z-50">
} mx-auto bg-white pl-5 pb-5 pt-5 border-0 shadow-none`} <span className="text-sm font-medium min-w-[45px]">{zoomLevel}%</span>
> <Slider
<CardContent className="p-0 space-y-2"> value={[zoomLevel]}
<AspectRatio onValueChange={(value) => handleZoom(value[0])}
ratio={getRatioValue(canvasRatio)} min={0}
className="overflow-y-scroll shadow-red-200 overflow-x-hidden shadow-lg rounded-lg border-2 border-primary/10 transition-all duration-300 ease-in-out hover:shadow-xl scrollbar-hide" max={100}
step={1}
className="w-32"
/>
</div>
{/* Canvas Container */}
<div className="w-full max-w-4xl mx-auto p-4">
<Card
className={`w-full p-2 rounded-none flex-1 flex flex-col
${activeObject ? "mt-20" : "mt-20"}
mx-auto pl-5 pb-5 pt-5 border-0 shadow-none bg-transparent
${zoomLevel < 100 ? "overflow-hidden" : ""}`}
> >
<div <CardContent
ref={canvasRef} className="p-0 h-full bg-transparent shadow-none"
className="w-full h-full flex items-center justify-center bg-white rounded-md shadow-lg" ref={containerRef}
id="canvas-ref"
> >
<canvas id="fabric-canvas" /> <AspectRatio
</div> ratio={getRatioValue(canvasRatio)}
</AspectRatio> className="rounded-lg border-0 border-primary/10 transition-all duration-300 ease-in-out"
</CardContent> >
</Card> <div
ref={canvasRef}
className="w-full h-full flex items-start justify-center rounded-md shadow-none touch-none"
id="canvas-ref"
style={{
touchAction: "none",
transform: `scale(${zoomLevel / 100})`,
transformOrigin: "50% 0",
transition: "transform 0.1s ease-out",
}}
>
<canvas id="fabric-canvas" />
</div>
</AspectRatio>
</CardContent>
</Card>
</div>
</>
); );
} }

View file

@ -1,290 +1,301 @@
import { useContext, useRef, useState } from 'react' import { useContext, useRef, useState } from "react";
import CanvasContext from './Context/canvasContext/CanvasContext'; import CanvasContext from "./Context/canvasContext/CanvasContext";
import { Card, CardTitle } from './ui/card'; import { Card, CardTitle } from "./ui/card";
import { Button } from './ui/button'; import { Button } from "./ui/button";
import { Trash2, UploadIcon, X } from 'lucide-react'; import { Trash2, UploadIcon, X } from "lucide-react";
import { Separator } from './ui/separator'; import { Separator } from "./ui/separator";
import ColorComponent from './ColorComponent'; import ColorComponent from "./ColorComponent";
import { Label } from './ui/label'; import { Label } from "./ui/label";
import { Input } from './ui/input'; import { Input } from "./ui/input";
import { Slider } from './ui/slider'; import { Slider } from "./ui/slider";
import { fabric } from 'fabric'; import { fabric } from "fabric";
import OpenContext from './Context/openContext/OpenContext'; import OpenContext from "./Context/openContext/OpenContext";
import { ScrollArea } from './ui/scroll-area'; import { ScrollArea } from "./ui/scroll-area";
import RndComponent from './Layouts/RndComponent'; import RndComponent from "./Layouts/RndComponent";
const CanvasSetting = () => { const CanvasSetting = () => {
const { canvas, canvasHeight, canvasWidth, screenWidth } = useContext(CanvasContext); const { canvas, canvasHeight, canvasWidth, screenWidth } =
const { setOpenSetting } = useContext(OpenContext); useContext(CanvasContext);
const bgImgRef = useRef(null); const { setOpenSetting } = useContext(OpenContext);
const bgImgRef = useRef(null);
const [canvasSettings, setCanvasSettings] = useState({ const [canvasSettings, setCanvasSettings] = useState({
width: canvas?.width, width: canvas?.width,
height: canvas?.height, height: canvas?.height,
perPixelTargetFind: true, // Enable per-pixel detection globally perPixelTargetFind: true, // Enable per-pixel detection globally
targetFindTolerance: 4, // Adjust for leniency in pixel-perfect detection targetFindTolerance: 4, // Adjust for leniency in pixel-perfect detection
}); });
const handleChange = (key, value) => { const handleChange = (key, value) => {
setCanvasSettings((prevSettings) => ({ setCanvasSettings((prevSettings) => ({
...prevSettings, ...prevSettings,
[key]: value, [key]: value,
})); }));
// Update canvas dimensions // Update canvas dimensions
if (key === 'width') { if (key === "width") {
canvas.setWidth(value); // Update canvas width canvas.setWidth(value); // Update canvas width
} else if (key === 'height') { } else if (key === "height") {
canvas.setHeight(value); // Update canvas height canvas.setHeight(value); // Update canvas height
}
// Adjust the background image to fit the updated canvas
const bgImage = canvas.backgroundImage;
if (bgImage) {
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// Calculate scaling factors for width and height
const scaleX = canvasWidth / bgImage.width;
const scaleY = canvasHeight / bgImage.height;
// Choose the larger scale to cover the entire canvas
const scale = Math.max(scaleX, scaleY);
// Apply the scale and center the image
bgImage.scaleX = scale;
bgImage.scaleY = scale;
bgImage.left = 0; // Align left
bgImage.top = 0; // Align top
// Mark the background image as needing an update
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
} else {
canvas.renderAll(); // Render if no background image
}
};
const setBackgroundImage = (e) => {
const file = e.target.files[0];
if (file) {
const blobUrl = URL.createObjectURL(file);
// Create an image element
const imgElement = new Image();
imgElement.src = blobUrl;
imgElement.onload = () => {
// Create a fabric.Image instance
const img = new fabric.Image(imgElement, {
scaleX: canvas.width / imgElement.width,
scaleY: canvas.height / imgElement.height,
});
// Set the background image on the canvas
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
// Revoke the object URL to free memory
URL.revokeObjectURL(blobUrl);
};
imgElement.onerror = () => {
console.error("Failed to load the image.");
URL.revokeObjectURL(blobUrl);
};
}
};
const setBackgroundOverlayImage = (e) => {
const file = e.target.files[0];
if (file) {
const blobUrl = URL.createObjectURL(file);
// Create an image element
const imgElement = new Image();
imgElement.src = blobUrl;
imgElement.onload = () => {
const img = new fabric.Image(imgElement, {
scaleX: canvas.width / imgElement.width,
scaleY: canvas.height / imgElement.height,
});
// Use setOverlayImage method to add the image as an overlay
canvas.setOverlayImage(img, () => {
canvas.renderAll();
});
// Revoke the object URL after image is loaded and set
URL.revokeObjectURL(blobUrl);
};
}
};
const adjustBackgroundOpacity = (value) => {
if (canvas) {
if (canvas.backgroundImage) {
// Update the opacity of the background image if it exists
canvas.backgroundImage.set("opacity", value);
} else if (canvas.overlayImage) {
// Update the opacity of the overlay image if it exists
canvas.overlayImage.set("opacity", value);
} else {
console.error("No background or overlay image found on the canvas.");
return;
}
// Re-render the canvas to apply changes
canvas.renderAll();
} else {
console.error("Canvas is not initialized.");
}
};
const removeBackgroundImage = () => {
if (canvas) {
canvas.backgroundImage = null;
canvas.renderAll();
}
if (bgImgRef.current) {
bgImgRef.current.value = "";
}
};
const removeBackgroundOverlayImage = () => {
if (canvas) {
canvas.overlayImage = null;
canvas.renderAll();
}
};
const rndValue = {
valueX: 0,
valueY: 20,
width: 250,
height: 0,
minWidth: 250,
maxWidth: 300,
minHeight: 0,
maxHeight: 400,
bound: "parent"
}
const content = () => {
return (
<Card className="xl:p-0 lg:p-0 md:p-0 p-2">
<CardTitle className="flex items-center flex-wrap justify-between gap-1 xl:hidden lg:hidden md:hidden">Canvas Setting <Button className="rnd-escape" variant="secondary" onClick={() => setOpenSetting(false)}><X /></Button> </CardTitle>
<Separator className="mt-4 block xl:hidden lg:hidden md:hidden" />
<ScrollArea className="h-[400px] xl:h-fit lg:h-fit md:h-fit">
<div className='rnd-escape'>
<ColorComponent />
<Separator className="mt-2" />
<div className='flex flex-col my-2 gap-2 rnd-escape'>
<div>
<Label>Background:</Label>
<div className='flex items-center w-fit gap-2 flex-wrap relative'>
<Button className="top-0 absolute flex items-center w-[30px]">
<UploadIcon className="cursor-pointer" />
<Input
ref={bgImgRef}
className="absolute top-0 opacity-0"
type="file"
accept="image/*"
onChange={setBackgroundImage}
/>
</Button>
<Button variant="secondary" className="ml-[35px]" onClick={removeBackgroundImage}><Trash2 /></Button>
</div>
</div>
<div>
<Label>Background Overlay:</Label>
<div className='flex items-center w-fit gap-2 flex-wrap relative'>
<Button className="top-0 absolute flex items-center w-[30px]">
<UploadIcon className="cursor-pointer" />
<Input
className="absolute top-0 opacity-0"
type="file"
accept="image/*"
onChange={setBackgroundOverlayImage}
/>
</Button>
<Button variant="secondary" className="ml-[35px]" onClick={removeBackgroundOverlayImage}><Trash2 /></Button>
</div>
</div>
{/* opacity */}
<div className='flex flex-col gap-2 rnd-escape mt-2'>
<Label>
Background Opacity:
</Label>
<Slider
defaultValue={[1.0]} // Default value, you can set it to 0.0 or another value
min={0.0}
max={1.0}
step={0.01} // Step size for fine control
onValueChange={(value) => {
const newOpacity = value[0]; // Extract slider value
adjustBackgroundOpacity(newOpacity); // Adjust Fabric.js background opacity
}}
/>
</div>
</div>
<Separator className="mt-4" />
{/* canvas size customization (width/height) */}
<div className='flex gap-2 my-2'>
<div className='flex flex-col gap-2'>
<Label>
Width:
</Label>
<Input
min={300}
type="number"
value={canvasSettings.width}
onChange={(e) => {
if (canvasWidth > parseInt(e.target.value)) {
handleChange('width', parseInt(e.target.value, 10));
}
}}
/>
</div>
<div className='flex flex-col gap-2'>
<Label>
Height:
</Label>
<Input
min={300}
type="number"
value={canvasSettings.height}
onChange={(e) => {
if (canvasHeight > parseInt(e.target.value)) {
handleChange('height', parseInt(e.target.value, 10));
}
}}
/>
</div>
</div>
</div>
</ScrollArea>
</Card>
)
} }
return screenWidth <= 768 ? ( // Adjust the background image to fit the updated canvas
<RndComponent value={rndValue}> const bgImage = canvas.backgroundImage;
{content()} if (bgImage) {
</RndComponent> const canvasWidth = canvas.width;
) : ( const canvasHeight = canvas.height;
<div>
{content()} // Calculate scaling factors for width and height
</div> const scaleX = canvasWidth / bgImage.width;
const scaleY = canvasHeight / bgImage.height;
// Choose the larger scale to cover the entire canvas
const scale = Math.max(scaleX, scaleY);
// Apply the scale and center the image
bgImage.scaleX = scale;
bgImage.scaleY = scale;
bgImage.left = 0; // Align left
bgImage.top = 0; // Align top
// Mark the background image as needing an update
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
} else {
canvas.renderAll(); // Render if no background image
}
};
const setBackgroundImage = (e) => {
const file = e.target.files[0];
if (file) {
const blobUrl = URL.createObjectURL(file);
// Create an image element
const imgElement = new Image();
imgElement.src = blobUrl;
imgElement.onload = () => {
// Create a fabric.Image instance
const img = new fabric.Image(imgElement, {
scaleX: canvas.width / imgElement.width,
scaleY: canvas.height / imgElement.height,
});
// Set the background image on the canvas
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
// Revoke the object URL to free memory
URL.revokeObjectURL(blobUrl);
};
imgElement.onerror = () => {
console.error("Failed to load the image.");
URL.revokeObjectURL(blobUrl);
};
}
};
const setBackgroundOverlayImage = (e) => {
const file = e.target.files[0];
if (file) {
const blobUrl = URL.createObjectURL(file);
// Create an image element
const imgElement = new Image();
imgElement.src = blobUrl;
imgElement.onload = () => {
const img = new fabric.Image(imgElement, {
scaleX: canvas.width / imgElement.width,
scaleY: canvas.height / imgElement.height,
});
// Use setOverlayImage method to add the image as an overlay
canvas.setOverlayImage(img, () => {
canvas.renderAll();
});
// Revoke the object URL after image is loaded and set
URL.revokeObjectURL(blobUrl);
};
}
};
const adjustBackgroundOpacity = (value) => {
if (canvas) {
if (canvas.backgroundImage) {
// Update the opacity of the background image if it exists
canvas.backgroundImage.set("opacity", value);
} else if (canvas.overlayImage) {
// Update the opacity of the overlay image if it exists
canvas.overlayImage.set("opacity", value);
} else {
console.error("No background or overlay image found on the canvas.");
return;
}
// Re-render the canvas to apply changes
canvas.renderAll();
} else {
console.error("Canvas is not initialized.");
}
};
const removeBackgroundImage = () => {
if (canvas) {
canvas.backgroundImage = null;
canvas.renderAll();
}
if (bgImgRef.current) {
bgImgRef.current.value = "";
}
};
const removeBackgroundOverlayImage = () => {
if (canvas) {
canvas.overlayImage = null;
canvas.renderAll();
}
};
const rndValue = {
valueX: 0,
valueY: 20,
width: 250,
height: 0,
minWidth: 250,
maxWidth: 300,
minHeight: 0,
maxHeight: 400,
bound: "parent",
};
const content = () => {
return (
<Card className="xl:p-0 lg:p-0 md:p-0 p-2 border-none shadow-none">
<CardTitle className="flex items-center flex-wrap justify-between gap-1 xl:hidden lg:hidden md:hidden">
Canvas Setting{" "}
<Button
className="rnd-escape"
variant="secondary"
onClick={() => setOpenSetting(false)}
>
<X />
</Button>{" "}
</CardTitle>
<Separator className="mt-4 block xl:hidden lg:hidden md:hidden" />
<ScrollArea className="h-[400px] xl:h-fit lg:h-fit md:h-fit">
<div className="rnd-escape">
<ColorComponent />
<Separator className="mt-2" />
<div className="flex flex-col my-2 gap-2 rnd-escape">
<div>
<Label>Background:</Label>
<div className="flex items-center w-fit gap-2 flex-wrap relative">
<Button className="top-0 absolute flex items-center w-[30px]">
<UploadIcon className="cursor-pointer" />
<Input
ref={bgImgRef}
className="absolute top-0 opacity-0"
type="file"
accept="image/*"
onChange={setBackgroundImage}
/>
</Button>
<Button
variant="secondary"
className="ml-[35px]"
onClick={removeBackgroundImage}
>
<Trash2 />
</Button>
</div>
</div>
<div>
<Label>Background Overlay:</Label>
<div className="flex items-center w-fit gap-2 flex-wrap relative">
<Button className="top-0 absolute flex items-center w-[30px]">
<UploadIcon className="cursor-pointer" />
<Input
className="absolute top-0 opacity-0"
type="file"
accept="image/*"
onChange={setBackgroundOverlayImage}
/>
</Button>
<Button
variant="secondary"
className="ml-[35px]"
onClick={removeBackgroundOverlayImage}
>
<Trash2 />
</Button>
</div>
</div>
{/* opacity */}
<div className="flex flex-col gap-2 rnd-escape mt-2">
<Label>Background Opacity:</Label>
<Slider
defaultValue={[1.0]} // Default value, you can set it to 0.0 or another value
min={0.0}
max={1.0}
step={0.01} // Step size for fine control
onValueChange={(value) => {
const newOpacity = value[0]; // Extract slider value
adjustBackgroundOpacity(newOpacity); // Adjust Fabric.js background opacity
}}
/>
</div>
</div>
<Separator className="mt-4" />
{/* canvas size customization (width/height) */}
<div className="flex gap-2 my-2">
<div className="flex flex-col gap-2">
<Label>Width:</Label>
<Input
min={300}
type="number"
value={canvasSettings.width}
onChange={(e) => {
if (canvasWidth > parseInt(e.target.value)) {
handleChange("width", parseInt(e.target.value, 10));
}
}}
/>
</div>
<div className="flex flex-col gap-2">
<Label>Height:</Label>
<Input
min={300}
type="number"
value={canvasSettings.height}
onChange={(e) => {
if (canvasHeight > parseInt(e.target.value)) {
handleChange("height", parseInt(e.target.value, 10));
}
}}
/>
</div>
</div>
</div>
</ScrollArea>
</Card>
); );
} };
export default CanvasSetting return screenWidth <= 768 ? (
<RndComponent value={rndValue}>{content()}</RndComponent>
) : (
<div>{content()}</div>
);
};
export default CanvasSetting;

View file

@ -1,68 +1,69 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext'; import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from '@/components/Context/canvasContext/CanvasContext'; import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { useContext } from 'react' import { useContext } from "react";
import { shapes } from './shapes'; import { shapes } from "./shapes";
import { fabric } from 'fabric'; import { fabric } from "fabric";
const CustomShape = () => { const CustomShape = () => {
const { canvas } = useContext(CanvasContext); const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext); const { setActiveObject } = useContext(ActiveObjectContext);
const addShape = (each) => { const addShape = (each) => {
// Load the SVG from the imported file // Load the SVG from the imported file
fabric.loadSVGFromURL(each, (objects, options) => { fabric.loadSVGFromURL(each, (objects, options) => {
const svgGroup = fabric.util.groupSVGElements(objects, options); const svgGroup = fabric.util.groupSVGElements(objects, options);
// Calculate canvas center // Calculate canvas center
const centerX = canvas.getWidth() / 2; const centerX = canvas.getWidth() / 2;
const centerY = canvas.getHeight() / 2; const centerY = canvas.getHeight() / 2;
// Set properties for centering the SVG // Set properties for centering the SVG
svgGroup.set({ svgGroup.set({
left: centerX, // Center horizontally left: centerX, // Center horizontally
top: centerY, // Center vertically top: centerY, // Center vertically
originX: 'center', // Set the origin to the center originX: "center", // Set the origin to the center
originY: 'center', originY: "center",
fill: "#f09b0a", fill: "#f09b0a",
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
}); strokeWidth: 0,
});
// Add SVG to the canvas // Add SVG to the canvas
canvas.add(svgGroup); canvas.add(svgGroup);
canvas.setActiveObject(svgGroup); canvas.setActiveObject(svgGroup);
// Update the active object state // Update the active object state
setActiveObject(svgGroup); setActiveObject(svgGroup);
// Render the canvas // Render the canvas
canvas.renderAll(); canvas.renderAll();
}); });
}; };
return ( return (
<div
className="rounded-md grid grid-cols-3 gap-2 p-2"
onClick={() => addShape()}
>
{shapes.map((each) => (
<div <div
className="rounded-md grid grid-cols-3 gap-2 p-2" key={each.shape}
onClick={() => addShape()} className="relative aspect-square flex items-center justify-center bg-secondary rounded-md cursor-pointer"
onClick={(e) => {
e.stopPropagation();
addShape(each.source);
}}
> >
{shapes.map((each) => ( <img
<div src={each.source}
key={each.shape} alt={`Shape ${each.shape}`}
className="relative aspect-square flex items-center justify-center bg-secondary rounded-md cursor-pointer" className="max-w-full max-h-full p-1 object-contain hover:bg-gray-200 rounded-md transition-colors duration-200"
onClick={(e) => { />
e.stopPropagation()
addShape(each.source)
}}
>
<img
src={each.source}
alt={`Shape ${each.shape}`}
className="max-w-full max-h-full p-1 object-contain hover:bg-gray-200 rounded-md transition-colors duration-200"
/>
</div>
))}
</div> </div>
) ))}
} </div>
);
};
export default CustomShape export default CustomShape;

View file

@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { Lock, Unlock } from "lucide-react"; import { Lock, Unlock } from "lucide-react";
import { Tooltip } from "react-tooltip";
const LockObject = () => { const LockObject = () => {
const { canvas } = useContext(CanvasContext); const { canvas } = useContext(CanvasContext);
@ -51,19 +52,22 @@ const LockObject = () => {
return ( return (
<div className="shadow-none border-0"> <div className="shadow-none border-0">
<Button <a data-tooltip-id="lock">
onClick={toggleLock} <Button
variant="outline" onClick={toggleLock}
size="icon" variant="outline"
disabled={!activeObject} size="icon"
title={isLocked ? "Unlock object" : "Lock object"} disabled={!activeObject}
> title={isLocked ? "Unlock object" : "Lock object"}
{isLocked ? ( >
<Unlock className="h-4 w-4" /> {isLocked ? (
) : ( <Unlock className="h-4 w-4" />
<Lock className="h-4 w-4" /> ) : (
)} <Lock className="h-4 w-4" />
</Button> )}
</Button>
</a>
<Tooltip id="lock" content="Lock object" place="bottom" />
</div> </div>
); );
}; };

View file

@ -10,6 +10,7 @@ import {
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { BsTransparency } from "react-icons/bs"; import { BsTransparency } from "react-icons/bs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Tooltip } from "react-tooltip";
const OpacityCustomization = () => { const OpacityCustomization = () => {
const { activeObject } = useContext(ActiveObjectContext); const { activeObject } = useContext(ActiveObjectContext);
@ -31,30 +32,35 @@ const OpacityCustomization = () => {
}; };
return ( return (
<Popover> <div>
<PopoverTrigger asChild> <Popover>
<Button variant="ghost" size="icon" className="h-8 w-8"> <PopoverTrigger asChild>
<BsTransparency className="h-4 w-4" size={20} /> <a data-tooltip-id="opacity-ic">
</Button> <Button variant="ghost" size="icon" className="h-8 w-8">
</PopoverTrigger> <BsTransparency className="h-4 w-4" size={20} />
<PopoverContent className="w-64 mt-3"> </Button>
<div className="grid gap-4"> </a>
<div className="flex items-center justify-between"> </PopoverTrigger>
<Label className="text-sm font-medium">Opacity</Label> <PopoverContent className="w-64 mt-3">
<span className="text-sm text-gray-500"> <div className="grid gap-4">
{Math.round(opacity * 100)}% <div className="flex items-center justify-between">
</span> <Label className="text-sm font-medium">Opacity</Label>
<span className="text-sm text-gray-500">
{Math.round(opacity * 100)}%
</span>
</div>
<Slider
value={[opacity]}
min={0}
max={1}
step={0.01}
onValueChange={(value) => adjustBackgroundOpacity(value[0])}
/>
</div> </div>
<Slider </PopoverContent>
value={[opacity]} </Popover>
min={0} <Tooltip id="opacity-ic" content="Transparency" place="bottom" />
max={1} </div>
step={0.01}
onValueChange={(value) => adjustBackgroundOpacity(value[0])}
/>
</div>
</PopoverContent>
</Popover>
); );
}; };

View file

@ -4,133 +4,141 @@ import { Card, CardDescription, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import * as lucideIcons from "lucide-react"; import * as lucideIcons from "lucide-react";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext"; import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { fabric } from 'fabric'; import { fabric } from "fabric";
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext"; import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
const AllIconsPage = () => { const AllIconsPage = () => {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const { canvas } = useContext(CanvasContext); const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext); const { setActiveObject } = useContext(ActiveObjectContext);
const { toast } = useToast(); const { toast } = useToast();
// Assume icons is already defined as shown previously, and filtered is created based on the search query // Assume icons is already defined as shown previously, and filtered is created based on the search query
const icons = Object.entries(lucideIcons)?.filter(([name, Icon]) => const icons = Object.entries(lucideIcons)?.filter(
!name.includes("Icon") && Icon?.$$typeof ([name, Icon]) => !name.includes("Icon") && Icon?.$$typeof
); );
const filtered = icons.filter(([name, Icon]) => const filtered = icons.filter(([name, Icon]) =>
name.toLowerCase().includes(search.toLowerCase()) name.toLowerCase().includes(search.toLowerCase())
); );
const handleSearch = (e) => { const handleSearch = (e) => {
setSearch(e.target.value); setSearch(e.target.value);
}; };
const handleIcon = (e) => { const handleIcon = (e) => {
// Check if the target is an SVG or path // Check if the target is an SVG or path
if (e.target.tagName.toLowerCase() === 'svg') { if (e.target.tagName.toLowerCase() === "svg") {
// Serialize the SVG element to a string and pass it // Serialize the SVG element to a string and pass it
const svgString = new XMLSerializer().serializeToString(e.target); const svgString = new XMLSerializer().serializeToString(e.target);
handleAddIcon(svgString); handleAddIcon(svgString);
} else {
toast({
title: "Invalid Choice",
description: "The target is a path element! Select the full icon.",
variant: "destructive",
});
}
};
const handleAddIcon = (svgString) => {
if (!canvas) {
console.error("Canvas is not initialized.");
return;
}
if (!svgString) {
console.error("Failed to retrieve SVG string from the icon.");
return;
}
fabric.loadSVGFromString(svgString, (objects, options) => {
if (!objects || !options) {
console.error("Failed to parse SVG.");
return;
}
// Recursively set fill color for all objects
const setFillColor = (obj, color) => {
if (obj.type === "group" && obj._objects) {
obj._objects.forEach((child) => setFillColor(child, color));
} else { } else {
toast({ obj.set("stroke", color);
title: "Invalid Choice",
description: "The target is a path element! Select the full icon.",
variant: "destructive",
})
} }
}; };
const handleAddIcon = (svgString) => { objects.forEach((obj) => setFillColor(obj, "#FFA500")); // Set fill color to orange
if (!canvas) {
console.error("Canvas is not initialized.");
return;
}
if (!svgString) { const iconGroup = fabric.util.groupSVGElements(objects, options);
console.error("Failed to retrieve SVG string from the icon."); iconGroup.set({
return; left: canvas.width / 2,
} top: canvas.height / 2,
originX: "center",
originY: "center",
scaleX: 6,
scaleY: 6,
});
fabric.loadSVGFromString(svgString, (objects, options) => { canvas.add(iconGroup);
if (!objects || !options) { canvas.setActiveObject(iconGroup);
console.error("Failed to parse SVG."); setActiveObject(iconGroup);
return; canvas.renderAll();
} });
};
// Recursively set fill color for all objects
const setFillColor = (obj, color) => {
if (obj.type === 'group' && obj._objects) {
obj._objects.forEach((child) => setFillColor(child, color));
} else {
obj.set('stroke', color);
}
};
objects.forEach((obj) => setFillColor(obj, '#FFA500')); // Set fill color to orange
const iconGroup = fabric.util.groupSVGElements(objects, options);
iconGroup.set({
left: canvas.width / 2,
top: canvas.height / 2,
originX: 'center',
originY: 'center',
scaleX: 6,
scaleY: 6,
});
canvas.add(iconGroup);
canvas.setActiveObject(iconGroup);
setActiveObject(iconGroup);
canvas.renderAll();
});
};
// Cell component for rendering each icon
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * 3 + columnIndex; // Adjust columns as needed (3 columns in this case)
if (index >= filtered.length) return null; // Handle out-of-bounds index
const [name, Icon] = filtered[index];
// Define cell-specific styles
return (
<div style={style}>
<div className="bg-red-50 rounded-md ml-1 p-1">
<Icon size={32} className="cursor-pointer bg-primary rounded-md text-white p-1" onClick={handleIcon} />
<p className="text-xs truncate w-full overflow-hidden whitespace-nowrap">{name}</p>
</div>
</div>
);
};
// Cell component for rendering each icon
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * 3 + columnIndex; // Adjust columns as needed (3 columns in this case)
if (index >= filtered.length) return null; // Handle out-of-bounds index
const [name, Icon] = filtered[index];
// Define cell-specific styles
return ( return (
<Card className="flex flex-col px-2 py-2 gap-1 scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white"> <div style={style}>
<CardTitle className="flex items-center flex-wrap my-1">All Icons</CardTitle> <div className="bg-red-50 rounded-md ml-1 p-1">
<CardDescription className="text-xs">All copyright (c) for Lucide are held by Lucide Contributors 2022.</CardDescription> <Icon
<Input size={32}
type="text" className="cursor-pointer bg-primary rounded-md text-white p-1 mx-auto"
placeholder="Search icons..." onClick={handleIcon}
onChange={handleSearch} />
className="border p-2 mb-0 w-full" <p className="text-xs text-center truncate w-full overflow-hidden whitespace-nowrap">
/> {name}
<Card className="flex items-center justify-center rounded-none p-1"> </p>
<Grid </div>
columnCount={4} </div>
columnWidth={50} );
height={330} };
rowCount={Math.ceil(filtered.length / 4)}
rowHeight={70} return (
width={240} <Card className="flex flex-col py-2 gap-1 scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white border-none shadow-none">
className="scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white" <CardTitle className="flex items-center flex-wrap my-1">
> All Icons
{Cell} </CardTitle>
</Grid> <CardDescription className="text-xs">
</Card> All copyright (c) for Lucide are held by Lucide Contributors 2022.
</Card> </CardDescription>
) <Input
type="text"
placeholder="Search icons..."
onChange={handleSearch}
className="border p-2 mb-0 w-[280px]"
/>
<Card className="flex items-center justify-center rounded-none p-1 border-none shadow-none">
<Grid
columnCount={3}
columnWidth={90}
height={330}
rowCount={Math.ceil(filtered.length / 3)}
rowHeight={70}
width={300}
className="scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white"
>
{Cell}
</Grid>
</Card>
</Card>
);
}; };
export default AllIconsPage; export default AllIconsPage;

View file

@ -1,85 +1,95 @@
import React, { useContext } from 'react' import React, { useContext } from "react";
import { ArrowBigRight, Diamond, Hexagon, Octagon, Pentagon, Sparkle, Square, Star, Triangle } from 'lucide-react' import {
ArrowBigRight,
Diamond,
Hexagon,
Octagon,
Pentagon,
Sparkle,
Square,
Star,
Triangle,
} from "lucide-react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
import { fabric } from 'fabric'; import { fabric } from "fabric";
import CanvasContext from '@/components/Context/canvasContext/CanvasContext'; import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext'; import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import { Card } from '@/components/ui/card'; import { Card } from "@/components/ui/card";
import { Separator } from '@/components/ui/separator'; import { Separator } from "@/components/ui/separator";
const RoundedShape = () => { const RoundedShape = () => {
const { canvas } = useContext(CanvasContext); const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext); const { setActiveObject } = useContext(ActiveObjectContext);
const shapes = [ const shapes = [
{ icon: <ArrowBigRight />, name: 'Arrow' }, { icon: <ArrowBigRight />, name: "Arrow" },
{ icon: <Diamond />, name: 'Diamond' }, { icon: <Diamond />, name: "Diamond" },
{ icon: <Hexagon />, name: 'Hexagon' }, { icon: <Hexagon />, name: "Hexagon" },
{ icon: <Octagon />, name: 'Octagon' }, { icon: <Octagon />, name: "Octagon" },
{ icon: <Pentagon />, name: 'Pentagon' }, { icon: <Pentagon />, name: "Pentagon" },
{ icon: <Sparkle />, name: 'Sparkle' }, { icon: <Sparkle />, name: "Sparkle" },
{ icon: <Square />, name: 'Square' }, { icon: <Square />, name: "Square" },
{ icon: <Star />, name: 'Star' }, { icon: <Star />, name: "Star" },
{ icon: <Triangle />, name: 'Triangle' }, { icon: <Triangle />, name: "Triangle" },
]; ];
const addObject = (icon) => { const addObject = (icon) => {
if (!canvas) { if (!canvas) {
console.error("Canvas is not initialized."); console.error("Canvas is not initialized.");
return; return;
} }
const svgString = ReactDOMServer.renderToStaticMarkup(icon); const svgString = ReactDOMServer.renderToStaticMarkup(icon);
if (!svgString) { if (!svgString) {
console.error("Failed to retrieve SVG string from icon."); console.error("Failed to retrieve SVG string from icon.");
return; return;
} }
// Load SVG onto the Fabric.js canvas // Load SVG onto the Fabric.js canvas
fabric.loadSVGFromString(svgString, (objects, options) => { fabric.loadSVGFromString(svgString, (objects, options) => {
if (!objects || !options) { if (!objects || !options) {
console.error("Failed to parse SVG."); console.error("Failed to parse SVG.");
return; return;
} }
const iconGroup = fabric.util.groupSVGElements(objects, options); const iconGroup = fabric.util.groupSVGElements(objects, options);
iconGroup.set({ iconGroup.set({
left: canvas.width / 2, left: canvas.width / 2,
top: canvas.height / 2, top: canvas.height / 2,
originX: 'center', originX: "center",
originY: 'center', originY: "center",
fill: "#f09b0a", fill: "#f09b0a",
scaleX: 6, scaleX: 6,
scaleY: 6, scaleY: 6,
strokeWidth: 0, strokeWidth: 0,
stroke: "#ffffff" stroke: "#ffffff",
}); });
canvas.add(iconGroup); canvas.add(iconGroup);
canvas.setActiveObject(iconGroup); canvas.setActiveObject(iconGroup);
setActiveObject(iconGroup) setActiveObject(iconGroup);
canvas.renderAll(); canvas.renderAll();
}); });
}; };
return ( return (
<Card className="p-2 bg-gradient-to-br from-white to-gray-100 rounded-xl shadow-lg"> <Card className="p-2 border-none shadow-none">
<h2 className="font-semibold text-sm mb-1">Rounded Shapes</h2> <h2 className="font-semibold text-sm mb-1">Rounded Shapes</h2>
<Separator className="my-2" /> <Separator className="my-2" />
<div className="grid grid-cols-3 gap-y-4 gap-x-2"> <div className="grid grid-cols-3 gap-y-4 gap-x-2">
{shapes.map((shape, index) => ( {shapes.map((shape, index) => (
<button <button
key={index} key={index}
className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105" className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105"
onClick={() => addObject(shape.icon)} onClick={() => addObject(shape.icon)}
> >
<div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300"> <div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300">
{React.cloneElement(shape.icon, { size: 36 })} {React.cloneElement(shape.icon, { size: 36 })}
</div>
{/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
</button>
))}
</div> </div>
</Card> {/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
) </button>
} ))}
</div>
</Card>
);
};
export default RoundedShape export default RoundedShape;

View file

@ -1,174 +1,326 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext'; import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from '@/components/Context/canvasContext/CanvasContext'; import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import React, { useContext } from 'react' import React, { useContext } from "react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
import { fabric } from 'fabric'; import { fabric } from "fabric";
import { Card } from '@/components/ui/card'; import { Card } from "@/components/ui/card";
import { Separator } from '@/components/ui/separator'; import { Separator } from "@/components/ui/separator";
import { Badge, Circle, Heart, Shield } from 'lucide-react'; import { Badge, Circle, Heart, Shield } from "lucide-react";
const PlainShapes = () => { const PlainShapes = () => {
const { canvas } = useContext(CanvasContext); const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext); const { setActiveObject } = useContext(ActiveObjectContext);
const shapes = [ const shapes = [
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" className="lucide lucide-arrow-big-left" fill='orange'> icon: (
<path d="M18 15H12v4L5 12l7-7v4h6v6z" /> <svg
</svg>, name: 'Arrow' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
className="lucide lucide-arrow-big-left"
fill="orange"
>
<path d="M18 15H12v4L5 12l7-7v4h6v6z" />
</svg>
),
name: "Arrow",
},
{ icon: <Badge />, name: 'Badge' }, { icon: <Badge />, name: "Badge" },
{ icon: <Circle />, name: 'Circle' }, { icon: <Circle />, name: "Circle" },
{ icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill='orange' className="lucide lucide-club"><path d="M17.28 9.05a5.5 5.5 0 1 0-10.56 0A5.5 5.5 0 1 0 12 17.66a5.5 5.5 0 1 0 5.28-8.6Z" /></svg>, name: 'Club' }, {
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-club"
>
<path d="M17.28 9.05a5.5 5.5 0 1 0-10.56 0A5.5 5.5 0 1 0 12 17.66a5.5 5.5 0 1 0 5.28-8.6Z" />
</svg>
),
name: "Club",
},
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#ea580c" strokeWidth="2" strokeLinecap="butt" strokeLinejoin="miter" className="lucide lucide-cross"> icon: (
<path d="M4 12h16M12 4v16" /> <svg
</svg>, name: 'Cross' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="none"
stroke="#ea580c"
strokeWidth="2"
strokeLinecap="butt"
strokeLinejoin="miter"
className="lucide lucide-cross"
>
<path d="M4 12h16M12 4v16" />
</svg>
),
name: "Cross",
},
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-diamond"> icon: (
<path d="M12 2L22 12L12 22L2 12Z" /> <svg
</svg>, name: 'Diamond' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-diamond"
>
<path d="M12 2L22 12L12 22L2 12Z" />
</svg>
),
name: "Diamond",
},
{ icon: <Heart />, name: 'Heart' }, { icon: <Heart />, name: "Heart" },
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-hexagon"> icon: (
<path d="M12 2L21 8v8l-9 6-9-6V8L12 2z" /> <svg
</svg>, name: 'Hexagon' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-hexagon"
>
<path d="M12 2L21 8v8l-9 6-9-6V8L12 2z" />
</svg>
),
name: "Hexagon",
},
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="white" stroke="#ea580c" strokeWidth="2" strokeLinecap="butt" strokeLinejoin="miter" className="lucide lucide-arrow-right"> icon: (
<path d="M5 12h14" /> <svg
</svg>, name: 'Line' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="white"
stroke="#ea580c"
strokeWidth="2"
strokeLinecap="butt"
strokeLinejoin="miter"
className="lucide lucide-arrow-right"
>
<path d="M5 12h14" />
</svg>
),
name: "Line",
},
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" stroke="#ec7e7e" strokeWidth="0" className="lucide lucide-octagon"> icon: (
<path d="M4 8 L8 4 H16 L20 8 V16 L16 20 H8 L4 16 Z" /> <svg
</svg>, name: 'Octagon' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
stroke="#ec7e7e"
strokeWidth="0"
className="lucide lucide-octagon"
>
<path d="M4 8 L8 4 H16 L20 8 V16 L16 20 H8 L4 16 Z" />
</svg>
),
name: "Octagon",
},
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-pentagon"> icon: (
<path d="M2 11 L12 2 L22 11 L19 21 L5 21 Z" /> <svg
</svg>, name: 'Pentagon' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-pentagon"
>
<path d="M2 11 L12 2 L22 11 L19 21 L5 21 Z" />
</svg>
),
name: "Pentagon",
},
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill='orange' className="lucide lucide-rectangle-horizontal"> icon: (
<path d="M2 6h20v12H2z" /> <svg
</svg>, name: 'Rectangle' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-rectangle-horizontal"
>
<path d="M2 6h20v12H2z" />
</svg>
),
name: "Rectangle",
},
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-triangle-right"> icon: (
<path d="M2 4 L22 20 L2 20 Z" /> <svg
</svg>, name: 'Right Triangle' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-triangle-right"
>
<path d="M2 4 L22 20 L2 20 Z" />
</svg>
),
name: "Right Triangle",
},
{
icon: <Shield fill="orange" stroke="0" />,
name: "Shield",
},
{ {
icon: <Shield fill="orange" stroke="0" />, name: 'Shield' icon: (
}, <svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-square"
>
<path d="M3 3h18v18H3z" />
</svg>
),
name: "Rectangle Square",
},
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-square"> icon: (
<path d="M3 3h18v18H3z" /> <svg
</svg>, name: 'Rectangle Square' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-star"
>
<path d="M12 2 L14.85 8.9 L22 10 L16.5 14.5 L18 21 L12 17.5 L6 21 L7.5 14.5 L2 10 L9.15 8.9 Z" />
</svg>
),
name: "Star",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-triangle"
>
<path d="M12 4L4 20h16L12 4Z" />
</svg>
),
name: "Triangle",
},
{ {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-star"> icon: (
<path d="M12 2 L14.85 8.9 L22 10 L16.5 14.5 L18 21 L12 17.5 L6 21 L7.5 14.5 L2 10 L9.15 8.9 Z" /> <svg
</svg>, name: 'Star' xmlns="http://www.w3.org/2000/svg"
}, width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-rectangle-vertical scale-125"
>
<path d="M6 2h12v20H6z" />
</svg>
),
name: "Rectangle Vertical",
},
];
{ const addObject = (icon, name) => {
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-triangle"> if (!canvas) {
<path d="M12 4L4 20h16L12 4Z" /> console.error("Canvas is not initialized.");
</svg>, name: 'Triangle' return;
}, }
{ const svgString = ReactDOMServer.renderToStaticMarkup(icon);
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-rectangle-vertical scale-125">
<path d="M6 2h12v20H6z" />
</svg>, name: 'Rectangle Vertical'
},
]; if (!svgString) {
console.error("Failed to retrieve SVG string from icon.");
const addObject = (icon, name) => { return;
if (!canvas) { }
console.error("Canvas is not initialized."); // Load SVG onto the Fabric.js canvas
return; fabric.loadSVGFromString(svgString, (objects, options) => {
} if (!objects || !options) {
console.error("Failed to parse SVG.");
const svgString = ReactDOMServer.renderToStaticMarkup(icon); return;
}
if (!svgString) { const iconGroup = fabric.util.groupSVGElements(objects, options);
console.error("Failed to retrieve SVG string from icon."); iconGroup.set({
return; left: canvas.width / 2,
} top: canvas.height / 2,
// Load SVG onto the Fabric.js canvas originX: "center",
fabric.loadSVGFromString(svgString, (objects, options) => { originY: "center",
if (!objects || !options) { fill: "#f09b0a",
console.error("Failed to parse SVG."); scaleX: 6,
return; scaleY: 6,
} strokeWidth: 0,
const iconGroup = fabric.util.groupSVGElements(objects, options); // rx: 0,
iconGroup.set({ // x: 0,
left: canvas.width / 2, // y: 0,
top: canvas.height / 2, });
originX: 'center', if (name === "Line") {
originY: 'center', iconGroup.set({
fill: "#f09b0a", strokeWidth: 2,
scaleX: 6,
scaleY: 6,
strokeWidth: 0,
// rx: 0,
// x: 0,
// y: 0,
});
if (name === "Line") {
iconGroup.set({
strokeWidth: 2,
})
}
canvas.add(iconGroup);
canvas.setActiveObject(iconGroup);
setActiveObject(iconGroup)
canvas.renderAll();
}); });
}; }
canvas.add(iconGroup);
canvas.setActiveObject(iconGroup);
setActiveObject(iconGroup);
canvas.renderAll();
});
};
return ( return (
<Card className="p-2 bg-gradient-to-br from-white to-gray-100 rounded-xl shadow-lg"> <Card className="p-2 rounded-xl shadow-none border-none">
<h2 className="font-semibold text-sm mb-1">Plain Shapes</h2> <h2 className="font-semibold text-sm mb-1">Plain Shapes</h2>
<Separator className="my-2" /> <Separator className="my-2" />
<div className="grid grid-cols-3 gap-y-4 gap-x-2"> <div className="grid grid-cols-3 gap-y-4 gap-x-2">
{shapes.map((shape, index) => ( {shapes.map((shape, index) => (
<button <button
key={index} key={index}
className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105" className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105"
onClick={() => addObject(shape.icon, shape.name)} onClick={() => addObject(shape.icon, shape.name)}
> >
<div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300"> <div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300">
{React.cloneElement(shape.icon, { size: 36, fill: "#ea580c" })} {React.cloneElement(shape.icon, { size: 36, fill: "#ea580c" })}
</div>
{/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
</button>
))}
</div> </div>
</Card> {/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
) </button>
} ))}
</div>
</Card>
);
};
export default PlainShapes export default PlainShapes;

View file

@ -1,288 +1,308 @@
import { useContext, useRef, useState } from 'react' import { useContext, useRef, useState } from "react";
import CanvasContext from '../Context/canvasContext/CanvasContext' import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { fabric } from 'fabric' import { fabric } from "fabric";
import { ImageIcon, Trash2 } from 'lucide-react' import { ImageIcon, Trash2 } from "lucide-react";
import { Label } from '@/components/ui/label' import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import {
import Resizer from "react-image-file-resizer" Select,
import { Slider } from '@/components/ui/slider' SelectContent,
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' SelectItem,
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' SelectTrigger,
import { useDropzone } from 'react-dropzone' SelectValue,
import ImageCustomization from './Customization/ImageCustomization' } from "@/components/ui/select";
import { Separator } from '../ui/separator' import Resizer from "react-image-file-resizer";
import ActiveObjectContext from '../Context/activeObject/ObjectContext' import { Slider } from "@/components/ui/slider";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useDropzone } from "react-dropzone";
import ImageCustomization from "./Customization/ImageCustomization";
import { Separator } from "../ui/separator";
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
const UploadImage = () => { const UploadImage = () => {
const { canvas } = useContext(CanvasContext); const { canvas } = useContext(CanvasContext);
const [width, setWidth] = useState(1080); const [width, setWidth] = useState(1080);
const [height, setHeight] = useState(1080); const [height, setHeight] = useState(1080);
const [quality, setQuality] = useState(100); const [quality, setQuality] = useState(100);
const [rotation, setRotation] = useState("0"); const [rotation, setRotation] = useState("0");
const [format, setFormat] = useState('JPEG'); const [format, setFormat] = useState("JPEG");
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const { activeObject, setActiveObject } = useContext(ActiveObjectContext); const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [preview, setPreview] = useState(null); const [preview, setPreview] = useState(null);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: { accept: {
'image/*': ['.jpeg', '.png', '.gif', '.jpg', '.webp', '.svg'] "image/*": [".jpeg", ".png", ".gif", ".jpg", ".webp", ".svg"],
}, },
// maxSize: 5 * 1024 * 1024, // 5MB max file size // maxSize: 5 * 1024 * 1024, // 5MB max file size
multiple: false, multiple: false,
onDrop: (acceptedFiles) => { onDrop: (acceptedFiles) => {
if (!acceptedFiles.length) { if (!acceptedFiles.length) {
console.error("No files were dropped."); console.error("No files were dropped.");
return; return;
} }
const selectedFile = acceptedFiles[0]; const selectedFile = acceptedFiles[0];
// Create a preview URL // Create a preview URL
const blobUrl = URL.createObjectURL(selectedFile); const blobUrl = URL.createObjectURL(selectedFile);
setFile(selectedFile); setFile(selectedFile);
setPreview(blobUrl); setPreview(blobUrl);
if (selectedFile.type === "image/svg+xml") { if (selectedFile.type === "image/svg+xml") {
addImageToCanvas(selectedFile); addImageToCanvas(selectedFile);
URL.revokeObjectURL(blobUrl); URL.revokeObjectURL(blobUrl);
} else { } else {
const imgElement = new Image(); const imgElement = new Image();
imgElement.src = blobUrl; imgElement.src = blobUrl;
imgElement.onload = () => { imgElement.onload = () => {
if (imgElement.width > 1080) { if (imgElement.width > 1080) {
handleResize(selectedFile, (compressedFile) => { handleResize(selectedFile, (compressedFile) => {
addImageToCanvas(compressedFile); addImageToCanvas(compressedFile);
}); });
} else { } else {
addImageToCanvas(selectedFile); addImageToCanvas(selectedFile);
} }
URL.revokeObjectURL(blobUrl); // Clean up URL.revokeObjectURL(blobUrl); // Clean up
}; };
imgElement.onerror = () => { imgElement.onerror = () => {
console.error("Failed to load image."); console.error("Failed to load image.");
URL.revokeObjectURL(blobUrl); // Clean up URL.revokeObjectURL(blobUrl); // Clean up
}; };
} }
} },
});
const removeFile = () => {
// Revoke the object URL to free up memory
if (preview) {
URL.revokeObjectURL(preview);
}
setFile(null);
setPreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
if (activeObject?.type === "image") {
canvas.remove(activeObject);
setActiveObject(null);
canvas.renderAll();
}
};
const handleResize = (file, callback) => {
Resizer.imageFileResizer(
file,
width,
height,
format,
quality,
parseInt(rotation),
(resizedFile) => {
callback(resizedFile);
},
"file"
);
};
const addImageToCanvas = (file) => {
const blobUrl = URL.createObjectURL(file);
fabric.Image.fromURL(blobUrl, (img) => {
img.set({
left: canvas.width / 4,
top: canvas.height / 4,
scaleX: 0.5,
scaleY: 0.5,
});
canvas.add(img);
canvas.setActiveObject(img);
// Update the active object state
setActiveObject(img);
canvas.renderAll();
URL.revokeObjectURL(blobUrl);
}); });
};
const removeFile = () => { return (
// Revoke the object URL to free up memory <Card className="w-full border-none shadow-none">
if (preview) { <CardHeader className="px-4 py-3">
URL.revokeObjectURL(preview) <CardTitle>Image Upload & Editing</CardTitle>
} <CardDescription>
setFile(null) Upload, resize, and apply effects to your images
setPreview(null) </CardDescription>
if (fileInputRef.current) { </CardHeader>
fileInputRef.current.value = "" <CardContent className="p-2">
} <Tabs defaultValue="upload">
if (activeObject?.type === "image") { <TabsList className="grid w-full grid-cols-2">
canvas.remove(activeObject); <TabsTrigger value="upload">Upload</TabsTrigger>
setActiveObject(null); <TabsTrigger value="effects">Effects</TabsTrigger>
canvas.renderAll(); </TabsList>
}
}
const handleResize = (file, callback) => { {/* Uploads */}
Resizer.imageFileResizer( <TabsContent value="upload">
file, <div className="space-y-4">
width, <div className="grid grid-cols-2 gap-4">
height, <div className="space-y-2">
format, <Label>Width: {width}px</Label>
quality, <Slider
parseInt(rotation), value={[width]}
(resizedFile) => { max={2000}
callback(resizedFile) min={300}
}, step={10}
'file', onValueChange={(value) => setWidth(value[0])}
) />
} </div>
<div className="space-y-2">
<Label>Height: {height}px</Label>
<Slider
value={[height]}
max={2000}
min={300}
step={10}
onValueChange={(value) => setHeight(value[0])}
/>
</div>
</div>
{format === "JPEG" && (
<div className="space-y-2">
<Label>Quality: {quality}%</Label>
<Slider
value={[quality]}
max={100}
step={1}
onValueChange={(value) => setQuality(value[0])}
/>
</div>
)}
const addImageToCanvas = (file) => { <div className="grid grid-cols-2 gap-4">
const blobUrl = URL.createObjectURL(file) <div className="space-y-2">
fabric.Image.fromURL(blobUrl, (img) => { <Label>Format</Label>
img.set({ <Select value={format} onValueChange={setFormat}>
left: canvas.width / 4, <SelectTrigger>
top: canvas.height / 4, <SelectValue />
scaleX: 0.5, </SelectTrigger>
scaleY: 0.5, <SelectContent>
}) <SelectItem value="JPEG">JPEG</SelectItem>
canvas.add(img) <SelectItem value="PNG">PNG</SelectItem>
canvas.setActiveObject(img); <SelectItem value="WEBP">WEBP</SelectItem>
// Update the active object state </SelectContent>
setActiveObject(img); </Select>
canvas.renderAll(); </div>
URL.revokeObjectURL(blobUrl); <div className="space-y-2">
}) <Label>Rotation</Label>
} <Select value={rotation} onValueChange={setRotation}>
<SelectTrigger>
<SelectValue />
</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>
</div>
return ( {/* upload image */}
<Card className="w-full mx-auto"> {!preview && (
<CardHeader className="px-4 py-3"> <div className="max-w-md mx-auto p-4">
<CardTitle>Image Upload & Editing</CardTitle> <Card>
<CardDescription>Upload, resize, and apply effects to your images</CardDescription> <CardContent className="p-6 space-y-4">
</CardHeader> <div
<CardContent className="p-2"> {...getRootProps()}
<Tabs defaultValue="upload"> className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors duration-300 ${
<TabsList className="grid w-full grid-cols-2"> isDragActive
<TabsTrigger value="upload">Upload</TabsTrigger> ? "border-primary bg-primary/10"
<TabsTrigger value="effects">Effects</TabsTrigger> : "border-gray-300 hover:border-primary"
</TabsList> } ${preview ? "hidden" : ""}`}
ref={fileInputRef}
{/* Uploads */} >
<TabsContent value="upload"> <input {...getInputProps()} />
<div className="space-y-4"> <div className="flex flex-col items-center justify-center space-y-4">
<div className="grid grid-cols-2 gap-4"> <ImageIcon
<div className="space-y-2"> className={`h-12 w-12 ${
<Label>Width: {width}px</Label> isDragActive ? "text-primary" : "text-gray-400"
<Slider }`}
value={[width]} />
max={2000} <p className="text-sm text-gray-600">
min={300} {isDragActive
step={10} ? "Drop file here"
onValueChange={(value) => setWidth(value[0])} : "Drag 'n' drop an image, or click to select a file"}
/> </p>
</div> <p className="text-xs text-gray-500">
<div className="space-y-2"> (Max 5MB, image files only)
<Label>Height: {height}px</Label> </p>
<Slider
value={[height]}
max={2000}
min={300}
step={10}
onValueChange={(value) => setHeight(value[0])}
/>
</div>
</div>
{format === "JPEG" && (
<div className="space-y-2">
<Label>Quality: {quality}%</Label>
<Slider
value={[quality]}
max={100}
step={1}
onValueChange={(value) => setQuality(value[0])}
/>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Format</Label>
<Select value={format} onValueChange={setFormat}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="JPEG">JPEG</SelectItem>
<SelectItem value="PNG">PNG</SelectItem>
<SelectItem value="WEBP">WEBP</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Rotation</Label>
<Select value={rotation} onValueChange={setRotation}>
<SelectTrigger>
<SelectValue />
</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>
</div>
{/* upload image */}
{
!preview &&
<div className="max-w-md mx-auto p-4">
<Card>
<CardContent className="p-6 space-y-4">
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors duration-300 ${isDragActive ? 'border-primary bg-primary/10' : 'border-gray-300 hover:border-primary'} ${preview ? 'hidden' : ''}`}
ref={fileInputRef}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center justify-center space-y-4">
<ImageIcon
className={`h-12 w-12 ${isDragActive ? 'text-primary' : 'text-gray-400'}`}
/>
<p className="text-sm text-gray-600">
{isDragActive
? 'Drop file here'
: 'Drag \'n\' drop an image, or click to select a file'
}
</p>
<p className="text-xs text-gray-500">
(Max 5MB, image files only)
</p>
</div>
</div>
</CardContent>
</Card>
</div>
}
{/* preview image */}
{preview && (
<Card className="overflow-y-scroll rounded-none">
<CardContent className="mt-2 mb-2">
<div className="w-fit aspect-square relative h-[100%]">
{
file?.type === "image/svg+xml" ? <object
data={preview}
type="image/svg+xml"
className="object-cover rounded-lg"
style={{ width: '100%', height: '100%' }}
>
Your browser does not support SVG, no preview available for SVG.
</object> :
<img
src={preview}
alt="Uploaded image"
className="object-cover rounded-lg overflow-hidden"
/>
}
<Separator className="my-4" />
<div className="grid grid-cols-1 gap-2 items-center pb-4">
<p className="text-sm text-gray-600 truncate">{file?.name}</p>
<Button
variant="destructive"
size="sm"
onClick={removeFile}
>
<Trash2 className="mr-2 h-4 w-4" />
Remove
</Button>
</div>
</div>
</CardContent>
</Card>
)}
</div> </div>
</TabsContent> </div>
</CardContent>
</Card>
</div>
)}
{/* Effects */} {/* preview image */}
<TabsContent value="effects"> {preview && (
<ImageCustomization /> <Card className="overflow-y-scroll rounded-none">
</TabsContent> <CardContent className="mt-2 mb-2">
</Tabs> <div className="w-fit aspect-square relative h-[100%]">
</CardContent> {file?.type === "image/svg+xml" ? (
</Card> <object
) data={preview}
} type="image/svg+xml"
export default UploadImage className="object-cover rounded-lg"
style={{ width: "100%", height: "100%" }}
>
Your browser does not support SVG, no preview
available for SVG.
</object>
) : (
<img
src={preview}
alt="Uploaded image"
className="object-cover rounded-lg overflow-hidden"
/>
)}
<Separator className="my-4" />
<div className="grid grid-cols-1 gap-2 items-center pb-4">
<p className="text-sm text-gray-600 truncate">
{file?.name}
</p>
<Button
variant="destructive"
size="sm"
onClick={removeFile}
>
<Trash2 className="mr-2 h-4 w-4" />
Remove
</Button>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</TabsContent>
{/* Effects */}
<TabsContent value="effects">
<ImageCustomization />
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
};
export default UploadImage;

View file

@ -176,6 +176,7 @@ export const ObjectShortcut = ({ value }) => {
const clearCanvas = () => { const clearCanvas = () => {
canvas.clear(); canvas.clear();
canvas.renderAll(); canvas.renderAll();
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
setActiveObject(null); setActiveObject(null);
}; };

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import CanvasSetting from "../CanvasSetting";
const CanvasPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Canvas Settings</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<CanvasSetting />
</ScrollArea>
</div>
);
};
export default CanvasPanel;

View file

@ -1,15 +1,7 @@
import { useContext } from "react"; import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext"; import CanvasContext from "../Context/canvasContext/CanvasContext";
import SelectObjectFromGroup from "../EachComponent/Customization/SelectObjectFromGroup";
import StrokeCustomization from "../EachComponent/Customization/StrokeCustomization";
import PositionCustomization from "../EachComponent/Customization/PositionCustomization";
import { Card } from "../ui/card";
import CollapsibleComponent from "../EachComponent/Customization/CollapsibleComponent";
import FlipCustomization from "../EachComponent/Customization/FlipCustomization";
import RotateCustomization from "../EachComponent/Customization/RotateCustomization";
import SkewCustomization from "../EachComponent/Customization/SkewCustomization"; import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
import ScaleObjects from "../EachComponent/Customization/ScaleObjects"; import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
import ShadowCustomization from "../EachComponent/Customization/ShadowCustomization";
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape"; import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
import ApplyColor from "../EachComponent/ApplyColor"; import ApplyColor from "../EachComponent/ApplyColor";
@ -23,45 +15,17 @@ const CommonPanel = () => {
return ( return (
<div> <div>
<div className="space-y-5"> <div className="space-y-5">
<SelectObjectFromGroup />
{/* Apply fill and background color */} {/* Apply fill and background color */}
{activeObjectType !== "image" && !hasClipPath && !customClipPath && ( {activeObjectType !== "image" && !hasClipPath && !customClipPath && (
<ApplyColor /> <ApplyColor />
)} )}
{/* Apply stroke and stroke color */}
{!customClipPath && (
<>
<StrokeCustomization />
</>
)}
{activeObject?.type !== "group" && (
<>
<PositionCustomization />
</>
)}
{/* Controls for opacity, flip, and rotation */}
<Card className="shadow-none border-0">
<CollapsibleComponent text={"Flip, Rotate Control"}>
<div className="space-y-2">
<FlipCustomization />
<RotateCustomization />
</div>
</CollapsibleComponent>
</Card>
{/* Skew Customization */} {/* Skew Customization */}
<SkewCustomization /> <SkewCustomization />
{/* Scale Objects */} {/* Scale Objects */}
<ScaleObjects /> <ScaleObjects />
{/* Shadow Customization */}
<ShadowCustomization />
{/* Add image into shape */} {/* Add image into shape */}
<AddImageIntoShape /> <AddImageIntoShape />
</div> </div>

View file

@ -2,6 +2,16 @@ import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext"; import CanvasContext from "../Context/canvasContext/CanvasContext";
import TextPanel from "./TextPanel"; import TextPanel from "./TextPanel";
import ColorPanel from "./ColorPanel"; import ColorPanel from "./ColorPanel";
import ShapePanel from "./ShapePanel";
import IconPanel from "./IconPanel";
import UploadPanel from "./UploadPanel";
import StrokePanel from "./StrokePanel";
import ShadowPanel from "./ShadowPanel";
import FlipPanel from "./FlipPanel";
import PositionPanel from "./PositionPanel";
import ImagePanel from "./ImagePanel";
import GroupObjectPanel from "./GroupObjectPanel";
import CanvasPanel from "./CanvasPanel";
const EditorPanel = () => { const EditorPanel = () => {
const { selectedPanel } = useContext(CanvasContext); const { selectedPanel } = useContext(CanvasContext);
@ -10,8 +20,28 @@ const EditorPanel = () => {
switch (selectedPanel) { switch (selectedPanel) {
case "text": case "text":
return <TextPanel />; return <TextPanel />;
case "shape":
return <ShapePanel />;
case "icon":
return <IconPanel />;
case "upload":
return <UploadPanel />;
case "color": case "color":
return <ColorPanel />; return <ColorPanel />;
case "stroke":
return <StrokePanel />;
case "shadow":
return <ShadowPanel />;
case "flip":
return <FlipPanel />;
case "position":
return <PositionPanel />;
case "image-insert":
return <ImagePanel />;
case "group-obj":
return <GroupObjectPanel />;
case "canvas":
return <CanvasPanel />;
default: default:
return; return;
} }

View file

@ -0,0 +1,39 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import { Card } from "../ui/card";
import CollapsibleComponent from "../EachComponent/Customization/CollapsibleComponent";
import FlipCustomization from "../EachComponent/Customization/FlipCustomization";
import RotateCustomization from "../EachComponent/Customization/RotateCustomization";
const FlipPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Flip & Rotate</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<Card className="shadow-none border-0">
<CollapsibleComponent text={"Flip, Rotate Control"}>
<div className="space-y-2">
<FlipCustomization />
<RotateCustomization />
</div>
</CollapsibleComponent>
</Card>
</ScrollArea>
</div>
);
};
export default FlipPanel;

View file

@ -0,0 +1,36 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import SelectObjectFromGroup from "../EachComponent/Customization/SelectObjectFromGroup";
import { X } from "lucide-react";
import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
const GroupObjectPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Group Object</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<SelectObjectFromGroup />
{/* Skew Customization */}
<SkewCustomization />
{/* Scale Objects */}
<ScaleObjects />
</ScrollArea>
</div>
);
};
export default GroupObjectPanel;

View file

@ -0,0 +1,30 @@
import { useContext } from "react";
import { Button } from "../ui/button";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { X } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area";
import AllIconsPage from "../EachComponent/Icons/AllIcons";
const IconPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Icons</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<AllIconsPage />
</ScrollArea>
</div>
);
};
export default IconPanel;

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
const ImagePanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Image</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<AddImageIntoShape />
</ScrollArea>
</div>
);
};
export default ImagePanel;

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import PositionCustomization from "../EachComponent/Customization/PositionCustomization";
const PositionPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Position Controller</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<PositionCustomization />
</ScrollArea>
</div>
);
};
export default PositionPanel;

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import ShadowCustomization from "../EachComponent/Customization/ShadowCustomization";
const ShadowPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Shadow Color</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<ShadowCustomization />
</ScrollArea>
</div>
);
};
export default ShadowPanel;

View file

@ -0,0 +1,47 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import { Separator } from "../ui/separator";
import CustomShape from "../EachComponent/CustomShape/CustomShape";
import RoundedShape from "../EachComponent/RoundedShapes/RoundedShape";
import PlainShapes from "../EachComponent/Shapes/PlainShapes";
const ShapePanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Shape</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<h2 className="font-semibold text-sm">Custom Shapes</h2>
<Separator className="my-2" />
<div className="space-y-4">
<div>
<CustomShape />
</div>
<div>
<RoundedShape />
</div>
<div>
<PlainShapes />
</div>
</div>
</ScrollArea>
</div>
);
};
export default ShapePanel;

View file

@ -1,16 +1,16 @@
import { useContext } from "react"; import { useContext } from "react";
import ApplyColor from "../EachComponent/ApplyColor";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import CanvasContext from "../Context/canvasContext/CanvasContext"; import CanvasContext from "../Context/canvasContext/CanvasContext";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area"; import { ScrollArea } from "../ui/scroll-area";
import StrokeCustomization from "../EachComponent/Customization/StrokeCustomization";
const ColorPanel = () => { const StrokePanel = () => {
const { setSelectedPanel } = useContext(CanvasContext); const { setSelectedPanel } = useContext(CanvasContext);
return ( return (
<div> <div>
<div className="flex justify-between items-center p-4 border-b"> <div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Color</h2> <h2 className="text-lg font-semibold">Stroke Color</h2>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -19,11 +19,11 @@ const ColorPanel = () => {
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</div> </div>
<ScrollArea className="lg:h-[calc(90vh-190px)] xl:h-[calc(100vh-190px)] px-4 py-4"> <ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<ApplyColor /> <StrokeCustomization />
</ScrollArea> </ScrollArea>
</div> </div>
); );
}; };
export default ColorPanel; export default StrokePanel;

View file

@ -5,9 +5,23 @@ import OpacityCustomization from "../EachComponent/Customization/OpacityCustomiz
import CanvasContext from "../Context/canvasContext/CanvasContext"; import CanvasContext from "../Context/canvasContext/CanvasContext";
import { useContext } from "react"; import { useContext } from "react";
import { ObjectShortcut } from "../ObjectShortcut"; import { ObjectShortcut } from "../ObjectShortcut";
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
import { Button } from "../ui/button";
import { ImagePlus } from "lucide-react";
import { RxBorderWidth } from "react-icons/rx";
import { LuFlipVertical } from "react-icons/lu";
import { SlTarget } from "react-icons/sl";
import { RiShadowLine } from "react-icons/ri";
import { FaRegObjectGroup } from "react-icons/fa";
import { Tooltip } from "react-tooltip";
export function TopBar() { export function TopBar() {
const { selectedPanel } = useContext(CanvasContext); const { activeObject } = useContext(ActiveObjectContext);
const { selectedPanel, setSelectedPanel, textColor } =
useContext(CanvasContext);
const activeObjectType = activeObject?.type;
const hasClipPath = !!activeObject?.clipPath;
const customClipPath = activeObject?.isClipPath;
return ( return (
<div> <div>
<ScrollArea <ScrollArea
@ -21,6 +35,147 @@ export function TopBar() {
<div> <div>
<TextCustomization /> <TextCustomization />
</div> </div>
<div className="flex items-center gap-4 px-2">
<Button
variant="outline"
className="flex items-center gap-2 border-dashed border-2 rounded-md hover:bg-gray-50"
onClick={() => setSelectedPanel("image-insert")}
>
<ImagePlus className="w-5 h-5" />
<span>Add image</span>
</Button>
<div>
<a data-tooltip-id="canvas">
<Button
variant="ghost"
size="icon"
className="w-10 h-10"
onClick={() => setSelectedPanel("canvas")}
>
<svg
width="100"
height="100"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="3"
y="5"
width="18"
height="12"
stroke="black"
strokeWidth="2"
fill="none"
/>
<path d="M14 14 L19 19" stroke="black" strokeWidth="2" />
<path
d="M15 15 Q16 12, 19 11"
stroke="black"
strokeWidth="2"
fill="none"
/>
<line
x1="3"
y1="17"
x2="7"
y2="21"
stroke="black"
strokeWidth="2"
/>
<line
x1="21"
y1="17"
x2="17"
y2="21"
stroke="black"
strokeWidth="2"
/>
</svg>
</Button>
</a>
</div>
<div className="flex items-center gap-2">
{activeObjectType !== "image" &&
activeObject?.type !== "i-text" &&
!hasClipPath &&
!customClipPath && (
<a data-tooltip-id="color-gr">
<Button
variant="ghost"
size="icon"
className={"rounded-full"}
onClick={() => setSelectedPanel("color")}
style={{ backgroundColor: textColor?.fill || "black" }}
></Button>
</a>
)}
{!customClipPath && (
<a data-tooltip-id="stroke">
<Button
variant="ghost"
size="icon"
className="w-10 h-10"
onClick={() => setSelectedPanel("stroke")}
>
<RxBorderWidth className="text-lg" />
</Button>
</a>
)}
{(activeObject || activeObject.type === "group") && (
<a data-tooltip-id="group-obj">
<Button
variant="ghost"
size="icon"
className="w-10 h-10"
onClick={() => setSelectedPanel("group-obj")}
>
<FaRegObjectGroup />
</Button>
</a>
)}
</div>
<div className="h-6 w-px bg-gray-200" />
<div className="flex items-center gap-2">
<a data-tooltip-id="flip">
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("flip")}
>
<LuFlipVertical />
</Button>
</a>
{activeObject?.type !== "group" && (
<a data-tooltip-id="position">
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("position")}
>
<SlTarget />
</Button>
</a>
)}
<a data-tooltip-id="shadow">
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("shadow")}
>
<RiShadowLine />
</Button>
</a>
</div>
</div>
<OpacityCustomization /> <OpacityCustomization />
<div className="h-4 w-px bg-border mx-2" /> <div className="h-4 w-px bg-border mx-2" />
@ -33,6 +188,14 @@ export function TopBar() {
</div> </div>
<ScrollBar orientation="horizontal" /> <ScrollBar orientation="horizontal" />
</ScrollArea> </ScrollArea>
<Tooltip id="color-gr" content="Color" place="bottom" />
<Tooltip id="stroke" content="Stroke" place="bottom" />
<Tooltip id="position" content="Object position" place="bottom" />
<Tooltip id="shadow" content="Shadow color" place="bottom" />
<Tooltip id="flip" content="Object flip" place="bottom" />
<Tooltip id="group-obj" content="Group Object" place="bottom" />
<Tooltip id="canvas" content="Canvas Settings" place="bottom" />
</div> </div>
); );
} }

View file

@ -0,0 +1,30 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { X } from "lucide-react";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import UploadImage from "../EachComponent/UploadImage";
const UploadPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Upload</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<UploadImage />
</ScrollArea>
</div>
);
};
export default UploadPanel;