From 26d3bd26abe1656cd44f423ab1732804764ea702 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Thu, 4 Dec 2025 09:34:04 +0330 Subject: [PATCH] image section --- package-lock.json | 51 +++++- package.json | 1 + src/components/UploadBoxDraggble.tsx | 151 ++++++++++++++++++ src/pages/editor/components/EditorCanvas.tsx | 32 ++++ .../editor/components/sidebar/ToolsBar.tsx | 8 +- .../sidebar/instructions/ImageUpload.tsx | 71 +++++--- .../editor/components/tools/ImageObject.tsx | 5 +- 7 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 src/components/UploadBoxDraggble.tsx diff --git a/package-lock.json b/package-lock.json index 4945747..8bbfdff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 0496e5d..a0df1bf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/UploadBoxDraggble.tsx b/src/components/UploadBoxDraggble.tsx new file mode 100644 index 0000000..97afd50 --- /dev/null +++ b/src/components/UploadBoxDraggble.tsx @@ -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) => { + + const [files, setFiles] = useState([]) + const [perviews, setPerviews] = useState(props.preview ? props.preview : []) + const [isFill, setIsFill] = useState(false) + const [cover, setCover] = useState(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 ( +
+
+ + +
+ {props.label} +
+ +
+ +
آپلود
+
+
+ + { + files.length > 0 || perviews.length > 0 ? ( +
+ { + perviews && perviews.map((item, index) => { + return ( +
+
+ { + 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' : '' + )} /> +
handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'> + +
+
+
+ ) + }) + } + { + files.map((file, index) => { + if (props.isFile) { + return ( +
+
+
{file.name}
+
+ handleDelete(index)} className='size-4 ' color='red' /> +
+
+
+ ) + } + else + return ( +
+
+ {file.name} +
+ handleDelete(index)} className='size-4 ' color='red' /> +
+
+
+ ) + }) + } +
+ ) : null + } + +
+ ) +} + +export default UploadBoxDraggble \ No newline at end of file diff --git a/src/pages/editor/components/EditorCanvas.tsx b/src/pages/editor/components/EditorCanvas.tsx index f096963..e1db2d8 100644 --- a/src/pages/editor/components/EditorCanvas.tsx +++ b/src/pages/editor/components/EditorCanvas.tsx @@ -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; diff --git a/src/pages/editor/components/sidebar/ToolsBar.tsx b/src/pages/editor/components/sidebar/ToolsBar.tsx index 0bb87c2..023d056 100644 --- a/src/pages/editor/components/sidebar/ToolsBar.tsx +++ b/src/pages/editor/components/sidebar/ToolsBar.tsx @@ -18,14 +18,14 @@ const tools: Array<{ icon: (color: string, variant?: "Bold" | "Outline" | "Broke { icon: (color, variant) => , tool: "select", label: "انتخاب" }, { icon: (color, variant) => , tool: "rectangle", label: "مستطیل" }, { icon: (color, variant) => , tool: "text", label: "متن" }, - { icon: (color, variant) => , tool: "line", label: "خط" }, { icon: (color, variant) => , tool: "grid", label: "گرید" }, - { icon: (color, variant) => , tool: "sticker", label: "استیکر" }, + { icon: (color, variant) => , tool: "image", label: "تصویر" }, + { icon: (color, variant) => , tool: "link", label: "لینک" }, { icon: (color, variant) => , tool: "document", label: "سند" }, { icon: (color, variant) => , tool: "arrow", label: "پیکان" }, - { icon: (color, variant) => , tool: "image", label: "تصویر" }, + { icon: (color, variant) => , tool: "line", label: "خط" }, { icon: (color, variant) => , tool: "video", label: "ویدیو" }, - { icon: (color, variant) => , tool: "link", label: "لینک" }, + { icon: (color, variant) => , tool: "sticker", label: "استیکر" }, ]; type ToolsBarProps = { diff --git a/src/pages/editor/components/sidebar/instructions/ImageUpload.tsx b/src/pages/editor/components/sidebar/instructions/ImageUpload.tsx index b5d2d1e..54193d3 100644 --- a/src/pages/editor/components/sidebar/instructions/ImageUpload.tsx +++ b/src/pages/editor/components/sidebar/instructions/ImageUpload.tsx @@ -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) => void; }; -const ImageUpload = ({ onImageUpload }: ImageUploadProps) => { +const ImageUpload: FC = ({ onImageUpload }) => { + const [uploadedImages, setUploadedImages] = useState([]); + const previousFilesCountRef = useRef(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; + + 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 (
- - تصویر
+ - + {uploadedImages.length > 0 && ( +
+ تصویر های آپلود شده +
+ )} ); }; diff --git a/src/pages/editor/components/tools/ImageObject.tsx b/src/pages/editor/components/tools/ImageObject.tsx index dd36b41..514d39a 100644 --- a/src/pages/editor/components/tools/ImageObject.tsx +++ b/src/pages/editor/components/tools/ImageObject.tsx @@ -16,17 +16,18 @@ const ImageObject = ({ const shapeRef = useRef(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 (