image section
This commit is contained in:
Generated
+48
-3
@@ -17,6 +17,7 @@
|
||||
"konva": "^10.0.9",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-i18next": "^16.3.0",
|
||||
"react-konva": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
@@ -2238,6 +2239,15 @@
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/attr-accept": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
||||
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-react-compiler": {
|
||||
"version": "19.1.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.3.tgz",
|
||||
@@ -2827,6 +2837,18 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
|
||||
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -3118,9 +3140,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3815,6 +3837,23 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone": {
|
||||
"version": "14.3.8",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
|
||||
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"attr-accept": "^2.2.4",
|
||||
"file-selector": "^2.1.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.0.tgz",
|
||||
@@ -4209,6 +4248,12 @@
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"konva": "^10.0.9",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-i18next": "^16.3.0",
|
||||
"react-konva": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { CloseCircle, DocumentUpload, Gallery } from 'iconsax-react'
|
||||
import { type FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { clx } from '../helpers/utils';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
onChange: (file: File[]) => void;
|
||||
isMultiple?: boolean;
|
||||
isFile?: boolean,
|
||||
preview?: string[],
|
||||
onChangePreview?: (preview: string[]) => void,
|
||||
getCover?: (url: string) => void,
|
||||
coverUrl?: string
|
||||
}
|
||||
|
||||
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [perviews, setPerviews] = useState<string[]>(props.preview ? props.preview : [])
|
||||
const [isFill, setIsFill] = useState<boolean>(false)
|
||||
const [cover, setCover] = useState<string>(props.coverUrl ? props.coverUrl : '')
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (props.isMultiple) {
|
||||
const array = [...files, ...acceptedFiles]
|
||||
setFiles(array)
|
||||
props.onChange(array)
|
||||
} else {
|
||||
setFiles([acceptedFiles[0]])
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files])
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
const array = [...files]
|
||||
array.splice(index, 1)
|
||||
setFiles(array)
|
||||
props.onChange(array)
|
||||
}
|
||||
|
||||
const handleDeletePreview = (index: number) => {
|
||||
const array = [...perviews];
|
||||
array.splice(index, 1);
|
||||
setPerviews(array);
|
||||
if (props.onChangePreview) {
|
||||
props.onChangePreview(array);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (props.preview && props.preview.length > 0 && !isFill) {
|
||||
setPerviews(props.preview)
|
||||
console.log('preview', props.preview);
|
||||
|
||||
setIsFill(true)
|
||||
}
|
||||
|
||||
}, [props.preview, isFill])
|
||||
|
||||
useEffect(() => {
|
||||
setCover(props.coverUrl ? props.coverUrl : '')
|
||||
}, [props.coverUrl])
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div {...getRootProps()} className='w-full py-8 border border-dashed border-description flex flex-col justify-center items-center gap-4 rounded-2xl'>
|
||||
<input {...getInputProps()} />
|
||||
<Gallery
|
||||
className='size-8'
|
||||
color='#8C90A3'
|
||||
/>
|
||||
<div className='text-description text-xs'>
|
||||
{props.label}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<DocumentUpload
|
||||
className='size-4'
|
||||
color='black'
|
||||
/>
|
||||
<div className='text-xs'>آپلود</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
files.length > 0 || perviews.length > 0 ? (
|
||||
<div className='mt-4 flex gap-4 items-center'>
|
||||
{
|
||||
perviews && perviews.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
<div key={index} className='flex relative items-center gap-2'>
|
||||
<img onClick={() => {
|
||||
if (props.getCover) {
|
||||
setCover(item)
|
||||
props.getCover(item)
|
||||
}
|
||||
}} src={item}
|
||||
className={clx(
|
||||
'size-10 rounded-full object-cover',
|
||||
cover === item ? 'border-2 border-red-400' : ''
|
||||
)} />
|
||||
<div onClick={() => handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
|
||||
<CloseCircle className='size-4 ' color='red' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
files.map((file, index) => {
|
||||
if (props.isFile) {
|
||||
return (
|
||||
<div key={index} className='flex border p-2 rounded-lg items-center gap-2'>
|
||||
<div className='flex relative items-center gap-2'>
|
||||
<div className='text-xs'>{file.name}</div>
|
||||
<div className='absolute -left-4 -top-4 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
|
||||
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else
|
||||
return (
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
<div key={index} className='flex relative items-center gap-2'>
|
||||
<img src={URL.createObjectURL(file)} alt={file.name} className='size-10 rounded-full object-cover' />
|
||||
<div className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
|
||||
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UploadBoxDraggble
|
||||
@@ -85,6 +85,38 @@ const EditorCanvas = () => {
|
||||
setEditingCell(null);
|
||||
};
|
||||
|
||||
// Attach transformer to selected object when it changes
|
||||
useEffect(() => {
|
||||
if (!transformerRef.current || !selectedObjectId || !layerRef.current) return;
|
||||
|
||||
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
||||
if (!selectedObject) return;
|
||||
|
||||
// For grid objects, handle separately
|
||||
if (selectedObject.type === "grid") {
|
||||
const tableNode = layerRef.current.findOne(`#${selectedObjectId}`);
|
||||
if (tableNode && transformerRef.current) {
|
||||
transformerRef.current.nodes([tableNode]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For other objects (like images), find the node by id and attach transformer
|
||||
// Use a small delay to ensure the node is mounted (especially for images that need to load)
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!layerRef.current || !transformerRef.current) return;
|
||||
|
||||
const node = layerRef.current.findOne(`#${selectedObjectId}`);
|
||||
if (node && transformerRef.current) {
|
||||
transformerRef.current.nodes([node]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [selectedObjectId, objects, transformerRef, layerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transformerRef.current || !selectedObjectId) return;
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ const tools: Array<{ icon: (color: string, variant?: "Bold" | "Outline" | "Broke
|
||||
{ icon: (color, variant) => <MouseSquare size={20} color={color} variant={variant} />, tool: "select", label: "انتخاب" },
|
||||
{ icon: (color, variant) => <Shapes size={20} color={color} variant={variant} />, tool: "rectangle", label: "مستطیل" },
|
||||
{ icon: (color, variant) => <Text size={20} color={color} variant={variant} />, tool: "text", label: "متن" },
|
||||
{ icon: (color, variant) => <Minus size={20} color={color} variant={variant} />, tool: "line", label: "خط" },
|
||||
{ icon: (color, variant) => <Grid8 size={20} color={color} variant={variant} />, tool: "grid", label: "گرید" },
|
||||
{ icon: (color, variant) => <Sticker size={20} color={color} variant={variant} />, tool: "sticker", label: "استیکر" },
|
||||
{ icon: (color, variant) => <Gallery size={20} color={color} variant={variant} />, tool: "image", label: "تصویر" },
|
||||
{ icon: (color, variant) => <Link size={20} color={color} variant={variant} />, tool: "link", label: "لینک" },
|
||||
{ icon: (color, variant) => <DocumentUpload size={20} color={color} variant={variant} />, tool: "document", label: "سند" },
|
||||
{ icon: (color, variant) => <Element size={20} color={color} variant={variant} />, tool: "arrow", label: "پیکان" },
|
||||
{ icon: (color, variant) => <Gallery size={20} color={color} variant={variant} />, tool: "image", label: "تصویر" },
|
||||
{ icon: (color, variant) => <Minus size={20} color={color} variant={variant} />, tool: "line", label: "خط" },
|
||||
{ icon: (color, variant) => <VideoSquare size={20} color={color} variant={variant} />, tool: "video", label: "ویدیو" },
|
||||
{ icon: (color, variant) => <Link size={20} color={color} variant={variant} />, tool: "link", label: "لینک" },
|
||||
{ icon: (color, variant) => <Sticker size={20} color={color} variant={variant} />, tool: "sticker", label: "استیکر" },
|
||||
];
|
||||
|
||||
type ToolsBarProps = {
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
import { Gallery } from "iconsax-react";
|
||||
import Button from "@/components/Button";
|
||||
import { type FC, useState, useRef } from "react";
|
||||
import UploadBoxDraggble from "@/components/UploadBoxDraggble";
|
||||
|
||||
type ImageUploadProps = {
|
||||
onImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
const ImageUpload = ({ onImageUpload }: ImageUploadProps) => {
|
||||
const ImageUpload: FC<ImageUploadProps> = ({ onImageUpload }) => {
|
||||
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
|
||||
const previousFilesCountRef = useRef<number>(0);
|
||||
|
||||
const handleFileChange = (files: File[]) => {
|
||||
// Update ref to track current files count
|
||||
if (files.length > previousFilesCountRef.current) {
|
||||
// Process only new files
|
||||
const newFiles = files.slice(previousFilesCountRef.current);
|
||||
|
||||
newFiles.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const imageUrl = event.target?.result as string;
|
||||
setUploadedImages((prev) => [...prev, imageUrl]);
|
||||
|
||||
// Create a synthetic event for onImageUpload
|
||||
const syntheticEvent = {
|
||||
target: {
|
||||
files: [file],
|
||||
},
|
||||
} as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||
|
||||
onImageUpload(syntheticEvent);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Always update ref to track current files count
|
||||
previousFilesCountRef.current = files.length;
|
||||
};
|
||||
|
||||
const handlePreviewChange = (previews: string[]) => {
|
||||
setUploadedImages(previews);
|
||||
// Update ref when previews change (e.g., when files are deleted)
|
||||
previousFilesCountRef.current = previews.length;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm mb-2">آپلود تصویر</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={onImageUpload}
|
||||
className="hidden"
|
||||
id="image-upload"
|
||||
<div className="text-base font-bold mb-4">تصویر</div>
|
||||
<UploadBoxDraggble
|
||||
label="تصویر مورد نظر را آپلود کنید"
|
||||
onChange={handleFileChange}
|
||||
isMultiple={true}
|
||||
preview={uploadedImages}
|
||||
onChangePreview={handlePreviewChange}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => document.getElementById("image-upload")?.click()}
|
||||
className="w-full"
|
||||
>
|
||||
<Gallery size={20} />
|
||||
انتخاب تصویر
|
||||
</Button>
|
||||
{uploadedImages.length > 0 && (
|
||||
<div className="mt-4 text-description text-xs">
|
||||
تصویر های آپلود شده
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,17 +16,18 @@ const ImageObject = ({
|
||||
const shapeRef = useRef<Konva.Image>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||
if (isSelected && shapeRef.current && transformerRef.current && image) {
|
||||
transformerRef.current.nodes([shapeRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected, transformerRef]);
|
||||
}, [isSelected, transformerRef, image]);
|
||||
|
||||
if (!image) return null;
|
||||
|
||||
return (
|
||||
<KonvaImage
|
||||
ref={shapeRef}
|
||||
id={obj.id}
|
||||
x={obj.x}
|
||||
y={obj.y}
|
||||
image={image}
|
||||
|
||||
Reference in New Issue
Block a user