This commit is contained in:
hamid zarghami
2025-06-23 18:06:47 +03:30
parent 95ece3691c
commit 717746e13e
12 changed files with 435 additions and 398 deletions
+9 -1
View File
@@ -1,5 +1,5 @@
import Tabs from '@/components/Tabs'
import { Brush2, Global, People, Setting3 } from 'iconsax-react'
import { Brush2, Global, PenClose, People, Setting3 } from 'iconsax-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingTabEnum } from './enum/SettingEnum'
@@ -8,6 +8,7 @@ import Domain from './domain/Domain'
import MailServer from './mail-server/MailServer'
import Address from './address/Address'
import PersonalitySidebar from './personality/SideBar'
import Signture from './signture/Signture'
const Setting: FC = () => {
const { t } = useTranslation()
@@ -21,6 +22,8 @@ const Setting: FC = () => {
return <Domain />;
case SettingTabEnum.SETTING_ADDRESS:
return <Address />;
case SettingTabEnum.SETTING_SIGNATURE:
return <Signture />;
case SettingTabEnum.SETTING_PERSONALITY:
return (
<div className='flex flex-col xl:flex-row-reverse gap-4 md:gap-6 mt-6 md:mt-8'>
@@ -63,6 +66,11 @@ const Setting: FC = () => {
icon: <Brush2 size={20} color={activeTab === SettingTabEnum.SETTING_PERSONALITY ? 'black' : '#8C90A3'} />,
label: t('setting.personality'),
value: SettingTabEnum.SETTING_PERSONALITY
},
{
icon: <PenClose size={20} color={activeTab === SettingTabEnum.SETTING_SIGNATURE ? 'black' : '#8C90A3'} />,
label: t('setting.signature'),
value: SettingTabEnum.SETTING_SIGNATURE
}
]}
active={activeTab}
+164 -221
View File
@@ -1,244 +1,187 @@
import Input from '@/components/Input'
import Select from '@/components/Select'
import Table from '@/components/Table'
import { Switch } from "@/components/ui/switch"
import { ColumnType } from '@/components/types/TableTypes'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Add, Edit2, Trash } from 'iconsax-react'
import Button from '@/components/Button'
import { TickCircle } from 'iconsax-react'
interface EmailAddress {
interface AddressData extends Record<string, unknown> {
id: number
title: string
email: string
name: string
isPrimary: boolean
status: boolean
}
const Address: FC = () => {
const { t } = useTranslation()
const [addresses, setAddresses] = useState<EmailAddress[]>([
{ id: 1, email: 'user@example.com', name: 'Main Account', isPrimary: true }
const [addresses] = useState<AddressData[]>([
{
id: 1,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
},
{
id: 2,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
},
{
id: 3,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
},
{
id: 4,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
},
{
id: 5,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
}
])
const [showAddForm, setShowAddForm] = useState(false)
const [newEmail, setNewEmail] = useState('')
const [newName, setNewName] = useState('')
const [editingId, setEditingId] = useState<number | null>(null)
const handleAddAddress = () => {
if (newEmail && newName) {
const newId = addresses.length > 0 ? Math.max(...addresses.map(a => a.id)) + 1 : 1
setAddresses([...addresses, {
id: newId,
email: newEmail,
name: newName,
isPrimary: addresses.length === 0
}])
setNewEmail('')
setNewName('')
setShowAddForm(false)
const columns: ColumnType<AddressData>[] = [
{
title: t('setting.address_title'),
key: 'title',
width: '200px'
},
{
title: t('setting.email'),
key: 'email',
width: '300px'
},
{
title: t('setting.status'),
key: 'status',
width: '120px',
align: 'center',
render: (item: AddressData) => (
<div key={item.id} className='dltr flex justify-end'>
<Switch />
</div>
)
}
}
]
const handleEditStart = (address: EmailAddress) => {
setEditingId(address.id)
setNewEmail(address.email)
setNewName(address.name)
}
const handleEditSave = () => {
if (editingId && newEmail && newName) {
setAddresses(addresses.map(addr =>
addr.id === editingId
? { ...addr, email: newEmail, name: newName }
: addr
))
setEditingId(null)
setNewEmail('')
setNewName('')
}
}
const handleSetPrimary = (id: number) => {
setAddresses(addresses.map(addr => ({
...addr,
isPrimary: addr.id === id
})))
}
const handleDelete = (id: number) => {
const filteredAddresses = addresses.filter(addr => addr.id !== id)
// If we're deleting the primary address, make the first one primary
if (addresses.find(addr => addr.id === id)?.isPrimary && filteredAddresses.length > 0) {
filteredAddresses[0].isPrimary = true
}
setAddresses(filteredAddresses)
}
const statusOptions = [
{ value: 'all', label: 'همه' },
{ value: 'active', label: t('setting.active') },
{ value: 'inactive', label: t('setting.inactive') }
]
return (
<div className='bg-white rounded-4xl p-8'>
<div className='flex justify-between mb-8'>
<h2 className='text-xl font-semibold'>{t('setting.address')}</h2>
<button className='bg-primary text-white px-6 py-2 rounded-lg'>{t('common.save')}</button>
</div>
<div className='mb-6'>
<div className='flex justify-between items-center mb-4'>
<h3 className='text-md font-medium'>{t('setting.email_addresses')}</h3>
{!showAddForm && (
<button
onClick={() => setShowAddForm(true)}
className='flex items-center gap-1 text-primary'
>
<Add size={18} />
<span>{t('setting.add_address')}</span>
</button>
)}
</div>
{showAddForm && (
<div className='p-4 bg-gray-50 rounded-lg mb-4'>
<h4 className='font-medium mb-3'>{t('setting.new_address')}</h4>
<div className='grid grid-cols-1 gap-4 mb-4'>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>{t('setting.email')}</label>
<input
type="email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
className='w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent'
placeholder='email@domain.com'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>{t('setting.display_name')}</label>
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
className='w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent'
placeholder={t('setting.display_name_placeholder')}
/>
</div>
</div>
<div className='flex justify-end gap-2'>
<button
onClick={() => {
setShowAddForm(false)
setNewEmail('')
setNewName('')
}}
className='px-4 py-2 border border-gray-300 rounded-lg'
>
{t('common.cancel')}
</button>
<button
onClick={handleAddAddress}
disabled={!newEmail || !newName}
className={`px-4 py-2 rounded-lg ${!newEmail || !newName ? 'bg-gray-300 text-gray-500' : 'bg-primary text-white'}`}
>
{t('common.save')}
</button>
</div>
</div>
)}
<div className='space-y-3'>
{addresses.map((address) => (
<div key={address.id} className='p-4 bg-gray-50 rounded-lg'>
{editingId === address.id ? (
<div className='grid grid-cols-1 gap-4 mb-4'>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>{t('setting.email')}</label>
<input
type="email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
className='w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>{t('setting.display_name')}</label>
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
className='w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent'
/>
</div>
<div className='flex justify-end gap-2'>
<button
onClick={() => {
setEditingId(null)
setNewEmail('')
setNewName('')
}}
className='px-4 py-2 border border-gray-300 rounded-lg'
>
{t('common.cancel')}
</button>
<button
onClick={handleEditSave}
disabled={!newEmail || !newName}
className={`px-4 py-2 rounded-lg ${!newEmail || !newName ? 'bg-gray-300 text-gray-500' : 'bg-primary text-white'}`}
>
{t('common.save')}
</button>
</div>
</div>
) : (
<div className='flex items-center justify-between'>
<div>
<div className='flex items-center gap-2'>
<p className='font-medium'>{address.name}</p>
{address.isPrimary && (
<span className='px-2 py-0.5 bg-green-100 text-green-600 text-xs rounded-full'>
{t('setting.primary')}
</span>
)}
</div>
<p className='text-gray-500'>{address.email}</p>
</div>
<div className='flex items-center gap-2'>
{!address.isPrimary && (
<>
<button
onClick={() => handleSetPrimary(address.id)}
className='text-blue-600 px-3 py-1 text-sm'
>
{t('setting.set_primary')}
</button>
<button
onClick={() => handleEditStart(address)}
className='text-gray-500'
>
<Edit2 size={18} />
</button>
<button
onClick={() => handleDelete(address.id)}
className='text-gray-500'
>
<Trash size={18} />
</button>
</>
)}
</div>
</div>
)}
</div>
))}
</div>
</div>
<div className='p-4 bg-yellow-50 rounded-lg border border-yellow-100'>
<div className='flex items-start gap-3'>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22Z" stroke="#F59E0B" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12 8V13" stroke="#F59E0B" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.9941 16H12.0031" stroke="#F59E0B" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div className='flex gap-8 mt-8'>
<div className='flex-1'>
<div className='flex justify-between'>
<div>
<h4 className='font-medium text-yellow-800'>{t('setting.verification_required')}</h4>
<p className='text-sm text-yellow-700 mt-1'>{t('setting.verification_note')}</p>
<Select
placeholder={t('setting.status')}
items={statusOptions}
className='w-[120px]'
/>
</div>
<div>
<Input
variant='search'
placeholder={t('setting.search')}
className='w-[300px]'
/>
</div>
</div>
<Table
columns={columns}
data={addresses}
pagination={{
currentPage: 1,
totalPages: 6,
onPageChange: (page) => console.log('Page:', page)
}}
/>
</div>
<div className='w-[350px] bg-white rounded-4xl p-8'>
<div>
{t('setting.add_address')}
</div>
<div className='mt-8 flex items-center justify-between'>
<div>
{t('setting.status')}
</div>
<div className='dltr pt-2'>
<Switch />
</div>
</div>
<div className='mt-6'>
<Input
label={t('setting.domain1')}
value={'example.com'}
readOnly
/>
</div>
<div className='mt-6'>
<Input
label={t('setting.title')}
/>
</div>
<div className='mt-6 relative'>
<Input
label={t('setting.username')}
className='text-left'
/>
<div className='absolute flex items-center dltr text-xs z-10 right-[1px] top-[29px] rounded-r-2.5 h-[38px] bg-[#EBEEF5] px-3'>
@ example.com
</div>
</div>
<div className='mt-6'>
<Input
label={t('setting.password')}
type='password'
/>
</div>
<div className='mt-2 text-xs'>
<div>رمز عبور میبایست:</div>
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
<li>حداقل ۸ کاراکتر باشد</li>
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
<li>شامل اعداد باشد</li>
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
</ul>
</div>
<div className='mt-9 flex justify-end'>
<Button
className='w-fit px-20'
>
<div className='flex gap-2'>
<TickCircle size={20} color='white' />
<div>
{t('setting.create')}
</div>
</div>
</Button>
</div>
</div>
</div>
)
}
export default Address
export default Address
+1 -1
View File
@@ -106,7 +106,7 @@ const Domain: FC = () => {
return (
<div>
<div className='flex justify-between'>
<div className='flex justify-between mt-9'>
<div>
<div className='text-lg'>
{t('setting.record_dns')}
+1
View File
@@ -3,6 +3,7 @@ export enum SettingTabEnum {
SETTING_DOMAIN = "setting_domain",
SETTING_ADDRESS = "setting_address",
SETTING_PERSONALITY = "setting_personality",
SETTING_SIGNATURE = "setting_signature",
}
export enum SideBarTab {
+18 -174
View File
@@ -1,185 +1,29 @@
import { FC, useState } from 'react'
import { Add, Edit } from 'iconsax-react'
import { AddCircle } from 'iconsax-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
const Personality: FC = () => {
const [backgroundImage, setBackgroundImage] = useState<string | null>(null)
const [emailContent, setEmailContent] = useState<string>('')
const [emailFooter, setEmailFooter] = useState<string>('')
const [showContentEditor, setShowContentEditor] = useState<boolean>(false)
const [showFooterEditor, setShowFooterEditor] = useState<boolean>(false)
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
setBackgroundImage(e.target?.result as string)
}
reader.readAsDataURL(file)
}
}
const handleAddEmailContent = () => {
if (!emailContent) {
setEmailContent('متن ایمیل خود را اینجا وارد کنید')
}
setShowContentEditor(true)
}
const handleAddEmailFooter = () => {
if (!emailFooter) {
setEmailFooter('پاورقی ایمیل خود را اینجا وارد کنید')
}
setShowFooterEditor(true)
}
const { t } = useTranslation()
return (
<div className='bg-white rounded-4xl p-6 flex-1'>
{/* Header buttons */}
<div className='flex justify-between mb-8'>
<button className='bg-black text-white px-5 py-2 rounded-xl text-sm'>ذخیره</button>
{/* <div className='flex gap-3'>
<button className='w-10 h-10 flex items-center justify-center bg-white border border-gray-200 rounded-full'>
<Edit size={20} variant="Bold" color="#FF008A" />
</button>
<button className='w-10 h-10 flex items-center justify-center bg-white border border-gray-200 rounded-full'>
<ArrowRight size={20} />
</button>
<button className='w-10 h-10 flex items-center justify-center bg-white border border-gray-200 rounded-full'>
<ArrowLeft size={20} />
</button>
<button className='w-10 h-10 flex items-center justify-center bg-white border border-gray-200 rounded-full'>
<Eye size={20} />
</button>
</div> */}
<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>
{/* Background image section */}
<div className='border border-dashed border-gray-300 rounded-2xl p-8 mb-6'>
{backgroundImage ? (
<div className='relative w-full'>
<img src={backgroundImage} alt="Background" className='w-full h-48 object-cover rounded-lg' />
<button
className='absolute top-2 right-2 bg-white p-1 rounded-full shadow-sm'
onClick={() => setBackgroundImage(null)}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22Z" stroke="#292D32" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.17 14.83L14.83 9.17" stroke="#292D32" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.83 14.83L9.17 9.17" stroke="#292D32" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</div>
) : (
<div className='flex flex-col items-center justify-center py-10'>
<div className='text-gray-500 mb-4'>تصویر پس زمینه مورد نظر را آپلود کنید</div>
<label className='flex items-center gap-2 bg-white border border-gray-200 rounded-lg px-4 py-2 text-sm cursor-pointer'>
<input
type="file"
accept="image/*"
className='hidden'
onChange={handleImageUpload}
/>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 17V11L7 13" stroke="#292D32" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9 11L11 13" stroke="#292D32" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 10V15C22 20 20 22 15 22H9C4 22 2 20 2 15V9C2 4 4 2 9 2H14" stroke="#292D32" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 10H18C15 10 14 9 14 6V2L22 10Z" stroke="#292D32" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
آپلود
</label>
</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>
{/* Email content section */}
<div className='border border-dashed border-gray-300 rounded-2xl p-8 mb-6'>
{emailContent && showContentEditor ? (
<div className='w-full'>
<textarea
className='w-full p-4 border border-gray-200 rounded-lg min-h-[150px] text-right'
value={emailContent}
onChange={(e) => setEmailContent(e.target.value)}
/>
<div className='flex justify-end mt-3'>
<button
className='px-4 py-2 bg-gray-200 rounded-lg text-sm'
onClick={() => setShowContentEditor(false)}
>
تایید
</button>
</div>
</div>
) : emailContent ? (
<div className='w-full'>
<div className='p-4 border border-gray-200 rounded-lg min-h-[150px] text-right'>
{emailContent}
</div>
<div className='flex justify-end mt-3'>
<button
className='p-2 bg-gray-100 rounded-lg'
onClick={() => setShowContentEditor(true)}
>
<Edit size={16} />
</button>
</div>
</div>
) : (
<div className='flex flex-col items-center justify-center py-14'>
<div className='text-gray-500 mb-3'>محتوای ایمیل شما</div>
<button
className='w-10 h-10 flex items-center justify-center rounded-full bg-gray-100'
onClick={handleAddEmailContent}
>
<Add size={24} />
</button>
</div>
)}
</div>
{/* Email footer section */}
<div className='border border-dashed border-gray-300 rounded-2xl p-8 mb-6'>
{emailFooter && showFooterEditor ? (
<div className='w-full'>
<textarea
className='w-full p-4 border border-gray-200 rounded-lg min-h-[100px] text-right'
value={emailFooter}
onChange={(e) => setEmailFooter(e.target.value)}
/>
<div className='flex justify-end mt-3'>
<button
className='px-4 py-2 bg-gray-200 rounded-lg text-sm'
onClick={() => setShowFooterEditor(false)}
>
تایید
</button>
</div>
</div>
) : emailFooter ? (
<div className='w-full'>
<div className='p-4 border border-gray-200 rounded-lg min-h-[100px] text-right'>
{emailFooter}
</div>
<div className='flex justify-end mt-3'>
<button
className='p-2 bg-gray-100 rounded-lg'
onClick={() => setShowFooterEditor(true)}
>
<Edit size={16} />
</button>
</div>
</div>
) : (
<div className='flex flex-col items-center justify-center py-10'>
<div className='text-gray-500 mb-3'>پاورقی ایمیل شما</div>
<button
className='w-10 h-10 flex items-center justify-center rounded-full bg-gray-100'
onClick={handleAddEmailFooter}
>
<Add size={24} />
</button>
</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>
</div>
)
+38
View File
@@ -0,0 +1,38 @@
import Textarea from '@/components/Textarea'
import UploadBox from '@/components/UploadBox'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
const Signture: FC = () => {
const { t } = useTranslation()
return (
<div className='bg-white rounded-4xl p-8 mt-9'>
<div className='mt-9'>
<div className='flex justify-between items-start pb-10'>
<div className='flex-1'>
<div className='text-lg'>
{t('setting.signature')}
</div>
<div className='text-sm text-description mt-1.5'>
{t('setting.signature_description')}
</div>
</div>
<div className='flex-1'>
<UploadBox
label={t('setting.upload_sign')}
/>
<div className='mt-8'>
<Textarea
label={t('setting.description_sign')}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default Signture