group object selection preview added, object locked/unloack feature added

This commit is contained in:
Saimon8420 2024-12-29 12:49:25 +06:00
parent 24f99a77fb
commit fa766e8ed3
13 changed files with 669 additions and 68 deletions

221
package-lock.json generated
View file

@ -21,6 +21,7 @@
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.4",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -2324,6 +2325,226 @@
} }
} }
}, },
"node_modules/@radix-ui/react-toast": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.4.tgz",
"integrity": "sha512-Sch9idFJHJTMH9YNpxxESqABcAFweJG4tKv+0zo0m5XBvUSL8FM5xKcJLFLXononpePs8IclyX1KieL5SDUNgA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collection": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.3",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz",
"integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
"integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz",

View file

@ -23,6 +23,7 @@
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.4",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -57,4 +58,4 @@
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"vite": "^5.4.10" "vite": "^5.4.10"
} }
} }

View file

@ -6,6 +6,7 @@ import Header from './components/Layouts/Header';
import SheetRightPanel from './components/Layouts/SheetRightPanel'; import SheetRightPanel from './components/Layouts/SheetRightPanel';
import SheetLeftPanel from './components/Layouts/SheetLeftPanel'; import SheetLeftPanel from './components/Layouts/SheetLeftPanel';
import CanvasCapture from "./components/CanvasCapture"; import CanvasCapture from "./components/CanvasCapture";
import { Toaster } from './components/ui/toaster';
function App() { function App() {
useEffect(() => { useEffect(() => {
@ -68,6 +69,7 @@ function App() {
return ( return (
<div className="relative flex flex-col h-screen overflow-hidden"> <div className="relative flex flex-col h-screen overflow-hidden">
<Toaster />
<SheetLeftPanel /> <SheetLeftPanel />
<Header /> <Header />
<SheetRightPanel /> <SheetRightPanel />

View file

@ -76,7 +76,7 @@ const Canvas = () => {
}; };
}, [removeSelected]); }, [removeSelected]);
// console.log(activeObject); console.log(activeObject);
return ( return (
<div className='flex flex-col items-center relative w-full h-full overflow-hidden'> <div className='flex flex-col items-center relative w-full h-full overflow-hidden'>

View file

@ -31,7 +31,7 @@ const AddImageIntoShape = () => {
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 [isOpen, setIsOpen] = useState(true); const [isOpen, setIsOpen] = useState(false);
const handleResize = (file, callback) => { const handleResize = (file, callback) => {
Resizer.imageFileResizer( Resizer.imageFileResizer(
@ -136,14 +136,35 @@ const AddImageIntoShape = () => {
absolutePositioned: true, 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 // Create a fabric image object with scaling and clipPath
const fabricImage = new fabric.Image(img, { const fabricImage = new fabric.Image(img, {
scaleX: 0.2, scaleX: scaleX,
scaleY: 0.2, scaleY: scaleY,
left: activeObject.left, left: activeObject.left,
top: activeObject.top, top: activeObject.top,
clipPath: activeObject, // Apply clipPath to the image clipPath: activeObject, // Apply clipPath to the image
originX: activeObject.originX, // Match origin point
originY: activeObject.originY // Match origin point
}); });
// 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.add(fabricImage);
canvas.setActiveObject(fabricImage); canvas.setActiveObject(fabricImage);
setActiveObject(fabricImage); setActiveObject(fabricImage);

View file

@ -0,0 +1,68 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
import { Button } from '@/components/ui/button';
import { useToast } from '@/hooks/use-toast';
import { useContext, useEffect, useState } from 'react';
import { Lock, Unlock } from "lucide-react";
import { Card } from '@/components/ui/card';
const LockObject = () => {
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [isLocked, setIsLocked] = useState(false);
const { toast } = useToast();
useEffect(() => {
if (activeObject?.lockMovementX === false) {
setIsLocked(false);
}
else {
setIsLocked(true);
}
}, [activeObject])
const toggleLock = () => {
if (!canvas || !activeObject) {
toast({
title: "No object selected",
description: "Please select an object to lock or unlock.",
variant: "destructive",
})
return;
}
const newLockState = !activeObject.lockMovementX;
activeObject.set({
lockMovementX: newLockState,
lockMovementY: newLockState,
lockRotation: newLockState,
lockScalingX: newLockState,
lockScalingY: newLockState,
});
setIsLocked(newLockState);
canvas.requestRenderAll();
toast({
title: newLockState ? "Object locked" : "Object unlocked",
description: newLockState ? "The object is now locked in place." : "The object can now be moved and resized.",
})
}
return (
<Card className="p-2">
<h2 className='font-bold'>{!isLocked ? "Lock" : "Unlock"} Object</h2>
<Button
onClick={toggleLock}
variant="outline"
size="icon"
disabled={!activeObject}
title={isLocked ? "Unlock object" : "Lock object"}
>
{isLocked ? <Unlock className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
</Button>
</Card>
)
}
export default LockObject

View file

@ -1,55 +1,80 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext'; import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useContext, useEffect, useState } from 'react' import { useContext, useEffect, useRef, useState } from 'react'
import { Card, } from '@/components/ui/card'; import { Card, } from '@/components/ui/card';
import CanvasContext from '@/components/Context/canvasContext/CanvasContext'; import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { fabric } from 'fabric';
const SelectObjectFromGroup = () => { const SelectObjectFromGroup = () => {
const [groupObjects, setGroupObjects] = useState([]); const [groupObjects, setGroupObjects] = useState([])
const [selectedObject, setSelectedObject] = useState(null); const [selectedObject, setSelectedObject] = useState(null)
const { setActiveObject } = useContext(ActiveObjectContext); const { setActiveObject } = useContext(ActiveObjectContext)
const { canvas } = useContext(CanvasContext); const { canvas } = useContext(CanvasContext)
const previewCanvasRef = useRef(null)
const activeObject = canvas?.getActiveObject(); const activeObject = canvas?.getActiveObject()
// Update group objects when the active object changes
useEffect(() => { useEffect(() => {
if (activeObject?.type === 'group') { if (activeObject?.type === 'group') {
setGroupObjects(activeObject._objects || []); setGroupObjects(activeObject._objects || [])
setSelectedObject(null); // Reset selection when group changes setSelectedObject(null)
} else { } else {
setGroupObjects([]); setGroupObjects([])
setSelectedObject(null); setSelectedObject(null)
} }
}, [activeObject]); }, [activeObject])
useEffect(() => {
if (selectedObject && previewCanvasRef.current) {
const previewCanvas = new fabric.StaticCanvas(previewCanvasRef.current);
previewCanvas.setDimensions({
width: 100,
height: 100
})
// Clone the active object to create a true deep copy
selectedObject.clone((clonedObject) => {
// Add the cloned object to the canvas
clonedObject.set({
left: 50,
top: 50,
originX: 'center',
originY: 'center',
selectable: false,
evented: false,
})
previewCanvas.add(clonedObject);
previewCanvas.renderAll();
});
return () => {
previewCanvas.dispose()
}
}
}, [selectedObject])
// Handle object selection from dropdown
const handleSelectObject = (value) => { const handleSelectObject = (value) => {
const selected = groupObjects[parseInt(value)]; const selected = groupObjects[parseInt(value)]
setSelectedObject(selected); setSelectedObject(selected)
setActiveObject(selected); setActiveObject(selected)
};
// Don't render if there's no active group
if (!activeObject || activeObject.type !== 'group') {
return null;
} }
console.log(selectedObject); if (!activeObject || activeObject.type !== 'group') {
console.log(selectedObject?.group); return null
console.log(selectedObject?.fill); }
return ( return (
<div> <div>
<Card className="p-2"> <Card className="p-4">
<h2 className='font-bold mb-2'>Group Objects</h2> <h2 className='font-bold mb-4'>Group Objects</h2>
<div className='flex flex-col items-center justify-center'> <div className='flex flex-col items-center justify-center space-y-4'>
<Select <Select
onValueChange={handleSelectObject} onValueChange={handleSelectObject}
value={selectedObject ? groupObjects.indexOf(selectedObject).toString() : undefined} value={selectedObject ? groupObjects.indexOf(selectedObject).toString() : undefined}
> >
<SelectTrigger className="w-[200px]"> <SelectTrigger className="w-full max-w-xs">
<SelectValue placeholder="Select object" /> <SelectValue placeholder="Select object" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -60,37 +85,16 @@ const SelectObjectFromGroup = () => {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div>
{selectedObject && ( {selectedObject && (
<div className='flex items-center justify-center bg-muted rounded-md my-2 mx-auto'> <div className='w-[100px] h-[100px] bg-muted rounded-md overflow-hidden border border-gray-300'>
<svg viewBox='0 0 24 24'> <canvas ref={previewCanvasRef} width={100} height={100} />
{selectedObject?.fill?.coords && selectedObject?.fill?.colorStops && ( </div>
<defs> )}
<linearGradient id="gradient1" </div>
x1={selectedObject.fill.coords.x1 || 0}
y1={selectedObject.fill.coords.y1 || 0}
x2={selectedObject.fill.coords.x2 || 1}
y2={selectedObject.fill.coords.y2 || 1}>
{selectedObject.fill.colorStops.map((stop, index) => (
<stop key={index}
offset={`${(stop.offset || 0) * 100}%`}
stopColor={stop.color || 'black'} />
))}
</linearGradient>
</defs>
)}
<path
d={selectedObject?.d}
fill={selectedObject?.fill?.coords ? 'url(#gradient1)' : selectedObject?.fill}
stroke={selectedObject?.stroke}
></path>
</svg>
</div>
)}
</Card> </Card>
<Separator className="my-2" /> <Separator className="my-4" />
</div> </div>
); )
}; }
export default SelectObjectFromGroup; export default SelectObjectFromGroup;

View file

@ -22,7 +22,7 @@ const ShadowCustomization = () => {
useEffect(() => { useEffect(() => {
if (activeObject) { if (activeObject) {
setShadowColor(activeObject?.shadow?.color || "#ffffff"); setShadowColor(activeObject?.shadow?.color || "#db1d62");
setOffsetX(activeObject?.shadow?.offsetX || 5); setOffsetX(activeObject?.shadow?.offsetX || 5);
setOffsetY(activeObject?.shadow?.offsetY || 5); setOffsetY(activeObject?.shadow?.offsetY || 5);
setBlur(activeObject?.shadow?.blur || 0.5); setBlur(activeObject?.shadow?.blur || 0.5);

View file

@ -16,6 +16,7 @@ import PositionCustomization from './Customization/PositionCustomization';
import CollapsibleComponent from './Customization/CollapsibleComponent'; import CollapsibleComponent from './Customization/CollapsibleComponent';
import ImageCustomization from './Customization/ImageCustomization'; import ImageCustomization from './Customization/ImageCustomization';
import SelectObjectFromGroup from './Customization/SelectObjectFromGroup'; import SelectObjectFromGroup from './Customization/SelectObjectFromGroup';
import LockObject from './Customization/LockObject';
const CustomizeShape = () => { const CustomizeShape = () => {
const { canvas } = useContext(CanvasContext); const { canvas } = useContext(CanvasContext);
@ -29,11 +30,14 @@ const CustomizeShape = () => {
return <p className='text-sm font-semibold'>No active object found</p>; return <p className='text-sm font-semibold'>No active object found</p>;
} }
console.log(activeObject?.type); // console.log(activeObject?.type);
return ( return (
<div className='p-1'> <div className='p-1'>
<LockObject />
<Separator className="my-2" />
<SelectObjectFromGroup /> <SelectObjectFromGroup />
{/* Apply fill and background color */} {/* Apply fill and background color */}

View file

@ -6,12 +6,15 @@ 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";
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();
// 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, Icon]) =>
!name.includes("Icon") && Icon?.$$typeof !name.includes("Icon") && Icon?.$$typeof
@ -32,7 +35,11 @@ const AllIconsPage = () => {
const svgString = new XMLSerializer().serializeToString(e.target); const svgString = new XMLSerializer().serializeToString(e.target);
handleAddIcon(svgString); handleAddIcon(svgString);
} else { } else {
window.alert('The target is a path element! Select the full icon.'); toast({
title: "Invalid Choice",
description: "The target is a path element! Select the full icon.",
variant: "destructive",
})
} }
}; };
@ -66,8 +73,8 @@ const AllIconsPage = () => {
const iconGroup = fabric.util.groupSVGElements(objects, options); const iconGroup = fabric.util.groupSVGElements(objects, options);
iconGroup.set({ iconGroup.set({
left: 100, left: canvas.width / 2,
top: 100, top: canvas.height / 2,
originX: 'center', originX: 'center',
originY: 'center', originY: 'center',
scaleX: 6, scaleX: 6,

View file

@ -0,0 +1,85 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva } from "class-variance-authority";
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[1000] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props} />
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
return (
(<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props} />)
);
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props} />
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props} />
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction };

View file

@ -0,0 +1,33 @@
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
(<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
(<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>)
);
})}
<ToastViewport />
</ToastProvider>)
);
}

155
src/hooks/use-toast.js Normal file
View file

@ -0,0 +1,155 @@
"use client";
// Inspired by react-hot-toast library
import * as React from "react"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST"
}
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString();
}
const toastTimeouts = new Map()
const addToRemoveQueue = (toastId) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state, action) => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t),
};
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
}
const listeners = []
let memoryState = { toasts: [] }
function dispatch(action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
function toast({
...props
}) {
const id = genId()
const update = (props) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
};
}, [state])
return {
...state,
toast,
dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast }