diff --git a/package.json b/package.json index 55dd0e9..bfa23a1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-switch": "^1.2.5", "@react-three/drei": "^10.1.2", "@react-three/fiber": "^9.1.2", "@tailwindcss/vite": "^4.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74c6b06..da06e17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.3.2 version: 1.3.2(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-switch': + specifier: ^1.2.5 + version: 1.2.5(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-three/drei': specifier: ^10.1.2 version: 10.1.2(@react-three/fiber@9.1.2(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.177.0))(@types/react@19.1.6)(@types/three@0.177.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.177.0) @@ -544,6 +547,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.2.5': + resolution: {integrity: sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-controllable-state@1.2.2': resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} peerDependencies: @@ -2463,6 +2479,21 @@ snapshots: optionalDependencies: '@types/react': 19.1.6 + '@radix-ui/react-switch@1.2.5(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.6)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.6)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.6)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.6)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.6)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.6 + '@types/react-dom': 19.1.5(@types/react@19.1.6) + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.6)(react@19.1.0)': dependencies: '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.6)(react@19.1.0) diff --git a/src/components/Textarea.tsx b/src/components/Textarea.tsx new file mode 100644 index 0000000..18831eb --- /dev/null +++ b/src/components/Textarea.tsx @@ -0,0 +1,36 @@ +import { FC, InputHTMLAttributes } from 'react' +import Error from './Error' +import { clx } from '../helpers/utils' + +type Props = { + label: string, + error_text?: string +} & InputHTMLAttributes + +const Textarea: FC = (props: Props) => { + return ( +
+ + + + { + props.error_text && + + } + +
+ ) +} + +export default Textarea \ No newline at end of file diff --git a/src/components/UploadBox.tsx b/src/components/UploadBox.tsx new file mode 100644 index 0000000..96dad28 --- /dev/null +++ b/src/components/UploadBox.tsx @@ -0,0 +1,87 @@ +import { FC, useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDropzone } from 'react-dropzone' +import { CloseCircle } from 'iconsax-react' + + +type Props = { + label: string, + isMultiple?: boolean, + onChange?: (file: File[]) => void; + isReset?: boolean; +} + +const UploadBox: FC = (props: Props) => { + + const { t } = useTranslation('global') + const [files, setFiles] = useState([]) + + const onDrop = useCallback((acceptedFiles: File[]) => { + if (props.isMultiple) { + const array = [...files] + array.push(acceptedFiles[0]) + setFiles(array) + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + props.onChange && props.onChange(array) + } else { + setFiles([acceptedFiles[0]]) + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + props.onChange && props.onChange([acceptedFiles[0]]) + } + }, [files]) + const { getRootProps, getInputProps } = useDropzone({ onDrop }) + + const handleRemove = (index: number) => { + const array = [...files] + array.splice(index, 1) + setFiles(array) + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + props.onChange && props.onChange(array) + } + + useEffect(() => { + + if (props.isReset) { + setFiles([]) + } + + }, [props.isReset]) + + + + return ( +
+
{props.label}
+
+ + +
+ { + files?.map((item, index) => ( +
+ handleRemove(index)} size={16} color='red' /> +
{item.name}
+
+ )) + } +
+
+
+ { + files?.map((item, index) => ( +
+ handleRemove(index)} size={16} color='red' /> +
{item.name}
+
+ )) + } +
+
+ ) +} + +export default UploadBox \ No newline at end of file diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..b0363e3 --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as SwitchPrimitive from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +function Switch({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Switch } diff --git a/src/langs/fa.json b/src/langs/fa.json index b58a2db..b8766de 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -96,8 +96,27 @@ "record_dns": "رکوردهای DNS", "record_dns_description": "در این قسمت میتوانید وضعیت رکوردهای DNS سرویس ایمیل خود را بررسی کنید.", "your_domain": "دامنه شما", - "submit": "ثبت" + "submit": "ثبت", + "status": "وضعیت", + "search": "جستجو", + "address_title": "عنوان نشانی", + "email": "ایمیل", + "marketing": "بازاریابی", + "actions": "عملیات", + "delete": "حذف کردن ایمیل", + "active": "فعال", + "inactive": "غیرفعال", + "add_address": "اضافه کردن نشانی", + "domain1": "دامنه", + "create": "ساخت", + "content_email": "محتوای ایمیل شما", + "footer_email": "پاورقی ایمیل شما", + "signature": "امضا", + "signature_description": "تصویر و توضیحات امضای خود را با دقت وارد کنید", + "upload_sign": "آپلود امضا", + "description_sign": "توضیحات امضا" }, + "select_file": "انتخاب فایل", "upload": "آپلود", "new_message": { "title": "ارسال پیام جدید", diff --git a/src/pages/setting/Setting.tsx b/src/pages/setting/Setting.tsx index c9f4036..e89145d 100644 --- a/src/pages/setting/Setting.tsx +++ b/src/pages/setting/Setting.tsx @@ -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 ; case SettingTabEnum.SETTING_ADDRESS: return
; + case SettingTabEnum.SETTING_SIGNATURE: + return ; case SettingTabEnum.SETTING_PERSONALITY: return (
@@ -63,6 +66,11 @@ const Setting: FC = () => { icon: , label: t('setting.personality'), value: SettingTabEnum.SETTING_PERSONALITY + }, + { + icon: , + label: t('setting.signature'), + value: SettingTabEnum.SETTING_SIGNATURE } ]} active={activeTab} diff --git a/src/pages/setting/address/Address.tsx b/src/pages/setting/address/Address.tsx index 85b82cd..b8504d5 100644 --- a/src/pages/setting/address/Address.tsx +++ b/src/pages/setting/address/Address.tsx @@ -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 { id: number + title: string email: string - name: string - isPrimary: boolean + status: boolean } const Address: FC = () => { const { t } = useTranslation() - const [addresses, setAddresses] = useState([ - { id: 1, email: 'user@example.com', name: 'Main Account', isPrimary: true } + const [addresses] = useState([ + { + 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(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[] = [ + { + 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) => ( +
+ +
+ ) } - } + ] - 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 ( -
-
-

{t('setting.address')}

- -
- -
-
-

{t('setting.email_addresses')}

- {!showAddForm && ( - - )} -
- - {showAddForm && ( -
-

{t('setting.new_address')}

-
-
- - 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' - /> -
-
- - 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')} - /> -
-
-
- - -
-
- )} - -
- {addresses.map((address) => ( -
- {editingId === address.id ? ( -
-
- - 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' - /> -
-
- - 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' - /> -
-
- - -
-
- ) : ( -
-
-
-

{address.name}

- {address.isPrimary && ( - - {t('setting.primary')} - - )} -
-

{address.email}

-
-
- {!address.isPrimary && ( - <> - - - - - )} -
-
- )} -
- ))} -
-
- -
-
- - - - - +
+
+
-

{t('setting.verification_required')}

-

{t('setting.verification_note')}

+ +
+
+ + console.log('Page:', page) + }} + /> + + +
+
+ {t('setting.add_address')} +
+ +
+
+ {t('setting.status')} +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ @ example.com +
+
+ +
+ +
+ +
+
رمز عبور می‌بایست:
+
    +
  • حداقل ۸ کاراکتر باشد
  • +
  • ترکیبی از حروف کوچک و بزرگ باشد
  • +
  • شامل اعداد باشد
  • +
  • شامل کاراکتر های خاص (نماد ها) باشد
  • +
+
+ +
+
) } -export default Address \ No newline at end of file +export default Address \ No newline at end of file diff --git a/src/pages/setting/domain/Domain.tsx b/src/pages/setting/domain/Domain.tsx index 8bc1804..7e475de 100644 --- a/src/pages/setting/domain/Domain.tsx +++ b/src/pages/setting/domain/Domain.tsx @@ -106,7 +106,7 @@ const Domain: FC = () => { return (
-
+
{t('setting.record_dns')} diff --git a/src/pages/setting/enum/SettingEnum.ts b/src/pages/setting/enum/SettingEnum.ts index f2eadef..2c4f334 100644 --- a/src/pages/setting/enum/SettingEnum.ts +++ b/src/pages/setting/enum/SettingEnum.ts @@ -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 { diff --git a/src/pages/setting/personality/Personality.tsx b/src/pages/setting/personality/Personality.tsx index 2afa51c..064f266 100644 --- a/src/pages/setting/personality/Personality.tsx +++ b/src/pages/setting/personality/Personality.tsx @@ -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(null) - const [emailContent, setEmailContent] = useState('') - const [emailFooter, setEmailFooter] = useState('') - const [showContentEditor, setShowContentEditor] = useState(false) - const [showFooterEditor, setShowFooterEditor] = useState(false) - const handleImageUpload = (event: React.ChangeEvent) => { - 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 ( -
- {/* Header buttons */} -
- - {/*
- - - - -
*/} +
+
+
+
- {/* Background image section */} -
- {backgroundImage ? ( -
- Background - -
- ) : ( -
-
تصویر پس زمینه مورد نظر را آپلود کنید
- -
- )} +
+
{t('setting.content_email')}
+
+ +
- - {/* Email content section */} -
- {emailContent && showContentEditor ? ( -
-