add renderer components
This commit is contained in:
@@ -12,7 +12,8 @@ type Props = {
|
|||||||
preview?: string[],
|
preview?: string[],
|
||||||
onChangePreview?: (preview: string[]) => void,
|
onChangePreview?: (preview: string[]) => void,
|
||||||
getCover?: (url: string) => void,
|
getCover?: (url: string) => void,
|
||||||
coverUrl?: string
|
coverUrl?: string,
|
||||||
|
isReset?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||||
@@ -67,6 +68,13 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
|||||||
setCover(props.coverUrl ? props.coverUrl : '')
|
setCover(props.coverUrl ? props.coverUrl : '')
|
||||||
}, [props.coverUrl])
|
}, [props.coverUrl])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isReset) {
|
||||||
|
setFiles([])
|
||||||
|
setPerviews([])
|
||||||
|
}
|
||||||
|
}, [props.isReset])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { ButtonType, ButtonSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
|
||||||
|
|
||||||
|
interface ButtonRendererProps {
|
||||||
|
buttons: ButtonType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonRenderer: FC<ButtonRendererProps> = ({ buttons }) => {
|
||||||
|
if (!buttons || buttons.length === 0) return null
|
||||||
|
|
||||||
|
// تبدیل enum به string برای HTML attributes
|
||||||
|
const getHorizontalAlign = (alignment?: HorizontalAlignment) => {
|
||||||
|
switch (alignment) {
|
||||||
|
case HorizontalAlignment.LEFT:
|
||||||
|
return 'left'
|
||||||
|
case HorizontalAlignment.RIGHT:
|
||||||
|
return 'right'
|
||||||
|
case HorizontalAlignment.CENTER:
|
||||||
|
default:
|
||||||
|
return 'center'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVerticalAlign = (verticalAlignment?: VerticalAlignment) => {
|
||||||
|
switch (verticalAlignment) {
|
||||||
|
case VerticalAlignment.TOP:
|
||||||
|
return 'top'
|
||||||
|
case VerticalAlignment.BOTTOM:
|
||||||
|
return 'bottom'
|
||||||
|
case VerticalAlignment.MIDDLE:
|
||||||
|
default:
|
||||||
|
return 'middle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align={getHorizontalAlign(buttons[0]?.alignment)}
|
||||||
|
valign={getVerticalAlign(buttons[0]?.verticalAlignment)}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<table cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
{buttons.slice(0, 2).map((button, buttonIndex) => (
|
||||||
|
<td key={button.id} style={{ paddingRight: buttonIndex === 0 && buttons.length > 1 ? '4px' : '0' }}>
|
||||||
|
<table cellPadding="0" cellSpacing="0" border={0} style={{
|
||||||
|
borderCollapse: 'collapse',
|
||||||
|
backgroundColor: button.backgroundColor,
|
||||||
|
border: button.isBorder ? `${button.borderSize}px solid ${button.borderColor}` : 'none',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
valign="middle"
|
||||||
|
style={{
|
||||||
|
padding: button.size === ButtonSize.SMALL ? '2px 4px' : button.size === ButtonSize.MEDIUM ? '4px 8px' : '8px 12px',
|
||||||
|
fontSize: button.size === ButtonSize.SMALL ? '11px' : button.size === ButtonSize.MEDIUM ? '13px' : '15px',
|
||||||
|
color: button.textColor,
|
||||||
|
textDecoration: 'none',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
maxWidth: 'fit-content',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a href={button.link || '#'} style={{
|
||||||
|
color: button.textColor,
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}>
|
||||||
|
{button.text}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ButtonRenderer
|
||||||
@@ -7,26 +7,55 @@ import { AlignRight, AlignBottom, AlignHorizontally, AlignLeft, AlignTop, AlignV
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePersonalityStore } from '../store/Store'
|
import { usePersonalityStore } from '../store/Store'
|
||||||
|
import { ButtonSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
|
||||||
|
|
||||||
const ButtonSidebar: FC = () => {
|
const ButtonSidebar: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { addButtonToActiveItem } = usePersonalityStore()
|
const { addButtonToActiveItem } = usePersonalityStore()
|
||||||
|
|
||||||
|
const [buttonText, setButtonText] = useState<string>('')
|
||||||
|
const [buttonLink, setButtonLink] = useState<string>('')
|
||||||
|
const [buttonSize, setButtonSize] = useState<ButtonSize>(ButtonSize.MEDIUM)
|
||||||
const [isBorder, setIsBorder] = useState<boolean>(false)
|
const [isBorder, setIsBorder] = useState<boolean>(false)
|
||||||
|
const [borderSize, setBorderSize] = useState<number>(1)
|
||||||
|
const [textColor, setTextColor] = useState<string>('#000')
|
||||||
|
const [backgroundColor, setBackgroundColor] = useState<string>('#000')
|
||||||
|
const [borderColor, setBorderColor] = useState<string>('#000')
|
||||||
|
const [horizontalAlignment, setHorizontalAlignment] = useState<HorizontalAlignment>(HorizontalAlignment.CENTER)
|
||||||
|
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
|
||||||
|
|
||||||
|
const sizeOptions = [
|
||||||
|
{ label: 'کوچک', value: ButtonSize.SMALL },
|
||||||
|
{ label: 'متوسط', value: ButtonSize.MEDIUM },
|
||||||
|
{ label: 'بزرگ', value: ButtonSize.LARGE }
|
||||||
|
]
|
||||||
|
|
||||||
const handleAddButton = () => {
|
const handleAddButton = () => {
|
||||||
addButtonToActiveItem({
|
addButtonToActiveItem({
|
||||||
text: 'دکمه جدید',
|
text: buttonText,
|
||||||
link: '#',
|
link: buttonLink,
|
||||||
size: 'medium',
|
size: buttonSize,
|
||||||
isBorder: true,
|
isBorder,
|
||||||
borderSize: 1,
|
borderSize,
|
||||||
textColor: '#ffffff',
|
textColor,
|
||||||
backgroundColor: '#0038FF',
|
backgroundColor,
|
||||||
borderColor: '#0038FF',
|
borderColor,
|
||||||
alignment: 'center',
|
alignment: horizontalAlignment,
|
||||||
verticalAlignment: 'middle',
|
verticalAlignment,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reset form after adding
|
||||||
|
setButtonText('')
|
||||||
|
setButtonLink('')
|
||||||
|
setButtonSize(ButtonSize.MEDIUM)
|
||||||
|
setIsBorder(false)
|
||||||
|
setBorderSize(1)
|
||||||
|
setTextColor('#000')
|
||||||
|
setBackgroundColor('#000')
|
||||||
|
setBorderColor('#000')
|
||||||
|
setHorizontalAlignment(HorizontalAlignment.CENTER)
|
||||||
|
setVerticalAlignment(VerticalAlignment.MIDDLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,12 +65,16 @@ const ButtonSidebar: FC = () => {
|
|||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Input
|
<Input
|
||||||
label={t('setting.button_text')}
|
label={t('setting.button_text')}
|
||||||
|
value={buttonText}
|
||||||
|
onChange={(e) => setButtonText(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<Input
|
<Input
|
||||||
label={t('setting.button_link')}
|
label={t('setting.button_link')}
|
||||||
|
value={buttonLink}
|
||||||
|
onChange={(e) => setButtonLink(e.target.value)}
|
||||||
endIcon={<Link2 size={18} color='#888' />}
|
endIcon={<Link2 size={18} color='#888' />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +82,9 @@ const ButtonSidebar: FC = () => {
|
|||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<Select
|
<Select
|
||||||
label={t('setting.size')}
|
label={t('setting.size')}
|
||||||
items={[]}
|
items={sizeOptions}
|
||||||
|
value={buttonSize}
|
||||||
|
onChange={(e) => setButtonSize(e.target.value as ButtonSize)}
|
||||||
placeholder={t('setting.select')}
|
placeholder={t('setting.select')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +103,8 @@ const ButtonSidebar: FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
type='number'
|
type='number'
|
||||||
label={t('setting.border')}
|
label={t('setting.border')}
|
||||||
|
value={borderSize.toString()}
|
||||||
|
onChange={(e) => setBorderSize(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -75,21 +112,24 @@ const ButtonSidebar: FC = () => {
|
|||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label={t('setting.text_color')}
|
label={t('setting.text_color')}
|
||||||
changeColor={() => null}
|
defaultColor={textColor}
|
||||||
|
changeColor={setTextColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label={t('setting.background_color')}
|
label={t('setting.background_color')}
|
||||||
changeColor={() => null}
|
defaultColor={backgroundColor}
|
||||||
|
changeColor={setBackgroundColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label={t('setting.border_color')}
|
label={t('setting.border_color')}
|
||||||
changeColor={() => null}
|
defaultColor={borderColor}
|
||||||
|
changeColor={setBorderColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -99,9 +139,21 @@ const ButtonSidebar: FC = () => {
|
|||||||
{t('setting.horizontal_position')}
|
{t('setting.horizontal_position')}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 flex gap-4'>
|
<div className='mt-3 flex gap-4'>
|
||||||
<AlignRight size={20} color='#000' />
|
<div
|
||||||
<AlignVertically size={20} color='#000' />
|
onClick={() => setHorizontalAlignment(HorizontalAlignment.RIGHT)}
|
||||||
<AlignLeft size={20} color='#000' />
|
>
|
||||||
|
<AlignRight size={20} color={horizontalAlignment === HorizontalAlignment.RIGHT ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => setHorizontalAlignment(HorizontalAlignment.CENTER)}
|
||||||
|
>
|
||||||
|
<AlignVertically size={20} color={horizontalAlignment === HorizontalAlignment.CENTER ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => setHorizontalAlignment(HorizontalAlignment.LEFT)}
|
||||||
|
>
|
||||||
|
<AlignLeft size={20} color={horizontalAlignment === HorizontalAlignment.LEFT ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -110,9 +162,21 @@ const ButtonSidebar: FC = () => {
|
|||||||
{t('setting.vertical_position')}
|
{t('setting.vertical_position')}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 flex gap-4'>
|
<div className='mt-3 flex gap-4'>
|
||||||
<AlignBottom size={20} color='#000' />
|
<div
|
||||||
<AlignHorizontally size={20} color='#000' />
|
onClick={() => setVerticalAlignment(VerticalAlignment.TOP)}
|
||||||
<AlignTop size={20} color='#000' />
|
>
|
||||||
|
<AlignTop size={20} color={verticalAlignment === VerticalAlignment.TOP ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => setVerticalAlignment(VerticalAlignment.MIDDLE)}
|
||||||
|
>
|
||||||
|
<AlignHorizontally size={20} color={verticalAlignment === VerticalAlignment.MIDDLE ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => setVerticalAlignment(VerticalAlignment.BOTTOM)}
|
||||||
|
>
|
||||||
|
<AlignBottom size={20} color={verticalAlignment === VerticalAlignment.BOTTOM ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ import { FC } from 'react'
|
|||||||
import { usePersonalityStore } from '../store/Store'
|
import { usePersonalityStore } from '../store/Store'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { AddCircle } from 'iconsax-react'
|
import { AddCircle } from 'iconsax-react'
|
||||||
|
import TextRenderer from './TextRenderer'
|
||||||
|
import ButtonRenderer from './ButtonRenderer'
|
||||||
|
import ImageRenderer from './ImageRenderer'
|
||||||
|
import { SectionName, VerticalAlignment, HorizontalAlignment } from '../types/Types'
|
||||||
|
|
||||||
const ContentSection: FC = () => {
|
const ContentSection: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
|
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
|
||||||
|
|
||||||
const handleSectionClick = (section: "header" | "content" | "footer") => {
|
const handleSectionClick = (section: SectionName) => {
|
||||||
setActiveSection(section)
|
setActiveSection(section)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,8 +20,35 @@ const ContentSection: FC = () => {
|
|||||||
setActiveItemIndex(index)
|
setActiveItemIndex(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// تابعهای helper برای تعیین alignment دکمهها
|
||||||
|
const getButtonHorizontalAlign = (button?: { alignment?: HorizontalAlignment }) => {
|
||||||
|
if (!button || !button.alignment) return 'center'
|
||||||
|
switch (button.alignment) {
|
||||||
|
case HorizontalAlignment.LEFT:
|
||||||
|
return 'left'
|
||||||
|
case HorizontalAlignment.RIGHT:
|
||||||
|
return 'right'
|
||||||
|
case HorizontalAlignment.CENTER:
|
||||||
|
default:
|
||||||
|
return 'center'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getButtonVerticalAlign = (button?: { verticalAlignment?: VerticalAlignment }) => {
|
||||||
|
if (!button || !button.verticalAlignment) return 'middle'
|
||||||
|
switch (button.verticalAlignment) {
|
||||||
|
case VerticalAlignment.TOP:
|
||||||
|
return 'top'
|
||||||
|
case VerticalAlignment.BOTTOM:
|
||||||
|
return 'bottom'
|
||||||
|
case VerticalAlignment.MIDDLE:
|
||||||
|
default:
|
||||||
|
return 'middle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr onClick={() => handleSectionClick("content")}>
|
<tr onClick={() => handleSectionClick(SectionName.CONTENT)}>
|
||||||
<td style={{ paddingTop: '16px' }}>
|
<td style={{ paddingTop: '16px' }}>
|
||||||
{
|
{
|
||||||
data.content.columnsCount > 0 ?
|
data.content.columnsCount > 0 ?
|
||||||
@@ -26,16 +57,16 @@ const ContentSection: FC = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
border: activeSection === "content" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
border: activeSection === SectionName.CONTENT ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
||||||
borderRadius: '24px'
|
borderRadius: '24px'
|
||||||
}}>
|
}}>
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
|
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ tableLayout: 'fixed' }}>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
{
|
{
|
||||||
Array.from({ length: data.content.columnsCount }, (_, index) => {
|
Array.from({ length: data.content.columnsCount }, (_, index) => {
|
||||||
const item = data.content.items[index];
|
const item = data.content.items[index];
|
||||||
const isActive = activeSection === "content" && activeItemIndex === index;
|
const isActive = activeSection === SectionName.CONTENT && activeItemIndex === index;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -46,32 +77,81 @@ const ContentSection: FC = () => {
|
|||||||
}}
|
}}
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
style={{
|
||||||
|
width: `${100 / data.content.columnsCount}%`,
|
||||||
height: '246px',
|
height: '246px',
|
||||||
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
|
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
|
||||||
backgroundColor: item?.backgroundColor || '#ffffff',
|
backgroundColor: item?.backgroundColor || '#ffffff',
|
||||||
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
|
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
|
||||||
backgroundSize: 'cover',
|
backgroundSize: item?.style?.backgroundSize || 'cover',
|
||||||
backgroundPosition: 'center',
|
backgroundRepeat: item?.style?.backgroundRepeat || 'no-repeat',
|
||||||
cursor: 'pointer'
|
backgroundPosition: item?.style?.backgroundPosition || 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '0',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
borderRadius: '8px',
|
||||||
|
position: 'relative'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* محتوای آیتم */}
|
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{
|
||||||
{item?.texts?.map(text => (
|
borderCollapse: 'collapse',
|
||||||
<div key={text.id} style={{ color: text.color }}>
|
height: '246px'
|
||||||
{text.text}
|
}}>
|
||||||
</div>
|
<tbody>
|
||||||
))}
|
{/* اگر فقط دکمه داریم و متن نداریم */}
|
||||||
{item?.buttons?.map(button => (
|
{(!item?.texts || item.texts.length === 0) && item?.buttons && item.buttons.length > 0 ? (
|
||||||
<button
|
<tr>
|
||||||
key={button.id}
|
<td
|
||||||
style={{
|
width="100%"
|
||||||
backgroundColor: button.backgroundColor,
|
height="246"
|
||||||
color: button.textColor
|
align={getButtonHorizontalAlign(item.buttons[0])}
|
||||||
}}
|
valign={getButtonVerticalAlign(item.buttons[0])}
|
||||||
>
|
style={{
|
||||||
{button.text}
|
padding: '8px'
|
||||||
</button>
|
}}
|
||||||
))}
|
>
|
||||||
|
<ButtonRenderer buttons={item.buttons} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* ردیف اصلی برای متن */}
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
width="100%"
|
||||||
|
height={item?.buttons && item.buttons.length > 0 ? "203" : "230"}
|
||||||
|
align={item?.texts?.[0]?.alignment || 'center'}
|
||||||
|
valign={item?.texts?.[0]?.verticalAlignment === VerticalAlignment.TOP ? 'top' :
|
||||||
|
item?.texts?.[0]?.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'middle'}
|
||||||
|
style={{
|
||||||
|
padding: '8px',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.4'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextRenderer texts={item?.texts || []} />
|
||||||
|
<ImageRenderer images={item?.images || []} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/* ردیف دکمهها */}
|
||||||
|
{item?.buttons && item.buttons.length > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
width="100%"
|
||||||
|
height="27"
|
||||||
|
align={getButtonHorizontalAlign(item.buttons[0])}
|
||||||
|
valign={getButtonVerticalAlign(item.buttons[0])}
|
||||||
|
style={{
|
||||||
|
padding: '0 8px 8px 8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonRenderer buttons={item.buttons} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</td>
|
</td>
|
||||||
{index < data.content.columnsCount - 1 && (
|
{index < data.content.columnsCount - 1 && (
|
||||||
<td style={{ width: '16px' }}></td>
|
<td style={{ width: '16px' }}></td>
|
||||||
@@ -93,7 +173,7 @@ const ContentSection: FC = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td style={{
|
<td style={{
|
||||||
height: '286px',
|
height: '286px',
|
||||||
border: activeSection === "content" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
border: activeSection === SectionName.CONTENT ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
||||||
borderRadius: '24px',
|
borderRadius: '24px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ import { FC } from 'react'
|
|||||||
import { usePersonalityStore } from '../store/Store'
|
import { usePersonalityStore } from '../store/Store'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { AddCircle } from 'iconsax-react'
|
import { AddCircle } from 'iconsax-react'
|
||||||
|
import TextRenderer from './TextRenderer'
|
||||||
|
import ButtonRenderer from './ButtonRenderer'
|
||||||
|
import ImageRenderer from './ImageRenderer'
|
||||||
|
import { SectionName, VerticalAlignment, HorizontalAlignment } from '../types/Types'
|
||||||
|
|
||||||
const FooterSection: FC = () => {
|
const FooterSection: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
|
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
|
||||||
|
|
||||||
const handleSectionClick = (section: "header" | "content" | "footer") => {
|
const handleSectionClick = (section: SectionName) => {
|
||||||
setActiveSection(section)
|
setActiveSection(section)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,8 +20,35 @@ const FooterSection: FC = () => {
|
|||||||
setActiveItemIndex(index)
|
setActiveItemIndex(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// تابعهای helper برای تعیین alignment دکمهها
|
||||||
|
const getButtonHorizontalAlign = (button?: { alignment?: HorizontalAlignment }) => {
|
||||||
|
if (!button || !button.alignment) return 'center'
|
||||||
|
switch (button.alignment) {
|
||||||
|
case HorizontalAlignment.LEFT:
|
||||||
|
return 'left'
|
||||||
|
case HorizontalAlignment.RIGHT:
|
||||||
|
return 'right'
|
||||||
|
case HorizontalAlignment.CENTER:
|
||||||
|
default:
|
||||||
|
return 'center'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getButtonVerticalAlign = (button?: { verticalAlignment?: VerticalAlignment }) => {
|
||||||
|
if (!button || !button.verticalAlignment) return 'middle'
|
||||||
|
switch (button.verticalAlignment) {
|
||||||
|
case VerticalAlignment.TOP:
|
||||||
|
return 'top'
|
||||||
|
case VerticalAlignment.BOTTOM:
|
||||||
|
return 'bottom'
|
||||||
|
case VerticalAlignment.MIDDLE:
|
||||||
|
default:
|
||||||
|
return 'middle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr onClick={() => handleSectionClick("footer")}>
|
<tr onClick={() => handleSectionClick(SectionName.FOOTER)}>
|
||||||
<td style={{ paddingTop: '16px' }}>
|
<td style={{ paddingTop: '16px' }}>
|
||||||
{
|
{
|
||||||
data.footer.columnsCount > 0 ?
|
data.footer.columnsCount > 0 ?
|
||||||
@@ -26,16 +57,16 @@ const FooterSection: FC = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
border: activeSection === "footer" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
border: activeSection === SectionName.FOOTER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
||||||
borderRadius: '24px'
|
borderRadius: '24px'
|
||||||
}}>
|
}}>
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
|
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ tableLayout: 'fixed' }}>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
{
|
{
|
||||||
Array.from({ length: data.footer.columnsCount }, (_, index) => {
|
Array.from({ length: data.footer.columnsCount }, (_, index) => {
|
||||||
const item = data.footer.items[index];
|
const item = data.footer.items[index];
|
||||||
const isActive = activeSection === "footer" && activeItemIndex === index;
|
const isActive = activeSection === SectionName.FOOTER && activeItemIndex === index;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -46,32 +77,81 @@ const FooterSection: FC = () => {
|
|||||||
}}
|
}}
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
style={{
|
||||||
height: '83px',
|
width: `${100 / data.footer.columnsCount}%`,
|
||||||
|
height: '123px',
|
||||||
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
|
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
|
||||||
backgroundColor: item?.backgroundColor || '#ffffff',
|
backgroundColor: item?.backgroundColor || '#ffffff',
|
||||||
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
|
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
|
||||||
backgroundSize: 'cover',
|
backgroundSize: item?.style?.backgroundSize || 'cover',
|
||||||
backgroundPosition: 'center',
|
backgroundRepeat: item?.style?.backgroundRepeat || 'no-repeat',
|
||||||
cursor: 'pointer'
|
backgroundPosition: item?.style?.backgroundPosition || 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '0',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
borderRadius: '8px',
|
||||||
|
position: 'relative'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* محتوای آیتم */}
|
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{
|
||||||
{item?.texts?.map(text => (
|
borderCollapse: 'collapse',
|
||||||
<div key={text.id} style={{ color: text.color }}>
|
height: '123px'
|
||||||
{text.text}
|
}}>
|
||||||
</div>
|
<tbody>
|
||||||
))}
|
{/* اگر فقط دکمه داریم و متن نداریم */}
|
||||||
{item?.buttons?.map(button => (
|
{(!item?.texts || item.texts.length === 0) && item?.buttons && item.buttons.length > 0 ? (
|
||||||
<button
|
<tr>
|
||||||
key={button.id}
|
<td
|
||||||
style={{
|
width="100%"
|
||||||
backgroundColor: button.backgroundColor,
|
height="123"
|
||||||
color: button.textColor
|
align={getButtonHorizontalAlign(item.buttons[0])}
|
||||||
}}
|
valign={getButtonVerticalAlign(item.buttons[0])}
|
||||||
>
|
style={{
|
||||||
{button.text}
|
padding: '8px'
|
||||||
</button>
|
}}
|
||||||
))}
|
>
|
||||||
|
<ButtonRenderer buttons={item.buttons} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* ردیف اصلی برای متن */}
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
width="100%"
|
||||||
|
height={item?.buttons && item.buttons.length > 0 ? "40" : "67"}
|
||||||
|
align={item?.texts?.[0]?.alignment || 'center'}
|
||||||
|
valign={item?.texts?.[0]?.verticalAlignment === VerticalAlignment.TOP ? 'top' :
|
||||||
|
item?.texts?.[0]?.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'middle'}
|
||||||
|
style={{
|
||||||
|
padding: '8px',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.4'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextRenderer texts={item?.texts || []} />
|
||||||
|
<ImageRenderer images={item?.images || []} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/* ردیف دکمهها */}
|
||||||
|
{item?.buttons && item.buttons.length > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
width="100%"
|
||||||
|
height="27"
|
||||||
|
align={getButtonHorizontalAlign(item.buttons[0])}
|
||||||
|
valign={getButtonVerticalAlign(item.buttons[0])}
|
||||||
|
style={{
|
||||||
|
padding: '0 8px 8px 8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonRenderer buttons={item.buttons} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</td>
|
</td>
|
||||||
{index < data.footer.columnsCount - 1 && (
|
{index < data.footer.columnsCount - 1 && (
|
||||||
<td style={{ width: '16px' }}></td>
|
<td style={{ width: '16px' }}></td>
|
||||||
@@ -93,7 +173,7 @@ const FooterSection: FC = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td style={{
|
<td style={{
|
||||||
height: '123px',
|
height: '123px',
|
||||||
border: activeSection === "footer" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
border: activeSection === SectionName.FOOTER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
||||||
borderRadius: '24px',
|
borderRadius: '24px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ import { FC } from 'react'
|
|||||||
import { usePersonalityStore } from '../store/Store'
|
import { usePersonalityStore } from '../store/Store'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { AddCircle } from 'iconsax-react'
|
import { AddCircle } from 'iconsax-react'
|
||||||
|
import TextRenderer from './TextRenderer'
|
||||||
|
import ButtonRenderer from './ButtonRenderer'
|
||||||
|
import ImageRenderer from './ImageRenderer'
|
||||||
|
import { SectionName, VerticalAlignment, HorizontalAlignment } from '../types/Types'
|
||||||
|
|
||||||
const HeaderSection: FC = () => {
|
const HeaderSection: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
|
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
|
||||||
|
|
||||||
const handleSectionClick = (section: "header" | "content" | "footer") => {
|
const handleSectionClick = (section: SectionName) => {
|
||||||
setActiveSection(section)
|
setActiveSection(section)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,22 +20,49 @@ const HeaderSection: FC = () => {
|
|||||||
setActiveItemIndex(index)
|
setActiveItemIndex(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// تابعهای helper برای تعیین alignment دکمهها
|
||||||
|
const getButtonHorizontalAlign = (button?: { alignment?: HorizontalAlignment }) => {
|
||||||
|
if (!button || !button.alignment) return 'center'
|
||||||
|
switch (button.alignment) {
|
||||||
|
case HorizontalAlignment.LEFT:
|
||||||
|
return 'left'
|
||||||
|
case HorizontalAlignment.RIGHT:
|
||||||
|
return 'right'
|
||||||
|
case HorizontalAlignment.CENTER:
|
||||||
|
default:
|
||||||
|
return 'center'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getButtonVerticalAlign = (button?: { verticalAlignment?: VerticalAlignment }) => {
|
||||||
|
if (!button || !button.verticalAlignment) return 'middle'
|
||||||
|
switch (button.verticalAlignment) {
|
||||||
|
case VerticalAlignment.TOP:
|
||||||
|
return 'top'
|
||||||
|
case VerticalAlignment.BOTTOM:
|
||||||
|
return 'bottom'
|
||||||
|
case VerticalAlignment.MIDDLE:
|
||||||
|
default:
|
||||||
|
return 'middle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr onClick={() => handleSectionClick("header")}>
|
<tr onClick={() => handleSectionClick(SectionName.HEADER)}>
|
||||||
{
|
{
|
||||||
data.header.columnsCount > 0 ?
|
data.header.columnsCount > 0 ?
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
border: activeSection === "header" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
border: activeSection === SectionName.HEADER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
||||||
borderRadius: '24px'
|
borderRadius: '24px'
|
||||||
}}>
|
}}>
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
|
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ tableLayout: 'fixed' }}>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
{
|
{
|
||||||
Array.from({ length: data.header.columnsCount }, (_, index) => {
|
Array.from({ length: data.header.columnsCount }, (_, index) => {
|
||||||
const item = data.header.items[index];
|
const item = data.header.items[index];
|
||||||
const isActive = activeSection === "header" && activeItemIndex === index;
|
const isActive = activeSection === SectionName.HEADER && activeItemIndex === index;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -42,32 +73,81 @@ const HeaderSection: FC = () => {
|
|||||||
}}
|
}}
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
style={{
|
||||||
height: '103px',
|
width: `${100 / data.header.columnsCount}%`,
|
||||||
|
height: '123px',
|
||||||
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
|
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
|
||||||
backgroundColor: item?.backgroundColor || '#ffffff',
|
backgroundColor: item?.backgroundColor || '#ffffff',
|
||||||
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
|
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
|
||||||
backgroundSize: 'cover',
|
backgroundSize: item?.style?.backgroundSize || 'cover',
|
||||||
backgroundPosition: 'center',
|
backgroundRepeat: item?.style?.backgroundRepeat || 'no-repeat',
|
||||||
cursor: 'pointer'
|
backgroundPosition: item?.style?.backgroundPosition || 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '0',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
borderRadius: '8px',
|
||||||
|
position: 'relative'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* محتوای آیتم */}
|
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{
|
||||||
{item?.texts?.map(text => (
|
borderCollapse: 'collapse',
|
||||||
<div key={text.id} style={{ color: text.color }}>
|
height: '123px'
|
||||||
{text.text}
|
}}>
|
||||||
</div>
|
<tbody>
|
||||||
))}
|
{/* اگر فقط دکمه داریم و متن نداریم */}
|
||||||
{item?.buttons?.map(button => (
|
{(!item?.texts || item.texts.length === 0) && item?.buttons && item.buttons.length > 0 ? (
|
||||||
<button
|
<tr>
|
||||||
key={button.id}
|
<td
|
||||||
style={{
|
width="100%"
|
||||||
backgroundColor: button.backgroundColor,
|
height="123"
|
||||||
color: button.textColor
|
align={getButtonHorizontalAlign(item.buttons[0])}
|
||||||
}}
|
valign={getButtonVerticalAlign(item.buttons[0])}
|
||||||
>
|
style={{
|
||||||
{button.text}
|
padding: '8px'
|
||||||
</button>
|
}}
|
||||||
))}
|
>
|
||||||
|
<ButtonRenderer buttons={item.buttons} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* ردیف اصلی برای متن */}
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
width="100%"
|
||||||
|
height={item?.buttons && item.buttons.length > 0 ? "60" : "87"}
|
||||||
|
align={item?.texts?.[0]?.alignment || 'center'}
|
||||||
|
valign={item?.texts?.[0]?.verticalAlignment === VerticalAlignment.TOP ? 'top' :
|
||||||
|
item?.texts?.[0]?.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'middle'}
|
||||||
|
style={{
|
||||||
|
padding: '8px',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.4'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextRenderer texts={item?.texts || []} />
|
||||||
|
<ImageRenderer images={item?.images || []} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/* ردیف دکمهها */}
|
||||||
|
{item?.buttons && item.buttons.length > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
width="100%"
|
||||||
|
height="27"
|
||||||
|
align={getButtonHorizontalAlign(item.buttons[0])}
|
||||||
|
valign={getButtonVerticalAlign(item.buttons[0])}
|
||||||
|
style={{
|
||||||
|
padding: '0 8px 8px 8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonRenderer buttons={item.buttons} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</td>
|
</td>
|
||||||
{index < data.header.columnsCount - 1 && (
|
{index < data.header.columnsCount - 1 && (
|
||||||
<td style={{ width: '16px' }}></td>
|
<td style={{ width: '16px' }}></td>
|
||||||
@@ -83,7 +163,7 @@ const HeaderSection: FC = () => {
|
|||||||
:
|
:
|
||||||
<td style={{
|
<td style={{
|
||||||
height: '123px',
|
height: '123px',
|
||||||
border: activeSection === "header" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
border: activeSection === SectionName.HEADER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
||||||
borderRadius: '24px',
|
borderRadius: '24px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { ImageType, ImageSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
|
||||||
|
|
||||||
|
interface ImageRendererProps {
|
||||||
|
images: ImageType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageRenderer: FC<ImageRendererProps> = ({ images }) => {
|
||||||
|
if (!images || images.length === 0) return null
|
||||||
|
|
||||||
|
const getBackgroundStyle = (image: ImageType) => {
|
||||||
|
let backgroundSize = 'cover'
|
||||||
|
let backgroundRepeat = 'no-repeat'
|
||||||
|
|
||||||
|
switch (image.size) {
|
||||||
|
case ImageSize.COVER:
|
||||||
|
backgroundSize = 'cover'
|
||||||
|
backgroundRepeat = 'no-repeat'
|
||||||
|
break
|
||||||
|
case ImageSize.CONTAIN:
|
||||||
|
backgroundSize = 'contain'
|
||||||
|
backgroundRepeat = 'no-repeat'
|
||||||
|
break
|
||||||
|
case ImageSize.REPEAT:
|
||||||
|
backgroundSize = 'auto'
|
||||||
|
backgroundRepeat = 'repeat'
|
||||||
|
break
|
||||||
|
case ImageSize.REPEAT_X:
|
||||||
|
backgroundSize = 'auto'
|
||||||
|
backgroundRepeat = 'repeat-x'
|
||||||
|
break
|
||||||
|
case ImageSize.REPEAT_Y:
|
||||||
|
backgroundSize = 'auto'
|
||||||
|
backgroundRepeat = 'repeat-y'
|
||||||
|
break
|
||||||
|
case ImageSize.NO_REPEAT:
|
||||||
|
backgroundSize = 'auto'
|
||||||
|
backgroundRepeat = 'no-repeat'
|
||||||
|
break
|
||||||
|
case ImageSize.AUTO:
|
||||||
|
default:
|
||||||
|
backgroundSize = 'auto'
|
||||||
|
backgroundRepeat = 'no-repeat'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundPosition = 'center center'
|
||||||
|
|
||||||
|
// تنظیم موقعیت بر اساس alignment
|
||||||
|
const horizontal = image.alignment === HorizontalAlignment.LEFT ? 'left' :
|
||||||
|
image.alignment === HorizontalAlignment.RIGHT ? 'right' : 'center'
|
||||||
|
const vertical = image.verticalAlignment === VerticalAlignment.TOP ? 'top' :
|
||||||
|
image.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'center'
|
||||||
|
|
||||||
|
backgroundPosition = `${horizontal} ${vertical}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundImage: `url(${image.src})`,
|
||||||
|
backgroundSize,
|
||||||
|
backgroundRepeat,
|
||||||
|
backgroundPosition,
|
||||||
|
width: image.width || '100%',
|
||||||
|
height: image.height || '60px',
|
||||||
|
display: 'block'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{images.map((image, imageIndex) => (
|
||||||
|
<table key={image.id} width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align={image.alignment || 'center'}
|
||||||
|
valign={image.verticalAlignment || 'middle'}
|
||||||
|
style={{
|
||||||
|
paddingBottom: imageIndex < images.length - 1 ? '4px' : '0'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={getBackgroundStyle(image)}>
|
||||||
|
{/* محتوا درون div با background image */}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageRenderer
|
||||||
@@ -1,12 +1,108 @@
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
|
import Select from '@/components/Select'
|
||||||
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
||||||
import { Add, AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, AlignVertically } from 'iconsax-react'
|
import { Add, AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, AlignVertically } from 'iconsax-react'
|
||||||
import { FC } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { usePersonalityStore } from '../store/Store'
|
||||||
|
import { ImageSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
|
||||||
|
|
||||||
const ImageSideBar: FC = () => {
|
const ImageSideBar: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { addImageToActiveItem, activeSection, activeItemIndex, data } = usePersonalityStore()
|
||||||
|
|
||||||
|
const [imageSrc, setImageSrc] = useState<string>('')
|
||||||
|
const [imageSize, setImageSize] = useState<ImageSize>(ImageSize.COVER)
|
||||||
|
const [horizontalAlignment, setHorizontalAlignment] = useState<HorizontalAlignment>(HorizontalAlignment.CENTER)
|
||||||
|
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
|
const [isReset, setIsReset] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const sizeOptions = [
|
||||||
|
{ label: 'کاور - تمام فضا را پوشش دهد (Cover)', value: ImageSize.COVER },
|
||||||
|
{ label: 'در مقیاس - در فضا جا شود (Contain)', value: ImageSize.CONTAIN },
|
||||||
|
{ label: 'تکرار - عکس تکرار شود (Repeat)', value: ImageSize.REPEAT },
|
||||||
|
{ label: 'تکرار افقی (Repeat-X)', value: ImageSize.REPEAT_X },
|
||||||
|
{ label: 'تکرار عمودی (Repeat-Y)', value: ImageSize.REPEAT_Y },
|
||||||
|
{ label: 'بدون تکرار (No-Repeat)', value: ImageSize.NO_REPEAT },
|
||||||
|
{ label: 'اندازه اصلی (Auto)', value: ImageSize.AUTO }
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleImageUpload = (files: File[]) => {
|
||||||
|
if (files.length === 0) return
|
||||||
|
|
||||||
|
const file = files[0] // فقط اولین فایل را در نظر میگیریم
|
||||||
|
// در اینجا باید فایل آپلود شود و URL برگردانده شود
|
||||||
|
// فعلاً از FileReader استفاده میکنم برای تست
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const result = e.target?.result as string
|
||||||
|
setImageSrc(result)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddImage = () => {
|
||||||
|
if (!imageSrc) return
|
||||||
|
|
||||||
|
console.log('Store state before adding image:', {
|
||||||
|
activeSection,
|
||||||
|
activeItemIndex,
|
||||||
|
hasActiveItem: activeSection && data[activeSection as keyof typeof data]?.items[activeItemIndex],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!activeSection) {
|
||||||
|
console.error('No active section selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionData = data[activeSection as keyof typeof data]
|
||||||
|
if (!sectionData?.items || activeItemIndex >= sectionData.items.length) {
|
||||||
|
console.error('No active item found or invalid index', {
|
||||||
|
activeItemIndex,
|
||||||
|
itemsLength: sectionData?.items?.length
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Adding image with data:', {
|
||||||
|
src: imageSrc,
|
||||||
|
alt: 'عکس آپلود شده',
|
||||||
|
size: imageSize,
|
||||||
|
alignment: horizontalAlignment,
|
||||||
|
verticalAlignment,
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
addImageToActiveItem({
|
||||||
|
src: imageSrc,
|
||||||
|
alt: 'عکس آپلود شده',
|
||||||
|
size: imageSize,
|
||||||
|
alignment: horizontalAlignment,
|
||||||
|
verticalAlignment,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Image added successfully')
|
||||||
|
|
||||||
|
// Reset form after adding
|
||||||
|
setImageSrc('')
|
||||||
|
setImageSize(ImageSize.COVER)
|
||||||
|
setHorizontalAlignment(HorizontalAlignment.CENTER)
|
||||||
|
setVerticalAlignment(VerticalAlignment.MIDDLE)
|
||||||
|
setIsReset(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsReset(false)
|
||||||
|
}, 1000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding image:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -15,7 +111,18 @@ const ImageSideBar: FC = () => {
|
|||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<UploadBoxDraggble
|
<UploadBoxDraggble
|
||||||
label={t('setting.upload_image')}
|
label={t('setting.upload_image')}
|
||||||
onChange={() => null}
|
onChange={handleImageUpload}
|
||||||
|
isReset={isReset}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-5'>
|
||||||
|
<Select
|
||||||
|
items={sizeOptions}
|
||||||
|
label={t('setting.size')}
|
||||||
|
value={imageSize}
|
||||||
|
onChange={(e) => setImageSize(e.target.value as ImageSize)}
|
||||||
|
placeholder={t('setting.size')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -25,28 +132,60 @@ const ImageSideBar: FC = () => {
|
|||||||
{t('setting.horizontal_position')}
|
{t('setting.horizontal_position')}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 flex gap-4'>
|
<div className='mt-3 flex gap-4'>
|
||||||
<AlignRight size={20} color='#000' />
|
<div
|
||||||
<AlignVertically size={20} color='#000' />
|
className={`cursor-pointer p-1 rounded ${horizontalAlignment === HorizontalAlignment.RIGHT ? 'bg-blue-100' : ''}`}
|
||||||
<AlignLeft size={20} color='#000' />
|
onClick={() => setHorizontalAlignment(HorizontalAlignment.RIGHT)}
|
||||||
|
>
|
||||||
|
<AlignRight size={20} color={horizontalAlignment === HorizontalAlignment.RIGHT ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`cursor-pointer p-1 rounded ${horizontalAlignment === HorizontalAlignment.CENTER ? 'bg-blue-100' : ''}`}
|
||||||
|
onClick={() => setHorizontalAlignment(HorizontalAlignment.CENTER)}
|
||||||
|
>
|
||||||
|
<AlignVertically size={20} color={horizontalAlignment === HorizontalAlignment.CENTER ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`cursor-pointer p-1 rounded ${horizontalAlignment === HorizontalAlignment.LEFT ? 'bg-blue-100' : ''}`}
|
||||||
|
onClick={() => setHorizontalAlignment(HorizontalAlignment.LEFT)}
|
||||||
|
>
|
||||||
|
<AlignLeft size={20} color={horizontalAlignment === HorizontalAlignment.LEFT ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className='text-sm'>
|
<div className='text-sm'>
|
||||||
{t('setting.horizontal_position')}
|
{t('setting.vertical_position')}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 flex gap-4'>
|
<div className='mt-3 flex gap-4'>
|
||||||
<AlignBottom size={20} color='#000' />
|
<div
|
||||||
<AlignHorizontally size={20} color='#000' />
|
className={`cursor-pointer p-1 rounded ${verticalAlignment === VerticalAlignment.BOTTOM ? 'bg-blue-100' : ''}`}
|
||||||
<AlignTop size={20} color='#000' />
|
onClick={() => setVerticalAlignment(VerticalAlignment.BOTTOM)}
|
||||||
|
>
|
||||||
|
<AlignBottom size={20} color={verticalAlignment === VerticalAlignment.BOTTOM ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`cursor-pointer p-1 rounded ${verticalAlignment === VerticalAlignment.MIDDLE ? 'bg-blue-100' : ''}`}
|
||||||
|
onClick={() => setVerticalAlignment(VerticalAlignment.MIDDLE)}
|
||||||
|
>
|
||||||
|
<AlignHorizontally size={20} color={verticalAlignment === VerticalAlignment.MIDDLE ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`cursor-pointer p-1 rounded ${verticalAlignment === VerticalAlignment.TOP ? 'bg-blue-100' : ''}`}
|
||||||
|
onClick={() => setVerticalAlignment(VerticalAlignment.TOP)}
|
||||||
|
>
|
||||||
|
<AlignTop size={20} color={verticalAlignment === VerticalAlignment.TOP ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-7'>
|
<div className='mt-7'>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => null}
|
onClick={handleAddImage}
|
||||||
className='bg-[#ECEEF5] text-black'
|
className='bg-[#ECEEF5] text-black'
|
||||||
|
disabled={!imageSrc}
|
||||||
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
<div className='flex justify-center items-center gap-2'>
|
<div className='flex justify-center items-center gap-2'>
|
||||||
<Add size={24} color='black' />
|
<Add size={24} color='black' />
|
||||||
|
|||||||
@@ -1,15 +1,42 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import UploadBoxDraggble from '@/components/UploadBoxDraggble';
|
import UploadBoxDraggble from '@/components/UploadBoxDraggble';
|
||||||
import Select from '@/components/Select';
|
|
||||||
import { AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, AlignVertically } from 'iconsax-react';
|
|
||||||
import ColorPicker from '@/components/ColorPicker';
|
import ColorPicker from '@/components/ColorPicker';
|
||||||
import { usePersonalityStore } from '../store/Store';
|
import { usePersonalityStore } from '../store/Store';
|
||||||
|
import { SectionItemType, BackgroundSize } from '../types/Types';
|
||||||
|
import Select from '@/components/Select';
|
||||||
|
|
||||||
const SettingSideBar: FC = () => {
|
const SettingSideBar: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { activeSection, setColumnsCount, updateActiveItem } = usePersonalityStore()
|
const { activeSection, setColumnsCount, updateActiveItem, data, activeItemIndex } = usePersonalityStore()
|
||||||
const [color, setColor] = useState("#aabbcc");
|
const [color, setColor] = useState("#aabbcc");
|
||||||
|
const [backgroundSize, setBackgroundSize] = useState<BackgroundSize>(BackgroundSize.COVER)
|
||||||
|
|
||||||
|
// تشخیص background size بر اساس style موجود
|
||||||
|
const getCurrentBackgroundSizeType = (style?: SectionItemType['style']) => {
|
||||||
|
if (!style) return BackgroundSize.COVER
|
||||||
|
|
||||||
|
const bgSize = style.backgroundSize
|
||||||
|
const bgRepeat = style.backgroundRepeat
|
||||||
|
|
||||||
|
if (bgSize === BackgroundSize.COVER) return BackgroundSize.COVER
|
||||||
|
if (bgSize === BackgroundSize.CONTAIN) return BackgroundSize.CONTAIN
|
||||||
|
if (bgSize === 'auto' || !bgSize) {
|
||||||
|
if (bgRepeat === BackgroundSize.ROUND) return BackgroundSize.ROUND
|
||||||
|
}
|
||||||
|
return BackgroundSize.COVER
|
||||||
|
}
|
||||||
|
|
||||||
|
// بهروزرسانی state وقتی activeSection یا activeItem تغییر میکند
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeSection && data[activeSection as keyof typeof data]) {
|
||||||
|
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
|
||||||
|
if (currentItem?.style) {
|
||||||
|
const currentType = getCurrentBackgroundSizeType(currentItem.style)
|
||||||
|
setBackgroundSize(currentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeSection, activeItemIndex, data])
|
||||||
|
|
||||||
const handleColumnClick = (columns: number) => {
|
const handleColumnClick = (columns: number) => {
|
||||||
if (activeSection) {
|
if (activeSection) {
|
||||||
@@ -26,10 +53,50 @@ const SettingSideBar: FC = () => {
|
|||||||
if (activeSection && file.length > 0) {
|
if (activeSection && file.length > 0) {
|
||||||
const imageUrl = URL.createObjectURL(file[0])
|
const imageUrl = URL.createObjectURL(file[0])
|
||||||
console.log('Setting background image:', imageUrl)
|
console.log('Setting background image:', imageUrl)
|
||||||
updateActiveItem({ backgroundImage: imageUrl })
|
|
||||||
|
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
|
||||||
|
updateActiveItem({
|
||||||
|
backgroundImage: imageUrl,
|
||||||
|
style: {
|
||||||
|
...currentItem?.style,
|
||||||
|
backgroundSize: getBackgroundSize(backgroundSize),
|
||||||
|
backgroundRepeat: getBackgroundRepeat(backgroundSize),
|
||||||
|
backgroundPosition: 'center center'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getBackgroundRepeat = (size: BackgroundSize) => {
|
||||||
|
switch (size) {
|
||||||
|
case BackgroundSize.ROUND:
|
||||||
|
return 'round'
|
||||||
|
case BackgroundSize.COVER:
|
||||||
|
case BackgroundSize.CONTAIN:
|
||||||
|
default:
|
||||||
|
return 'no-repeat'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBackgroundSize = (size: BackgroundSize) => {
|
||||||
|
switch (size) {
|
||||||
|
case BackgroundSize.COVER:
|
||||||
|
return 'cover'
|
||||||
|
case BackgroundSize.CONTAIN:
|
||||||
|
return 'contain'
|
||||||
|
case BackgroundSize.ROUND:
|
||||||
|
return 'auto'
|
||||||
|
default:
|
||||||
|
return 'cover'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeOptions = [
|
||||||
|
{ label: 'کاور - تمام فضا را پوشش دهد (Cover)', value: BackgroundSize.COVER },
|
||||||
|
{ label: 'در مقیاس - در فضا جا شود (Contain)', value: BackgroundSize.CONTAIN },
|
||||||
|
{ label: 'گرد - عکس به صورت گرد تکرار شود (Round)', value: BackgroundSize.ROUND }
|
||||||
|
]
|
||||||
|
|
||||||
if (!activeSection) {
|
if (!activeSection) {
|
||||||
return (
|
return (
|
||||||
<div className='text-center text-gray-500 mt-8'>
|
<div className='text-center text-gray-500 mt-8'>
|
||||||
@@ -93,65 +160,26 @@ const SettingSideBar: FC = () => {
|
|||||||
|
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<Select
|
<Select
|
||||||
items={[]}
|
items={sizeOptions}
|
||||||
label={t('setting.size')}
|
label={t('setting.size')}
|
||||||
|
value={backgroundSize}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newSize = e.target.value as BackgroundSize
|
||||||
|
setBackgroundSize(newSize)
|
||||||
|
|
||||||
|
const currentItem = activeSection ? data[activeSection as keyof typeof data]?.items[activeItemIndex] : null
|
||||||
|
updateActiveItem({
|
||||||
|
style: {
|
||||||
|
...currentItem?.style,
|
||||||
|
backgroundSize: getBackgroundSize(newSize),
|
||||||
|
backgroundRepeat: getBackgroundRepeat(newSize),
|
||||||
|
backgroundPosition: 'center center'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
placeholder={t('setting.size')}
|
placeholder={t('setting.size')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-5 flex justify-between'>
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('setting.horizontal_position')}
|
|
||||||
</div>
|
|
||||||
<div className='mt-3 flex gap-4'>
|
|
||||||
<AlignRight
|
|
||||||
size={20}
|
|
||||||
color='#000'
|
|
||||||
className='cursor-pointer'
|
|
||||||
onClick={() => updateActiveItem({ alignment: 'right' })}
|
|
||||||
/>
|
|
||||||
<AlignVertically
|
|
||||||
size={20}
|
|
||||||
color='#000'
|
|
||||||
className='cursor-pointer'
|
|
||||||
onClick={() => updateActiveItem({ alignment: 'center' })}
|
|
||||||
/>
|
|
||||||
<AlignLeft
|
|
||||||
size={20}
|
|
||||||
color='#000'
|
|
||||||
className='cursor-pointer'
|
|
||||||
onClick={() => updateActiveItem({ alignment: 'left' })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('setting.vertical_position')}
|
|
||||||
</div>
|
|
||||||
<div className='mt-3 flex gap-4'>
|
|
||||||
<AlignBottom
|
|
||||||
size={20}
|
|
||||||
color='#000'
|
|
||||||
className='cursor-pointer'
|
|
||||||
onClick={() => updateActiveItem({ verticalAlignment: 'bottom' })}
|
|
||||||
/>
|
|
||||||
<AlignHorizontally
|
|
||||||
size={20}
|
|
||||||
color='#000'
|
|
||||||
className='cursor-pointer'
|
|
||||||
onClick={() => updateActiveItem({ verticalAlignment: 'middle' })}
|
|
||||||
/>
|
|
||||||
<AlignTop
|
|
||||||
size={20}
|
|
||||||
color='#000'
|
|
||||||
className='cursor-pointer'
|
|
||||||
onClick={() => updateActiveItem({ verticalAlignment: 'top' })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { TextType } from '../types/Types'
|
||||||
|
|
||||||
|
interface TextRendererProps {
|
||||||
|
texts: TextType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextRenderer: FC<TextRendererProps> = ({ texts }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{texts?.map((textItem, textIndex) => (
|
||||||
|
<table key={textItem.id} width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align={textItem.alignment || 'center'}
|
||||||
|
style={{
|
||||||
|
color: textItem.color || '#000000',
|
||||||
|
fontSize: `${textItem.fontSize || 14}px`,
|
||||||
|
lineHeight: '1.4',
|
||||||
|
paddingBottom: textIndex < (texts?.length || 0) - 1 ? '4px' : '0'
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: textItem.text }}
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextRenderer
|
||||||
@@ -3,11 +3,39 @@ import { Add, AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, A
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ReactQuill from 'react-quill-new';
|
import ReactQuill from 'react-quill-new';
|
||||||
|
import { usePersonalityStore } from '../store/Store';
|
||||||
|
import ColorPicker from '@/components/ColorPicker';
|
||||||
|
import { HorizontalAlignment, VerticalAlignment } from '../types/Types';
|
||||||
|
|
||||||
const TextSidebar: FC = () => {
|
const TextSidebar: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
const [color, setColor] = useState('#000');
|
||||||
|
const [horizontalPosition, setHorizontalPosition] = useState<HorizontalAlignment>(HorizontalAlignment.CENTER);
|
||||||
|
const [verticalPosition, setVerticalPosition] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE);
|
||||||
|
const { addTextToActiveItem } = usePersonalityStore();
|
||||||
|
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddText = () => {
|
||||||
|
if (value.trim()) {
|
||||||
|
addTextToActiveItem({
|
||||||
|
text: value,
|
||||||
|
color: color,
|
||||||
|
verticalAlignment: verticalPosition,
|
||||||
|
alignment: horizontalPosition,
|
||||||
|
});
|
||||||
|
|
||||||
|
// خالی کردن فیلدها بعد از ذخیره
|
||||||
|
setValue('');
|
||||||
|
setColor('#000');
|
||||||
|
setHorizontalPosition(HorizontalAlignment.CENTER);
|
||||||
|
setVerticalPosition(VerticalAlignment.MIDDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -27,39 +55,71 @@ const TextSidebar: FC = () => {
|
|||||||
}}
|
}}
|
||||||
theme="snow"
|
theme="snow"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={handleChange}
|
||||||
style={{ minHeight: '120px' }}
|
style={{ minHeight: '120px' }}
|
||||||
className='text-sm'
|
className='text-sm'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-5'>
|
||||||
|
<ColorPicker
|
||||||
|
changeColor={setColor}
|
||||||
|
label={t('setting.text_color')}
|
||||||
|
defaultColor={color}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-5 flex justify-between'>
|
<div className='mt-5 flex justify-between'>
|
||||||
<div>
|
<div>
|
||||||
<div className='text-sm'>
|
<div className='text-sm'>
|
||||||
{t('setting.horizontal_position')}
|
{t('setting.horizontal_position')}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 flex gap-4'>
|
<div className='mt-3 flex gap-4'>
|
||||||
<AlignRight size={20} color='#000' />
|
<div
|
||||||
<AlignVertically size={20} color='#000' />
|
onClick={() => setHorizontalPosition(HorizontalAlignment.RIGHT)}
|
||||||
<AlignLeft size={20} color='#000' />
|
>
|
||||||
|
<AlignRight size={20} color={horizontalPosition === HorizontalAlignment.RIGHT ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => setHorizontalPosition(HorizontalAlignment.CENTER)}
|
||||||
|
>
|
||||||
|
<AlignVertically size={20} color={horizontalPosition === HorizontalAlignment.CENTER ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => setHorizontalPosition(HorizontalAlignment.LEFT)}
|
||||||
|
>
|
||||||
|
<AlignLeft size={20} color={horizontalPosition === HorizontalAlignment.LEFT ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className='text-sm'>
|
<div className='text-sm'>
|
||||||
{t('setting.horizontal_position')}
|
{t('setting.vertical_position')}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 flex gap-4'>
|
<div className='mt-3 flex gap-4'>
|
||||||
<AlignBottom size={20} color='#000' />
|
<div
|
||||||
<AlignHorizontally size={20} color='#000' />
|
onClick={() => setVerticalPosition(VerticalAlignment.TOP)}
|
||||||
<AlignTop size={20} color='#000' />
|
>
|
||||||
|
<AlignBottom size={20} color={verticalPosition === VerticalAlignment.TOP ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => setVerticalPosition(VerticalAlignment.MIDDLE)}
|
||||||
|
>
|
||||||
|
<AlignHorizontally size={20} color={verticalPosition === VerticalAlignment.MIDDLE ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => setVerticalPosition(VerticalAlignment.BOTTOM)}
|
||||||
|
>
|
||||||
|
<AlignTop size={20} color={verticalPosition === VerticalAlignment.BOTTOM ? '#0038FF' : '#000'} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-7'>
|
<div className='mt-7'>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => null}
|
onClick={handleAddText}
|
||||||
className='bg-[#ECEEF5] text-black'
|
className='bg-[#ECEEF5] text-black'
|
||||||
>
|
>
|
||||||
<div className='flex justify-center items-center gap-2'>
|
<div className='flex justify-center items-center gap-2'>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
SectionItemType,
|
SectionItemType,
|
||||||
TextType,
|
TextType,
|
||||||
ButtonType,
|
ButtonType,
|
||||||
|
ImageType,
|
||||||
|
SectionName,
|
||||||
|
HorizontalAlignment,
|
||||||
|
VerticalAlignment,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const usePersonalityStore = create<PersonalityStore>((set) => ({
|
export const usePersonalityStore = create<PersonalityStore>((set) => ({
|
||||||
@@ -18,7 +22,7 @@ export const usePersonalityStore = create<PersonalityStore>((set) => ({
|
|||||||
activeSection: "",
|
activeSection: "",
|
||||||
activeItemIndex: 0,
|
activeItemIndex: 0,
|
||||||
|
|
||||||
setActiveSection: (section: "header" | "content" | "footer") =>
|
setActiveSection: (section: SectionName) =>
|
||||||
set({ activeSection: section, activeItemIndex: 0 }),
|
set({ activeSection: section, activeItemIndex: 0 }),
|
||||||
|
|
||||||
setActiveItemIndex: (index: number) => set({ activeItemIndex: index }),
|
setActiveItemIndex: (index: number) => set({ activeItemIndex: index }),
|
||||||
@@ -37,8 +41,9 @@ export const usePersonalityStore = create<PersonalityStore>((set) => ({
|
|||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
texts: [],
|
texts: [],
|
||||||
buttons: [],
|
buttons: [],
|
||||||
alignment: "center" as const,
|
images: [],
|
||||||
verticalAlignment: "middle" as const,
|
alignment: HorizontalAlignment.CENTER,
|
||||||
|
verticalAlignment: VerticalAlignment.MIDDLE,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -291,4 +296,81 @@ export const usePersonalityStore = create<PersonalityStore>((set) => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
addImage: (
|
||||||
|
sectionKey: keyof PersonalityDataType,
|
||||||
|
itemId: string,
|
||||||
|
image: Omit<ImageType, "id">
|
||||||
|
) =>
|
||||||
|
set((state) => ({
|
||||||
|
data: {
|
||||||
|
...state.data,
|
||||||
|
[sectionKey]: {
|
||||||
|
...state.data[sectionKey],
|
||||||
|
items: state.data[sectionKey].items.map((item) =>
|
||||||
|
item.id === itemId
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
images: [...item.images, { ...image, id: uuidv4() }],
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
addImageToActiveItem: (image: Omit<ImageType, "id">) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.activeSection) return state;
|
||||||
|
|
||||||
|
const sectionKey = state.activeSection as keyof PersonalityDataType;
|
||||||
|
const items = state.data[sectionKey].items;
|
||||||
|
const activeIndex = state.activeItemIndex;
|
||||||
|
|
||||||
|
if (activeIndex >= items.length) return state;
|
||||||
|
|
||||||
|
const updatedItems = items.map((item, index) =>
|
||||||
|
index === activeIndex
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
images: [...item.images, { ...image, id: uuidv4() }],
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
...state.data,
|
||||||
|
[sectionKey]: {
|
||||||
|
...state.data[sectionKey],
|
||||||
|
items: updatedItems,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateImage: (
|
||||||
|
sectionKey: keyof PersonalityDataType,
|
||||||
|
itemId: string,
|
||||||
|
imageId: string,
|
||||||
|
updates: Partial<ImageType>
|
||||||
|
) =>
|
||||||
|
set((state) => ({
|
||||||
|
data: {
|
||||||
|
...state.data,
|
||||||
|
[sectionKey]: {
|
||||||
|
...state.data[sectionKey],
|
||||||
|
items: state.data[sectionKey].items.map((item) =>
|
||||||
|
item.id === itemId
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
images: item.images.map((image) =>
|
||||||
|
image.id === imageId ? { ...image, ...updates } : image
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,3 +1,44 @@
|
|||||||
|
// Enums for common types
|
||||||
|
export enum HorizontalAlignment {
|
||||||
|
LEFT = "left",
|
||||||
|
CENTER = "center",
|
||||||
|
RIGHT = "right",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VerticalAlignment {
|
||||||
|
TOP = "top",
|
||||||
|
MIDDLE = "middle",
|
||||||
|
BOTTOM = "bottom",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ButtonSize {
|
||||||
|
SMALL = "small",
|
||||||
|
MEDIUM = "medium",
|
||||||
|
LARGE = "large",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ImageSize {
|
||||||
|
COVER = "cover",
|
||||||
|
CONTAIN = "contain",
|
||||||
|
AUTO = "auto",
|
||||||
|
REPEAT = "repeat",
|
||||||
|
REPEAT_X = "repeat-x",
|
||||||
|
REPEAT_Y = "repeat-y",
|
||||||
|
NO_REPEAT = "no-repeat",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BackgroundSize {
|
||||||
|
COVER = "cover",
|
||||||
|
CONTAIN = "contain",
|
||||||
|
ROUND = "round",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SectionName {
|
||||||
|
HEADER = "header",
|
||||||
|
CONTENT = "content",
|
||||||
|
FOOTER = "footer",
|
||||||
|
}
|
||||||
|
|
||||||
export type PersonalityDataType = {
|
export type PersonalityDataType = {
|
||||||
header: SectionType;
|
header: SectionType;
|
||||||
content: SectionType;
|
content: SectionType;
|
||||||
@@ -15,12 +56,16 @@ export type SectionItemType = {
|
|||||||
backgroundImage?: string;
|
backgroundImage?: string;
|
||||||
texts: TextType[];
|
texts: TextType[];
|
||||||
buttons: ButtonType[];
|
buttons: ButtonType[];
|
||||||
alignment?: "left" | "center" | "right";
|
images: ImageType[];
|
||||||
verticalAlignment?: "top" | "middle" | "bottom";
|
alignment?: HorizontalAlignment;
|
||||||
|
verticalAlignment?: VerticalAlignment;
|
||||||
style?: {
|
style?: {
|
||||||
borderRadius?: number;
|
borderRadius?: number;
|
||||||
padding?: string;
|
padding?: string;
|
||||||
fontFamily?: string;
|
fontFamily?: string;
|
||||||
|
backgroundSize?: string;
|
||||||
|
backgroundRepeat?: string;
|
||||||
|
backgroundPosition?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,30 +75,41 @@ export type TextType = {
|
|||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
fontFamily?: string;
|
fontFamily?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
alignment?: "left" | "center" | "right";
|
alignment?: HorizontalAlignment;
|
||||||
verticalAlignment?: "top" | "middle" | "bottom";
|
verticalAlignment?: VerticalAlignment;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ButtonType = {
|
export type ButtonType = {
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
link: string;
|
link: string;
|
||||||
size: "small" | "medium" | "large";
|
size: ButtonSize;
|
||||||
isBorder: boolean;
|
isBorder: boolean;
|
||||||
borderSize: number;
|
borderSize: number;
|
||||||
textColor: string;
|
textColor: string;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
borderColor: string;
|
borderColor: string;
|
||||||
alignment?: "left" | "center" | "right";
|
alignment?: HorizontalAlignment;
|
||||||
verticalAlignment?: "top" | "middle" | "bottom";
|
verticalAlignment?: VerticalAlignment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageType = {
|
||||||
|
id: string;
|
||||||
|
src: string;
|
||||||
|
alt?: string;
|
||||||
|
size: ImageSize;
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
alignment?: HorizontalAlignment;
|
||||||
|
verticalAlignment?: VerticalAlignment;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PersonalityStore = {
|
export type PersonalityStore = {
|
||||||
data: PersonalityDataType;
|
data: PersonalityDataType;
|
||||||
activeSection: "header" | "content" | "footer" | "";
|
activeSection: SectionName | "";
|
||||||
activeItemIndex: number;
|
activeItemIndex: number;
|
||||||
|
|
||||||
setActiveSection: (section: "header" | "content" | "footer") => void;
|
setActiveSection: (section: SectionName) => void;
|
||||||
setActiveItemIndex: (index: number) => void;
|
setActiveItemIndex: (index: number) => void;
|
||||||
|
|
||||||
setItems: (
|
setItems: (
|
||||||
@@ -99,4 +155,16 @@ export type PersonalityStore = {
|
|||||||
buttonId: string,
|
buttonId: string,
|
||||||
updates: Partial<ButtonType>
|
updates: Partial<ButtonType>
|
||||||
) => void;
|
) => void;
|
||||||
|
addImage: (
|
||||||
|
sectionKey: keyof PersonalityDataType,
|
||||||
|
itemId: string,
|
||||||
|
image: Omit<ImageType, "id">
|
||||||
|
) => void;
|
||||||
|
addImageToActiveItem: (image: Omit<ImageType, "id">) => void;
|
||||||
|
updateImage: (
|
||||||
|
sectionKey: keyof PersonalityDataType,
|
||||||
|
itemId: string,
|
||||||
|
imageId: string,
|
||||||
|
updates: Partial<ImageType>
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user