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

This commit is contained in:
2026-07-17 23:19:14 +03:30
parent a5724369da
commit 94a0091d1b
9 changed files with 175 additions and 52 deletions
+42
View File
@@ -0,0 +1,42 @@
import { type FC } from 'react'
import { usePresignedUrl } from '@/pages/uploader/hooks/usePresignedUrl'
import { clx } from '@/helpers/utils'
type Props = {
src?: string | null
firstName?: string | null
lastName?: string | null
className?: string
textClassName?: string
}
const UserAvatar: FC<Props> = ({
src,
firstName,
lastName,
className = 'size-10',
textClassName = 'text-xs',
}) => {
const resolvedSrc = usePresignedUrl(src ?? undefined)
const initials =
`${String(firstName ?? '').charAt(0)}${String(lastName ?? '').charAt(0)}`.trim() ||
'?'
return (
<div
className={clx(
'shrink-0 rounded-full bg-description overflow-hidden flex items-center justify-center text-white font-medium',
className,
textClassName,
)}
>
{resolvedSrc ? (
<img src={resolvedSrc} className="size-full object-cover" alt="" />
) : (
initials
)}
</div>
)
}
export default UserAvatar
@@ -1,10 +1,11 @@
import { type FC, useState } from 'react'
import { Link } from 'react-router-dom'
import { Call, DocumentDownload, Edit2, Location, Paperclip2, Profile2User, Receipt21, User } from 'iconsax-react'
import { Call, DocumentDownload, Edit2, Location, Paperclip2, Receipt21 } from 'iconsax-react'
import { clx } from '@/helpers/utils'
import { getFileNameAndExtensionFromUrl } from '@/config/func'
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
import { Paths } from '@/config/Paths'
import UserAvatar from '@/components/UserAvatar'
import {
getAttachmentUrl,
formatJalaliDateTime,
@@ -51,18 +52,20 @@ const PersonCard: FC<{
name: string
subtitle?: string
phone?: string
icon?: 'user' | 'designer'
}> = ({ title, name, subtitle, phone, icon = 'user' }) => (
avatarUrl?: string | null
firstName?: string | null
lastName?: string | null
}> = ({ title, name, subtitle, phone, avatarUrl, firstName, lastName }) => (
<div className="rounded-2xl border border-gray-100 bg-gray-50/80 p-4">
<p className="text-xs text-gray-500 mb-3">{title}</p>
<div className="flex items-center gap-3">
<div className="size-11 shrink-0 rounded-full bg-white border border-gray-200 flex items-center justify-center">
{icon === 'designer' ? (
<Profile2User size={22} className="text-gray-400" />
) : (
<User size={22} className="text-gray-400" />
)}
</div>
<UserAvatar
src={avatarUrl}
firstName={firstName}
lastName={lastName}
className="size-11 border border-gray-200"
textClassName="text-sm"
/>
<div className="min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{name}</p>
{subtitle && <p className="text-xs text-gray-500 mt-0.5">{subtitle}</p>}
@@ -174,6 +177,9 @@ const OrderDetailSidebar: FC<Props> = ({
title="اطلاعات تماس"
name={getUserDisplayName(order.user)}
phone={order.user.phone}
avatarUrl={order.user.avatarUrl}
firstName={order.user.firstName}
lastName={order.user.lastName}
/>
{order.user.addresse && (
<div className="flex items-start gap-2 text-sm text-gray-700 rounded-2xl border border-gray-100 bg-gray-50/80 p-4">
@@ -193,6 +199,9 @@ const OrderDetailSidebar: FC<Props> = ({
name={getCreatorDisplayName(order.creator)}
subtitle="ادمین ثبت‌کننده"
phone={order.creator.phone}
avatarUrl={order.creator.avatarUrl}
firstName={order.creator.firstName}
lastName={order.creator.lastName}
/>
)}
{(order?.designers ?? []).map((orderDesigner) => (
@@ -202,7 +211,9 @@ const OrderDetailSidebar: FC<Props> = ({
name={getCreatorDisplayName(orderDesigner.designer)}
subtitle="مسئول طراحی"
phone={orderDesigner.designer?.phone}
icon="designer"
avatarUrl={orderDesigner.designer?.avatarUrl}
firstName={orderDesigner.designer?.firstName}
lastName={orderDesigner.designer?.lastName}
/>
))}
</section>
+42 -27
View File
@@ -1,6 +1,7 @@
import Button from '@/components/Button'
import RefreshButton from '@/components/RefreshButton'
import UploadBox from '@/components/UploadBox'
import UserAvatar from '@/components/UserAvatar'
import { Microphone, Paperclip2 } from 'iconsax-react'
import { useState, type FC } from 'react'
import { useVoiceRecorder } from '@/hooks/useVoiceRecorder'
@@ -97,41 +98,49 @@ const TicketSection: FC = () => {
data?.data?.map((item) => {
if (item.user)
return (
<div key={item.id} className='bg-[#F5F7FC] rounded-4xl rounded-tr-none mt-6 p-6 max-w-[55%]'>
<div className='text-sm font-light text-black leading-6'>
{item.content}
</div>
<div key={item.id} className='flex gap-3 mt-6'>
<UserAvatar
src={item.user.avatarUrl}
firstName={item.user.firstName}
lastName={item.user.lastName}
className="size-9 mt-1"
/>
<div className='bg-[#F5F7FC] rounded-4xl rounded-tr-none p-6 max-w-[55%]'>
<div className='text-sm font-light text-black leading-6'>
{item.content}
</div>
{/* attachments (non-voice) */}
<div className="flex gap-3 flex-wrap mt-3">
{item.attachments
?.filter((a) => a.type !== 'voice')
.map((attach) => (
<div
key={attach.url}
onClick={() => handleOpenLink(attach.url)}
className="flex cursor-pointer items-center gap-1.5 text-[#0047FF]"
>
<Paperclip2 size={20} color="#0047FF" />
<div className="text-xs">
{getFileNameAndExtensionFromUrl(attach.url).fileName}
{/* attachments (non-voice) */}
<div className="flex gap-3 flex-wrap mt-3">
{item.attachments
?.filter((a) => a.type !== 'voice')
.map((attach) => (
<div
key={attach.url}
onClick={() => handleOpenLink(attach.url)}
className="flex cursor-pointer items-center gap-1.5 text-[#0047FF]"
>
<Paperclip2 size={20} color="#0047FF" />
<div className="text-xs">
{getFileNameAndExtensionFromUrl(attach.url).fileName}
</div>
</div>
</div>
))}
</div>
{/* voice messages */}
{item.attachments
?.filter((a) => a.type === 'voice')
.map((voice) => (
<VoicePlayer key={voice.url} url={voice.url} />
))}
</div>
{/* voice messages */}
{item.attachments
?.filter((a) => a.type === 'voice')
.map((voice) => (
<VoicePlayer key={voice.url} url={voice.url} />
))}
</div>
)
else if (item.admin)
return (
<div className='flex justify-end'>
<div className='bg-[#F5F7FC] rounded-4xl rounded-tl-none mt-6 p-6 max-w-[55%]'>
<div key={item.id} className='flex justify-end gap-3 mt-6'>
<div className='bg-[#F5F7FC] rounded-4xl rounded-tl-none p-6 max-w-[55%]'>
<div className='flex gap-1 text-sm mb-2'>
<div className='font-bold'>طراح : </div>
<div>{item.admin?.firstName + ' ' + item?.admin?.lastName}</div>
@@ -165,6 +174,12 @@ const TicketSection: FC = () => {
<VoicePlayer key={voice.url} url={voice.url} />
))}
</div>
<UserAvatar
src={item.admin.avatarUrl}
firstName={item.admin.firstName}
lastName={item.admin.lastName}
className="size-9 mt-1"
/>
</div>
)
})
+10 -1
View File
@@ -100,6 +100,7 @@ export type OrderListResponseType = BaseResponse<OrderListItemType[]>;
export type UserType = {
addresse?: string | null;
avatarUrl?: string | null;
createdAt?: string;
deletedAt?: string | null;
firstName?: string | null;
@@ -130,6 +131,7 @@ export type OrderDetailCreatorType = {
lastName: string;
role: string;
phone: string;
avatarUrl?: string | null;
};
export type OrderDesignerType = {
@@ -174,12 +176,19 @@ export type TicketType = {
lastName: string;
phone: string;
id: string;
avatarUrl?: string | null;
};
attachments: AttachmentsType[];
content: string;
createdAt: string;
id: number;
user?: string;
user?: {
id: string;
firstName?: string | null;
lastName?: string | null;
phone?: string;
avatarUrl?: string | null;
} | null;
};
export interface MyOrderType extends RowDataType {
+9 -2
View File
@@ -1,7 +1,7 @@
import { type FC } from 'react'
import { Link, useParams } from 'react-router-dom'
import moment from 'moment-jalaali'
import { Calendar, Profile2User, TickSquare } from 'iconsax-react'
import { Calendar, TickSquare } from 'iconsax-react'
import { useGetRequestDetail } from './hooks/useRequestData'
import RequestDetailItem from './components/RequestDetailItem'
import TicketSection from './components/TicketSection'
@@ -9,6 +9,7 @@ import { Paths } from '@/config/Paths'
import BackButton from '@/components/BackButton'
import Button from '@/components/Button'
import RefreshButton from '@/components/RefreshButton'
import UserAvatar from '@/components/UserAvatar'
const RequestDetail: FC = () => {
const { id } = useParams()
@@ -79,7 +80,13 @@ const RequestDetail: FC = () => {
<>
<span className="text-[#D8DCE8]">|</span>
<div className="flex items-center gap-1.5">
<Profile2User size={14} />
<UserAvatar
src={request.user?.avatarUrl}
firstName={request.user?.firstName}
lastName={request.user?.lastName}
className="size-5"
textClassName="text-[9px]"
/>
<span>{customerName}</span>
</div>
</>
@@ -4,6 +4,7 @@ import { Paperclip2 } from 'iconsax-react'
import { getFileNameAndExtensionFromUrl } from '@/config/func'
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
import VoicePlayer from '@/components/VoicePlayer'
import UserAvatar from '@/components/UserAvatar'
import type { AttachmentsType } from '@/pages/order/types/Types'
type Props = {
@@ -13,6 +14,9 @@ type Props = {
senderLabel?: string
createdAt?: string
isAdmin?: boolean
avatarUrl?: string | null
firstName?: string | null
lastName?: string | null
}
const TicketMessage: FC<Props> = ({
@@ -22,6 +26,9 @@ const TicketMessage: FC<Props> = ({
senderLabel = 'پشتیبان',
createdAt,
isAdmin = false,
avatarUrl,
firstName,
lastName,
}) => {
const fileAttachments = attachments.filter((a) => a.type !== 'voice')
const voiceAttachments = attachments.filter((a) => a.type === 'voice')
@@ -31,8 +38,18 @@ const TicketMessage: FC<Props> = ({
window.open(url, '_blank', 'noopener,noreferrer')
}
const avatar = (
<UserAvatar
src={avatarUrl}
firstName={firstName}
lastName={lastName}
className="size-9 mt-1"
/>
)
return (
<div className={isAdmin ? 'flex justify-end' : ''}>
<div className={`flex gap-3 ${isAdmin ? 'justify-end' : ''}`}>
{!isAdmin && avatar}
<div
className={`bg-[#F5F7FC] rounded-3xl p-5 max-w-[min(100%,520px)] ${
isAdmin ? 'rounded-tl-none' : 'rounded-tr-none'
@@ -79,6 +96,7 @@ const TicketMessage: FC<Props> = ({
</div>
)}
</div>
{isAdmin && avatar}
</div>
)
}
@@ -124,7 +124,16 @@ const TicketSection: FC<Props> = ({ customerName }) => {
attachments={item.attachments}
createdAt={item.createdAt}
senderLabel="مشتری"
senderName={customerName}
senderName={
customerName ||
[item.user.firstName, item.user.lastName]
.filter(Boolean)
.join(' ') ||
item.user.phone
}
avatarUrl={item.user.avatarUrl}
firstName={item.user.firstName}
lastName={item.user.lastName}
/>
)
}
@@ -139,6 +148,9 @@ const TicketSection: FC<Props> = ({ customerName }) => {
isAdmin
senderLabel="پشتیبان"
senderName={`${item.admin.firstName} ${item.admin.lastName}`}
avatarUrl={item.admin.avatarUrl}
firstName={item.admin.firstName}
lastName={item.admin.lastName}
/>
)
}
+1
View File
@@ -47,6 +47,7 @@ export type RequestDetailUserType = {
maxCredit: number;
addresse: string | null;
phone: string;
avatarUrl?: string | null;
};
export type RequestDetailProductType = {
+15 -7
View File
@@ -1,5 +1,6 @@
import { type FC, useEffect, useState } from 'react'
import Input from '@/components/Input'
import UserAvatar from '@/components/UserAvatar'
import { ArrowDown2, CloseCircle, HambergerMenu, Logout } from 'iconsax-react'
import { Link } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
@@ -21,7 +22,6 @@ const Header: FC = () => {
const displayName = [admin?.firstName, admin?.lastName].filter(Boolean).join(' ').trim()
const roleTitle = admin?.role?.title || admin?.role?.name || ''
const initials = `${String(admin?.firstName ?? '').charAt(0)}${String(admin?.lastName ?? '').charAt(0)}`
useEffect(() => {
setPopoverKey((prevKey) => prevKey + 1);
@@ -53,9 +53,13 @@ const Header: FC = () => {
<Popover className="relative" key={popoverKey}>
<PopoverButton>
<div className='flex gap-2 items-center mt-2.5'>
<div className='size-6 rounded-full bg-description overflow-hidden flex items-center justify-center text-white text-xs font-medium'>
{initials}
</div>
<UserAvatar
src={admin.avatarUrl}
firstName={admin.firstName}
lastName={admin.lastName}
className='size-6'
textClassName='text-xs'
/>
<div className='xl:flex hidden gap-1 items-center'>
<div className='flex flex-col items-start'>
<div className='text-xs'>
@@ -77,9 +81,13 @@ const Header: FC = () => {
<CloseCircle onClick={() => setPopoverKey((prevKey) => prevKey + 1)} size={20} color='black' />
</div>
<Link to={`${Paths.admin.update}${admin.id}`} className='flex flex-col gap-2 items-center justify-center border-b border-[#EAEDF5] pb-4'>
<div className='size-14 rounded-full bg-description flex items-center justify-center text-white text-lg font-medium'>
{initials}
</div>
<UserAvatar
src={admin.avatarUrl}
firstName={admin.firstName}
lastName={admin.lastName}
className='size-14'
textClassName='text-lg'
/>
<div>
{displayName || String(admin.phone ?? '')}
</div>