login page
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"formik": "^2.4.6",
|
||||||
"i18next": "^25.0.1",
|
"i18next": "^25.0.1",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
"react-loading-skeleton": "^3.5.0",
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-multi-date-picker": "^4.5.2",
|
"react-multi-date-picker": "^4.5.2",
|
||||||
|
"react-otp-input": "^3.1.1",
|
||||||
"react-quill": "^2.0.0",
|
"react-quill": "^2.0.0",
|
||||||
"react-quill-new": "^3.4.6",
|
"react-quill-new": "^3.4.6",
|
||||||
"react-router-dom": "^7.5.2",
|
"react-router-dom": "^7.5.2",
|
||||||
|
|||||||
Generated
+65
@@ -38,6 +38,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
formik:
|
||||||
|
specifier: ^2.4.6
|
||||||
|
version: 2.4.6(react@19.1.0)
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^25.0.1
|
specifier: ^25.0.1
|
||||||
version: 25.2.1(typescript@5.7.3)
|
version: 25.2.1(typescript@5.7.3)
|
||||||
@@ -83,6 +86,9 @@ importers:
|
|||||||
react-multi-date-picker:
|
react-multi-date-picker:
|
||||||
specifier: ^4.5.2
|
specifier: ^4.5.2
|
||||||
version: 4.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 4.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react-otp-input:
|
||||||
|
specifier: ^3.1.1
|
||||||
|
version: 3.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
react-quill:
|
react-quill:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -1082,6 +1088,9 @@ packages:
|
|||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||||
|
|
||||||
|
'@types/hoist-non-react-statics@3.3.6':
|
||||||
|
resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==}
|
||||||
|
|
||||||
'@types/js-cookie@3.0.6':
|
'@types/js-cookie@3.0.6':
|
||||||
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||||
|
|
||||||
@@ -1431,6 +1440,10 @@ packages:
|
|||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
|
deepmerge@2.2.1:
|
||||||
|
resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
define-data-property@1.1.4:
|
define-data-property@1.1.4:
|
||||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1682,6 +1695,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
|
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
formik@2.4.6:
|
||||||
|
resolution: {integrity: sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
fs-extra@11.3.0:
|
fs-extra@11.3.0:
|
||||||
resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
|
resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
|
||||||
engines: {node: '>=14.14'}
|
engines: {node: '>=14.14'}
|
||||||
@@ -1799,6 +1817,9 @@ packages:
|
|||||||
hls.js@1.6.5:
|
hls.js@1.6.5:
|
||||||
resolution: {integrity: sha512-KMn5n7JBK+olC342740hDPHnGWfE8FiHtGMOdJPfUjRdARTWj9OB+8c13fnsf9sk1VtpuU2fKSgUjHvg4rNbzQ==}
|
resolution: {integrity: sha512-KMn5n7JBK+olC342740hDPHnGWfE8FiHtGMOdJPfUjRdARTWj9OB+8c13fnsf9sk1VtpuU2fKSgUjHvg4rNbzQ==}
|
||||||
|
|
||||||
|
hoist-non-react-statics@3.3.2:
|
||||||
|
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||||
|
|
||||||
html-parse-stringify@3.0.1:
|
html-parse-stringify@3.0.1:
|
||||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||||
|
|
||||||
@@ -2483,6 +2504,9 @@ packages:
|
|||||||
react: '>=16.8.0'
|
react: '>=16.8.0'
|
||||||
react-dom: '>=16.8.0'
|
react-dom: '>=16.8.0'
|
||||||
|
|
||||||
|
react-fast-compare@2.0.4:
|
||||||
|
resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==}
|
||||||
|
|
||||||
react-hook-form@7.60.0:
|
react-hook-form@7.60.0:
|
||||||
resolution: {integrity: sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==}
|
resolution: {integrity: sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -2519,6 +2543,12 @@ packages:
|
|||||||
react: '>=16.8.0'
|
react: '>=16.8.0'
|
||||||
react-dom: '>=16.8.0'
|
react-dom: '>=16.8.0'
|
||||||
|
|
||||||
|
react-otp-input@3.1.1:
|
||||||
|
resolution: {integrity: sha512-bjPavgJ0/Zmf/AYi4onj8FbH93IjeD+e8pWwxIJreDEWsU1ILR5fs8jEJmMGWSBe/yyvPP6X/W6Mk9UkOCkTPw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.6 || ^17.0.0 || ^18.0.0'
|
||||||
|
react-dom: '>=16.8.6 || ^17.0.0 || ^18.0.0'
|
||||||
|
|
||||||
react-quill-new@3.4.6:
|
react-quill-new@3.4.6:
|
||||||
resolution: {integrity: sha512-S2kEAwoKRo+xUIAEpb94fwiPe2QU3FmwIfQ+7Lkchf+izPa2nRu1mr4i4QxyVYg8TjHDryDUiOEYZuFEV45QFA==}
|
resolution: {integrity: sha512-S2kEAwoKRo+xUIAEpb94fwiPe2QU3FmwIfQ+7Lkchf+izPa2nRu1mr4i4QxyVYg8TjHDryDUiOEYZuFEV45QFA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2811,6 +2841,9 @@ packages:
|
|||||||
tiny-case@1.0.3:
|
tiny-case@1.0.3:
|
||||||
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
||||||
|
|
||||||
|
tiny-warning@1.0.3:
|
||||||
|
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
||||||
|
|
||||||
tinyglobby@0.2.14:
|
tinyglobby@0.2.14:
|
||||||
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -4119,6 +4152,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
'@types/hoist-non-react-statics@3.3.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 19.1.6
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
|
||||||
'@types/js-cookie@3.0.6': {}
|
'@types/js-cookie@3.0.6': {}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
@@ -4498,6 +4536,8 @@ snapshots:
|
|||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
|
deepmerge@2.2.1: {}
|
||||||
|
|
||||||
define-data-property@1.1.4:
|
define-data-property@1.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-define-property: 1.0.1
|
es-define-property: 1.0.1
|
||||||
@@ -4842,6 +4882,18 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
formik@2.4.6(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@types/hoist-non-react-statics': 3.3.6
|
||||||
|
deepmerge: 2.2.1
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
lodash: 4.17.21
|
||||||
|
lodash-es: 4.17.21
|
||||||
|
react: 19.1.0
|
||||||
|
react-fast-compare: 2.0.4
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
fs-extra@11.3.0:
|
fs-extra@11.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -4956,6 +5008,10 @@ snapshots:
|
|||||||
|
|
||||||
hls.js@1.6.5: {}
|
hls.js@1.6.5: {}
|
||||||
|
|
||||||
|
hoist-non-react-statics@3.3.2:
|
||||||
|
dependencies:
|
||||||
|
react-is: 16.13.1
|
||||||
|
|
||||||
html-parse-stringify@3.0.1:
|
html-parse-stringify@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
void-elements: 3.1.0
|
void-elements: 3.1.0
|
||||||
@@ -5614,6 +5670,8 @@ snapshots:
|
|||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
|
||||||
|
react-fast-compare@2.0.4: {}
|
||||||
|
|
||||||
react-hook-form@7.60.0(react@19.1.0):
|
react-hook-form@7.60.0(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@@ -5641,6 +5699,11 @@ snapshots:
|
|||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
react-element-popper: 2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
react-element-popper: 2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
||||||
|
react-otp-input@3.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
|
||||||
react-quill-new@3.4.6(quill-delta@5.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
react-quill-new@3.4.6(quill-delta@5.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash-es: 4.17.21
|
lodash-es: 4.17.21
|
||||||
@@ -5986,6 +6049,8 @@ snapshots:
|
|||||||
|
|
||||||
tiny-case@1.0.3: {}
|
tiny-case@1.0.3: {}
|
||||||
|
|
||||||
|
tiny-warning@1.0.3: {}
|
||||||
|
|
||||||
tinyglobby@0.2.14:
|
tinyglobby@0.2.14:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.4.4(picomatch@4.0.2)
|
fdir: 6.4.4(picomatch@4.0.2)
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-qu
|
|||||||
import { IApiErrorRepsonse } from './types/error.types';
|
import { IApiErrorRepsonse } from './types/error.types';
|
||||||
import ToastContainer from './components/Toast';
|
import ToastContainer from './components/Toast';
|
||||||
import { getRefreshToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
|
import { getRefreshToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
|
||||||
import { refreshToken } from './pages/auth/service/Service';
|
import { refreshToken } from './pages/auth/service/AuthService';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { CloseCircle, Paperclip2 } from 'iconsax-react'
|
||||||
|
import { FC, useCallback, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const UploadButton: FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { title } = props
|
||||||
|
|
||||||
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
|
||||||
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
|
setFiles((prev) => [...prev, ...acceptedFiles])
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||||
|
|
||||||
|
const handleRemove = (index: number) => {
|
||||||
|
const array = [...files]
|
||||||
|
array.splice(index, 1)
|
||||||
|
setFiles(array)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4 sm:flex-row'>
|
||||||
|
<div {...getRootProps()} className='h-10 bg-[#ECEEF5] rounded-full flex gap-2 items-center px-3 cursor-pointer'>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
|
||||||
|
<Paperclip2 size={20} color='#000' />
|
||||||
|
<div className='text-xs'>{title ? title : t('attachment_select')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<div key={file.name} className='bg-[#EBEDF5] text-xs flex gap-2 h-10 rounded-full items-center px-3'>
|
||||||
|
<span>{file.name}</span>
|
||||||
|
<CloseCircle size={19} color='#D52903' className='cursor-pointer' onClick={() => handleRemove(index)} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UploadButton
|
||||||
@@ -6,6 +6,7 @@ import Input from '../Input'
|
|||||||
import ReactQuill from 'react-quill-new';
|
import ReactQuill from 'react-quill-new';
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import Select from '../Select'
|
import Select from '../Select'
|
||||||
|
import UploadButton from '../UploadButton'
|
||||||
|
|
||||||
|
|
||||||
const NewMessage: FC = () => {
|
const NewMessage: FC = () => {
|
||||||
@@ -93,15 +94,8 @@ const NewMessage: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-wrap gap-2.5 items-center'>
|
<div className=''>
|
||||||
<div className='bg-[#EBEDF5] text-sm flex gap-2 h-10 rounded-full items-center px-4'>
|
<UploadButton />
|
||||||
<span>Lorem Ipsum.pdf</span>
|
|
||||||
<CloseCircle size={20} color='#D52903' />
|
|
||||||
</div>
|
|
||||||
<div className='bg-[#EBEDF5] text-sm flex gap-2 h-10 rounded-full items-center px-4'>
|
|
||||||
<span>Lorem Ipsum.pdf</span>
|
|
||||||
<CloseCircle size={20} color='#D52903' />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
||||||
@@ -109,7 +103,7 @@ const NewMessage: FC = () => {
|
|||||||
<Select
|
<Select
|
||||||
items={[]}
|
items={[]}
|
||||||
placeholder={t('new_message.select_priority')}
|
placeholder={t('new_message.select_priority')}
|
||||||
className='xl:w-[190px]'
|
className='xl:w-[190px] min-w-[130px]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1 justify-end flex gap-3'>
|
<div className='flex-1 justify-end flex gap-3'>
|
||||||
|
|||||||
@@ -64,3 +64,8 @@ export const convertToGB = (bytes: number): string => {
|
|||||||
const GB = bytes / (1024 * 1024 * 1024);
|
const GB = bytes / (1024 * 1024 * 1024);
|
||||||
return GB.toFixed(2);
|
return GB.toFixed(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getRedirectUrl = () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return urlParams.get("redirect");
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
const toPersianDigits = (text: string): string => {
|
||||||
|
return text.replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[parseInt(d, 10)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertNumbersInDOM = (node: Node): void => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
node.nodeValue = toPersianDigits(node.nodeValue || "");
|
||||||
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
// Skip conversion if element has 'en' class
|
||||||
|
if ((node as Element).classList?.contains("en")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.childNodes.forEach(convertNumbersInDOM);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useConvertNumbers = (): void => {
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === "childList") {
|
||||||
|
mutation.addedNodes.forEach(convertNumbersInDOM);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// اجرای اولیه روی کل صفحه
|
||||||
|
convertNumbersInDOM(document.body);
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useConvertNumbers;
|
||||||
Executable
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export interface ICountDown {
|
||||||
|
m: number;
|
||||||
|
s: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param minutes how many minutes.
|
||||||
|
* @param autoplay by default true. if you want play coundDown after an action, you can set it to false and manually play it.
|
||||||
|
* @param onEnd you can pass a callback function that will trigger at the END.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function useCountDown(minutes: number, autoplay = true, onEnd = () => {}) {
|
||||||
|
|
||||||
|
const [countDown, setCountDown] = useState<ICountDown>({m: minutes, s: 0});
|
||||||
|
const [stop, setStop] = useState<boolean>(!autoplay);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
if (countDown.m !== 0 || countDown.s !== 0) {
|
||||||
|
setCountDown(calculateCountDown(countDown));
|
||||||
|
} else {
|
||||||
|
onEnd();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [countDown]);
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setCountDown({m: minutes, s: 0});
|
||||||
|
};
|
||||||
|
const pause = () => {
|
||||||
|
setStop(true);
|
||||||
|
};
|
||||||
|
const play = () => {
|
||||||
|
setStop(false);
|
||||||
|
setCountDown({...countDown});
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateCountDown = (st: ICountDown): ICountDown => {
|
||||||
|
if (stop) {
|
||||||
|
return countDown;
|
||||||
|
}
|
||||||
|
const timeLeft = { m: 0, s: 0 };
|
||||||
|
const remain = st.m * 60 + st.s - 1;
|
||||||
|
if (remain > 0) {
|
||||||
|
timeLeft.m = Math.floor((remain) / 60);
|
||||||
|
timeLeft.s = remain - timeLeft.m * 60;
|
||||||
|
return timeLeft;
|
||||||
|
} else {
|
||||||
|
if (remain === 0 && countDown.s === 1) {
|
||||||
|
return timeLeft;
|
||||||
|
} else {
|
||||||
|
return countDown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: `${countDown.m > 9 ? countDown.m : '0' + countDown.m}:${countDown.s > 9 ? countDown.s : '0' + countDown.s}`,
|
||||||
|
reset,
|
||||||
|
pause,
|
||||||
|
play
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
const useDetectLatinAndNumbers = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
// تابعی برای اضافه کردن کلاسها
|
||||||
|
const addClassIfContainsLatinOrNumbers = (element: HTMLElement) => {
|
||||||
|
element.childNodes.forEach((node) => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
const textContent = node.nodeValue;
|
||||||
|
if (textContent) {
|
||||||
|
// اگر عدد داشت
|
||||||
|
if (/\d/.test(textContent)) {
|
||||||
|
element.classList.add("has-number");
|
||||||
|
}
|
||||||
|
// اگر حرف لاتین داشت
|
||||||
|
if (/[a-zA-Z]/.test(textContent)) {
|
||||||
|
element.classList.add("has-latin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const el = node as HTMLElement;
|
||||||
|
|
||||||
|
// بررسی `input` و `textarea`
|
||||||
|
if (
|
||||||
|
el instanceof HTMLInputElement ||
|
||||||
|
el instanceof HTMLTextAreaElement
|
||||||
|
) {
|
||||||
|
const checkAndApplyClasses = () => {
|
||||||
|
// با استفاده از requestAnimationFrame برای جلوگیری از بههمریختگی در سافاری
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (/\d/.test(el.value)) {
|
||||||
|
el.classList.add("has-number");
|
||||||
|
} else {
|
||||||
|
el.classList.remove("has-number");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[a-zA-Z]/.test(el.value)) {
|
||||||
|
el.classList.add("has-latin");
|
||||||
|
} else {
|
||||||
|
el.classList.remove("has-latin");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// مقدار اولیه را بررسی کنیم
|
||||||
|
checkAndApplyClasses();
|
||||||
|
|
||||||
|
// هر تغییری را بررسی کنیم
|
||||||
|
el.addEventListener("input", checkAndApplyClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
// باز هم اضافه کردن کلاس به سایر عناصر
|
||||||
|
addClassIfContainsLatinOrNumbers(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// استفاده از MutationObserver برای نظارت بر تغییرات DOM
|
||||||
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
mutationsList.forEach((mutation: any) => {
|
||||||
|
// تایپ دقیقتر برای جلوگیری از خطا در تایپاسکریپت
|
||||||
|
if (
|
||||||
|
mutation.type === "childList" ||
|
||||||
|
mutation.type === "subtree" ||
|
||||||
|
mutation.type === "attributes"
|
||||||
|
) {
|
||||||
|
addClassIfContainsLatinOrNumbers(mutation.target as HTMLElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// شروع به نظارت بر تمام تغییرات در بدنه صفحه
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// در نهایت، پاکسازی اثرات هنگام خروج از کامپوننت
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDetectLatinAndNumbers;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export const useOutsideClick = (callback: () => void) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', handleClickOutside);
|
||||||
|
document.addEventListener('touchend', handleClickOutside);
|
||||||
|
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mouseup', handleClickOutside);
|
||||||
|
document.removeEventListener('touchend', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
};
|
||||||
@@ -123,6 +123,7 @@
|
|||||||
"title1": "عنوان",
|
"title1": "عنوان",
|
||||||
"quota": "فضا/ استفاده شده "
|
"quota": "فضا/ استفاده شده "
|
||||||
},
|
},
|
||||||
|
"attachment_select": "اضافه کردن فایل ضمیمه",
|
||||||
"domain": {
|
"domain": {
|
||||||
"table": {
|
"table": {
|
||||||
"record_type": "نوع رکورد",
|
"record_type": "نوع رکورد",
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import LogoImage from '../../assets/images/logo.svg'
|
||||||
|
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||||
|
import { useAuthStore } from './store/AuthStore'
|
||||||
|
import LoginStep1 from './components/LoginStep1'
|
||||||
|
import LoginStep2 from './components/LoginStep2'
|
||||||
|
import LoginStep3 from './components/LoginStep3'
|
||||||
|
|
||||||
|
const Login: FC = () => {
|
||||||
|
|
||||||
|
const { stepLogin, phone, email } = useAuthStore()
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||||
|
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||||
|
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||||
|
<div className='flex-1 h-full flex flex-col'>
|
||||||
|
|
||||||
|
<div className='flex relative flex-1 flex-col h-full justify-center'>
|
||||||
|
<img src={LogoSmallImage} className='w-8 hidden xl:block' />
|
||||||
|
<img src={LogoImage} className='w-44 right-0 left-0 mx-auto absolute top-10 xl:hidden' />
|
||||||
|
{
|
||||||
|
stepLogin === 1 ?
|
||||||
|
<LoginStep1 />
|
||||||
|
:
|
||||||
|
stepLogin === 2 && !!phone ?
|
||||||
|
<LoginStep2 />
|
||||||
|
: stepLogin === 2 && !!email ?
|
||||||
|
<LoginStep3 />
|
||||||
|
: stepLogin === 3 ?
|
||||||
|
<LoginStep3 />
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
|
||||||
|
<img src={LogoImage} className='h-20' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { LoginType } from '../types/AuthTypes'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
|
import Error from '../../../components/Error'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { isEmail } from '../../../config/func'
|
||||||
|
import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { ArrowLeft } from 'iconsax-react'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
const LoginStep1: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { setPhone, phone, setStepLogin, setEmail } = useAuthStore()
|
||||||
|
const loginWithOtp = useLoginWithOtp()
|
||||||
|
const checkHasAccount = useCheckHasAccount()
|
||||||
|
|
||||||
|
const formik = useFormik<LoginType>({
|
||||||
|
initialValues: {
|
||||||
|
phone_email: phone,
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
phone_email: Yup.string()
|
||||||
|
.required(t('errors.required'))
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
if (isEmail(values.phone_email)) {
|
||||||
|
setEmail(values.phone_email)
|
||||||
|
checkHasAccount.mutate({ email: values.phone_email }, {
|
||||||
|
onSuccess() {
|
||||||
|
setStepLogin(2)
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast(error.response?.data?.error.message?.[0], 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setPhone(values.phone_email)
|
||||||
|
loginWithOtp.mutate({ phone: values.phone_email }, {
|
||||||
|
onSuccess() {
|
||||||
|
setStepLogin(2)
|
||||||
|
toast(t('auth.otp_sent'), 'success')
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast(error?.response?.data?.error?.message[0], 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='mt-5'>
|
||||||
|
<h2 className='text-2xl font-bold'>
|
||||||
|
{t('auth.welcome')}
|
||||||
|
</h2>
|
||||||
|
<p className='text-description text-sm mt-2'>
|
||||||
|
{t('auth.enter_info_login')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-16'>
|
||||||
|
<Input
|
||||||
|
label={t('auth.mobile_or_email')}
|
||||||
|
placeholder={t('auth.enter_mobile_or_email')}
|
||||||
|
type='text'
|
||||||
|
className='text-right'
|
||||||
|
name='phone_email'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.phone_email}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
formik.touched.phone_email && formik.errors.phone_email &&
|
||||||
|
<Error
|
||||||
|
errorText={formik.errors.phone_email}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={t('auth.next')}
|
||||||
|
className='mt-8'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
loading={loginWithOtp.isPending || checkHasAccount.isPending}
|
||||||
|
/>
|
||||||
|
<a href='https://accounts.danakcorp.com' target='_blank' className='mt-4 flex gap-1 text-sm justify-center text-description'>
|
||||||
|
<div>
|
||||||
|
{t('auth.old_version')}
|
||||||
|
</div>
|
||||||
|
<ArrowLeft size={20} color='black' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginStep1
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { FC, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { OtpVerifyType } from '../types/AuthTypes'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import OTPInput from 'react-otp-input'
|
||||||
|
import { useCountDown } from '../../../hooks/useCountDown'
|
||||||
|
// import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
||||||
|
import { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { setRefreshToken } from '../../../config/func'
|
||||||
|
import { setToken } from '../../../config/func'
|
||||||
|
import { getRedirectUrl } from '../../../config/func'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
import { Paths } from '@/utils/Paths'
|
||||||
|
const LoginStep2: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { phone, setStepLogin } = useAuthStore()
|
||||||
|
const { value, reset } = useCountDown(2, true)
|
||||||
|
const otpVerify = useOtpVerify()
|
||||||
|
const loginWithOtp = useLoginWithOtp()
|
||||||
|
|
||||||
|
const formik = useFormik<OtpVerifyType>({
|
||||||
|
initialValues: {
|
||||||
|
phone: phone,
|
||||||
|
code: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
code: Yup.string()
|
||||||
|
.required(t('errors.required'))
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
const params: OtpVerifyType = {
|
||||||
|
phone: phone,
|
||||||
|
code: values.code
|
||||||
|
}
|
||||||
|
otpVerify.mutate(params, {
|
||||||
|
onSuccess(data) {
|
||||||
|
setToken(data?.data?.accessToken?.token)
|
||||||
|
setRefreshToken(data?.data?.refreshToken?.token)
|
||||||
|
// Check if there's a redirect parameter in the URL
|
||||||
|
const redirectUrl = getRedirectUrl();
|
||||||
|
if (redirectUrl) {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
return; // Prevent the default navigation
|
||||||
|
}
|
||||||
|
window.location.href = Paths.received
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast(error?.response?.data?.error?.message[0], 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// بررسی پشتیبانی از Web OTP API
|
||||||
|
if (!("OTPCredential" in window)) return;
|
||||||
|
|
||||||
|
const ac = new AbortController();
|
||||||
|
|
||||||
|
navigator.credentials
|
||||||
|
.get({
|
||||||
|
otp: { transport: ["sms"] },
|
||||||
|
signal: ac.signal,
|
||||||
|
} as CredentialRequestOptions) // اطمینان از تطابق تایپها
|
||||||
|
.then((otpCredential) => {
|
||||||
|
if (otpCredential && "code" in otpCredential) {
|
||||||
|
formik.setFieldValue("code", (otpCredential).code);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => console.error("SMS AutoFill Error:", err));
|
||||||
|
|
||||||
|
return () => ac.abort();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reSend = () => {
|
||||||
|
loginWithOtp.mutate({ phone: phone }, {
|
||||||
|
onSuccess: () => {
|
||||||
|
reset()
|
||||||
|
toast(t('auth.toast_resend_code'), 'success')
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
if (formik.values.code.length === 5) {
|
||||||
|
formik.handleSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [formik.values.code])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='mt-5'>
|
||||||
|
<h2 className='lg:text-2xl font-bold'>
|
||||||
|
{t('auth.enter_otp')}
|
||||||
|
</h2>
|
||||||
|
<p className='text-description flex lg:flex-row flex-col lg:gap-1 gap-2 text-sm lg:mt-2 mt-3'>
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
<div>{t('auth.verfify_code_text_1')}</div>
|
||||||
|
<div>{phone}</div>
|
||||||
|
<div>
|
||||||
|
{t('auth.verfify_code_text_2')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => setStepLogin(1)} className='text-black cursor-pointer'>
|
||||||
|
{t('auth.change_number')}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-16'>
|
||||||
|
<div className='text-sm'>
|
||||||
|
{t('auth.verify_code')}
|
||||||
|
</div>
|
||||||
|
<div className='mt-2 w-full flex justify-center dltr otp'>
|
||||||
|
<OTPInput
|
||||||
|
shouldAutoFocus
|
||||||
|
value={formik.values.code}
|
||||||
|
numInputs={5}
|
||||||
|
inputType='tel'
|
||||||
|
onChange={(otp: string) => formik.setFieldValue('code', otp)}
|
||||||
|
renderInput={(props) =>
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
type='tel'
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
inputMode="numeric"
|
||||||
|
className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
formik.touched.code && formik.errors.code &&
|
||||||
|
<div className='text-xs mt-2 text-red-500'>{formik.errors.code}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className='flex mt-4 justify-end'>
|
||||||
|
{
|
||||||
|
value !== '00:00' ?
|
||||||
|
<div className='flex gap-1 text-description text-xs'>
|
||||||
|
<div>{value}</div>
|
||||||
|
<div>{t('auth.counter_otp')}</div>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div onClick={reSend} className='flex cursor-pointer gap-1 pl-2 text-black text-sm'>
|
||||||
|
<div className=''>
|
||||||
|
{t('auth.try_again')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={t('auth.login')}
|
||||||
|
className='mt-8'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
loading={otpVerify.isPending}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <div onClick={() => setStepLogin(3)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
|
||||||
|
<p className='text-description'>
|
||||||
|
{t('auth.login_with_password')}
|
||||||
|
</p>
|
||||||
|
<img src={ArrowLeftIcon} className='w-5' />
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginStep2
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { LoginWithPasswordType } from '../types/AuthTypes'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
|
import Error from '../../../components/Error'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { useLoginWithPassword } from '../hooks/useAuthData'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { setRefreshToken } from '../../../config/func'
|
||||||
|
import { setToken } from '../../../config/func'
|
||||||
|
import { getRedirectUrl } from '../../../config/func'
|
||||||
|
import { Paths } from '@/utils/Paths'
|
||||||
|
import { ArrowLeft } from 'iconsax-react'
|
||||||
|
|
||||||
|
const LoginStep3: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { setStepLogin, email, phone } = useAuthStore()
|
||||||
|
const loginWithPassword = useLoginWithPassword()
|
||||||
|
|
||||||
|
const formik = useFormik<LoginWithPasswordType>({
|
||||||
|
initialValues: {
|
||||||
|
password: '',
|
||||||
|
email: email ? email : undefined,
|
||||||
|
phone: phone ? phone : undefined,
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
password: Yup.string()
|
||||||
|
.required(t('errors.required'))
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
loginWithPassword.mutate(values, {
|
||||||
|
onSuccess(data) {
|
||||||
|
setToken(data?.data?.accessToken?.token)
|
||||||
|
setRefreshToken(data?.data?.refreshToken?.token)
|
||||||
|
const redirectUrl = getRedirectUrl();
|
||||||
|
if (redirectUrl) {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
return; // Prevent the default navigation
|
||||||
|
}
|
||||||
|
window.location.href = Paths.received
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast(error?.response?.data?.error?.message[0], 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='mt-5'>
|
||||||
|
<h2 className='text-2xl font-bold'>
|
||||||
|
{t('auth.enter_password_step3')}
|
||||||
|
</h2>
|
||||||
|
<p className='text-description text-sm mt-2'>
|
||||||
|
{t('auth.enter_your_password')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-16'>
|
||||||
|
<Input
|
||||||
|
label={t('auth.password')}
|
||||||
|
type='password'
|
||||||
|
className='text-right'
|
||||||
|
name='password'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.password}
|
||||||
|
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
formik.touched.password && formik.errors.password &&
|
||||||
|
<Error
|
||||||
|
errorText={formik.errors.password}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={t('auth.login')}
|
||||||
|
className='mt-8'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div onClick={() => setStepLogin(1)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
|
||||||
|
<p className='text-description'>
|
||||||
|
{t('auth.login_with_otp')}
|
||||||
|
</p>
|
||||||
|
<ArrowLeft size={20} color='black' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginStep3
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import * as api from '../service/AuthService'
|
||||||
|
import { CheckHasAccountPhoneType, CheckHasAccountType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType } from '../types/AuthTypes';
|
||||||
|
|
||||||
|
export const useLoginWithPassword = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: LoginWithPasswordType) => api.loginWithPassword(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLoginWithOtp = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: LoginWithOtpType) => api.loginWithOtp(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOtpVerify = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: OtpVerifyType) => api.otpVerify(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCheckHasAccount = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CheckHasAccountType) => api.checkHasAccount(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCheckHasAccountRegister = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CheckHasAccountPhoneType) => api.checkHasAccountRegister(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRegister = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: RegisterType) => api.register(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLogout = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => api.logout(),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import {
|
||||||
|
CheckHasAccountPhoneType,
|
||||||
|
CheckHasAccountType,
|
||||||
|
LoginWithOtpType,
|
||||||
|
LoginWithPasswordType,
|
||||||
|
OtpVerifyType,
|
||||||
|
RefreshTokenType,
|
||||||
|
RegisterType,
|
||||||
|
} from "../types/AuthTypes";
|
||||||
|
|
||||||
|
export const loginWithPassword = async (params: LoginWithPasswordType) => {
|
||||||
|
const { data } = await axios.post(`/auth/login/password`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginWithOtp = async (params: LoginWithOtpType) => {
|
||||||
|
const { data } = await axios.post(`/auth/otp/send`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const otpVerify = async (params: OtpVerifyType) => {
|
||||||
|
const { data } = await axios.post(`/auth/otp/verify`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
export const checkHasAccount = async (params: CheckHasAccountType) => {
|
||||||
|
const { data } = await axios.post(`/auth/check`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkHasAccountRegister = async (
|
||||||
|
params: CheckHasAccountPhoneType
|
||||||
|
) => {
|
||||||
|
const { data } = await axios.post(`/auth/register/initiate`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
export const register = async (params: RegisterType) => {
|
||||||
|
const { data } = await axios.post(`/auth/register/complete`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshToken = async (params: RefreshTokenType) => {
|
||||||
|
const { data } = await axios.post(`/auth/refresh`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logout = async () => {
|
||||||
|
const { data } = await axios.post(`/auth/logout`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import axiosDanak from "@/config/axiosDanak";
|
|
||||||
import { RefreshTokenType } from "../types/Types";
|
|
||||||
|
|
||||||
export const refreshToken = async (params: RefreshTokenType) => {
|
|
||||||
const { data } = await axiosDanak.post(`/auth/refresh`, params);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { AuthStoreType } from "../../auth/types/AuthTypes";
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthStoreType>((set) => ({
|
||||||
|
phone: "",
|
||||||
|
setPhone(value) {
|
||||||
|
set({ phone: value });
|
||||||
|
},
|
||||||
|
email: "",
|
||||||
|
setEmail(value) {
|
||||||
|
set({ email: value });
|
||||||
|
},
|
||||||
|
stepLogin: 1,
|
||||||
|
setStepLogin(value) {
|
||||||
|
set({ stepLogin: value });
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
export type LoginType = {
|
||||||
|
phone_email: string;
|
||||||
|
};
|
||||||
|
export type LoginPasswordType = {
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthStoreType = {
|
||||||
|
phone: string;
|
||||||
|
setPhone: (value: string) => void;
|
||||||
|
email: string;
|
||||||
|
setEmail: (value: string) => void;
|
||||||
|
stepLogin: number;
|
||||||
|
setStepLogin: (value: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginWithPasswordType = {
|
||||||
|
email?: string;
|
||||||
|
password: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginWithOtpType = {
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OtpVerifyType = {
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CheckHasAccountType = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CheckHasAccountPhoneType = {
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegisterType = {
|
||||||
|
phone: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
birthDate: string;
|
||||||
|
nationalCode: string;
|
||||||
|
password: string;
|
||||||
|
code: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RefreshTokenType = {
|
||||||
|
refreshToken: string;
|
||||||
|
};
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export type RefreshTokenType = {
|
|
||||||
refreshToken: string;
|
|
||||||
};
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { FC, useEffect, useState } from 'react'
|
|
||||||
import { useWorkspaces } from './hooks/useHomeData'
|
|
||||||
import { workspaceItem } from './types/HomeTypes'
|
|
||||||
import moment from 'moment-jalaali'
|
|
||||||
|
|
||||||
const Home: FC = () => {
|
|
||||||
|
|
||||||
const getWorkspaces = useWorkspaces()
|
|
||||||
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedWorkspace(localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (getWorkspaces.isError && getWorkspaces.error instanceof Error) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const error = getWorkspaces.error as any;
|
|
||||||
if (error.response?.status === 403) {
|
|
||||||
localStorage.removeItem(import.meta.env.VITE_WORKSPACE_ID)
|
|
||||||
window.location.href = 'https://console.danakcorp.com'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [getWorkspaces.isError, getWorkspaces.data])
|
|
||||||
|
|
||||||
|
|
||||||
const handleChangeWorkspace = (id: string) => {
|
|
||||||
localStorage.setItem(import.meta.env.VITE_WORKSPACE_ID, id)
|
|
||||||
setSelectedWorkspace(id)
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full flex gap-6'>
|
|
||||||
<div className='flex-1'>
|
|
||||||
|
|
||||||
<div className='mt-8 flex-wrap text-sm flex gap-6 items-center'>
|
|
||||||
{
|
|
||||||
getWorkspaces.data?.data?.workspaces?.map((item: workspaceItem) => {
|
|
||||||
return (
|
|
||||||
<div className={`p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 bg-white rounded-3xl cursor-pointer transition-all duration-300 hover:shadow-lg ${selectedWorkspace === item.id ? 'border border-black' : ''}`}
|
|
||||||
onClick={() => handleChangeWorkspace(item.id)}
|
|
||||||
>
|
|
||||||
<div className='mt-5'>
|
|
||||||
{item.businessName}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
{item.plan?.name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-between items-center mt-5 w-full'>
|
|
||||||
<div className=' flex gap-1'>
|
|
||||||
<div>تاریخ پایان: </div>
|
|
||||||
<div className='text-description text-sm'>{moment(item?.endDate).format('jYYYY/jMM/jDD')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5 flex justify-end w-full'>
|
|
||||||
|
|
||||||
<a onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}} target='_blank' href={`${import.meta.env.VITE_DZONE_URL}/${item.slug}`}>
|
|
||||||
{`${import.meta.env.VITE_DZONE_URL}/${item.slug}`}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className='px-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px]'></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Home
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import { Add } from 'iconsax-react'
|
|
||||||
import { FC, useState } from 'react'
|
|
||||||
import DefaulModal from '../../../components/DefaulModal'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import Input from '../../../components/Input'
|
|
||||||
import Select from '../../../components/Select'
|
|
||||||
import SwitchComponent from '../../../components/Switch'
|
|
||||||
|
|
||||||
const BoxNewAccessbility: FC = () => {
|
|
||||||
|
|
||||||
const [open, setOpen] = useState<boolean>(false)
|
|
||||||
const { t } = useTranslation('global')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
className="flex-1 min-w-[40%] xl:min-w-[20%] flex justify-center items-center h-[160px]"
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
zIndex: -1,
|
|
||||||
borderRadius: '22px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
fill="none"
|
|
||||||
// rx="22"
|
|
||||||
ry="22"
|
|
||||||
stroke="#8C90A3"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeDasharray="6,14"
|
|
||||||
// strokeDashoffset="25"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div style={{ zIndex: 1 }}>
|
|
||||||
<Add size={38} color="#8C90A3" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DefaulModal
|
|
||||||
open={open}
|
|
||||||
close={() => setOpen(false)}
|
|
||||||
isHeader={true}
|
|
||||||
title_header={t('home.create_new_access')}
|
|
||||||
>
|
|
||||||
<div className='mt-6'>
|
|
||||||
<div className='flex items-center gap-6'>
|
|
||||||
<div className='min-w-[210px]'>
|
|
||||||
<Input
|
|
||||||
variant='search'
|
|
||||||
className='bg-opacity-25 bg-white'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
items={[
|
|
||||||
{ value: '1', label: '1' },
|
|
||||||
{ value: '2', label: '2' },
|
|
||||||
{ value: '3', label: '3' },
|
|
||||||
]}
|
|
||||||
placeholder={t('all')}
|
|
||||||
className='bg-opacity-25 bg-white w-[100px]'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6 flex flex-wrap gap-6'>
|
|
||||||
<div className='flex-1 xl:min-w-[40%] min-w-full flex justify-between items-center border-b border-[#8C90A3] border-opacity-30 py-2'>
|
|
||||||
<div className='flex gap-4'>
|
|
||||||
<div className='size-10 rounded-xl bg-red-300'></div>
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
دی منو
|
|
||||||
</div>
|
|
||||||
<div className='text-xs text-description'>
|
|
||||||
منو رستوران
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<SwitchComponent
|
|
||||||
active
|
|
||||||
onChange={() => { }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex-1 xl:min-w-[40%] min-w-full flex justify-between items-center border-b border-[#8C90A3] border-opacity-30 py-2'>
|
|
||||||
<div className='flex gap-4'>
|
|
||||||
<div className='size-10 rounded-xl bg-blue-300'></div>
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
دی منو
|
|
||||||
</div>
|
|
||||||
<div className='text-xs text-description'>
|
|
||||||
منو رستوران
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<SwitchComponent
|
|
||||||
active
|
|
||||||
onChange={() => { }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex-1 xl:min-w-[40%] min-w-full flex justify-between items-center border-b border-[#8C90A3] border-opacity-30 py-2'>
|
|
||||||
<div className='flex gap-4'>
|
|
||||||
<div className='size-10 rounded-xl bg-green-300'></div>
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
دی منو
|
|
||||||
</div>
|
|
||||||
<div className='text-xs text-description'>
|
|
||||||
منو رستوران
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<SwitchComponent
|
|
||||||
active
|
|
||||||
onChange={() => { }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex-1 min-w-[40%]'></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DefaulModal>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BoxNewAccessbility
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { ArrowLeft } from 'iconsax-react'
|
|
||||||
import CoverImage from '../../../assets/images/banner.png'
|
|
||||||
|
|
||||||
const DanakLearning: FC = () => {
|
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='bg-white w-sidebar text-xs hidden 2xl:block h-fit px-5 py-7 rounded-3xl'>
|
|
||||||
<div className='flex justify-between items-center'>
|
|
||||||
<div>
|
|
||||||
{t('home.danak_learning')}
|
|
||||||
</div>
|
|
||||||
<div className='flex gap-2 items-center text-[10px]'>
|
|
||||||
<div>{t('home.see_all')}</div>
|
|
||||||
<ArrowLeft size={12} color='black' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-7'>
|
|
||||||
<div className='flex gap-3'>
|
|
||||||
<div>
|
|
||||||
<img src={CoverImage} alt='danak-learning-1' className='w-36 h-24 object-cover rounded-2xl' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='text-xs'>
|
|
||||||
<div className='leading-5'>
|
|
||||||
لورم ایپسوم متن ساختگی با تولید سادگی
|
|
||||||
</div>
|
|
||||||
<div className='flex gap-1 text-[11px] mt-3 text-description'>
|
|
||||||
<div>دیزاین ,</div>
|
|
||||||
<div>۲۴ دقیقه</div>
|
|
||||||
</div>
|
|
||||||
<div className='text-description text-[11px] mt-2'>
|
|
||||||
آذر ماه 1403
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DanakLearning
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { FC, ReactNode } from 'react'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string
|
|
||||||
icon: ReactNode,
|
|
||||||
color: string,
|
|
||||||
count: number,
|
|
||||||
description: string,
|
|
||||||
to?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ItemDashboard: FC<Props> = (props: Props) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to={props.to ? props.to : ''} className='p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px] bg-white rounded-3xl'>
|
|
||||||
<div className='size-10 rounded-full bg-[#EEF0F7] flex justify-center items-center'>
|
|
||||||
{props.icon}
|
|
||||||
</div>
|
|
||||||
<div className='mt-5'>
|
|
||||||
{props.title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5 text-xs text-description flex gap-1.5 items-center'>
|
|
||||||
<div style={{ background: props.color }} className='size-1.5 rounded-full'></div>
|
|
||||||
<div className='flex gap-0.5'>
|
|
||||||
<div>{props.count}</div>
|
|
||||||
<div className='whitespace-nowrap'>
|
|
||||||
{props.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ItemDashboard
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import * as api from "../service/HomeService";
|
|
||||||
|
|
||||||
export const useWorkspaces = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["workspaces"],
|
|
||||||
queryFn: api.getWorkspaces,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import danakAxios from "../../../config/axiosDanak";
|
|
||||||
|
|
||||||
export const getWorkspaces = async () => {
|
|
||||||
const { data } = await danakAxios.get(
|
|
||||||
`/subscriptions/workspaces/${import.meta.env.VITE_SERVICE_ID}`
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { UserItemType } from "../../users/types/UserTypes";
|
|
||||||
|
|
||||||
export type workspaceItem = {
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
plan: {
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
name: string;
|
|
||||||
duration: number;
|
|
||||||
price: number;
|
|
||||||
originalPrice: number;
|
|
||||||
isActive: boolean;
|
|
||||||
service: {
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
slug: string;
|
|
||||||
isDanakSuggest: boolean;
|
|
||||||
description: string;
|
|
||||||
author: string;
|
|
||||||
createDate: string;
|
|
||||||
userCount: number;
|
|
||||||
serviceLanguage: string;
|
|
||||||
softwareLanguage: string;
|
|
||||||
metaDescription: string;
|
|
||||||
isActive: boolean;
|
|
||||||
showInSlider: boolean;
|
|
||||||
link: string;
|
|
||||||
icon: string;
|
|
||||||
coverUrl: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
};
|
|
||||||
deletedAt: string | null;
|
|
||||||
};
|
|
||||||
startDate: string;
|
|
||||||
endDate: string;
|
|
||||||
businessName: string;
|
|
||||||
businessPhone: string;
|
|
||||||
description: string;
|
|
||||||
slug: string;
|
|
||||||
status: string;
|
|
||||||
staff: UserItemType[];
|
|
||||||
};
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import Tabs from '@/components/Tabs'
|
|
||||||
import { Brush2, Global, PenClose, People, Setting3 } from 'iconsax-react'
|
|
||||||
import { FC, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { SettingTabEnum } from './enum/SettingEnum'
|
|
||||||
import Personality from './personality/Personality'
|
|
||||||
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()
|
|
||||||
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.SETTING_DOMAIN)
|
|
||||||
|
|
||||||
const renderActiveTab = () => {
|
|
||||||
switch (activeTab) {
|
|
||||||
case SettingTabEnum.SETTING_MAIL_SERVER:
|
|
||||||
return <MailServer />;
|
|
||||||
case SettingTabEnum.SETTING_DOMAIN:
|
|
||||||
return <Domain />;
|
|
||||||
case SettingTabEnum.SETTING_ADDRESS:
|
|
||||||
return <Address />;
|
|
||||||
case SettingTabEnum.SETTING_SIGNATURE:
|
|
||||||
return <Signture />;
|
|
||||||
case SettingTabEnum.SETTING_PERSONALITY:
|
|
||||||
return (
|
|
||||||
<div className='flex xl:flex-row-reverse gap-2 md:gap-6 mt-6 md:mt-8'>
|
|
||||||
<div className="xl:w-auto">
|
|
||||||
<PersonalitySidebar />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Personality />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
|
||||||
<h1 className='text-lg mb-4 md:mb-0'>{t('setting.title')}</h1>
|
|
||||||
|
|
||||||
<div className='mt-4 md:mt-7'>
|
|
||||||
<Tabs
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
icon: <Setting3 size={20} color={activeTab === SettingTabEnum.SETTING_MAIL_SERVER ? 'black' : '#8C90A3'} />,
|
|
||||||
label: t('setting.mail_server'),
|
|
||||||
value: SettingTabEnum.SETTING_MAIL_SERVER
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Global size={20} color={activeTab === SettingTabEnum.SETTING_DOMAIN ? 'black' : '#8C90A3'} />,
|
|
||||||
label: t('setting.domain'),
|
|
||||||
value: SettingTabEnum.SETTING_DOMAIN
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <People size={20} color={activeTab === SettingTabEnum.SETTING_ADDRESS ? 'black' : '#8C90A3'} />,
|
|
||||||
label: t('setting.address'),
|
|
||||||
value: SettingTabEnum.SETTING_ADDRESS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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}
|
|
||||||
onChange={(value) => setActiveTab(value as SettingTabEnum)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{renderActiveTab()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Setting
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
import Input from '@/components/Input'
|
|
||||||
import Select from '@/components/Select'
|
|
||||||
import Table from '@/components/Table'
|
|
||||||
import { ColumnType } from '@/components/types/TableTypes'
|
|
||||||
import { FC, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import Button from '@/components/Button'
|
|
||||||
import { TickCircle } from 'iconsax-react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import { AddressData, CreateAddressType } from './types/Types'
|
|
||||||
import { useGetDomains } from '../domain/hooks/useDomainData'
|
|
||||||
import { yupResolver } from "@hookform/resolvers/yup"
|
|
||||||
import * as yup from "yup"
|
|
||||||
import { useCreateAddress, useGetAddress } from './hooks/useAddressData'
|
|
||||||
import { toast } from '@/components/Toast'
|
|
||||||
import { ErrorType } from '@/helpers/types'
|
|
||||||
import ProgressBarEmail from './components/ProgressBarEmail'
|
|
||||||
import ToggleStatus from './components/ToggleStatus'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Address: FC = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const schema = yup.object().shape({
|
|
||||||
title: yup.string().required(t('errors.required')),
|
|
||||||
username: yup.string().required(t('errors.required')),
|
|
||||||
password: yup.string().required(t('errors.required')),
|
|
||||||
domainId: yup.string().required(t('errors.required')),
|
|
||||||
})
|
|
||||||
|
|
||||||
const [search, setSearch] = useState<string>('')
|
|
||||||
const [status, setStatus] = useState<string>('all')
|
|
||||||
|
|
||||||
const { data: domain } = useGetDomains()
|
|
||||||
const { mutate: createAddress, isPending } = useCreateAddress()
|
|
||||||
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status)
|
|
||||||
const { register, handleSubmit, formState: { errors },
|
|
||||||
} = useForm<CreateAddressType>({
|
|
||||||
resolver: yupResolver(schema),
|
|
||||||
defaultValues: {
|
|
||||||
domainId: domain?.data?.domain?.id || '',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns: ColumnType<AddressData>[] = [
|
|
||||||
{
|
|
||||||
title: t('setting.address_title'),
|
|
||||||
key: 'title',
|
|
||||||
width: '200px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('setting.email'),
|
|
||||||
key: 'email',
|
|
||||||
width: '300px',
|
|
||||||
render: (item: AddressData) => (
|
|
||||||
<div key={item.id} className='dltr flex justify-end'>
|
|
||||||
{item.emailAddress}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('setting.quota'),
|
|
||||||
key: 'quota',
|
|
||||||
width: '300px',
|
|
||||||
render: (item: AddressData) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProgressBarEmail key={item.id} item={item} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('setting.status'),
|
|
||||||
key: 'status',
|
|
||||||
width: '120px',
|
|
||||||
align: 'center',
|
|
||||||
render: (item: AddressData) => (
|
|
||||||
<div key={item.id} className='dltr flex justify-end'>
|
|
||||||
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const statusOptions = [
|
|
||||||
{ value: 'all', label: 'همه' },
|
|
||||||
{ value: 'active', label: t('setting.active') },
|
|
||||||
{ value: 'inactive', label: t('setting.inactive') }
|
|
||||||
]
|
|
||||||
|
|
||||||
const onSubmit = (params: CreateAddressType) => {
|
|
||||||
createAddress(params, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
toast(data?.data?.message, 'success')
|
|
||||||
},
|
|
||||||
onError: (error: ErrorType) => {
|
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex xl:flex-row flex-col-reverse gap-8 mt-8'>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='flex gap-2 justify-between'>
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
placeholder={t('setting.status')}
|
|
||||||
items={statusOptions}
|
|
||||||
className='w-[120px]'
|
|
||||||
onChange={(e) => setStatus(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex-1'>
|
|
||||||
<Input
|
|
||||||
variant='search'
|
|
||||||
placeholder={t('setting.search')}
|
|
||||||
className='xl:w-[300px] flex-1'
|
|
||||||
onChangeSearchFinal={(e) => setSearch(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table
|
|
||||||
isLoading={isPendingAddress}
|
|
||||||
columns={columns}
|
|
||||||
data={addresses?.data?.users}
|
|
||||||
pagination={{
|
|
||||||
currentPage: 1,
|
|
||||||
totalPages: 6,
|
|
||||||
onPageChange: (page) => console.log('Page:', page)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='xl:w-[350px] w-full 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={domain?.data?.domain?.name}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Input
|
|
||||||
label={t('setting.title1')}
|
|
||||||
{...register('title')}
|
|
||||||
error_text={errors.title?.message}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6 relative'>
|
|
||||||
<Input
|
|
||||||
label={t('setting.username')}
|
|
||||||
className='text-left'
|
|
||||||
{...register('username')}
|
|
||||||
error_text={errors.username?.message}
|
|
||||||
/>
|
|
||||||
<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'>
|
|
||||||
@ {domain?.data?.domain?.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Input
|
|
||||||
label={t('setting.password')}
|
|
||||||
type='password'
|
|
||||||
{...register('password')}
|
|
||||||
error_text={errors.password?.message}
|
|
||||||
/>
|
|
||||||
</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'
|
|
||||||
onClick={handleSubmit(onSubmit)}
|
|
||||||
loading={isPending}
|
|
||||||
>
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<TickCircle size={20} color='white' />
|
|
||||||
<div>
|
|
||||||
{t('setting.create')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Address
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { convertToGB } from '@/config/func'
|
|
||||||
import { FC } from 'react'
|
|
||||||
import { AddressData } from '../types/Types'
|
|
||||||
|
|
||||||
const ProgressBarEmail: FC<{ item: AddressData }> = ({ item }) => {
|
|
||||||
const usedQuota = item.emailQuotaUsed || 0;
|
|
||||||
const totalQuota = item.emailQuota || 0;
|
|
||||||
const percentage = Math.min((usedQuota / totalQuota) * 100, 100);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='flex justify-between text-sm'>
|
|
||||||
<span className='text-gray-600 dltr'>
|
|
||||||
{convertToGB(usedQuota)} GB / {convertToGB(totalQuota)} GB
|
|
||||||
</span>
|
|
||||||
{/* <span className='text-gray-500'>
|
|
||||||
{percentage.toFixed(1)}%
|
|
||||||
</span> */}
|
|
||||||
</div>
|
|
||||||
<div className='w-full bg-gray-200 dltr overflow-hidden rounded-full h-2'>
|
|
||||||
<div
|
|
||||||
className={`h-2 rounded-full transition-all duration-300 ${percentage > 90 ? 'bg-red-500' :
|
|
||||||
percentage > 70 ? 'bg-yellow-500' :
|
|
||||||
'bg-green-500'
|
|
||||||
}`}
|
|
||||||
style={{ width: `${percentage}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProgressBarEmail
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import { FC, useState } from 'react'
|
|
||||||
import { useToggleStatus } from '../hooks/useAddressData'
|
|
||||||
|
|
||||||
const ToggleStatus: FC<{ defaultStatus: boolean, id: string }> = ({ defaultStatus, id }) => {
|
|
||||||
const [isActive, setIsActive] = useState(defaultStatus)
|
|
||||||
const { mutate: toggleStatus } = useToggleStatus()
|
|
||||||
|
|
||||||
const handleToggleStatus = () => {
|
|
||||||
toggleStatus(id)
|
|
||||||
setIsActive(!isActive)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Switch checked={isActive} onCheckedChange={handleToggleStatus} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ToggleStatus
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
||||||
import * as api from "../service/Service";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
export const useGetAddress = (search: string, status: string) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["address", search, status],
|
|
||||||
queryFn: () => api.getAddress(search, status),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCreateAddress = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: api.createAddress,
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["address"] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useToggleStatus = () => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: api.toggleStatus,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import axios from "@/config/axios";
|
|
||||||
import { CreateAddressType } from "../types/Types";
|
|
||||||
|
|
||||||
export const getAddress = async (search: string, status: string) => {
|
|
||||||
const query = new URLSearchParams();
|
|
||||||
if (search) query.set("q", search);
|
|
||||||
if (status && status !== "all")
|
|
||||||
query.set("isActive", status === "active" ? "1" : "0");
|
|
||||||
const { data } = await axios.get(`/users?${query.toString()}`);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createAddress = async (params: CreateAddressType) => {
|
|
||||||
const { data } = await axios.post(`/users`, params);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toggleStatus = async (id: string) => {
|
|
||||||
const { data } = await axios.patch(`/users/${id}/toggle-status`);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
export type CreateAddressType = {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
domainId: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface AddressData extends Record<string, unknown> {
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
title: string;
|
|
||||||
userName: string;
|
|
||||||
password: string;
|
|
||||||
emailAddress: string;
|
|
||||||
emailEnabled: boolean;
|
|
||||||
emailQuota: number;
|
|
||||||
emailQuotaUsed: number;
|
|
||||||
emailAccountId: string;
|
|
||||||
displayName: string;
|
|
||||||
isActive: boolean;
|
|
||||||
emailSignature: string | null;
|
|
||||||
role: string;
|
|
||||||
business: {
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
danakSubscriptionId: string;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
logoUrl: string | null;
|
|
||||||
domain: string;
|
|
||||||
};
|
|
||||||
domain: {
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
name: string;
|
|
||||||
status: string;
|
|
||||||
isVerified: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
verifiedAt: string;
|
|
||||||
lastCheckedAt: string | null;
|
|
||||||
notes: string;
|
|
||||||
dkimEnabled: boolean;
|
|
||||||
dkimSelector: string;
|
|
||||||
dkimPrivateKey: string;
|
|
||||||
dkimPublicKey: string;
|
|
||||||
spfEnabled: boolean;
|
|
||||||
spfRecord: string | null;
|
|
||||||
dmarcEnabled: boolean;
|
|
||||||
dmarcPolicy: string | null;
|
|
||||||
business: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import Table from '@/components/Table'
|
|
||||||
import { FC, useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { TickCircle, Copy, CloseCircle, Clock, TickSquare } from 'iconsax-react'
|
|
||||||
import CreateDomain from './components/CreateDomain'
|
|
||||||
import { useGetDnsRecords } from './hooks/useDomainData'
|
|
||||||
import { DnsRecordType } from './types/Types'
|
|
||||||
|
|
||||||
const Domain: FC = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
const { data, isLoading } = useGetDnsRecords(disabled)
|
|
||||||
const [copiedField, setCopiedField] = useState<string | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data?.data?.overallStatus?.isVerified && !disabled) {
|
|
||||||
setDisabled(true)
|
|
||||||
}
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
const getStatusIcon = (status: DnsRecordType['status']) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'VERIFIED':
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<TickCircle size={20} color="#22C55E" variant="Bold" />
|
|
||||||
<span className="text-green-500 text-xs">{t('domain.status.verified')}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
case 'PENDING':
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Clock size={20} color="#F59E0B" variant="Bold" />
|
|
||||||
<span className="text-yellow-500 text-xs">{t('domain.status.pending')}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
case 'FAILED':
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CloseCircle size={20} color="#EF4444" variant="Bold" />
|
|
||||||
<span className="text-red-500 text-xs">{t('domain.status.failed')}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyToClipboard = (text: string, fieldKey: string) => {
|
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
|
||||||
setCopiedField(fieldKey)
|
|
||||||
setTimeout(() => {
|
|
||||||
setCopiedField(null)
|
|
||||||
}, 2000)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: t('domain.table.record_type'),
|
|
||||||
key: 'type',
|
|
||||||
width: '100px',
|
|
||||||
align: 'center' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('domain.table.record_name'),
|
|
||||||
key: 'name',
|
|
||||||
render: (record: DnsRecordType) => {
|
|
||||||
const fieldKey = `name-${record.id}`
|
|
||||||
const isCopied = copiedField === fieldKey
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => copyToClipboard(record.name, fieldKey)}
|
|
||||||
className={`transition-all duration-200 ${isCopied ? 'text-green-500' : 'text-blue-500 hover:text-blue-700'}`}
|
|
||||||
>
|
|
||||||
{isCopied ? (
|
|
||||||
<TickSquare size={16} color='#22C55E' variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<Copy size={16} color='#0038FF' />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<span className="truncate max-w-[200px] dltr">{record.name}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('domain.table.record_value'),
|
|
||||||
key: 'value',
|
|
||||||
render: (record: DnsRecordType) => {
|
|
||||||
const fieldKey = `value-${record.id}`
|
|
||||||
const isCopied = copiedField === fieldKey
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => copyToClipboard(record.value, fieldKey)}
|
|
||||||
className={`transition-all duration-200 ${isCopied ? 'text-green-500' : 'text-blue-500 hover:text-blue-700'}`}
|
|
||||||
>
|
|
||||||
{isCopied ? (
|
|
||||||
<TickSquare size={16} color='#22C55E' variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<Copy size={16} color='#0038FF' />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<span className="truncate max-w-[300px] dltr">{record.value}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('domain.table.priority'),
|
|
||||||
key: 'priority',
|
|
||||||
width: '80px',
|
|
||||||
align: 'center' as const,
|
|
||||||
render: (record: DnsRecordType) => (
|
|
||||||
<span>{record.priority || '-'}</span>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('domain.table.ttl'),
|
|
||||||
key: 'ttl',
|
|
||||||
width: '80px',
|
|
||||||
align: 'center' as const,
|
|
||||||
render: (record: DnsRecordType) => (
|
|
||||||
<span>{record.ttl}</span>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('domain.table.status'),
|
|
||||||
key: 'status',
|
|
||||||
render: (record: DnsRecordType) => getStatusIcon(record.status)
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const dnsRecords = data?.data?.dnsRecords || []
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<CreateDomain />
|
|
||||||
|
|
||||||
<div className='mt-9'>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
data={dnsRecords}
|
|
||||||
className="!mt-0"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Domain
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import Button from '@/components/Button'
|
|
||||||
import Input from '@/components/Input'
|
|
||||||
import { toast } from '@/components/Toast'
|
|
||||||
import { FC, useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useCreateDomain, useGetDomains } from '../hooks/useDomainData'
|
|
||||||
import { ErrorType } from '@/helpers/types'
|
|
||||||
|
|
||||||
const CreateDomain: FC = () => {
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { mutate: createDomain, isPending } = useCreateDomain()
|
|
||||||
const { data: domains } = useGetDomains()
|
|
||||||
const [domain, setDomain] = useState('')
|
|
||||||
const [isVerified, setIsVerified] = useState<boolean>(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (domains?.data?.domain) {
|
|
||||||
setDomain(domains?.data?.domain?.name)
|
|
||||||
if (domains?.data?.domain?.isVerified) {
|
|
||||||
setIsVerified(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [domains])
|
|
||||||
|
|
||||||
|
|
||||||
const handleCreateDomain = () => {
|
|
||||||
createDomain({
|
|
||||||
name: domain,
|
|
||||||
notes: domain
|
|
||||||
}, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
toast(data.message, 'success')
|
|
||||||
setDomain('')
|
|
||||||
},
|
|
||||||
onError: (error: ErrorType) => {
|
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className='flex xl:flex-row gap-4 flex-col justify-between mt-9 items-end'>
|
|
||||||
<div>
|
|
||||||
<div className='xl:text-lg'>
|
|
||||||
{t('setting.record_dns')}
|
|
||||||
</div>
|
|
||||||
<p className='mt-2 xl:text-sm text-xs font-extralight'>
|
|
||||||
{t('setting.record_dns_description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<Input
|
|
||||||
placeholder={t('setting.your_domain')}
|
|
||||||
className='xl:w-[300px]'
|
|
||||||
value={domain}
|
|
||||||
onChange={(e) => setDomain(e.target.value)}
|
|
||||||
readOnly={isVerified}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
!isVerified &&
|
|
||||||
<Button
|
|
||||||
variant='secondary'
|
|
||||||
className='w-fit xl:px-14 px-8 border border-black'
|
|
||||||
label={t('setting.submit')}
|
|
||||||
onClick={handleCreateDomain}
|
|
||||||
loading={isPending}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateDomain
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
||||||
import { CreateDomainType } from "../types/Types";
|
|
||||||
import * as api from "../service/DomainService";
|
|
||||||
|
|
||||||
export const useCreateDomain = () => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (variables: CreateDomainType) => api.createDomain(variables),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetDomains = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["domains"],
|
|
||||||
queryFn: () => api.getDomains(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetDnsRecords = (disabled?: boolean) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["dns-records"],
|
|
||||||
queryFn: () => api.getDnsRecords(),
|
|
||||||
refetchInterval: 3000,
|
|
||||||
enabled: !disabled,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useVerifyDnsRecord = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["verify-dns-record"],
|
|
||||||
queryFn: () => api.verifyDnsRecord(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import axios from "@/config/axios";
|
|
||||||
import { CreateDomainType } from "../types/Types";
|
|
||||||
|
|
||||||
export const createDomain = async (params: CreateDomainType) => {
|
|
||||||
const { data } = await axios.post(`/domains`, params);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDomains = async () => {
|
|
||||||
const { data } = await axios.get(`/domains`);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDnsRecords = async () => {
|
|
||||||
const { data } = await axios.get(`/domains/dns-records`);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const verifyDnsRecord = async () => {
|
|
||||||
const { data } = await axios.get(`/domains/verify-dns`);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { IResponse } from "@/types/response.types";
|
|
||||||
|
|
||||||
export type CreateDomainType = {
|
|
||||||
name: string;
|
|
||||||
notes?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DnsRecordType = {
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
wildduckId: string | null;
|
|
||||||
name: string;
|
|
||||||
type: "MX" | "SPF" | "DMARC" | "DKIM" | "TXT" | "A" | "AAAA" | "CNAME";
|
|
||||||
value: string;
|
|
||||||
ttl: number;
|
|
||||||
priority: number | null;
|
|
||||||
status: "PENDING" | "VERIFIED" | "FAILED";
|
|
||||||
isRequired: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
lastVerifiedAt: string | null;
|
|
||||||
lastCheckedAt: string | null;
|
|
||||||
description: string;
|
|
||||||
errorMessage: string | null;
|
|
||||||
verificationAttempts: number;
|
|
||||||
nextVerificationAt: string | null;
|
|
||||||
domain: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DnsRecordResponseType = IResponse<{
|
|
||||||
dnsRecords: DnsRecordType[];
|
|
||||||
}>;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
export enum SettingTabEnum {
|
|
||||||
SETTING_MAIL_SERVER = "setting_mail_server",
|
|
||||||
SETTING_DOMAIN = "setting_domain",
|
|
||||||
SETTING_ADDRESS = "setting_address",
|
|
||||||
SETTING_PERSONALITY = "setting_personality",
|
|
||||||
SETTING_SIGNATURE = "setting_signature",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SideBarTab {
|
|
||||||
SETTING = "setting",
|
|
||||||
TEXT = "text",
|
|
||||||
BUTTON = "button",
|
|
||||||
IMAGE = "image",
|
|
||||||
NONE = "none",
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import Input from '@/components/Input'
|
|
||||||
import RadioGroup from '@/components/RadioGroup'
|
|
||||||
import { FC } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
const Domain: FC = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
return (
|
|
||||||
<div className='bg-white rounded-4xl p-8 mt-9'>
|
|
||||||
<div>
|
|
||||||
<div className='flex xl:flex-row gap-4 flex-col justify-between xl:items-end border-b pb-10 broder-border'>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='xl:text-lg'>
|
|
||||||
{t('setting.setting_fetch')}
|
|
||||||
</div>
|
|
||||||
<div className='xl:text-sm text-xs 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 xl:flex-row gap-4 flex-col justify-between pb-10'>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='xl:text-lg'>
|
|
||||||
{t('setting.setting_smtp')}
|
|
||||||
</div>
|
|
||||||
<div className='xl:text-sm text-xs text-description mt-1.5'>
|
|
||||||
{t('setting.setting_smtp_description')}
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Domain
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import HeaderSection from './components/HeaderSection'
|
|
||||||
import ContentSection from './components/ContentSection'
|
|
||||||
import FooterSection from './components/FooterSection'
|
|
||||||
|
|
||||||
const Personality: FC = () => {
|
|
||||||
return (
|
|
||||||
<div className='flex-1 bg-white rounded-4xl p-8'>
|
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
|
|
||||||
<tbody>
|
|
||||||
<HeaderSection />
|
|
||||||
<ContentSection />
|
|
||||||
<FooterSection />
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Personality
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { FC, useState, useRef, useEffect } from 'react'
|
|
||||||
import { Gallery, LinkSquare, Setting4, Text } from 'iconsax-react'
|
|
||||||
import Logo from '@/assets/images/logo-small.svg'
|
|
||||||
import SettingSideBar from './components/SettingSideBar'
|
|
||||||
import { SideBarTab } from '../enum/SettingEnum'
|
|
||||||
import TextSidebar from './components/TextSidebar'
|
|
||||||
import ButtonSidebar from './components/ButtonSidebar'
|
|
||||||
import ImageSideBar from './components/ImageSideBar'
|
|
||||||
import { clx } from '@/helpers/utils'
|
|
||||||
|
|
||||||
const PersonalitySidebar: FC = () => {
|
|
||||||
|
|
||||||
const [active, setActive] = useState<SideBarTab>(SideBarTab.SETTING)
|
|
||||||
const sidebarRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
// فقط در موبایل (عرض کمتر از 1280px) کار کند
|
|
||||||
if (window.innerWidth < 1280 && sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) {
|
|
||||||
setActive(SideBarTab.NONE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (active !== SideBarTab.NONE) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside)
|
|
||||||
}
|
|
||||||
}, [active])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={sidebarRef} className={clx(
|
|
||||||
'bg-white xl:rounded-4xl xl:overflow-hidden xl:w-[360px] flex h-full sticky top-0',
|
|
||||||
active === SideBarTab.NONE ? 'rounded-4xl' : 'rounded-r-4xl'
|
|
||||||
)}>
|
|
||||||
{active !== SideBarTab.NONE && (
|
|
||||||
<div className='absolute rounded-l-4xl shadow-[-2px_0_4px_-1px_rgba(0,0,0,0.05)] xl:shadow-none xl:static bg-white right-10 h-full z-10 xl:w-[360px] w-[250px] p-6 overflow-y-auto'>
|
|
||||||
{
|
|
||||||
active === SideBarTab.SETTING ? <SettingSideBar />
|
|
||||||
: active === SideBarTab.TEXT ? <TextSidebar />
|
|
||||||
: active === SideBarTab.BUTTON ? <ButtonSidebar />
|
|
||||||
: active === SideBarTab.IMAGE ? <ImageSideBar />
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className='xl:w-20 xl:border-r py-5 border-gray-200 w-10 flex flex-col items-center pt-6 pb-6 h-full'>
|
|
||||||
{
|
|
||||||
active !== SideBarTab.NONE && <img src={Logo} className='w-8' alt="Logo" />
|
|
||||||
}
|
|
||||||
<div className={clx(
|
|
||||||
'flex flex-col gap-10 mt-16',
|
|
||||||
active === SideBarTab.NONE && 'mt-4'
|
|
||||||
)}>
|
|
||||||
<Setting4 size={22} color='black' variant={SideBarTab.SETTING === active ? 'Bold' : 'Outline'} onClick={() => setActive(SideBarTab.SETTING)} className='cursor-pointer' />
|
|
||||||
<Text size={22} color='black' variant={SideBarTab.TEXT === active ? 'Bold' : 'Outline'} onClick={() => setActive(SideBarTab.TEXT)} className='cursor-pointer' />
|
|
||||||
<LinkSquare size={22} color='black' variant={SideBarTab.BUTTON === active ? 'Bold' : 'Outline'} onClick={() => setActive(SideBarTab.BUTTON)} className='cursor-pointer' />
|
|
||||||
<Gallery size={22} color='black' variant={SideBarTab.IMAGE === active ? 'Bold' : 'Outline'} onClick={() => setActive(SideBarTab.IMAGE)} className='cursor-pointer' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PersonalitySidebar
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import { ButtonType, ButtonSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
|
|
||||||
|
|
||||||
interface ButtonRendererProps {
|
|
||||||
buttons: ButtonType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const ButtonRenderer: FC<ButtonRendererProps> = ({ buttons }) => {
|
|
||||||
if (!buttons || buttons.length === 0) return null
|
|
||||||
|
|
||||||
// تبدیل enum به string برای HTML attributes
|
|
||||||
const getHorizontalAlign = (alignment?: HorizontalAlignment) => {
|
|
||||||
switch (alignment) {
|
|
||||||
case HorizontalAlignment.LEFT:
|
|
||||||
return 'left'
|
|
||||||
case HorizontalAlignment.RIGHT:
|
|
||||||
return 'right'
|
|
||||||
case HorizontalAlignment.CENTER:
|
|
||||||
default:
|
|
||||||
return 'center'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getVerticalAlign = (verticalAlignment?: VerticalAlignment) => {
|
|
||||||
switch (verticalAlignment) {
|
|
||||||
case VerticalAlignment.TOP:
|
|
||||||
return 'top'
|
|
||||||
case VerticalAlignment.BOTTOM:
|
|
||||||
return 'bottom'
|
|
||||||
case VerticalAlignment.MIDDLE:
|
|
||||||
default:
|
|
||||||
return 'middle'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
align={getHorizontalAlign(buttons[0]?.alignment)}
|
|
||||||
valign={getVerticalAlign(buttons[0]?.verticalAlignment)}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<table cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
{buttons.slice(0, 2).map((button, buttonIndex) => (
|
|
||||||
<td key={button.id} style={{ paddingRight: buttonIndex === 0 && buttons.length > 1 ? '4px' : '0' }}>
|
|
||||||
<table cellPadding="0" cellSpacing="0" border={0} style={{
|
|
||||||
borderCollapse: 'collapse',
|
|
||||||
backgroundColor: button.backgroundColor,
|
|
||||||
border: button.isBorder ? `${button.borderSize}px solid ${button.borderColor}` : 'none',
|
|
||||||
borderRadius: '4px'
|
|
||||||
}}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
align="center"
|
|
||||||
valign="middle"
|
|
||||||
style={{
|
|
||||||
padding: button.size === ButtonSize.SMALL ? '2px 4px' : button.size === ButtonSize.MEDIUM ? '4px 8px' : '8px 12px',
|
|
||||||
fontSize: button.size === ButtonSize.SMALL ? '11px' : button.size === ButtonSize.MEDIUM ? '13px' : '15px',
|
|
||||||
color: button.textColor,
|
|
||||||
textDecoration: 'none',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
maxWidth: 'fit-content',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a href={button.link || '#'} style={{
|
|
||||||
color: button.textColor,
|
|
||||||
textDecoration: 'none',
|
|
||||||
}}>
|
|
||||||
{button.text}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ButtonRenderer
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import Button from '@/components/Button'
|
|
||||||
import ColorPicker from '@/components/ColorPicker'
|
|
||||||
import Input from '@/components/Input'
|
|
||||||
import Select from '@/components/Select'
|
|
||||||
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'
|
|
||||||
import { ButtonSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
|
|
||||||
|
|
||||||
const ButtonSidebar: FC = () => {
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { addButtonToActiveItem } = usePersonalityStore()
|
|
||||||
|
|
||||||
const [buttonText, setButtonText] = useState<string>('')
|
|
||||||
const [buttonLink, setButtonLink] = useState<string>('')
|
|
||||||
const [buttonSize, setButtonSize] = useState<ButtonSize>(ButtonSize.MEDIUM)
|
|
||||||
const [isBorder, setIsBorder] = useState<boolean>(false)
|
|
||||||
const [borderSize, setBorderSize] = useState<number>(1)
|
|
||||||
const [textColor, setTextColor] = useState<string>('#fff')
|
|
||||||
const [backgroundColor, setBackgroundColor] = useState<string>('#000')
|
|
||||||
const [borderColor, setBorderColor] = useState<string>('#fff')
|
|
||||||
const [horizontalAlignment, setHorizontalAlignment] = useState<HorizontalAlignment>(HorizontalAlignment.CENTER)
|
|
||||||
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
|
|
||||||
|
|
||||||
const sizeOptions = [
|
|
||||||
{ label: 'کوچک', value: ButtonSize.SMALL },
|
|
||||||
{ label: 'متوسط', value: ButtonSize.MEDIUM },
|
|
||||||
{ label: 'بزرگ', value: ButtonSize.LARGE }
|
|
||||||
]
|
|
||||||
|
|
||||||
const handleAddButton = () => {
|
|
||||||
addButtonToActiveItem({
|
|
||||||
text: buttonText,
|
|
||||||
link: buttonLink,
|
|
||||||
size: buttonSize,
|
|
||||||
isBorder,
|
|
||||||
borderSize,
|
|
||||||
textColor,
|
|
||||||
backgroundColor,
|
|
||||||
borderColor,
|
|
||||||
alignment: horizontalAlignment,
|
|
||||||
verticalAlignment,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reset form after adding
|
|
||||||
setButtonText('')
|
|
||||||
setButtonLink('')
|
|
||||||
setButtonSize(ButtonSize.MEDIUM)
|
|
||||||
setIsBorder(false)
|
|
||||||
setBorderSize(1)
|
|
||||||
setTextColor('#fff')
|
|
||||||
setBackgroundColor('#000')
|
|
||||||
setBorderColor('#fff')
|
|
||||||
setHorizontalAlignment(HorizontalAlignment.CENTER)
|
|
||||||
setVerticalAlignment(VerticalAlignment.MIDDLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className='text-lg'>{t('setting.button')}</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
|
||||||
<Input
|
|
||||||
label={t('setting.button_text')}
|
|
||||||
value={buttonText}
|
|
||||||
onChange={(e) => setButtonText(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<Input
|
|
||||||
label={t('setting.button_link')}
|
|
||||||
value={buttonLink}
|
|
||||||
onChange={(e) => setButtonLink(e.target.value)}
|
|
||||||
endIcon={<Link2 size={18} color='#888' />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<Select
|
|
||||||
label={t('setting.size')}
|
|
||||||
items={sizeOptions}
|
|
||||||
value={buttonSize}
|
|
||||||
onChange={(e) => setButtonSize(e.target.value as ButtonSize)}
|
|
||||||
placeholder={t('setting.select')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5 flex gap-2 items-center text-sm'>
|
|
||||||
<Checkbox
|
|
||||||
checked={isBorder}
|
|
||||||
onCheckedChange={() => setIsBorder(!isBorder)}
|
|
||||||
/>
|
|
||||||
<div>{t('setting.border')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isBorder &&
|
|
||||||
<div className='mt-5'>
|
|
||||||
<Input
|
|
||||||
type='number'
|
|
||||||
label={t('setting.border')}
|
|
||||||
value={borderSize.toString()}
|
|
||||||
onChange={(e) => setBorderSize(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<ColorPicker
|
|
||||||
label={t('setting.text_color')}
|
|
||||||
defaultColor={textColor}
|
|
||||||
changeColor={setTextColor}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<ColorPicker
|
|
||||||
label={t('setting.background_color')}
|
|
||||||
defaultColor={backgroundColor}
|
|
||||||
changeColor={setBackgroundColor}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<ColorPicker
|
|
||||||
label={t('setting.border_color')}
|
|
||||||
defaultColor={borderColor}
|
|
||||||
changeColor={setBorderColor}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5 flex justify-between'>
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('setting.horizontal_position')}
|
|
||||||
</div>
|
|
||||||
<div className='mt-3 flex gap-4'>
|
|
||||||
<div
|
|
||||||
onClick={() => setHorizontalAlignment(HorizontalAlignment.RIGHT)}
|
|
||||||
>
|
|
||||||
<AlignRight size={20} color={horizontalAlignment === HorizontalAlignment.RIGHT ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setHorizontalAlignment(HorizontalAlignment.CENTER)}
|
|
||||||
>
|
|
||||||
<AlignVertically size={20} color={horizontalAlignment === HorizontalAlignment.CENTER ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setHorizontalAlignment(HorizontalAlignment.LEFT)}
|
|
||||||
>
|
|
||||||
<AlignLeft size={20} color={horizontalAlignment === HorizontalAlignment.LEFT ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('setting.vertical_position')}
|
|
||||||
</div>
|
|
||||||
<div className='mt-3 flex gap-4'>
|
|
||||||
<div
|
|
||||||
onClick={() => setVerticalAlignment(VerticalAlignment.TOP)}
|
|
||||||
>
|
|
||||||
<AlignTop size={20} color={verticalAlignment === VerticalAlignment.TOP ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setVerticalAlignment(VerticalAlignment.MIDDLE)}
|
|
||||||
>
|
|
||||||
<AlignHorizontally size={20} color={verticalAlignment === VerticalAlignment.MIDDLE ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setVerticalAlignment(VerticalAlignment.BOTTOM)}
|
|
||||||
>
|
|
||||||
<AlignBottom size={20} color={verticalAlignment === VerticalAlignment.BOTTOM ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-7'>
|
|
||||||
<Button
|
|
||||||
onClick={handleAddButton}
|
|
||||||
className='bg-[#ECEEF5] text-black'
|
|
||||||
>
|
|
||||||
<div className='flex justify-center items-center gap-2'>
|
|
||||||
<Add size={24} color='black' />
|
|
||||||
<span>{t('setting.add_button')}</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ButtonSidebar
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import { usePersonalityStore } from '../store/Store'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { AddCircle } from 'iconsax-react'
|
|
||||||
import TextRenderer from './TextRenderer'
|
|
||||||
import ButtonRenderer from './ButtonRenderer'
|
|
||||||
import ImageRenderer from './ImageRenderer'
|
|
||||||
import { SectionName, VerticalAlignment, HorizontalAlignment } from '../types/Types'
|
|
||||||
|
|
||||||
const ContentSection: FC = () => {
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
|
|
||||||
|
|
||||||
const handleSectionClick = (section: SectionName) => {
|
|
||||||
setActiveSection(section)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleItemClick = (index: number) => {
|
|
||||||
setActiveItemIndex(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// تابعهای helper برای تعیین alignment دکمهها
|
|
||||||
const getButtonHorizontalAlign = (button?: { alignment?: HorizontalAlignment }) => {
|
|
||||||
if (!button || !button.alignment) return 'center'
|
|
||||||
switch (button.alignment) {
|
|
||||||
case HorizontalAlignment.LEFT:
|
|
||||||
return 'left'
|
|
||||||
case HorizontalAlignment.RIGHT:
|
|
||||||
return 'right'
|
|
||||||
case HorizontalAlignment.CENTER:
|
|
||||||
default:
|
|
||||||
return 'center'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getButtonVerticalAlign = (button?: { verticalAlignment?: VerticalAlignment }) => {
|
|
||||||
if (!button || !button.verticalAlignment) return 'middle'
|
|
||||||
switch (button.verticalAlignment) {
|
|
||||||
case VerticalAlignment.TOP:
|
|
||||||
return 'top'
|
|
||||||
case VerticalAlignment.BOTTOM:
|
|
||||||
return 'bottom'
|
|
||||||
case VerticalAlignment.MIDDLE:
|
|
||||||
default:
|
|
||||||
return 'middle'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr onClick={() => handleSectionClick(SectionName.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 === SectionName.CONTENT ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
|
||||||
borderRadius: '24px'
|
|
||||||
}}>
|
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ tableLayout: 'fixed' }}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
{
|
|
||||||
Array.from({ length: data.content.columnsCount }, (_, index) => {
|
|
||||||
const item = data.content.items[index];
|
|
||||||
const isActive = activeSection === SectionName.CONTENT && activeItemIndex === index;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<td
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleItemClick(index);
|
|
||||||
}}
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
width: `${100 / data.content.columnsCount}%`,
|
|
||||||
height: '246px',
|
|
||||||
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
|
|
||||||
backgroundColor: item?.backgroundColor || '#ffffff',
|
|
||||||
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
|
|
||||||
backgroundSize: item?.style?.backgroundSize || 'cover',
|
|
||||||
backgroundRepeat: item?.style?.backgroundRepeat || 'no-repeat',
|
|
||||||
backgroundPosition: item?.style?.backgroundPosition || 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '0',
|
|
||||||
verticalAlign: 'top',
|
|
||||||
borderRadius: '8px',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{
|
|
||||||
borderCollapse: 'collapse',
|
|
||||||
height: '246px'
|
|
||||||
}}>
|
|
||||||
<tbody>
|
|
||||||
{/* اگر فقط دکمه داریم و متن نداریم */}
|
|
||||||
{(!item?.texts || item.texts.length === 0) && item?.buttons && item.buttons.length > 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
width="100%"
|
|
||||||
height="246"
|
|
||||||
align={getButtonHorizontalAlign(item.buttons[0])}
|
|
||||||
valign={getButtonVerticalAlign(item.buttons[0])}
|
|
||||||
style={{
|
|
||||||
padding: '8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ButtonRenderer buttons={item.buttons} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* ردیف اصلی برای متن */}
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
width="100%"
|
|
||||||
height={item?.buttons && item.buttons.length > 0 ? "203" : "230"}
|
|
||||||
align={item?.texts?.[0]?.alignment || 'center'}
|
|
||||||
valign={item?.texts?.[0]?.verticalAlignment === VerticalAlignment.TOP ? 'top' :
|
|
||||||
item?.texts?.[0]?.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'middle'}
|
|
||||||
style={{
|
|
||||||
padding: '8px',
|
|
||||||
fontSize: '14px',
|
|
||||||
lineHeight: '1.4'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextRenderer texts={item?.texts || []} />
|
|
||||||
<ImageRenderer images={item?.images || []} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/* ردیف دکمهها */}
|
|
||||||
{item?.buttons && item.buttons.length > 0 && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
width="100%"
|
|
||||||
height="27"
|
|
||||||
align={getButtonHorizontalAlign(item.buttons[0])}
|
|
||||||
valign={getButtonVerticalAlign(item.buttons[0])}
|
|
||||||
style={{
|
|
||||||
padding: '0 8px 8px 8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ButtonRenderer buttons={item.buttons} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</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 === SectionName.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
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import { usePersonalityStore } from '../store/Store'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { AddCircle } from 'iconsax-react'
|
|
||||||
import TextRenderer from './TextRenderer'
|
|
||||||
import ButtonRenderer from './ButtonRenderer'
|
|
||||||
import ImageRenderer from './ImageRenderer'
|
|
||||||
import { SectionName, VerticalAlignment, HorizontalAlignment } from '../types/Types'
|
|
||||||
|
|
||||||
const FooterSection: FC = () => {
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
|
|
||||||
|
|
||||||
const handleSectionClick = (section: SectionName) => {
|
|
||||||
setActiveSection(section)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleItemClick = (index: number) => {
|
|
||||||
setActiveItemIndex(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// تابعهای helper برای تعیین alignment دکمهها
|
|
||||||
const getButtonHorizontalAlign = (button?: { alignment?: HorizontalAlignment }) => {
|
|
||||||
if (!button || !button.alignment) return 'center'
|
|
||||||
switch (button.alignment) {
|
|
||||||
case HorizontalAlignment.LEFT:
|
|
||||||
return 'left'
|
|
||||||
case HorizontalAlignment.RIGHT:
|
|
||||||
return 'right'
|
|
||||||
case HorizontalAlignment.CENTER:
|
|
||||||
default:
|
|
||||||
return 'center'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getButtonVerticalAlign = (button?: { verticalAlignment?: VerticalAlignment }) => {
|
|
||||||
if (!button || !button.verticalAlignment) return 'middle'
|
|
||||||
switch (button.verticalAlignment) {
|
|
||||||
case VerticalAlignment.TOP:
|
|
||||||
return 'top'
|
|
||||||
case VerticalAlignment.BOTTOM:
|
|
||||||
return 'bottom'
|
|
||||||
case VerticalAlignment.MIDDLE:
|
|
||||||
default:
|
|
||||||
return 'middle'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr onClick={() => handleSectionClick(SectionName.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 === SectionName.FOOTER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
|
||||||
borderRadius: '24px'
|
|
||||||
}}>
|
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ tableLayout: 'fixed' }}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
{
|
|
||||||
Array.from({ length: data.footer.columnsCount }, (_, index) => {
|
|
||||||
const item = data.footer.items[index];
|
|
||||||
const isActive = activeSection === SectionName.FOOTER && activeItemIndex === index;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<td
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleItemClick(index);
|
|
||||||
}}
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
width: `${100 / data.footer.columnsCount}%`,
|
|
||||||
height: '123px',
|
|
||||||
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
|
|
||||||
backgroundColor: item?.backgroundColor || '#ffffff',
|
|
||||||
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
|
|
||||||
backgroundSize: item?.style?.backgroundSize || 'cover',
|
|
||||||
backgroundRepeat: item?.style?.backgroundRepeat || 'no-repeat',
|
|
||||||
backgroundPosition: item?.style?.backgroundPosition || 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '0',
|
|
||||||
verticalAlign: 'top',
|
|
||||||
borderRadius: '8px',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{
|
|
||||||
borderCollapse: 'collapse',
|
|
||||||
height: '123px'
|
|
||||||
}}>
|
|
||||||
<tbody>
|
|
||||||
{/* اگر فقط دکمه داریم و متن نداریم */}
|
|
||||||
{(!item?.texts || item.texts.length === 0) && item?.buttons && item.buttons.length > 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
width="100%"
|
|
||||||
height="123"
|
|
||||||
align={getButtonHorizontalAlign(item.buttons[0])}
|
|
||||||
valign={getButtonVerticalAlign(item.buttons[0])}
|
|
||||||
style={{
|
|
||||||
padding: '8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ButtonRenderer buttons={item.buttons} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* ردیف اصلی برای متن */}
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
width="100%"
|
|
||||||
height={item?.buttons && item.buttons.length > 0 ? "40" : "67"}
|
|
||||||
align={item?.texts?.[0]?.alignment || 'center'}
|
|
||||||
valign={item?.texts?.[0]?.verticalAlignment === VerticalAlignment.TOP ? 'top' :
|
|
||||||
item?.texts?.[0]?.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'middle'}
|
|
||||||
style={{
|
|
||||||
padding: '8px',
|
|
||||||
fontSize: '14px',
|
|
||||||
lineHeight: '1.4'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextRenderer texts={item?.texts || []} />
|
|
||||||
<ImageRenderer images={item?.images || []} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/* ردیف دکمهها */}
|
|
||||||
{item?.buttons && item.buttons.length > 0 && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
width="100%"
|
|
||||||
height="27"
|
|
||||||
align={getButtonHorizontalAlign(item.buttons[0])}
|
|
||||||
valign={getButtonVerticalAlign(item.buttons[0])}
|
|
||||||
style={{
|
|
||||||
padding: '0 8px 8px 8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ButtonRenderer buttons={item.buttons} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</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 === SectionName.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
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import { usePersonalityStore } from '../store/Store'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { AddCircle } from 'iconsax-react'
|
|
||||||
import TextRenderer from './TextRenderer'
|
|
||||||
import ButtonRenderer from './ButtonRenderer'
|
|
||||||
import ImageRenderer from './ImageRenderer'
|
|
||||||
import { SectionName, VerticalAlignment, HorizontalAlignment } from '../types/Types'
|
|
||||||
|
|
||||||
const HeaderSection: FC = () => {
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
|
|
||||||
|
|
||||||
const handleSectionClick = (section: SectionName) => {
|
|
||||||
setActiveSection(section)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleItemClick = (index: number) => {
|
|
||||||
setActiveItemIndex(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// تابعهای helper برای تعیین alignment دکمهها
|
|
||||||
const getButtonHorizontalAlign = (button?: { alignment?: HorizontalAlignment }) => {
|
|
||||||
if (!button || !button.alignment) return 'center'
|
|
||||||
switch (button.alignment) {
|
|
||||||
case HorizontalAlignment.LEFT:
|
|
||||||
return 'left'
|
|
||||||
case HorizontalAlignment.RIGHT:
|
|
||||||
return 'right'
|
|
||||||
case HorizontalAlignment.CENTER:
|
|
||||||
default:
|
|
||||||
return 'center'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getButtonVerticalAlign = (button?: { verticalAlignment?: VerticalAlignment }) => {
|
|
||||||
if (!button || !button.verticalAlignment) return 'middle'
|
|
||||||
switch (button.verticalAlignment) {
|
|
||||||
case VerticalAlignment.TOP:
|
|
||||||
return 'top'
|
|
||||||
case VerticalAlignment.BOTTOM:
|
|
||||||
return 'bottom'
|
|
||||||
case VerticalAlignment.MIDDLE:
|
|
||||||
default:
|
|
||||||
return 'middle'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr onClick={() => handleSectionClick(SectionName.HEADER)}>
|
|
||||||
{
|
|
||||||
data.header.columnsCount > 0 ?
|
|
||||||
<td style={{
|
|
||||||
padding: '10px',
|
|
||||||
border: activeSection === SectionName.HEADER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
|
|
||||||
borderRadius: '24px'
|
|
||||||
}}>
|
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ tableLayout: 'fixed' }}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
{
|
|
||||||
Array.from({ length: data.header.columnsCount }, (_, index) => {
|
|
||||||
const item = data.header.items[index];
|
|
||||||
const isActive = activeSection === SectionName.HEADER && activeItemIndex === index;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<td
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleItemClick(index);
|
|
||||||
}}
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
width: `${100 / data.header.columnsCount}%`,
|
|
||||||
height: '123px',
|
|
||||||
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
|
|
||||||
backgroundColor: item?.backgroundColor || '#ffffff',
|
|
||||||
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
|
|
||||||
backgroundSize: item?.style?.backgroundSize || 'cover',
|
|
||||||
backgroundRepeat: item?.style?.backgroundRepeat || 'no-repeat',
|
|
||||||
backgroundPosition: item?.style?.backgroundPosition || 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '0',
|
|
||||||
verticalAlign: 'top',
|
|
||||||
borderRadius: '8px',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{
|
|
||||||
borderCollapse: 'collapse',
|
|
||||||
height: '123px'
|
|
||||||
}}>
|
|
||||||
<tbody>
|
|
||||||
{/* اگر فقط دکمه داریم و متن نداریم */}
|
|
||||||
{(!item?.texts || item.texts.length === 0) && item?.buttons && item.buttons.length > 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
width="100%"
|
|
||||||
height="123"
|
|
||||||
align={getButtonHorizontalAlign(item.buttons[0])}
|
|
||||||
valign={getButtonVerticalAlign(item.buttons[0])}
|
|
||||||
style={{
|
|
||||||
padding: '8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ButtonRenderer buttons={item.buttons} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* ردیف اصلی برای متن */}
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
width="100%"
|
|
||||||
height={item?.buttons && item.buttons.length > 0 ? "60" : "87"}
|
|
||||||
align={item?.texts?.[0]?.alignment || 'center'}
|
|
||||||
valign={item?.texts?.[0]?.verticalAlignment === VerticalAlignment.TOP ? 'top' :
|
|
||||||
item?.texts?.[0]?.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'middle'}
|
|
||||||
style={{
|
|
||||||
padding: '8px',
|
|
||||||
fontSize: '14px',
|
|
||||||
lineHeight: '1.4'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextRenderer texts={item?.texts || []} />
|
|
||||||
<ImageRenderer images={item?.images || []} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/* ردیف دکمهها */}
|
|
||||||
{item?.buttons && item.buttons.length > 0 && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
width="100%"
|
|
||||||
height="27"
|
|
||||||
align={getButtonHorizontalAlign(item.buttons[0])}
|
|
||||||
valign={getButtonVerticalAlign(item.buttons[0])}
|
|
||||||
style={{
|
|
||||||
padding: '0 8px 8px 8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ButtonRenderer buttons={item.buttons} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
{index < data.header.columnsCount - 1 && (
|
|
||||||
<td style={{ width: '16px' }}></td>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
:
|
|
||||||
<td style={{
|
|
||||||
height: '123px',
|
|
||||||
border: activeSection === SectionName.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
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import { ImageType, ImageSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
|
|
||||||
|
|
||||||
interface ImageRendererProps {
|
|
||||||
images: ImageType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImageRenderer: FC<ImageRendererProps> = ({ images }) => {
|
|
||||||
if (!images || images.length === 0) return null
|
|
||||||
|
|
||||||
// تابعهای helper برای تبدیل enum به string
|
|
||||||
const getHorizontalAlign = (alignment?: HorizontalAlignment) => {
|
|
||||||
switch (alignment) {
|
|
||||||
case HorizontalAlignment.LEFT:
|
|
||||||
return 'left'
|
|
||||||
case HorizontalAlignment.RIGHT:
|
|
||||||
return 'right'
|
|
||||||
case HorizontalAlignment.CENTER:
|
|
||||||
default:
|
|
||||||
return 'center'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getVerticalAlign = (verticalAlignment?: VerticalAlignment) => {
|
|
||||||
switch (verticalAlignment) {
|
|
||||||
case VerticalAlignment.TOP:
|
|
||||||
return 'top'
|
|
||||||
case VerticalAlignment.BOTTOM:
|
|
||||||
return 'bottom'
|
|
||||||
case VerticalAlignment.MIDDLE:
|
|
||||||
default:
|
|
||||||
return 'middle'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImageStyle = (image: ImageType) => {
|
|
||||||
return {
|
|
||||||
width: image.width || '100%',
|
|
||||||
height: image.height || 'auto',
|
|
||||||
border: '0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldUseBackgroundImage = (imageSize: ImageSize) => {
|
|
||||||
return imageSize === ImageSize.REPEAT ||
|
|
||||||
imageSize === ImageSize.REPEAT_X ||
|
|
||||||
imageSize === ImageSize.REPEAT_Y
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBackgroundStyle = (image: ImageType) => {
|
|
||||||
let backgroundRepeat = 'no-repeat'
|
|
||||||
|
|
||||||
switch (image.size) {
|
|
||||||
case ImageSize.REPEAT:
|
|
||||||
backgroundRepeat = 'repeat'
|
|
||||||
break
|
|
||||||
case ImageSize.REPEAT_X:
|
|
||||||
backgroundRepeat = 'repeat-x'
|
|
||||||
break
|
|
||||||
case ImageSize.REPEAT_Y:
|
|
||||||
backgroundRepeat = 'repeat-y'
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
backgroundRepeat = 'no-repeat'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const horizontal = image.alignment === HorizontalAlignment.LEFT ? 'left' :
|
|
||||||
image.alignment === HorizontalAlignment.RIGHT ? 'right' : 'center'
|
|
||||||
const vertical = image.verticalAlignment === VerticalAlignment.TOP ? 'top' :
|
|
||||||
image.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'center'
|
|
||||||
|
|
||||||
return {
|
|
||||||
backgroundImage: `url(${image.src})`,
|
|
||||||
backgroundPosition: `${horizontal} ${vertical}`,
|
|
||||||
backgroundRepeat,
|
|
||||||
width: image.width || '100%',
|
|
||||||
height: image.height || '100px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderImage = (image: ImageType) => {
|
|
||||||
if (shouldUseBackgroundImage(image.size)) {
|
|
||||||
// برای pattern های repeat از background استفاده میکنیم
|
|
||||||
return (
|
|
||||||
<div style={getBackgroundStyle(image)}>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// برای تصاویر عادی
|
|
||||||
if (image.size === ImageSize.CONTAIN) {
|
|
||||||
// برای contain از table wrapper استفاده میکنیم تا نسبت حفظ شود
|
|
||||||
return (
|
|
||||||
<table width={image.width || '100%'} cellPadding="0" cellSpacing="0" border={0}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center" valign="middle" style={{ textAlign: 'center' }}>
|
|
||||||
<img
|
|
||||||
src={image.src}
|
|
||||||
alt={image.alt || 'تصویر آپلود شده'}
|
|
||||||
width={image.width || '100%'}
|
|
||||||
height={image.height || 'auto'}
|
|
||||||
style={{ border: '0' }}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// برای cover و auto
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={image.src}
|
|
||||||
alt={image.alt || 'تصویر آپلود شده'}
|
|
||||||
width={image.width || '100%'}
|
|
||||||
height={image.height || 'auto'}
|
|
||||||
style={getImageStyle(image)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{images.map((image, imageIndex) => (
|
|
||||||
<table key={image.id} width="100%" cellPadding="0" cellSpacing="0" border={0}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
align={getHorizontalAlign(image.alignment)}
|
|
||||||
valign={getVerticalAlign(image.verticalAlignment)}
|
|
||||||
style={{
|
|
||||||
paddingBottom: imageIndex < images.length - 1 ? '4px' : '0'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderImage(image)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ImageRenderer
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
import Button from '@/components/Button'
|
|
||||||
import Select from '@/components/Select'
|
|
||||||
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
|
||||||
import { Add, AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, AlignVertically } from 'iconsax-react'
|
|
||||||
import { FC, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { usePersonalityStore } from '../store/Store'
|
|
||||||
import { ImageSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
|
|
||||||
|
|
||||||
const ImageSideBar: FC = () => {
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { addImageToActiveItem, activeSection, activeItemIndex, data } = usePersonalityStore()
|
|
||||||
|
|
||||||
const [imageSrc, setImageSrc] = useState<string>('')
|
|
||||||
const [imageSize, setImageSize] = useState<ImageSize>(ImageSize.COVER)
|
|
||||||
const [horizontalAlignment, setHorizontalAlignment] = useState<HorizontalAlignment>(HorizontalAlignment.CENTER)
|
|
||||||
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
|
||||||
const [isReset, setIsReset] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const sizeOptions = [
|
|
||||||
{ label: 'کاور - تمام فضا را پوشش دهد (Cover)', value: ImageSize.COVER },
|
|
||||||
{ label: 'در مقیاس - در فضا جا شود (Contain)', value: ImageSize.CONTAIN },
|
|
||||||
{ label: 'تکرار - عکس تکرار شود (Repeat)', value: ImageSize.REPEAT },
|
|
||||||
{ label: 'تکرار افقی (Repeat-X)', value: ImageSize.REPEAT_X },
|
|
||||||
{ label: 'تکرار عمودی (Repeat-Y)', value: ImageSize.REPEAT_Y },
|
|
||||||
{ label: 'بدون تکرار (No-Repeat)', value: ImageSize.NO_REPEAT },
|
|
||||||
{ label: 'اندازه اصلی (Auto)', value: ImageSize.AUTO }
|
|
||||||
]
|
|
||||||
|
|
||||||
const handleImageUpload = (files: File[]) => {
|
|
||||||
if (files.length === 0) return
|
|
||||||
|
|
||||||
const file = files[0] // فقط اولین فایل را در نظر میگیریم
|
|
||||||
// در اینجا باید فایل آپلود شود و URL برگردانده شود
|
|
||||||
// فعلاً از FileReader استفاده میکنم برای تست
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const result = e.target?.result as string
|
|
||||||
setImageSrc(result)
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddImage = () => {
|
|
||||||
if (!imageSrc) return
|
|
||||||
|
|
||||||
console.log('Store state before adding image:', {
|
|
||||||
activeSection,
|
|
||||||
activeItemIndex,
|
|
||||||
hasActiveItem: activeSection && data[activeSection as keyof typeof data]?.items[activeItemIndex],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!activeSection) {
|
|
||||||
console.error('No active section selected')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const sectionData = data[activeSection as keyof typeof data]
|
|
||||||
if (!sectionData?.items || activeItemIndex >= sectionData.items.length) {
|
|
||||||
console.error('No active item found or invalid index', {
|
|
||||||
activeItemIndex,
|
|
||||||
itemsLength: sectionData?.items?.length
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Adding image with data:', {
|
|
||||||
src: imageSrc,
|
|
||||||
alt: 'عکس آپلود شده',
|
|
||||||
size: imageSize,
|
|
||||||
alignment: horizontalAlignment,
|
|
||||||
verticalAlignment,
|
|
||||||
})
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
addImageToActiveItem({
|
|
||||||
src: imageSrc,
|
|
||||||
alt: 'عکس آپلود شده',
|
|
||||||
size: imageSize,
|
|
||||||
alignment: horizontalAlignment,
|
|
||||||
verticalAlignment,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('Image added successfully')
|
|
||||||
|
|
||||||
// Reset form after adding
|
|
||||||
setImageSrc('')
|
|
||||||
setImageSize(ImageSize.COVER)
|
|
||||||
setHorizontalAlignment(HorizontalAlignment.CENTER)
|
|
||||||
setVerticalAlignment(VerticalAlignment.MIDDLE)
|
|
||||||
setIsReset(true)
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsReset(false)
|
|
||||||
}, 1000)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding image:', error)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className='text-lg'>{t('setting.image')}</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
|
||||||
<UploadBoxDraggble
|
|
||||||
label={t('setting.upload_image')}
|
|
||||||
onChange={handleImageUpload}
|
|
||||||
isReset={isReset}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<Select
|
|
||||||
items={sizeOptions}
|
|
||||||
label={t('setting.size')}
|
|
||||||
value={imageSize}
|
|
||||||
onChange={(e) => setImageSize(e.target.value as ImageSize)}
|
|
||||||
placeholder={t('setting.size')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5 flex justify-between'>
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('setting.horizontal_position')}
|
|
||||||
</div>
|
|
||||||
<div className='mt-3 flex gap-4'>
|
|
||||||
<div
|
|
||||||
onClick={() => setHorizontalAlignment(HorizontalAlignment.RIGHT)}
|
|
||||||
>
|
|
||||||
<AlignRight size={20} color={horizontalAlignment === HorizontalAlignment.RIGHT ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setHorizontalAlignment(HorizontalAlignment.CENTER)}
|
|
||||||
>
|
|
||||||
<AlignVertically size={20} color={horizontalAlignment === HorizontalAlignment.CENTER ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setHorizontalAlignment(HorizontalAlignment.LEFT)}
|
|
||||||
>
|
|
||||||
<AlignLeft size={20} color={horizontalAlignment === HorizontalAlignment.LEFT ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('setting.vertical_position')}
|
|
||||||
</div>
|
|
||||||
<div className='mt-3 flex gap-4'>
|
|
||||||
<div
|
|
||||||
onClick={() => setVerticalAlignment(VerticalAlignment.BOTTOM)}
|
|
||||||
>
|
|
||||||
<AlignBottom size={20} color={verticalAlignment === VerticalAlignment.BOTTOM ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setVerticalAlignment(VerticalAlignment.MIDDLE)}
|
|
||||||
>
|
|
||||||
<AlignHorizontally size={20} color={verticalAlignment === VerticalAlignment.MIDDLE ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setVerticalAlignment(VerticalAlignment.TOP)}
|
|
||||||
>
|
|
||||||
<AlignTop size={20} color={verticalAlignment === VerticalAlignment.TOP ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-7'>
|
|
||||||
<Button
|
|
||||||
onClick={handleAddImage}
|
|
||||||
className='bg-[#ECEEF5] text-black'
|
|
||||||
disabled={!imageSrc}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
<div className='flex justify-center items-center gap-2'>
|
|
||||||
<Add size={24} color='black' />
|
|
||||||
<span>{t('setting.add_image')}</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ImageSideBar
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
import { FC, useState, useEffect } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import UploadBoxDraggble from '@/components/UploadBoxDraggble';
|
|
||||||
import ColorPicker from '@/components/ColorPicker';
|
|
||||||
import { usePersonalityStore } from '../store/Store';
|
|
||||||
import { SectionItemType, BackgroundSize, SectionName } from '../types/Types';
|
|
||||||
import Select from '@/components/Select';
|
|
||||||
|
|
||||||
const SettingSideBar: FC = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { activeSection, setColumnsCount, updateActiveItem, data, activeItemIndex } = usePersonalityStore()
|
|
||||||
const [color, setColor] = useState("#aabbcc");
|
|
||||||
const [backgroundSize, setBackgroundSize] = useState<BackgroundSize>(BackgroundSize.COVER)
|
|
||||||
|
|
||||||
// تشخیص background size بر اساس style موجود
|
|
||||||
const getCurrentBackgroundSizeType = (style?: SectionItemType['style']) => {
|
|
||||||
if (!style) return BackgroundSize.COVER
|
|
||||||
|
|
||||||
const bgSize = style.backgroundSize
|
|
||||||
const bgRepeat = style.backgroundRepeat
|
|
||||||
|
|
||||||
if (bgSize === 'cover') return BackgroundSize.COVER
|
|
||||||
if (bgSize === 'contain') return BackgroundSize.CONTAIN
|
|
||||||
if (bgSize === 'auto' || !bgSize) {
|
|
||||||
if (bgRepeat === 'round') return BackgroundSize.ROUND
|
|
||||||
}
|
|
||||||
return BackgroundSize.COVER
|
|
||||||
}
|
|
||||||
|
|
||||||
// بهروزرسانی state وقتی activeSection یا activeItem تغییر میکند
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeSection && data[activeSection as keyof typeof data]) {
|
|
||||||
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
|
|
||||||
if (currentItem?.style) {
|
|
||||||
const currentType = getCurrentBackgroundSizeType(currentItem.style)
|
|
||||||
setBackgroundSize(currentType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [activeSection, activeItemIndex, data])
|
|
||||||
|
|
||||||
const handleColumnClick = (columns: number) => {
|
|
||||||
if (activeSection) {
|
|
||||||
setColumnsCount(activeSection as SectionName, 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)
|
|
||||||
|
|
||||||
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
|
|
||||||
updateActiveItem({
|
|
||||||
backgroundImage: imageUrl,
|
|
||||||
style: {
|
|
||||||
...currentItem?.style,
|
|
||||||
backgroundSize: getBackgroundSize(backgroundSize),
|
|
||||||
backgroundRepeat: getBackgroundRepeat(backgroundSize),
|
|
||||||
backgroundPosition: 'center center'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBackgroundRepeat = (size: BackgroundSize) => {
|
|
||||||
switch (size) {
|
|
||||||
case BackgroundSize.ROUND:
|
|
||||||
return 'round'
|
|
||||||
case BackgroundSize.COVER:
|
|
||||||
case BackgroundSize.CONTAIN:
|
|
||||||
default:
|
|
||||||
return 'no-repeat'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBackgroundSize = (size: BackgroundSize) => {
|
|
||||||
switch (size) {
|
|
||||||
case BackgroundSize.COVER:
|
|
||||||
return 'cover'
|
|
||||||
case BackgroundSize.CONTAIN:
|
|
||||||
return 'contain'
|
|
||||||
case BackgroundSize.ROUND:
|
|
||||||
return 'auto'
|
|
||||||
default:
|
|
||||||
return 'cover'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeOptions = [
|
|
||||||
{ label: 'کاور - تمام فضا را پوشش دهد (Cover)', value: BackgroundSize.COVER },
|
|
||||||
{ label: 'در مقیاس - در فضا جا شود (Contain)', value: BackgroundSize.CONTAIN },
|
|
||||||
{ label: 'گرد - عکس به صورت گرد تکرار شود (Round)', value: BackgroundSize.ROUND }
|
|
||||||
]
|
|
||||||
|
|
||||||
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] 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 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 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>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<ColorPicker
|
|
||||||
label={t('setting.background')}
|
|
||||||
defaultColor={color}
|
|
||||||
changeColor={handleColorChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<UploadBoxDraggble
|
|
||||||
label={t('setting.upload_background')}
|
|
||||||
onChange={handleUploadBackground}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<Select
|
|
||||||
items={sizeOptions}
|
|
||||||
label={t('setting.size')}
|
|
||||||
value={backgroundSize}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newSize = e.target.value as BackgroundSize
|
|
||||||
setBackgroundSize(newSize)
|
|
||||||
|
|
||||||
const currentItem = activeSection ? data[activeSection as keyof typeof data]?.items[activeItemIndex] : null
|
|
||||||
updateActiveItem({
|
|
||||||
style: {
|
|
||||||
...currentItem?.style,
|
|
||||||
backgroundSize: getBackgroundSize(newSize),
|
|
||||||
backgroundRepeat: getBackgroundRepeat(newSize),
|
|
||||||
backgroundPosition: 'center center'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
placeholder={t('setting.size')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SettingSideBar
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import { TextType } from '../types/Types'
|
|
||||||
|
|
||||||
interface TextRendererProps {
|
|
||||||
texts: TextType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const TextRenderer: FC<TextRendererProps> = ({ texts }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{texts?.map((textItem, textIndex) => (
|
|
||||||
<table key={textItem.id} width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
align={textItem.alignment || 'center'}
|
|
||||||
style={{
|
|
||||||
color: textItem.color || '#000000',
|
|
||||||
fontSize: `${textItem.fontSize || 14}px`,
|
|
||||||
lineHeight: '1.4',
|
|
||||||
paddingBottom: textIndex < (texts?.length || 0) - 1 ? '4px' : '0'
|
|
||||||
}}
|
|
||||||
dangerouslySetInnerHTML={{ __html: textItem.text }}
|
|
||||||
/>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TextRenderer
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import Button from '@/components/Button';
|
|
||||||
import { Add, AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, AlignVertically } from 'iconsax-react';
|
|
||||||
import { FC, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import ReactQuill from 'react-quill-new';
|
|
||||||
import { usePersonalityStore } from '../store/Store';
|
|
||||||
import ColorPicker from '@/components/ColorPicker';
|
|
||||||
import { HorizontalAlignment, VerticalAlignment } from '../types/Types';
|
|
||||||
|
|
||||||
const TextSidebar: FC = () => {
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [value, setValue] = useState('');
|
|
||||||
const [color, setColor] = useState('#000');
|
|
||||||
const [horizontalPosition, setHorizontalPosition] = useState<HorizontalAlignment>(HorizontalAlignment.CENTER);
|
|
||||||
const [verticalPosition, setVerticalPosition] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE);
|
|
||||||
const { addTextToActiveItem } = usePersonalityStore();
|
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
|
||||||
setValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddText = () => {
|
|
||||||
if (value.trim()) {
|
|
||||||
addTextToActiveItem({
|
|
||||||
text: value,
|
|
||||||
color: color,
|
|
||||||
verticalAlignment: verticalPosition,
|
|
||||||
alignment: horizontalPosition,
|
|
||||||
});
|
|
||||||
|
|
||||||
// خالی کردن فیلدها بعد از ذخیره
|
|
||||||
setValue('');
|
|
||||||
setColor('#000');
|
|
||||||
setHorizontalPosition(HorizontalAlignment.CENTER);
|
|
||||||
setVerticalPosition(VerticalAlignment.MIDDLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className='text-lg'>{t('setting.text')}</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
|
||||||
<ReactQuill
|
|
||||||
modules={{
|
|
||||||
toolbar: [
|
|
||||||
[{ header: '1' }, { header: '2' }, { font: [] }],
|
|
||||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
||||||
['bold', 'italic', 'underline'],
|
|
||||||
['link', 'image'],
|
|
||||||
[{ align: [] }],
|
|
||||||
['clean']
|
|
||||||
]
|
|
||||||
}}
|
|
||||||
theme="snow"
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
style={{ minHeight: '120px' }}
|
|
||||||
className='text-sm'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
|
||||||
<ColorPicker
|
|
||||||
changeColor={setColor}
|
|
||||||
label={t('setting.text_color')}
|
|
||||||
defaultColor={color}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5 flex justify-between'>
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('setting.horizontal_position')}
|
|
||||||
</div>
|
|
||||||
<div className='mt-3 flex gap-4'>
|
|
||||||
<div
|
|
||||||
onClick={() => setHorizontalPosition(HorizontalAlignment.RIGHT)}
|
|
||||||
>
|
|
||||||
<AlignRight size={20} color={horizontalPosition === HorizontalAlignment.RIGHT ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setHorizontalPosition(HorizontalAlignment.CENTER)}
|
|
||||||
>
|
|
||||||
<AlignVertically size={20} color={horizontalPosition === HorizontalAlignment.CENTER ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setHorizontalPosition(HorizontalAlignment.LEFT)}
|
|
||||||
>
|
|
||||||
<AlignLeft size={20} color={horizontalPosition === HorizontalAlignment.LEFT ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('setting.vertical_position')}
|
|
||||||
</div>
|
|
||||||
<div className='mt-3 flex gap-4'>
|
|
||||||
<div
|
|
||||||
onClick={() => setVerticalPosition(VerticalAlignment.TOP)}
|
|
||||||
>
|
|
||||||
<AlignTop size={20} color={verticalPosition === VerticalAlignment.TOP ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setVerticalPosition(VerticalAlignment.MIDDLE)}
|
|
||||||
>
|
|
||||||
<AlignHorizontally size={20} color={verticalPosition === VerticalAlignment.MIDDLE ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setVerticalPosition(VerticalAlignment.BOTTOM)}
|
|
||||||
>
|
|
||||||
<AlignBottom size={20} color={verticalPosition === VerticalAlignment.BOTTOM ? '#0038FF' : '#000'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-7'>
|
|
||||||
<Button
|
|
||||||
onClick={handleAddText}
|
|
||||||
className='bg-[#ECEEF5] text-black'
|
|
||||||
>
|
|
||||||
<div className='flex justify-center items-center gap-2'>
|
|
||||||
<Add size={24} color='black' />
|
|
||||||
<span>{t('setting.add_text')}</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TextSidebar
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
import { create } from "zustand";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import {
|
|
||||||
PersonalityStore,
|
|
||||||
PersonalityDataType,
|
|
||||||
SectionItemType,
|
|
||||||
TextType,
|
|
||||||
ButtonType,
|
|
||||||
ImageType,
|
|
||||||
SectionName,
|
|
||||||
HorizontalAlignment,
|
|
||||||
VerticalAlignment,
|
|
||||||
} 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: SectionName) =>
|
|
||||||
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: [],
|
|
||||||
images: [],
|
|
||||||
alignment: HorizontalAlignment.CENTER,
|
|
||||||
verticalAlignment: VerticalAlignment.MIDDLE,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
|
|
||||||
addImage: (
|
|
||||||
sectionKey: keyof PersonalityDataType,
|
|
||||||
itemId: string,
|
|
||||||
image: Omit<ImageType, "id">
|
|
||||||
) =>
|
|
||||||
set((state) => ({
|
|
||||||
data: {
|
|
||||||
...state.data,
|
|
||||||
[sectionKey]: {
|
|
||||||
...state.data[sectionKey],
|
|
||||||
items: state.data[sectionKey].items.map((item) =>
|
|
||||||
item.id === itemId
|
|
||||||
? {
|
|
||||||
...item,
|
|
||||||
images: [...item.images, { ...image, id: uuidv4() }],
|
|
||||||
}
|
|
||||||
: item
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
|
|
||||||
addImageToActiveItem: (image: Omit<ImageType, "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,
|
|
||||||
images: [...item.images, { ...image, id: uuidv4() }],
|
|
||||||
}
|
|
||||||
: item
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
...state.data,
|
|
||||||
[sectionKey]: {
|
|
||||||
...state.data[sectionKey],
|
|
||||||
items: updatedItems,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateImage: (
|
|
||||||
sectionKey: keyof PersonalityDataType,
|
|
||||||
itemId: string,
|
|
||||||
imageId: string,
|
|
||||||
updates: Partial<ImageType>
|
|
||||||
) =>
|
|
||||||
set((state) => ({
|
|
||||||
data: {
|
|
||||||
...state.data,
|
|
||||||
[sectionKey]: {
|
|
||||||
...state.data[sectionKey],
|
|
||||||
items: state.data[sectionKey].items.map((item) =>
|
|
||||||
item.id === itemId
|
|
||||||
? {
|
|
||||||
...item,
|
|
||||||
images: item.images.map((image) =>
|
|
||||||
image.id === imageId ? { ...image, ...updates } : image
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: item
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
// Enums for common types
|
|
||||||
export enum HorizontalAlignment {
|
|
||||||
LEFT = "left",
|
|
||||||
CENTER = "center",
|
|
||||||
RIGHT = "right",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum VerticalAlignment {
|
|
||||||
TOP = "top",
|
|
||||||
MIDDLE = "middle",
|
|
||||||
BOTTOM = "bottom",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ButtonSize {
|
|
||||||
SMALL = "small",
|
|
||||||
MEDIUM = "medium",
|
|
||||||
LARGE = "large",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ImageSize {
|
|
||||||
COVER = "cover",
|
|
||||||
CONTAIN = "contain",
|
|
||||||
AUTO = "auto",
|
|
||||||
REPEAT = "repeat",
|
|
||||||
REPEAT_X = "repeat-x",
|
|
||||||
REPEAT_Y = "repeat-y",
|
|
||||||
NO_REPEAT = "no-repeat",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum BackgroundSize {
|
|
||||||
COVER = "cover",
|
|
||||||
CONTAIN = "contain",
|
|
||||||
ROUND = "round",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SectionName {
|
|
||||||
HEADER = "header",
|
|
||||||
CONTENT = "content",
|
|
||||||
FOOTER = "footer",
|
|
||||||
}
|
|
||||||
|
|
||||||
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[];
|
|
||||||
images: ImageType[];
|
|
||||||
alignment?: HorizontalAlignment;
|
|
||||||
verticalAlignment?: VerticalAlignment;
|
|
||||||
style?: {
|
|
||||||
borderRadius?: number;
|
|
||||||
padding?: string;
|
|
||||||
fontFamily?: string;
|
|
||||||
backgroundSize?: string;
|
|
||||||
backgroundRepeat?: string;
|
|
||||||
backgroundPosition?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TextType = {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
fontSize?: number;
|
|
||||||
fontFamily?: string;
|
|
||||||
color?: string;
|
|
||||||
alignment?: HorizontalAlignment;
|
|
||||||
verticalAlignment?: VerticalAlignment;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ButtonType = {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
link: string;
|
|
||||||
size: ButtonSize;
|
|
||||||
isBorder: boolean;
|
|
||||||
borderSize: number;
|
|
||||||
textColor: string;
|
|
||||||
backgroundColor: string;
|
|
||||||
borderColor: string;
|
|
||||||
alignment?: HorizontalAlignment;
|
|
||||||
verticalAlignment?: VerticalAlignment;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ImageType = {
|
|
||||||
id: string;
|
|
||||||
src: string;
|
|
||||||
alt?: string;
|
|
||||||
size: ImageSize;
|
|
||||||
width?: string;
|
|
||||||
height?: string;
|
|
||||||
alignment?: HorizontalAlignment;
|
|
||||||
verticalAlignment?: VerticalAlignment;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PersonalityStore = {
|
|
||||||
data: PersonalityDataType;
|
|
||||||
activeSection: SectionName | "";
|
|
||||||
activeItemIndex: number;
|
|
||||||
|
|
||||||
setActiveSection: (section: SectionName) => 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;
|
|
||||||
addImage: (
|
|
||||||
sectionKey: keyof PersonalityDataType,
|
|
||||||
itemId: string,
|
|
||||||
image: Omit<ImageType, "id">
|
|
||||||
) => void;
|
|
||||||
addImageToActiveItem: (image: Omit<ImageType, "id">) => void;
|
|
||||||
updateImage: (
|
|
||||||
sectionKey: keyof PersonalityDataType,
|
|
||||||
itemId: string,
|
|
||||||
imageId: string,
|
|
||||||
updates: Partial<ImageType>
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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 xl:flex-row gap-4 flex-col justify-between pb-10'>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='xl:text-lg'>
|
|
||||||
{t('setting.signature')}
|
|
||||||
</div>
|
|
||||||
<div className='xl:text-sm text-xs text-description mt-1.5'>
|
|
||||||
{t('setting.signature_description')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex-1 mt-4 xl:mt-0'>
|
|
||||||
<UploadBox
|
|
||||||
label={t('setting.upload_sign')}
|
|
||||||
/>
|
|
||||||
<div className='xl:mt-8 mt-5'>
|
|
||||||
<Textarea
|
|
||||||
label={t('setting.description_sign')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Signture
|
|
||||||
@@ -2,24 +2,22 @@ import { FC } from 'react'
|
|||||||
import { Paths } from '@/utils/Paths'
|
import { Paths } from '@/utils/Paths'
|
||||||
import { Routes, Route } from 'react-router-dom'
|
import { Routes, Route } from 'react-router-dom'
|
||||||
import ReceivedList from '@/pages/received/List'
|
import ReceivedList from '@/pages/received/List'
|
||||||
import Setting from '@/pages/setting/Setting'
|
|
||||||
import SentList from '@/pages/sent/List'
|
import SentList from '@/pages/sent/List'
|
||||||
import DraftList from '@/pages/draft/List'
|
import DraftList from '@/pages/draft/List'
|
||||||
import ArchiveList from '@/pages/archive/List'
|
import ArchiveList from '@/pages/archive/List'
|
||||||
import TrashList from '@/pages/Trash/List'
|
import TrashList from '@/pages/Trash/List'
|
||||||
import DetailEmail from '@/pages/received/Detail'
|
import DetailEmail from '@/pages/received/Detail'
|
||||||
import Home from '@/pages/home/Home'
|
import Login from '@/pages/auth/Login'
|
||||||
const AppRouter: FC = () => {
|
const AppRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<Home />} />
|
|
||||||
<Route path={Paths.received} element={<ReceivedList />} />
|
<Route path={Paths.received} element={<ReceivedList />} />
|
||||||
<Route path={Paths.setting} element={<Setting />} />
|
|
||||||
<Route path={Paths.sent} element={<SentList />} />
|
<Route path={Paths.sent} element={<SentList />} />
|
||||||
<Route path={Paths.draft} element={<DraftList />} />
|
<Route path={Paths.draft} element={<DraftList />} />
|
||||||
<Route path={Paths.archive} element={<ArchiveList />} />
|
<Route path={Paths.archive} element={<ArchiveList />} />
|
||||||
<Route path={Paths.trash} element={<TrashList />} />
|
<Route path={Paths.trash} element={<TrashList />} />
|
||||||
<Route path={Paths.detailEmail + ':id'} element={<DetailEmail />} />
|
<Route path={Paths.detailEmail + ':id'} element={<DetailEmail />} />
|
||||||
|
<Route path={Paths.login} element={<Login />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import LogoImage from '../assets/images/logo.svg'
|
import LogoImage from '../assets/images/logo.svg'
|
||||||
import { Add, ArchiveTick, DirectInbox, DirectSend, Edit, InfoCircle, Logout, Setting2, Star, Trash } from 'iconsax-react'
|
import { Add, ArchiveTick, DirectInbox, DirectSend, Edit, InfoCircle, Logout, Star, Trash } from 'iconsax-react'
|
||||||
import SideBarItem from './SideBarItem'
|
import SideBarItem from './SideBarItem'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { useSharedStore } from './store/sharedStore'
|
import { useSharedStore } from './store/sharedStore'
|
||||||
@@ -113,14 +113,6 @@ const SideBar: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex-1 flex flex-col justify-end mt-10 md:mt-14 pb-6 md:pb-8'>
|
<div className='flex-1 flex flex-col justify-end mt-10 md:mt-14 pb-6 md:pb-8'>
|
||||||
<div className='text-xs text-[#8C90A3]'>
|
|
||||||
<SideBarItem
|
|
||||||
icon={<Setting2 variant={isActive(Paths.setting) ? 'Bold' : 'Outline'} color={isActive(Paths.setting) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
|
||||||
title={t('sidebar.setting')}
|
|
||||||
isActive={isActive(Paths.setting)}
|
|
||||||
link={Paths.setting}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='text-xs text-[#8C90A3]'>
|
<div className='text-xs text-[#8C90A3]'>
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
icon={<Logout variant={isActive('logout') ? 'Bold' : 'Outline'} color={isActive('logout') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
icon={<Logout variant={isActive('logout') ? 'Bold' : 'Outline'} color={isActive('logout') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ export const Paths = {
|
|||||||
spam: "/spam",
|
spam: "/spam",
|
||||||
setting: "/setting",
|
setting: "/setting",
|
||||||
detailEmail: "/mail/",
|
detailEmail: "/mail/",
|
||||||
|
login: "/auth/login",
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user