multi upload icons
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-06-14 09:51:57 +03:30
parent 328402fd7f
commit 35c1c1c8a9
3 changed files with 106 additions and 69 deletions
+32 -3
View File
@@ -12,6 +12,34 @@ type Props = {
accept?: string; accept?: string;
} }
const MIME_TO_EXTENSIONS: Record<string, string[]> = {
'image/png': ['.png'],
'image/svg+xml': ['.svg'],
'image/jpeg': ['.jpg', '.jpeg'],
'image/gif': ['.gif'],
'image/webp': ['.webp'],
}
const buildAcceptTypes = (accept?: string) => {
if (!accept) return undefined
return accept.split(',').reduce<Record<string, string[]>>((acc, type) => {
const trimmed = type.trim()
if (trimmed.startsWith('.')) {
const ext = trimmed.toLowerCase()
const mimeEntry = Object.entries(MIME_TO_EXTENSIONS).find(([, exts]) => exts.includes(ext))
if (mimeEntry) {
acc[mimeEntry[0]] = mimeEntry[1]
}
} else if (MIME_TO_EXTENSIONS[trimmed]) {
acc[trimmed] = MIME_TO_EXTENSIONS[trimmed]
} else {
acc[trimmed] = []
}
return acc
}, {})
}
const UploadBox: FC<Props> = (props: Props) => { const UploadBox: FC<Props> = (props: Props) => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
@@ -19,8 +47,7 @@ const UploadBox: FC<Props> = (props: Props) => {
const onDrop = useCallback((acceptedFiles: File[]) => { const onDrop = useCallback((acceptedFiles: File[]) => {
if (props.isMultiple) { if (props.isMultiple) {
const array = [...files] const array = [...files, ...acceptedFiles]
array.push(acceptedFiles[0])
setFiles(array) setFiles(array)
if (props.onChange) { if (props.onChange) {
props.onChange(array); props.onChange(array);
@@ -33,9 +60,11 @@ const UploadBox: FC<Props> = (props: Props) => {
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [files]) }, [files])
const acceptTypes = buildAcceptTypes(props.accept)
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
onDrop, onDrop,
accept: props.accept ? { [props.accept]: [] } : undefined accept: acceptTypes,
multiple: props.isMultiple,
}) })
const handleRemove = (index: number) => { const handleRemove = (index: number) => {
+37 -33
View File
@@ -9,7 +9,7 @@ import * as Yup from 'yup'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { TickCircle } from 'iconsax-react' import { TickCircle } from 'iconsax-react'
import { useCreateIcon, useGetGroupIcons } from '../../hooks/useIconData' import { useCreateIcon, useGetGroupIcons } from '../../hooks/useIconData'
import { useSingleUpload } from '../../../service/hooks/useServiceData' import { useMultiUpload } from '../../../service/hooks/useServiceData'
import { CreateIconType, GroupIconType } from '../../types/Types' import { CreateIconType, GroupIconType } from '../../types/Types'
import { ErrorType } from '../../../../helpers/types' import { ErrorType } from '../../../../helpers/types'
@@ -21,9 +21,10 @@ interface CreateIconProps {
const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => { const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const [file, setFile] = useState<File>() const [files, setFiles] = useState<File[]>([])
const [isSubmitting, setIsSubmitting] = useState(false)
const createIcon = useCreateIcon() const createIcon = useCreateIcon()
const singleUpload = useSingleUpload() const multiUpload = useMultiUpload()
const { data: groupIcons } = useGetGroupIcons() const { data: groupIcons } = useGetGroupIcons()
const groups = (groupIcons as unknown as { data?: GroupIconType[] })?.data || [] const groups = (groupIcons as unknown as { data?: GroupIconType[] })?.data || []
@@ -32,21 +33,6 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
label: group.name label: group.name
})) }))
const handleCreateIcon = (values: CreateIconType) => {
createIcon.mutate(values, {
onSuccess: () => {
formik.resetForm()
setFile(undefined)
close()
toast.success(t('success'))
onSuccess?.()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
const formik = useFormik<CreateIconType>({ const formik = useFormik<CreateIconType>({
initialValues: { initialValues: {
url: '', url: '',
@@ -57,27 +43,44 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
.required(t('errors.required')) .required(t('errors.required'))
}), }),
onSubmit: async (values) => { onSubmit: async (values) => {
if (!file) { if (files.length === 0) {
toast.error(t('errors.required')) toast.error(t('errors.required'))
return return
} }
const formData = new FormData()
formData.append('file', file) setIsSubmitting(true)
await singleUpload.mutateAsync(formData, { try {
onSuccess: (data) => { const formData = new FormData()
values.url = data?.data?.url files.forEach((file) => {
handleCreateIcon(values) formData.append('files', file)
}, })
onError: (error: ErrorType) => { const uploadResult = await multiUpload.mutateAsync(formData)
toast.error(error.response?.data?.error?.message[0]) const uploadedUrls: string[] = uploadResult.data.map((item: { url: string }) => item.url)
for (const url of uploadedUrls) {
await createIcon.mutateAsync({
url,
groupId: values.groupId
})
} }
})
formik.resetForm()
setFiles([])
close()
toast.success(t('success'))
onSuccess?.()
} catch (error) {
const err = error as ErrorType
toast.error(err.response?.data?.error?.message?.[0])
} finally {
setIsSubmitting(false)
}
} }
}) })
const handleClose = () => { const handleClose = () => {
formik.resetForm() formik.resetForm()
setFile(undefined) setFiles([])
close() close()
} }
@@ -91,9 +94,10 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
<div className='mt-6'> <div className='mt-6'>
<UploadBox <UploadBox
label={t('icon.url')} label={t('icon.url')}
onChange={(files) => setFile(files[0])} onChange={(selectedFiles) => setFiles(selectedFiles)}
isReset={!open} isReset={!open}
accept="image/svg+xml" isMultiple
accept="image/svg+xml,.svg,image/png,.png"
/> />
<div className='mt-4'> <div className='mt-4'>
@@ -117,7 +121,7 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
<Button <Button
onClick={() => formik.handleSubmit()} onClick={() => formik.handleSubmit()}
isLoading={createIcon.isPending || singleUpload.isPending} isLoading={isSubmitting}
> >
<div className='flex gap-2'> <div className='flex gap-2'>
<TickCircle <TickCircle
+37 -33
View File
@@ -9,7 +9,7 @@ import * as Yup from 'yup'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { TickCircle } from 'iconsax-react' import { TickCircle } from 'iconsax-react'
import { useCreateIcon, useGetGroupIcons } from '../../hooks/useIconData' import { useCreateIcon, useGetGroupIcons } from '../../hooks/useIconData'
import { useSingleUpload } from '../../../service/hooks/useServiceData' import { useMultiUpload } from '../../../service/hooks/useServiceData'
import { CreateIconType, GroupIconType } from '../../types/Types' import { CreateIconType, GroupIconType } from '../../types/Types'
import { ErrorType } from '../../../../helpers/types' import { ErrorType } from '../../../../helpers/types'
@@ -21,9 +21,10 @@ interface CreateIconProps {
const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => { const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const [file, setFile] = useState<File>() const [files, setFiles] = useState<File[]>([])
const [isSubmitting, setIsSubmitting] = useState(false)
const createIcon = useCreateIcon() const createIcon = useCreateIcon()
const singleUpload = useSingleUpload() const multiUpload = useMultiUpload()
const { data: groupIcons } = useGetGroupIcons() const { data: groupIcons } = useGetGroupIcons()
const groups = (groupIcons as unknown as { data?: GroupIconType[] })?.data || [] const groups = (groupIcons as unknown as { data?: GroupIconType[] })?.data || []
@@ -32,21 +33,6 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
label: group.name label: group.name
})) }))
const handleCreateIcon = (values: CreateIconType) => {
createIcon.mutate(values, {
onSuccess: () => {
formik.resetForm()
setFile(undefined)
close()
toast.success(t('success'))
onSuccess?.()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
const formik = useFormik<CreateIconType>({ const formik = useFormik<CreateIconType>({
initialValues: { initialValues: {
url: '', url: '',
@@ -57,27 +43,44 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
.required(t('errors.required')) .required(t('errors.required'))
}), }),
onSubmit: async (values) => { onSubmit: async (values) => {
if (!file) { if (files.length === 0) {
toast.error(t('errors.required')) toast.error(t('errors.required'))
return return
} }
const formData = new FormData()
formData.append('file', file) setIsSubmitting(true)
await singleUpload.mutateAsync(formData, { try {
onSuccess: (data) => { const formData = new FormData()
values.url = data?.data?.url files.forEach((file) => {
handleCreateIcon(values) formData.append('files', file)
}, })
onError: (error: ErrorType) => { const uploadResult = await multiUpload.mutateAsync(formData)
toast.error(error.response?.data?.error?.message[0]) const uploadedUrls: string[] = uploadResult.data.map((item: { url: string }) => item.url)
for (const url of uploadedUrls) {
await createIcon.mutateAsync({
url,
groupId: values.groupId
})
} }
})
formik.resetForm()
setFiles([])
close()
toast.success(t('success'))
onSuccess?.()
} catch (error) {
const err = error as ErrorType
toast.error(err.response?.data?.error?.message?.[0])
} finally {
setIsSubmitting(false)
}
} }
}) })
const handleClose = () => { const handleClose = () => {
formik.resetForm() formik.resetForm()
setFile(undefined) setFiles([])
close() close()
} }
@@ -91,9 +94,10 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
<div className='mt-6'> <div className='mt-6'>
<UploadBox <UploadBox
label={t('icon.url')} label={t('icon.url')}
onChange={(files) => setFile(files[0])} onChange={(selectedFiles) => setFiles(selectedFiles)}
isReset={!open} isReset={!open}
accept="image/svg+xml" isMultiple
accept="image/svg+xml,.svg,image/png,.png"
/> />
<div className='mt-4'> <div className='mt-4'>
@@ -117,7 +121,7 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
<Button <Button
onClick={() => formik.handleSubmit()} onClick={() => formik.handleSubmit()}
isLoading={createIcon.isPending || singleUpload.isPending} isLoading={isSubmitting}
> >
<div className='flex gap-2'> <div className='flex gap-2'>
<TickCircle <TickCircle