uploader template
This commit is contained in:
@@ -2,6 +2,7 @@ import { FC, useCallback, useEffect, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
import { CloseCircle } from 'iconsax-react'
|
import { CloseCircle } from 'iconsax-react'
|
||||||
|
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData'
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -9,34 +10,93 @@ type Props = {
|
|||||||
isMultiple?: boolean,
|
isMultiple?: boolean,
|
||||||
onChange?: (file: File[]) => void;
|
onChange?: (file: File[]) => void;
|
||||||
isReset?: boolean;
|
isReset?: boolean;
|
||||||
|
useUploadService?: boolean,
|
||||||
|
onUploadComplete?: (urls: string[]) => void,
|
||||||
|
onUploadError?: (error: Error) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadBox: FC<Props> = (props: Props) => {
|
const UploadBox: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [files, setFiles] = useState<File[]>([])
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||||
if (props.isMultiple) {
|
|
||||||
const array = [...files]
|
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||||
array.push(acceptedFiles[0])
|
if (props.useUploadService) {
|
||||||
setFiles(array)
|
setIsUploading(true)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
try {
|
||||||
props.onChange && props.onChange(array)
|
const uploadPromises = acceptedFiles.map(file => singleUpload(file))
|
||||||
|
const results = await Promise.all(uploadPromises)
|
||||||
|
const urls = results.map(result => result?.data?.url).filter(Boolean)
|
||||||
|
|
||||||
|
if (props.onUploadComplete) {
|
||||||
|
props.onUploadComplete(urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.isMultiple) {
|
||||||
|
const array = [...files]
|
||||||
|
array.push(acceptedFiles[0])
|
||||||
|
setFiles(array)
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(array)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFiles([acceptedFiles[0]])
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange([acceptedFiles[0]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload failed:', error)
|
||||||
|
if (props.onUploadError) {
|
||||||
|
props.onUploadError(error as Error)
|
||||||
|
}
|
||||||
|
// Fallback to normal file handling
|
||||||
|
if (props.isMultiple) {
|
||||||
|
const array = [...files]
|
||||||
|
array.push(acceptedFiles[0])
|
||||||
|
setFiles(array)
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(array)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFiles([acceptedFiles[0]])
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange([acceptedFiles[0]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setFiles([acceptedFiles[0]])
|
// Original behavior
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
if (props.isMultiple) {
|
||||||
props.onChange && props.onChange([acceptedFiles[0]])
|
const array = [...files]
|
||||||
|
array.push(acceptedFiles[0])
|
||||||
|
setFiles(array)
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(array)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFiles([acceptedFiles[0]])
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange([acceptedFiles[0]])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [files])
|
}, [files, props.useUploadService, props.onUploadComplete, props.onUploadError, singleUpload])
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||||
|
|
||||||
const handleRemove = (index: number) => {
|
const handleRemove = (index: number) => {
|
||||||
const array = [...files]
|
const array = [...files]
|
||||||
array.splice(index, 1)
|
array.splice(index, 1)
|
||||||
setFiles(array)
|
setFiles(array)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
if (props.onChange) {
|
||||||
props.onChange && props.onChange(array)
|
props.onChange(array)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -53,10 +113,10 @@ const UploadBox: FC<Props> = (props: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className='text-sm'>{props.label}</div>
|
<div className='text-sm'>{props.label}</div>
|
||||||
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
||||||
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary' disabled={isUploading}>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
|
|
||||||
{t('select_file')}
|
{isUploading ? 'در حال آپلود...' : t('select_file')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className='lg:flex gap-2 hidden'>
|
<div className='lg:flex gap-2 hidden'>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FC, useCallback, useEffect, useState } from 'react'
|
|||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { clx } from '../helpers/utils';
|
import { clx } from '../helpers/utils';
|
||||||
|
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -13,7 +14,10 @@ type Props = {
|
|||||||
onChangePreview?: (preview: string[]) => void,
|
onChangePreview?: (preview: string[]) => void,
|
||||||
getCover?: (url: string) => void,
|
getCover?: (url: string) => void,
|
||||||
coverUrl?: string,
|
coverUrl?: string,
|
||||||
isReset?: boolean
|
isReset?: boolean,
|
||||||
|
useUploadService?: boolean,
|
||||||
|
onUploadComplete?: (urls: string[]) => void,
|
||||||
|
onUploadError?: (error: Error) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||||
@@ -23,18 +27,63 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
|||||||
const [perviews, setPerviews] = useState<string[]>(props.preview ? props.preview : [])
|
const [perviews, setPerviews] = useState<string[]>(props.preview ? props.preview : [])
|
||||||
const [isFill, setIsFill] = useState<boolean>(false)
|
const [isFill, setIsFill] = useState<boolean>(false)
|
||||||
const [cover, setCover] = useState<string>(props.coverUrl ? props.coverUrl : '')
|
const [cover, setCover] = useState<string>(props.coverUrl ? props.coverUrl : '')
|
||||||
|
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||||
if (props.isMultiple) {
|
|
||||||
const array = [...files]
|
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||||
array.push(acceptedFiles[0])
|
if (props.useUploadService) {
|
||||||
setFiles(array)
|
setIsUploading(true)
|
||||||
props.onChange(array)
|
try {
|
||||||
|
const uploadPromises = acceptedFiles.map(file => singleUpload(file))
|
||||||
|
const results = await Promise.all(uploadPromises)
|
||||||
|
const urls = results.map(result => result?.data?.url).filter(Boolean)
|
||||||
|
|
||||||
|
if (props.onUploadComplete) {
|
||||||
|
props.onUploadComplete(urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.isMultiple) {
|
||||||
|
const array = [...files]
|
||||||
|
array.push(acceptedFiles[0])
|
||||||
|
setFiles(array)
|
||||||
|
props.onChange(array)
|
||||||
|
} else {
|
||||||
|
setFiles([acceptedFiles[0]])
|
||||||
|
props.onChange([acceptedFiles[0]])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload failed:', error)
|
||||||
|
if (props.onUploadError) {
|
||||||
|
props.onUploadError(error as Error)
|
||||||
|
}
|
||||||
|
// Fallback to normal file handling
|
||||||
|
if (props.isMultiple) {
|
||||||
|
const array = [...files]
|
||||||
|
array.push(acceptedFiles[0])
|
||||||
|
setFiles(array)
|
||||||
|
props.onChange(array)
|
||||||
|
} else {
|
||||||
|
setFiles([acceptedFiles[0]])
|
||||||
|
props.onChange([acceptedFiles[0]])
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setFiles([acceptedFiles[0]])
|
// Original behavior
|
||||||
props.onChange([acceptedFiles[0]])
|
if (props.isMultiple) {
|
||||||
|
const array = [...files]
|
||||||
|
array.push(acceptedFiles[0])
|
||||||
|
setFiles(array)
|
||||||
|
props.onChange(array)
|
||||||
|
} else {
|
||||||
|
setFiles([acceptedFiles[0]])
|
||||||
|
props.onChange([acceptedFiles[0]])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [files])
|
}, [files, props.useUploadService, props.onUploadComplete, props.onUploadError, singleUpload])
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||||
|
|
||||||
const handleDelete = (index: number) => {
|
const handleDelete = (index: number) => {
|
||||||
@@ -84,7 +133,7 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
|||||||
color='#8C90A3'
|
color='#8C90A3'
|
||||||
/>
|
/>
|
||||||
<div className='text-description text-xs'>
|
<div className='text-description text-xs'>
|
||||||
{props.label}
|
{isUploading ? 'در حال آپلود...' : props.label}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import { CloseCircle, Paperclip2 } from 'iconsax-react'
|
|||||||
import { FC, useCallback, useState } from 'react'
|
import { FC, useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData'
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title?: string
|
title?: string
|
||||||
|
useUploadService?: boolean,
|
||||||
|
onUploadComplete?: (urls: string[]) => void,
|
||||||
|
onUploadError?: (error: Error) => void,
|
||||||
|
onChange?: (files: File[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadButton: FC<Props> = (props) => {
|
const UploadButton: FC<Props> = (props) => {
|
||||||
@@ -14,10 +19,53 @@ const UploadButton: FC<Props> = (props) => {
|
|||||||
const { title } = props
|
const { title } = props
|
||||||
|
|
||||||
const [files, setFiles] = useState<File[]>([])
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||||
setFiles((prev) => [...prev, ...acceptedFiles])
|
|
||||||
}, [])
|
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||||
|
if (props.useUploadService) {
|
||||||
|
setIsUploading(true)
|
||||||
|
try {
|
||||||
|
const uploadPromises = acceptedFiles.map(file => singleUpload(file))
|
||||||
|
const results = await Promise.all(uploadPromises)
|
||||||
|
const urls = results.map(result => result?.data?.url).filter(Boolean)
|
||||||
|
|
||||||
|
if (props.onUploadComplete) {
|
||||||
|
props.onUploadComplete(urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFiles = [...files, ...acceptedFiles]
|
||||||
|
setFiles(newFiles)
|
||||||
|
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(newFiles)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload failed:', error)
|
||||||
|
if (props.onUploadError) {
|
||||||
|
props.onUploadError(error as Error)
|
||||||
|
}
|
||||||
|
// Fallback to normal file handling
|
||||||
|
const newFiles = [...files, ...acceptedFiles]
|
||||||
|
setFiles(newFiles)
|
||||||
|
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(newFiles)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Original behavior
|
||||||
|
const newFiles = [...files, ...acceptedFiles]
|
||||||
|
setFiles(newFiles)
|
||||||
|
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(newFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [files, props.useUploadService, props.onUploadComplete, props.onUploadError, props.onChange, singleUpload])
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||||
|
|
||||||
@@ -25,6 +73,10 @@ const UploadButton: FC<Props> = (props) => {
|
|||||||
const array = [...files]
|
const array = [...files]
|
||||||
array.splice(index, 1)
|
array.splice(index, 1)
|
||||||
setFiles(array)
|
setFiles(array)
|
||||||
|
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(array)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +86,7 @@ const UploadButton: FC<Props> = (props) => {
|
|||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
|
|
||||||
<Paperclip2 size={20} color='#000' />
|
<Paperclip2 size={20} color='#000' />
|
||||||
<div className='text-xs'>{title ? title : t('attachment_select')}</div>
|
<div className='text-xs'>{isUploading ? 'در حال آپلود...' : (title ? title : t('attachment_select'))}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{files.map((file, index) => (
|
{files.map((file, index) => (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { FC, useState } from 'react'
|
|||||||
import { ImageBlock as ImageBlockType } from '../../types/blocks'
|
import { ImageBlock as ImageBlockType } from '../../types/blocks'
|
||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
||||||
|
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData'
|
||||||
|
|
||||||
interface ImageBlockProps {
|
interface ImageBlockProps {
|
||||||
block: ImageBlockType
|
block: ImageBlockType
|
||||||
@@ -21,6 +22,9 @@ const ImageBlock: FC<ImageBlockProps> = ({
|
|||||||
const [localSrc, setLocalSrc] = useState(block.data.src)
|
const [localSrc, setLocalSrc] = useState(block.data.src)
|
||||||
const [localAlt, setLocalAlt] = useState(block.data.alt)
|
const [localAlt, setLocalAlt] = useState(block.data.alt)
|
||||||
const [localUrl, setLocalUrl] = useState(block.data.url || '')
|
const [localUrl, setLocalUrl] = useState(block.data.url || '')
|
||||||
|
const [isUploading, setIsUploading] = useState(false)
|
||||||
|
|
||||||
|
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||||
|
|
||||||
const handleSrcChange = (src: string) => {
|
const handleSrcChange = (src: string) => {
|
||||||
setLocalSrc(src)
|
setLocalSrc(src)
|
||||||
@@ -52,11 +56,26 @@ const ImageBlock: FC<ImageBlockProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImageUpload = (files: File[]) => {
|
const handleImageUpload = async (files: File[]) => {
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
const file = files[0]
|
const file = files[0]
|
||||||
const imageUrl = URL.createObjectURL(file)
|
setIsUploading(true)
|
||||||
handleSrcChange(imageUrl)
|
|
||||||
|
try {
|
||||||
|
const uploadResult = await singleUpload(file)
|
||||||
|
const imageUrl = uploadResult?.data?.url
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
handleSrcChange(imageUrl)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading image:', error)
|
||||||
|
// Fallback to object URL if upload fails
|
||||||
|
const imageUrl = URL.createObjectURL(file)
|
||||||
|
handleSrcChange(imageUrl)
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +125,7 @@ const ImageBlock: FC<ImageBlockProps> = ({
|
|||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="space-y-3 p-2 bg-gray-50 rounded">
|
<div className="space-y-3 p-2 bg-gray-50 rounded">
|
||||||
<UploadBoxDraggble
|
<UploadBoxDraggble
|
||||||
label="آپلود تصویر"
|
label={isUploading ? "در حال آپلود..." : "آپلود تصویر"}
|
||||||
onChange={handleImageUpload}
|
onChange={handleImageUpload}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -114,18 +133,21 @@ const ImageBlock: FC<ImageBlockProps> = ({
|
|||||||
value={localSrc}
|
value={localSrc}
|
||||||
onChange={(e) => handleSrcChange(e.target.value)}
|
onChange={(e) => handleSrcChange(e.target.value)}
|
||||||
placeholder="https://example.com/image.jpg"
|
placeholder="https://example.com/image.jpg"
|
||||||
|
disabled={isUploading}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="متن جایگزین (Alt Text)"
|
label="متن جایگزین (Alt Text)"
|
||||||
value={localAlt}
|
value={localAlt}
|
||||||
onChange={(e) => handleAltChange(e.target.value)}
|
onChange={(e) => handleAltChange(e.target.value)}
|
||||||
placeholder="توضیح تصویر"
|
placeholder="توضیح تصویر"
|
||||||
|
disabled={isUploading}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="لینک تصویر (اختیاری)"
|
label="لینک تصویر (اختیاری)"
|
||||||
value={localUrl}
|
value={localUrl}
|
||||||
onChange={(e) => handleUrlChange(e.target.value)}
|
onChange={(e) => handleUrlChange(e.target.value)}
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
|
disabled={isUploading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const Setting: FC = () => {
|
|||||||
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.SETTING_DOMAIN)
|
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.SETTING_DOMAIN)
|
||||||
const [templateName, setTemplateName] = useState('')
|
const [templateName, setTemplateName] = useState('')
|
||||||
const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false)
|
const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false)
|
||||||
|
const [isSaveProcessing, setIsSaveProcessing] = useState(false)
|
||||||
|
|
||||||
const { mutate: saveTemplate, isPending: isSaving } = useSaveTemplate()
|
const { mutate: saveTemplate, isPending: isSaving } = useSaveTemplate()
|
||||||
const { mutate: updateTemplate, isPending: isUpdating } = useUpdateTemplate()
|
const { mutate: updateTemplate, isPending: isUpdating } = useUpdateTemplate()
|
||||||
@@ -33,7 +34,7 @@ const Setting: FC = () => {
|
|||||||
const { mutateAsync: singleUpload, isPending: isUploading } = useSingleUpload()
|
const { mutateAsync: singleUpload, isPending: isUploading } = useSingleUpload()
|
||||||
|
|
||||||
const isEditMode = !!id
|
const isEditMode = !!id
|
||||||
const isLoading = isSaving || isUpdating || isUploading || isCapturingScreenshot
|
const isLoading = isSaving || isUpdating || isUploading || isCapturingScreenshot || isSaveProcessing
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditMode && template?.data?.template?.name) {
|
if (isEditMode && template?.data?.template?.name) {
|
||||||
@@ -102,50 +103,56 @@ const Setting: FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = await exportPersonalityToHTML(data)
|
setIsSaveProcessing(true)
|
||||||
|
|
||||||
setIsCapturingScreenshot(true)
|
try {
|
||||||
const screenshotFile = await capturePersonalityScreenshot(templateName)
|
const html = await exportPersonalityToHTML(data)
|
||||||
setIsCapturingScreenshot(false)
|
|
||||||
|
|
||||||
if (!screenshotFile) {
|
setIsCapturingScreenshot(true)
|
||||||
toast('خطا در گرفتن اسکرینشات', 'error')
|
const screenshotFile = await capturePersonalityScreenshot(templateName)
|
||||||
return
|
setIsCapturingScreenshot(false)
|
||||||
}
|
|
||||||
|
|
||||||
const thumbnailUrl = await singleUpload(screenshotFile)
|
if (!screenshotFile) {
|
||||||
|
toast('خطا در گرفتن اسکرینشات', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isEditMode) {
|
const thumbnailUrl = await singleUpload(screenshotFile)
|
||||||
updateTemplate({
|
|
||||||
id: id!,
|
if (isEditMode) {
|
||||||
params: {
|
updateTemplate({
|
||||||
|
id: id!,
|
||||||
|
params: {
|
||||||
|
name: templateName,
|
||||||
|
rawHtml: html,
|
||||||
|
structure: data,
|
||||||
|
thumbnailUrl: thumbnailUrl?.data?.url
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast(data?.data?.message || 'قالب با موفقیت بروزرسانی شد', 'success')
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast('خطا در بروزرسانی قالب', 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
saveTemplate({
|
||||||
name: templateName,
|
name: templateName,
|
||||||
rawHtml: html,
|
rawHtml: html,
|
||||||
structure: data,
|
structure: data,
|
||||||
thumbnailUrl: thumbnailUrl?.data?.url
|
thumbnailUrl: thumbnailUrl?.data?.url
|
||||||
}
|
}, {
|
||||||
}, {
|
onSuccess: (data) => {
|
||||||
onSuccess: (data) => {
|
toast(data?.data?.message || 'قالب با موفقیت ایجاد شد', 'success')
|
||||||
toast(data?.data?.message || 'قالب با موفقیت بروزرسانی شد', 'success')
|
},
|
||||||
},
|
onError: () => {
|
||||||
onError: () => {
|
toast('خطا در ایجاد قالب', 'error')
|
||||||
toast('خطا در بروزرسانی قالب', 'error')
|
}
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
} else {
|
} finally {
|
||||||
saveTemplate({
|
setIsSaveProcessing(false)
|
||||||
name: templateName,
|
|
||||||
rawHtml: html,
|
|
||||||
structure: data,
|
|
||||||
thumbnailUrl: thumbnailUrl?.data?.url
|
|
||||||
}, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
toast(data?.data?.message || 'قالب با موفقیت ایجاد شد', 'success')
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast('خطا در ایجاد قالب', 'error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Button from '@/components/Button'
|
|||||||
import { TickCircle } from 'iconsax-react'
|
import { TickCircle } from 'iconsax-react'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FC, useEffect } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { AddressData, CreateAddressType, UpdateAddressType } from '../types/Types'
|
import { AddressData, CreateAddressType, UpdateAddressType } from '../types/Types'
|
||||||
import { useGetDomains } from '../../domain/hooks/useDomainData'
|
import { useGetDomains } from '../../domain/hooks/useDomainData'
|
||||||
import { useCreateAddress, useUpdateAddress } from '../hooks/useAddressData'
|
import { useCreateAddress, useUpdateAddress } from '../hooks/useAddressData'
|
||||||
@@ -12,6 +12,7 @@ import { useGetSpace } from '../../domain/hooks/useDomainData'
|
|||||||
import { useGetTemplates } from '../../personality/hooks/usePersonalityData'
|
import { useGetTemplates } from '../../personality/hooks/usePersonalityData'
|
||||||
import { toast } from '@/components/Toast'
|
import { toast } from '@/components/Toast'
|
||||||
import { ErrorType } from '@/helpers/types'
|
import { ErrorType } from '@/helpers/types'
|
||||||
|
import ForwarderManager from './ForwarderManager'
|
||||||
|
|
||||||
interface AddressFormProps {
|
interface AddressFormProps {
|
||||||
selectedAddress: AddressData | null
|
selectedAddress: AddressData | null
|
||||||
@@ -27,6 +28,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
|||||||
const { data: spaceData } = useGetSpace()
|
const { data: spaceData } = useGetSpace()
|
||||||
const { mutate: createAddress, isPending } = useCreateAddress()
|
const { mutate: createAddress, isPending } = useCreateAddress()
|
||||||
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
|
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
|
||||||
|
const [forwarders, setForwarders] = useState<string[]>([])
|
||||||
|
|
||||||
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
|
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
|
||||||
|
|
||||||
@@ -85,6 +87,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
|||||||
title: params.title,
|
title: params.title,
|
||||||
password: params.password || undefined,
|
password: params.password || undefined,
|
||||||
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
||||||
|
forwarders: forwarders,
|
||||||
}
|
}
|
||||||
updateAddress({ id: selectedAddress.id, params: updateParams }, {
|
updateAddress({ id: selectedAddress.id, params: updateParams }, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -99,11 +102,13 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
|||||||
const createParams: CreateAddressType = {
|
const createParams: CreateAddressType = {
|
||||||
...params,
|
...params,
|
||||||
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
||||||
|
forwarders: forwarders,
|
||||||
}
|
}
|
||||||
createAddress(createParams, {
|
createAddress(createParams, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast(data?.data?.message, 'success')
|
toast(data?.data?.message, 'success')
|
||||||
reset()
|
reset()
|
||||||
|
setForwarders([])
|
||||||
onSuccess()
|
onSuccess()
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
@@ -130,6 +135,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
|||||||
quotaMB: selectedAddress.emailQuota ? Math.round(selectedAddress.emailQuota / (1024 * 1024)) : undefined,
|
quotaMB: selectedAddress.emailQuota ? Math.round(selectedAddress.emailQuota / (1024 * 1024)) : undefined,
|
||||||
templateId: selectedAddress.template || undefined,
|
templateId: selectedAddress.template || undefined,
|
||||||
})
|
})
|
||||||
|
setForwarders(selectedAddress.forwarders || [])
|
||||||
} else if (!isEditMode) {
|
} else if (!isEditMode) {
|
||||||
reset({
|
reset({
|
||||||
title: '',
|
title: '',
|
||||||
@@ -139,6 +145,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
|||||||
quotaMB: undefined,
|
quotaMB: undefined,
|
||||||
templateId: domain?.data?.template,
|
templateId: domain?.data?.template,
|
||||||
})
|
})
|
||||||
|
setForwarders([])
|
||||||
}
|
}
|
||||||
}, [selectedAddress, isEditMode, reset, domain])
|
}, [selectedAddress, isEditMode, reset, domain])
|
||||||
|
|
||||||
@@ -219,6 +226,13 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<ForwarderManager
|
||||||
|
forwarders={forwarders}
|
||||||
|
onChange={setForwarders}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<Input
|
<Input
|
||||||
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
|
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
|
||||||
@@ -241,6 +255,8 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className='mt-9 flex justify-end'>
|
<div className='mt-9 flex justify-end'>
|
||||||
<Button
|
<Button
|
||||||
className='w-fit px-20'
|
className='w-fit px-20'
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { Add, Trash } from 'iconsax-react'
|
||||||
|
import { toast } from '@/components/Toast'
|
||||||
|
|
||||||
|
interface ForwarderManagerProps {
|
||||||
|
forwarders: string[]
|
||||||
|
onChange: (forwarders: string[]) => void
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ForwarderManager: FC<ForwarderManagerProps> = ({ forwarders, onChange, error }) => {
|
||||||
|
const [newForwarder, setNewForwarder] = useState('')
|
||||||
|
|
||||||
|
const validateEmail = (email: string): boolean => {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
return emailRegex.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addForwarder = () => {
|
||||||
|
if (!newForwarder.trim()) return
|
||||||
|
|
||||||
|
if (!validateEmail(newForwarder)) {
|
||||||
|
toast('لطفاً یک ایمیل معتبر وارد کنید', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwarders.includes(newForwarder)) {
|
||||||
|
toast('این ایمیل قبلاً اضافه شده است', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange([...forwarders, newForwarder])
|
||||||
|
setNewForwarder('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeForwarder = (index: number) => {
|
||||||
|
const newForwarders = forwarders.filter((_, i) => i !== index)
|
||||||
|
onChange(newForwarders)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
addForwarder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className='block text-sm font-medium text-gray-700 mb-2'>
|
||||||
|
Forwarders
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Add new forwarder */}
|
||||||
|
<div className='flex gap-2 mb-3'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<Input
|
||||||
|
placeholder='example@domain.com'
|
||||||
|
value={newForwarder}
|
||||||
|
onChange={(e) => setNewForwarder(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
className='text-left'
|
||||||
|
dir='ltr'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='primary'
|
||||||
|
onClick={addForwarder}
|
||||||
|
className='w-fit'
|
||||||
|
disabled={!newForwarder.trim()}
|
||||||
|
>
|
||||||
|
<Add size={16} color='white' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List of existing forwarders */}
|
||||||
|
{forwarders.length > 0 && (
|
||||||
|
<div className='space-y-2'>
|
||||||
|
{forwarders.map((forwarder, index) => (
|
||||||
|
<div key={index} className='flex items-center justify-between bg-gray-50 px-2 py-1 rounded-lg'>
|
||||||
|
<span className='text-sm text-right flex-1 ' dir='ltr'>{forwarder}</span>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='secondary'
|
||||||
|
onClick={() => removeForwarder(index)}
|
||||||
|
className='w-fit h-9 text-red-500 hover:bg-red-50'
|
||||||
|
>
|
||||||
|
<Trash size={16} color='red' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className='text-red-500 text-sm mt-1'>{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='text-[10px] text-gray-500 mt-2 leading-4'>
|
||||||
|
ایمیلهایی که به این آدرس ارسال میشوند به آدرسهای فوق نیز فرستاده خواهند شد
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ForwarderManager
|
||||||
@@ -5,6 +5,7 @@ export type CreateAddressType = {
|
|||||||
title: string;
|
title: string;
|
||||||
quota?: number;
|
quota?: number;
|
||||||
templateId?: string;
|
templateId?: string;
|
||||||
|
forwarders?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateAddressType = {
|
export type UpdateAddressType = {
|
||||||
@@ -13,6 +14,7 @@ export type UpdateAddressType = {
|
|||||||
quota?: number;
|
quota?: number;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
title?: string;
|
title?: string;
|
||||||
|
forwarders?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AddressData extends Record<string, unknown> {
|
export interface AddressData extends Record<string, unknown> {
|
||||||
@@ -32,6 +34,7 @@ export interface AddressData extends Record<string, unknown> {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
emailSignature: string | null;
|
emailSignature: string | null;
|
||||||
role: string;
|
role: string;
|
||||||
|
forwarders?: string[];
|
||||||
business: {
|
business: {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ const List: FC = () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
<div className='flex-1 rounded-3xl px-4'></div>
|
<div className='flex-1 max-w-[25%] min-w-[20%] w-full px-4'></div>
|
||||||
<div className='flex-1 rounded-3xl px-4'></div>
|
<div className='flex-1 max-w-[25%] min-w-[20%] w-full px-4'></div>
|
||||||
<div className='flex-1 rounded-3xl px-4'></div>
|
<div className='flex-1 max-w-[25%] min-w-[20%] w-full px-4'></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { FC, useState, useEffect } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePersonalityStore } from '../store/Store'
|
import { usePersonalityStore } from '../store/Store'
|
||||||
import { ImageSize, HorizontalAlignment, VerticalAlignment, ElementType } from '../types/Types'
|
import { ImageSize, HorizontalAlignment, VerticalAlignment, ElementType } from '../types/Types'
|
||||||
|
import { useSingleUpload } from '../hooks/usePersonalityData'
|
||||||
|
|
||||||
const ImageSideBar: FC = () => {
|
const ImageSideBar: FC = () => {
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ const ImageSideBar: FC = () => {
|
|||||||
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
|
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
const [isReset, setIsReset] = useState<boolean>(false)
|
const [isReset, setIsReset] = useState<boolean>(false)
|
||||||
|
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||||
|
|
||||||
const isEditMode = selectedElement?.type === ElementType.IMAGE;
|
const isEditMode = selectedElement?.type === ElementType.IMAGE;
|
||||||
|
|
||||||
@@ -80,18 +84,31 @@ const ImageSideBar: FC = () => {
|
|||||||
{ label: 'اندازه اصلی (Auto)', value: ImageSize.AUTO }
|
{ label: 'اندازه اصلی (Auto)', value: ImageSize.AUTO }
|
||||||
]
|
]
|
||||||
|
|
||||||
const handleImageUpload = (files: File[]) => {
|
const handleImageUpload = async (files: File[]) => {
|
||||||
if (files.length === 0) return
|
if (files.length === 0) return
|
||||||
|
|
||||||
const file = files[0] // فقط اولین فایل را در نظر میگیریم
|
const file = files[0]
|
||||||
// در اینجا باید فایل آپلود شود و URL برگردانده شود
|
setIsUploading(true)
|
||||||
// فعلاً از FileReader استفاده میکنم برای تست
|
|
||||||
const reader = new FileReader()
|
try {
|
||||||
reader.onload = (e) => {
|
const uploadResult = await singleUpload(file)
|
||||||
const result = e.target?.result as string
|
const imageUrl = uploadResult?.data?.url
|
||||||
setImageSrc(result)
|
|
||||||
|
if (imageUrl) {
|
||||||
|
setImageSrc(imageUrl)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading image:', error)
|
||||||
|
// Fallback to FileReader if upload fails
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const result = e.target?.result as string
|
||||||
|
setImageSrc(result)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddImage = () => {
|
const handleAddImage = () => {
|
||||||
@@ -198,7 +215,7 @@ const ImageSideBar: FC = () => {
|
|||||||
|
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<UploadBoxDraggble
|
<UploadBoxDraggble
|
||||||
label={t('setting.upload_image')}
|
label={isUploading ? 'در حال آپلود...' : t('setting.upload_image')}
|
||||||
onChange={handleImageUpload}
|
onChange={handleImageUpload}
|
||||||
isReset={isReset}
|
isReset={isReset}
|
||||||
/>
|
/>
|
||||||
@@ -218,6 +235,7 @@ const ImageSideBar: FC = () => {
|
|||||||
value={imageSize}
|
value={imageSize}
|
||||||
onChange={(e) => setImageSize(e.target.value as ImageSize)}
|
onChange={(e) => setImageSize(e.target.value as ImageSize)}
|
||||||
placeholder={t('setting.size')}
|
placeholder={t('setting.size')}
|
||||||
|
disabled={isUploading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -275,8 +293,8 @@ const ImageSideBar: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleUpdateImage}
|
onClick={handleUpdateImage}
|
||||||
className='bg-[#0038FF] text-white w-full'
|
className='bg-[#0038FF] text-white w-full'
|
||||||
disabled={!imageSrc}
|
disabled={!imageSrc || isUploading}
|
||||||
loading={isLoading}
|
loading={isLoading || isUploading}
|
||||||
>
|
>
|
||||||
<div className='flex justify-center items-center gap-2'>
|
<div className='flex justify-center items-center gap-2'>
|
||||||
<Edit size={18} color='white' />
|
<Edit size={18} color='white' />
|
||||||
@@ -305,8 +323,8 @@ const ImageSideBar: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleAddImage}
|
onClick={handleAddImage}
|
||||||
className='bg-[#ECEEF5] text-black w-full'
|
className='bg-[#ECEEF5] text-black w-full'
|
||||||
disabled={!imageSrc}
|
disabled={!imageSrc || isUploading}
|
||||||
loading={isLoading}
|
loading={isLoading || isUploading}
|
||||||
>
|
>
|
||||||
<div className='flex justify-center items-center gap-2'>
|
<div className='flex justify-center items-center gap-2'>
|
||||||
<Add size={24} color='black' />
|
<Add size={24} color='black' />
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import ColorPicker from '@/components/ColorPicker';
|
|||||||
import { usePersonalityStore } from '../store/Store';
|
import { usePersonalityStore } from '../store/Store';
|
||||||
import { SectionItemType, BackgroundSize, SectionName } from '../types/Types';
|
import { SectionItemType, BackgroundSize, SectionName } from '../types/Types';
|
||||||
import Select from '@/components/Select';
|
import Select from '@/components/Select';
|
||||||
|
import { useSingleUpload } from '../hooks/usePersonalityData';
|
||||||
|
import { toast } from '@/components/Toast';
|
||||||
|
|
||||||
const SettingSideBar: FC = () => {
|
const SettingSideBar: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { activeSection, setColumnsCount, updateActiveItem, data, activeItemIndex } = usePersonalityStore()
|
const { activeSection, setColumnsCount, updateActiveItem, data, activeItemIndex } = usePersonalityStore()
|
||||||
const [color, setColor] = useState("#aabbcc");
|
const [color, setColor] = useState("#aabbcc");
|
||||||
const [backgroundSize, setBackgroundSize] = useState<BackgroundSize>(BackgroundSize.COVER)
|
const [backgroundSize, setBackgroundSize] = useState<BackgroundSize>(BackgroundSize.COVER)
|
||||||
|
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||||
|
|
||||||
// تشخیص background size بر اساس style موجود
|
// تشخیص background size بر اساس style موجود
|
||||||
const getCurrentBackgroundSizeType = (style?: SectionItemType['style']) => {
|
const getCurrentBackgroundSizeType = (style?: SectionItemType['style']) => {
|
||||||
@@ -49,20 +54,47 @@ const SettingSideBar: FC = () => {
|
|||||||
updateActiveItem({ backgroundColor: color })
|
updateActiveItem({ backgroundColor: color })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUploadBackground = (file: File[]) => {
|
const handleUploadBackground = async (file: File[]) => {
|
||||||
if (activeSection && file.length > 0) {
|
if (activeSection && file.length > 0) {
|
||||||
const imageUrl = URL.createObjectURL(file[0])
|
setIsUploading(true)
|
||||||
|
|
||||||
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
|
try {
|
||||||
updateActiveItem({
|
const uploadResult = await singleUpload(file[0])
|
||||||
backgroundImage: imageUrl,
|
const imageUrl = uploadResult?.data?.url
|
||||||
style: {
|
|
||||||
...currentItem?.style,
|
if (imageUrl) {
|
||||||
backgroundSize: getBackgroundSize(backgroundSize),
|
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
|
||||||
backgroundRepeat: getBackgroundRepeat(backgroundSize),
|
updateActiveItem({
|
||||||
backgroundPosition: 'center center'
|
backgroundImage: imageUrl,
|
||||||
|
style: {
|
||||||
|
...currentItem?.style,
|
||||||
|
backgroundSize: getBackgroundSize(backgroundSize),
|
||||||
|
backgroundRepeat: getBackgroundRepeat(backgroundSize),
|
||||||
|
backgroundPosition: 'center center'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
toast('تصویر پسزمینه با موفقیت آپلود شد', 'success')
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error('Error uploading background image:', error)
|
||||||
|
toast('خطا در آپلود تصویر پسزمینه، از فایل محلی استفاده شد', 'error')
|
||||||
|
|
||||||
|
// Fallback to object URL if upload fails
|
||||||
|
const imageUrl = URL.createObjectURL(file[0])
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,35 +142,35 @@ const SettingSideBar: FC = () => {
|
|||||||
|
|
||||||
<div className='flex h-[51px] gap-5 mt-6'>
|
<div className='flex h-[51px] gap-5 mt-6'>
|
||||||
<div
|
<div
|
||||||
className='w-[102px] h-full bg-[#EAECF4] cursor-pointer hover:bg-[#d1d5db] transition-colors'
|
className={`w-[102px] h-full bg-[#EAECF4] transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-[#d1d5db]'}`}
|
||||||
onClick={() => handleColumnClick(1)}
|
onClick={() => !isUploading && handleColumnClick(1)}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className='w-[102px] h-full flex gap-1 cursor-pointer group'
|
className={`w-[102px] h-full flex gap-1 group transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
onClick={() => handleColumnClick(2)}
|
onClick={() => !isUploading && handleColumnClick(2)}
|
||||||
>
|
>
|
||||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex h-[51px] gap-5 mt-5'>
|
<div className='flex h-[51px] gap-5 mt-5'>
|
||||||
<div
|
<div
|
||||||
className='w-[102px] h-full flex gap-1 cursor-pointer group'
|
className={`w-[102px] h-full flex gap-1 group transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
onClick={() => handleColumnClick(3)}
|
onClick={() => !isUploading && handleColumnClick(3)}
|
||||||
>
|
>
|
||||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className='w-[102px] h-full flex gap-1 cursor-pointer group'
|
className={`w-[102px] h-full flex gap-1 group transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
onClick={() => handleColumnClick(4)}
|
onClick={() => !isUploading && handleColumnClick(4)}
|
||||||
>
|
>
|
||||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -152,7 +184,7 @@ const SettingSideBar: FC = () => {
|
|||||||
|
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<UploadBoxDraggble
|
<UploadBoxDraggble
|
||||||
label={t('setting.upload_background')}
|
label={isUploading ? 'در حال آپلود...' : t('setting.upload_background')}
|
||||||
onChange={handleUploadBackground}
|
onChange={handleUploadBackground}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,6 +209,7 @@ const SettingSideBar: FC = () => {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
placeholder={t('setting.size')}
|
placeholder={t('setting.size')}
|
||||||
|
disabled={isUploading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const Templete: FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div key={item.id} className='flex-1 bg-white rounded-3xl p-4'>
|
<div key={item.id} className='flex-1 max-w-[25%] min-w-[20%] w-full bg-white rounded-3xl p-4'>
|
||||||
<div className='h-[140px] bg-gray-200 rounded-3xl'>
|
<div className='h-[140px] bg-gray-200 rounded-3xl'>
|
||||||
<img src={item.thumbnailUrl} alt={item.name} className='w-full h-full object-contain' />
|
<img src={item.thumbnailUrl} alt={item.name} className='w-full h-full object-contain' />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user