add renderer components

This commit is contained in:
hamid zarghami
2025-07-01 14:36:27 +03:30
parent 2babdb1897
commit 7638e07e39
13 changed files with 1102 additions and 192 deletions
+9 -1
View File
@@ -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
width="100%"
height="246"
align={getButtonHorizontalAlign(item.buttons[0])}
valign={getButtonVerticalAlign(item.buttons[0])}
style={{ style={{
backgroundColor: button.backgroundColor, padding: '8px'
color: button.textColor
}} }}
> >
{button.text} <ButtonRenderer buttons={item.buttons} />
</button> </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
width="100%"
height="123"
align={getButtonHorizontalAlign(item.buttons[0])}
valign={getButtonVerticalAlign(item.buttons[0])}
style={{ style={{
backgroundColor: button.backgroundColor, padding: '8px'
color: button.textColor
}} }}
> >
{button.text} <ButtonRenderer buttons={item.buttons} />
</button> </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
width="100%"
height="123"
align={getButtonHorizontalAlign(item.buttons[0])}
valign={getButtonVerticalAlign(item.buttons[0])}
style={{ style={{
backgroundColor: button.backgroundColor, padding: '8px'
color: button.textColor
}} }}
> >
{button.text} <ButtonRenderer buttons={item.buttons} />
</button> </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 */}
&nbsp;
</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'>
+85 -3
View File
@@ -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
),
},
},
})),
})); }));
+77 -9
View File
@@ -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;
}; };