personality background image and background color

This commit is contained in:
hamid zarghami
2025-07-01 08:50:22 +03:30
parent 0ce7e86120
commit 2babdb1897
13 changed files with 876 additions and 51 deletions
+1
View File
@@ -38,6 +38,7 @@
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
"three": "^0.177.0",
"uuid": "^11.1.0",
"vite-tsconfig-paths": "^5.1.4",
"zustand": "^5.0.3"
},
+9
View File
@@ -89,6 +89,9 @@ importers:
three:
specifier: ^0.177.0
version: 0.177.0
uuid:
specifier: ^11.1.0
version: 11.1.0
vite-tsconfig-paths:
specifier: ^5.1.4
version: 5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@24.0.7)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))
@@ -2835,6 +2838,10 @@ packages:
resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==}
engines: {node: '>= 4'}
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
validator@13.15.15:
resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==}
engines: {node: '>= 0.10'}
@@ -5920,6 +5927,8 @@ snapshots:
utility-types@3.11.0: {}
uuid@11.1.0: {}
validator@13.15.15: {}
vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@24.0.7)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)):
+3 -1
View File
@@ -109,12 +109,14 @@
"add_address": "اضافه کردن نشانی",
"domain1": "دامنه",
"create": "ساخت",
"header_email": "هدر ایمیل شما",
"content_email": "محتوای ایمیل شما",
"footer_email": "پاورقی ایمیل شما",
"signature": "امضا",
"signature_description": "تصویر و توضیحات امضای خود را با دقت وارد کنید",
"upload_sign": "آپلود امضا",
"description_sign": "توضیحات امضا"
"description_sign": "توضیحات امضا",
"select_section": "لطفا یک بخش انتخاب کنید"
},
"select_file": "انتخاب فایل",
"upload": "آپلود",
+3 -1
View File
@@ -1,16 +1,18 @@
import Button from '@/components/Button'
import { ArrowRight, InfoCircle, SmsNotification, Star, VolumeMute, More, DocumentDownload, ArchiveTick, Printer, Trash } from 'iconsax-react'
import { useNavigate } from 'react-router-dom'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
const Header: FC = () => {
const navigate = useNavigate()
const { t } = useTranslation()
return (
<div className='flex justify-between items-center border-b border-border pb-6'>
<div className='flex xl:gap-4 gap-2'>
<ArrowRight size={18} color='#8C90A3' />
<ArrowRight size={18} color='#8C90A3' onClick={() => navigate(-1)} />
<SmsNotification size={18} color='black' className='xl:block hidden' />
<ArchiveTick size={18} color='black' className='xl:block hidden' />
<InfoCircle size={18} color='black' className='xl:block hidden' />
+1 -1
View File
@@ -40,7 +40,7 @@ const Domain: FC = () => {
</div>
</div>
<div className='mt-9'>
<div className='flex xl:flex-row gap-4 flex-col justify-between xl:items-end pb-10'>
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
<div className='flex-1'>
<div className='xl:text-lg'>
{t('setting.setting_smtp')}
+10 -22
View File
@@ -1,30 +1,18 @@
import { AddCircle } from 'iconsax-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import HeaderSection from './components/HeaderSection'
import ContentSection from './components/ContentSection'
import FooterSection from './components/FooterSection'
const Personality: FC = () => {
const { t } = useTranslation()
return (
<div className='flex-1 bg-white rounded-4xl p-8'>
<div className='border flex items-center p-2.5 gap-4 border-dashed border-border h-[123px] rounded-4xl'>
<div className='flex-1 h-[103px] rounded-4xl border border-dashed border-[#0038FF]'></div>
<div className='flex-1 h-[103px] rounded-4xl border border-dashed border-border'></div>
</div>
<div className='mt-4 h-[286px] border border-dashed border-border rounded-4xl flex flex-col items-center justify-center p-2.5'>
<div className='text-description'>{t('setting.content_email')}</div>
<div className='mt-1'>
<AddCircle size={24} color='#888' />
</div>
</div>
<div className='border flex flex-col justify-center mt-4 items-center p-2.5 gap-4 border-dashed border-border h-[123px] rounded-4xl'>
<div className='text-description'>{t('setting.footer_email')}</div>
<div className='mt-1'>
<AddCircle size={24} color='#888' />
</div>
</div>
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
<tbody>
<HeaderSection />
<ContentSection />
<FooterSection />
</tbody>
</table>
</div>
)
}
@@ -6,12 +6,29 @@ import { Checkbox } from '@/components/ui/checkbox'
import { AlignRight, AlignBottom, AlignHorizontally, AlignLeft, AlignTop, AlignVertically, Link2, Add } from 'iconsax-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePersonalityStore } from '../store/Store'
const ButtonSidebar: FC = () => {
const { t } = useTranslation()
const { addButtonToActiveItem } = usePersonalityStore()
const [isBorder, setIsBorder] = useState<boolean>(false)
const handleAddButton = () => {
addButtonToActiveItem({
text: 'دکمه جدید',
link: '#',
size: 'medium',
isBorder: true,
borderSize: 1,
textColor: '#ffffff',
backgroundColor: '#0038FF',
borderColor: '#0038FF',
alignment: 'center',
verticalAlignment: 'middle',
})
}
return (
<div>
<div className='text-lg'>{t('setting.button')}</div>
@@ -90,7 +107,7 @@ const ButtonSidebar: FC = () => {
<div>
<div className='text-sm'>
{t('setting.horizontal_position')}
{t('setting.vertical_position')}
</div>
<div className='mt-3 flex gap-4'>
<AlignBottom size={20} color='#000' />
@@ -102,7 +119,7 @@ const ButtonSidebar: FC = () => {
<div className='mt-7'>
<Button
onClick={() => null}
onClick={handleAddButton}
className='bg-[#ECEEF5] text-black'
>
<div className='flex justify-center items-center gap-2'>
@@ -0,0 +1,118 @@
import { FC } from 'react'
import { usePersonalityStore } from '../store/Store'
import { useTranslation } from 'react-i18next'
import { AddCircle } from 'iconsax-react'
const ContentSection: FC = () => {
const { t } = useTranslation()
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
const handleSectionClick = (section: "header" | "content" | "footer") => {
setActiveSection(section)
}
const handleItemClick = (index: number) => {
setActiveItemIndex(index)
}
return (
<tr onClick={() => handleSectionClick("content")}>
<td style={{ paddingTop: '16px' }}>
{
data.content.columnsCount > 0 ?
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td style={{
padding: '10px',
border: activeSection === "content" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px'
}}>
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
{
Array.from({ length: data.content.columnsCount }, (_, index) => {
const item = data.content.items[index];
const isActive = activeSection === "content" && activeItemIndex === index;
return (
<>
<td
onClick={(e) => {
e.stopPropagation();
handleItemClick(index);
}}
key={index}
style={{
height: '246px',
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
backgroundColor: item?.backgroundColor || '#ffffff',
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
cursor: 'pointer'
}}
>
{/* محتوای آیتم */}
{item?.texts?.map(text => (
<div key={text.id} style={{ color: text.color }}>
{text.text}
</div>
))}
{item?.buttons?.map(button => (
<button
key={button.id}
style={{
backgroundColor: button.backgroundColor,
color: button.textColor
}}
>
{button.text}
</button>
))}
</td>
{index < data.content.columnsCount - 1 && (
<td style={{ width: '16px' }}></td>
)}
</>
)
})
}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
:
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td style={{
height: '286px',
border: activeSection === "content" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px',
textAlign: 'center',
verticalAlign: 'middle',
padding: '10px'
}}>
<div style={{ color: '#888', fontSize: '14px', marginBottom: '8px' }}>
{t('setting.content_email')}
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<AddCircle size={24} color='#888' />
</div>
</td>
</tr>
</tbody>
</table>
}
</td>
</tr>
)
}
export default ContentSection
@@ -0,0 +1,118 @@
import { FC } from 'react'
import { usePersonalityStore } from '../store/Store'
import { useTranslation } from 'react-i18next'
import { AddCircle } from 'iconsax-react'
const FooterSection: FC = () => {
const { t } = useTranslation()
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
const handleSectionClick = (section: "header" | "content" | "footer") => {
setActiveSection(section)
}
const handleItemClick = (index: number) => {
setActiveItemIndex(index)
}
return (
<tr onClick={() => handleSectionClick("footer")}>
<td style={{ paddingTop: '16px' }}>
{
data.footer.columnsCount > 0 ?
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td style={{
padding: '10px',
border: activeSection === "footer" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px'
}}>
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
{
Array.from({ length: data.footer.columnsCount }, (_, index) => {
const item = data.footer.items[index];
const isActive = activeSection === "footer" && activeItemIndex === index;
return (
<>
<td
onClick={(e) => {
e.stopPropagation();
handleItemClick(index);
}}
key={index}
style={{
height: '83px',
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
backgroundColor: item?.backgroundColor || '#ffffff',
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
cursor: 'pointer'
}}
>
{/* محتوای آیتم */}
{item?.texts?.map(text => (
<div key={text.id} style={{ color: text.color }}>
{text.text}
</div>
))}
{item?.buttons?.map(button => (
<button
key={button.id}
style={{
backgroundColor: button.backgroundColor,
color: button.textColor
}}
>
{button.text}
</button>
))}
</td>
{index < data.footer.columnsCount - 1 && (
<td style={{ width: '16px' }}></td>
)}
</>
)
})
}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
:
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td style={{
height: '123px',
border: activeSection === "footer" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px',
textAlign: 'center',
verticalAlign: 'middle',
padding: '10px'
}}>
<div style={{ color: '#888', fontSize: '14px', marginBottom: '8px' }}>
{t('setting.footer_email')}
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<AddCircle size={24} color='#888' />
</div>
</td>
</tr>
</tbody>
</table>
}
</td>
</tr>
)
}
export default FooterSection
@@ -0,0 +1,104 @@
import { FC } from 'react'
import { usePersonalityStore } from '../store/Store'
import { useTranslation } from 'react-i18next'
import { AddCircle } from 'iconsax-react'
const HeaderSection: FC = () => {
const { t } = useTranslation()
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
const handleSectionClick = (section: "header" | "content" | "footer") => {
setActiveSection(section)
}
const handleItemClick = (index: number) => {
setActiveItemIndex(index)
}
return (
<tr onClick={() => handleSectionClick("header")}>
{
data.header.columnsCount > 0 ?
<td style={{
padding: '10px',
border: activeSection === "header" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px'
}}>
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
{
Array.from({ length: data.header.columnsCount }, (_, index) => {
const item = data.header.items[index];
const isActive = activeSection === "header" && activeItemIndex === index;
return (
<>
<td
onClick={(e) => {
e.stopPropagation();
handleItemClick(index);
}}
key={index}
style={{
height: '103px',
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
backgroundColor: item?.backgroundColor || '#ffffff',
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
cursor: 'pointer'
}}
>
{/* محتوای آیتم */}
{item?.texts?.map(text => (
<div key={text.id} style={{ color: text.color }}>
{text.text}
</div>
))}
{item?.buttons?.map(button => (
<button
key={button.id}
style={{
backgroundColor: button.backgroundColor,
color: button.textColor
}}
>
{button.text}
</button>
))}
</td>
{index < data.header.columnsCount - 1 && (
<td style={{ width: '16px' }}></td>
)}
</>
)
})
}
</tr>
</tbody>
</table>
</td>
:
<td style={{
height: '123px',
border: activeSection === "header" ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px',
textAlign: 'center',
verticalAlign: 'middle',
padding: '10px'
}}>
<div style={{ color: '#888', fontSize: '14px', marginBottom: '8px' }}>
{t('setting.header_email')}
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<AddCircle size={24} color='#888' />
</div>
</td>
}
</tr>
)
}
export default HeaderSection
@@ -4,35 +4,75 @@ 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 { usePersonalityStore } from '../store/Store';
const SettingSideBar: FC = () => {
const { t } = useTranslation()
const { activeSection, setColumnsCount, updateActiveItem } = usePersonalityStore()
const [color, setColor] = useState("#aabbcc");
const handleColumnClick = (columns: number) => {
if (activeSection) {
setColumnsCount(activeSection as "header" | "content" | "footer", columns);
}
}
const handleColorChange = (color: string) => {
setColor(color)
updateActiveItem({ backgroundColor: color })
}
const handleUploadBackground = (file: File[]) => {
if (activeSection && file.length > 0) {
const imageUrl = URL.createObjectURL(file[0])
console.log('Setting background image:', imageUrl)
updateActiveItem({ backgroundImage: imageUrl })
}
}
if (!activeSection) {
return (
<div className='text-center text-gray-500 mt-8'>
{t('setting.select_section')}
</div>
)
}
return (
<div>
<div className='text-lg'>{t('setting.setting')}</div>
<div className='flex h-[51px] gap-5 mt-6'>
<div className='w-[102px] h-full bg-[#EAECF4]'></div>
<div className='w-[102px] h-full flex gap-1'>
<div className='flex-1 bg-[#EAECF4]'></div>
<div className='flex-1 bg-[#EAECF4]'></div>
<div
className='w-[102px] h-full bg-[#EAECF4] cursor-pointer hover:bg-[#d1d5db] transition-colors'
onClick={() => handleColumnClick(1)}
></div>
<div
className='w-[102px] h-full flex gap-1 cursor-pointer group'
onClick={() => handleColumnClick(2)}
>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
</div>
</div>
<div className='flex h-[51px] gap-5 mt-5'>
<div className='w-[102px] h-full flex gap-1'>
<div className='flex-1 bg-[#EAECF4]'></div>
<div className='flex-1 bg-[#EAECF4]'></div>
<div className='flex-1 bg-[#EAECF4]'></div>
<div
className='w-[102px] h-full flex gap-1 cursor-pointer group'
onClick={() => handleColumnClick(3)}
>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
</div>
<div className='w-[102px] h-full flex gap-1'>
<div className='flex-1 bg-[#EAECF4]'></div>
<div className='flex-1 bg-[#EAECF4]'></div>
<div className='flex-1 bg-[#EAECF4]'></div>
<div className='flex-1 bg-[#EAECF4]'></div>
<div
className='w-[102px] h-full flex gap-1 cursor-pointer group'
onClick={() => handleColumnClick(4)}
>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
</div>
</div>
@@ -40,14 +80,14 @@ const SettingSideBar: FC = () => {
<ColorPicker
label={t('setting.background')}
defaultColor={color}
changeColor={setColor}
changeColor={handleColorChange}
/>
</div>
<div className='mt-5'>
<UploadBoxDraggble
label={t('setting.upload_background')}
onChange={() => null}
onChange={handleUploadBackground}
/>
</div>
@@ -65,20 +105,50 @@ const SettingSideBar: FC = () => {
{t('setting.horizontal_position')}
</div>
<div className='mt-3 flex gap-4'>
<AlignRight size={20} color='#000' />
<AlignVertically size={20} color='#000' />
<AlignLeft size={20} color='#000' />
<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.horizontal_position')}
{t('setting.vertical_position')}
</div>
<div className='mt-3 flex gap-4'>
<AlignBottom size={20} color='#000' />
<AlignHorizontally size={20} color='#000' />
<AlignTop size={20} color='#000' />
<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>
@@ -0,0 +1,294 @@
import { create } from "zustand";
import { v4 as uuidv4 } from "uuid";
import {
PersonalityStore,
PersonalityDataType,
SectionItemType,
TextType,
ButtonType,
} from "../types/Types";
export const usePersonalityStore = create<PersonalityStore>((set) => ({
data: {
header: { columnsCount: 0, items: [] },
content: { columnsCount: 0, items: [] },
footer: { columnsCount: 0, items: [] },
},
activeSection: "",
activeItemIndex: 0,
setActiveSection: (section: "header" | "content" | "footer") =>
set({ activeSection: section, activeItemIndex: 0 }),
setActiveItemIndex: (index: number) => set({ activeItemIndex: index }),
setColumnsCount: (
sectionKey: keyof PersonalityDataType,
columnsCount: number
) =>
set((state) => {
const currentItems = state.data[sectionKey].items;
const newItems = Array.from(
{ length: columnsCount },
(_, index) =>
currentItems[index] || {
id: uuidv4(),
backgroundColor: "#ffffff",
texts: [],
buttons: [],
alignment: "center" as const,
verticalAlignment: "middle" as const,
}
);
return {
data: {
...state.data,
[sectionKey]: {
columnsCount,
items: newItems,
},
},
activeItemIndex: 0,
};
}),
setItems: (sectionKey: keyof PersonalityDataType, items: SectionItemType[]) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items,
},
},
})),
addItem: (
sectionKey: keyof PersonalityDataType,
item: Omit<SectionItemType, "id">
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: [...state.data[sectionKey].items, { ...item, id: uuidv4() }],
},
},
})),
updateActiveItem: (updates: Partial<SectionItemType>) =>
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;
console.log("Updating active item:", {
sectionKey,
activeIndex,
updates,
});
const updatedItems = items.map((item, index) =>
index === activeIndex ? { ...item, ...updates } : item
);
return {
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: updatedItems,
},
},
};
}),
updateItem: (
sectionKey: keyof PersonalityDataType,
id: string,
updates: Partial<SectionItemType>
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === id ? { ...item, ...updates } : item
),
},
},
})),
removeItem: (sectionKey: keyof PersonalityDataType, id: string) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.filter((item) => item.id !== id),
},
},
})),
addText: (
sectionKey: keyof PersonalityDataType,
itemId: string,
text: Omit<TextType, "id">
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
texts: [...item.texts, { ...text, id: uuidv4() }],
}
: item
),
},
},
})),
addTextToActiveItem: (text: Omit<TextType, "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,
texts: [...item.texts, { ...text, id: uuidv4() }],
}
: item
);
return {
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: updatedItems,
},
},
};
}),
addButton: (
sectionKey: keyof PersonalityDataType,
itemId: string,
button: Omit<ButtonType, "id">
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
buttons: [...item.buttons, { ...button, id: uuidv4() }],
}
: item
),
},
},
})),
addButtonToActiveItem: (button: Omit<ButtonType, "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,
buttons: [...item.buttons, { ...button, id: uuidv4() }],
}
: item
);
return {
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: updatedItems,
},
},
};
}),
updateText: (
sectionKey: keyof PersonalityDataType,
itemId: string,
textId: string,
updates: Partial<TextType>
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
texts: item.texts.map((text) =>
text.id === textId ? { ...text, ...updates } : text
),
}
: item
),
},
},
})),
updateButton: (
sectionKey: keyof PersonalityDataType,
itemId: string,
buttonId: string,
updates: Partial<ButtonType>
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
buttons: item.buttons.map((button) =>
button.id === buttonId ? { ...button, ...updates } : button
),
}
: item
),
},
},
})),
}));
@@ -0,0 +1,102 @@
export type PersonalityDataType = {
header: SectionType;
content: SectionType;
footer: SectionType;
};
export type SectionType = {
columnsCount: number;
items: SectionItemType[];
};
export type SectionItemType = {
id: string;
backgroundColor: string;
backgroundImage?: string;
texts: TextType[];
buttons: ButtonType[];
alignment?: "left" | "center" | "right";
verticalAlignment?: "top" | "middle" | "bottom";
style?: {
borderRadius?: number;
padding?: string;
fontFamily?: string;
};
};
export type TextType = {
id: string;
text: string;
fontSize?: number;
fontFamily?: string;
color?: string;
alignment?: "left" | "center" | "right";
verticalAlignment?: "top" | "middle" | "bottom";
};
export type ButtonType = {
id: string;
text: string;
link: string;
size: "small" | "medium" | "large";
isBorder: boolean;
borderSize: number;
textColor: string;
backgroundColor: string;
borderColor: string;
alignment?: "left" | "center" | "right";
verticalAlignment?: "top" | "middle" | "bottom";
};
export type PersonalityStore = {
data: PersonalityDataType;
activeSection: "header" | "content" | "footer" | "";
activeItemIndex: number;
setActiveSection: (section: "header" | "content" | "footer") => void;
setActiveItemIndex: (index: number) => void;
setItems: (
sectionKey: keyof PersonalityDataType,
items: SectionItemType[]
) => void;
addItem: (
sectionKey: keyof PersonalityDataType,
item: Omit<SectionItemType, "id">
) => void;
updateItem: (
sectionKey: keyof PersonalityDataType,
id: string,
updates: Partial<SectionItemType>
) => void;
updateActiveItem: (updates: Partial<SectionItemType>) => void;
removeItem: (sectionKey: keyof PersonalityDataType, id: string) => void;
setColumnsCount: (
sectionKey: keyof PersonalityDataType,
columnsCount: number
) => void;
addText: (
sectionKey: keyof PersonalityDataType,
itemId: string,
text: Omit<TextType, "id">
) => void;
addTextToActiveItem: (text: Omit<TextType, "id">) => void;
addButton: (
sectionKey: keyof PersonalityDataType,
itemId: string,
button: Omit<ButtonType, "id">
) => void;
addButtonToActiveItem: (button: Omit<ButtonType, "id">) => void;
updateText: (
sectionKey: keyof PersonalityDataType,
itemId: string,
textId: string,
updates: Partial<TextType>
) => void;
updateButton: (
sectionKey: keyof PersonalityDataType,
itemId: string,
buttonId: string,
updates: Partial<ButtonType>
) => void;
};