Files
dpage-editor/src/pages/editor/components/SettingsPanel.tsx
T
hamid zarghami b9cde62566 edit text
2026-05-10 15:14:19 +03:30

357 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
import ColorPicker from '@/components/ColorPicker'
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: 'gradient', label: 'گرادیانت' },
{ value: 'image', label: 'تصویر' },
]
const SettingsPanel = () => {
const {
documentSettings,
pages,
currentPageId,
updateDocumentSettings,
updateCurrentPageBackground,
} = useEditorStore()
const colorInputRef = useRef<HTMLInputElement>(null)
const { mutateAsync: uploadFile, isPending: imageLoading } = useSingleUpload()
const currentPage = pages.find((page) => page.id === currentPageId)
const backgroundType = currentPage?.backgroundType ?? 'color'
const backgroundColor = currentPage?.backgroundColor ?? '#ffffff'
const backgroundGradient = currentPage?.backgroundGradient ?? {
from: '#ffffff',
to: '#e2e8f0',
angle: 135,
}
const backgroundImageUrl = currentPage?.backgroundImageUrl ?? ''
const [uploadedPreviews, setUploadedPreviews] = useState<string[]>(
backgroundImageUrl ? [backgroundImageUrl] : []
)
const {
displayStyle,
autoPlay,
pageFlipSound,
// leftToRightFlip,
} = documentSettings
useEffect(() => {
setUploadedPreviews(
backgroundImageUrl ? [backgroundImageUrl] : []
)
}, [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
updateCurrentPageBackground({ backgroundImageUrl: imageUrl })
setUploadedPreviews([imageUrl])
} catch {
// TODO: show error toast
}
}
const handlePreviewChange = (previews: string[]) => {
setUploadedPreviews(previews)
if (previews.length === 0) {
updateCurrentPageBackground({ 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={() =>
updateCurrentPageBackground({
backgroundType: opt.value,
...(opt.value === 'gradient'
? {
backgroundGradient: currentPage?.backgroundGradient ?? {
from: '#ffffff',
to: '#e2e8f0',
angle: 135,
},
}
: {}),
})
}
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={() => updateCurrentPageBackground({ 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) => updateCurrentPageBackground({ 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 === 'gradient' && (
<div className="space-y-3">
<p className="text-xs text-gray-500">گرادیانت خطی</p>
<ColorPicker
label="رنگ شروع"
value={backgroundGradient.from}
onChange={(value) =>
updateCurrentPageBackground({
backgroundGradient: { ...backgroundGradient, from: value },
})
}
/>
<ColorPicker
label="رنگ پایان"
value={backgroundGradient.to}
onChange={(value) =>
updateCurrentPageBackground({
backgroundGradient: { ...backgroundGradient, to: value },
})
}
/>
<div className="space-y-1">
<label className="text-xs text-gray-500">زاویه</label>
<div className="flex items-center gap-2">
<input
type="range"
min={0}
max={360}
value={backgroundGradient.angle}
onChange={(e) =>
updateCurrentPageBackground({
backgroundGradient: {
...backgroundGradient,
angle: Number(e.target.value),
},
})
}
className="w-full"
/>
<input
type="number"
min={0}
max={360}
value={backgroundGradient.angle}
onChange={(e) =>
updateCurrentPageBackground({
backgroundGradient: {
...backgroundGradient,
angle: Number(e.target.value) || 0,
},
})
}
className="w-16 h-9 rounded-lg border border-border px-2 text-xs"
/>
</div>
</div>
<div
className="h-12 rounded-xl border border-border"
style={{
backgroundImage: `linear-gradient(${backgroundGradient.angle}deg, ${backgroundGradient.from} 0%, ${backgroundGradient.to} 100%)`,
}}
/>
</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