This commit is contained in:
@@ -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;
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: () => {
|
||||||
|
|||||||
@@ -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} />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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: 'عنوان',
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
Reference in New Issue
Block a user