add setting

This commit is contained in:
hamid zarghami
2026-05-06 12:33:23 +03:30
parent e15dc76468
commit 192036e519
7 changed files with 384 additions and 30 deletions
+43 -3
View File
@@ -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 && (
<KonvaImage
image={bgImage}
x={0}
y={0}
width={stageSize.width}
height={stageSize.height}
crop={bgImageCrop ?? undefined}
listening={false}
/>
)}
</Layer>
<ObjectsLayer
+35 -20
View File
@@ -1,16 +1,23 @@
import { Layer, Book1, ArrowLeft2 } from 'iconsax-react'
import { Layer, Book1, ArrowLeft2, Setting2 } from 'iconsax-react'
import { clx } from '@/helpers/utils'
import { useState } from 'react'
import PagesPanel from './PagesPanel'
import LayersList from './LayersList'
import SettingsPanel from './SettingsPanel'
type TabType = 'pages' | 'layers'
type TabType = 'pages' | 'layers' | 'settings'
type LayersPanelProps = {
isOpen: boolean
setIsOpen: (isOpen: boolean) => void
}
const TAB_LABELS: Record<TabType, string> = {
pages: 'صفحات',
layers: 'لایه ها',
settings: 'تنظیمات',
}
const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
const [activeTab, setActiveTab] = useState<TabType>('pages')
@@ -19,23 +26,23 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
<div className="fixed left-4 top-[100px] bottom-4 z-30 flex flex-col">
<div className="bg-white rounded-t-[32px] rounded-b-[32px] w-16 h-full flex flex-col items-center gap-4 p-4">
<button
onClick={() => {
setActiveTab('pages')
setIsOpen(true)
}}
onClick={() => { setActiveTab('pages'); setIsOpen(true) }}
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-gray-200 transition-colors"
>
<Book1 size={24} color="black" />
</button>
<button
onClick={() => {
setActiveTab('layers')
setIsOpen(true)
}}
onClick={() => { setActiveTab('layers'); setIsOpen(true) }}
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-gray-200 transition-colors"
>
<Layer size={24} color="black" />
</button>
<button
onClick={() => { setActiveTab('settings'); setIsOpen(true) }}
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-gray-200 transition-colors"
>
<Setting2 size={24} color="black" />
</button>
</div>
</div>
)
@@ -45,28 +52,34 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
<div className="fixed left-4 top-[100px] bottom-4 z-30 flex flex-col">
<div className="bg-white rounded-l-4xl w-16 h-full flex flex-col items-center gap-4 p-4">
<button
onClick={() => {
setActiveTab('pages')
}}
onClick={() => setActiveTab('pages')}
className={clx(
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
activeTab === 'pages' ? 'bg-white' : 'hover:bg-gray-200'
activeTab === 'pages' ? 'bg-gray-100' : 'hover:bg-gray-200'
)}
>
<Book1 variant={activeTab === 'pages' ? 'Bold' : 'Outline'} size={24} color="black" />
</button>
<button
onClick={() => {
setActiveTab('layers')
}}
onClick={() => setActiveTab('layers')}
className={clx(
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
activeTab === 'layers' ? 'bg-white' : 'hover:bg-gray-200'
activeTab === 'layers' ? 'bg-gray-100' : 'hover:bg-gray-200'
)}
>
<Layer variant={activeTab === 'layers' ? 'Bold' : 'Outline'} size={24} color="black" />
</button>
<button
onClick={() => setActiveTab('settings')}
className={clx(
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
activeTab === 'settings' ? 'bg-gray-100' : 'hover:bg-gray-200'
)}
>
<Setting2 variant={activeTab === 'settings' ? 'Bold' : 'Outline'} size={24} color="black" />
</button>
</div>
<div className="fixed border-l border-border left-20 top-[100px] bottom-4 z-30 flex flex-col w-[232px]">
@@ -80,12 +93,14 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
<ArrowLeft2 size={20} color="black" />
</button>
<h2 className="text-lg font-medium">
{activeTab === 'pages' ? 'صفحات' : 'لایه ها'}
{TAB_LABELS[activeTab]}
</h2>
</div>
</div>
{activeTab === 'pages' ? <PagesPanel /> : <LayersList />}
{activeTab === 'pages' && <PagesPanel />}
{activeTab === 'layers' && <LayersList />}
{activeTab === 'settings' && <SettingsPanel />}
</div>
</div>
</div>
@@ -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<HTMLInputElement>(null)
const { mutateAsync: uploadFile, isPending: imageLoading } = useSingleUpload()
const [uploadedPreviews, setUploadedPreviews] = useState<string[]>(
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 (
<div className="flex flex-col gap-0 overflow-y-auto h-full text-right" dir="rtl">
{/* ─── تنظیمات نمایش ─── */}
<section className="p-4 border-b border-border">
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-gray-800">تنظیمات نمایش</h3>
</div>
{/* استایل نمایش */}
<div className="mb-4">
<label className="block text-xs text-gray-500 mb-2">استایل نمایش</label>
<div className="relative">
<select
value={displayStyle}
onChange={(e) =>
updateDocumentSettings({ displayStyle: e.target.value as DisplayStyle })
}
className="w-full appearance-none border border-border rounded-xl px-3 py-2.5 text-sm bg-white text-right pr-3 pl-8 cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-200"
>
{DISPLAY_STYLE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<span className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2 4l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
</div>
</div>
{/* چک‌باکس‌ها */}
<div className="flex flex-col gap-3">
<CheckboxRow
label="پخش خودکار"
checked={autoPlay}
onChange={(v) => updateDocumentSettings({ autoPlay: v })}
/>
<CheckboxRow
label="صدای برگ خوردن"
checked={pageFlipSound}
onChange={(v) => updateDocumentSettings({ pageFlipSound: v })}
/>
<CheckboxRow
label="چپ به راست ورق خوردن"
checked={leftToRightFlip}
onChange={(v) => updateDocumentSettings({ leftToRightFlip: v })}
/>
</div>
</section>
{/* ─── پس زمینه ─── */}
<section className="p-4">
<h3 className="text-sm font-semibold text-gray-800 mb-4">پس زمینه</h3>
{/* نوع پس‌زمینه */}
<div className="flex items-center gap-5 mb-4">
{BACKGROUND_TYPE_OPTIONS.map((opt) => (
<label key={opt.value} className="flex items-center gap-1.5 cursor-pointer select-none">
<span className="text-xs text-gray-600">{opt.label}</span>
<div
onClick={() => updateDocumentSettings({ backgroundType: opt.value })}
className={clx(
'w-4 h-4 rounded-full border-2 flex items-center justify-center transition-colors cursor-pointer',
backgroundType === opt.value
? 'border-gray-800 bg-gray-800'
: 'border-gray-300 bg-white'
)}
>
{backgroundType === opt.value && (
<div className="w-1.5 h-1.5 rounded-full bg-white" />
)}
</div>
</label>
))}
</div>
{/* ─── انتخاب رنگ ─── */}
{backgroundType === 'color' && (
<div>
<p className="text-xs text-gray-500 mb-3">انتخاب رنگ</p>
<div className="flex items-center gap-2 flex-wrap">
{PRESET_COLORS.map((color) => (
<button
key={color}
onClick={() => updateDocumentSettings({ backgroundColor: color })}
className={clx(
'w-9 h-9 rounded-xl border-2 transition-all',
backgroundColor === color
? 'border-gray-800 scale-110'
: 'border-transparent hover:scale-105'
)}
style={{ backgroundColor: color }}
title={color}
/>
))}
{/* Color Wheel */}
<button
onClick={() => colorInputRef.current?.click()}
className="w-9 h-9 rounded-xl border-2 border-transparent hover:scale-105 transition-all overflow-hidden relative"
title="رنگ دلخواه"
>
<img src={ColorsImage} alt="انتخاب رنگ" className="w-full h-full object-cover" />
<input
ref={colorInputRef}
type="color"
value={backgroundColor}
onChange={(e) => updateDocumentSettings({ backgroundColor: e.target.value })}
className="absolute inset-0 opacity-0 w-full h-full cursor-pointer"
/>
</button>
</div>
{/* نمایش رنگ انتخاب‌شده */}
<div className="mt-3 flex items-center gap-2">
<div
className="w-6 h-6 rounded-lg border border-border"
style={{ backgroundColor }}
/>
<span className="text-xs text-gray-500 font-mono">{backgroundColor}</span>
</div>
</div>
)}
{/* ─── آپلود تصویر ─── */}
{backgroundType === 'image' && (
<div>
{imageLoading ? (
<div className="flex flex-col items-center justify-center gap-3 py-8">
<span className="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-gray-800" />
<span className="text-xs text-gray-500">در حال آپلود...</span>
</div>
) : (
<UploadBoxDraggble
label="تصویر پس‌زمینه را آپلود کنید"
onChange={handleImageFiles}
isMultiple={false}
preview={uploadedPreviews}
onChangePreview={handlePreviewChange}
/>
)}
</div>
)}
</section>
</div>
)
}
type CheckboxRowProps = {
label: string
checked: boolean
onChange: (value: boolean) => void
}
const CheckboxRow = ({ label, checked, onChange }: CheckboxRowProps) => (
<label className="flex items-center justify-between cursor-pointer select-none group">
<span className="text-sm text-gray-700">{label}</span>
<div
onClick={() => onChange(!checked)}
className={clx(
'w-5 h-5 rounded-md border-2 flex items-center justify-center transition-all',
checked
? 'border-gray-800 bg-gray-800'
: 'border-gray-300 bg-white group-hover:border-gray-400'
)}
>
{checked && (
<svg width="10" height="8" viewBox="0 0 10 8" fill="none">
<path
d="M1 4l3 3 5-6"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
</label>
)
export default SettingsPanel