presigned url
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-10 20:08:31 +03:30
parent 7fd8b4c7e5
commit 7271ea8f21
26 changed files with 186 additions and 123 deletions
+18
View File
@@ -0,0 +1,18 @@
import { type FC, type ImgHTMLAttributes } from "react";
import { usePresignedUrl } from "@/pages/uploader/hooks/usePresignedUrl";
type Props = ImgHTMLAttributes<HTMLImageElement> & {
src?: string;
};
const PresignedImage: FC<Props> = ({ src, alt = "", ...props }) => {
const resolvedSrc = usePresignedUrl(src);
if (!resolvedSrc) {
return null;
}
return <img src={resolvedSrc} alt={alt} {...props} />;
};
export default PresignedImage;
+11 -7
View File
@@ -2,6 +2,7 @@ import { CloseCircle, DocumentUpload, Gallery } from 'iconsax-react'
import { type FC, useCallback, useEffect, useState } from 'react' import { type FC, useCallback, useEffect, useState } from 'react'
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { clx } from '../helpers/utils'; import { clx } from '../helpers/utils';
import PresignedImage from './PresignedImage';
type Props = { type Props = {
label: string; label: string;
@@ -100,16 +101,19 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
return ( return (
<div key={index} className='flex items-center gap-2'> <div key={index} className='flex items-center gap-2'>
<div key={index} className='flex relative items-center gap-2'> <div key={index} className='flex relative items-center gap-2'>
<img onClick={() => { <PresignedImage
if (props.getCover) { onClick={() => {
setCover(item) if (props.getCover) {
props.getCover(item) setCover(item)
} props.getCover(item)
}} src={item} }
}}
src={item}
className={clx( className={clx(
'size-10 rounded-full object-cover', 'size-10 rounded-full object-cover',
cover === item ? 'border-2 border-red-400' : '' cover === item ? 'border-2 border-red-400' : ''
)} /> )}
/>
<div onClick={() => handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'> <div onClick={() => handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
<CloseCircle className='size-4 ' color='red' /> <CloseCircle className='size-4 ' color='red' />
</div> </div>
+5 -3
View File
@@ -1,18 +1,20 @@
import { Pause, Play } from "iconsax-react" import { Pause, Play } from "iconsax-react"
import { useEffect, useRef, useState, type FC } from "react" import { useEffect, useRef, useState, type FC } from "react"
import { usePresignedUrl } from "@/pages/uploader/hooks/usePresignedUrl"
type VoicePlayerProps = { type VoicePlayerProps = {
url: string url: string
} }
const VoicePlayer: FC<VoicePlayerProps> = ({ url }) => { const VoicePlayer: FC<VoicePlayerProps> = ({ url }) => {
const resolvedUrl = usePresignedUrl(url)
const audioRef = useRef<HTMLAudioElement | null>(null) const audioRef = useRef<HTMLAudioElement | null>(null)
const [isPlaying, setIsPlaying] = useState(false) const [isPlaying, setIsPlaying] = useState(false)
const [progress, setProgress] = useState(0) const [progress, setProgress] = useState(0)
useEffect(() => { useEffect(() => {
const audio = audioRef.current const audio = audioRef.current
if (!audio) return if (!audio || !resolvedUrl) return
const update = () => { const update = () => {
if (!audio.duration) return if (!audio.duration) return
@@ -28,7 +30,7 @@ const VoicePlayer: FC<VoicePlayerProps> = ({ url }) => {
return () => { return () => {
audio.removeEventListener('timeupdate', update) audio.removeEventListener('timeupdate', update)
} }
}, []) }, [resolvedUrl])
const toggle = () => { const toggle = () => {
if (!audioRef.current) return if (!audioRef.current) return
@@ -66,7 +68,7 @@ const VoicePlayer: FC<VoicePlayerProps> = ({ url }) => {
})} })}
</div> </div>
<audio ref={audioRef} src={url} className="hidden" /> <audio ref={audioRef} src={resolvedUrl} className="hidden" />
</div> </div>
) )
} }
+1 -1
View File
@@ -73,7 +73,7 @@ const CreateInvoice: FC = () => {
uploadResult?.data?.forEach((item) => { uploadResult?.data?.forEach((item) => {
attachments.push({ attachments.push({
type: "uploads_attach", type: "uploads_attach",
url: item.url, url: item.key,
}); });
}); });
} }
+1 -1
View File
@@ -127,7 +127,7 @@ const UpdateInvoice: FC = () => {
uploadResult?.data?.forEach((item) => { uploadResult?.data?.forEach((item) => {
attachments.push({ attachments.push({
type: "uploads_attach", type: "uploads_attach",
url: item.url, url: item.key,
}); });
}); });
} }
+2 -2
View File
@@ -49,10 +49,10 @@ const CreateLearning: FC = () => {
if (videoFile && coverFile) { if (videoFile && coverFile) {
const videoResult = await singleUpload.mutateAsync(videoFile) const videoResult = await singleUpload.mutateAsync(videoFile)
values.videoUrl = videoResult?.data?.url ?? '' values.videoUrl = videoResult?.data?.key ?? ''
const coverResult = await singleUpload.mutateAsync(coverFile) const coverResult = await singleUpload.mutateAsync(coverFile)
values.coverUrl = coverResult?.data?.url ?? '' values.coverUrl = coverResult?.data?.key ?? ''
createLearning.mutate(values, { createLearning.mutate(values, {
onSuccess: () => { onSuccess: () => {
+2 -1
View File
@@ -7,6 +7,7 @@ import { Add } from 'iconsax-react'
import { useGetLearning } from './hooks/useLearningData' import { useGetLearning } from './hooks/useLearningData'
import { type LearningItemType } from './hooks/useLearningData' import { type LearningItemType } from './hooks/useLearningData'
import Table from '../../components/Table' import Table from '../../components/Table'
import PresignedImage from '@/components/PresignedImage'
import { type ColumnType } from '../../components/types/TableTypes' import { type ColumnType } from '../../components/types/TableTypes'
const LearningList: FC = () => { const LearningList: FC = () => {
const [page, setPage] = useState<number>(1) const [page, setPage] = useState<number>(1)
@@ -19,7 +20,7 @@ const LearningList: FC = () => {
key: 'coverUrl', key: 'coverUrl',
width: '100px', width: '100px',
render: (item: LearningItemType) => ( render: (item: LearningItemType) => (
<img src={item.coverUrl} className='w-20 h-12 object-cover rounded' alt={item.title} /> <PresignedImage src={item.coverUrl} className='w-20 h-12 object-cover rounded' alt={item.title} />
) )
}, },
{ {
+1 -1
View File
@@ -75,7 +75,7 @@ const ConvertToOrders: FC = () => {
if (attachmentFiles.length > 0) { if (attachmentFiles.length > 0) {
const result = await multiUpload.mutateAsync(attachmentFiles) const result = await multiUpload.mutateAsync(attachmentFiles)
result?.data?.forEach((item) => { result?.data?.forEach((item) => {
attachments.push(item.url) attachments.push(item.key)
}) })
} }
+1 -1
View File
@@ -70,7 +70,7 @@ const EditOrder: FC = () => {
if (attachmentFiles.length > 0) { if (attachmentFiles.length > 0) {
const result = await multiUpload.mutateAsync(attachmentFiles) const result = await multiUpload.mutateAsync(attachmentFiles)
result?.data?.forEach((item) => { result?.data?.forEach((item) => {
attachments.push(item.url) attachments.push(item.key)
}) })
} }
@@ -2,6 +2,7 @@ import { type FC, type ReactNode } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import moment from 'moment-jalaali' import moment from 'moment-jalaali'
import { DocumentText } from 'iconsax-react' import { DocumentText } from 'iconsax-react'
import PresignedImage from '@/components/PresignedImage'
import { Paths } from '@/config/Paths' import { Paths } from '@/config/Paths'
import { useGetInvoiceItem } from '@/pages/invoice/hooks/useInvoiceData' import { useGetInvoiceItem } from '@/pages/invoice/hooks/useInvoiceData'
import { formatItemDiscountDisplay } from '@/pages/invoice/utils/invoiceItem' import { formatItemDiscountDisplay } from '@/pages/invoice/utils/invoiceItem'
@@ -80,7 +81,7 @@ const InvoiceItemDetailSection: FC<Props> = ({ invoiceItemId }) => {
<div className="flex flex-col gap-5 md:flex-row"> <div className="flex flex-col gap-5 md:flex-row">
<div className="size-24 shrink-0 overflow-hidden rounded-xl border border-gray-200 bg-gray-50"> <div className="size-24 shrink-0 overflow-hidden rounded-xl border border-gray-200 bg-gray-50">
{imageUrl ? ( {imageUrl ? (
<img <PresignedImage
src={imageUrl} src={imageUrl}
alt={invoiceItem.product?.title} alt={invoiceItem.product?.title}
className="size-full object-cover" className="size-full object-cover"
+10 -16
View File
@@ -51,25 +51,19 @@ const Order: FC<Props> = ({ addNewItem }) => {
onSubmit: async (values) => { onSubmit: async (values) => {
const attachments: AttachmentsType[] = [] const attachments: AttachmentsType[] = []
if (files.length) { if (files.length) {
await multiUpload.mutateAsync(files, { const filesResult = await multiUpload.mutateAsync(files)
onSuccess: (data) => { filesResult.data?.forEach((item) => {
data?.data?.map((item) => { attachments.push({
attachments.push({ type: 'uploads_attach',
type: 'uploads_attach', url: item.key,
url: item.url })
})
})
}
}) })
} }
if (voiceFile) { if (voiceFile) {
await singleUpload.mutateAsync(voiceFile, { const voiceResult = await singleUpload.mutateAsync(voiceFile)
onSuccess: (data) => { attachments.push({
attachments.push({ type: 'voice',
type: 'voice', url: voiceResult.data.key,
url: data?.data?.url
})
}
}) })
} }
if (indexOrder === -1) { if (indexOrder === -1) {
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'
import { Call, Edit2, Location, Paperclip2, Profile2User, Receipt21, User } from 'iconsax-react' import { Call, Edit2, Location, Paperclip2, Profile2User, Receipt21, User } from 'iconsax-react'
import { clx } from '@/helpers/utils' import { clx } from '@/helpers/utils'
import { getFileNameAndExtensionFromUrl } from '@/config/func' import { getFileNameAndExtensionFromUrl } from '@/config/func'
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
import { Paths } from '@/config/Paths' import { Paths } from '@/config/Paths'
import { import {
getAttachmentUrl, getAttachmentUrl,
@@ -201,19 +202,21 @@ const OrderDetailSidebar: FC<Props> = ({
<section className="bg-white rounded-3xl p-5 shadow-sm"> <section className="bg-white rounded-3xl p-5 shadow-sm">
<h3 className="text-sm font-medium text-gray-900 mb-4">پیوستها</h3> <h3 className="text-sm font-medium text-gray-900 mb-4">پیوستها</h3>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{attachments.map((url, idx) => { {attachments.map((key, idx) => {
const { fileName } = getFileNameAndExtensionFromUrl(url) const { fileName } = getFileNameAndExtensionFromUrl(key)
return ( return (
<a <button
key={url} key={key}
href={url} type="button"
target="_blank" onClick={async () => {
rel="noopener noreferrer" const resolvedUrl = await getPresignedUrl(key)
className="flex items-center gap-2 rounded-xl border border-gray-100 bg-gray-50/80 px-3 py-2.5 text-sm text-[#0037FF] hover:bg-blue-50 transition-colors" window.open(resolvedUrl, '_blank', 'noopener,noreferrer')
}}
className="flex items-center gap-2 rounded-xl border border-gray-100 bg-gray-50/80 px-3 py-2.5 text-sm text-[#0037FF] hover:bg-blue-50 transition-colors text-right w-full"
> >
<Paperclip2 size={18} className="shrink-0" /> <Paperclip2 size={18} className="shrink-0" />
<span className="truncate">{fileName || `پیوست ${idx + 1}`}</span> <span className="truncate">{fileName || `پیوست ${idx + 1}`}</span>
</a> </button>
) )
})} })}
</div> </div>
+2 -1
View File
@@ -6,6 +6,7 @@ import { Link, useParams } from 'react-router-dom'
import { Paths } from '@/config/Paths' import { Paths } from '@/config/Paths'
import { clx } from '@/helpers/utils' import { clx } from '@/helpers/utils'
import { DocumentText, Printer } from 'iconsax-react' import { DocumentText, Printer } from 'iconsax-react'
import PresignedImage from '@/components/PresignedImage'
type Props = { type Props = {
item: OrderItemType item: OrderItemType
@@ -66,7 +67,7 @@ const OrderItem: FC<Props> = ({ item, estimatedDays, designerName, index }) => {
<div className="flex flex-col gap-5 md:flex-row"> <div className="flex flex-col gap-5 md:flex-row">
<div className="size-24 shrink-0 overflow-hidden rounded-xl border border-gray-200 bg-white"> <div className="size-24 shrink-0 overflow-hidden rounded-xl border border-gray-200 bg-white">
{imageUrl ? ( {imageUrl ? (
<img <PresignedImage
src={imageUrl} src={imageUrl}
alt={item.product?.title} alt={item.product?.title}
className="size-full object-cover" className="size-full object-cover"
+10 -16
View File
@@ -55,25 +55,19 @@ const EditOrder: FC<Props> = ({ addNewItem, initialValues }) => {
onSubmit: async (values) => { onSubmit: async (values) => {
const attachments: AttachmentsType[] = [] const attachments: AttachmentsType[] = []
if (files.length) { if (files.length) {
await multiUpload.mutateAsync(files, { const filesResult = await multiUpload.mutateAsync(files)
onSuccess: (data) => { filesResult.data?.forEach((item) => {
data?.data?.map((item) => { attachments.push({
attachments.push({ type: 'uploads_attach',
type: 'uploads_attach', url: item.key,
url: item.url })
})
})
}
}) })
} }
if (voiceFile) { if (voiceFile) {
await singleUpload.mutateAsync(voiceFile, { const voiceResult = await singleUpload.mutateAsync(voiceFile)
onSuccess: (data) => { attachments.push({
attachments.push({ type: 'voice',
type: 'voice', url: voiceResult.data.key,
url: data?.data?.url
})
}
}) })
} }
if (indexOrder === -1) { if (indexOrder === -1) {
+13 -17
View File
@@ -11,6 +11,7 @@ import { useAddTicket, useGetTickets } from '../hooks/useOrderData'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { toast } from '@/shared/toast' import { toast } from '@/shared/toast'
import { extractErrorMessage, getFileNameAndExtensionFromUrl } from '@/config/func' import { extractErrorMessage, getFileNameAndExtensionFromUrl } from '@/config/func'
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
import VoicePlayer from '@/components/VoicePlayer' import VoicePlayer from '@/components/VoicePlayer'
@@ -44,25 +45,19 @@ const TicketSection: FC = () => {
content: message content: message
} }
if (audioFile) { if (audioFile) {
await singleUpload.mutateAsync(audioFile, { const voiceResult = await singleUpload.mutateAsync(audioFile)
onSuccess: (data) => { params.attachments.push({
params.attachments.push({ type: 'voice',
type: 'voice', url: voiceResult.data.key,
url: data?.data?.url
})
}
}) })
} }
if (files.length) { if (files.length) {
await multiUpload.mutateAsync(files, { const filesResult = await multiUpload.mutateAsync(files)
onSuccess: (data) => { filesResult.data?.forEach((item) => {
data?.data?.map((item) => { params.attachments.push({
params.attachments.push({ type: 'uploads_attach',
type: 'uploads_attach', url: item.key,
url: item.url })
})
})
}
}) })
} }
@@ -80,7 +75,8 @@ const TicketSection: FC = () => {
}) })
} }
const handleOpenLink = (url: string) => { const handleOpenLink = async (key: string) => {
const url = await getPresignedUrl(key)
window.open( window.open(
url, url,
'_blank' '_blank'
+1 -1
View File
@@ -56,7 +56,7 @@ const CreateProduct: FC = () => {
onSuccess: (data) => { onSuccess: (data) => {
const imagesUrl: string[] = [] const imagesUrl: string[] = []
data.data?.map((item) => { data.data?.map((item) => {
imagesUrl.push(item.url) imagesUrl.push(item.key)
}) })
values.images = imagesUrl values.images = imagesUrl
handleSave(values) handleSave(values)
+1 -1
View File
@@ -59,7 +59,7 @@ const UpdateProduct: FC = () => {
onSuccess: (data) => { onSuccess: (data) => {
const imagesUrl: string[] = [...values.images] const imagesUrl: string[] = [...values.images]
data.data?.map((item) => { data.data?.map((item) => {
imagesUrl.push(item.url) imagesUrl.push(item.key)
}) })
values.images = imagesUrl values.images = imagesUrl
handleSave(values) handleSave(values)
+1 -1
View File
@@ -58,7 +58,7 @@ const ProductCategory: FC = () => {
if (file) { if (file) {
upload(file, { upload(file, {
onSuccess: (data) => { onSuccess: (data) => {
values.avatarUrl = data?.data?.url values.avatarUrl = data?.data?.key
handleSave(values) handleSave(values)
} }
}) })
+2 -1
View File
@@ -1,4 +1,5 @@
import { type FC, useMemo } from 'react' import { type FC, useMemo } from 'react'
import PresignedImage from '@/components/PresignedImage'
import { useDeleteCategory, useGetCategory } from '../hooks/useProductData' import { useDeleteCategory, useGetCategory } from '../hooks/useProductData'
import Table from '@/components/Table' import Table from '@/components/Table'
import type { ColumnType, RowDataType } from '@/components/types/TableTypes' import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
@@ -33,7 +34,7 @@ const CategoryList: FC = () => {
{ {
title: 'تصویر', title: 'تصویر',
key: 'avatarUrl', key: 'avatarUrl',
render: (item) => item.avatarUrl ? <img src={item.avatarUrl} className='w-14' alt={item.title} /> : '-' render: (item) => item.avatarUrl ? <PresignedImage src={item.avatarUrl} className='w-14' alt={item.title} /> : '-'
}, },
{ {
title: 'عنوان', title: 'عنوان',
+1 -1
View File
@@ -60,7 +60,7 @@ const UpdateCategory: FC = () => {
if (file) { if (file) {
upload(file, { upload(file, {
onSuccess: (data) => { onSuccess: (data) => {
values.avatarUrl = data?.data?.url values.avatarUrl = data?.data?.key
handleSave(values) handleSave(values)
} }
}) })
@@ -3,6 +3,8 @@ import { Paperclip2 } from 'iconsax-react'
import placeholderProductImage from '@/assets/images/placeholder-product.svg' import placeholderProductImage from '@/assets/images/placeholder-product.svg'
import { getFileNameAndExtensionFromUrl } from '@/config/func' import { getFileNameAndExtensionFromUrl } from '@/config/func'
import VoicePlayer from '@/components/VoicePlayer' import VoicePlayer from '@/components/VoicePlayer'
import PresignedImage from '@/components/PresignedImage'
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
import { useGetEntityField } from '@/pages/formBuilder/hooks/useFormBuilderData' import { useGetEntityField } from '@/pages/formBuilder/hooks/useFormBuilderData'
import type { RequestDetailItemType } from '../types/Types' import type { RequestDetailItemType } from '../types/Types'
@@ -47,7 +49,8 @@ const RequestDetailItem: FC<Props> = ({ item, index, total }) => {
const voiceAttachments = item.attachments?.filter((a) => a.type === 'voice') ?? [] const voiceAttachments = item.attachments?.filter((a) => a.type === 'voice') ?? []
const fileAttachments = item.attachments?.filter((a) => a.type !== 'voice') ?? [] const fileAttachments = item.attachments?.filter((a) => a.type !== 'voice') ?? []
const handleOpenLink = (url: string) => { const handleOpenLink = async (key: string) => {
const url = await getPresignedUrl(key)
window.open(url, '_blank', 'noopener,noreferrer') window.open(url, '_blank', 'noopener,noreferrer')
} }
@@ -55,14 +58,19 @@ const RequestDetailItem: FC<Props> = ({ item, index, total }) => {
<article className={index > 0 ? 'pt-8 mt-8 border-t border-[#EBEDF5]' : ''}> <article className={index > 0 ? 'pt-8 mt-8 border-t border-[#EBEDF5]' : ''}>
<div className="flex items-start gap-5"> <div className="flex items-start gap-5">
<div className="size-20 rounded-xl overflow-hidden bg-[#F5F7FC] shrink-0"> <div className="size-20 rounded-xl overflow-hidden bg-[#F5F7FC] shrink-0">
<img {productImage ? (
src={productImage || placeholderProductImage} <PresignedImage
alt={item.product?.title ?? 'محصول'} src={productImage}
className="size-full object-cover" alt={item.product?.title ?? 'محصول'}
onError={(e) => { className="size-full object-cover"
e.currentTarget.src = placeholderProductImage />
}} ) : (
/> <img
src={placeholderProductImage}
alt={item.product?.title ?? 'محصول'}
className="size-full object-cover"
/>
)}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -115,19 +123,18 @@ const RequestDetailItem: FC<Props> = ({ item, index, total }) => {
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{fileAttachments.map((att) => {fileAttachments.map((att) =>
isImageUrl(att.url) ? ( isImageUrl(att.url) ? (
<a <button
key={att.url} key={att.url}
href={att.url} type="button"
target="_blank" onClick={() => handleOpenLink(att.url)}
rel="noopener noreferrer"
className="block size-20 rounded-xl overflow-hidden border border-[#EBEDF5] hover:opacity-90 transition-opacity" className="block size-20 rounded-xl overflow-hidden border border-[#EBEDF5] hover:opacity-90 transition-opacity"
> >
<img <PresignedImage
src={att.url} src={att.url}
alt="پیوست" alt="پیوست"
className="size-full object-cover" className="size-full object-cover"
/> />
</a> </button>
) : ( ) : (
<button <button
key={att.url} key={att.url}
@@ -2,6 +2,7 @@ import { type FC } from 'react'
import moment from 'moment-jalaali' import moment from 'moment-jalaali'
import { Paperclip2 } from 'iconsax-react' import { Paperclip2 } from 'iconsax-react'
import { getFileNameAndExtensionFromUrl } from '@/config/func' import { getFileNameAndExtensionFromUrl } from '@/config/func'
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
import VoicePlayer from '@/components/VoicePlayer' import VoicePlayer from '@/components/VoicePlayer'
import type { AttachmentsType } from '@/pages/order/types/Types' import type { AttachmentsType } from '@/pages/order/types/Types'
@@ -25,7 +26,8 @@ const TicketMessage: FC<Props> = ({
const fileAttachments = attachments.filter((a) => a.type !== 'voice') const fileAttachments = attachments.filter((a) => a.type !== 'voice')
const voiceAttachments = attachments.filter((a) => a.type === 'voice') const voiceAttachments = attachments.filter((a) => a.type === 'voice')
const handleOpenLink = (url: string) => { const handleOpenLink = async (key: string) => {
const url = await getPresignedUrl(key)
window.open(url, '_blank', 'noopener,noreferrer') window.open(url, '_blank', 'noopener,noreferrer')
} }
+10 -16
View File
@@ -54,26 +54,20 @@ const TicketSection: FC<Props> = ({ customerName }) => {
} }
if (audioFile) { if (audioFile) {
await singleUpload.mutateAsync(audioFile, { const voiceResult = await singleUpload.mutateAsync(audioFile)
onSuccess: (data) => { params.attachments.push({
params.attachments.push({ type: 'voice',
type: 'voice', url: voiceResult.data.key,
url: data?.data?.url,
})
},
}) })
} }
if (files.length) { if (files.length) {
await multiUpload.mutateAsync(files, { const filesResult = await multiUpload.mutateAsync(files)
onSuccess: (data) => { filesResult.data?.forEach((item) => {
data?.data?.forEach((item) => { params.attachments.push({
params.attachments.push({ type: 'uploads_attach',
type: 'uploads_attach', url: item.key,
url: item.url, })
})
})
},
}) })
} }
@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import { getPresignedUrl } from "../service/UploaderService";
export const usePresignedUrl = (key?: string) => {
const [url, setUrl] = useState<string>();
useEffect(() => {
if (!key) {
setUrl(undefined);
return;
}
let cancelled = false;
getPresignedUrl(key)
.then((resolvedUrl) => {
if (!cancelled) {
setUrl(resolvedUrl);
}
})
.catch(() => {
if (!cancelled) {
setUrl(undefined);
}
});
return () => {
cancelled = true;
};
}, [key]);
return url;
};
@@ -1,6 +1,7 @@
import axios from "@/config/axios"; import axios from "@/config/axios";
import type { import type {
MultiUploadResponseType, MultiUploadResponseType,
PresignedUrlResponseType,
SingleUploadResponseType, SingleUploadResponseType,
} from "../types/Types"; } from "../types/Types";
@@ -25,3 +26,11 @@ export const multiUpload = async (files: File[]) => {
); );
return data; return data;
}; };
export const getPresignedUrl = async (key: string) => {
const { data } = await axios.get<PresignedUrlResponseType>(
"/admin/presigned-url",
{ params: { key } },
);
return data.data.url;
};
+12 -9
View File
@@ -1,13 +1,16 @@
import type { BaseResponse } from "@/shared/types/Types"; import type { BaseResponse } from "@/shared/types/Types";
export type SingleUploadType = { export type UploadResultType = {
url: string; key: string;
file: { bucket: string;
url: string;
key: string;
bucket: string;
};
}; };
export type SingleUploadResponseType = BaseResponse<SingleUploadType>; export type PresignedUrlResultType = {
export type MultiUploadResponseType = BaseResponse<SingleUploadType[]>; key: string;
url: string;
expiresIn: number;
};
export type SingleUploadResponseType = BaseResponse<UploadResultType>;
export type MultiUploadResponseType = BaseResponse<UploadResultType[]>;
export type PresignedUrlResponseType = BaseResponse<PresignedUrlResultType>;