This commit is contained in:
hamid zarghami
2025-06-22 22:48:15 +03:30
parent 037113ee09
commit 0aab282d6f
12 changed files with 344 additions and 113 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.7634 4.92222C14.6506 4.95068 14.5325 4.95068 14.4198 4.92222H8.04008C6.41809 4.9032 4.83282 5.40536 3.5176 6.3548C2.20238 7.30423 1.22667 8.65079 0.734074 10.1963C0.503887 10.9056 0.387896 11.6469 0.390438 12.3926C0.390438 13.1695 0.390438 13.9464 0.390438 14.7233C0.390438 14.8332 0.412079 14.942 0.454126 15.0435C0.496174 15.145 0.557802 15.2373 0.635496 15.3149C0.713188 15.3926 0.805424 15.4543 0.906935 15.4963C1.00845 15.5384 1.11724 15.56 1.22712 15.56C1.33699 15.56 1.44579 15.5384 1.5473 15.4963C1.64881 15.4543 1.74104 15.3926 1.81874 15.3149C1.89643 15.2373 1.95806 15.145 2.00011 15.0435C2.04216 14.942 2.0638 14.8332 2.0638 14.7233V12.5121C2.05855 11.0817 2.57429 9.69836 3.51465 8.62052C4.45501 7.54269 5.75564 6.84411 7.17352 6.65534C7.44199 6.6331 7.71184 6.6331 7.98032 6.65534H14.5991C14.6671 6.6336 14.7402 6.6336 14.8082 6.65534C14.8082 6.75992 14.7036 6.80475 14.6439 6.87945L11.8649 9.6136C11.7407 9.7257 11.653 9.87245 11.613 10.0349C11.5731 10.1974 11.5827 10.3681 11.6408 10.525C11.6886 10.6771 11.7812 10.8113 11.9065 10.9099C12.0318 11.0086 12.1839 11.0671 12.343 11.0778C12.4613 11.1 12.5832 11.0954 12.6995 11.0644C12.8158 11.0333 12.9237 10.9767 13.0153 10.8985L17.4975 6.41629C17.5841 6.33785 17.6533 6.24216 17.7007 6.13537C17.748 6.02858 17.7725 5.91306 17.7725 5.79625C17.7725 5.67943 17.748 5.56391 17.7007 5.45712C17.6533 5.35033 17.5841 5.25464 17.4975 5.17621L14.3002 1.93407L13.0751 0.738816C12.9996 0.640595 12.9036 0.560032 12.7937 0.502748C12.6838 0.445464 12.5628 0.412839 12.4391 0.407149C12.3153 0.401459 12.1918 0.42284 12.0771 0.469802C11.9625 0.516764 11.8595 0.588175 11.7753 0.679054C11.6883 0.75883 11.6189 0.855804 11.5714 0.963827C11.5239 1.07185 11.4994 1.18856 11.4994 1.30656C11.4994 1.42456 11.5239 1.54128 11.5714 1.6493C11.6189 1.75732 11.6883 1.8543 11.7753 1.93407L14.7634 4.92222Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 577 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7209_44189)">
<path d="M2.98855 4.92222C3.10133 4.95068 3.21941 4.95068 3.33219 4.92222H9.71187C11.3339 4.9032 12.9191 5.40536 14.2344 6.3548C15.5496 7.30423 16.5253 8.65079 17.0179 10.1963C17.2481 10.9056 17.3641 11.6469 17.3615 12.3926C17.3615 13.1695 17.3615 13.9464 17.3615 14.7233C17.3615 14.8332 17.3399 14.942 17.2978 15.0435C17.2558 15.145 17.1942 15.2373 17.1165 15.3149C17.0388 15.3926 16.9465 15.4543 16.845 15.4963C16.7435 15.5384 16.6347 15.56 16.5248 15.56C16.415 15.56 16.3062 15.5384 16.2047 15.4963C16.1031 15.4543 16.0109 15.3926 15.9332 15.3149C15.8555 15.2373 15.7939 15.145 15.7518 15.0435C15.7098 14.942 15.6882 14.8332 15.6882 14.7233V12.5121C15.6934 11.0817 15.1777 9.69836 14.2373 8.62052C13.2969 7.54269 11.9963 6.84411 10.5784 6.65534C10.31 6.6331 10.0401 6.6331 9.77164 6.65534H3.1529C3.08487 6.6336 3.01176 6.6336 2.94373 6.65534C2.94373 6.75992 3.04832 6.80475 3.10808 6.87945L5.88705 9.6136C6.01124 9.7257 6.09898 9.87245 6.13893 10.0349C6.17888 10.1974 6.16921 10.3681 6.11116 10.525C6.06338 10.6771 5.97079 10.8113 5.84549 10.9099C5.7202 11.0086 5.56805 11.0671 5.40895 11.0778C5.2906 11.1 5.1688 11.0954 5.05246 11.0644C4.93612 11.0333 4.8282 10.9767 4.73662 10.8985L0.254403 6.41629C0.167835 6.33785 0.0986495 6.24216 0.0513015 6.13537C0.00395355 6.02858 -0.0205078 5.91306 -0.0205078 5.79625C-0.0205078 5.67943 0.00395355 5.56391 0.0513015 5.45712C0.0986495 5.35033 0.167835 5.25464 0.254403 5.17621L3.45172 1.93407L4.67685 0.738816C4.75237 0.640595 4.8484 0.560032 4.95825 0.502748C5.06811 0.445464 5.18914 0.412839 5.3129 0.407149C5.43666 0.401459 5.56017 0.42284 5.67482 0.469802C5.78947 0.516764 5.89249 0.588175 5.9767 0.679054C6.06364 0.75883 6.13306 0.855804 6.18055 0.963827C6.22804 1.07185 6.25256 1.18856 6.25256 1.30656C6.25256 1.42456 6.22804 1.54128 6.18055 1.6493C6.13306 1.75732 6.06364 1.8543 5.9767 1.93407L2.98855 4.92222Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_7209_44189">
<rect width="17.376" height="15.12" fill="white" transform="translate(0 0.44043)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+2
View File
@@ -9,12 +9,14 @@ interface ButtonProps {
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
variant?: 'primary' | 'secondary'
}
const Button: FC<ButtonProps> = (props) => {
const buttonClass = clx(
'w-full bg-primary text-white rounded-xl font-normal text-xs md:text-sm h-10 px-3 md:px-4 transition-all duration-200 hover:bg-opacity-90 disabled:opacity-50 disabled:cursor-not-allowed',
props.variant === 'secondary' && 'bg-[#ECEEF5] text-black',
props.className
)
+20
View File
@@ -0,0 +1,20 @@
import { FC } from 'react'
type Props = {
isActive: boolean,
value: string,
onChange: (value: string) => void
}
const Radio: FC<Props> = (props: Props) => {
return (
<div onClick={() => props.onChange(props.value)} className='size-4 cursor-pointer rounded-full bg-[#EAEDF5] flex justify-center items-center'>
{
props.isActive &&
<div className='size-2 bg-black rounded-full'></div>
}
</div>
)
}
export default Radio
+31
View File
@@ -0,0 +1,31 @@
import { FC } from 'react'
import Radio from './Radio'
type Props = {
items: {
label: string
value: string
}[]
selected: string
onChange: (value: string) => void
}
const RadioGroup: FC<Props> = (props: Props) => {
return (
<div className='flex gap-5 items-center text-xs'>
{
props.items.map((item, index) => (
<div key={index} className='flex gap-2 items-center'>
<Radio value={item.value} onChange={props.onChange} isActive={item.value === props.selected} />
<div className='mt-0.5'>
{item.label}
</div>
</div>
))
}
</div>
)
}
export default RadioGroup
+18 -1
View File
@@ -84,7 +84,15 @@
"add_button": "اضافه کردن دکمه",
"image": "تصویر",
"upload_image": "تصویر مورد نظر را آپلود کنید",
"add_image": "اضافه کردن تصویر"
"add_image": "اضافه کردن تصویر",
"setting_fetch": "تنظیمات فچ",
"setting_fetch_description": "تنظیمات فچ خود را با دقت انتخاب کنید.",
"setting_smtp": "تنظیمات SMTP",
"setting_smtp_description": "تنظیمات SMTP خود را با دقت وارد کنید.",
"smtp_server": "SMTP سرور",
"smtp_port": "SMTP پورت",
"username": "نام کاربری",
"password": "پسورد"
},
"upload": "آپلود",
"new_message": {
@@ -98,5 +106,14 @@
"subject_placeholder": "موضوع پیام",
"message_placeholder": "متن پیام",
"select_priority": "انتخاب اولویت"
},
"mail": {
"download": "دانلود",
"print": "چاپ",
"subject": "موضوع : ",
"attachments": "فایل های ضمیمه",
"answer": "پاسخ",
"forward": "ارسال به دیگران",
"message": "متن پبام"
}
}
+45
View File
@@ -0,0 +1,45 @@
import Button from '@/components/Button'
import { ArrowRight, InfoCircle, SmsNotification, Star, VolumeMute, More, DocumentDownload, ArchiveTick, Printer, Trash } from 'iconsax-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
const Header: FC = () => {
const { t } = useTranslation()
return (
<div className='flex justify-between items-center border-b border-border pb-6'>
<div className='flex gap-6'>
<ArrowRight size={24} color='#8C90A3' />
<SmsNotification size={24} color='black' />
<ArchiveTick size={24} color='black' />
<InfoCircle size={24} color='black' />
<Star size={24} color='black' />
<VolumeMute size={24} color='black' />
<Trash size={24} color='#CD0000' />
<More size={24} color='black' className='rotate-90' />
</div>
<div className='flex gap-2'>
<Button
variant='secondary'
>
<div className='flex gap-2 items-center px-5'>
<DocumentDownload size={20} color='black' />
<span className='pt-0.5'>{t('mail.download')}</span>
</div>
</Button>
<Button
variant='secondary'
>
<div className='flex gap-2 items-center px-5'>
<Printer size={20} color='black' />
<span className='pt-0.5'>{t('mail.print')}</span>
</div>
</Button>
</div>
</div>
)
}
export default Header
+135
View File
@@ -0,0 +1,135 @@
import Button from '@/components/Button'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AvatarImage from '@/assets/images/avatar.svg'
import { DocumentDownload, EmojiHappy } from 'iconsax-react'
import ForwardIcon from '@/assets/images/forward.svg'
import AnswerIcon from '@/assets/images/answer.svg'
import Header from './Components/Header'
import ReactQuill from 'react-quill-new';
import Select from '@/components/Select'
const DetailEmail: FC = () => {
const { t } = useTranslation()
const [showAnswer, setShowAnswer] = useState<boolean>(false)
const [value, setValue] = useState<string>('')
return (
<div className='bg-white rounded-4xl p-8'>
<Header />
<div className='mt-6 flex justify-between items-center'>
<div className='flex gap-4 items-center'>
<div className='flex gap-1 items-center'>
<div className='font-extralight '>{t('mail.subject')}</div>
<div>لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ</div>
</div>
<Button
label='دریافتی ها'
className='bg-[#ECEFF6] text-description w-fit px-6 rounded-full'
/>
<Button
label='الویت زیاد'
className='bg-[#FFE9E9] text-[#CD0000] w-fit px-6 rounded-full'
/>
</div>
<div className='flex text-description text-sm'>
۵ اسفندماه ۱۴۰۳، ساعت ۱۲:۰۰
</div>
</div>
<div className='mt-6 flex gap-4 items-center'>
<img src={AvatarImage} className='size-10' />
<div>مهرداد مظفری</div>
</div>
<p className='mt-8 text-sm leading-7 font-light'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد کتابهای زیادی در شصت و سه درصد گذشته حال و آیندهلورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد کتابهای زیادی در شصت و سه درصد گذشته حال و آینده
</p>
<div className='mt-10 text-sm font-bold'>
{t('mail.attachments')}
</div>
<div className='flex border-b border-border pb-10 mt-4 flex-wrap gap-2.5 items-center'>
<div className='bg-[#EBEDF5] text-sm flex gap-2 h-10 rounded-full items-center px-4'>
<span>Lorem Ipsum.pdf</span>
<DocumentDownload size={20} color='#0038FF' />
</div>
</div>
{
!showAnswer ?
<div className='flex mt-9 justify-end'>
<div className='flex gap-5 text-sm'>
<EmojiHappy size={24} color='black' />
<div onClick={() => setShowAnswer(true)} className='flex gap-2 cursor-pointer items-center border-r border-border pr-5'>
<img src={AnswerIcon} className='w-[17px]' />
<div>{t('mail.answer')}</div>
</div>
<div className='flex gap-2 cursor-pointer items-center border-r border-border pr-5'>
<img src={ForwardIcon} className='w-[17px]' />
<div>{t('mail.forward')}</div>
</div>
</div>
</div>
:
<div className='mt-9 flex gap-4'>
<img src={AvatarImage} className='size-10' />
<div className='w-full'>
<div className='mb-2 text-sm'>
{t('mail.message')}
</div>
<ReactQuill
modules={{
toolbar: [
[{ header: '1' }, { header: '2' }, { font: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
['bold', 'italic', 'underline'],
['link', 'image'],
[{ align: [] }],
['clean']
]
}}
theme="snow"
value={value}
onChange={setValue}
style={{ minHeight: '120px', width: '100%' }}
className='text-sm'
/>
<div>
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
<div>
<Select
items={[]}
placeholder={t('new_message.select_priority')}
className='xl:w-[190px]'
/>
</div>
<div className='flex-1 justify-end flex gap-3'>
<Button
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
label={t('new_message.draft')}
/>
<Button
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
label={t('new_message.send')}
/>
</div>
</div>
</div>
</div>
</div>
}
</div>
)
}
export default DetailEmail
+63 -112
View File
@@ -1,123 +1,74 @@
import { FC, useState } from 'react'
import Input from '@/components/Input'
import RadioGroup from '@/components/RadioGroup'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Add, CloseCircle } from 'iconsax-react'
const Domain: FC = () => {
const { t } = useTranslation()
const [domains, setDomains] = useState<string[]>(['example.com'])
const [newDomain, setNewDomain] = useState('')
const [error, setError] = useState('')
const handleAddDomain = () => {
if (!newDomain) {
setError(t('setting.domain_required'))
return
}
// Simple domain validation
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/
if (!domainRegex.test(newDomain)) {
setError(t('setting.invalid_domain'))
return
}
if (domains.includes(newDomain)) {
setError(t('setting.domain_exists'))
return
}
setDomains([...domains, newDomain])
setNewDomain('')
setError('')
}
const handleRemoveDomain = (domain: string) => {
setDomains(domains.filter(d => d !== domain))
}
return (
<div className='bg-white rounded-4xl p-8'>
<div className='flex justify-between mb-8'>
<h2 className='text-xl font-semibold'>{t('setting.domain')}</h2>
<button className='bg-primary text-white px-6 py-2 rounded-lg'>{t('common.save')}</button>
</div>
<div className='mb-6'>
<label className='block text-sm font-medium text-gray-700 mb-1'>{t('setting.add_domain')}</label>
<div className='flex gap-2'>
<input
type="text"
value={newDomain}
onChange={(e) => {
setNewDomain(e.target.value)
setError('')
}}
className={`flex-1 px-4 py-2 border ${error ? 'border-red-500' : 'border-gray-300'} rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent`}
placeholder='domain.com'
/>
<button
onClick={handleAddDomain}
className='bg-primary text-white px-4 py-2 rounded-lg'
>
<Add size={20} />
</button>
</div>
{error && <p className='text-red-500 text-sm mt-1'>{error}</p>}
</div>
<div>
<h3 className='text-md font-medium mb-4'>{t('setting.your_domains')}</h3>
<div className='space-y-3'>
{domains.map((domain, index) => (
<div key={index} className='flex items-center justify-between p-4 bg-gray-50 rounded-lg'>
<div>
<p className='font-medium'>{domain}</p>
<p className='text-sm text-gray-500'>
{index === 0 ? t('setting.primary_domain') : t('setting.additional_domain')}
</p>
</div>
<div className='flex items-center gap-3'>
<div className={`px-2 py-1 rounded-full text-xs ${index === 0 ? 'bg-green-100 text-green-600' : 'bg-blue-100 text-blue-600'}`}>
{index === 0 ? t('setting.verified') : t('setting.pending')}
</div>
{index !== 0 && (
<button
onClick={() => handleRemoveDomain(domain)}
className='text-gray-400 hover:text-red-500'
>
<CloseCircle size={20} />
</button>
)}
</div>
<div className='bg-white rounded-4xl p-8 mt-9'>
<div className=''>
<div className='flex justify-between items-end border-b pb-10 broder-border'>
<div className='flex-1'>
<div className='text-lg'>
{t('setting.setting_fetch')}
</div>
))}
<div className='text-sm text-description mt-1.5'>
{t('setting.setting_fetch_description')}
</div>
</div>
<div className='flex-1'>
<RadioGroup
items={[
{
label: '۱ دقیقه',
value: '1'
},
{
label: '۲ دقیقه',
value: '2'
},
{
label: '۳ دقیقه',
value: '3'
}
]}
onChange={() => null}
selected='1'
/>
</div>
</div>
</div>
<div className='mt-9'>
<div className='flex justify-between items-start pb-10'>
<div className='flex-1'>
<div className='text-lg'>
{t('setting.setting_smtp')}
</div>
<div className='text-sm text-description mt-1.5'>
{t('setting.setting_smtp_description')}
</div>
</div>
<div className='mt-8 p-4 bg-blue-50 rounded-lg'>
<h3 className='text-md font-medium text-blue-700 mb-2'>{t('setting.dns_records')}</h3>
<p className='text-sm text-blue-600 mb-4'>{t('setting.dns_instructions')}</p>
<div className='bg-white p-4 rounded-lg border border-blue-100'>
<div className='grid grid-cols-3 gap-4 text-sm font-medium text-gray-500 pb-2 border-b'>
<div>{t('setting.type')}</div>
<div>{t('setting.hostname')}</div>
<div>{t('setting.value')}</div>
</div>
<div className='grid grid-cols-3 gap-4 text-sm py-4 border-b'>
<div>MX</div>
<div>@</div>
<div>mail.example.com</div>
</div>
<div className='grid grid-cols-3 gap-4 text-sm py-4 border-b'>
<div>TXT</div>
<div>@</div>
<div className='break-all'>v=spf1 include:spf.example.com ~all</div>
</div>
<div className='grid grid-cols-3 gap-4 text-sm py-4'>
<div>CNAME</div>
<div>mail</div>
<div>mailserver.example.com</div>
<div className='flex-1'>
<Input
label={t('setting.smtp_server')}
/>
<div className='mt-8'>
<Input
label={t('setting.smtp_port')}
/>
</div>
<div className='mt-8'>
<Input
label={t('setting.username')}
/>
</div>
<div className='mt-8'>
<Input
label={t('setting.password')}
/>
</div>
</div>
</div>
</div>
@@ -125,4 +76,4 @@ const Domain: FC = () => {
)
}
export default Domain
export default Domain
+2
View File
@@ -8,6 +8,7 @@ import SentList from '@/pages/sent/List'
import DraftList from '@/pages/draft/List'
import ArchiveList from '@/pages/archive/List'
import TrashList from '@/pages/Trash/List'
import DetailEmail from '@/pages/received/Detail'
const AppRouter: FC = () => {
return (
<Routes>
@@ -18,6 +19,7 @@ const AppRouter: FC = () => {
<Route path={Paths.draft} element={<DraftList />} />
<Route path={Paths.archive} element={<ArchiveList />} />
<Route path={Paths.trash} element={<TrashList />} />
<Route path={Paths.detailEmail + ':id'} element={<DetailEmail />} />
</Routes>
)
}
+1
View File
@@ -8,4 +8,5 @@ export const Paths = {
favorite: "/favorite",
spam: "/spam",
setting: "/setting",
detailEmail: "/mail/",
};