From ed8cd1392d983a3a4c249b36122a66c6478025b3 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Tue, 15 Jul 2025 14:57:54 +0330 Subject: [PATCH] template --- src/pages/email-builder/utils/htmlExporter.ts | 7 +- src/pages/setting/address/Address.tsx | 391 ++---------------- .../address/components/AddressDeleteModal.tsx | 57 +++ .../address/components/AddressForm.tsx | 261 ++++++++++++ .../address/components/AddressTable.tsx | 147 +++++++ src/pages/setting/address/types/Types.ts | 2 + 6 files changed, 504 insertions(+), 361 deletions(-) create mode 100644 src/pages/setting/address/components/AddressDeleteModal.tsx create mode 100644 src/pages/setting/address/components/AddressForm.tsx create mode 100644 src/pages/setting/address/components/AddressTable.tsx diff --git a/src/pages/email-builder/utils/htmlExporter.ts b/src/pages/email-builder/utils/htmlExporter.ts index 39250e0..80c56bc 100644 --- a/src/pages/email-builder/utils/htmlExporter.ts +++ b/src/pages/email-builder/utils/htmlExporter.ts @@ -149,10 +149,7 @@ const renderBlockToHTML = (block: Block): string => { }; // Generate CSS for email clients -const generateEmailCSS = ( - template: Template, - options: ExportOptions -): string => { +const generateEmailCSS = (options: ExportOptions): string => { if (!options.includeCSS) return ""; return ` @@ -213,7 +210,7 @@ export const exportToHTML = ( ): string => { const finalOptions = { ...defaultExportOptions, ...options }; - const css = generateEmailCSS(template, finalOptions); + const css = generateEmailCSS(finalOptions); const bodyHTML = template.blocks .map((block) => renderBlockToHTML(block)) .join(""); diff --git a/src/pages/setting/address/Address.tsx b/src/pages/setting/address/Address.tsx index be38c50..f45a49c 100644 --- a/src/pages/setting/address/Address.tsx +++ b/src/pages/setting/address/Address.tsx @@ -1,26 +1,10 @@ -import Input from '@/components/Input' -import Select from '@/components/Select' -import Table from '@/components/Table' -import { ColumnType } from '@/components/types/TableTypes' import { FC, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import Button from '@/components/Button' -import { TickCircle, Edit2, Trash } from 'iconsax-react' -import { useForm } from 'react-hook-form' -import { AddressData, CreateAddressType, UpdateAddressType } from './types/Types' -import { useGetDomains } from '../domain/hooks/useDomainData' -import { useCreateAddress, useGetAddress, useUpdateAddress, useDeleteAddress } from './hooks/useAddressData' -import { useGetSpace } from '../domain/hooks/useDomainData' -import { toast } from '@/components/Toast' -import { ErrorType } from '@/helpers/types' -import ProgressBarEmail from './components/ProgressBarEmail' -import ToggleStatus from './components/ToggleStatus' -import RowActionsDropdown, { RowActionItem } from '@/components/RowActionsDropdown' -import ModalConfrim from '@/components/ModalConfrim' +import { AddressData } from './types/Types' +import AddressTable from './components/AddressTable' +import AddressForm from './components/AddressForm' +import AddressDeleteModal from './components/AddressDeleteModal' const Address: FC = () => { - const { t } = useTranslation() - const [search, setSearch] = useState('') const [status, setStatus] = useState('all') const [currentPage, setCurrentPage] = useState(1) @@ -29,70 +13,9 @@ const Address: FC = () => { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [addressToDelete, setAddressToDelete] = useState(null) - const { data: domain } = useGetDomains() - const { data: spaceData } = useGetSpace() - const { mutate: createAddress, isPending } = useCreateAddress() - const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress() - const { mutate: deleteAddress } = useDeleteAddress() - const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status, currentPage, 10) - - type FormData = CreateAddressType & { password?: string; quotaMB?: number } - - const { register, handleSubmit, formState: { errors }, setValue, reset, setError, clearErrors - } = useForm({ - defaultValues: { - domainId: domain?.data?.domain?.id, - } - }) - - const validateForm = (params: FormData): boolean => { - clearErrors() - let isValid = true - - if (!params.title) { - setError('title', { message: t('errors.required') }) - isValid = false - } - - if (!params.username) { - setError('username', { message: t('errors.required') }) - isValid = false - } - - if (!isEditMode && !params.password) { - setError('password', { message: t('errors.required') }) - isValid = false - } - - if (!params.domainId) { - setError('domainId', { message: t('errors.required') }) - isValid = false - } - - // اعتبارسنجی quota - if (params.quotaMB) { - const quotaInBytes = params.quotaMB * 1024 * 1024 - const remainingSpace = spaceData?.data?.quota?.totalInGB * 1024 * 1024 * 1024 - spaceData?.data?.quota?.usedInGB * 1024 * 1024 * 1024 - - if (quotaInBytes > remainingSpace) { - setError('quotaMB', { message: 'فضای وارد شده بیش از فضای باقی‌مانده است' }) - isValid = false - } - } - - return isValid - } - const handleEdit = (address: AddressData) => { setSelectedAddress(address) setIsEditMode(true) - reset({ - title: address.title, - username: address.displayName, - password: '', - domainId: address.domain.id, - quotaMB: address.emailQuota ? Math.round(address.emailQuota / (1024 * 1024)) : undefined, - }) } const handleDelete = (address: AddressData) => { @@ -100,153 +23,24 @@ const Address: FC = () => { setIsDeleteModalOpen(true) } - const handleConfirmDelete = () => { - if (addressToDelete) { - deleteAddress(addressToDelete.id, { - onSuccess: (data) => { - toast(data?.data?.message || 'آدرس با موفقیت حذف شد', 'success') - setIsDeleteModalOpen(false) - setAddressToDelete(null) - }, - onError: (error: ErrorType) => { - toast(error.response?.data?.error?.message[0] || 'خطا در حذف', 'error') - setIsDeleteModalOpen(false) - setAddressToDelete(null) - } - }) - } - } - - const handleCloseDeleteModal = () => { - setIsDeleteModalOpen(false) - setAddressToDelete(null) - } - const handleCancelEdit = () => { setSelectedAddress(null) setIsEditMode(false) - reset({ - title: '', - username: '', - password: '', - domainId: domain?.data?.domain?.id, - quotaMB: undefined, - }) } - const getRowActions = (address: AddressData): RowActionItem[] => [ - { - label: 'ویرایش', - icon: , - onClick: () => handleEdit(address), - }, - { - label: 'حذف', - icon: , - onClick: () => handleDelete(address), - className: 'text-red-600 hover:bg-red-50' - } - ] - - const columns: ColumnType[] = [ - { - title: t('setting.address_title'), - key: 'title', - width: '200px' - }, - { - title: t('setting.email'), - key: 'email', - width: '300px', - render: (item: AddressData) => ( -
- {item.emailAddress} -
- ) - }, - { - title: t('setting.quota'), - key: 'quota', - width: '300px', - render: (item: AddressData) => { - return ( - - ) - } - }, - { - title: t('setting.status'), - key: 'status', - width: '120px', - align: 'center', - render: (item: AddressData) => ( -
- -
- ) - }, - { - title: 'عملیات', - key: 'actions', - width: '120px', - align: 'center', - render: (item: AddressData) => ( - - ) - } - ] - - const statusOptions = [ - { value: 'all', label: 'همه' }, - { value: 'active', label: t('setting.active') }, - { value: 'inactive', label: t('setting.inactive') } - ] - - const onSubmit = (params: FormData) => { - if (!validateForm(params)) { - return - } - - if (isEditMode && selectedAddress) { - const updateParams: UpdateAddressType = { - title: params.title, - password: params.password || undefined, - quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined, - } - updateAddress({ id: selectedAddress.id, params: updateParams }, { - onSuccess: (data) => { - toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success') - handleCancelEdit() - }, - onError: (error: ErrorType) => { - toast(error.response?.data?.error?.message[0] || 'خطا در آپدیت', 'error') - } - }) - } else { - const createParams: CreateAddressType = { - ...params, - quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined, - } - createAddress(createParams, { - onSuccess: (data) => { - toast(data?.data?.message, 'success') - reset() - }, - onError: (error: ErrorType) => { - toast(error.response?.data?.error?.message[0], 'error') - } - }) - } + const handleFormSuccess = () => { + setSelectedAddress(null) + setIsEditMode(false) } - useEffect(() => { - if (domain?.data?.domain?.id && !isEditMode) { - setValue('domainId', domain?.data?.domain?.id) - } - }, [domain, isEditMode]) + const handleDeleteSuccess = () => { + setAddressToDelete(null) + } + + const handleDeleteModalClose = () => { + setIsDeleteModalOpen(false) + setAddressToDelete(null) + } useEffect(() => { setCurrentPage(1) @@ -254,144 +48,29 @@ const Address: FC = () => { return (
-
-
-
- setSearch(e)} - /> -
-
+ - setCurrentPage(page) - }} - /> - - -
-
-
- {isEditMode ? 'ویرایش آدرس' : t('setting.add_address')} -
- {isEditMode && ( - - )} -
- -
- -
- -
- -
- -
- -
- @ {domain?.data?.domain?.name} -
-
- -
- -
- فضای باقی‌مانده: {spaceData?.data?.quota ? Math.round((spaceData.data.quota.totalInGB - spaceData.data.quota.usedInGB) * 1024) : 0} مگابایت -
-
- -
- -
- - - - {!isEditMode && ( -
-
رمز عبور می‌بایست:
-
    -
  • حداقل ۸ کاراکتر باشد
  • -
  • ترکیبی از حروف کوچک و بزرگ باشد
  • -
  • شامل اعداد باشد
  • -
  • شامل کاراکتر های خاص (نماد ها) باشد
  • -
-
- )} - -
- -
-
- - ) diff --git a/src/pages/setting/address/components/AddressDeleteModal.tsx b/src/pages/setting/address/components/AddressDeleteModal.tsx new file mode 100644 index 0000000..09d3119 --- /dev/null +++ b/src/pages/setting/address/components/AddressDeleteModal.tsx @@ -0,0 +1,57 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import ModalConfrim from '@/components/ModalConfrim' +import { useDeleteAddress } from '../hooks/useAddressData' +import { AddressData } from '../types/Types' +import { toast } from '@/components/Toast' +import { ErrorType } from '@/helpers/types' + +interface AddressDeleteModalProps { + isOpen: boolean + addressToDelete: AddressData | null + onClose: () => void + onSuccess: () => void +} + +const AddressDeleteModal: FC = ({ + isOpen, + addressToDelete, + onClose, + onSuccess +}) => { + const { t } = useTranslation() + const { mutate: deleteAddress, isPending } = useDeleteAddress() + + const handleConfirmDelete = () => { + if (addressToDelete) { + deleteAddress(addressToDelete.id, { + onSuccess: (data) => { + toast(data?.data?.message || 'آدرس با موفقیت حذف شد', 'success') + onSuccess() + onClose() + }, + onError: (error: ErrorType) => { + toast(error.response?.data?.error?.message[0] || 'خطا در حذف', 'error') + onClose() + } + }) + } + } + + return ( + + ) +} + +export default AddressDeleteModal \ No newline at end of file diff --git a/src/pages/setting/address/components/AddressForm.tsx b/src/pages/setting/address/components/AddressForm.tsx new file mode 100644 index 0000000..5be5dc9 --- /dev/null +++ b/src/pages/setting/address/components/AddressForm.tsx @@ -0,0 +1,261 @@ +import Input from '@/components/Input' +import Select from '@/components/Select' +import Button from '@/components/Button' +import { TickCircle } from 'iconsax-react' +import { useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import { FC, useEffect } from 'react' +import { AddressData, CreateAddressType, UpdateAddressType } from '../types/Types' +import { useGetDomains } from '../../domain/hooks/useDomainData' +import { useCreateAddress, useUpdateAddress } from '../hooks/useAddressData' +import { useGetSpace } from '../../domain/hooks/useDomainData' +import { useGetTemplates } from '../../personality/hooks/usePersonalityData' +import { toast } from '@/components/Toast' +import { ErrorType } from '@/helpers/types' + +interface AddressFormProps { + selectedAddress: AddressData | null + isEditMode: boolean + onCancel: () => void + onSuccess: () => void +} + +const AddressForm: FC = ({ selectedAddress, isEditMode, onCancel, onSuccess }) => { + const { t } = useTranslation() + const { data: templates } = useGetTemplates() + const { data: domain } = useGetDomains() + const { data: spaceData } = useGetSpace() + const { mutate: createAddress, isPending } = useCreateAddress() + const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress() + + type FormData = CreateAddressType & { password?: string; quotaMB?: number } + + const { register, handleSubmit, formState: { errors }, setValue, reset, setError, clearErrors, getValues + } = useForm({ + defaultValues: { + domainId: domain?.data?.domain?.id, + } + }) + + const validateForm = (params: FormData): boolean => { + clearErrors() + let isValid = true + + if (!params.title) { + setError('title', { message: t('errors.required') }) + isValid = false + } + + if (!params.username) { + setError('username', { message: t('errors.required') }) + isValid = false + } + + if (!isEditMode && !params.password) { + setError('password', { message: t('errors.required') }) + isValid = false + } + + if (!params.domainId) { + setError('domainId', { message: t('errors.required') }) + isValid = false + } + + // اعتبارسنجی quota + if (params.quotaMB) { + const quotaInBytes = params.quotaMB * 1024 * 1024 + const remainingSpace = spaceData?.data?.quota?.totalInGB * 1024 * 1024 * 1024 - spaceData?.data?.quota?.usedInGB * 1024 * 1024 * 1024 + + if (quotaInBytes > remainingSpace) { + setError('quotaMB', { message: 'فضای وارد شده بیش از فضای باقی‌مانده است' }) + isValid = false + } + } + + return isValid + } + + const onSubmit = (params: FormData) => { + if (!validateForm(params)) { + return + } + + if (isEditMode && selectedAddress) { + const updateParams: UpdateAddressType = { + title: params.title, + password: params.password || undefined, + quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined, + } + updateAddress({ id: selectedAddress.id, params: updateParams }, { + onSuccess: (data) => { + toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success') + onSuccess() + }, + onError: (error: ErrorType) => { + toast(error.response?.data?.error?.message[0] || 'خطا در آپدیت', 'error') + } + }) + } else { + const createParams: CreateAddressType = { + ...params, + quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined, + } + createAddress(createParams, { + onSuccess: (data) => { + toast(data?.data?.message, 'success') + reset() + onSuccess() + }, + onError: (error: ErrorType) => { + toast(error.response?.data?.error?.message[0], 'error') + } + }) + } + } + + useEffect(() => { + if (domain?.data?.domain?.id && !isEditMode) { + setValue('domainId', domain?.data?.domain?.id) + setValue('templateId', domain?.data?.template) + } + }, [domain, isEditMode, setValue]) + + useEffect(() => { + if (selectedAddress && isEditMode) { + reset({ + title: selectedAddress.title, + username: selectedAddress.displayName, + password: '', + domainId: selectedAddress.domain.id, + quotaMB: selectedAddress.emailQuota ? Math.round(selectedAddress.emailQuota / (1024 * 1024)) : undefined, + templateId: selectedAddress.template || undefined, + }) + } else if (!isEditMode) { + reset({ + title: '', + username: '', + password: '', + domainId: domain?.data?.domain?.id, + quotaMB: undefined, + templateId: domain?.data?.template, + }) + } + }, [selectedAddress, isEditMode, reset, domain]) + + return ( +
+
+
+ {isEditMode ? 'ویرایش آدرس' : t('setting.add_address')} +
+ {isEditMode && ( + + )} +
+ +
+ +
+ +
+ +
+ +
+ +
+ @ {domain?.data?.domain?.name} +
+
+ +
+ +
+ فضای باقی‌مانده: {spaceData?.data?.quota ? Math.round((spaceData.data.quota.totalInGB - spaceData.data.quota.usedInGB) * 1024) : 0} مگابایت +
+
+ +
+ +
+ + {!isEditMode && ( +
+
رمز عبور می‌بایست:
+
    +
  • حداقل ۸ کاراکتر باشد
  • +
  • ترکیبی از حروف کوچک و بزرگ باشد
  • +
  • شامل اعداد باشد
  • +
  • شامل کاراکتر های خاص (نماد ها) باشد
  • +
+
+ )} + +
+ +
+
+ ) +} + +export default AddressForm \ No newline at end of file diff --git a/src/pages/setting/address/components/AddressTable.tsx b/src/pages/setting/address/components/AddressTable.tsx new file mode 100644 index 0000000..a6c5f13 --- /dev/null +++ b/src/pages/setting/address/components/AddressTable.tsx @@ -0,0 +1,147 @@ +import Input from '@/components/Input' +import Select from '@/components/Select' +import Table from '@/components/Table' +import { ColumnType } from '@/components/types/TableTypes' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Edit2, Trash } from 'iconsax-react' +import { AddressData } from '../types/Types' +import { useGetAddress } from '../hooks/useAddressData' +import ProgressBarEmail from './ProgressBarEmail' +import ToggleStatus from './ToggleStatus' +import RowActionsDropdown, { RowActionItem } from '@/components/RowActionsDropdown' + +interface AddressTableProps { + search: string + status: string + currentPage: number + onSearchChange: (search: string) => void + onStatusChange: (status: string) => void + onPageChange: (page: number) => void + onEdit: (address: AddressData) => void + onDelete: (address: AddressData) => void +} + +const AddressTable: FC = ({ + search, + status, + currentPage, + onSearchChange, + onStatusChange, + onPageChange, + onEdit, + onDelete +}) => { + const { t } = useTranslation() + const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status, currentPage, 10) + + const getRowActions = (address: AddressData): RowActionItem[] => [ + { + label: 'ویرایش', + icon: , + onClick: () => onEdit(address), + }, + { + label: 'حذف', + icon: , + onClick: () => onDelete(address), + className: 'text-red-600 hover:bg-red-50' + } + ] + + const columns: ColumnType[] = [ + { + title: t('setting.address_title'), + key: 'title', + width: '200px' + }, + { + title: t('setting.email'), + key: 'email', + width: '300px', + render: (item: AddressData) => ( +
+ {item.emailAddress} +
+ ) + }, + { + title: t('setting.quota'), + key: 'quota', + width: '300px', + render: (item: AddressData) => { + return ( + + ) + } + }, + { + title: t('setting.status'), + key: 'status', + width: '120px', + align: 'center', + render: (item: AddressData) => ( +
+ +
+ ) + }, + { + title: 'عملیات', + key: 'actions', + width: '120px', + align: 'center', + render: (item: AddressData) => ( + + ) + } + ] + + const statusOptions = [ + { value: 'all', label: 'همه' }, + { value: 'active', label: t('setting.active') }, + { value: 'inactive', label: t('setting.inactive') } + ] + + return ( +
+
+
+ +
+
+ +
+ + ) +} + +export default AddressTable \ No newline at end of file diff --git a/src/pages/setting/address/types/Types.ts b/src/pages/setting/address/types/Types.ts index cd0c1b9..28294cb 100644 --- a/src/pages/setting/address/types/Types.ts +++ b/src/pages/setting/address/types/Types.ts @@ -4,6 +4,7 @@ export type CreateAddressType = { domainId: string; title: string; quota?: number; + templateId?: string; }; export type UpdateAddressType = { @@ -64,6 +65,7 @@ export interface AddressData extends Record { dmarcPolicy: string | null; business: string; }; + template: string | null; } export interface AddressResponse {