diff --git a/src/assets/images/colors.png b/src/assets/images/colors.png new file mode 100644 index 0000000..f6bc357 Binary files /dev/null and b/src/assets/images/colors.png differ diff --git a/src/pages/editor/Editor.tsx b/src/pages/editor/Editor.tsx index 5e364f6..ce18c58 100644 --- a/src/pages/editor/Editor.tsx +++ b/src/pages/editor/Editor.tsx @@ -22,17 +22,19 @@ const Editor: FC = () => { } }, []) const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false) - const { loadPages, pages } = useEditorStore() + const { loadPages, pages, documentSettings } = useEditorStore() const { data } = useGetCatalogById(id!) const { scheduleUpdate, isSaving } = useUpdateCatalog() useEffect(() => { if (data?.data && data?.data?.content) { const json = JSON.parse(data?.data?.content) - console.log(json); + if (!json) return - if (json) { + if (Array.isArray(json)) { loadPages(json) + } else if (json.pages) { + loadPages(json.pages, json.documentSettings) } } @@ -45,11 +47,11 @@ const Editor: FC = () => { scheduleUpdate({ id, - content: JSON.stringify(pages), + content: JSON.stringify({ pages, documentSettings }), }) // عمداً scheduleUpdate را در وابستگی‌ها نیاوردیم تا از لوپ ذخیره‌سازی جلوگیری شود // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id, pages]) + }, [id, pages, documentSettings]) return ( diff --git a/src/pages/editor/components/EditorCanvas.tsx b/src/pages/editor/components/EditorCanvas.tsx index c13d5be..9765f51 100644 --- a/src/pages/editor/components/EditorCanvas.tsx +++ b/src/pages/editor/components/EditorCanvas.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "react"; -import { Stage, Layer, Rect } from "react-konva"; +import { Stage, Layer, Rect, Image as KonvaImage } from "react-konva"; +import useImage from "use-image"; import Konva from "konva"; import { useEditorStore } from "../store/editorStore"; import { useDrawingHandlers } from "./canvas/useDrawingHandlers"; @@ -39,10 +40,38 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { setLayerRef, setGuides, zoom, + documentSettings, } = useEditorStore(); - const { transformerRef, handleStageMouseDown, handleStageMouseMove, handleStageMouseUp } = useDrawingHandlers(); + const bgColor = + documentSettings.backgroundType === 'color' + ? documentSettings.backgroundColor + : '#ffffff'; + + const [bgImage] = useImage( + documentSettings.backgroundType === 'image' + ? documentSettings.backgroundImageUrl + : '', + ); const stageSize = useStageSize(catalogSize); + const bgImageCrop = (() => { + if (!bgImage) return null; + + const containerRatio = stageSize.width / stageSize.height; + const imageRatio = bgImage.width / bgImage.height; + + if (imageRatio > containerRatio) { + const cropWidth = bgImage.height * containerRatio; + const cropX = (bgImage.width - cropWidth) / 2; + return { x: cropX, y: 0, width: cropWidth, height: bgImage.height }; + } + + const cropHeight = bgImage.width / containerRatio; + const cropY = (bgImage.height - cropHeight) / 2; + return { x: 0, y: cropY, width: bgImage.width, height: cropHeight }; + })(); + + const { transformerRef, handleStageMouseDown, handleStageMouseMove, handleStageMouseUp } = useDrawingHandlers(); useKeyboardMovement(); const { handleSelect, handleCellClick, handleCellDblClick } = useSelectionHandlers(); @@ -362,9 +391,20 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { y={0} width={stageSize.width} height={stageSize.height} - fill="#ffffff" + fill={bgColor} listening={false} /> + {documentSettings.backgroundType === 'image' && bgImage && ( + + )} void } +const TAB_LABELS: Record = { + pages: 'صفحات', + layers: 'لایه ها', + settings: 'تنظیمات', +} + const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => { const [activeTab, setActiveTab] = useState('pages') @@ -19,23 +26,23 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
+
) @@ -45,28 +52,34 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
+ +
@@ -80,12 +93,14 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {

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

- {activeTab === 'pages' ? : } + {activeTab === 'pages' && } + {activeTab === 'layers' && } + {activeTab === 'settings' && } diff --git a/src/pages/editor/components/SettingsPanel.tsx b/src/pages/editor/components/SettingsPanel.tsx new file mode 100644 index 0000000..48074b6 --- /dev/null +++ b/src/pages/editor/components/SettingsPanel.tsx @@ -0,0 +1,261 @@ +import { useEffect, useRef, useState } from 'react' +import { clx } from '@/helpers/utils' +import { useEditorStore } from '../store/editorStore' +import type { DisplayStyle, BackgroundType } from '../store/editorStore' +import UploadBoxDraggble from '@/components/UploadBoxDraggble' +import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData' +import ColorsImage from '@/assets/images/colors.png' + +const PRESET_COLORS = [ + '#a8edcf', + '#e91e8c', + '#ff9800', + '#6d8b8b', + '#111111', + '#e53935', + '#e0e0e0', +] + +const DISPLAY_STYLE_OPTIONS: { value: DisplayStyle; label: string }[] = [ + { value: 'single', label: 'تک صفحه ای' }, + { value: 'double', label: 'دو صفحه ای' }, +] + +const BACKGROUND_TYPE_OPTIONS: { value: BackgroundType; label: string }[] = [ + { value: 'color', label: 'رنگ' }, + { value: 'image', label: 'تصویر' }, +] + +const SettingsPanel = () => { + const { documentSettings, updateDocumentSettings } = useEditorStore() + const colorInputRef = useRef(null) + const { mutateAsync: uploadFile, isPending: imageLoading } = useSingleUpload() + const [uploadedPreviews, setUploadedPreviews] = useState( + documentSettings.backgroundImageUrl ? [documentSettings.backgroundImageUrl] : [] + ) + + const { + displayStyle, + autoPlay, + pageFlipSound, + leftToRightFlip, + backgroundType, + backgroundColor, + } = documentSettings + + useEffect(() => { + setUploadedPreviews( + documentSettings.backgroundImageUrl ? [documentSettings.backgroundImageUrl] : [] + ) + }, [documentSettings.backgroundImageUrl]) + + const handleImageFiles = async (files: File[]) => { + if (files.length === 0) return + const file = files[files.length - 1] + if (!file) return + + try { + const result = await uploadFile(file) + const imageUrl = result?.data?.url + if (!imageUrl) return + + updateDocumentSettings({ backgroundImageUrl: imageUrl }) + setUploadedPreviews([imageUrl]) + } catch { + // TODO: show error toast + } + } + + const handlePreviewChange = (previews: string[]) => { + setUploadedPreviews(previews) + if (previews.length === 0) { + updateDocumentSettings({ backgroundImageUrl: '' }) + } + } + + return ( +
+ + {/* ─── تنظیمات نمایش ─── */} +
+
+

تنظیمات نمایش

+
+ + {/* استایل نمایش */} +
+ +
+ + + + + + +
+
+ + {/* چک‌باکس‌ها */} +
+ updateDocumentSettings({ autoPlay: v })} + /> + updateDocumentSettings({ pageFlipSound: v })} + /> + updateDocumentSettings({ leftToRightFlip: v })} + /> +
+
+ + {/* ─── پس زمینه ─── */} +
+

پس زمینه

+ + {/* نوع پس‌زمینه */} +
+ {BACKGROUND_TYPE_OPTIONS.map((opt) => ( +
+
+ ) +} + +type CheckboxRowProps = { + label: string + checked: boolean + onChange: (value: boolean) => void +} + +const CheckboxRow = ({ label, checked, onChange }: CheckboxRowProps) => ( + +) + +export default SettingsPanel diff --git a/src/pages/editor/store/editorStore.ts b/src/pages/editor/store/editorStore.ts index 013891f..1884a95 100644 --- a/src/pages/editor/store/editorStore.ts +++ b/src/pages/editor/store/editorStore.ts @@ -18,6 +18,7 @@ import type { TableCell, TableData, ToolType, + DocumentSettings, } from "./editorStore.types"; const MAX_OBJECT_HISTORY = 50; @@ -38,6 +39,9 @@ export type { TableCell, TableData, ToolType, + DocumentSettings, + DisplayStyle, + BackgroundType, } from "./editorStore.types"; type EditorStoreType = { @@ -117,7 +121,7 @@ type EditorStoreType = { toggleObjectVisibility: (id: string) => void; groupObjects: (objectIds: string[]) => void; ungroupObjects: (groupId: string) => void; - loadPages: (pages: Page[]) => void; + loadPages: (pages: Page[], documentSettings?: Partial) => void; clipboardObjects: EditorObject[]; objectHistoryPast: EditorObject[][]; objectHistoryFuture: EditorObject[][]; @@ -128,6 +132,8 @@ type EditorStoreType = { redoObjects: () => void; bringObjectForward: (id: string) => void; sendObjectBackward: (id: string) => void; + documentSettings: DocumentSettings; + updateDocumentSettings: (updates: Partial) => void; }; export const useEditorStore = create((set, get) => { @@ -172,6 +178,19 @@ export const useEditorStore = create((set, get) => { }; return { + documentSettings: { + displayStyle: 'double', + autoPlay: false, + pageFlipSound: true, + leftToRightFlip: false, + backgroundType: 'color', + backgroundColor: '#ffffff', + backgroundImageUrl: '', + }, + updateDocumentSettings: (updates) => + set((state) => ({ + documentSettings: { ...state.documentSettings, ...updates }, + })), tool: "select", setTool: (tool) => set((state) => ({ @@ -957,7 +976,7 @@ export const useEditorStore = create((set, get) => { const state = get(); return state.objects.filter((obj) => obj.groupId === groupId); }, - loadPages: (pages) => { + loadPages: (pages, documentSettings) => { if (pages.length === 0) return; const normalizedPages = pages.map((page) => ({ ...page, @@ -966,6 +985,7 @@ export const useEditorStore = create((set, get) => { objects: ensureMissingGroupObjects(page.objects || []), })); const firstPage = normalizedPages[0]; + const currentSettings = get().documentSettings; set({ pages: normalizedPages, currentPageId: firstPage.id, @@ -975,6 +995,9 @@ export const useEditorStore = create((set, get) => { selectedObjectIds: [], objectHistoryPast: [], objectHistoryFuture: [], + ...(documentSettings && { + documentSettings: { ...currentSettings, ...documentSettings }, + }), }); }, }; diff --git a/src/pages/editor/store/editorStore.types.ts b/src/pages/editor/store/editorStore.types.ts index 001f8bc..5e7c0c6 100644 --- a/src/pages/editor/store/editorStore.types.ts +++ b/src/pages/editor/store/editorStore.types.ts @@ -81,3 +81,16 @@ export type Page = { objects: EditorObject[]; guides: PageGuide[]; }; + +export type DisplayStyle = 'single' | 'double'; +export type BackgroundType = 'color' | 'image'; + +export type DocumentSettings = { + displayStyle: DisplayStyle; + autoPlay: boolean; + pageFlipSound: boolean; + leftToRightFlip: boolean; + backgroundType: BackgroundType; + backgroundColor: string; + backgroundImageUrl: string; +};