uploader template

This commit is contained in:
hamid zarghami
2025-07-16 20:51:15 +03:30
parent 2043daae39
commit 6f640a80a7
12 changed files with 490 additions and 119 deletions
+70 -10
View File
@@ -2,6 +2,7 @@ import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDropzone } from 'react-dropzone'
import { CloseCircle } from 'iconsax-react'
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData'
type Props = {
@@ -9,34 +10,93 @@ type Props = {
isMultiple?: boolean,
onChange?: (file: File[]) => void;
isReset?: boolean;
useUploadService?: boolean,
onUploadComplete?: (urls: string[]) => void,
onUploadError?: (error: Error) => void
}
const UploadBox: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [files, setFiles] = useState<File[]>([])
const [isUploading, setIsUploading] = useState<boolean>(false)
const { mutateAsync: singleUpload } = useSingleUpload()
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 onDrop = useCallback((acceptedFiles: File[]) => {
if (props.isMultiple) {
const array = [...files]
array.push(acceptedFiles[0])
setFiles(array)
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
props.onChange && props.onChange(array)
if (props.onChange) {
props.onChange(array)
}
} else {
setFiles([acceptedFiles[0]])
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
props.onChange && props.onChange([acceptedFiles[0]])
if (props.onChange) {
props.onChange([acceptedFiles[0]])
}
}, [files])
}
} 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 {
// Original behavior
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]])
}
}
}
}, [files, props.useUploadService, props.onUploadComplete, props.onUploadError, singleUpload])
const { getRootProps, getInputProps } = useDropzone({ onDrop })
const handleRemove = (index: number) => {
const array = [...files]
array.splice(index, 1)
setFiles(array)
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
props.onChange && props.onChange(array)
if (props.onChange) {
props.onChange(array)
}
}
useEffect(() => {
@@ -53,10 +113,10 @@ const UploadBox: FC<Props> = (props: Props) => {
<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'>
<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()} />
{t('select_file')}
{isUploading ? 'در حال آپلود...' : t('select_file')}
</button>
<div className='lg:flex gap-2 hidden'>
+53 -4
View File
@@ -3,6 +3,7 @@ import { FC, useCallback, useEffect, useState } from 'react'
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { clx } from '../helpers/utils';
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData';
type Props = {
label: string;
@@ -13,7 +14,10 @@ type Props = {
onChangePreview?: (preview: string[]) => void,
getCover?: (url: string) => void,
coverUrl?: string,
isReset?: boolean
isReset?: boolean,
useUploadService?: boolean,
onUploadComplete?: (urls: string[]) => void,
onUploadError?: (error: Error) => void
}
const UploadBoxDraggble: FC<Props> = (props: Props) => {
@@ -23,8 +27,22 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
const [perviews, setPerviews] = useState<string[]>(props.preview ? props.preview : [])
const [isFill, setIsFill] = useState<boolean>(false)
const [cover, setCover] = useState<string>(props.coverUrl ? props.coverUrl : '')
const [isUploading, setIsUploading] = useState<boolean>(false)
const { mutateAsync: singleUpload } = useSingleUpload()
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 onDrop = useCallback((acceptedFiles: File[]) => {
if (props.isMultiple) {
const array = [...files]
array.push(acceptedFiles[0])
@@ -34,7 +52,38 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
setFiles([acceptedFiles[0]])
props.onChange([acceptedFiles[0]])
}
}, [files])
} 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 {
// Original behavior
if (props.isMultiple) {
const array = [...files]
array.push(acceptedFiles[0])
setFiles(array)
props.onChange(array)
} else {
setFiles([acceptedFiles[0]])
props.onChange([acceptedFiles[0]])
}
}
}, [files, props.useUploadService, props.onUploadComplete, props.onUploadError, singleUpload])
const { getRootProps, getInputProps } = useDropzone({ onDrop })
const handleDelete = (index: number) => {
@@ -84,7 +133,7 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
color='#8C90A3'
/>
<div className='text-description text-xs'>
{props.label}
{isUploading ? 'در حال آپلود...' : props.label}
</div>
<div className='flex items-center gap-2'>
+56 -4
View File
@@ -2,10 +2,15 @@ import { CloseCircle, Paperclip2 } from 'iconsax-react'
import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDropzone } from 'react-dropzone'
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData'
type Props = {
title?: string
useUploadService?: boolean,
onUploadComplete?: (urls: string[]) => void,
onUploadError?: (error: Error) => void,
onChange?: (files: File[]) => void
}
const UploadButton: FC<Props> = (props) => {
@@ -14,10 +19,53 @@ const UploadButton: FC<Props> = (props) => {
const { title } = props
const [files, setFiles] = useState<File[]>([])
const [isUploading, setIsUploading] = useState<boolean>(false)
const onDrop = useCallback((acceptedFiles: File[]) => {
setFiles((prev) => [...prev, ...acceptedFiles])
}, [])
const { mutateAsync: singleUpload } = useSingleUpload()
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 })
@@ -25,6 +73,10 @@ const UploadButton: FC<Props> = (props) => {
const array = [...files]
array.splice(index, 1)
setFiles(array)
if (props.onChange) {
props.onChange(array)
}
}
@@ -34,7 +86,7 @@ const UploadButton: FC<Props> = (props) => {
<input {...getInputProps()} />
<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>
{files.map((file, index) => (
@@ -2,6 +2,7 @@ import { FC, useState } from 'react'
import { ImageBlock as ImageBlockType } from '../../types/blocks'
import Input from '@/components/Input'
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData'
interface ImageBlockProps {
block: ImageBlockType
@@ -21,6 +22,9 @@ const ImageBlock: FC<ImageBlockProps> = ({
const [localSrc, setLocalSrc] = useState(block.data.src)
const [localAlt, setLocalAlt] = useState(block.data.alt)
const [localUrl, setLocalUrl] = useState(block.data.url || '')
const [isUploading, setIsUploading] = useState(false)
const { mutateAsync: singleUpload } = useSingleUpload()
const handleSrcChange = (src: string) => {
setLocalSrc(src)
@@ -52,11 +56,26 @@ const ImageBlock: FC<ImageBlockProps> = ({
})
}
const handleImageUpload = (files: File[]) => {
const handleImageUpload = async (files: File[]) => {
if (files.length > 0) {
const file = files[0]
setIsUploading(true)
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 ? (
<div className="space-y-3 p-2 bg-gray-50 rounded">
<UploadBoxDraggble
label="آپلود تصویر"
label={isUploading ? "در حال آپلود..." : "آپلود تصویر"}
onChange={handleImageUpload}
/>
<Input
@@ -114,18 +133,21 @@ const ImageBlock: FC<ImageBlockProps> = ({
value={localSrc}
onChange={(e) => handleSrcChange(e.target.value)}
placeholder="https://example.com/image.jpg"
disabled={isUploading}
/>
<Input
label="متن جایگزین (Alt Text)"
value={localAlt}
onChange={(e) => handleAltChange(e.target.value)}
placeholder="توضیح تصویر"
disabled={isUploading}
/>
<Input
label="لینک تصویر (اختیاری)"
value={localUrl}
onChange={(e) => handleUrlChange(e.target.value)}
placeholder="https://example.com"
disabled={isUploading}
/>
</div>
) : (
+8 -1
View File
@@ -26,6 +26,7 @@ const Setting: FC = () => {
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.SETTING_DOMAIN)
const [templateName, setTemplateName] = useState('')
const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false)
const [isSaveProcessing, setIsSaveProcessing] = useState(false)
const { mutate: saveTemplate, isPending: isSaving } = useSaveTemplate()
const { mutate: updateTemplate, isPending: isUpdating } = useUpdateTemplate()
@@ -33,7 +34,7 @@ const Setting: FC = () => {
const { mutateAsync: singleUpload, isPending: isUploading } = useSingleUpload()
const isEditMode = !!id
const isLoading = isSaving || isUpdating || isUploading || isCapturingScreenshot
const isLoading = isSaving || isUpdating || isUploading || isCapturingScreenshot || isSaveProcessing
useEffect(() => {
if (isEditMode && template?.data?.template?.name) {
@@ -102,6 +103,9 @@ const Setting: FC = () => {
return
}
setIsSaveProcessing(true)
try {
const html = await exportPersonalityToHTML(data)
setIsCapturingScreenshot(true)
@@ -147,6 +151,9 @@ const Setting: FC = () => {
}
})
}
} finally {
setIsSaveProcessing(false)
}
}
return (
@@ -4,7 +4,7 @@ import Button from '@/components/Button'
import { TickCircle } from 'iconsax-react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { FC, useEffect } from 'react'
import { FC, useEffect, useState } from 'react'
import { AddressData, CreateAddressType, UpdateAddressType } from '../types/Types'
import { useGetDomains } from '../../domain/hooks/useDomainData'
import { useCreateAddress, useUpdateAddress } from '../hooks/useAddressData'
@@ -12,6 +12,7 @@ import { useGetSpace } from '../../domain/hooks/useDomainData'
import { useGetTemplates } from '../../personality/hooks/usePersonalityData'
import { toast } from '@/components/Toast'
import { ErrorType } from '@/helpers/types'
import ForwarderManager from './ForwarderManager'
interface AddressFormProps {
selectedAddress: AddressData | null
@@ -27,6 +28,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
const { data: spaceData } = useGetSpace()
const { mutate: createAddress, isPending } = useCreateAddress()
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
const [forwarders, setForwarders] = useState<string[]>([])
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
@@ -85,6 +87,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
title: params.title,
password: params.password || undefined,
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
forwarders: forwarders,
}
updateAddress({ id: selectedAddress.id, params: updateParams }, {
onSuccess: (data) => {
@@ -99,11 +102,13 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
const createParams: CreateAddressType = {
...params,
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
forwarders: forwarders,
}
createAddress(createParams, {
onSuccess: (data) => {
toast(data?.data?.message, 'success')
reset()
setForwarders([])
onSuccess()
},
onError: (error: ErrorType) => {
@@ -130,6 +135,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
quotaMB: selectedAddress.emailQuota ? Math.round(selectedAddress.emailQuota / (1024 * 1024)) : undefined,
templateId: selectedAddress.template || undefined,
})
setForwarders(selectedAddress.forwarders || [])
} else if (!isEditMode) {
reset({
title: '',
@@ -139,6 +145,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
quotaMB: undefined,
templateId: domain?.data?.template,
})
setForwarders([])
}
}, [selectedAddress, isEditMode, reset, domain])
@@ -219,6 +226,13 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
</div>
</div>
<div className='mt-6'>
<ForwarderManager
forwarders={forwarders}
onChange={setForwarders}
/>
</div>
<div className='mt-6'>
<Input
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
@@ -241,6 +255,8 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
</div>
)}
<div className='mt-9 flex justify-end'>
<Button
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
+3
View File
@@ -5,6 +5,7 @@ export type CreateAddressType = {
title: string;
quota?: number;
templateId?: string;
forwarders?: string[];
};
export type UpdateAddressType = {
@@ -13,6 +14,7 @@ export type UpdateAddressType = {
quota?: number;
aliases?: string[];
title?: string;
forwarders?: string[];
};
export interface AddressData extends Record<string, unknown> {
@@ -32,6 +34,7 @@ export interface AddressData extends Record<string, unknown> {
isActive: boolean;
emailSignature: string | null;
role: string;
forwarders?: string[];
business: {
id: string;
createdAt: string;
+3 -3
View File
@@ -57,9 +57,9 @@ const List: FC = () => {
)
})
}
<div className='flex-1 rounded-3xl px-4'></div>
<div className='flex-1 rounded-3xl 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 max-w-[25%] min-w-[20%] w-full px-4'></div>
<div className='flex-1 max-w-[25%] min-w-[20%] w-full px-4'></div>
</div>
</div>
@@ -6,6 +6,7 @@ import { FC, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { usePersonalityStore } from '../store/Store'
import { ImageSize, HorizontalAlignment, VerticalAlignment, ElementType } from '../types/Types'
import { useSingleUpload } from '../hooks/usePersonalityData'
const ImageSideBar: FC = () => {
@@ -27,6 +28,9 @@ const ImageSideBar: FC = () => {
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
const [isLoading, setIsLoading] = 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;
@@ -80,18 +84,31 @@ const ImageSideBar: FC = () => {
{ label: 'اندازه اصلی (Auto)', value: ImageSize.AUTO }
]
const handleImageUpload = (files: File[]) => {
const handleImageUpload = async (files: File[]) => {
if (files.length === 0) return
const file = files[0] // فقط اولین فایل را در نظر می‌گیریم
// در اینجا باید فایل آپلود شود و URL برگردانده شود
// فعلاً از FileReader استفاده می‌کنم برای تست
const file = files[0]
setIsUploading(true)
try {
const uploadResult = await singleUpload(file)
const imageUrl = uploadResult?.data?.url
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)
}
}
const handleAddImage = () => {
@@ -198,7 +215,7 @@ const ImageSideBar: FC = () => {
<div className='mt-8'>
<UploadBoxDraggble
label={t('setting.upload_image')}
label={isUploading ? 'در حال آپلود...' : t('setting.upload_image')}
onChange={handleImageUpload}
isReset={isReset}
/>
@@ -218,6 +235,7 @@ const ImageSideBar: FC = () => {
value={imageSize}
onChange={(e) => setImageSize(e.target.value as ImageSize)}
placeholder={t('setting.size')}
disabled={isUploading}
/>
</div>
@@ -275,8 +293,8 @@ const ImageSideBar: FC = () => {
<Button
onClick={handleUpdateImage}
className='bg-[#0038FF] text-white w-full'
disabled={!imageSrc}
loading={isLoading}
disabled={!imageSrc || isUploading}
loading={isLoading || isUploading}
>
<div className='flex justify-center items-center gap-2'>
<Edit size={18} color='white' />
@@ -305,8 +323,8 @@ const ImageSideBar: FC = () => {
<Button
onClick={handleAddImage}
className='bg-[#ECEEF5] text-black w-full'
disabled={!imageSrc}
loading={isLoading}
disabled={!imageSrc || isUploading}
loading={isLoading || isUploading}
>
<div className='flex justify-center items-center gap-2'>
<Add size={24} color='black' />
@@ -5,12 +5,17 @@ import ColorPicker from '@/components/ColorPicker';
import { usePersonalityStore } from '../store/Store';
import { SectionItemType, BackgroundSize, SectionName } from '../types/Types';
import Select from '@/components/Select';
import { useSingleUpload } from '../hooks/usePersonalityData';
import { toast } from '@/components/Toast';
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)
const [isUploading, setIsUploading] = useState<boolean>(false)
const { mutateAsync: singleUpload } = useSingleUpload()
// تشخیص background size بر اساس style موجود
const getCurrentBackgroundSizeType = (style?: SectionItemType['style']) => {
@@ -49,8 +54,32 @@ const SettingSideBar: FC = () => {
updateActiveItem({ backgroundColor: color })
}
const handleUploadBackground = (file: File[]) => {
const handleUploadBackground = async (file: File[]) => {
if (activeSection && file.length > 0) {
setIsUploading(true)
try {
const uploadResult = await singleUpload(file[0])
const imageUrl = uploadResult?.data?.url
if (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'
}
})
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]
@@ -63,6 +92,9 @@ const SettingSideBar: FC = () => {
backgroundPosition: 'center center'
}
})
} finally {
setIsUploading(false)
}
}
}
@@ -110,35 +142,35 @@ const SettingSideBar: FC = () => {
<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)}
className={`w-[102px] h-full bg-[#EAECF4] transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-[#d1d5db]'}`}
onClick={() => !isUploading && handleColumnClick(1)}
></div>
<div
className='w-[102px] h-full flex gap-1 cursor-pointer group'
onClick={() => handleColumnClick(2)}
className={`w-[102px] h-full flex gap-1 group transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
onClick={() => !isUploading && 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 className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></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)}
className={`w-[102px] h-full flex gap-1 group transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
onClick={() => !isUploading && 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 className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
</div>
<div
className='w-[102px] h-full flex gap-1 cursor-pointer group'
onClick={() => handleColumnClick(4)}
className={`w-[102px] h-full flex gap-1 group transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
onClick={() => !isUploading && 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 className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
</div>
</div>
@@ -152,7 +184,7 @@ const SettingSideBar: FC = () => {
<div className='mt-5'>
<UploadBoxDraggble
label={t('setting.upload_background')}
label={isUploading ? 'در حال آپلود...' : t('setting.upload_background')}
onChange={handleUploadBackground}
/>
</div>
@@ -177,6 +209,7 @@ const SettingSideBar: FC = () => {
})
}}
placeholder={t('setting.size')}
disabled={isUploading}
/>
</div>
</div>
@@ -51,7 +51,7 @@ const Templete: FC<{
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'>
<img src={item.thumbnailUrl} alt={item.name} className='w-full h-full object-contain' />
</div>