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

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