428 lines
15 KiB
TypeScript
428 lines
15 KiB
TypeScript
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'
|
||
import Select from '@/components/Select'
|
||
import { toCssLinearGradient } from '../utils/gradient'
|
||
|
||
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: 'تصویر' },
|
||
{ value: 'video', label: 'ویدیو' },
|
||
]
|
||
|
||
const SettingsPanel = () => {
|
||
const {
|
||
documentSettings,
|
||
pages,
|
||
currentPageId,
|
||
updateDocumentSettings,
|
||
updateCurrentPageBackground,
|
||
} = useEditorStore()
|
||
const colorInputRef = useRef<HTMLInputElement>(null)
|
||
const { mutateAsync: uploadFile, isPending: mediaLoading } = 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 backgroundVideoUrl = currentPage?.backgroundVideoUrl ?? ''
|
||
const [uploadedPreviews, setUploadedPreviews] = useState<string[]>(
|
||
backgroundImageUrl ? [backgroundImageUrl] : []
|
||
)
|
||
|
||
const {
|
||
displayStyle,
|
||
autoPlay,
|
||
pageFlipSound,
|
||
smartGuide,
|
||
// leftToRightFlip,
|
||
} = documentSettings
|
||
|
||
useEffect(() => {
|
||
setUploadedPreviews(
|
||
backgroundImageUrl ? [backgroundImageUrl] : []
|
||
)
|
||
}, [backgroundImageUrl])
|
||
|
||
useEffect(() => {
|
||
if (backgroundType !== 'video') return
|
||
setUploadedPreviews(backgroundVideoUrl ? [backgroundVideoUrl] : [])
|
||
}, [backgroundType, backgroundVideoUrl])
|
||
|
||
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 handleVideoFiles = async (files: File[]) => {
|
||
if (files.length === 0) return
|
||
const file = files[files.length - 1]
|
||
if (!file) return
|
||
|
||
try {
|
||
const result = await uploadFile({ file })
|
||
const videoUrl = result?.data?.url
|
||
if (!videoUrl) return
|
||
|
||
updateCurrentPageBackground({ backgroundVideoUrl: videoUrl })
|
||
setUploadedPreviews([videoUrl])
|
||
} catch {
|
||
// TODO: show error toast
|
||
}
|
||
}
|
||
|
||
const handlePreviewChange = (previews: string[]) => {
|
||
setUploadedPreviews(previews)
|
||
if (previews.length === 0) {
|
||
if (backgroundType === 'image') {
|
||
updateCurrentPageBackground({ backgroundImageUrl: '' })
|
||
} else if (backgroundType === 'video') {
|
||
updateCurrentPageBackground({ backgroundVideoUrl: '' })
|
||
}
|
||
}
|
||
}
|
||
|
||
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 text-gray-800">تنظیمات نمایش</h3>
|
||
</div>
|
||
|
||
{/* استایل نمایش */}
|
||
<div className="mb-4">
|
||
<label className="block text-xs text-gray-500 mb-2">استایل نمایش</label>
|
||
<Select
|
||
value={displayStyle}
|
||
items={DISPLAY_STYLE_OPTIONS}
|
||
onChange={(e) =>
|
||
updateDocumentSettings({ displayStyle: e.target.value as DisplayStyle })
|
||
}
|
||
/>
|
||
</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 border-b border-border">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<h3 className="text-sm">تنظیمات ویرایشگر</h3>
|
||
</div>
|
||
<div className="flex flex-col gap-3">
|
||
<CheckboxRow
|
||
label="راهنمای هوشمند"
|
||
checked={smartGuide}
|
||
onChange={(v) => updateDocumentSettings({ smartGuide: v })}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
{/* ─── پس زمینه ─── */}
|
||
<section className="p-4">
|
||
<h3 className="text-sm mb-4">پس زمینه</h3>
|
||
|
||
{/* نوع پسزمینه */}
|
||
<div className="flex flex-wrap 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,
|
||
},
|
||
}
|
||
: {}),
|
||
...(opt.value !== 'image' ? { backgroundImageUrl: '' } : {}),
|
||
...(opt.value !== 'video' ? { backgroundVideoUrl: '' } : {}),
|
||
})
|
||
}
|
||
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(
|
||
'size-7 rounded-lg 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: toCssLinearGradient(backgroundGradient),
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* ─── آپلود تصویر ─── */}
|
||
{backgroundType === 'image' && (
|
||
<div>
|
||
{mediaLoading ? (
|
||
<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>
|
||
)}
|
||
|
||
{backgroundType === 'video' && (
|
||
<div>
|
||
{mediaLoading ? (
|
||
<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={handleVideoFiles}
|
||
isMultiple={false}
|
||
hidePreview
|
||
/>
|
||
)}
|
||
{backgroundVideoUrl && (
|
||
<div className="mt-3 space-y-2">
|
||
<video
|
||
src={backgroundVideoUrl}
|
||
controls
|
||
muted
|
||
className="w-full h-36 rounded-lg border border-border bg-black object-cover"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
updateCurrentPageBackground({ backgroundVideoUrl: '' })
|
||
setUploadedPreviews([])
|
||
}}
|
||
className="text-xs text-red-500 hover:text-red-600"
|
||
>
|
||
حذف ویدیو
|
||
</button>
|
||
</div>
|
||
)}
|
||
</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-xs text-gray-700">{label}</span>
|
||
<div
|
||
onClick={() => onChange(!checked)}
|
||
className={clx(
|
||
'size-[18px] 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
|