From d1a55184df3720024e6af307fb1d397926e3fe6c Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 31 Dec 2025 11:21:09 +0330 Subject: [PATCH] layer panel with took left --- src/pages/editor/components/LayersPanel.tsx | 276 +++++--- src/pages/editor/store/editorStore.ts | 682 +++++++++++++------- 2 files changed, 634 insertions(+), 324 deletions(-) diff --git a/src/pages/editor/components/LayersPanel.tsx b/src/pages/editor/components/LayersPanel.tsx index 9ef52d2..0abcf58 100644 --- a/src/pages/editor/components/LayersPanel.tsx +++ b/src/pages/editor/components/LayersPanel.tsx @@ -1,6 +1,9 @@ -import { DocumentText, Layer, Eye, EyeSlash, Lock1, Lock, Trash, ArrowRight2, Text, Shapes } from 'iconsax-react' +import { DocumentText, Layer, Eye, Lock, Trash, ArrowRight2, Text, Shapes, Add, Copy } from 'iconsax-react' import { useEditorStore } from '../store/editorStore' import { clx } from '@/helpers/utils' +import { useState } from 'react' + +type TabType = 'pages' | 'layers' type LayersPanelProps = { isOpen: boolean @@ -8,7 +11,16 @@ type LayersPanelProps = { } const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => { + const [activeTab, setActiveTab] = useState('pages') const { objects, selectedObjectId, setSelectedObjectId, deleteObject } = useEditorStore() + const { + pages, + currentPageId, + setCurrentPage, + addPage, + duplicatePage, + deletePage, + } = useEditorStore() const getLayerIcon = (type: string) => { switch (type) { @@ -36,19 +48,39 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => { } } + const handleAddPage = () => { + addPage() + } + + const handleDuplicatePage = (pageId: string) => { + duplicatePage(pageId) + } + + const handleDeletePage = (pageId: string) => { + if (pages.length > 1) { + deletePage(pageId) + } + } + if (!isOpen) { return (
@@ -58,78 +90,172 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => { } return ( -
-
-
-
- -

لایه ها

-
-
+
+
+ + +
-
-
- {objects.length === 0 ? ( -
- لایه‌ای وجود ندارد -
- ) : ( - objects.map((obj) => { - const isSelected = selectedObjectId === obj.id - return ( -
setSelectedObjectId(obj.id)} - className={clx( - 'flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-colors', - isSelected ? 'bg-blue-50 border border-blue-200' : 'bg-gray-50 hover:bg-gray-100' - )} - > -
- {getLayerIcon(obj.type)} -
-
- {getLayerLabel(obj.type)} -
-
- - - -
-
- ) - }) - )} +
+
+
+
+ +

+ {activeTab === 'pages' ? 'صفحات' : 'لایه ها'} +

+
+ + {activeTab === 'pages' ? ( + <> +
+
+ {pages.length === 0 ? ( +
+ صفحه‌ای وجود ندارد +
+ ) : ( + pages.map((page) => { + const isSelected = currentPageId === page.id + return ( +
setCurrentPage(page.id)} + className={clx( + 'flex flex-col gap-2 p-3 rounded-lg cursor-pointer transition-colors', + isSelected + ? 'bg-blue-50 border border-blue-200' + : 'bg-gray-50 hover:bg-gray-100' + )} + > +
+ +
+
+ {page.name} +
+
+ ) + }) + )} +
+
+ +
+
+ + {currentPageId && ( + <> + + + + )} +
+
+ + ) : ( +
+
+ {objects.length === 0 ? ( +
+ لایه‌ای وجود ندارد +
+ ) : ( + objects.map((obj) => { + const isSelected = selectedObjectId === obj.id + return ( +
setSelectedObjectId(obj.id)} + className={clx( + 'flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-colors', + isSelected ? 'bg-blue-50 border border-blue-200' : 'bg-gray-50 hover:bg-gray-100' + )} + > +
+ {getLayerIcon(obj.type)} +
+
+ {getLayerLabel(obj.type)} +
+
+ + + +
+
+ ) + }) + )} +
+
+ )}
diff --git a/src/pages/editor/store/editorStore.ts b/src/pages/editor/store/editorStore.ts index 27728ea..2309a2d 100644 --- a/src/pages/editor/store/editorStore.ts +++ b/src/pages/editor/store/editorStore.ts @@ -61,6 +61,12 @@ export type EditorObject = { tableData?: TableData; }; +export type Page = { + id: string; + name: string; + objects: EditorObject[]; +}; + type EditorStoreType = { tool: ToolType; setTool: (tool: ToolType) => void; @@ -74,16 +80,38 @@ type EditorStoreType = { setSelectedTableId: (id: string | null) => void; selectedCellId: string | null; setSelectedCellId: (id: string | null) => void; - editingCell: { tableId: string; cellId: string; x: number; y: number; value: string } | null; - setEditingCell: (cell: { tableId: string; cellId: string; x: number; y: number; value: string } | null) => void; + editingCell: { + tableId: string; + cellId: string; + x: number; + y: number; + value: string; + } | null; + setEditingCell: ( + cell: { + tableId: string; + cellId: string; + x: number; + y: number; + value: string; + } | null + ) => void; addTable: (x: number, y: number, rows?: number, cols?: number) => void; addRow: (tableId: string) => void; addColumn: (tableId: string) => void; removeRow: (tableId: string) => void; removeColumn: (tableId: string) => void; updateCellValue: (tableId: string, cellId: string, text: string) => void; - changeCellBackground: (tableId: string, cellId: string, color: string) => void; - applyTableResize: (tableId: string, newWidth: number, newHeight: number) => void; + changeCellBackground: ( + tableId: string, + cellId: string, + color: string + ) => void; + applyTableResize: ( + tableId: string, + newWidth: number, + newHeight: number + ) => void; stageRef: React.RefObject | null; setStageRef: (ref: React.RefObject) => void; layerRef: React.RefObject | null; @@ -92,11 +120,23 @@ type EditorStoreType = { setZoom: (zoom: number) => void; zoomIn: () => void; zoomOut: () => void; + pages: Page[]; + currentPageId: string | null; + addPage: (name?: string) => void; + duplicatePage: (pageId: string) => void; + deletePage: (pageId: string) => void; + setCurrentPage: (pageId: string) => void; + updatePageName: (pageId: string, name: string) => void; }; const createTableCellId = (row: number, col: number) => `cell-${row}-${col}`; -const createInitialCells = (rows: number, cols: number, cellWidth: number, cellHeight: number): Record => { +const createInitialCells = ( + rows: number, + cols: number, + cellWidth: number, + cellHeight: number +): Record => { const cells: Record = {}; for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { @@ -113,257 +153,401 @@ const createInitialCells = (rows: number, cols: number, cellWidth: number, cellH return cells; }; -export const useEditorStore = create((set, get) => ({ - tool: "select", - setTool: (tool) => - set((state) => ({ - tool, - selectedObjectId: tool === "select" ? state.selectedObjectId : null, - })), +const createInitialPage = (name: string): Page => ({ + id: `page-${crypto.randomUUID()}`, + name, objects: [], - addObject: (object) => - set((state) => ({ objects: [...state.objects, object] })), - updateObject: (id, updates) => - set((state) => ({ - objects: state.objects.map((obj) => - obj.id === id ? { ...obj, ...updates } : obj - ), - })), - deleteObject: (id) => - set((state) => ({ - objects: state.objects.filter((obj) => obj.id !== id), - selectedObjectId: - state.selectedObjectId === id ? null : state.selectedObjectId, - selectedTableId: state.selectedTableId === id ? null : state.selectedTableId, - })), - selectedObjectId: null, - setSelectedObjectId: (id) => set({ selectedObjectId: id }), - selectedTableId: null, - setSelectedTableId: (id) => set({ selectedTableId: id }), - selectedCellId: null, - setSelectedCellId: (id) => set({ selectedCellId: id }), - editingCell: null, - setEditingCell: (cell) => set({ editingCell: cell }), - addTable: (x, y, rows = 4, cols = 5) => { - const tableId = `table-${crypto.randomUUID()}`; - const cellWidth = 100; - const cellHeight = 50; - const tableData: TableData = { - id: tableId, - rows, - cols, - cellWidth, - cellHeight, - cells: createInitialCells(rows, cols, cellWidth, cellHeight), - stroke: "#808080", - strokeWidth: 1, - }; - const tableObject: EditorObject = { - id: tableId, - type: "grid", - x, - y, - width: cols * cellWidth, - height: rows * cellHeight, - tableData, - }; - get().addObject(tableObject); - get().setSelectedObjectId(tableId); - get().setSelectedTableId(tableId); - }, - addRow: (tableId) => { - const state = get(); - const obj = state.objects.find((o) => o.id === tableId); - if (!obj || !obj.tableData) return; +}); - const { tableData } = obj; - const newRow = tableData.rows; - const newCells = { ...tableData.cells }; - - for (let col = 0; col < tableData.cols; col++) { - const cellId = createTableCellId(newRow, col); - newCells[cellId] = { - id: cellId, - text: "", - background: "#f5f5f5", - width: tableData.cellWidth, - height: tableData.cellHeight, +export const useEditorStore = create((set, get) => { + const initialPage = createInitialPage("جلد"); + return { + tool: "select", + setTool: (tool) => + set((state) => ({ + tool, + selectedObjectId: tool === "select" ? state.selectedObjectId : null, + })), + objects: [], + addObject: (object) => { + const state = get(); + if (state.currentPageId) { + const currentPage = state.pages.find( + (p) => p.id === state.currentPageId + ); + if (currentPage) { + set((state) => ({ + objects: [...state.objects, object], + pages: state.pages.map((p) => + p.id === state.currentPageId + ? { ...p, objects: [...p.objects, object] } + : p + ), + })); + return; + } + } + set((state) => ({ objects: [...state.objects, object] })); + }, + updateObject: (id, updates) => { + const state = get(); + if (state.currentPageId) { + set((state) => ({ + objects: state.objects.map((obj) => + obj.id === id ? { ...obj, ...updates } : obj + ), + pages: state.pages.map((p) => + p.id === state.currentPageId + ? { + ...p, + objects: p.objects.map((obj) => + obj.id === id ? { ...obj, ...updates } : obj + ), + } + : p + ), + })); + return; + } + set((state) => ({ + objects: state.objects.map((obj) => + obj.id === id ? { ...obj, ...updates } : obj + ), + })); + }, + deleteObject: (id) => { + const state = get(); + if (state.currentPageId) { + set((state) => ({ + objects: state.objects.filter((obj) => obj.id !== id), + pages: state.pages.map((p) => + p.id === state.currentPageId + ? { ...p, objects: p.objects.filter((obj) => obj.id !== id) } + : p + ), + selectedObjectId: + state.selectedObjectId === id ? null : state.selectedObjectId, + selectedTableId: + state.selectedTableId === id ? null : state.selectedTableId, + })); + return; + } + set((state) => ({ + objects: state.objects.filter((obj) => obj.id !== id), + selectedObjectId: + state.selectedObjectId === id ? null : state.selectedObjectId, + selectedTableId: + state.selectedTableId === id ? null : state.selectedTableId, + })); + }, + selectedObjectId: null, + setSelectedObjectId: (id) => set({ selectedObjectId: id }), + selectedTableId: null, + setSelectedTableId: (id) => set({ selectedTableId: id }), + selectedCellId: null, + setSelectedCellId: (id) => set({ selectedCellId: id }), + editingCell: null, + setEditingCell: (cell) => set({ editingCell: cell }), + addTable: (x, y, rows = 4, cols = 5) => { + const tableId = `table-${crypto.randomUUID()}`; + const cellWidth = 100; + const cellHeight = 50; + const tableData: TableData = { + id: tableId, + rows, + cols, + cellWidth, + cellHeight, + cells: createInitialCells(rows, cols, cellWidth, cellHeight), + stroke: "#808080", + strokeWidth: 1, }; - } - - state.updateObject(tableId, { - tableData: { - ...tableData, - rows: tableData.rows + 1, - cells: newCells, - }, - height: (tableData.rows + 1) * tableData.cellHeight, - }); - }, - addColumn: (tableId) => { - const state = get(); - const obj = state.objects.find((o) => o.id === tableId); - if (!obj || !obj.tableData) return; - - const { tableData } = obj; - const newCol = tableData.cols; - const newCells = { ...tableData.cells }; - - for (let row = 0; row < tableData.rows; row++) { - const cellId = createTableCellId(row, newCol); - newCells[cellId] = { - id: cellId, - text: "", - background: "#f5f5f5", - width: tableData.cellWidth, - height: tableData.cellHeight, + const tableObject: EditorObject = { + id: tableId, + type: "grid", + x, + y, + width: cols * cellWidth, + height: rows * cellHeight, + tableData, }; - } + get().addObject(tableObject); + get().setSelectedObjectId(tableId); + get().setSelectedTableId(tableId); + }, + addRow: (tableId) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; - state.updateObject(tableId, { - tableData: { - ...tableData, - cols: tableData.cols + 1, - cells: newCells, - }, - width: (tableData.cols + 1) * tableData.cellWidth, - }); - }, - removeRow: (tableId) => { - const state = get(); - const obj = state.objects.find((o) => o.id === tableId); - if (!obj || !obj.tableData) return; + const { tableData } = obj; + const newRow = tableData.rows; + const newCells = { ...tableData.cells }; - const { tableData } = obj; - if (tableData.rows <= 1) return; - - const newCells = { ...tableData.cells }; - for (let col = 0; col < tableData.cols; col++) { - const cellId = createTableCellId(tableData.rows - 1, col); - delete newCells[cellId]; - } - - state.updateObject(tableId, { - tableData: { - ...tableData, - rows: tableData.rows - 1, - cells: newCells, - }, - height: (tableData.rows - 1) * tableData.cellHeight, - }); - }, - removeColumn: (tableId) => { - const state = get(); - const obj = state.objects.find((o) => o.id === tableId); - if (!obj || !obj.tableData) return; - - const { tableData } = obj; - if (tableData.cols <= 1) return; - - const newCells = { ...tableData.cells }; - for (let row = 0; row < tableData.rows; row++) { - const cellId = createTableCellId(row, tableData.cols - 1); - delete newCells[cellId]; - } - - state.updateObject(tableId, { - tableData: { - ...tableData, - cols: tableData.cols - 1, - cells: newCells, - }, - width: (tableData.cols - 1) * tableData.cellWidth, - }); - }, - updateCellValue: (tableId, cellId, text) => { - const state = get(); - const obj = state.objects.find((o) => o.id === tableId); - if (!obj || !obj.tableData) return; - - const { tableData } = obj; - const updatedCells = { - ...tableData.cells, - [cellId]: { - ...tableData.cells[cellId], - text, - }, - }; - - state.updateObject(tableId, { - tableData: { - ...tableData, - cells: updatedCells, - }, - }); - }, - changeCellBackground: (tableId, cellId, color) => { - const state = get(); - const obj = state.objects.find((o) => o.id === tableId); - if (!obj || !obj.tableData) return; - - const { tableData } = obj; - const updatedCells = { - ...tableData.cells, - [cellId]: { - ...tableData.cells[cellId], - background: color, - }, - }; - - state.updateObject(tableId, { - tableData: { - ...tableData, - cells: updatedCells, - }, - }); - }, - applyTableResize: (tableId, newWidth, newHeight) => { - const state = get(); - const obj = state.objects.find((o) => o.id === tableId); - if (!obj || !obj.tableData) return; - - const { tableData } = obj; - const newCellWidth = newWidth / tableData.cols; - const newCellHeight = newHeight / tableData.rows; - - const updatedCells: Record = {}; - for (let row = 0; row < tableData.rows; row++) { for (let col = 0; col < tableData.cols; col++) { - const cellId = createTableCellId(row, col); - const oldCell = tableData.cells[cellId]; - updatedCells[cellId] = { - ...oldCell, - width: newCellWidth, - height: newCellHeight, + const cellId = createTableCellId(newRow, col); + newCells[cellId] = { + id: cellId, + text: "", + background: "#f5f5f5", + width: tableData.cellWidth, + height: tableData.cellHeight, }; } - } - state.updateObject(tableId, { - width: newWidth, - height: newHeight, - tableData: { - ...tableData, - cellWidth: newCellWidth, - cellHeight: newCellHeight, - cells: updatedCells, - }, - }); - }, - stageRef: null, - setStageRef: (ref) => set({ stageRef: ref }), - layerRef: null, - setLayerRef: (ref) => set({ layerRef: ref }), - zoom: 1, - setZoom: (zoom) => set({ zoom }), - zoomIn: () => { - const state = get(); - const newZoom = Math.min(state.zoom + 0.1, 2); - set({ zoom: newZoom }); - }, - zoomOut: () => { - const state = get(); - const newZoom = Math.max(state.zoom - 0.1, 0.5); - set({ zoom: newZoom }); - }, -})); + state.updateObject(tableId, { + tableData: { + ...tableData, + rows: tableData.rows + 1, + cells: newCells, + }, + height: (tableData.rows + 1) * tableData.cellHeight, + }); + }, + addColumn: (tableId) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + const newCol = tableData.cols; + const newCells = { ...tableData.cells }; + + for (let row = 0; row < tableData.rows; row++) { + const cellId = createTableCellId(row, newCol); + newCells[cellId] = { + id: cellId, + text: "", + background: "#f5f5f5", + width: tableData.cellWidth, + height: tableData.cellHeight, + }; + } + + state.updateObject(tableId, { + tableData: { + ...tableData, + cols: tableData.cols + 1, + cells: newCells, + }, + width: (tableData.cols + 1) * tableData.cellWidth, + }); + }, + removeRow: (tableId) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + if (tableData.rows <= 1) return; + + const newCells = { ...tableData.cells }; + for (let col = 0; col < tableData.cols; col++) { + const cellId = createTableCellId(tableData.rows - 1, col); + delete newCells[cellId]; + } + + state.updateObject(tableId, { + tableData: { + ...tableData, + rows: tableData.rows - 1, + cells: newCells, + }, + height: (tableData.rows - 1) * tableData.cellHeight, + }); + }, + removeColumn: (tableId) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + if (tableData.cols <= 1) return; + + const newCells = { ...tableData.cells }; + for (let row = 0; row < tableData.rows; row++) { + const cellId = createTableCellId(row, tableData.cols - 1); + delete newCells[cellId]; + } + + state.updateObject(tableId, { + tableData: { + ...tableData, + cols: tableData.cols - 1, + cells: newCells, + }, + width: (tableData.cols - 1) * tableData.cellWidth, + }); + }, + updateCellValue: (tableId, cellId, text) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + const updatedCells = { + ...tableData.cells, + [cellId]: { + ...tableData.cells[cellId], + text, + }, + }; + + state.updateObject(tableId, { + tableData: { + ...tableData, + cells: updatedCells, + }, + }); + }, + changeCellBackground: (tableId, cellId, color) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + const updatedCells = { + ...tableData.cells, + [cellId]: { + ...tableData.cells[cellId], + background: color, + }, + }; + + state.updateObject(tableId, { + tableData: { + ...tableData, + cells: updatedCells, + }, + }); + }, + applyTableResize: (tableId, newWidth, newHeight) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + const newCellWidth = newWidth / tableData.cols; + const newCellHeight = newHeight / tableData.rows; + + const updatedCells: Record = {}; + for (let row = 0; row < tableData.rows; row++) { + for (let col = 0; col < tableData.cols; col++) { + const cellId = createTableCellId(row, col); + const oldCell = tableData.cells[cellId]; + updatedCells[cellId] = { + ...oldCell, + width: newCellWidth, + height: newCellHeight, + }; + } + } + + state.updateObject(tableId, { + width: newWidth, + height: newHeight, + tableData: { + ...tableData, + cellWidth: newCellWidth, + cellHeight: newCellHeight, + cells: updatedCells, + }, + }); + }, + stageRef: null, + setStageRef: (ref) => set({ stageRef: ref }), + layerRef: null, + setLayerRef: (ref) => set({ layerRef: ref }), + zoom: 1, + setZoom: (zoom) => set({ zoom }), + zoomIn: () => { + const state = get(); + const newZoom = Math.min(state.zoom + 0.1, 2); + set({ zoom: newZoom }); + }, + zoomOut: () => { + const state = get(); + const newZoom = Math.max(state.zoom - 0.1, 0.5); + set({ zoom: newZoom }); + }, + pages: [initialPage], + currentPageId: initialPage.id, + addPage: (name) => { + const state = get(); + const pageNumber = state.pages.length + 1; + const newPage = createInitialPage(name || `${pageNumber}`); + set((state) => ({ + pages: [...state.pages, newPage], + currentPageId: newPage.id, + objects: [], + selectedObjectId: null, + })); + }, + duplicatePage: (pageId) => { + const state = get(); + const pageToDuplicate = state.pages.find((p) => p.id === pageId); + if (!pageToDuplicate) return; + + const duplicatedObjects = pageToDuplicate.objects.map((obj) => ({ + ...obj, + id: `${obj.id}-${crypto.randomUUID()}`, + })); + + const newPage: Page = { + id: `page-${crypto.randomUUID()}`, + name: `${pageToDuplicate.name} (کپی)`, + objects: duplicatedObjects, + }; + + set((state) => ({ + pages: [...state.pages, newPage], + })); + }, + deletePage: (pageId) => { + const state = get(); + if (state.pages.length <= 1) return; + + const newPages = state.pages.filter((p) => p.id !== pageId); + const newCurrentPageId = + state.currentPageId === pageId + ? newPages[0]?.id || null + : state.currentPageId; + + const newCurrentPage = newPages.find((p) => p.id === newCurrentPageId); + + set({ + pages: newPages, + currentPageId: newCurrentPageId, + objects: newCurrentPage?.objects || [], + selectedObjectId: null, + }); + }, + setCurrentPage: (pageId) => { + const state = get(); + const targetPage = state.pages.find((p) => p.id === pageId); + if (!targetPage) return; + + // Save current objects to current page before switching + const updatedPages = state.currentPageId + ? state.pages.map((p) => + p.id === state.currentPageId ? { ...p, objects: state.objects } : p + ) + : state.pages; + + const targetPageFromUpdated = updatedPages.find((p) => p.id === pageId); + if (!targetPageFromUpdated) return; + + set({ + pages: updatedPages, + currentPageId: pageId, + objects: targetPageFromUpdated.objects, + selectedObjectId: null, + }); + }, + updatePageName: (pageId, name) => { + set((state) => ({ + pages: state.pages.map((p) => (p.id === pageId ? { ...p, name } : p)), + })); + }, + }; +});