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;
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Pause, Play } from "iconsax-react"
|
||||
import { useEffect, useRef, useState, type FC } from "react"
|
||||
import { usePresignedUrl } from "@/pages/uploader/hooks/usePresignedUrl"
|
||||
|
||||
type VoicePlayerProps = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const VoicePlayer: FC<VoicePlayerProps> = ({ url }) => {
|
||||
const resolvedUrl = usePresignedUrl(url)
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
if (!audio) return
|
||||
if (!audio || !resolvedUrl) return
|
||||
|
||||
const update = () => {
|
||||
if (!audio.duration) return
|
||||
@@ -28,7 +30,7 @@ const VoicePlayer: FC<VoicePlayerProps> = ({ url }) => {
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', update)
|
||||
}
|
||||
}, [])
|
||||
}, [resolvedUrl])
|
||||
|
||||
const toggle = () => {
|
||||
if (!audioRef.current) return
|
||||
@@ -66,9 +68,9 @@ const VoicePlayer: FC<VoicePlayerProps> = ({ url }) => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<audio ref={audioRef} src={url} className="hidden" />
|
||||
<audio ref={audioRef} src={resolvedUrl} className="hidden" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VoicePlayer
|
||||
export default VoicePlayer
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { type FC } from 'react'
|
||||
import { useState, type FC } from 'react'
|
||||
import Input from '@/components/Input'
|
||||
import UploadBox from '@/components/UploadBox'
|
||||
import PresignedImage from '@/components/PresignedImage'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import Button from '@/components/Button'
|
||||
import Error from '@/components/Error'
|
||||
import { useGetMe, useUpdateProfile } from '@/pages/user/hooks/useUserData'
|
||||
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
||||
import { toast } from '@/shared/toast'
|
||||
import { extractErrorMessage } from '@/config/func'
|
||||
import { Paths } from '@/config/Paths'
|
||||
@@ -14,12 +17,34 @@ import type { UpdateProfileType } from '@/pages/user/types/Types'
|
||||
const CompleteProfileForm: FC = () => {
|
||||
const { data: user } = useGetMe()
|
||||
const updateProfile = useUpdateProfile()
|
||||
const { mutate: upload, isPending: isUploading } = useSingleUpload()
|
||||
const [file, setFile] = useState<File>()
|
||||
|
||||
const handleSave = (values: UpdateProfileType) => {
|
||||
updateProfile.mutate(
|
||||
{
|
||||
firstName: values.firstName.trim(),
|
||||
lastName: values.lastName.trim(),
|
||||
...(values.avatarUrl ? { avatarUrl: values.avatarUrl } : {}),
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
toast('اطلاعات شما با موفقیت ثبت شد', 'success')
|
||||
appNavigate(Paths.home, { replace: true })
|
||||
},
|
||||
onError(error) {
|
||||
toast(extractErrorMessage(error), 'error')
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const formik = useFormik<UpdateProfileType>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
firstName: user?.firstName ?? '',
|
||||
lastName: user?.lastName ?? '',
|
||||
avatarUrl: user?.avatarUrl ?? undefined,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
firstName: Yup.string()
|
||||
@@ -30,21 +55,21 @@ const CompleteProfileForm: FC = () => {
|
||||
.required('نام خانوادگی اجباری است.'),
|
||||
}),
|
||||
onSubmit(values) {
|
||||
updateProfile.mutate(
|
||||
{
|
||||
firstName: values.firstName.trim(),
|
||||
lastName: values.lastName.trim(),
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
toast('اطلاعات شما با موفقیت ثبت شد', 'success')
|
||||
appNavigate(Paths.home, { replace: true })
|
||||
if (file) {
|
||||
upload(file, {
|
||||
onSuccess(data) {
|
||||
handleSave({
|
||||
...values,
|
||||
avatarUrl: data?.data?.key,
|
||||
})
|
||||
},
|
||||
onError(error) {
|
||||
toast(extractErrorMessage(error), 'error')
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
} else {
|
||||
handleSave(values)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -60,6 +85,19 @@ const CompleteProfileForm: FC = () => {
|
||||
</div>
|
||||
|
||||
<div className='mt-16 flex flex-col gap-6'>
|
||||
{formik.values.avatarUrl && !file && (
|
||||
<PresignedImage
|
||||
src={formik.values.avatarUrl}
|
||||
className='w-20 h-20 rounded-full object-cover'
|
||||
alt='آواتار'
|
||||
/>
|
||||
)}
|
||||
|
||||
<UploadBox
|
||||
label='آواتار (اختیاری)'
|
||||
onChange={(files) => setFile(files[0])}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
label='نام'
|
||||
@@ -94,7 +132,7 @@ const CompleteProfileForm: FC = () => {
|
||||
label='ثبت و ادامه'
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={updateProfile.isPending}
|
||||
isLoading={updateProfile.isPending || isUploading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ const AddCriticisms: FC = () => {
|
||||
if (files.length > 0) {
|
||||
await multiUpload.mutateAsync(files, {
|
||||
onSuccess: async (data) => {
|
||||
values.files = await data.data.map((item: { url: string }) => item?.url)
|
||||
values.files = data.data.map((item) => item.key)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { type FC } from 'react'
|
||||
import { type ProductType } from '@/pages/request/type/Types'
|
||||
import PresignedImage from '@/components/PresignedImage'
|
||||
|
||||
const ServiceItem: FC<{ product: ProductType }> = ({ product }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='size-20 rounded-full bg-gray-200 border-[3px] border-primary'>
|
||||
<img src={product.images?.[0]} alt={product.title} className='w-full h-full object-cover' />
|
||||
<div className='size-20 rounded-full bg-gray-200 border-[3px] border-primary overflow-hidden'>
|
||||
{product.images?.[0] ? (
|
||||
<PresignedImage
|
||||
src={product.images[0]}
|
||||
alt={product.title}
|
||||
className='w-full h-full object-cover'
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceItem
|
||||
export default ServiceItem
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Paths } from '@/config/Paths'
|
||||
import { useGetPaymentInvoice } from '../payment/hooks/usePaymentData'
|
||||
import InvoicePaymentList from './InvoicePaymentList'
|
||||
import { formatItemDiscountDisplay } from './utils/invoiceItem'
|
||||
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
|
||||
|
||||
interface TableInvoiceItem extends RowDataType {
|
||||
id: string
|
||||
@@ -237,13 +238,20 @@ const InvoiceDetail: FC = () => {
|
||||
<span className='text-[#8C90A3]'>پیوستها:</span>
|
||||
<ul className='text-black leading-6 list-disc list-inside space-y-1'>
|
||||
{invoice.attachments.map((att: unknown, index: number) => {
|
||||
const url = typeof att === 'string' ? att : (att as { url?: string })?.url
|
||||
const name = typeof att === 'object' && att && 'name' in (att as object) ? (att as { name?: string }).name : url || `پیوست ${index + 1}`
|
||||
return url ? (
|
||||
const key = typeof att === 'string' ? att : (att as { url?: string })?.url
|
||||
const name = typeof att === 'object' && att && 'name' in (att as object) ? (att as { name?: string }).name : key || `پیوست ${index + 1}`
|
||||
return key ? (
|
||||
<li key={index}>
|
||||
<a href={url} target='_blank' rel='noopener noreferrer' className='text-[#0047FF] hover:underline'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={async () => {
|
||||
const url = await getPresignedUrl(key)
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}}
|
||||
className='text-[#0047FF] hover:underline'
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
) : null
|
||||
})}
|
||||
|
||||
@@ -6,11 +6,14 @@ import PageLoading from '../../components/PageLoading'
|
||||
import { Calendar2, Clock, Element3 } from 'iconsax-react'
|
||||
import moment from 'moment-jalaali'
|
||||
import { type LearningItemType } from './types/LearningTypes'
|
||||
import { usePresignedUrl } from '@/pages/uploader/hooks/usePresignedUrl'
|
||||
|
||||
const LearningDetail: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const getLearningDetail = useGetLearningDetail(id ? id : '')
|
||||
const learning = (getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning
|
||||
const videoUrl = usePresignedUrl(learning?.videoUrl)
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
@@ -23,13 +26,13 @@ const LearningDetail: FC = () => {
|
||||
|
||||
|
||||
<div className='bg-white p-8 rounded-3xl'>
|
||||
<video className='w-full rounded-3xl' controls>
|
||||
<source src={(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.videoUrl} />
|
||||
<video className='w-full rounded-3xl' controls key={videoUrl}>
|
||||
{videoUrl ? <source src={videoUrl} /> : null}
|
||||
</video>
|
||||
|
||||
<div className='mt-7 flex xl:flex-row flex-col gap-4 justify-between'>
|
||||
<div className='xl:text-lg'>
|
||||
{(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.title}
|
||||
{learning?.title}
|
||||
</div>
|
||||
|
||||
<div className='flex xl:flex-row flex-col xl:max-h-5 gap-3 text-description text-xs'>
|
||||
@@ -40,7 +43,7 @@ const LearningDetail: FC = () => {
|
||||
color={'#888888'}
|
||||
/>
|
||||
<div>
|
||||
{(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.videoDuration + ' دقیقه'}
|
||||
{(learning?.videoDuration ?? '') + ' دقیقه'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +53,7 @@ const LearningDetail: FC = () => {
|
||||
color={'#888888'}
|
||||
/>
|
||||
<div>
|
||||
{(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.category?.name}
|
||||
{learning?.category?.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +63,7 @@ const LearningDetail: FC = () => {
|
||||
color={'#888888'}
|
||||
/>
|
||||
<div>
|
||||
{moment((getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.createdAt).format('jYYYY-jMM-jDD')}
|
||||
{moment(learning?.createdAt).format('jYYYY-jMM-jDD')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +72,7 @@ const LearningDetail: FC = () => {
|
||||
|
||||
</div>
|
||||
<div className='mt-6 text-description'>
|
||||
{(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.description}
|
||||
{learning?.description}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import PageLoading from '../../components/PageLoading'
|
||||
import { type LearningItemType } from './types/LearningTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
import { Link } from 'react-router-dom'
|
||||
import PresignedImage from '@/components/PresignedImage'
|
||||
// import { Pages } from '../../config/Pages'
|
||||
|
||||
const LearningList: FC = () => {
|
||||
@@ -55,7 +56,7 @@ const LearningList: FC = () => {
|
||||
(getLearnings.data as { data: { learnings: LearningItemType[] } })?.data?.learnings?.map((item: LearningItemType) => {
|
||||
return (
|
||||
<Link to={`/learning/${item.id}`} className='bg-white p-4 rounded-3xl xl:min-w-[30%] min-w-full flex-1'>
|
||||
<img src={item.coverUrl} className='w-full rounded-[20px]' />
|
||||
<PresignedImage src={item.coverUrl} className='w-full rounded-[20px]' alt={item.title} />
|
||||
<div className='mt-4'>
|
||||
{item.title}
|
||||
</div>
|
||||
|
||||
@@ -6,14 +6,17 @@ import { useGetOrderDetails } from './hooks/useOrderData'
|
||||
import TicketSection from './components/TicketSection'
|
||||
import { getFileNameAndExtensionFromUrl } from '@/config/func'
|
||||
import { orderStatusLabels } from './constants/statusLabels'
|
||||
import PresignedImage from '@/components/PresignedImage'
|
||||
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
|
||||
|
||||
const OrderDetail: FC = () => {
|
||||
const { id } = useParams()
|
||||
const { data } = useGetOrderDetails(id!)
|
||||
const order = data?.data
|
||||
|
||||
const handleOpenLink = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
const handleOpenLink = async (key: string) => {
|
||||
const url = await getPresignedUrl(key)
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -36,11 +39,13 @@ const OrderDetail: FC = () => {
|
||||
<div className="flex items-center gap-6">
|
||||
{order?.product && (
|
||||
<div className="size-[86px] rounded-lg overflow-hidden shrink-0">
|
||||
<img
|
||||
src={order.product.images?.[0]}
|
||||
className="size-full object-cover"
|
||||
alt=""
|
||||
/>
|
||||
{order.product.images?.[0] ? (
|
||||
<PresignedImage
|
||||
src={order.product.images[0]}
|
||||
className="size-full object-cover"
|
||||
alt=""
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 flex gap-x-16 gap-y-4 pr-4 flex-wrap">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useParams } from 'react-router-dom'
|
||||
import { toast } from '@/shared/toast'
|
||||
import { extractErrorMessage, getFileNameAndExtensionFromUrl } from '@/config/func'
|
||||
import VoicePlayer from '@/components/VoicePlayer'
|
||||
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
|
||||
|
||||
const TicketSection: FC = () => {
|
||||
const { id } = useParams()
|
||||
@@ -44,7 +45,7 @@ const TicketSection: FC = () => {
|
||||
onSuccess: (data) => {
|
||||
params.attachments.push({
|
||||
type: 'voice',
|
||||
url: data?.data?.url,
|
||||
url: data?.data?.key,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -55,7 +56,7 @@ const TicketSection: FC = () => {
|
||||
data?.data?.map((item) => {
|
||||
params.attachments.push({
|
||||
type: 'uploads_attach',
|
||||
url: item.url,
|
||||
url: item.key,
|
||||
})
|
||||
})
|
||||
},
|
||||
@@ -79,8 +80,9 @@ const TicketSection: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const handleOpenLink = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
const handleOpenLink = async (key: string) => {
|
||||
const url = await getPresignedUrl(key)
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -33,6 +33,23 @@ export type OrderUserType = {
|
||||
phone: string
|
||||
}
|
||||
|
||||
export type OrderDesignerAdminType = {
|
||||
id: string
|
||||
createdAt?: string
|
||||
deletedAt?: string | null
|
||||
firstName?: string | null
|
||||
lastName?: string | null
|
||||
phone?: string | null
|
||||
role?: string
|
||||
}
|
||||
|
||||
export type OrderDesignerType = {
|
||||
id: string
|
||||
createdAt: string
|
||||
deletedAt: string | null
|
||||
designer: OrderDesignerAdminType
|
||||
}
|
||||
|
||||
export interface OrderType extends RowDataType {
|
||||
id: string
|
||||
createdAt: string
|
||||
@@ -43,7 +60,7 @@ export interface OrderType extends RowDataType {
|
||||
title: string
|
||||
type: string
|
||||
user: OrderUserType
|
||||
designer: unknown
|
||||
designers: OrderDesignerType[]
|
||||
creator: string
|
||||
orderNumber: number
|
||||
status: string
|
||||
|
||||
@@ -90,7 +90,7 @@ const PayInvoice: FC = () => {
|
||||
try {
|
||||
const uploadResult = await multiUpload.mutateAsync(attachmentFiles)
|
||||
attachments = (uploadResult?.data ?? []).map(
|
||||
(item: { url?: string }) => item?.url ?? ''
|
||||
(item) => item.key
|
||||
).filter(Boolean)
|
||||
} catch (error) {
|
||||
toast((error as ErrorType)?.response?.data?.error?.message?.[0] ?? 'خطا در آپلود فایلها', 'error')
|
||||
|
||||
@@ -53,7 +53,7 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
||||
data?.data?.forEach((item) => {
|
||||
attachments.push({
|
||||
type: 'uploads_attach',
|
||||
url: item.url,
|
||||
url: item.key,
|
||||
})
|
||||
})
|
||||
},
|
||||
@@ -64,7 +64,7 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
||||
onSuccess: (data) => {
|
||||
attachments.push({
|
||||
type: 'voice',
|
||||
url: data?.data?.url,
|
||||
url: data?.data?.key,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Paperclip2 } from 'iconsax-react'
|
||||
import placeholderProductImage from '@/assets/images/placeholder-product.svg'
|
||||
import { getFileNameAndExtensionFromUrl } from '@/config/func'
|
||||
import VoicePlayer from '@/components/VoicePlayer'
|
||||
import PresignedImage from '@/components/PresignedImage'
|
||||
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
|
||||
import type { RequestDetailItemType } from '../type/Types'
|
||||
|
||||
type Props = {
|
||||
@@ -18,7 +20,8 @@ const RequestDetailItem: FC<Props> = ({ item, index, total }) => {
|
||||
const voiceAttachments = 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')
|
||||
}
|
||||
|
||||
@@ -26,14 +29,19 @@ const RequestDetailItem: FC<Props> = ({ item, index, total }) => {
|
||||
<article className={index > 0 ? 'pt-8 mt-8 border-t border-[#EBEDF5]' : ''}>
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="size-20 rounded-xl overflow-hidden bg-[#F5F7FC] shrink-0">
|
||||
<img
|
||||
src={productImage || placeholderProductImage}
|
||||
alt={item.product?.title ?? 'محصول'}
|
||||
className="size-full object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = placeholderProductImage
|
||||
}}
|
||||
/>
|
||||
{productImage ? (
|
||||
<PresignedImage
|
||||
src={productImage}
|
||||
alt={item.product?.title ?? 'محصول'}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={placeholderProductImage}
|
||||
alt={item.product?.title ?? 'محصول'}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -86,19 +94,18 @@ const RequestDetailItem: FC<Props> = ({ item, index, total }) => {
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{fileAttachments.map((att) =>
|
||||
isImageUrl(att.url) ? (
|
||||
<a
|
||||
<button
|
||||
key={att.url}
|
||||
href={att.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
type="button"
|
||||
onClick={() => handleOpenLink(att.url)}
|
||||
className="block size-20 rounded-xl overflow-hidden border border-[#EBEDF5] hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<img
|
||||
<PresignedImage
|
||||
src={att.url}
|
||||
alt="پیوست"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</a>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
key={att.url}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type FC } from 'react'
|
||||
import moment from 'moment-jalaali'
|
||||
import { Paperclip2 } from 'iconsax-react'
|
||||
import { getFileNameAndExtensionFromUrl } from '@/config/func'
|
||||
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
|
||||
import VoicePlayer from '@/components/VoicePlayer'
|
||||
import type { AttachmentsType } from '../type/Types'
|
||||
|
||||
@@ -25,7 +26,8 @@ const TicketMessage: FC<Props> = ({
|
||||
const fileAttachments = 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')
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ const TicketSection: FC = () => {
|
||||
onSuccess: (data) => {
|
||||
params.attachments.push({
|
||||
type: 'voice',
|
||||
url: data?.data?.url,
|
||||
url: data?.data?.key,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -65,7 +65,7 @@ const TicketSection: FC = () => {
|
||||
data?.data?.forEach((item) => {
|
||||
params.attachments.push({
|
||||
type: 'uploads_attach',
|
||||
url: item.url,
|
||||
url: item.key,
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -4,11 +4,18 @@ import { clx } from "@/helpers/utils";
|
||||
import { t } from "@/locale";
|
||||
import type { TicketThreadItemType } from "./types/TicketTypes";
|
||||
import { formatDate } from "./detailUtils";
|
||||
import { getPresignedUrl } from "@/pages/uploader/service/UploaderService";
|
||||
|
||||
export type MessageBubbleProps = { item: TicketThreadItemType };
|
||||
|
||||
const MessageBubble: FC<MessageBubbleProps> = ({ item }) => {
|
||||
const isUser = item.isFromUser;
|
||||
|
||||
const handleOpenLink = async (key: string) => {
|
||||
const url = await getPresignedUrl(key);
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clx(
|
||||
@@ -30,16 +37,15 @@ const MessageBubble: FC<MessageBubbleProps> = ({ item }) => {
|
||||
{item.attachments && item.attachments.length > 0 && (
|
||||
<div className="flex gap-2 flex-wrap text-sm mt-2">
|
||||
{item.attachments.map((att, index) => (
|
||||
<a
|
||||
<button
|
||||
key={att.id}
|
||||
href={att.attachmentUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
type="button"
|
||||
onClick={() => handleOpenLink(att.attachmentUrl)}
|
||||
className="text-[#0047FF] flex gap-1 items-center"
|
||||
>
|
||||
<Paperclip2 size={20} color="#0047FF" />
|
||||
{t("attach")} {index + 1}
|
||||
</a>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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 type {
|
||||
MultiUploadResponseType,
|
||||
PresignedUrlResponseType,
|
||||
SingleUploadResponseType,
|
||||
} from "../types/Types";
|
||||
|
||||
@@ -25,3 +26,11 @@ export const multiUpload = async (files: File[]) => {
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getPresignedUrl = async (key: string) => {
|
||||
const { data } = await axios.get<PresignedUrlResponseType>(
|
||||
"/public/presigned-url",
|
||||
{ params: { key } },
|
||||
);
|
||||
return data.data.url;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { BaseResponse } from "@/shared/types/Types";
|
||||
|
||||
export type SingleUploadType = {
|
||||
url: string;
|
||||
file: {
|
||||
url: string;
|
||||
key: string;
|
||||
bucket: string;
|
||||
};
|
||||
export type UploadResultType = {
|
||||
key: string;
|
||||
bucket: string;
|
||||
};
|
||||
|
||||
export type SingleUploadResponseType = BaseResponse<SingleUploadType>;
|
||||
export type MultiUploadResponseType = BaseResponse<SingleUploadType[]>;
|
||||
export type PresignedUrlResultType = {
|
||||
key: string;
|
||||
url: string;
|
||||
expiresIn: number;
|
||||
};
|
||||
|
||||
export type SingleUploadResponseType = BaseResponse<UploadResultType>;
|
||||
export type MultiUploadResponseType = BaseResponse<UploadResultType[]>;
|
||||
export type PresignedUrlResponseType = BaseResponse<PresignedUrlResultType>;
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface UserMeType {
|
||||
maxCredit: number;
|
||||
addresse: string | null;
|
||||
phone: string;
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
export type GetMeResponseType = BaseResponse<UserMeType>;
|
||||
@@ -18,4 +19,5 @@ export type GetMeResponseType = BaseResponse<UserMeType>;
|
||||
export type UpdateProfileType = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
avatarUrl?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user